Pythonでパスワードの強度をしらべるライブラリSafeのコードリーディング


Safeはパスワードをの強度をしらべるためのライブラリです。

https://pypi.python.org/pypi/Safe/
https://github.com/lepture/safe


# INSTALL 

pipでインストールできます。

```shell-command
$ pip install safe
```

# READMEのsampleコードを試す

とりあえず `safe` をimportしておきます。

```py
>>> import safe
```

`safe.check()` 関数で強度をしらべます。

```py
>>> safe.check(1)
terrible
>>> safe.check('password')
simple
>>> safe.check('is.safe')
terrible
>>> safe.check('x*V-92Ba')
strong
```

返値はsafe.Strength()のインスタンスです。

```py
>>> type(safe.check('x*V-92Ba'))
<class 'safe.Strength'>
```

結果はbool()に変換できます。

```
>>> bool(safe.check('x*V-92Ba'))
True
>>> bool(safe.check('a'))
False
```


# コードリーディング

コードを読んでいきます。

## safe.check() - パスワードの強度をチェックする
https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L147-L189

`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](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(数字/小文字)です。


```py
>>> 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()に渡ってしまいます。

```py
    if is_asdf(raw) or is_by_step(raw):
        return Strength(False, 'simple', 'password has a pattern')
```

https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L163-L164

とはいえ1文字のチェックを許可するケースなんて稀なのであんまり神経質にならなくてもいいところな気もします。

## safe.is_asdf() - キーボードの並び順かどうかを判定する

https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L72-L83

与えられた文字列がキーボードの並び順になっているかどうかを判定します。
ここで並び順と言っているものは次の箇所で定義された文字列のことです。
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() - シフト暗号っぽい文字列かどうかを判定する

https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L86-L95

シフト暗号とは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() - よく用いられるパスワードかどうかを判定する

https://github.com/lepture/safe/blob/cb554022df8be8a4f7fa0b8fbee39639b4819495/safe/__init__.py#L98-L106

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行ぐらいかな