テキストを暗号化するツールを作った

本気で暗号化するならopensslなりpgpなりを使うので、もうちょっと手軽にテキストの暗号化ができれば便利なのではと思って作った。Chrome拡張をインストールして、あらかじめ鍵を登録しておけば、テキストを選択してコンテキストメニューから変換できる。中身は[crypto-js](https://code.google.com/p/crypto-js/)のAES。ブルートフォースに耐えられるような鍵さえ使っていれば、そうそう破られることはないはず。以下、プログラムの解説や作っているときに気が付いたことのメモ。

- ウェブサイト http://tinygma.sweetduet.info/
- Chrome拡張 https://chrome.google.com/webstore/detail/tinygma/fnljhmjjkjdimegljaahcindfpikkdnb

![tile.png](https://qiita-image-store.s3.amazonaws.com/0/25276/d33c6aba-69ee-ca01-b9b8-e917b43d5dd5.png)

# 暗号文のフォーマット

crypto-jsをそのまま使ってもBase64形式にしてくれるが、以下の理由で独自のフォーマットを使っている。

- Twitterはバイト単位ではなく文字単位で制限があるので、Base64は無駄が多い
- crypto-jsには復号結果が正しいかどうかを検証する仕組みが無い（後述）ので、間違った鍵でも（間違った）平文が復号される可能性がある。手元にある鍵を全部試すような使い方だと都合が悪い

変換の手順は次の通り

1. 平文の先頭に `tiny` という文字列を付けて、crypto-jsで暗号化。復号した文字列の先頭に `tiny` があれば復号成功とみなす
2. ソルトと暗号文を連結して、後述のBase32768でテキストに変換
3. 目印になるように、先頭に `䴀` (0x4d00)を、末尾に `䴁`(0x4d01)を付加する。

例えば、 `にゃんぱすー` という文字列を `Non Non Biyori Repeat` という鍵で暗号化すると、 `䴀揊暉䰕么鳌鎒貞弣頻닯䀣㛺召趒溔㮐袳欕㑫笽㰡贀䴁` になる（ソルトがあるので実行する度に結果は異なる）。
先頭と末尾の1文字を取り除いて、Base32768で復号すると、 `5d 94 c6 24 c0 a9 94 8c f9 97 a4 ab cf 2a 23 c6 77 c3 bc 61 18 2f a3 dd 96 24 9c ca 07 90 a7 66 d8 54 03 5c 63 d1 04 36` になる。先頭に `Salted__` を付ければopensslで暗号化したものと同じフォーマットなので、そのまま復号できる。

```bash
$ python -c "import sys; sys.stdout.write('Salted__'+'5d94c624c0a9948cf997a4abcf2a23c677c3bc61182fa3dd96249cca0790a766d854035c63d10436'.decode('hex'))" | openssl enc -d -aes-256-cbc -pass 'pass:Non Non Biyori Repeat'
tinyにゃんぱすー
```

# Base32768

バイナリを漢字やハングルを使ったテキストに変換する。Twitterではバイト数ではなく文字数に制限があるので、Base64の倍以上の情報を詰め込める。既存のテキスト変換はたいていASCII文字列への変換なので意味があるかと思ったけど、そういえばish形式なんてものもあったな……。

<blockquote class="twitter-tweet" lang="ja"><p lang="ja" dir="ltr">なんかISH思いだした <a href="https://t.co/KJKeP3F9Z8">https://t.co/KJKeP3F9Z8</a></p>&mdash; rnurachue ミ:匚＞ (@murachue) <a href="https://twitter.com/murachue/status/637570300285030400">2015, 8月 29</a></blockquote>

漢字とハングルを合わせると、0x8000 (= 32768)文字以上あるので、15bitを1文字に割り当てるようにする。数字と文字コードの対応は次の通り。

|数字|文字コード|
|---|---|
|0x0000 - 0x18ff|0x3400 - 0x4cff|
|0x1900 - 0x69ff|0x4e00 - 0x9eff|
|0x6a00 - 0x8000|0xac00 - 0xc200|

入力を15bitずつに区切り、対応する文字に変換する。

```
aaaaaaaa bbbbbbbb cccccccc dddddddd eeeeeeee ffffffff gggggggg hhhhhhhh iiiiiiii jjjjjjjj kkkkkkkk llllllll mmmmmmmm nnnnnnnn oooooooo
  ↓
aaaaaaaabbbbbbb bccccccccdddddd ddeeeeeeeefffff fffgggggggghhhh hhhhiiiiiiiijjj jjjjjkkkkkkkkll llllllmmmmmmmmn nnnnnnnoooooooo
```

入力のバイト数の剰余が、2, 4, 6, 8, 10, 12, 14のときに、それぞれ3, 5, 7, 9, 11, 13, 15と区別ができないので、3, 5, 7, 9, 11, 13, 15の場合は末尾に `숀` (0x80000 → 0xc200)を付ける。
例えば、 `00 01 02` を変換すると、 `0000 4080` で、 `㒁㓀` 。入力が3文字なので、末尾に `숀` を付けて、 `㒁㓀숀` となる。

JavaScriptのソースコード：

```js
//  0x0000 - 0x18ff => 0x3400 - 0x4cff
//  0x1900 - 0x69ff => 0x4e00 - 0x9eff
//  0x6a00 - 0x8000 => 0xac00 - 0xc200
function num2cjk(num) {
    if (num<0 || 0x8000<num)
        throw "num2cjk";
    return String.fromCharCode(
        num<0x1900 ? num+0x3400 :
        num<0x6a00 ? num-0x1900+0x4e00 :
                     num-0x6a00+0xac00);
}

//  0x3400 - 0x4cff => 0x0000 - 0x18ff
//  0x4e00 - 0x9eff => 0x1900 - 0x69ff
//  0xac00 - 0xc200 => 0x6a00 - 0x8000
function cjk2num(cjk) {
    var code = cjk.charCodeAt(0);
    if (0x3400<=code && code<0x4d00) return code-0x3400;
    if (0x4e00<=code && code<0x9f00) return code-0x4e00+0x1900;
    if (0xac00<=code && code<0xc201) return code-0xac00+0x6a00;
    throw "cjk2num";
}

//  [1,2,3] => "㒁㓀숀"
function base32768enc(bin) {
    var str = "";
    var t = 0;
    var tl = 0;
    for (var i=0; i<bin.length; i++) {
        if (tl<=7) {
            t |= bin[i]<<(7-tl);
            tl += 8;
        } else {
            t |= bin[i]>>(tl-7);
            str += num2cjk(t);
            t = bin[i]<<(22-tl)&0x7fff,
            tl -= 7;
        }
    }
    if (tl>0) {
        str += num2cjk(t);
        if (tl>=9)
            str += num2cjk(0x8000);
    }
    return str;
}

//  "㒁㓀숀" => [1,2,3]
function base32768dec(str) {
    if (str.length==0)
        return new Uint8Array(0);

    var sl = str.length;
    var bl = ((sl-1)>>3)*15 + (sl-1)%8*2;
    var f = str[sl-1] == num2cjk(0x8000);
    if ((sl-1)%8==0 && !f)
        bl++;
    if ((sl-1)%8!=0 && f)
        bl--;
    if (f)
        sl--;

    var bin = new Uint8Array(bl);
    var t = 0;
    var tl = 0;
    var si = 0;
    for (var bi=0; bi<bl; bi++) {
        if (tl<8) {
            t = t<<15 | cjk2num(str[si++]);
            tl += 15;
        }
        bin[bi] = t>>(tl-8)&0xff;
        tl -= 8;
    }
    return bin;
}
```

# opensslやcrypto-jsの復号結果の検証

```js
x = CryptoJS.AES.encrypt("hoge", "password")
""+CryptoJS.AES.decrypt(x, "password")        //  "686f6765"
""+CryptoJS.AES.decrypt(x, "wrong password")  //  ""
```

間違ったパスワードで、間違った平文が復号されるのではなく、エラーになるのが不思議だった。

```bash
$ echo "hoge" | openssl enc -e -aes-256-cbc -pass 'pass:password' -base64
U2FsdGVkX1+E7AllgMdmHjXdFYF1bUsS+sjgrUns++c=
$ echo 'U2FsdGVkX1+E7AllgMdmHjXdFYF1bUsS+sjgrUns++c=' | openssl enc -d -aes-256-cbc -pass 'pass:password' -base64
hoge
$ echo 'U2FsdGVkX1+E7AllgMdmHjXdFYF1bUsS+sjgrUns++c=' | openssl enc -d -aes-256-cbc -pass 'pass:wrong pas
sword' -base64
bad decrypt
139874269058888:error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt:evp_enc.c:596:
```
 opensslで同じように試すと、こんなエラーが出る。ググって見るとパディングのチェックをしている所だった。パディングはデフォルトではPKCS#7で、これはパディングするバイト数とバイトの値を同じにするものらしい。これだと1/256くらいの確率で、間違った暗号鍵でも復号されてしまうので、復号が成功したかどうかの判定をこれに頼るのはちょっと心許ない。

[Secret Cipher](http://sehermitage.web.fc2.com/crypto/sec_cipher.html)

# crypto-jsの独自フォーマット

crypto-jsでは暗号化した文字列のフォーマットはデフォルトではBase64だが、変えることができる。JSONに変換するサンプルが[載っている](https://code.google.com/p/crypto-js/#The_Cipher_Output)。サンプルではivもJSONに含めているが、ivは鍵とソルトから決定されるので変換後の文字列に含める必要は無かった。

[Opensslの暗号鍵とIVの生成アルゴリズム: GKの魅力とITアーキテクトへの挑戦](http://gkisland.seesaa.net/article/280867065.html)

# HTML5でのコンボボックス

暗号鍵は、既に登録してあるものをリストから選択することも、手入力することもできるようにしたかった。WindowsのUIだとコンボボックスがそういう機能。「コンボボックス HTML」でググると、自分で実装するにはとか、jQueryのライブラリが出てくるので、そういう機能は無いのかと思ったが、HTML5で追加された `<datalist>` を使えば良いらしい。

[datalist 要素 - HTML | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/datalist)

Chromeのバグで、Chrome拡張のpopupだと上手く動かないらしい。残念……。

[Issue 161302 - chromium - datalists do not work in the default_popup of a chrome extension. - An open-source project to help move the web forward. - Google Project Hosting](https://code.google.com/p/chromium/issues/detail?id=161302)

# PowerPointの図形の合成

[接合や合成などで図形を自在に作成する | Microsoft Office 2010 活用 TIPS | Microsoft Office 2010](http://www.microsoft.com/ja-jp/office/2010/business/training/tips/powerpoint21.aspx)

PowerPointに図形を足し引きする機能があった。グループ化と違って、1個のオブジェクトとして扱われるので便利。例えば歯車は、こんな感じで作った。

![image](https://qiita-image-store.s3.amazonaws.com/0/25276/59fcea80-d540-bc45-d780-5f5e6f82a00c.png)