passlocker-tk 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- passlocker_tk-1.0.0/LICENSE +19 -0
- passlocker_tk-1.0.0/PKG-INFO +33 -0
- passlocker_tk-1.0.0/README.md +8 -0
- passlocker_tk-1.0.0/passlocker_tk/__init__.py +3 -0
- passlocker_tk-1.0.0/passlocker_tk/__main__.py +8 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/__init__.py +16 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/about_dialog.py +88 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/account_display_dialog.py +138 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/accounts_table.py +65 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/generate_password_dialog.py +209 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/new_otp_account_dialog.py +94 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/new_password_account_dialog.py +126 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/new_totp_account_dialog.py +147 -0
- passlocker_tk-1.0.0/passlocker_tk/dialogs/password_dialog.py +48 -0
- passlocker_tk-1.0.0/passlocker_tk/gui.py +217 -0
- passlocker_tk-1.0.0/passlocker_tk.egg-info/PKG-INFO +33 -0
- passlocker_tk-1.0.0/passlocker_tk.egg-info/SOURCES.txt +23 -0
- passlocker_tk-1.0.0/passlocker_tk.egg-info/dependency_links.txt +1 -0
- passlocker_tk-1.0.0/passlocker_tk.egg-info/entry_points.txt +2 -0
- passlocker_tk-1.0.0/passlocker_tk.egg-info/requires.txt +10 -0
- passlocker_tk-1.0.0/passlocker_tk.egg-info/top_level.txt +4 -0
- passlocker_tk-1.0.0/pyproject.toml +48 -0
- passlocker_tk-1.0.0/setup.cfg +4 -0
- passlocker_tk-1.0.0/setup.py +6 -0
- passlocker_tk-1.0.0/tests/create_demo.py +18 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
|
2
|
+
|
|
3
|
+
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License").
|
|
4
|
+
|
|
5
|
+
You may not use the material for commercial purposes.
|
|
6
|
+
|
|
7
|
+
You are free to:
|
|
8
|
+
- Share — copy and redistribute the material in any medium or format
|
|
9
|
+
- Adapt — remix, transform, and build upon the material
|
|
10
|
+
|
|
11
|
+
Under the following terms:
|
|
12
|
+
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
|
|
13
|
+
- NonCommercial — You may not use the material for commercial purposes.
|
|
14
|
+
- ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
|
|
15
|
+
- No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
|
|
16
|
+
|
|
17
|
+
For the full license text, see: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
|
|
18
|
+
|
|
19
|
+
SPDX-License-Identifier: CC-BY-NC-SA-4.0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: passlocker-tk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Password Encryption GUI
|
|
5
|
+
Author-email: Chris Lee <github@chrislee.dhs.org>
|
|
6
|
+
License: CC-BY-NC-SA-4.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/chrislee35/passlocker-tk
|
|
8
|
+
Keywords: encryption,passwords,security
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Topic :: Utilities
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: passlocker>=1.0.2
|
|
16
|
+
Requires-Dist: customtkinter>=5.2.2
|
|
17
|
+
Requires-Dist: py-debounce>=0.1.2
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: build; extra == "dev"
|
|
20
|
+
Requires-Dist: twine; extra == "dev"
|
|
21
|
+
Requires-Dist: black; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# Overview
|
|
27
|
+
|
|
28
|
+
PassLocker-Tk provides a Tkinter interface around using [Passlocker](https://github.com/chrislee35/passlocker), a local-first password management program.
|
|
29
|
+
|
|
30
|
+
## Screenshot
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .password_dialog import PasswordDialog
|
|
2
|
+
from .accounts_table import AccountsTable
|
|
3
|
+
from .new_password_account_dialog import NewPasswordAccountDialog
|
|
4
|
+
from .new_otp_account_dialog import NewOTPAccountDialog
|
|
5
|
+
from .new_totp_account_dialog import NewTOTPAccountDialog
|
|
6
|
+
from .about_dialog import AboutDialog
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"PasswordDialog",
|
|
10
|
+
"AccountsTable",
|
|
11
|
+
"NewPasswordAccountDialog",
|
|
12
|
+
"NewOTPAccountDialog",
|
|
13
|
+
"NewTOTPAccountDialog",
|
|
14
|
+
"AboutDialog",
|
|
15
|
+
"PasswordDialog",
|
|
16
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
import webbrowser
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AboutDialog(ctk.CTkToplevel):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
parent,
|
|
9
|
+
*,
|
|
10
|
+
title,
|
|
11
|
+
version,
|
|
12
|
+
author,
|
|
13
|
+
github_url,
|
|
14
|
+
description,
|
|
15
|
+
):
|
|
16
|
+
super().__init__(parent)
|
|
17
|
+
|
|
18
|
+
self.title("About")
|
|
19
|
+
self.resizable(False, False)
|
|
20
|
+
|
|
21
|
+
# Modal
|
|
22
|
+
self.transient(parent)
|
|
23
|
+
self.grab_set()
|
|
24
|
+
|
|
25
|
+
self._build_ui(
|
|
26
|
+
title=title,
|
|
27
|
+
version=version,
|
|
28
|
+
author=author,
|
|
29
|
+
github_url=github_url,
|
|
30
|
+
description=description,
|
|
31
|
+
)
|
|
32
|
+
self._center(parent)
|
|
33
|
+
|
|
34
|
+
self.bind("<Escape>", lambda _: self.destroy())
|
|
35
|
+
self.wait_window()
|
|
36
|
+
|
|
37
|
+
# --------------------
|
|
38
|
+
# UI
|
|
39
|
+
# --------------------
|
|
40
|
+
def _build_ui(self, *, title, version, author, github_url, description):
|
|
41
|
+
frame = ctk.CTkFrame(self)
|
|
42
|
+
frame.pack(padx=24, pady=24)
|
|
43
|
+
|
|
44
|
+
# Title
|
|
45
|
+
ctk.CTkLabel(frame, text=title, font=ctk.CTkFont(size=20, weight="bold")).pack(
|
|
46
|
+
anchor="w"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Version / Author
|
|
50
|
+
ctk.CTkLabel(frame, text=f"Version {version}\n{author}", justify="left").pack(
|
|
51
|
+
anchor="w", pady=(4, 12)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Description
|
|
55
|
+
ctk.CTkLabel(frame, text=description, justify="left", wraplength=420).pack(
|
|
56
|
+
anchor="w", pady=(0, 12)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# GitHub link
|
|
60
|
+
self._link(frame, text="Source code on GitHub", url=github_url).pack(
|
|
61
|
+
anchor="w", pady=(0, 6)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# License link
|
|
65
|
+
self._link(
|
|
66
|
+
frame,
|
|
67
|
+
text="Licensed under CC-BY-NC-SA-4.0",
|
|
68
|
+
url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode",
|
|
69
|
+
).pack(anchor="w", pady=(0, 16))
|
|
70
|
+
|
|
71
|
+
# Close button
|
|
72
|
+
ctk.CTkButton(frame, text="Close", width=120, command=self.destroy).pack(
|
|
73
|
+
anchor="e"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# --------------------
|
|
77
|
+
# Helpers
|
|
78
|
+
# --------------------
|
|
79
|
+
def _link(self, parent, *, text, url):
|
|
80
|
+
lbl = ctk.CTkLabel(parent, text=text, text_color="#4ea1ff", cursor="hand2")
|
|
81
|
+
lbl.bind("<Button-1>", lambda _: webbrowser.open(url))
|
|
82
|
+
return lbl
|
|
83
|
+
|
|
84
|
+
def _center(self, parent):
|
|
85
|
+
self.update_idletasks()
|
|
86
|
+
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
|
|
87
|
+
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
|
|
88
|
+
self.geometry(f"+{x}+{y}")
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from secrets import token_bytes
|
|
5
|
+
|
|
6
|
+
from passlocker.models.account import Account
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AccountDisplayDialog(ctk.CTkToplevel):
|
|
10
|
+
def __init__(self, parent, *, account: Account):
|
|
11
|
+
super().__init__(parent)
|
|
12
|
+
|
|
13
|
+
self.title("Password Record")
|
|
14
|
+
self.resizable(False, False)
|
|
15
|
+
|
|
16
|
+
self.account: Account = account
|
|
17
|
+
|
|
18
|
+
self._revealed = False
|
|
19
|
+
self.iv: bytes = b""
|
|
20
|
+
self.passhash: str = ""
|
|
21
|
+
|
|
22
|
+
# Modal behavior
|
|
23
|
+
self.transient(parent)
|
|
24
|
+
self.wait_visibility()
|
|
25
|
+
self.grab_set()
|
|
26
|
+
|
|
27
|
+
self._build_ui()
|
|
28
|
+
self._center(parent)
|
|
29
|
+
|
|
30
|
+
self.bind("<Escape>", lambda _: self.destroy())
|
|
31
|
+
self.wait_window()
|
|
32
|
+
|
|
33
|
+
# --------------------
|
|
34
|
+
# UI
|
|
35
|
+
# --------------------
|
|
36
|
+
def _build_ui(self):
|
|
37
|
+
container: ctk.CTkFrame = ctk.CTkFrame(self)
|
|
38
|
+
container.pack(padx=10, pady=10)
|
|
39
|
+
|
|
40
|
+
# Account
|
|
41
|
+
ctk.CTkLabel(container, text="Account").grid(
|
|
42
|
+
row=0, column=0, sticky="w", padx=5
|
|
43
|
+
)
|
|
44
|
+
ctk.CTkLabel(
|
|
45
|
+
container, text=self.account.account, font=ctk.CTkFont(weight="bold")
|
|
46
|
+
).grid(row=0, column=1, sticky="w")
|
|
47
|
+
|
|
48
|
+
# Username
|
|
49
|
+
ctk.CTkLabel(container, text="Username").grid(
|
|
50
|
+
row=1, column=0, sticky="w", padx=5
|
|
51
|
+
)
|
|
52
|
+
ctk.CTkLabel(container, text=self.account.username).grid(
|
|
53
|
+
row=1, column=1, sticky="w"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Created on
|
|
57
|
+
ctk.CTkLabel(container, text="Created").grid(
|
|
58
|
+
row=2, column=0, sticky="w", padx=5
|
|
59
|
+
)
|
|
60
|
+
ctk.CTkLabel(container, text=self.account.created_on).grid(
|
|
61
|
+
row=2, column=1, sticky="w"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Password
|
|
65
|
+
ctk.CTkLabel(container, text="Password").grid(
|
|
66
|
+
row=3, column=0, sticky="w", padx=5
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.password_var: ctk.StringVar = ctk.StringVar(value="••••••••")
|
|
70
|
+
self.password_label: ctk.CTkLabel = ctk.CTkLabel(
|
|
71
|
+
container, textvariable=self.password_var, font=ctk.CTkFont(size=14)
|
|
72
|
+
)
|
|
73
|
+
self.password_label.grid(row=3, column=1, sticky="w")
|
|
74
|
+
|
|
75
|
+
# Buttons
|
|
76
|
+
btn_frame: ctk.CTkFrame = ctk.CTkFrame(container, fg_color="transparent")
|
|
77
|
+
btn_frame.grid(row=4, column=1, sticky="w", pady=(10, 0))
|
|
78
|
+
|
|
79
|
+
self.reveal_btn: ctk.CTkButton = ctk.CTkButton(
|
|
80
|
+
btn_frame, text="Show Password", width=100, command=self.toggle_reveal
|
|
81
|
+
)
|
|
82
|
+
self.reveal_btn.grid(row=0, column=0, padx=(0, 10))
|
|
83
|
+
|
|
84
|
+
self.copy_btn: ctk.CTkButton = ctk.CTkButton(
|
|
85
|
+
btn_frame, text="Copy to Clipboard", width=100, command=self.copy_password
|
|
86
|
+
)
|
|
87
|
+
self.copy_btn.grid(row=0, column=1)
|
|
88
|
+
|
|
89
|
+
container.grid_columnconfigure(1, weight=1)
|
|
90
|
+
|
|
91
|
+
# --------------------
|
|
92
|
+
# Actions
|
|
93
|
+
# --------------------
|
|
94
|
+
def toggle_reveal(self):
|
|
95
|
+
self._revealed = not self._revealed
|
|
96
|
+
|
|
97
|
+
if self._revealed:
|
|
98
|
+
password: bytes = self.account.get_active_password()
|
|
99
|
+
self.password_var.set(password.decode("utf-8"))
|
|
100
|
+
self.reveal_btn.configure(text="Hide")
|
|
101
|
+
else:
|
|
102
|
+
self.password_var.set("••••••••")
|
|
103
|
+
self.reveal_btn.configure(text="Reveal")
|
|
104
|
+
|
|
105
|
+
def copy_password(self):
|
|
106
|
+
self.clipboard_clear()
|
|
107
|
+
password: bytes = self.account.get_active_password()
|
|
108
|
+
self.iv: bytes = token_bytes(16)
|
|
109
|
+
self.passhash: str = sha256(self.iv + password).hexdigest()
|
|
110
|
+
self.clipboard_append(password.decode("utf-8"))
|
|
111
|
+
self.update() # ensure clipboard is updated
|
|
112
|
+
self._schedule_clipboard_clear()
|
|
113
|
+
|
|
114
|
+
def _schedule_clipboard_clear(self, delay_ms=15000):
|
|
115
|
+
self.after(delay_ms, self._clear_clipboard)
|
|
116
|
+
|
|
117
|
+
def _clear_clipboard(self):
|
|
118
|
+
try:
|
|
119
|
+
checkhash: str = sha256(
|
|
120
|
+
self.iv + self.clipboard_get().encode("utf-8")
|
|
121
|
+
).hexdigest()
|
|
122
|
+
if checkhash == self.passhash:
|
|
123
|
+
self.clipboard_clear()
|
|
124
|
+
except tk.TclError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
# --------------------
|
|
128
|
+
# Helpers
|
|
129
|
+
# --------------------
|
|
130
|
+
def _center(self, parent):
|
|
131
|
+
self.update_idletasks()
|
|
132
|
+
x: int = (
|
|
133
|
+
parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
|
|
134
|
+
)
|
|
135
|
+
y: int = (
|
|
136
|
+
parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
|
|
137
|
+
)
|
|
138
|
+
self.geometry(f"+{x}+{y}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
from passlocker.models.account import Account
|
|
5
|
+
from .account_display_dialog import AccountDisplayDialog
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AccountsTable(ctk.CTkFrame):
|
|
9
|
+
def __init__(self, parent: ctk.CTk) -> None:
|
|
10
|
+
super().__init__(parent)
|
|
11
|
+
|
|
12
|
+
columns = ("account", "username", "type")
|
|
13
|
+
|
|
14
|
+
self.tree: ttk.Treeview = ttk.Treeview(
|
|
15
|
+
self, columns=columns, show="headings", height=10
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Column definitions
|
|
19
|
+
self.tree.heading("account", text="Account")
|
|
20
|
+
self.tree.heading("username", text="Username")
|
|
21
|
+
self.tree.heading("type", text="Type")
|
|
22
|
+
|
|
23
|
+
self.tree.column("account", anchor="w", width=200)
|
|
24
|
+
self.tree.column("username", anchor="w", width=180)
|
|
25
|
+
self.tree.column("type", anchor="center", width=80)
|
|
26
|
+
|
|
27
|
+
# Scrollbar
|
|
28
|
+
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
|
|
29
|
+
self.tree.configure(yscrollcommand=scrollbar.set)
|
|
30
|
+
|
|
31
|
+
# Layout
|
|
32
|
+
self.tree.grid(row=0, column=0, sticky="nsew")
|
|
33
|
+
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
34
|
+
|
|
35
|
+
self.grid_rowconfigure(0, weight=1)
|
|
36
|
+
self.grid_columnconfigure(0, weight=1)
|
|
37
|
+
|
|
38
|
+
self.tree.bind("<Double-1>", self.on_open_account)
|
|
39
|
+
self._accounts_by_id: dict[int, Account] = {}
|
|
40
|
+
|
|
41
|
+
# --------------------
|
|
42
|
+
# Public API
|
|
43
|
+
# --------------------
|
|
44
|
+
def populate(self, accounts: list[Account]) -> None:
|
|
45
|
+
"""
|
|
46
|
+
accounts: iterable of objects or dicts
|
|
47
|
+
expected fields: account, username, type
|
|
48
|
+
"""
|
|
49
|
+
self.tree.delete(*self.tree.get_children())
|
|
50
|
+
|
|
51
|
+
for acc in accounts:
|
|
52
|
+
self.tree.insert(
|
|
53
|
+
"", "end", iid=id(acc), values=(acc.account, acc.username, acc.type)
|
|
54
|
+
)
|
|
55
|
+
self._accounts_by_id: dict[int, Account] = {id(acc): acc for acc in accounts}
|
|
56
|
+
|
|
57
|
+
def on_open_account(self, event: tk.Event) -> None:
|
|
58
|
+
selected = self.tree.selection()
|
|
59
|
+
if not selected:
|
|
60
|
+
return
|
|
61
|
+
item_id = int(selected[0])
|
|
62
|
+
account = self._accounts_by_id.get(item_id)
|
|
63
|
+
if account is None:
|
|
64
|
+
return
|
|
65
|
+
_ = AccountDisplayDialog(self, account=account)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
from tkinter import ttk
|
|
3
|
+
import secrets
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GeneratePasswordDialog(ctk.CTkToplevel):
|
|
7
|
+
def __init__(self, parent) -> None:
|
|
8
|
+
super().__init__(parent)
|
|
9
|
+
|
|
10
|
+
self.title("Generate Password")
|
|
11
|
+
self.resizable(False, False)
|
|
12
|
+
|
|
13
|
+
self.result: str | None = None
|
|
14
|
+
|
|
15
|
+
# Modal behavior
|
|
16
|
+
self.transient(parent)
|
|
17
|
+
self.wait_visibility()
|
|
18
|
+
self.grab_set()
|
|
19
|
+
|
|
20
|
+
self._build_ui()
|
|
21
|
+
self._center(parent)
|
|
22
|
+
|
|
23
|
+
self.bind("<Escape>", lambda _: self._cancel())
|
|
24
|
+
|
|
25
|
+
self.wait_window()
|
|
26
|
+
|
|
27
|
+
# --------------------
|
|
28
|
+
# UI
|
|
29
|
+
# --------------------
|
|
30
|
+
def _build_ui(self) -> None:
|
|
31
|
+
root = ctk.CTkFrame(self)
|
|
32
|
+
root.pack(padx=20, pady=20)
|
|
33
|
+
|
|
34
|
+
# ---- Controls ----
|
|
35
|
+
controls = ctk.CTkFrame(root)
|
|
36
|
+
controls.pack(fill="x")
|
|
37
|
+
|
|
38
|
+
# Algorithm selector
|
|
39
|
+
ctk.CTkLabel(controls, text="Algorithm").grid(row=0, column=0, sticky="w")
|
|
40
|
+
|
|
41
|
+
self.algorithm_var = ctk.StringVar(value="random")
|
|
42
|
+
self.algorithm_menu = ctk.CTkOptionMenu(
|
|
43
|
+
controls,
|
|
44
|
+
variable=self.algorithm_var,
|
|
45
|
+
values=["random", "memorable", "numeric"],
|
|
46
|
+
width=140,
|
|
47
|
+
)
|
|
48
|
+
self.algorithm_menu.grid(row=0, column=1, padx=(10, 20))
|
|
49
|
+
|
|
50
|
+
# Length selector
|
|
51
|
+
ctk.CTkLabel(controls, text="Length").grid(row=0, column=2, sticky="w")
|
|
52
|
+
|
|
53
|
+
self.length_var = ctk.IntVar(value=16)
|
|
54
|
+
self.length_slider = ctk.CTkSlider(
|
|
55
|
+
controls, from_=8, to=64, number_of_steps=56, command=self._on_length_change
|
|
56
|
+
)
|
|
57
|
+
self.length_slider.set(16)
|
|
58
|
+
self.length_slider.grid(row=0, column=3, padx=(10, 10))
|
|
59
|
+
|
|
60
|
+
self.length_label = ctk.CTkLabel(controls, text="16", width=32, anchor="e")
|
|
61
|
+
self.length_label.grid(row=0, column=4)
|
|
62
|
+
|
|
63
|
+
controls.grid_columnconfigure(3, weight=1)
|
|
64
|
+
|
|
65
|
+
# Generate button
|
|
66
|
+
self.generate_btn = ctk.CTkButton(root, text="Generate", command=self._generate)
|
|
67
|
+
self.generate_btn.pack(pady=(10, 10))
|
|
68
|
+
|
|
69
|
+
# ---- Table ----
|
|
70
|
+
table_frame = ctk.CTkFrame(root)
|
|
71
|
+
table_frame.pack()
|
|
72
|
+
|
|
73
|
+
self.tree = ttk.Treeview(
|
|
74
|
+
table_frame, columns=("password",), show="headings", height=10
|
|
75
|
+
)
|
|
76
|
+
self.tree.heading("password", text="Generated Password")
|
|
77
|
+
self.tree.column("password", anchor="w", width=320)
|
|
78
|
+
|
|
79
|
+
scrollbar = ttk.Scrollbar(
|
|
80
|
+
table_frame, orient="vertical", command=self.tree.yview
|
|
81
|
+
)
|
|
82
|
+
self.tree.configure(yscrollcommand=scrollbar.set)
|
|
83
|
+
|
|
84
|
+
self.tree.grid(row=0, column=0, sticky="nsew")
|
|
85
|
+
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
86
|
+
|
|
87
|
+
table_frame.grid_rowconfigure(0, weight=1)
|
|
88
|
+
table_frame.grid_columnconfigure(0, weight=1)
|
|
89
|
+
|
|
90
|
+
# Double-click selection
|
|
91
|
+
self.tree.bind("<Double-1>", self._on_select)
|
|
92
|
+
|
|
93
|
+
# --------------------
|
|
94
|
+
# Actions
|
|
95
|
+
# --------------------
|
|
96
|
+
def _on_length_change(self, value):
|
|
97
|
+
self.length_label.configure(text=str(int(value)))
|
|
98
|
+
|
|
99
|
+
def _generate(self):
|
|
100
|
+
algorithm = self.algorithm_var.get()
|
|
101
|
+
length = self.length_var.get()
|
|
102
|
+
|
|
103
|
+
passwords = self.generate_passwords(algorithm, length, count=10)
|
|
104
|
+
self.tree.delete(*self.tree.get_children())
|
|
105
|
+
|
|
106
|
+
for pw in passwords:
|
|
107
|
+
self.tree.insert("", "end", values=(pw,))
|
|
108
|
+
|
|
109
|
+
def _on_select(self, event):
|
|
110
|
+
selection = self.tree.selection()
|
|
111
|
+
if not selection:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
self.result = self.tree.item(selection[0], "values")[0]
|
|
115
|
+
self.destroy()
|
|
116
|
+
|
|
117
|
+
def _cancel(self):
|
|
118
|
+
self.result = None
|
|
119
|
+
self.destroy()
|
|
120
|
+
|
|
121
|
+
# --------------------
|
|
122
|
+
# Helpers
|
|
123
|
+
# --------------------
|
|
124
|
+
def _center(self, parent):
|
|
125
|
+
self.update_idletasks()
|
|
126
|
+
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
|
|
127
|
+
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
|
|
128
|
+
self.geometry(f"+{x}+{y}")
|
|
129
|
+
|
|
130
|
+
def generate_passwords(
|
|
131
|
+
self, algorithm: str, length: int, count: int = 10
|
|
132
|
+
) -> list[str]:
|
|
133
|
+
if algorithm == "memorable":
|
|
134
|
+
return [self.generate_memorable(length) for _ in range(count)]
|
|
135
|
+
elif algorithm == "random":
|
|
136
|
+
return [self.generate_random(length) for _ in range(count)]
|
|
137
|
+
elif algorithm == "numeric":
|
|
138
|
+
return [self.generate_numbers(length) for _ in range(count)]
|
|
139
|
+
else:
|
|
140
|
+
raise Exception(f"Unknown generation algorithm: {algorithm}")
|
|
141
|
+
|
|
142
|
+
def generate_memorable(self, length: int) -> str:
|
|
143
|
+
if length <= 8:
|
|
144
|
+
maxnum = 100
|
|
145
|
+
special = 0
|
|
146
|
+
elif length <= 16:
|
|
147
|
+
maxnum = 1000
|
|
148
|
+
special = 1
|
|
149
|
+
elif length <= 24:
|
|
150
|
+
maxnum = 10000
|
|
151
|
+
special = 2
|
|
152
|
+
else:
|
|
153
|
+
maxnum = 100000
|
|
154
|
+
special = 3
|
|
155
|
+
|
|
156
|
+
random_int: int = secrets.randbelow(maxnum)
|
|
157
|
+
random_special: str = "".join(
|
|
158
|
+
secrets.choice(
|
|
159
|
+
[
|
|
160
|
+
"!",
|
|
161
|
+
"@",
|
|
162
|
+
"#",
|
|
163
|
+
"$",
|
|
164
|
+
"%",
|
|
165
|
+
"^",
|
|
166
|
+
"&",
|
|
167
|
+
"*",
|
|
168
|
+
"(",
|
|
169
|
+
")",
|
|
170
|
+
"-",
|
|
171
|
+
"_",
|
|
172
|
+
"+",
|
|
173
|
+
"=",
|
|
174
|
+
"{",
|
|
175
|
+
"}",
|
|
176
|
+
"[",
|
|
177
|
+
"]",
|
|
178
|
+
":",
|
|
179
|
+
";",
|
|
180
|
+
"<",
|
|
181
|
+
">",
|
|
182
|
+
".",
|
|
183
|
+
",",
|
|
184
|
+
"?",
|
|
185
|
+
"/",
|
|
186
|
+
"~",
|
|
187
|
+
"`",
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
for i in range(special)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
secret_sauce = f"{random_int}{random_special}"
|
|
194
|
+
password: str = ""
|
|
195
|
+
|
|
196
|
+
with open("/usr/share/dict/words") as f:
|
|
197
|
+
words: list[str] = [word.strip() for word in f]
|
|
198
|
+
genlen: int = 0
|
|
199
|
+
while genlen != length:
|
|
200
|
+
password = secret_sauce.join(secrets.choice(words) for _ in range(2))
|
|
201
|
+
genlen = len(password)
|
|
202
|
+
|
|
203
|
+
return password
|
|
204
|
+
|
|
205
|
+
def generate_random(self, length: int) -> str:
|
|
206
|
+
return secrets.token_urlsafe(32)[0:length]
|
|
207
|
+
|
|
208
|
+
def generate_numbers(self, length: int) -> str:
|
|
209
|
+
return "".join([str(secrets.choice(range(10))) for i in range(length)])
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import customtkinter as ctk
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NewOTPAccountDialog(ctk.CTkToplevel):
|
|
5
|
+
def __init__(self, parent):
|
|
6
|
+
super().__init__(parent)
|
|
7
|
+
|
|
8
|
+
self.title("New Account")
|
|
9
|
+
self.resizable(False, False)
|
|
10
|
+
|
|
11
|
+
self.result = None
|
|
12
|
+
|
|
13
|
+
# Modal behavior
|
|
14
|
+
self.transient(parent)
|
|
15
|
+
self.grab_set()
|
|
16
|
+
|
|
17
|
+
self._build_ui()
|
|
18
|
+
self._center(parent)
|
|
19
|
+
|
|
20
|
+
self.bind("<Escape>", lambda _: self._cancel())
|
|
21
|
+
self.bind("<Return>", lambda _: self._ok())
|
|
22
|
+
|
|
23
|
+
self.wait_window()
|
|
24
|
+
|
|
25
|
+
# --------------------
|
|
26
|
+
# UI
|
|
27
|
+
# --------------------
|
|
28
|
+
def _build_ui(self):
|
|
29
|
+
frame = ctk.CTkFrame(self)
|
|
30
|
+
frame.pack(padx=20, pady=20)
|
|
31
|
+
|
|
32
|
+
# Variables
|
|
33
|
+
self.account_var = ctk.StringVar()
|
|
34
|
+
self.username_var = ctk.StringVar()
|
|
35
|
+
|
|
36
|
+
# Account name
|
|
37
|
+
ctk.CTkLabel(frame, text="Account").grid(row=0, column=0, sticky="w")
|
|
38
|
+
self.account_entry = ctk.CTkEntry(
|
|
39
|
+
frame, textvariable=self.account_var, width=260
|
|
40
|
+
)
|
|
41
|
+
self.account_entry.grid(row=0, column=1, pady=5)
|
|
42
|
+
self.account_entry.focus()
|
|
43
|
+
|
|
44
|
+
# Username
|
|
45
|
+
ctk.CTkLabel(frame, text="Username").grid(row=1, column=0, sticky="w")
|
|
46
|
+
ctk.CTkEntry(frame, textvariable=self.username_var, width=260).grid(
|
|
47
|
+
row=1, column=1, pady=5
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Password
|
|
51
|
+
ctk.CTkLabel(frame, text="Passwords").grid(row=2, column=0, sticky="w")
|
|
52
|
+
self.password_entry = ctk.CTkTextbox(frame, width=260, height=112)
|
|
53
|
+
self.password_entry.grid(row=2, column=1, pady=5)
|
|
54
|
+
|
|
55
|
+
# Buttons
|
|
56
|
+
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
|
57
|
+
btn_frame.grid(row=3, column=1, pady=(15, 0), sticky="e")
|
|
58
|
+
|
|
59
|
+
ctk.CTkButton(btn_frame, text="Cancel", width=100, command=self._cancel).grid(
|
|
60
|
+
row=0, column=0, padx=(0, 10)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.ok_btn = ctk.CTkButton(btn_frame, text="OK", width=100, command=self._ok)
|
|
64
|
+
self.ok_btn.grid(row=0, column=1)
|
|
65
|
+
|
|
66
|
+
frame.grid_columnconfigure(1, weight=1)
|
|
67
|
+
|
|
68
|
+
def _ok(self):
|
|
69
|
+
account = self.account_var.get().strip()
|
|
70
|
+
username = self.username_var.get().strip()
|
|
71
|
+
password = self.password_entry.get("0.0", ctk.END)
|
|
72
|
+
|
|
73
|
+
if not account or not password:
|
|
74
|
+
return # minimal validation; extend as needed
|
|
75
|
+
|
|
76
|
+
self.result = {
|
|
77
|
+
"account": account,
|
|
78
|
+
"username": username,
|
|
79
|
+
"password": password,
|
|
80
|
+
}
|
|
81
|
+
self.destroy()
|
|
82
|
+
|
|
83
|
+
def _cancel(self):
|
|
84
|
+
self.result = None
|
|
85
|
+
self.destroy()
|
|
86
|
+
|
|
87
|
+
# --------------------
|
|
88
|
+
# Helpers
|
|
89
|
+
# --------------------
|
|
90
|
+
def _center(self, parent):
|
|
91
|
+
self.update_idletasks()
|
|
92
|
+
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
|
|
93
|
+
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
|
|
94
|
+
self.geometry(f"+{x}+{y}")
|