no-image

Python で bcrypt を使う

$ sudo apt install build-essential libffi-dev python-dev
$ sudo pip install bcrypt

これで import bcrypt するだけです。


以下は細かな内容など。

パスワードのハッシュ化に使われる bcrypt を Python から使うには、モジュールが既に用意されているのでそれを使います。

pip で一発インストールのはずですが

$ pip install bcrypt

としたところ

(略)
compilation terminated.

Traceback (most recent call last):

 File "<string>", line 1, in <module>

 File "/tmp/pip-build-jQm5iu/bcrypt/setup.py", line 238, in <module>

 **keywords_with_side_effects(sys.argv)

 File "/usr/lib/python2.7/distutils/core.py", line 111, in setup

 _setup_distribution = dist = klass(attrs)

 File "/usr/lib/python2.7/dist-packages/setuptools/dist.py", line 262, in __init__

 self.fetch_build_eggs(attrs['setup_requires'])

 File "/usr/lib/python2.7/dist-packages/setuptools/dist.py", line 287, in fetch_build_eggs

 replace_conflicting=True,

 File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 631, in resolve

 dist = best[req.key] = env.best_match(req, ws, installer)

 File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 874, in best_match

 return self.obtain(req, installer)

 File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 886, in obtain

 return installer(requirement)

 File "/usr/lib/python2.7/dist-packages/setuptools/dist.py", line 338, in fetch_build_egg

 return cmd.easy_install(req)

 File "/usr/lib/python2.7/dist-packages/setuptools/command/easy_install.py", line 636, in easy_install

 return self.install_item(spec, dist.location, tmpdir, deps)

 File "/usr/lib/python2.7/dist-packages/setuptools/command/easy_install.py", line 666, in install_item

 dists = self.install_eggs(spec, download, tmpdir)

 File "/usr/lib/python2.7/dist-packages/setuptools/command/easy_install.py", line 856, in install_eggs

 return self.build_and_install(setup_script, setup_base)

 File "/usr/lib/python2.7/dist-packages/setuptools/command/easy_install.py", line 1078, in build_and_install

 self.run_setup(setup_script, setup_base, args)

 File "/usr/lib/python2.7/dist-packages/setuptools/command/easy_install.py", line 1066, in run_setup

 raise DistutilsError("Setup script exited with %s" % (v.args[0],))

distutils.errors.DistutilsError: Setup script exited with error: command 'arm-linux-gnueabihf-gcc' failed with exit status 1

とエラーを吐いてインストールに失敗したので、必要なパッケージをインストールして再度インストールします。

$ sudo apt install build-essential libffi-dev python-dev
$ sudo pip install bcrypt
Downloading/unpacking bcrypt
 Downloading bcrypt-3.1.4.tar.gz (42kB): 42kB downloaded
 Running setup.py (path:/tmp/pip-build-6PxpF9/bcrypt/setup.py) egg_info for package bcrypt

 warning: no previously-included files found matching 'requirements.txt'
 warning: no previously-included files found matching 'tasks.py'
 warning: no previously-included files found matching '.travis.yml'
 warning: no previously-included files found matching 'wheel-scripts'
 warning: no previously-included files found matching 'Jenkinsfile'
 warning: no previously-included files found matching '.jenkins'
 warning: no previously-included files matching '*' found under directory '.jenkins'
 warning: no previously-included files matching '*' found under directory 'wheel-scripts'
 no previously-included directories found matching '.travis'
Downloading/unpacking cffi>=1.1 (from bcrypt)
 Downloading cffi-1.11.5.tar.gz (438kB): 438kB downloaded
 Running setup.py (path:/tmp/pip-build-6PxpF9/cffi/setup.py) egg_info for package cffi

Requirement already satisfied (use --upgrade to upgrade): six>=1.4.1 in /usr/lib/python2.7/dist-packages (from bcrypt)
Downloading/unpacking pycparser (from cffi>=1.1->bcrypt)
 Downloading pycparser-2.18.tar.gz (245kB): 245kB downloaded
 Running setup.py (path:/tmp/pip-build-6PxpF9/pycparser/setup.py) egg_info for package pycparser

 warning: no previously-included files matching 'yacctab.*' found under directory 'tests'
 warning: no previously-included files matching 'lextab.*' found under directory 'tests'
 warning: no previously-included files matching 'yacctab.*' found under directory 'examples'
 warning: no previously-included files matching 'lextab.*' found under directory 'examples'
