$ 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.'