以前こういう記事を書きました。今回はその続きです。
トリップとは
トリップとは、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 さんに passlib の passlib.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モジュールの実装をコピペしてきたのですが……。
ともあれ 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'
何度かリンクを貼っていましたがリポジトリはこちらです。
*2:https://foss.heptapod.net/python-libs/passlib/-/blob/branch/stable/passlib/handlers/des_crypt.py#L221-222
*3:ここで見つかっていればC拡張を書く必要はなかったです。実はあるよ! とか誰か知ってたら教えてください
*4:https://github.com/python/cpython/blob/f7f0ed59bcc41ed20674d4b2aa443d3b79e725f4/Modules/_cryptmodule.c#L33-L49
*5:https://github.com/python/cpython/blob/f7f0ed59bcc41ed20674d4b2aa443d3b79e725f4/Modules/clinic/_cryptmodule.c.h#L23
*6:bcrypt にすると名前が被ってややこしい
*7:https://github.com/utgwkk/20210103-sketch-tripcode/blob/094fbca7c9a6e21d1381ed820d69f6e6305b3cc5/bytecrypt.c#L9-L34
*8:https://github.com/utgwkk/20210103-sketch-tripcode/blob/094fbca7c9a6e21d1381ed820d69f6e6305b3cc5/tripcode.py#L7
*9:Unicode時代になってから2バイト文字って聞かなくなりましたね
*10:テストケースに用いた入力は トリップ倉庫 - (^p^)レア酉集積所(^q^)@Returns【12/29更新】 - atwiki(アットウィキ) を参照しました