Installing collected packages: bcrypt, cffi, pycparser
 Running setup.py install for bcrypt

 Installed /tmp/pip-build-6PxpF9/bcrypt/cffi-1.11.5-py2.7-linux-armv7l.egg
 Searching for pycparser
 Reading https://pypi.python.org/simple/pycparser/
 Best match: pycparser 2.18
 Downloading https://files.pythonhosted.org/packages/8c/2d/aad7f16146f4197a11f8e91fb81df177adcc2073d36a17b1491fd09df6ed/pycparser-2.18.tar.gz#sha256=99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226
 Processing pycparser-2.18.tar.gz
 Writing /tmp/easy_install-RvGBgb/pycparser-2.18/setup.cfg
 Running pycparser-2.18/setup.py -q bdist_egg --dist-dir /tmp/easy_install-RvGBgb/pycparser-2.18/egg-dist-tmp-ve58NH
 warning: no previously-included files matching 'yacctab.*' found under directory 'tests'
 warning: no previously-included files matching 'lextab.*' found under directory 'tests'
 warning: no previously-included files matching 'yacctab.*' found under directory 'examples'
 warning: no previously-included files matching 'lextab.*' found under directory 'examples'
 zip_safe flag not set; analyzing archive contents...
 pycparser.ply.ygen: module references __file__
 pycparser.ply.yacc: module references __file__
 pycparser.ply.yacc: module MAY be using inspect.getsourcefile
 pycparser.ply.yacc: module MAY be using inspect.stack
 pycparser.ply.lex: module references __file__
 pycparser.ply.lex: module MAY be using inspect.getsourcefile

 Installed /tmp/pip-build-6PxpF9/bcrypt/pycparser-2.18-py2.7.egg
 generating cffi module 'build/temp.linux-armv7l-2.7/_bcrypt.c'
 building '_bcrypt' extension
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Isrc/_csrc -I/usr/include/python2.7 -c build/temp.linux-armv7l-2.7/_bcrypt.c -o build/temp.linux-armv7l-2.7/build/temp.linux-armv7l-2.7/_bcrypt.o
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Isrc/_csrc -I/usr/include/python2.7 -c src/_csrc/blf.c -o build/temp.linux-armv7l-2.7/src/_csrc/blf.o
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Isrc/_csrc -I/usr/include/python2.7 -c src/_csrc/bcrypt.c -o build/temp.linux-armv7l-2.7/src/_csrc/bcrypt.o
 src/_csrc/bcrypt.c: In function 窶賄ecode_base64窶・
 src/_csrc/bcrypt.c:205:22: warning: pointer targets in initialization differ in signedness [-Wpointer-sign]
 const u_int8_t *p = b64data;
 ^
 src/_csrc/bcrypt.c: In function 窶脇ncode_base64窶・
 src/_csrc/bcrypt.c:247:17: warning: pointer targets in initialization differ in signedness [-Wpointer-sign]
 u_int8_t *bp = b64buffer;
 ^
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Isrc/_csrc -I/usr/include/python2.7 -c src/_csrc/bcrypt_pbkdf.c -o build/temp.linux-armv7l-2.7/src/_csrc/bcrypt_pbkdf.o
 In file included from src/_csrc/bcrypt_pbkdf.c:24:0:
 src/_csrc/sha2.h:68:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__string__,2,3)));
 ^
 src/_csrc/sha2.h:70:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__minbytes__,1,SHA256_DIGEST_LENGTH)));
 ^
 src/_csrc/sha2.h:74:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__string__,2,3)));
 ^
 src/_csrc/sha2.h:76:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__minbytes__,1,SHA384_DIGEST_LENGTH)));
 ^
 src/_csrc/sha2.h:80:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__string__,2,3)));
 ^
 src/_csrc/sha2.h:82:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__minbytes__,1,SHA512_DIGEST_LENGTH)));
 ^
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Isrc/_csrc -I/usr/include/python2.7 -c src/_csrc/sha2.c -o build/temp.linux-armv7l-2.7/src/_csrc/sha2.o
 In file included from src/_csrc/sha2.c:38:0:
 src/_csrc/sha2.h:68:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__string__,2,3)));
 ^
 src/_csrc/sha2.h:70:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__minbytes__,1,SHA256_DIGEST_LENGTH)));
 ^
 src/_csrc/sha2.h:74:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__string__,2,3)));
 ^
 src/_csrc/sha2.h:76:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__minbytes__,1,SHA384_DIGEST_LENGTH)));
 ^
 src/_csrc/sha2.h:80:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__string__,2,3)));
 ^
 src/_csrc/sha2.h:82:2: warning: 窶論_bounded__窶・attribute directive ignored [-Wattributes]
 __attribute__((__bounded__(__minbytes__,1,SHA512_DIGEST_LENGTH)));
 ^
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Isrc/_csrc -I/usr/include/python2.7 -c src/_csrc/timingsafe_bcmp.c -o build/temp.linux-armv7l-2.7/src/_csrc/timingsafe_bcmp.o
 arm-linux-gnueabihf-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wl,-z,relro -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security build/temp.linux-armv7l-2.7/build/temp.linux-armv7l-2.7/_bcrypt.o build/temp.linux-armv7l-2.7/src/_csrc/blf.o build/temp.linux-armv7l-2.7/src/_csrc/bcrypt.o build/temp.linux-armv7l-2.7/src/_csrc/bcrypt_pbkdf.o build/temp.linux-armv7l-2.7/src/_csrc/sha2.o build/temp.linux-armv7l-2.7/src/_csrc/timingsafe_bcmp.o -o build/lib.linux-armv7l-2.7/bcrypt/_bcrypt.so

 warning: no previously-included files found matching 'requirements.txt'
 warning: no previously-included files found matching 'tasks.py'
 warning: no previously-included files found matching '.travis.yml'
 warning: no previously-included files found matching 'wheel-scripts'
 warning: no previously-included files found matching 'Jenkinsfile'
 warning: no previously-included files found matching '.jenkins'
 warning: no previously-included files matching '*' found under directory '.jenkins'
 warning: no previously-included files matching '*' found under directory 'wheel-scripts'
 no previously-included directories found matching '.travis'
 Running setup.py install for cffi
 building '_cffi_backend' extension
 arm-linux-gnueabihf-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -DUSE__THREAD -DHAVE_SYNC_SYNCHRONIZE -I/usr/include/python2.7 -c c/_cffi_backend.c -o build/temp.linux-armv7l-2.7/c/_cffi_backend.o
 arm-linux-gnueabihf-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wl,-z,relro -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security build/temp.linux-armv7l-2.7/c/_cffi_backend.o -lffi -o build/lib.linux-armv7l-2.7/_cffi_backend.so

 Running setup.py install for pycparser

 warning: no previously-included files matching 'yacctab.*' found under directory 'tests'
 warning: no previously-included files matching 'lextab.*' found under directory 'tests'
 warning: no previously-included files matching 'yacctab.*' found under directory 'examples'
 warning: no previously-included files matching 'lextab.*' found under directory 'examples'
 Build the lexing/parsing tables
