Tutorials

PyCharm で パスフレーズジェネレーターを作成する方法

Read this post in other languages:

このチュートリアルでは、PyCharm でパスフレーズジェネレーターを作成します。 また、以下の内容も学習します。

このチュートリアルは、無料の PyCharm Community Edition を使って日常的なタスクを自動化するシンプルな CLI アプリケーションの開発方法を説明することを目的としています。 このチュートリアルの最後には実際に動作するパスフレーズジェネレーターが完成しますが、これはあくまでも学習プロジェクトになります。 このジェネレーターが生成するパスワードを実際のデータを保護する目的で使用することは絶対におやめください。

完全なコードを取得するには、リポジトリをクローンしてください。 クローン操作についての詳細は、PyCharm のドキュメントをご覧ください。

パスフレーズについて

パスフレーズとは?

多くの人はパスワードを日常的に使用しています。 サービスやウェブサイトに登録する際には、数字、特殊文字、大文字などを使った長い一意のパスワードの作成が求められます。

このような要件はすべて、パスワードにブルートフォース攻撃への耐性を持たせるためにあります。 ブルートフォース攻撃とは、基本的には正しいパスワードが分かるまで何度もパスワードを推測して試行することです。 必要となる試行回数や試行時間は、パスワードの長さや複雑さによって決まります。

パスフレーズは複数のランダムな単語で構成されるパスワードです。 意味を持たせたり、文法的に正しくしたりする必要はありません。 パスフレーズには通常 4~5 単語が含まれており、単語数が増えるほど良いとされます。 たとえば、「PhysicianBuiltHotPotatoRegularly」はパスフレーズです。

パスフレーズが優れている理由

A^1rL#2k2oPiA9H 」は適切かつ強力なパスワードです。 小文字、大文字、数字、特殊文字が含まれており、15 文字の長さです。 とはいえ、記憶するなら「A^1rL#2k2oPiA9H」と「PhysicianBuiltHotPotatoRegularly」のどちらがいいでしょうか? ちなみに、後者は 32 文字です。

パスワードを記憶することの易しさや難しさだけでなく、解読することの易しさや難しさにも注目する必要があります。 以下の表をご覧ください。

  A^1rL#2k2oPiA9H PhysicianBuiltHotPotatoRegularly
