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

# TL; DR

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

https://github.com/Syuparn/go-read-once

# はじめに

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

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

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

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

# 中身を一度しか読み込めなくする
## オリジナル

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

https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/concurrent/atomic/AtomicReference.html

```java: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` が導入され、任意の型を型安全に扱えるようになったので使ってみます。

https://pkg.go.dev/sync/atomic#Pointer

```go
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` に型変換してから比較する必要があります。

詳細については、こちらの記事が分かりやすいです。

https://qiita.com/umisama/items/e215d49138e949d7f805

# ログに中身が表示されないようにする
## オリジナル

「セキュア・バイ・デザイン」では、`Externalizable` インターフェースを実装し、ダミーの出力メソッドでオーバーライドすることで内部情報の表示を防いでいます。

https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/io/Externalizable.html

## Go移植

すべてを対策するのは難しいですが、文字列出力のメジャーどころ

- `json.Marshal`
- `xml.Marshal`
- `fmt.Printf`

の対策を考えます。

### `json.Marshal` と `xml.Marshal`

この2つについては、そもそも公開された(exported)フィールドしか対象にしないため、非公開フィールドにしてしまえば出力されません。

https://pkg.go.dev/encoding/json#Marshal

https://pkg.go.dev/encoding/xml#Marshal

```go
type Password struct {
	// unexported
	value *atomic.Pointer[string]
}
```

```go
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` の表示を制御できます。

https://pkg.go.dev/fmt#hdr-Printing

https://pkg.go.dev/fmt#GoStringer

この2つを実装し、ダミー文字列を返すようにします。

```go
// fmt.Stringerを実装
func (p *Password) String() string {
	return "{credential}"
}

// fmt.GoStringerを実装
func (p *Password) GoString() string {
	return "{credential}"
}
```

# おわりに

以上、GoでRead-Onceオブジェクトを作ってみた紹介でした。JavaとGoで同じような機能を実現できる標準ライブラリがあったため、予想よりも簡単に実装できました。

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