数字に語呂合わせ可能な文字列をSKKの辞書データから探す

# 暗証番号用の数値を語呂合わせで覚えておきたい？

パスワードは十分な強度のものを用いるべきであり、そのためには十分な長さの文字列が必要ですね。最近では「8文字で英大小文字＋数字＋記号」よりも「12文字の英大小文字＋数字」のほうが安全という話も出ています。

しかし利用するサービスによっては、暗証番号しか利用できない場合があります。キャッシュカードやクレジットカードの暗証番号、携帯電話のネットワーク暗証番号やSIM PINあたりでは数字列の暗証番号しか使えません。Windows10へのログオンでもMicrosoftアカウントの代わりにPINを用いたりします。

そういったサービスで使用する暗証番号を作成する際に、ランダムな数値列で生成して管理してもよいのですけれど、語呂合わせできる数値のほうが覚えやすくて便利だとお思います。

しかし語呂合わせ可能な単語を考えるのは案外面倒です。そこで思いついたのが「日本語IMEの辞書データから語呂合わせ可能な単語を抽出する」という方法です。ここではSKKの辞書データから語呂合わせ可能な単語を抽出する方法や、それを行うためのスクリプトを書いてみることにします。

# 実際の例

たとえばこれは SKK-JISHO.L で語呂合わせ可能な単語のうち、文字列長の長い順に上位10件を抽出してみた。

```
7 2391316 ふさくいさいむ /不作為債務/
6 758471 なごやしない /名古屋市内/
6 715931 ないこくさい /内国債/
6 712989 ないふくやく /内服薬/
6 556101 こころいわい /心祝い/
6 446161 ししるいるい /死屍累々/
6 373459 みなみしこく /南四国/
6 317109 さいないわく /最内枠/
6 313314 さいさんさいし /再三再四/
6 279164 ふなくいむし /船食虫/
```

カラムの並び順は以下のとおりです。

- 語呂合わせの桁数
- 語呂合わせの数字列
- SKKの読み
- SKKの漢字変換データ

こういう単語を探すのは大変ですけど、プログラムやスクリプトが代わりに見つけてくれるならラクですよね。

# 使い方

SKKの辞書データはここらへんから入手できます。
https://github.com/skk-dev/dict

入手した辞書データに対して、この記事で紹介するスクリプトを以下のように実行すれば、語呂合わせ可能なデータだけが抜き出されます。

```bash
nkf -w SKK-JISYO.L | ~/bin/extract-skkdict2goroawase.awk
```

# 利用例？

- 覚えやすい暗証番号を探すのに使えます。
- 覚えやすい暗証番号をサービス側でブロックしたい場合にも使えるかもしれませんが、そんなブロックがかかっているサービスは嫌だなあ。
- 覚えやすい暗証番号が設定されているかどうかを brute force で探す場合にも使えるかも？


# 実装

awk で書いてあります。動作は macOS 上で確認しています。基本的な実装は以下のように行っています。

- 数値1桁と語呂合わせの一覧を作成する
- SKKの辞書データ1レコードについて読みを取り出して、全桁を語呂合わせに置換可能かどうかを試行する
- 試行は数値1桁に対して語呂合わせの文字数の多いものから試行する
- すべての文字を数値の語呂合わせに置換できたものを抽出する

ざっくりと作っていますので速度は遅いです。SKK-JISYO.L を処理すると、Macbook Pro retina 2012 で1分くらいかかります。だけどめったに使わないし、実装を改良している時間よりも処理時間のほうが速いので、これをチューニングする意味はあまりなさげ。

