読者です 読者をやめる 読者になる 読者になる

SecureString

同じ研究室の某M君の研究だけど、なかなか興味深い。彼はRubyでの実装を頑張っているけど、ちょっとJavaで似たようなもの(あくまで似たようなもの)を作ってみたり。

// セキュアな文字列クラス
public abstract class SecureString {
    // (1)
    private final String str;
    // (2)
    private String secStr = null;
    // (3)
    public SecureString(String str) {
        this.str = str;
    }
    // (4)
    public SecureString(SecureString secStr) {
        this.str = secStr.str;
    }
    // (5)
    public final String unsecureStr() {
        return this.str;
    }
    // (6)
    public final String secureStr() {
        if (this.secStr == null)
            this.secStr = wash();
        return this.secStr;
    }
    // (7)
    public abstract String wash();
    
    // これはただのヘルパメソッド
    // 引数がLinkedHashMap限定なのは置換には登録された順番が重要だから。
    protected String replace(LinkedHashMap<String, String> repMap) {
        String outStr = this.str;
        for (String key : repMap.keySet())
            outStr = outStr.replaceAll(key, repMap.get(key));
        return outStr;
    }
}

このセキュアな文字列クラスはイミュータブルで、一度構築したらオブジェクトの状態が変更できないようになっている。
一番重要な内部状態である(1)はfinalがついていて、しかもprivateなので弄れない。
セキュアな文字列を格納する(2)では効率化のためにfinalではないけど、これはC++でいうmutableがついた変数で、用はキャッシュ。本質には関係のないところなので気にする必要はなし。
コンストラクタは(3)と(4)で、StringかSecureString(自分自身)でのみ構築可能。デフォルトコンストラクタ*1は意味がなさそうなので用意していないけど、まぁしても害はないかも。
オリジナルの文字列を取り出すメソッドは(5)で、洗われた文字列を取り出すメソッドが(6)。どちらもfinalとなっているのでオーバーライドは不可能に。メソッド名に悩んだ*2けど、ユーザが意識しやすいようにわざと長いメソッド名に。secureStrメソッド(6)では内部で抽象メソッドwash(7)を呼び出していて、ここで文字列を「洗う」。
(7)はもちろんサブクラスで実装してもらう。


で、HTML用のデフォルト実装が次の通り。

// SecureStringのHTML用デフォルト実装
public class DefaultSecureHtmlString extends SecureString {
    // コンストラクタは親クラスのものを呼び出すだけ
    public DefaultSecureHtmlString(String str) {
        super(str);
    }
    public DefaultSecureHtmlString(SecureString secStr) {
        super(secStr);
    }
    // 抽象メソッドの実装
    public String wash() {
        LinkedHashMap<String, String> repMap
            = new LinkedHashMap<String, String>(3);
        // (8)
        repMap.put("&", "&amp;");
        repMap.put("<", "&lt;");
        repMap.put(">", "&gt;");
        return super.replace(repMap);
    }
}

ここでは、デフォルト実装は "&"と "<"と ">"をエスケープする簡単な実装とした。SecureStringのヘルパメソッドreplaceがなぜLinkedHashMapを引数にとるかというと、登録した順番が重要だから。例えばHTMLでは(8)のように、まずは&からエスケープする必要がある*3


最後に、クライアント側のコードがこんな感じ。

// (9)
SecureString str = new SecureString("<s>test</s>") {
    public String wash() {
        LinkedHashMap<String, String> repMap
            = new LinkedHashMap<String, String>(1);
        repMap.put("/", "[s]");
        return super.replace(repMap);
    }
};
System.out.println("unsecure string:" + str.unsecureStr());
System.out.println("secure string  :" + str.secureStr());

// (10)
str = new DefaultSecureHtmlString(str);
System.out.println("unsecure string:" + str.unsecureStr());
System.out.println("secure string  :" + str.secureStr());

(9)では無名クラスを使ってユーザが定義したwashメソッドを使うようにしている。この例ではスラッシュを[s]に変換している。
(10)ではユーザ定義のSecureStringからHTML用デフォルト実装のSecureStringへと変換している。このとき、変換後(washメソッドを通した後)の文字列ではなく、オリジナルの文字列をコピーする*4


こんな感じでどうなんだろう?あとはequals、hashCode、toStringとか*5オーバーライドしたほうがいいのかな?一番の問題は多分toStringメソッドで何を返すか。案としては、

  1. secureStrメソッドと同じ
  2. unsecureStrメソッドと同じ
  3. [unsecure string: 〜, secure string: 〜] みたいな形式でどちらも返す

ってところかなぁ。


実装してみて改めて思ったんだけど、こういう仕組みが言語に組み込まれていると本当に便利だろうな、ということ。流石はA先生、考えることが他とは違う。

*1:デフォルトコンストラクタを実装するなら空文字列でstrを初期化するんだろうけど、空文字列ってサニタイズする必要ない気が・・・ ってことで実装していない。

*2:dataとstrとか、originStrとwashedStrとか。

*3:もちろん "<"や ">"を先にエスケープしてしまうと、"&"をエスケープしたときに "&lt;"や "&gt;"の "&"がエスケープされてしまうから。

*4:これによってHTML用のwashをした後のオブジェクトからSQL用のSecureStringを作った場合でも不具合が起きない。

*5:他にもcompareToとか。でも別にsecureStrとunsecureStrメソッドでStringクラスのインスタンスが取得できるしなぁ。