文字の種類の数 95 52
パスワードの長さ 15 32
解読に必要な試行回数(? 298 2182

いずれも強力ではありますが、パスフレーズの方が強力であり、はるかに記憶しやすいと言えます。 しかも、パスフレーズに数字と特殊文字を数個追加すれば、必要な平均推測試行回数が 2210 に増え、実質的に解読不可能となります!

まとめ:

  • ランダムな単語で構成されるパスフレーズは、ランダムな文字で構成されるパスワードよりも記憶しやすい。
  • パスフレーズは概してほとんどのパスワードよりも長いため、ブルートフォース攻撃への耐性がより高く、結果的により安全である。
  • パスフレーズは複雑さ要件に合わせて変更可能である。 たとえば、各単語の先頭文字を大文字にして大文字を含めたり、特殊文字や数字を単語の区切り文字として追加したりすることが可能である。

パスフレーズジェネレーターとは

一般に、パスフレーズジェネレーターはランダムな単語を疑似文に組み合わせてパスワードを作成するプログラムです。 このチュートリアルでは、PyCharmTyper を使用して以下を行うコマンドラインツールを作成します。

  • ランダムな 4~5 単語で構成されるパスフレーズを生成する。
  • 対応するオプションをオンにしたときにパスフレーズの各単語の先頭を大文字にする。 デフォルトでは単語の先頭文字は大文字になりません。
  • 単語の区切り文字として記号を使用する。 デフォルトでは区切り文字は使用されません。
  • 5 番目の単語を追加して、より長いパスフレーズを作成する。 デフォルトの長さは 4 単語です。

このツールはパスワードを保存しません。

前提条件

最初のステップ

Typer で「Hello World」を書く

PyCharm を初めて起動すると「ようこそ」画面が表示されます。 New Project(新規プロジェクト)をクリックします。

PyCharm のようこそ画面

PyCharm がすでに起動している場合は、メインメニューから File| New Project(ファイル | 新規プロジェクト)を選択します。

New Project(新規プロジェクト)ウィンドウが開いたら、上部にある Location(場所)フィールドを使ってプロジェクトのディレクトリを指定します。 このディレクトリ名はプロジェクト名としても使用されます。

プロジェクトの場所を指定する

PyCharm がプロジェクトの依存関係をインストールする仮想環境のタイプも選択できます。 また、環境が作成される場所とベース Python インタープリターも選択可能です。

好みの環境タイプを選択してオプションを指定(またはデフォルトを維持)したら、Create(作成)をクリックします。

PyCharm が仮想環境(この例では venv)を含むプロジェクトディレクトリを作成します。 前の手順で Create a main.py welcome script(main.py ウェルカムスクリプトの作成)チェックボックスをオフにしていなかった場合、PyCharm は main.py も作成してエディターで開きます。

PyCharm で新規に作成されたプロジェクト

このファイルには基本的な説明が記載された「Hello World」スクリプトが含まれています。 次のコードをクリップボードにコピーします。

def main():
    print("Hello World")


if __name__ == "__main__":
    typer.run(main)

次は PyCharm に移動し、⌘A / Ctrl+A⌘V / Ctrl+V を続けて押して main.py の内容を置き換えます。 以下のようになるはずです。

Typer の「Hello World」

typer に赤い波線が付いているのがわかります。 これは、Python インタープリターが Typer が何であるかを認識していないことを意味します。 このスクリプトを起動するには、このパッケージをインストールして main.py にインポートしなければなりません。

マウスポインターをハイライトされているシンボルに合わせ、ポップアップにある Install and import package ‘typer’(パッケージ ‘typer’ のインストールとインポート)を選択します。

Typer のインストールとインポート

PyCharm が Typer パッケージをプロジェクト環境にインストールして main.py にインポートします。

これでスクリプトを実行できるようになりました。 ガターの実行アイコンをクリックし、Run ‘main’(‘main’ の実行)を選択します。

ガターアイコンを使用したスクリプトの実行

「Hello World」が表示された Run(実行)ツールウィンドウが下に表示されます。

実行ツールウィンドウ

最初のパスフレーズを生成する

このコードを変更して「Hello World」の代わりにパスフレーズが出力されるようにしましょう。 ランダムな単語を選んでフレーズを作成するようにします。 そのため、単語を選び出すための単語リストが最低 1 つは必要になります。 このようなリストは手動で準備することも、入手可能ないずれかの大規模言語モデルを使用して生成することも可能です。

単語リストを作成する際は、必ず安全に保管してください。 悪意のある人物が単語リストにアクセスした場合、わずか数秒でパスワードが解読されてしまいます。

こちらのリポジトリには、このチュートリアルに掲載しているすべてのコードと複数の単語リストが含まれています。 これらは自己責任で学習のみを目的としてダウンロードし、使用できます。 これらの単語リストに基づいて実際のパスフレーズを生成しないことを強くお勧めします。

この段階では以下の 4 つの単語リストが必要です。

  • obj_nouns.txt: 生成された疑似文で目的語として機能する名詞が含まれます。
  • sub_nouns.txt: 主語として機能する名詞が含まれます。
  • verbs.txt: 動詞が含まれます。
  • adjectives.txt: 形容詞が含まれます。

各リストの単語数が多いほど、スクリプトが生成できる組み合わせ数も多くなります。 各単語は改行を入れて追加する必要があります。

生成された、またはダウンロードした単語リストをプロジェクトのディレクトリにコピーします。 必要な場合は PyCharm で単語リストを手動で作成できます。

  1. Project(プロジェクト)ツールウィンドウでプロジェクトディレクトリをクリックし、⌘N / Ctrl+N を押します。
  2. File(ファイル)を選択し、obj_nouns.txt などのファイル名を指定します。
  3. PyCharm がファイルを作成し、エディターで開きます。

プロジェクトの構造は以下のようになります。

プロジェクト構造

まず、テキストファイルから単語を読み取る必要があります。 print("Hello World") を以下のコードに置き換えましょう。

sub_nouns = read_words('sub_nouns.txt')

ここでも read_words に赤い波線が付いています。 この関数を作成しなければなりません。 マウスポインターを read_words に合わせ、ポップアップに記載された Create function ‘read_words’(関数 ‘read_words’ の作成)をクリックします。

read_words 関数の作成

PyCharm が関数スタブを作成します。 関数のパラメーターに file_name を指定し、Tab を押して関数のコードを書き始めます。

関数パラメーターの指定

ハイライトされたコードを関数の本体にコピーできます。

def read_words(file_name):
    with open(file_name, 'r') as f:
        words = f.readlines()
    return words

この関数は、パラメーターとして指定された名前のファイルを開きます。 次に、ファイルの行を要素として含む Python のリストを返す readlines() メソッドを適用します。 このリストは words 変数に格納され、関数によって返されます。

main() 関数に戻り、新たに作成した read_words 関数を使って他の 3 つの単語リストを読み取りましょう。

def main():
    sub_nouns = read_words('sub_nouns.txt')
    verbs = read_words('verbs.txt')
    adjectives = read_words('adjectives.txt')
    obj_nouns = read_words('obj_nouns.txt')

次に、単語リストのリストを作成して word_bank と名付けます。 このリストは、後でパスフレーズに使うランダムな単語を取得する際に反復処理します。

    word_bank = [sub_nouns, verbs, adjectives, obj_nouns]

選び出されたランダムな単語をさらに別のリストに格納します。 このリストを phrase_words と名付けて初期化しましょう。

    phrase_words = []

次の for サイクルで、word_bank の項目を反復処理します。 word_bank の各項目は単語を含むリストです。 組み込みの random モジュールから SystemRandom() クラスの choice() メソッドを呼び出し、そのリストからランダムな単語を 1 つ選び出します。 次に、その単語を phrase_words に追加します。

    for word_list in word_bank:
        random_word = random.SystemRandom().choice(word_list)
        phrase_words.append(random_word)

random は組み込みのモジュールではありますが、インポートする必要があります。 これに関しては、これまでと同様にエディター内に表示される赤い波線で判断できるかと思います。 マウスポインターを合わせ、Import this name(この名前をインポート)を選択します。

最後に、join を使用してランダムに選び出された単語のリストをフレーズに変換し、その結果を出力しましょう。

    passphrase = ''.join(phrase_words)
    print(passphrase)

この段階では、main() は次のようになります。

def main():
    sub_nouns = read_words('sub_nouns.txt')
    verbs = read_words('verbs.txt')
    adjectives = read_words('adjectives.txt')
    obj_nouns = read_words('obj_nouns.txt')
    word_bank = [sub_nouns, verbs, adjectives, obj_nouns]
    phrase_words = []
    for word_list in word_bank:
        random_word = random.SystemRandom().choice(word_list)
        phrase_words.append(random_word)
    passphrase = ''.join(phrase_words)
    print(passphrase)

このスクリプトを実行して、正しく動作することを確認しましょう。 ガターの Run(実行)アイコンをクリックし、Run ‘main’(‘main’ の実行)を選択すると、以下のようになります。

スクリプトの初回実行

4 つの単語がありますが、明らかにフレーズではありません。 コードが大まかに動いているのに予期しない結果になるなら、デバッグする必要があります。

現在の出力からわかるように、このスクリプトは単語リストからランダムに単語を選び出せています。 うまくいっていないのは、単語を 1 つのフレーズに組み合わせる部分です。 main() の下から 2 行目が怪しそうです。

def main():
    ...
    passphrase = ''.join(phrase_words)
    print(passphrase)

コードの特定の行が生成する結果を確認するため、その行にブレークポイントを置きましょう。 デバッガーはブレークポイントのある行を実行する直前に停止します。 ブレークポイントを設定するには、確認したい行の横のガターをクリックします。

ブレークポイントの設定

デバッグプロセスを開始するには、前に行ったようにガターの Run(実行)アイコンをクリックしますが、今回は、ポップアップの Debug ‘main’(‘main’ のデバッグ)を選択します。 デバッガーが起動してブレークポイントで実行が停止すると、Debug(デバッグ)ツールウィンドウが下に開きます。

デバッガーツールウィンドウ

Debug(デバッグ)ツールウィンドウの右側のペインでは、その時点までに代入された変数を確認できます。 phrase_words を展開して中身を確認しましょう。

phrase_words 変数の調査

リストには str 型の項目が 4 個あります。 各文字列は改行(‘\n’)で終了しています。 後でこれらの文字列を結合して出力する際、各単語が個別の行に出力されてしまうのはこれが原因です。

adjectives などの他のリストを見てみると、そこでもすべての項目が ‘\n’ で終了しているのがわかります。 これらのリストは read_words 関数から取得しています。 そのため、末尾の ‘\n’ を付けずに単語のリストを返すようにその関数を修正する必要があります。

strip() とリスト内包表記を使用して、リストの各項目を返す前に ‘\n’ を取り除くようにしましょう。

def read_words(file_name):
    with open(file_name, 'r') as f:
        words = f.readlines()
        words = [word.strip() for word in words]
    return words

main() に戻り、結果を見てみましょう。

修正後のスクリプトの実行

より優れたパスフレーズを作成する

記憶しやすくする

お気づきかもしれませんが、前のステップで生成されたパスフレーズには若干の読みにくさがあります。 各単語の先頭文字を大文字にして、可読性を改善してみてはどうでしょうか?

このアプリケーションでは、単にスクリプトを実行してパスフレーズを取得したくないため、Typer を使用しています。 このスクリプトにさまざまなオプションを渡して生成されるパスフレーズのプロパティを制御できるよう、コマンドラインインターフェースを作成する必要があります。 このようなオプションの 1 つが単語の先頭文字の大文字化です。

main.py を実行すると、次の行が実行されます。

...
if __name__ == "__main__":
    typer.run(main)

このように、typer を使用して main 関数を実行します。 このようにしているのは、Typer がコマンドライン引数を受け入れ、それを関数にパラメーターとして渡せるためです。

main() 関数に capitalize パラメーターを導入し、デフォルトで False にしましょう。

def main(capitalize = False):
    sub_nouns = read_words('sub_nouns.txt')
    ...
    passphrase = ''.join(phrase_words)
    print(passphrase)

Python コーディングのベストプラクティスによると、パラメーターの型を指定する必要があります。 capitalize にキャレットを置き、⌥Enter/ Alt+Enter を押します。 そして、Specify type for the reference using annotation(アノテーションを使用して参照する型を指定)を選択します。 capitalize にはブール値が必要であるため、bool と入力します。

では、capitalizeTrue の場合に、単語を結合する前に先頭文字を大文字にしてみましょう。 if capitalize: を使用する if ステートメントから始めます。 read_words 関数を修正した時と同じような方法で行いますが、今回はリスト内包表記を手動で書かずにライブテンプレートを使用します。 compl と入力して Enter を押します。 次に、リスト内包表記のすべての要素を指定し、Tab を押して次の要素に移動します。

以下のようになります。

def main(capitalize: bool = False):
    sub_nouns = read_words('sub_nouns.txt')
    verbs = read_words('verbs.txt')
    adjectives = read_words('adjectives.txt')
    obj_nouns = read_words('obj_nouns.txt')
    word_bank = [sub_nouns, verbs, adjectives, obj_nouns]
    phrase_words = []
    for word_list in word_bank:
        random_word = random.SystemRandom().choice(word_list)
        phrase_words.append(random_word)
    if capitalize:
        phrase_words = [phrase_word.capitalize() for phrase_word in phrase_words]
    passphrase = ''.join(phrase_words)
    print(passphrase)

先頭文字の大文字化を正しく動作させるため、main.py を実行する際に capitalize を引数として渡す必要があります。 これを行うため、実行構成を編集しましょう。 IDE ウィンドウの上部にある Run(実行)ウィジェットを見つけましょう。

PyCharm の実行ウィジェット

このウィジェットは、希望する実行構成の選択と、実行またはデバッグモードでの起動を選択するのに使用できます。 PyCharm ではガターアイコンをクリックしてスクリプトを起動した時点ですでに main 構成が作成されています。 その構成の名前をクリックしてメニューを開き、Edit configurations(構成の編集)を選択します。

実行/デバッグ構成の編集

開いたダイアログで、Parameters(パラメーター)フィールドに --capitalize を指定します。

パラメーターを構成に追加

OK をクリックして、更新された構成を保存します。 次に、ウィジェットの実行アイコンをクリックします。

結果は以下のようになります。

‘capitalize’ オプションを含むスクリプトの実行

可読性を高めるため、パスフレーズの単語を区切ることができます。 特殊文字を区切り文字として使用すると、特定のパスワード複雑さ要件に合ったパスフレーズを生成できるため、2 つの目的を達成できます。

main() 関数の下から 2 行目を以下のように編集します。

def main(capitalize: bool = False):
    ...
    passphrase = separator.join(phrase_words)

この関数には separator パラメーターがまだ存在しないため、PyCharm はこのパラメーターを赤い波線でハイライトします。 その波線にマウスポインターを合わせ、ポップアップの Create parameter ‘separator’(パラメーター ‘separator’ の作成)を選択します。 デフォルトでは区切り文字を追加しないため、‘’ をデフォルト値に指定します。

また、パラメーターの型に str を指定しておきましょう。 以下のようになります。

def main(capitalize: bool = False, separator: str = ''):
    ...
    passphrase = separator.join(phrase_words)
    print(passphrase)

さあ、結果を確認したくなったはずです。 その前に実行構成を更新することを忘れないでください。 Run(実行)ウィジェットをクリックし、Edit configurations(構成の編集)を選択して、Parameters(パラメーター)フィールドに新しいパラメーターを追加します。

別のパラメーターを実行構成に追加

構成を実行して結果を確認します。

‘separator’ オプションを含むスクリプトの実行

特殊文字と数字でパスフレーズの単語が区切られるようになりました。 さらに、“#4” や “%7” などの複数の記号を使用し、さまざまなウェブサイトが指定するパスワード要件も満たすことができます。

さらに解読しにくくする

パスフレーズが長いほど、ブルートフォース攻撃を成功させるために必要な試行回数も増えます。 パスフレーズに単語をもう 1 つ含められるようにしましょう。

まずは副詞を含む 5 番目の単語リストを準備し、プロジェクトのディレクトリに配置します。 main() 関数のシグネチャーにブール型の long パラメーターを追加します。 このパラメーター(オプションかつデフォルトでは False)に応じて word_bank に単語リストがもう 1 つ追加されます。

def main(capitalize: bool = False, separator: str = '', long: bool = False):
    ...
    word_bank = [sub_nouns, verbs, adjectives, obj_nouns]
    if long:
        adverbs = read_words('adverbs.txt')
        word_bank.append(adverbs)
	...

今回は組み込みのターミナルを使ってスクリプトを実行してみましょう。 ⌥F12/ Alt+F12 を押して開いた Terminal(ターミナル)ツールウィンドウに以下のコマンドを入力します。

python main.py --capitalize --separator "1_" --long

以下のように表示されます。

‘long’ パラメーターを含むスクリプトの実行

より実用的なツールにするための変更

短いオプション名を定義する

CLI ツールを使用した経験のある方は、通常は 1 文字だけで引数を指定できることをご存知でしょう。 このツールにもその機能を追加しましょう。 コードの可読性を高めるため、関数シグネチャーを整形して各パラメーターを別々の行に分けましょう。

def main(
    capitalize: bool = False,
    separator: str = '',
    long: bool = False
):
...

次に、各パラメーターのデフォルト値を typer.Option(, , ) に置き換えます。

main() のシグネチャーは最終的に以下のようになります。

def main(
    capitalize: bool = typer.Option(False, '--caps', '-c'),
    separator: str = typer.Option('', '--separator', '-s'),
    long: bool = typer.Option(False, '--long', '-l')
):
...

すべてのオプションをまとめて指定できるようになりました。 区切り文字(‘-s’)は後に文字列を書く必要があるため、最後に配置すべきです。

すべてのオプションを含むスクリプトの実行

オプションのドキュメントを書く

Typer はデフォルトで --help オプションも追加します。 現時点でどのように機能するか見てみましょう。

‘--help’ オプションを含むスクリプトの実行

どんなパラメーターが存在し、その正式な名前と短い形式の名前が何であるかがわかります。 実際の動作を説明するコメントを追加してはどうでしょうか? 以下のように、main() の各パラメーターに help を追加します。

def main(
    capitalize: bool = typer.Option(False, '--caps', '-c', help='Capitalize each word.'),
    separator: str = typer.Option('', '--separator', '-s', help='Separate words with the given symbol.'),
    long: bool = typer.Option(False, '--long', '-l', help='Make the passphrase longer by including an adverb.')
):
...

--help がより有用な情報を表示するようになりました。

スクリプトの help に有用な情報を表示

PyCharm を使わずにシステムのターミナルなどでパスフレーズジェネレーターを使用することもできます。 その場合は、以下のコマンドを使用してシステムインタープリターに Typer をインストールする必要があります。

python3 -m pip install --user typer

まとめ

このチュートリアルでは以下について学習しました。

  • PyCharm Community Edition でのプロジェクトの作成
  • Python と Typer による使いやすい CLI ツールの作成
  • エラーを回避しながらより迅速にコードを書くためのクイックフィックスとライブテンプレートの使用
  • コードのデバッグ
  • 実行構成とターミナルを使用した PyCharm でのコードの実行

オリジナル(英語)ブログ投稿記事の作者:

Ryuji Owan

Denis Mashutin

image description