TL; DR
- 「セキュア・バイ・デザイン」のRead-OnceオブジェクトをGoで実装
-
atomic.Pointer#Swap
で取得時にnil
と差し替えて一度しか読めないようにする -
fmt.Stringer
,fmt.GoStringer
を実装してログ表示を防止
はじめに
「セキュア・バイ・デザイン」では、ドメインモデル中の機密性の高い概念を実装するためのデザインパターンとして「Read-Onceオブジェクト」を提唱しています。
Read-Onceオブジェクトは以下のような特徴を持ち、機密情報を(比較的)安全に扱うことができます。
-
値を一度しか読み込めない
- 機密情報を必要な箇所以外で参照されない
-
シリアライズできない
- うっかりログに載らずに済む
-
継承できない
- 安全でない拡張を防ぐ
実装はJavaで書かれていたので、本記事ではそのテクニックをGoで再現してみました。
中身を一度しか読み込めなくする
オリジナル
「セキュア・バイ・デザイン」では、AtomicReference<String>
フィールドをクラスに持たせ、取得時に null
と差し替えることで2回目以降は取得できないようにしています。
public final class SensitiveValue implements Externalizable {
private transient final AtomicReference<String> value;
public SensitiveValue(final String value) {
this.value = new AtomicReference<>(value);
}
// value.getAndSetで中身を取り出しnullと入れ替え。2回目以降はnullが取り出され、notNull違反で例外発生
public String value() {
return notNull(value.getAndSet(null), "Sensitive value has already been consumed");
}
ただの String
を使わないのは、マルチスレッドで呼び出されても競合しないためです。
Go移植
Goでも、sync/atomic
パッケージを使うことで同様のことが可能です。ちょうどv1.19で atomic.Pointer
が導入され、任意の型を型安全に扱えるようになったので使ってみます。
type Password struct {
// パスワード文字列
value *atomic.Pointer[string]
}
func NewPassword(s string) *Password {
v := &atomic.Pointer[string]{}
v.Store(&s)
return &Password{value: v}
}
func (p *Password) Value() (string, error) {
// 古い値を取り出し、新しい値と差し替え
s := p.value.Swap(nil)
// 注:ただのnilと比較するとうまくいかない(後述)
if s == (*string)(nil) {
return "", errors.New("sensitive value has already been consumed")
}
return *s, nil
}
Javaの value.getAndSet(null)
相当のことを p.value.Swap(nil)
で行っています。Goなので、nilだった場合は例外を起こす代わりにエラーを返しています。
1点注意点として、Swapの戻り値 s
のnilチェックで単に s == nil
を使うと、nilが返ってきた場合でも等しいと判定されません。これは、Goのnilが型を区別するため(typed nil)で、上記のように *string
に型変換してから比較する必要があります。
詳細については、こちらの記事が分かりやすいです。
ログに中身が表示されないようにする
オリジナル
「セキュア・バイ・デザイン」では、Externalizable
インターフェースを実装し、ダミーの出力メソッドでオーバーライドすることで内部情報の表示を防いでいます。
Go移植
すべてを対策するのは難しいですが、文字列出力のメジャーどころ
json.Marshal
xml.Marshal
fmt.Printf
の対策を考えます。
json.Marshal
と xml.Marshal
この2つについては、そもそも公開された(exported)フィールドしか対象にしないため、非公開フィールドにしてしまえば出力されません。
type Password struct {
// unexported
value *atomic.Pointer[string]
}
p := NewPassword(passwordStr)
jsonB, err := json.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, string(jsonB), "{}")
xmlB, err := xml.Marshal(p)
assert.NoError(t, err)
assert.Equal(t, string(xmlB), "<Password></Password>")
fmt.Printf
fmt.Stringer
を実装することで %v
, %+v
の表示を、fmt.GoStringer
を実装することで %#v
の表示を制御できます。
この2つを実装し、ダミー文字列を返すようにします。
// fmt.Stringerを実装
func (p *Password) String() string {
return "{credential}"
}
// fmt.GoStringerを実装
func (p *Password) GoString() string {
return "{credential}"
}
おわりに
以上、GoでRead-Onceオブジェクトを作ってみた紹介でした。JavaとGoで同じような機能を実現できる標準ライブラリがあったため、予想よりも簡単に実装できました。
一方、実際に使ってみると思わぬ落とし穴が見つかるかもしれないので、実戦投入しながら改良していきたいと思います。