```awk:extract-skkdict2goroawase.awk
#!/usr/bin/awk -f

BEGIN {
# 語呂合わせ可能な文字列と数値のテーブルを作る
    count=0

    goroawase_yomi[count]="ここのつ"
    goroawase_num[count++]=9

    goroawase_yomi[count]="いつつ"
    goroawase_num[count++]=5

    goroawase_yomi[count]="きゅう"
    goroawase_num[count++]=9

    goroawase_yomi[count]="ここの"
    goroawase_num[count++]=9

    goroawase_yomi[count]="ななつ"
    goroawase_num[count++]=7

    goroawase_yomi[count]="ひとつ"
    goroawase_num[count++]=1

    goroawase_yomi[count]="ふたつ"
    goroawase_num[count++]=2

    goroawase_yomi[count]="みっつ"
    goroawase_num[count++]=3

    goroawase_yomi[count]="むっつ"
    goroawase_num[count++]=6

    goroawase_yomi[count]="やっつ"
    goroawase_num[count++]=8

    goroawase_yomi[count]="よっつ"
    goroawase_num[count++]=4

    goroawase_yomi[count]="いち"
    goroawase_num[count++]=1

    goroawase_yomi[count]="いつ"
    goroawase_num[count++]=5

    goroawase_yomi[count]="おー"
    goroawase_num[count++]=0

    goroawase_yomi[count]="きゅ"
    goroawase_num[count++]=9

    goroawase_yomi[count]="さん"
    goroawase_num[count++]=3

    goroawase_yomi[count]="しち"
    goroawase_num[count++]=7

    goroawase_yomi[count]="ぜろ"
    goroawase_num[count++]=0

    goroawase_yomi[count]="なな"
    goroawase_num[count++]=7

    goroawase_yomi[count]="はち"
    goroawase_num[count++]=8

    goroawase_yomi[count]="ぱあ"
    goroawase_num[count++]=8

    goroawase_yomi[count]="ひと"
    goroawase_num[count++]=1

    goroawase_yomi[count]="ふぉ"
    goroawase_num[count++]=4

    goroawase_yomi[count]="ふた"
    goroawase_num[count++]=2

    goroawase_yomi[count]="まる"
    goroawase_num[count++]=0

    goroawase_yomi[count]="みつ"
    goroawase_num[count++]=3

    goroawase_yomi[count]="むつ"
    goroawase_num[count++]=6

    goroawase_yomi[count]="やあ"
    goroawase_num[count++]=8

    goroawase_yomi[count]="やつ"
    goroawase_num[count++]=8

    goroawase_yomi[count]="よつ"
    goroawase_num[count++]=4

    goroawase_yomi[count]="よん"
    goroawase_num[count++]=4

    goroawase_yomi[count]="れい"
    goroawase_num[count++]=0

    goroawase_yomi[count]="ろく"
    goroawase_num[count++]=6

    goroawase_yomi[count]="い"
    goroawase_num[count++]=1

    goroawase_yomi[count]="く"
    goroawase_num[count++]=9

    goroawase_yomi[count]="こ"
    goroawase_num[count++]=5

    goroawase_yomi[count]="ご"
    goroawase_num[count++]=5

    goroawase_yomi[count]="さ"
    goroawase_num[count++]=3

    goroawase_yomi[count]="し"
    goroawase_num[count++]=4

    goroawase_yomi[count]="じ"
    goroawase_num[count++]=2

    goroawase_yomi[count]="な"
    goroawase_num[count++]=7

    goroawase_yomi[count]="に"
    goroawase_num[count++]=2

    goroawase_yomi[count]="は"
    goroawase_num[count++]=8

    goroawase_yomi[count]="ふ"
    goroawase_num[count++]=2

    goroawase_yomi[count]="み"
    goroawase_num[count++]=3

    goroawase_yomi[count]="む"
    goroawase_num[count++]=6

    goroawase_yomi[count]="や"
    goroawase_num[count++]=8

    goroawase_yomi[count]="よ"
    goroawase_num[count++]=4

    goroawase_yomi[count]="る"
    goroawase_num[count++]=6

    goroawase_yomi[count]="れ"
    goroawase_num[count++]=0

    goroawase_yomi[count]="ろ"
    goroawase_num[count++]=6

    goroawase_yomi[count]="わ"
    goroawase_num[count++]=0
}

# ここのループで、SKK の辞書データ１行ごとに語呂合わせ可能かどうかを判定します
{
    # 読みを抽出してパターンマッチ用の変数に入れる
    skk_yomi=$1

    # 語呂合わせの結果を保持する変数。初期値は抽出された読み。
    skk_goro=$1

    for ( idx=0 ; idx < length(goroawase_yomi) ; idx++ ) {
        # 読みが語呂合わせにマッチする部分を探して数字に置き換える
        if ( match(skk_yomi, goroawase_yomi[idx]) ){
            # マッチした部分はもう使えないので消す
            gsub(goroawase_yomi[idx],"",skk_yomi)

            # マッチした部分を数字に置換する
            gsub(goroawase_yomi[idx],goroawase_num[idx],skk_goro)
        }
        if ( length(skk_yomi) == 0 ) break;
    }

    # 読みの全ての文字が数字に語呂合わせできた場合は結果を出力する
    if ( length(skk_yomi) == 0 ) {
        print length(skk_goro),skk_goro,$0 | "sort -nr"
    }
}
```