Successfully installed bcrypt cffi pycparser
Cleaning up...

これで完了。

サンプルコードの実行

試しにサンプルコードを実行してみます。

$ python
Python 2.7.9 (default, Sep 17 2016, 20:26:04)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import bcrypt
>>> pw = b'This is a test password.'
>>> hashed = bcrypt.hashpw(pw, bcrypt.gensalt())
>>> bcrypt.checkpw(pw, hashed)
True
>>> print hashed
$2b$12$ceIKFEMb6Bee.xA.2exoQetEBX4h6n8jPiD1orfSBgwPQpovivE.e
>>> bcrypt.gensalt()
'$2b$12$OAKukakmx8djY4tStjH54e'
>>> bcrypt.gensalt()
'$2b$12$NKorWTVLa9CTqlMGSIRx4u'
>>> bcrypt.gensalt()
'$2b$12$Yqo1TF6.rvpgKlOYZw3FVe'

生成された $2b$12$ceIKFEMb6Bee.xA.2exoQetEBX4h6n8jPiD1orfSBgwPQpovivE.e がハッシュ化されたパスワードです。このハッシュ化されたパスワードは OpenBSD のパスワードファイルの書式である "Modular Crypt Format" に準じているそうです。

最初の '$' に挟まれた部分がハッシュ化するときに使われるアルゴリズムを示していて、$1$ は MD5, $5$ はSHA256, $6$ は SHA512 と定められているようです。

