私が歌川です

@utgwkk が書いている

続・2ちゃんねるのトリップを Unicode の時代に実装する

blog.utgw.net

以前こういう記事を書きました。今回はその続きです。

トリップとは

トリップとは、2ちゃんねるなどの匿名掲示板において個人を簡単に証明するための合言葉、またその機能によって表示される文字列のことを指します*1

前回までのあらすじ

Python2までは文字列 str のエンコードは環境依存でUnicode文字列は unicode 型になる、Python3からは str がUnicode文字列になる、ということに困っていました。

どう困るのかというと、 crypt.crypt()str 型を受け付ける、つまりUnicode文字列しか受け付けなくなってしまいます。トリップを生成するのに使う文字列はShift-JISにエンコードされていないと正しい結果が得られません。

バイト列 bytes にしてしまえばShift-JISの文字列 (バイト列) を操作できますが、 crypt.crypt()bytes 型を受け付けません。Perlの場合は内部表現文字列とバイト列のどちらも crypt に渡すことができたので困りませんでした。

そうこうしているうちに、Python2はEOLを迎え、2ちゃんねるは 2ch.sc と 5ch.net に分かれてしまいました。


(2021/1/5 21:45 追記ここから)

id:nonylene さんに passlibpasslib.hash.des_crypt.hash にはバイト列も渡せる、というのを教えてもらいました。やはりC拡張を書く必要はなかった!!!

全ての詳細をすっ飛ばすと、これだけでやりたいことが実現できます。

from passlib.hash import des_crypt

# 中略

tripkey = tripstr[1:]
# treat as Shift-JIS bytes
tripkey = bytes(tripkey, encoding='shift-jis')
salt = (tripkey + b'H.')[1:3]
salt = re.sub(rb'[^\.-z]', b'.', salt)
salt = salt.translate(bytes.maketrans(b':;<=>?@[\\]^_`', b'ABCDEFGabcdef'))
trip = des_crypt.hash(tripkey, salt=salt.decode('shift-jis'))
trip = trip[-10:]

passlibのコードを見ると*2、Python3ではUnicode文字列しか受け付けなくなったのでpure Pythonな実装にフォールバックします、といったコメントが書いてあります。 crypt(3) には char * を渡せばよいなら文字コード関係なくバイト列でいいはず、と思っていたのですが、どうやら間違っていなかったようです。

ということで、以降に書いてあるC言語のコードは必要なくなりました。よかったですね。

(追記ここまで)


解法

Pythonの crypt.crypt() ならびにPerlの crypt は、crypt(3)を実装したものです。 そして、Python3でもバイト列は種々の文字コードでencodeできます。 ということは crypt(3) にバイト列をもとにした文字列 char * を渡せるインタフェースがあればよさそうですね。 軽く検索した感じでは bytes を渡せる crypt(3) のPython実装は見つかりませんでした*3

ということでPythonのC拡張を書きます。C拡張は書いたことがあるので、サクッとできるつもりでした。結局、C拡張内で関数の引数をパースするのがうまくいかなくてCPythonのcryptモジュールの実装をコピペしてきたのですが……。

blog.utgw.net

ともあれ crypt.crypt() の実装の本質はこのあたり*4*5なので、あとは bytes 型を受け付けて char * に変換するように書き換えてやればよさそうです。

バイト列を受け付けるように書き換えた crypt.crypt() の実装 bytecrypt.crypt() *6がこちらになります*7。エラーハンドリングが雑なので、このまま実用するのは危険です。

static PyObject* bytecrypt (PyObject *module, PyObject *const *args, Py_ssize_t nargs) {
    PyObject *return_value = NULL;
    const char *word, *salt, *crypted;
    struct crypt_data data;

    word = PyBytes_AsString(args[0]);
    if (word == NULL) {
        goto exit;
    }
    salt = PyBytes_AsString(args[1]);
    if (salt == NULL) {
        goto exit;
    }

    memset(&data, 0, sizeof(data));
    crypted = crypt_r(word, salt, &data);
    if (crypted == NULL) {
        return_value = PyErr_SetFromErrno(PyExc_OSError);
        goto exit;
    }

    return_value = PyBytes_FromString(crypted);

exit:
    return return_value;
}

あとはこの bytecrypt.crypt() を使ってやれば完成です*8

>>> generate_trip('#istrip')
'◆/WG5qp963c'
>>> generate_trip('#ニコニコ')
'◆pA8Bpf.Qvk'

2バイト文字*9や半角カナを含む入力に対しても正しく出力できていそうですね*10

>>> generate_trip('#t%{rシ)L,')
'◆HAckEr.Tac'
>>> generate_trip('#DRL諞Qq@')
'◆AAAAAAAc.s'
>>> generate_trip('#JM@/!詫8')
'◆GoGoGO/Bos'
>>> generate_trip('#s0ンX[aF-')
'◆1uzee/wmbQ'
>>> generate_trip('#ゥ.N避y承')
'◆4/9.......'
>>> generate_trip('#DRL諞Qq@')
'◆AAAAAAAc.s'

何度かリンクを貼っていましたがリポジトリはこちらです。

github.com