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

Secure String(C++版)

C++ Program Security

Secure StringのC++版を作ってみた。Java版ともっとも大きく違うのはテンプレートを使用していること。あとコンストラクタがexplicitじゃないから初期化のときに = が使えるのも大きい。

#pragma once

#include <string>

/*
 * C++版セキュアな文字列クラス。
 * std::basic_stringとは違いイミュータブルなクラスとして設計。
 */
template
<
    template <typename, class, class> class Washer, // 文字列洗浄用オブジェクト
    typename charT,                                 // 文字列の文字
    class traits = std::char_traits<charT>,         // 文字の特性
    class allocator = std::allocator<charT>         // アロケータ
>
class basic_secure_string : public Washer<charT, traits, allocator> {
private:
    typedef std::basic_string<charT, traits, allocator> b_string;
    typedef Washer<charT, traits, allocator> washer;

    const b_string str;
    mutable bool isTaint;
    mutable b_string secStr;

    // 代入演算子と+=演算子は使用禁止にしておく。
    template <template <typename, class, class> class W, typename T, class CT, class A>
    basic_secure_string<W, T, CT, A>& operator= (const basic_secure_string<W, T, CT, A>&);
    template <template <typename, class, class> class W, typename T, class CT, class A>
    basic_secure_string<W, T, CT, A>& operator+= (const basic_secure_string<W, T, CT, A>&);
public:
    // コンストラクタはCの文字列を受け取るものと、
    basic_secure_string(const char* str)
        : str(str), isTaint(true) {}
    // C++の文字列を受け取るものと、
    template <typename T, class CT, class A>
    basic_secure_string(const std::basic_string<T, CT, A>& str)
        : str(str.c_str()), isTaint(true) {}
    // 自分自身を受け取るもので、explicitではない。
    template <typename T, class CT, class A>
    basic_secure_string(const basic_secure_string<T, CT, A>& str)
        : str(str.unsecureStr()), isTaint(true) {}

    // メンバ関数はオリジナルの文字列を取得するものと、
    const b_string& unsecureStr() const {
        return this->str;
    }
    // セキュアな文字列を取得するものだけ。
    const b_string& secureStr() const {
        if (isTaint) {
            secStr = washer::wash(unsecureStr());
            isTaint = false;
        }
        return secStr;
    }
};

/*
 * basic_secure_stringに渡すテンプレートパラメータのWasher用ユーティリティ。
 * と言っても、今のところ長ったらしいbasic_stringのtypedefと
 * C++では割と面倒な全置換しか実装していない。
 */
template
<
    typename charT,
    class traits,
    class allocator
>
class washer_util {
protected:
    // いちいちstd::basic_string<charT, traits, allocator>って書くのは・・・
    typedef std::basic_string<charT, traits, allocator> b_string;
    // outputのoldStrを全てnewStrに置換する関数。
    static void replaceAll(b_string& output, const b_string& oldStr, const b_string& newStr) {
        for (int i = output.find(oldStr, 0); i != output.npos; i = output.find(oldStr, i+1))
            output.replace(i, oldStr.length(), newStr);
    }
};

/*
 * Washer作るのにテンプレートを書くのが面倒だからマクロ化。
 */
#define MAKE_WASHER(name)       \
    template                    \
    <                           \
        typename charT,         \
        class traits,           \
        class allocator         \
    >                           \
    struct name                 \
    : public washer_util<charT, traits, allocator>
    

/*
 * HTML用のWasher。
 * MAKE_WASHERマクロを使用して作られており、他のWasher作成の参考に。
 */
MAKE_WASHER(html_washer) {
    static const b_string wash(const b_string& str) {
        b_string secStr = str;
        replaceAll(secStr, "&", "&amp;");
        replaceAll(secStr, "<", "&lt;");
        replaceAll(secStr, ">", "&gt;");
        return secStr;
    }
};

/*
 * これまたいちいちbasic_secure_string<html_washer, char>
 * って書くのが面倒なのでtypedefしておく。
 */
typedef basic_secure_string<html_washer, char> html_secure_string;

Javaに比べて劣る点はもちろんコードが長いことと、複雑に見えることw その分クライアント側はすっきりしてるけど。

で、クライアントコードは下の通り。

#include <iostream>
#include "secure_string.h"

MAKE_WASHER(test) {
    static const b_string wash(const b_string& str) {
        b_string secStr = str;
        replaceAll(secStr, "test", "foo");
        return secStr;
    }
};

int main() {
    // 文字列リテラル(Cの文字列)からセキュアな文字列を構築。
    html_secure_string str = "<s>test</s>";
    std::cout << str.unsecureStr() << std::endl;    // セキュアじゃない(オリジナルの)文字列を表示
    std::cout << str.secureStr() << std::endl;      // セキュアな(サニタイズされた)文字列を表示
    std::cout << std::endl;

    // str(セキュアな文字列)からstr2を構築。
    basic_secure_string<test, char> str2 = str;
    std::cout << str2.unsecureStr() << std::endl;   // コピーしてもオリジナルの文字列は残る
    std::cout << str2.secureStr() << std::endl;
    std::cout << std::endl;

    // str.unsecureStr(basic_string:C++の文字列)からstr3を構築。
    basic_secure_string<test, char> str3 = str.unsecureStr();
    std::cout << str3.unsecureStr() << std::endl;
    std::cout << str3.secureStr() << std::endl;
    std::cout << std::endl;
}

やっぱり問題はoperator<<演算子をどうするかだけど、自分の中ではsecureStrを呼ぶようにするのが(C++では)自然かなぁ、と思ったり。Javaは組み合わせて出力するのが自然かも。

ちなみにVisual Studio 2005でしか試してないからほかの環境では動かない可能性が・・・ そもそも#pragma once使ってる時点で・・・ねぇ?