パスワードマネージャー
暗号化してパスワードを安全に保存するパスワードマネージャー。cryptographyライブラリでAES暗号化を学びます。
1. アプリ概要
暗号化してパスワードを安全に保存するパスワードマネージャー。cryptographyライブラリでAES暗号化を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・cryptography で、難易度は ★★★ です。
Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。
ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。
GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。
2. 機能一覧
- パスワードマネージャーのメイン機能
- 直感的なGUIインターフェース
- 入力値のバリデーション
- エラーハンドリング
- 結果の見やすい表示
- キーボードショートカット対応
3. 事前準備・環境
Python 3.10 以上 / Windows・Mac・Linux すべて対応
以下の環境で動作確認しています。
- Python 3.10 以上
- OS: Windows 10/11・macOS 12+・Ubuntu 20.04+
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
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 _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
with open(self.KEY_FILE, "r") as f:
stored = json.load(f)
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if hashed != stored["hash"]:
messagebox.showerror("エラー", "パスワードが正しくありません")
return
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
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()
with open(self.KEY_FILE, "w") as f:
json.dump({"hash": hashed, "salt": salt}, f)
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
self._load_passwords()
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=right.cget("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=right.cget("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):
self.entries = []
if not os.path.exists(self.DATA_FILE):
return
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 Exception:
pass
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クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
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 _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
with open(self.KEY_FILE, "r") as f:
stored = json.load(f)
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if hashed != stored["hash"]:
messagebox.showerror("エラー", "パスワードが正しくありません")
return
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
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()
with open(self.KEY_FILE, "w") as f:
json.dump({"hash": hashed, "salt": salt}, f)
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
self._load_passwords()
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=right.cget("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=right.cget("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):
self.entries = []
if not os.path.exists(self.DATA_FILE):
return
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 Exception:
pass
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()
LabelFrameによるセクション分け
ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
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 _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
with open(self.KEY_FILE, "r") as f:
stored = json.load(f)
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if hashed != stored["hash"]:
messagebox.showerror("エラー", "パスワードが正しくありません")
return
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
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()
with open(self.KEY_FILE, "w") as f:
json.dump({"hash": hashed, "salt": salt}, f)
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
self._load_passwords()
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=right.cget("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=right.cget("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):
self.entries = []
if not os.path.exists(self.DATA_FILE):
return
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 Exception:
pass
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()
Entryウィジェットとイベントバインド
ttk.Entryで入力フィールドを作成します。bind('
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
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 _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
with open(self.KEY_FILE, "r") as f:
stored = json.load(f)
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if hashed != stored["hash"]:
messagebox.showerror("エラー", "パスワードが正しくありません")
return
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
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()
with open(self.KEY_FILE, "w") as f:
json.dump({"hash": hashed, "salt": salt}, f)
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
self._load_passwords()
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=right.cget("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=right.cget("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):
self.entries = []
if not os.path.exists(self.DATA_FILE):
return
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 Exception:
pass
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()
Textウィジェットでの結果表示
結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
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 _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
with open(self.KEY_FILE, "r") as f:
stored = json.load(f)
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if hashed != stored["hash"]:
messagebox.showerror("エラー", "パスワードが正しくありません")
return
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
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()
with open(self.KEY_FILE, "w") as f:
json.dump({"hash": hashed, "salt": salt}, f)
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
self._load_passwords()
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=right.cget("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=right.cget("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):
self.entries = []
if not os.path.exists(self.DATA_FILE):
return
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 Exception:
pass
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()
例外処理とmessagebox
try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os
import hashlib
import base64
import secrets
import string
try:
from cryptography.fernet import Fernet
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
class App11:
"""パスワードマネージャー"""
DATA_FILE = os.path.join(os.path.dirname(__file__), "passwords.enc")
KEY_FILE = os.path.join(os.path.dirname(__file__), "passwords.key")
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 _authenticate(self):
pw = self.master_entry.get()
if not pw:
messagebox.showwarning("警告", "パスワードを入力してください")
return
has_data = os.path.exists(self.KEY_FILE)
if has_data:
# 認証
with open(self.KEY_FILE, "r") as f:
stored = json.load(f)
hashed = hashlib.sha256((pw + stored["salt"]).encode()).hexdigest()
if hashed != stored["hash"]:
messagebox.showerror("エラー", "パスワードが正しくありません")
return
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
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()
with open(self.KEY_FILE, "w") as f:
json.dump({"hash": hashed, "salt": salt}, f)
if CRYPTO_AVAILABLE:
key = base64.urlsafe_b64encode(
hashlib.sha256(pw.encode()).digest())
self.fernet = Fernet(key)
self._load_passwords()
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=right.cget("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=right.cget("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):
self.entries = []
if not os.path.exists(self.DATA_FILE):
return
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 Exception:
pass
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()
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app11.py と保存します。
-
2クラスの骨格を作る
App11クラスを定義し、__init__とmainloop()の最小構成を作ります。
-
3タイトルバーを作る
Frameを使ってカラーバー付きのタイトルエリアを作ります。
-
4入力フォームを実装する
LabelFrameとEntryウィジェットで入力エリアを作ります。
-
5処理ロジックを実装する
_calculate()メソッドに計算・処理ロジックを実装します。
-
6結果表示を実装する
TextウィジェットかLabelに結果を表示する_show_result()を実装します。
-
7エラー処理を追加する
try-exceptとmessageboxでエラーハンドリングを追加します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 データのエクスポート機能
計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 日本語フォントが表示されない
原因:システムに日本語フォントが見つからない場合があります。
解決法:font引数を省略するかシステムに合ったフォントを指定してください。
❌ ウィンドウのサイズが変更できない
原因:resizable(False, False)が設定されています。
解決法:resizable(True, True)に変更してください。
9. 練習問題
アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。
-
課題1:機能拡張
パスワードマネージャーに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。
-
課題2:UIの改善
色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。
-
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。