Safeはパスワードをの強度をしらべるためのライブラリです。
https://pypi.python.org/pypi/Safe/
https://github.com/lepture/safe
INSTALL
pipでインストールできます。
$ pip install safe
READMEのsampleコードを試す
とりあえず safe
をimportしておきます。
>>> import safe
safe.check()
関数で強度をしらべます。
>>> safe.check(1)
terrible
>>> safe.check('password')
simple
>>> safe.check('is.safe')
terrible
>>> safe.check('x*V-92Ba')
strong
返値はsafe.Strength()のインスタンスです。
>>> type(safe.check('x*V-92Ba'))
<class 'safe.Strength'>
結果はbool()に変換できます。
>>> bool(safe.check('x*V-92Ba'))
True
>>> bool(safe.check('a'))
False
コードリーディング
コードを読んでいきます。
safe.check() - パスワードの強度をチェックする
safe.check()
関数はパスワードの強度を調べます。
引数
raw
, length
, freq
, min_types
, level
を引数に渡せます。
raw
は調べたいパスワードの文字列です。通常はstrを渡すのですが、関数ないでto_unicode()しており、intを渡してもstrに変換して処理したりします。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L156
to_unicode()は_compat.pyで定義されています。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/_compat.py#L28-L38
length
は必要なパスワード長です。freq
はよくあるパスワードをしらべるときに使います。
min_types
は文字の種類の数がいくつ含まれているべきかを指定します。例えばmin_types=3でlength=3の場合1qA
はOK(数字/小文字/大文字)、1qa
はNG(数字/小文字)です。
>>> bool(safe.check('1qA', length=3, min_types=3))
True
>>> bool(safe.check('1qa', length=3, min_types=3))
False
level
はそのパスワードの強度がvalidがinvalidかどうかの閾値です。入力値はint/floatが許容されそうですが、STRONG以上の数値を指定してもSTRONGに戻されます(そうしないと常にinvalidになってしまいますからね...)。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L157-L158
levelは次のように定義されています。
TERRIBLE = 0
SIMPLE = 1
MEDIUM = 2
STRONG = 3
返値
戻り値はsafe.Strength
のインスタンスです。safe.Strength
はvalid
、strength
、message
の属性を持っています。
valid
はbool値です。Trueがvalidです。strength
は強度を示す文字列です。strengthに入る文字列は次のいずれかです。
streangthに用いられる文字列 | 意味 |
---|---|
strong | つよい |
medium | まあまあ |
simple | 簡単 |
terrible | 鼻クソ(意訳) |
(ちなみにこの文字列はsafe.Strength
のインスタンス生成時のコードにハードコーディングされちゃっていますorz)
message
はなぜそう判断されたかを表すメッセージです。
この関数は例えば1文字でsafe.is_asdf()に引っかかるような文字列を渡すと正しく結果を返します。
>>> safe.check('a', length=0, min_types=0)
simple
しかし1文字でsafe.is_asdf()に引っかからないような文字列を渡すとsafe.is_by_step()が送出した例外をハンドリングせず流してしまいます。
>>> safe.check('(', length=0, min_types=0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/sximada/ng/env/lib/python3.4/site-packages/safe/__init__.py", line 163, in check
if is_asdf(raw) or is_by_step(raw):
File "/home/sximada/ng/env/lib/python3.4/site-packages/safe/__init__.py", line 89, in is_by_step
delta = ord(raw[1]) - ord(raw[0])
IndexError: string index out of range
それは条件チェックの時に正しくエラーハンドリングされていないからです。
safe.is_by_step()は後述しますが1文字の文字列を渡す時はIndexError例外を送出してきます。safe.is_asdf()がTrueにならない文字列(たとえば(
)の場合は、1文字(
がsafe.is_by_step()に渡ってしまいます。
if is_asdf(raw) or is_by_step(raw):
return Strength(False, 'simple', 'password has a pattern')
とはいえ1文字のチェックを許可するケースなんて稀なのであんまり神経質にならなくてもいいところな気もします。
safe.is_asdf() - キーボードの並び順かどうかを判定する
与えられた文字列がキーボードの並び順になっているかどうかを判定します。
ここで並び順と言っているものは次の箇所で定義された文字列のことです。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L69
そのため全てのキーボードの並び順のパターンを検査しているわけではありません。
(ちょっと少なすぎじゃね?)
引数
raw
は判定する文字列です。raw
で指定した文字列とその並び順を反対にした文字列が判定の対象になります。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L75
戻り値
Bool値です。キーボードの並び順になっている場合、Trueを返します。
safe.is_by_step() - シフト暗号っぽい文字列かどうかを判定する
シフト暗号とはn文字ずつずらすという古典的で愛すべき(でも絶対使っちゃいけない)暗号方式です。中でもカエサル暗号は有名で3文字づつずらすというものです。例えばtest
という文字列を3文字づつずらすとwhvw
という文字列ができます。このような文字列はあっというまにクラックされるので、隠した気になるだけで、何の意味もないです。
この関数ではそういう文字列かどうかを判定しています。
指定した文字列raw
の1文字目と0文字目の距離を測っています。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L89
その後全ての文字がその距離通りにシフトされているかどうかを判定しています。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L91-L95
長さ1の文字列を渡すとIndexErrorを送出してきます。
>>> safe.is_by_step('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/sximada/ng/env/lib/python3.4/site-packages/safe/__init__.py", line 89, in is_by_step
delta = ord(raw[1]) - ord(raw[0])
IndexError: string index out of range
引数
raw
は判定する文字列です。
返値
bool値です。シフト暗号っぽい文字列ならTrueです。
safe.is_common_password() - よく用いられるパスワードかどうかを判定する
docstringによると https://xato.net/passwords/more-top-worst-passwords/ にあるよくあるパスワード1000を用いているという記述がありますが、残念なことにこのURLは404 Not Foundでした(おい!!)。
よく使われるパスワードのリストはpackage内に内包されています。デフォルトではこのリストが使われます。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/words.dat
このファイルは次のようなフォーマットをしています。
文字列 頻出レベル
頻出レベルとは判定するために使われる整数で値が大きいほどよく使われることを表します。
このファイルはカスタマイズしたファイルを用いることもできます。その場合は環境変数PYTHON_SAFE_WORDS_FILE
にカスタマイズしたファイルのパスを指定すればそちらを使います。
このファイルのデータは使用時には {'文字列': 整数, ...}
の形式のdictとして使用されます。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L58-L61
その後、そのdictをキャッシュするのですが、そのキャッシュファイルの場所を環境変数PYTHON_SAFE_WORDS_CACHE
で指定できます。
このデータはsafeパッケージをimportする際にファイルをreadしてsafe.WORDSにそのデータを保持します。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L68
そのためPYTHON_SAFE_WORDS_FILE
を指定していて、そのファイルがない場合はFileNotFoundErrorが送出され、importに失敗するためるでしょう(若干辛い仕様)。
引数
raw
は判定する文字列です。
freq
はよく使われるレベルという感じです。数字が大きいほどよく使われることを表します。
返値
bool値です。よく使われる文字列の場合はTrueです。引数freq
で指定した数値以下の頻出レベルの場合はFalseです。よく使われる文字列に該当しない場合もFalseです。
safe.safety() - 公開されていないけど公開utilityっぽい関数
safe.safety()はsafe.check()をcallしてその戻り値を返すだけの関数です。
両者の違いは引数のデフォルト値です。safe.check()のmin_typesが3なのに対して、safe.safety()は2です。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L192-L193
しかしこの関数は__all__の中には含まれていません。そしてpackageの内部でも使っていません。
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L20-L23
含め忘れなのか、それとも廃止していきたいという作者の意図があってあえて入れていないのか(そうだとしてもそういうやり方はして欲しくないけど...)、何れにしてもこの関数は使わずsafe.check()を使って行ったほうがよさそうです。(ちなみにこの作者は_compat.pyでも__all__を割と律儀に書いているので、書き忘れ臭がします。)
あとがき
このライブラリは200行程度しかない小さなライブラリです。
シンプルにできているので使い勝手はとても良さそうでした。
この記事を書くのに2時間程度使いました。 -> 1時間100行ぐらいかな