新12桁トリップなど

いつの間にかトリップコードの仕様が新しくなっていた。例のごとく情報が散逸していて仕様を調べるのに苦労したが、この記事が大いに参考になった。

まず12桁トリップから行こう。こちらは結構簡単で、キーをSHA1でみじん切りにしたあと、Base64で見た目を整えれば完成。

# Python 2.6.6
# -*- coding: shift_jis -*-
import base64
import hashlib

def trip12(key):
    """12桁トリップを計算する。
    
    >>> trip12('123456789012')
    'jZk8zfYo4m4X'
    >>> trip12('cWXV3rkHusy8')
    'V/Q./xpP.l0c'
    >>> trip12('いろはにほへと')
    '2R7QVeXeLHrO'
    """
    
    assert len(key) >= 12
    
    code = hashlib.sha1(key).digest()
    code = base64.b64encode(code, './')
    code = code[:12]
    
    return code

3.x版はこちら。

# Python 3.1.3
import base64
import hashlib

def trip12(key):
    """12桁トリップを計算する。
    
    >>> trip12('123456789012')
    'jZk8zfYo4m4X'
    >>> trip12('cWXV3rkHusy8')
    'V/Q./xpP.l0c'
    >>> trip12('いろはにほへと')
    '2R7QVeXeLHrO'
    """

    key = key.encode('cp932')
    assert len(key) >= 12
    
    code = hashlib.sha1(key).digest()
    code = base64.b64encode(code, b'./')
    code = code[:12]
    code = code.decode('cp932')
    
    return code


従来の10桁トリップも変更が加えられている。新しく「生キー指定」ができるようになった。

# Python 2.6.6
# -*- coding: shift_jis -*-
import re
import crypt

def trip10hex(key):
    """10桁トリップを16進数表現から計算する(生キー指定)。

    >>> trip10hex('#4141414141414141AA')
    'DLUg7SsaxM'

    >>> trip10hex('#0000000000000000ZZ')
    'IHp4MBMwSE'
    """
    assert re.match(r'^#[0-9A-Fa-f]{16}[./0-9A-Za-z]{0,2}$', key)
    
    key = key[1:]
    
    plain = key[:16]
    plain = plain.decode('hex')
    plain = plain.split('\0', 1)[0]

    salt = key[16:18] + '..'
    
    code = crypt.crypt(plain, salt)
    code = code[-10:]

    return code

3.x版の方は結構難しいが、無理やり書くとこんな感じ。

# Python 3.1.3
import re
import ctypes
import binascii

libcrypt = ctypes.cdll.LoadLibrary('libcrypt-2.11.3.so')
libcrypt.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
libcrypt.crypt.restype = ctypes.c_char_p

def trip10hex(key):
    """10桁トリップを16進数表現から計算する(生キー指定)。

    >>> trip10hex('#4141414141414141AA')
    'DLUg7SsaxM'

    >>> trip10hex('#0000000000000000ZZ')
    'IHp4MBMwSE'
    """
    assert re.match(r'^#[0-9A-Fa-f]{16}[./0-9A-Za-z]{0,2}$', key)
    
    key = key[1:]
    key = key.encode()
    
    plain = key[:16]
    plain = binascii.a2b_hex(plain)

    salt = key[16:18] + b'..'
    
    code = libcrypt.crypt(plain, salt)
    code = code[-10:]
    code = code.decode()
    
    return code


こうして 2.x と 3.x のコードを比べてみると、Python3 は strbytes の使い分けがうまくできてないな。例えば binascii.a2b_hex は引数に bytes しか受け付けない。別に 0〜F しか使っていなければ str を受け付けてもいいと思うのだが(それ以前に hex_codec とか base64_encode とかが使えないままになっているのも気になるとこだが)。

>>> import binascii
>>> binascii.a2b_hex(b'1234')
b'\x124'
>>> binascii.a2b_hex('1234')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' does not support the buffer interface

それから crypt の実装がおかしい。C言語の crypt 関数は仮引数の型が char * なのだが、Python3 の crypt モジュールは不思議なことに str しか受け付けない。 bytes を渡すとエラーになる。どういうことかと思って中身を見てみると、UTF-8 で変換してから渡してやがった。これでは旧来の文字コードShift_JISEUC-JP、Latin-1 など)が置き去りではないか。

>>> import crypt
>>> crypt.crypt('hello', 'ab')
'abl0JrMf6tlhw'
>>> crypt.crypt(b'hello', b'ab')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be string, not bytes

しょうがないので上のコードでは ctypes を使って crypt を直接呼ぶことにした。おかげでマルチプラットフォームがさらに遠のいてしまった。このあたりは zipfile の問題と同じ構図だろう。

Python3 になれば文字コードから開放されると思ってたが、どうやらこれからも文字コード周りのトラブルは続きそうだな。