bcrypt もいくつかバージョンがあり $2$, $2a$, $2b$, $2x$, $2y$ が存在していますが、現在は $2a$ が使われることが多いみたいです。今回のモジュールでは何も指定しないと $2b$ が使われます。変更するには gensalt() の引数に prefix = b'2a' を指定します。

>>> bcrypt.gensalt(prefix = b'2a')
'$2a$12$Mk13orflQQ.LStP0DrjP1.'

$2a$ に続く 2 桁の数字はハッシュ化を行う回数で、初期値は 12 です。bcrypt は複数回ハッシュ化を行うことで計算コストを大きくし、総当りによるパスワードの複合化をより困難にしています。

ここで注意しないといけないのは、この数字は 2 の冪乗数であり、例えば 10 を指定すると 2^10 = 1024 回のハッシュ化を行います。そのため安易に数を大きくすると計算コストが膨大になります。指定できる値は 4~31 です。

ハッシュ化回数を 8, バージョンを 2a とする場合は次のように salt を生成します。

>>> bcrypt.gensalt(8, b'2a')
'$2a$08$df2HzkpTqy2mkKxm2dD76.'

または引数名を指定して次のようにします。

>>> bcrypt.gensalt(rounds = 8, prefix = b'2a')
'$2a$08$mfmnY9QvYPVwTiFnl22/8u'

ハッシュ化の回数と計算時間

ハッシュ化の回数でどれだけ計算コストが変化をするのかを実際に試しました。環境は Raspbian 8.0 Jessie on Raspberry Pi 3 Model B で、ハードウェアに手は加えていません。

$ python -m timeit -s "import bcrypt" "bcrypt.hashpw('password', bcrypt.gensalt(4, b'2a'))"
100 loops, best of 3: 3.36 msec per loop
$ python -m timeit -s "import bcrypt" "bcrypt.hashpw('password', bcrypt.gensalt(8, b'2a'))"
10 loops, best of 3: 50.2 msec per loop
$ python -m timeit -s "import bcrypt" "bcrypt.hashpw('password', bcrypt.gensalt(10, b'2a'))"
10 loops, best of 3: 200 msec per loop
$ python -m timeit -s "import bcrypt" "bcrypt.hashpw('password', bcrypt.gensalt(12, b'2a'))"
10 loops, best of 3: 800 msec per loop
$ python -m timeit -s "import bcrypt" "bcrypt.hashpw('password', bcrypt.gensalt(16, b'2a'))"
10 loops, best of 3: 12.8 sec per loop

ご覧の通り rounds = 12 でも計算に 800ms かかっており、ある程度のレスポンスが要求されるシステムでは遅すぎます。この辺りの調整は使用するハードウェアによって異なるので、実際に検証してみるしかないですね。

salt

3つ目の $ から始まる 23 文字は salt と呼ばれるもので、元のパスワードと連結させることによってよりハッシュからの復号を困難にするためのランダムな文字列です。何度も 登場している gensalt() ではこの salt を含めたヘッダを生成しています。このことは次のようなコードでわかると思います。

 $ python
Python 2.7.9 (default, Sep 17 2016, 20:26:04)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import bcrypt
>>>
>>> pw = 'This ia a test password.'
>>> salt = bcrypt.gensalt(10, b'2a')
>>> print(salt)
$2a$10$lg5X.w9U34VfZodWbtam7.
>>> hashed = bcrypt.hashpw(pw, salt)
>>> print(hashed)
$2a$10$lg5X.w9U34VfZodWbtam7.ZLnYgB8H2BosS5xAhWCeZsrWd9AKQSa

salt がランダムな文字列であることは、次のように確かめられます。

>>> bcrypt.gensalt(10, b'2a')
'$2a$10$o1hAFIp6QZSuihkklZC4RO'
>>> bcrypt.gensalt(10, b'2a')
'$2a$10$SkbMXNUw41SHN9xVhZTeY.'
>>> bcrypt.gensalt(10, b'2a')
'$2a$10$MrWxbzFaw1U47dFGYj5Hvu'
>>> bcrypt.gensalt(10, b'2a')
'$2a$10$rk9bz44EwtlOY5k33Tt5oO'
>>> bcrypt.gensalt(10, b'2a')
'$2a$10$QDiH3lpoCIqWPIs46uGYn.'

参考