パスワードマネージャー
パスワードを暗号化して保存する仕組みを学ぶ学習用パスワードマネージャー。cryptographyライブラリでAES暗号化を学びます。
1. アプリ概要
パスワードを暗号化して保存する仕組みを学ぶ学習用パスワードマネージャー。cryptographyライブラリでAES暗号化を学びます。
Fernet の暗号鍵は、マスターパスワードを PBKDF2HMAC(ハッシュ SHA-256・鍵長 32 バイト・ソルト os.urandom(16)=128 ビットのランダム値・反復回数 1,200,000 回)に通して導出しています。この 4 つのパラメータは cryptography 公式ドキュメントの PBKDF2HMAC の例と同じ値です。出典: cryptography 公式ドキュメント(Key derivation functions)。ソルトは初回セットアップ時に生成して passwords.key に kdf_salt として保存するので、同じマスターパスワードでもソルトが違えば別の鍵になります(当サイトで実行して確認しました)。反復回数を 1,200,000 回にすると導出に時間がかかり、手元の Windows 11・Python 3.12・cryptography 48.0.0 では 1 回あたり 0.2〜0.4 秒でした。ログインのたびにこの待ちが入るので、ステータス欄に「鍵を導出しています…」と表示しています。
それでもこのコードが実運用の基準を満たすとは言えません。弱点が残っているからです。ログイン照合用のハッシュはソルト付きの SHA-256 を 1 回通しただけで、鍵ファイルを持ち出した攻撃者は、反復回数の重い PBKDF2 を通らずにこの高速なハッシュ側からマスターパスワードを総当たりできます。ほかにも、暗号化データと鍵ファイルを同じフォルダに置いている、入力後のマスターパスワードをメモリから確実に消す処理・クリップボードの自動クリア・一定時間で自動ロックする仕組みが無い、第三者の監査を受けていない、といった点が学習用のままです。
このコードをそのまま本番のパスワード管理に使わないでください。実際にパスワードを保管するときは、実績のある専用パスワードマネージャーをご利用ください。
旧バージョン(ソルト無しの SHA-256 で鍵を作っていた版)の passwords.key が残っている場合、その保存データは新しい鍵導出では復号できません。アプリは鍵ファイルの version を見て「旧形式のデータです」と案内して停止します。passwords.key と passwords.enc を削除してから、マスターパスワードを作り直してください(旧データを読み替える移行機能はありません)。削除すると保存済みのパスワードは復元できません。必要な項目は、手元に残っている旧バージョンの app11.py(このページで配布しているファイル名)で先にログインして控えてから削除してください。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・cryptography で、難易度は ★★★ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ)・cryptography を活かして実装するこの構造は、他のアプリにも応用が効きます。
コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。
アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。
2. 機能一覧
- ループで生成するボタン群を
_toggle_pwd()に接続 - 「🔒 ロック」ボタン(
_lock())・ボタン(_authenticate())で操作 - キー
<Return>から_authenticate()を実行 - イベント
<<ListboxSelect>>から_on_select()を実行 tk.Listboxによる一覧表示(スクロールバー付き)- JSON ファイルの保存・読み込み(起動時に自動実行)
messagebox.askyesno()・messagebox.showerror()・messagebox.showwarning()によるダイアログ通知- ウィンドウはタイトル「パスワードマネージャー」・サイズ 780x540 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import hmac
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class InvalidToken(Exception):
"""cryptography 未インストール時の代替(復号を行わないので発生しない)"""
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
KEY_VERSION = 2 # 鍵ファイルの形式(1 = ソルト無しの旧形式・復号不可)
KDF_ITERATIONS = 1200000 # cryptography 公式ドキュメントの例と同じ反復回数
KDF_SALT_BYTES = 16 # 128 ビット(公式ドキュメントが示すソルトの下限)
def __init__(self, root):
self.root = root
self.root.title("パスワードマネージャー")
self.root.geometry("780x540")
self.root.configure(bg="#f8f9fc")
self.fernet = None
self.entries = []
self.master_hash = None
self._build_ui()
self._check_crypto()
def _check_crypto(self):
if not CRYPTO_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"cryptography が必要です。\n"
"pip install cryptography でインストールしてください。\n\n"
"デモモード(暗号化なし)で動作します。")
self._show_login()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#c62828", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🔒 パスワードマネージャー",
font=("Noto Sans JP", 15, "bold"),
bg="#c62828", fg="white").pack(side=tk.LEFT, padx=12)
self.lock_btn = ttk.Button(title_frame, text="🔒 ロック",
command=self._lock)
self.lock_btn.pack(side=tk.RIGHT, padx=12)
# メインコンテンツ
self.main_frame = tk.Frame(self.root, bg="#f8f9fc")
self.main_frame.pack(fill=tk.BOTH, expand=True)
# ログイン/セットアップフレーム
self.login_frame = tk.Frame(self.main_frame, bg="#f8f9fc")
# パスワード一覧フレーム
self.list_frame = tk.Frame(self.main_frame, bg="#f8f9fc")
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
def _show_login(self):
self.list_frame.pack_forget()
self.login_frame.pack(fill=tk.BOTH, expand=True)
for w in self.login_frame.winfo_children():
w.destroy()
c = tk.Frame(self.login_frame, bg="#f8f9fc")
c.place(relx=0.5, rely=0.4, anchor="center")
has_data = os.path.exists(self.KEY_FILE)
title = "マスターパスワードを入力" if has_data else "マスターパスワードを設定"
tk.Label(c, text=title, font=("Noto Sans JP", 14, "bold"),
bg="#f8f9fc").pack(pady=8)
frame = tk.Frame(c, bg="#f8f9fc")
frame.pack()
tk.Label(frame, text="マスターパスワード:",
bg="#f8f9fc").grid(row=0, column=0, sticky="w", pady=4)
self.master_entry = ttk.Entry(frame, show="●", width=24,
font=("Arial", 12))
self.master_entry.grid(row=0, column=1, padx=8)
if not has_data:
tk.Label(frame, text="確認用:",
bg="#f8f9fc").grid(row=1, column=0, sticky="w", pady=4)
self.master_confirm = ttk.Entry(frame, show="●", width=24,
font=("Arial", 12))
self.master_confirm.grid(row=1, column=1, padx=8)
else:
self.master_confirm = None
btn_text = "ログイン" if has_data else "作成"
ttk.Button(c, text=btn_text,
command=self._authenticate).pack(pady=12)
self.master_entry.bind("<Return>", lambda e: self._authenticate())
self.master_entry.focus_set()
def _derive_key(self, password, kdf_salt):
"""マスターパスワードとソルトから Fernet 用の鍵(32バイト)を導出する。
パラメータは cryptography 公式ドキュメント(Key derivation functions)
の PBKDF2HMAC の例に合わせている。
https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/
"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=kdf_salt,
iterations=self.KDF_ITERATIONS,
)
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
def _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
try:
with open(self.KEY_FILE, "r", encoding="utf-8") as f:
stored = json.load(f)
except (OSError, ValueError):
messagebox.showerror(
"エラー", "鍵ファイルを読み込めませんでした")
return
if stored.get("version") != self.KEY_VERSION:
messagebox.showerror(
"旧形式のデータです",
"この鍵ファイルは旧バージョン(ソルト無しの鍵導出)で"
"作られています。\n"
"現在の鍵導出方式では保存データを復号できません。\n"
"passwords.key と passwords.enc を削除してから、"
"マスターパスワードを作り直してください。\n"
"削除すると保存済みのパスワードは復元できません。"
"必要なら、手元に残っている旧バージョンの"
"app11.pyで先にログインして控えてから削除して"
"ください。")
return
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if not hmac.compare_digest(hashed, stored["hash"]):
messagebox.showerror("エラー", "パスワードが正しくありません")
return
try:
kdf_salt = bytes.fromhex(stored["kdf_salt"])
except (KeyError, ValueError):
messagebox.showerror(
"エラー", "鍵ファイルのソルトが読み取れません")
return
else:
# 新規作成
if self.master_confirm and pw != self.master_confirm.get():
messagebox.showerror("エラー", "パスワードが一致しません")
return
salt = secrets.token_hex(16)
hashed = hashlib.sha256((pw + salt).encode()).hexdigest()
kdf_salt = os.urandom(self.KDF_SALT_BYTES)
with open(self.KEY_FILE, "w", encoding="utf-8") as f:
json.dump({"version": self.KEY_VERSION, "hash": hashed,
"salt": salt, "kdf_salt": kdf_salt.hex(),
"kdf_iterations": self.KDF_ITERATIONS}, f)
if CRYPTO_AVAILABLE:
self.status_var.set("鍵を導出しています…")
self.root.update_idletasks()
self.fernet = Fernet(self._derive_key(pw, kdf_salt))
del pw
self.master_entry.delete(0, tk.END)
if self.master_confirm:
self.master_confirm.delete(0, tk.END)
if not self._load_passwords():
self.status_var.set("")
return
self._show_manager()
def _show_manager(self):
self.login_frame.pack_forget()
self.list_frame.pack(fill=tk.BOTH, expand=True)
for w in self.list_frame.winfo_children():
w.destroy()
paned = ttk.PanedWindow(self.list_frame, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 一覧
left = tk.Frame(paned, bg="#f8f9fc")
tk.Label(left, text="🔍 検索:").pack(anchor="w", padx=8)
self.search_var = tk.StringVar()
search_entry = ttk.Entry(left, textvariable=self.search_var, width=26)
search_entry.pack(fill=tk.X, padx=8, pady=2)
self.search_var.trace_add("write", lambda *a: self._refresh_list())
self.lb = tk.Listbox(left, font=("Arial", 11),
selectbackground="#c62828",
selectforeground="white")
sb = ttk.Scrollbar(left, command=self.lb.yview)
self.lb.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.lb.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
self.lb.bind("<<ListboxSelect>>", self._on_select)
paned.add(left, weight=1)
# 右: 詳細・編集
right = ttk.LabelFrame(paned, text="詳細・編集", padding=12)
for lbl, attr in [("サービス名", "svc"), ("ユーザー名", "usr"),
("パスワード", "pwd"), ("URL", "url"),
("メモ", "memo")]:
tk.Label(right, text=f"{lbl}:").pack(anchor="w")
if lbl == "メモ":
var = tk.Text(right, height=3, width=28, font=("Arial", 11))
var.pack(fill=tk.X, pady=2)
setattr(self, f"{attr}_text", var)
else:
var = tk.StringVar()
entry = ttk.Entry(right, textvariable=var, width=28,
show="●" if lbl == "パスワード" else "")
entry.pack(fill=tk.X, pady=2)
setattr(self, f"{attr}_var", var)
if lbl == "パスワード":
self.pwd_entry = entry
btn_f = tk.Frame(right, bg=ttk.Style().lookup("TLabelframe", "background"))
btn_f.pack(fill=tk.X)
ttk.Button(btn_f, text="👁 表示",
command=self._toggle_pwd).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_f, text="🎲 生成",
command=self._gen_password).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_f, text="📋 コピー",
command=self._copy_pwd).pack(side=tk.LEFT, padx=2)
btn_frame = tk.Frame(right, bg=ttk.Style().lookup("TLabelframe", "background"))
btn_frame.pack(fill=tk.X, pady=8)
for text, cmd in [("➕ 追加", self._add_entry),
("✏️ 更新", self._update_entry),
("🗑️ 削除", self._delete_entry)]:
ttk.Button(btn_frame, text=text, command=cmd).pack(
side=tk.LEFT, padx=4)
paned.add(right, weight=1)
self._refresh_list()
self.status_var.set(f"{len(self.entries)} 件")
def _load_passwords(self):
"""保存データを読み込む。成功したら True を返す。"""
self.entries = []
if not os.path.exists(self.DATA_FILE):
return True
try:
with open(self.DATA_FILE, "rb") as f:
raw = f.read()
if CRYPTO_AVAILABLE and self.fernet:
raw = self.fernet.decrypt(raw)
self.entries = json.loads(raw)
except InvalidToken:
self.fernet = None
messagebox.showerror(
"復号できません",
"保存データを復号できませんでした。\n"
"鍵ファイル(passwords.key)と暗号化データ(passwords.enc)の"
"組み合わせが違う可能性があります。")
return False
except (OSError, ValueError):
messagebox.showerror(
"読み込みエラー", "保存データを読み込めませんでした")
return False
return True
def _save_passwords(self):
raw = json.dumps(self.entries, ensure_ascii=False).encode()
if CRYPTO_AVAILABLE and self.fernet:
raw = self.fernet.encrypt(raw)
with open(self.DATA_FILE, "wb") as f:
f.write(raw)
def _refresh_list(self):
q = self.search_var.get().lower()
self.lb.delete(0, tk.END)
self._filtered = [e for e in self.entries
if q in e.get("svc", "").lower()
or q in e.get("usr", "").lower()]
for e in self._filtered:
self.lb.insert(tk.END, f" {e.get('svc', '')} ({e.get('usr', '')})")
def _on_select(self, event):
sel = self.lb.curselection()
if not sel:
return
idx = sel[0]
if idx < len(self._filtered):
e = self._filtered[idx]
self.svc_var.set(e.get("svc", ""))
self.usr_var.set(e.get("usr", ""))
self.pwd_var.set(e.get("pwd", ""))
self.url_var.set(e.get("url", ""))
self.memo_text.delete("1.0", tk.END)
self.memo_text.insert("1.0", e.get("memo", ""))
self._selected_idx = self.entries.index(e)
def _add_entry(self):
svc = self.svc_var.get().strip()
if not svc:
messagebox.showwarning("警告", "サービス名を入力してください")
return
entry = {
"svc": svc, "usr": self.usr_var.get(),
"pwd": self.pwd_var.get(), "url": self.url_var.get(),
"memo": self.memo_text.get("1.0", tk.END).strip()
}
self.entries.append(entry)
self._save_passwords()
self._refresh_list()
self.status_var.set(f"{len(self.entries)} 件")
def _update_entry(self):
if not hasattr(self, "_selected_idx"):
return
self.entries[self._selected_idx] = {
"svc": self.svc_var.get(), "usr": self.usr_var.get(),
"pwd": self.pwd_var.get(), "url": self.url_var.get(),
"memo": self.memo_text.get("1.0", tk.END).strip()
}
self._save_passwords()
self._refresh_list()
def _delete_entry(self):
if not hasattr(self, "_selected_idx"):
return
if messagebox.askyesno("確認", "このエントリを削除しますか?"):
self.entries.pop(self._selected_idx)
self._save_passwords()
self._refresh_list()
self.status_var.set(f"{len(self.entries)} 件")
def _toggle_pwd(self):
show = self.pwd_entry.cget("show")
self.pwd_entry.config(show="" if show else "●")
def _gen_password(self):
chars = string.ascii_letters + string.digits + "!@#$%&"
pwd = "".join(secrets.choice(chars) for _ in range(16))
self.pwd_var.set(pwd)
def _copy_pwd(self):
self.root.clipboard_clear()
self.root.clipboard_append(self.pwd_var.get())
self.status_var.set("パスワードをクリップボードにコピーしました")
def _lock(self):
self.fernet = None
self.entries = []
self._show_login()
if __name__ == "__main__":
root = tk.Tk()
app = App11(root)
root.mainloop()
5. コード解説
パスワードマネージャーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App11 クラスにアプリの全機能をまとめています(メソッド18個・全375行)。__init__ ではタイトル「パスワードマネージャー」とウィンドウサイズ 780x540 を設定します。あわせて状態を保持する変数(self.fernet)を初期化し、そのあと _build_ui() → _check_crypto() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×8(ほかにループでも生成)・tk.Label×6(ほかにループでも生成)・ttk.Button×2(ほかにループでも生成)・ttk.Entry×3(ほかにループでも生成)・ttk.PanedWindow・tk.Listbox・ttk.Scrollbar・ttk.LabelFrame・tk.Text(ループで生成)・ttk.Style(ループで生成) で構成しています。見出し付きの枠(LabelFrame)は「詳細・編集」のラベルでエリアを区切っています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「🔒 ロック」ボタン(_lock())・ボタン(_authenticate())を command= で接続しています。ボタンはループ内で生成し、_toggle_pwd() に接続しています。また bind("<Return>", ...) から _authenticate()、bind("<<ListboxSelect>>", ...) から _on_select() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
JSON への保存と読み込み
データは json.dump() でファイルへ保存します。起動時は json.load() で読み戻します。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:DATA_FILE = os.path.join(os.path.dirname(__file__), 'passwords.enc')、KEY_FILE = os.path.join(os.path.dirname(__file__), 'passwords.key')。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で ImportError・InvalidToken・KeyError・OSError・ValueError を捕捉しています。あわせて messagebox.askyesno()・messagebox.showerror()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app11.py と保存します。外部ライブラリ
cryptographyを使います。先にpip install cryptographyを実行してください。 -
2クラスの骨格を作る
App11クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("パスワードマネージャー")・geometry("780x540")・configure(bg="#f8f9fc")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×8(ほかにループでも生成)・tk.Label×6(ほかにループでも生成)・ttk.Button×2(ほかにループでも生成)・ttk.Entry×3(ほかにループでも生成)・ttk.PanedWindow・tk.Listbox・ttk.Scrollbar・ttk.LabelFrame・tk.Text(ループで生成)・ttk.Style(ループで生成) を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_lock()・_authenticate())につなぎます。ループ生成のボタンはcommand=self._toggle_pwdの形で接続します。bind("<Return>", ...)・bind("<<ListboxSelect>>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_authenticate()(65行)・_on_select()(14行)・_add_entry()(14行)など を実装します。 -
7動作確認する
python app11.pyで起動し、各ボタンが反応すること・キー操作(<Return>)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(DATA_FILE・KEY_FILE)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ <Return> キーを押しても反応しない
原因:bind("<Return>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("780x540") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。このアプリは仕組みを学ぶための学習用です。実際のパスワードの保管には使わないでください(1章の注意も参照)。練習では test1 のような架空の名前と、その場で「🎲 生成」した値だけを入れてください。「➕ 追加」を押すと、同じフォルダに passwords.enc と passwords.key が作られます(26-27行)。下の2問は画面の操作まわりを直すもので、鍵の作り方(_derive_key()・114-127行)とマスターパスワードの扱いは変えないでください。課題1・課題2に追加のライブラリは要りません(アプリ本体を暗号化ありで動かすには pip install cryptography が必要です)。
-
課題1:削除したあとにもう一度「🗑️ 削除」を押すと、選んでいない項目が消える
削除に使う番号は、一覧で項目を選んだときに覚えます。削除したあとも、この番号は前のままです。選び直さずにもう一度押すと、その番号にずれ込んだ別の項目が消えます。
期待結果:サービス名に
test1・test2・test3と入れて「➕ 追加」を3回押す(ステータス欄が3 件)。一覧のtest1を選んで「🗑️ 削除」から「はい」を押すと2 件になる。一覧を選び直さずにもう一度「🗑️ 削除」から「はい」を押すと、確認だけでtest2が消えて1 件になる。直したあとは、2回目の「🗑️ 削除」では確認も出ず、件数も変わらない。合格条件
- 選んだ項目を「🗑️ 削除」で消す動きは、今までどおり
- 削除したあと選び直していないときは、「🗑️ 削除」で何も消えない
- 削除したあと選び直していないときは、「✏️ 更新」でも中身が書き換わらない
ヒント①(どこを触るか): 触るのは1か所です。
_delete_entry()のself.entries.pop(self._selected_idx)(347行)の直後。
ヒント②(使うもの): 覚えている番号を捨てれば、次の操作は入口で止まります。_delete_entry()と_update_entry()はどちらもhasattr(self, "_selected_idx")で守られているので(333行・344行)、del self._selected_idxの1行が両方に効きます。
つまずきやすい点: 残り1件を選んで削除し一覧を空にしてから、選び直さずにもう一度押すとIndexError: pop from empty listが出ます。_on_select()はself.entries.index(e)で番号を求めるため(315行)、同じ内容の項目が2つあると先に見つかったほうの番号です。検索欄で絞ると一覧の並びはself._filteredのものになりますが、番号はself.entries側に直してから覚えています(307-315行)。 -
課題2:コピーしたパスワードがクリップボードに残り続ける
「📋 コピー」はパスワードをクリップボードへ入れます。消す処理はどこにもないため、別の物をコピーするまで残ったままです。ほかのアプリへうっかり貼り付けてしまう事故のもとになります。
期待結果:エントリを選んで「📋 コピー」を押し、メモ帳などに貼り付けられることを確かめる。ステータス欄は
パスワードをクリップボードにコピーしましたのまま変わらない。しばらく待ってからもう一度貼り付けても、同じ値が貼れる。直したあとは、決めた時間(30秒など)が過ぎるとクリップボードが空になり、貼り付けても何も入らない。合格条件
- 「📋 コピー」の直後は、今までどおり貼り付けられる
- 決めた時間が過ぎると、クリップボードからパスワードが消える
- そのあいだに別のアプリでコピーしたときは、そちらを消さない
ヒント①(どこを触るか): 触るのは2か所です。
_copy_pwd()(361-364行)の最後と、消すためのメソッドを1つ追加。
ヒント②(使うもの):self.root.after(30000, …)で30秒後の予約を入れておきます。消す側では、self.root.clipboard_get()が自分の入れた値と同じときだけself.root.clipboard_clear()を呼ぶ形が安全です。いつ消えるかをself.status_varに出しておくと親切です。
つまずきやすい点:clipboard_get()は、クリップボードが空のときや画像が入っているときにtk.TclErrorを出すので、try:とexcept tk.TclError:で囲んでください。中身を確かめずにclipboard_clear()を呼ぶと、そのあいだに別のアプリでコピーした内容まで消えます。_copy_pwd()は詳細欄が空でも押せて、空の文字列をクリップボードに入れます(363行)。予約が何本も重ならないよう、after_cancel()で前の予約を取り消す形にしておくと確実です。 -
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。