LoginSignup
0
0

More than 1 year has passed since last update.

GoでRead-Onceオブジェクトを作ってみた

Posted at

TL; DR

  • 「セキュア・バイ・デザイン」のRead-OnceオブジェクトをGoで実装
  • atomic.Pointer#Swap で取得時に nil と差し替えて一度しか読めないようにする
  • fmt.Stringer, fmt.GoStringer を実装してログ表示を防止

はじめに

セキュア・バイ・デザイン」では、ドメインモデル中の機密性の高い概念を実装するためのデザインパターンとして「Read-Onceオブジェクト」を提唱しています。

Read-Onceオブジェクトは以下のような特徴を持ち、機密情報を(比較的)安全に扱うことができます。

  • 値を一度しか読み込めない
    • 機密情報を必要な箇所以外で参照されない
  • シリアライズできない
    • うっかりログに載らずに済む
  • 継承できない
    • 安全でない拡張を防ぐ

実装はJavaで書かれていたので、本記事ではそのテクニックをGoで再現してみました。

中身を一度しか読み込めなくする

オリジナル

「セキュア・バイ・デザイン」では、AtomicReference<String> フィールドをクラスに持たせ、取得時に null と差し替えることで2回目以降は取得できないようにしています。

SensitiveValue.java(書籍より一部引用)
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.Marshalxml.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で同じような機能を実現できる標準ライブラリがあったため、予想よりも簡単に実装できました。

一方、実際に使ってみると思わぬ落とし穴が見つかるかもしれないので、実戦投入しながら改良していきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0