Blockchain Secure Login GUI in Python | CTk + PBKDF2 + Proof-of-Work – FuzzuTech
Demo :
Click Video πππ
π Features:
-
Embedded YouTube Short
-
Short description + code snippet highlights
-
SEO keywords: Python GUI, Blockchain Login, PBKDF2 Security, Proof-of-Work in Python
-
Internal links to previous Shorts
Code :
# secure_login_blockchain_ctk.py
# ------------------------------------------------------------
# Modern CTk GUI: Blockchain-based Secure Login System (Local)
# - Users stored with salted PBKDF2 hashes (SHA256)
# - "Blockchain" = append-only hash chain of events (genesis + register + login)
# - Simple Proof-of-Work (difficulty = 3 leading zeros)
# - Chain validation & export; clean, dark UI with tabs
# ------------------------------------------------------------
import os
import json
import time
import hmac
import json
import hashlib
import secrets
from datetime import datetime
import customtkinter as ctk
from tkinter import messagebox
APP_TITLE = "Blockchain Secure Login (CTk)"
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
USERS_PATH = os.path.join(DATA_DIR, "users.json")
CHAIN_PATH = os.path.join(DATA_DIR, "chain.json")
# --------------------------
# Utils: files & time
# --------------------------
def ensure_data_dir():
os.makedirs(DATA_DIR, exist_ok=True)
def read_json(path, default):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
def write_json(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
def utc_iso():
return datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
# --------------------------
# Password hashing (PBKDF2)
# --------------------------
PBKDF2_ITERS = 120_000
SALT_BYTES = 16
KEY_LEN = 32
def hash_password(password: str, salt: bytes | None = None) -> dict:
if salt is None:
salt = secrets.token_bytes(SALT_BYTES)
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, PBKDF2_ITERS, dklen=KEY_LEN)
return {
"algo": "pbkdf2_sha256",
"iterations": PBKDF2_ITERS,
"salt_hex": salt.hex(),
"hash_hex": dk.hex(),
}
def verify_password(password: str, record: dict) -> bool:
try:
salt = bytes.fromhex(record["salt_hex"])
iters = int(record["iterations"])
expect = bytes.fromhex(record["hash_hex"])
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iters, dklen=len(expect))
# constant-time compare
return hmac.compare_digest(dk, expect)
except Exception:
return False
# --------------------------
# Simple Blockchain
# --------------------------
DIFFICULTY = 3 # number of leading zeros required in hex hash
def block_hash(block: dict) -> str:
# Exclude 'hash' field when hashing
copy = {k: block[k] for k in block if k != "hash"}
content = json.dumps(copy, sort_keys=True, separators=(",", ":")).encode("utf-8")
return hashlib.sha256(content).hexdigest()
def mine_block(block: dict, difficulty: int = DIFFICULTY) -> dict:
prefix = "0" * difficulty
nonce = 0
while True:
block["nonce"] = nonce
hx = block_hash(block)
if hx.startswith(prefix):
block["hash"] = hx
return block
nonce += 1
def load_chain() -> list:
chain = read_json(CHAIN_PATH, [])
if not chain:
# Create genesis block
genesis = {
"index": 0,
"timestamp": utc_iso(),
"event": {"type": "genesis", "msg": "Chain created"},
"prev_hash": "0" * 64,
"nonce": 0,
}
mined = mine_block(genesis)
chain = [mined]
write_json(CHAIN_PATH, chain)
return chain
def add_event_to_chain(event: dict) -> dict:
chain = load_chain()
prev = chain[-1]
block = {
"index": len(chain),
"timestamp": utc_iso(),
"event": event,
"prev_hash": prev["hash"],
"nonce": 0,
}
mined = mine_block(block)
chain.append(mined)
write_json(CHAIN_PATH, chain)
return mined
def validate_chain(chain: list | None = None) -> tuple[bool, str]:
if chain is None:
chain = load_chain()
if not chain:
return False, "Chain is empty."
prefix = "0" * DIFFICULTY
for i, blk in enumerate(chain):
# Verify hash
hx = block_hash(blk)
if "hash" not in blk or hx != blk["hash"]:
return False, f"Hash mismatch at index {i}"
if not blk["hash"].startswith(prefix):
return False, f"Proof-of-work broken at index {i}"
if i == 0:
# genesis
if blk["prev_hash"] != "0" * 64 or blk["index"] != 0:
return False, "Invalid genesis block."
else:
prev = chain[i - 1]
if blk["prev_hash"] != prev["hash"]:
return False, f"Broken link at index {i}"
if blk["index"] != i:
return False, f"Index mismatch at {i}"
return True, f"OK · {len(chain)} blocks · difficulty {DIFFICULTY}"
# --------------------------
# Users store
# --------------------------
def load_users() -> dict:
return read_json(USERS_PATH, {})
def save_users(users: dict):
write_json(USERS_PATH, users)
def user_exists(username: str) -> bool:
users = load_users()
return username.lower() in users
def create_user(username: str, password: str) -> bool:
u = username.lower().strip()
if not u or not password:
return False
users = load_users()
if u in users:
return False
hpw = hash_password(password)
users[u] = {
"username": u,
"password": hpw,
"created_at": utc_iso(),
}
save_users(users)
# add event to chain
add_event_to_chain({"type": "register", "username": u})
return True
def authenticate(username: str, password: str) -> bool:
u = username.lower().strip()
users = load_users()
if u not in users:
# Also log failed attempt to chain for auditability (no password stored)
add_event_to_chain({"type": "login_fail", "username": u})
return False
ok = verify_password(password, users[u]["password"])
add_event_to_chain({"type": "login_ok" if ok else "login_fail", "username": u})
return ok
# --------------------------
# GUI: CustomTkinter App
# --------------------------
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title(APP_TITLE)
self.geometry("920x620")
self.minsize(860, 560)
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue") # subtle accent
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
self.header = ctk.CTkFrame(self, corner_radius=0)
self.header.grid(row=0, column=0, sticky="ew")
self.header.grid_columnconfigure(2, weight=1)
self.logo = ctk.CTkLabel(self.header, text="π", font=("Segoe UI Emoji", 24))
self.logo.grid(row=0, column=0, padx=(14, 6), pady=10)
self.title_lbl = ctk.CTkLabel(self.header, text=APP_TITLE, font=("Segoe UI", 20, "bold"))
self.title_lbl.grid(row=0, column=1, padx=(0, 12))
self.status_lbl = ctk.CTkLabel(self.header, text="Chain: validating…", text_color=("gray70", "gray70"))
self.status_lbl.grid(row=0, column=3, padx=14)
self.body = ctk.CTkFrame(self, fg_color="transparent")
self.body.grid(row=1, column=0, sticky="nsew", padx=16, pady=16)
self.body.grid_rowconfigure(0, weight=1)
self.body.grid_columnconfigure(0, weight=1)
self.tabs = ctk.CTkTabview(self.body, segmented_button_selected_color=None)
self.tabs.grid(row=0, column=0, sticky="nsew")
self.tab_login = self.tabs.add("Login")
self.tab_register = self.tabs.add("Register")
self.tab_chain = self.tabs.add("Blockchain")
self.tab_settings = self.tabs.add("Settings")
self.build_login_tab()
self.build_register_tab()
self.build_chain_tab()
self.build_settings_tab()
self.after(150, self.refresh_chain_status)
# ------------- Login tab
def build_login_tab(self):
p = self.tab_login
for i in range(8):
p.grid_rowconfigure(i, weight=0)
p.grid_rowconfigure(9, weight=1)
p.grid_columnconfigure(0, weight=1)
p.grid_columnconfigure(1, weight=1)
card = ctk.CTkFrame(p, corner_radius=20)
card.grid(row=0, column=0, columnspan=2, padx=40, pady=40, sticky="n")
card.grid_columnconfigure(0, weight=1)
heading = ctk.CTkLabel(card, text="Welcome back", font=("Segoe UI", 22, "bold"))
heading.grid(row=0, column=0, padx=20, pady=(20, 6))
sub = ctk.CTkLabel(card, text="Sign in securely • Events are chained on the ledger", text_color=("gray70", "gray70"))
sub.grid(row=1, column=0, padx=20, pady=(0, 16))
self.login_user = ctk.CTkEntry(card, placeholder_text="Username", width=360)
self.login_user.grid(row=2, column=0, padx=20, pady=8)
self.login_pass = ctk.CTkEntry(card, placeholder_text="Password", show="•", width=360)
self.login_pass.grid(row=3, column=0, padx=20, pady=8)
self.show_pw_var = ctk.BooleanVar(value=False)
show_pw = ctk.CTkCheckBox(card, text="Show password", variable=self.show_pw_var, command=self.toggle_login_pw)
show_pw.grid(row=4, column=0, padx=20, pady=(0, 12))
btn = ctk.CTkButton(card, text="Sign In", height=40, command=self.on_login)
btn.grid(row=5, column=0, padx=20, pady=(8, 20), sticky="nsew")
self.login_msg = ctk.CTkLabel(card, text="", text_color=("gray80", "gray80"))
self.login_msg.grid(row=6, column=0, padx=20, pady=(0, 20))
def toggle_login_pw(self):
self.login_pass.configure(show="" if self.show_pw_var.get() else "•")
def on_login(self):
u = self.login_user.get().strip()
p = self.login_pass.get()
if not u or not p:
self.toast("Please enter username and password.", kind="warn")
return
ok = authenticate(u, p)
if ok:
self.toast(f"Welcome, {u}!", kind="ok")
self.login_msg.configure(text=f"✅ Authenticated as {u}")
else:
self.toast("Invalid credentials.", kind="error")
self.login_msg.configure(text="❌ Login failed")
self.refresh_chain_status()
# ------------- Register tab
def build_register_tab(self):
p = self.tab_register
p.grid_columnconfigure(0, weight=1)
p.grid_rowconfigure(9, weight=1)
card = ctk.CTkFrame(p, corner_radius=20)
card.grid(row=0, column=0, padx=40, pady=40, sticky="n")
card.grid_columnconfigure(0, weight=1)
heading = ctk.CTkLabel(card, text="Create a secure account", font=("Segoe UI", 22, "bold"))
heading.grid(row=0, column=0, padx=20, pady=(20, 6))
self.reg_user = ctk.CTkEntry(card, placeholder_text="Username (unique)", width=360)
self.reg_user.grid(row=1, column=0, padx=20, pady=8)
self.reg_pass = ctk.CTkEntry(card, placeholder_text="Password", show="•", width=360)
self.reg_pass.grid(row=2, column=0, padx=20, pady=8)
self.reg_pass2 = ctk.CTkEntry(card, placeholder_text="Confirm password", show="•", width=360)
self.reg_pass2.grid(row=3, column=0, padx=20, pady=8)
self.reg_show_var = ctk.BooleanVar(value=False)
reg_show = ctk.CTkCheckBox(card, text="Show passwords", variable=self.reg_show_var, command=self.toggle_reg_pw)
reg_show.grid(row=4, column=0, padx=20, pady=(0, 12))
self.policy_lbl = ctk.CTkLabel(card, text="Tip: use 10+ chars, mix of letters, numbers, symbols.", text_color=("gray70", "gray70"))
self.policy_lbl.grid(row=5, column=0, padx=20, pady=(0, 8))
btn = ctk.CTkButton(card, text="Create Account", height=40, command=self.on_register)
btn.grid(row=6, column=0, padx=20, pady=(8, 20), sticky="nsew")
self.reg_msg = ctk.CTkLabel(card, text="", text_color=("gray80", "gray80"))
self.reg_msg.grid(row=7, column=0, padx=20, pady=(0, 20))
def toggle_reg_pw(self):
show = "" if self.reg_show_var.get() else "•"
self.reg_pass.configure(show=show)
self.reg_pass2.configure(show=show)
def on_register(self):
u = self.reg_user.get().strip()
p1 = self.reg_pass.get()
p2 = self.reg_pass2.get()
if not u or not p1 or not p2:
self.toast("Fill all fields.", kind="warn")
return
if p1 != p2:
self.toast("Passwords do not match.", kind="error")
self.reg_msg.configure(text="❌ Passwords do not match")
return
if len(p1) < 8:
self.toast("Use at least 8 characters.", kind="warn")
return
if user_exists(u):
self.toast("Username already exists.", kind="error")
self.reg_msg.configure(text="❌ Username taken")
return
ok = create_user(u, p1)
if ok:
self.toast("Account created! You can login now.", kind="ok")
self.reg_msg.configure(text="✅ Account created")
self.reg_user.delete(0, "end")
self.reg_pass.delete(0, "end")
self.reg_pass2.delete(0, "end")
self.refresh_chain_status()
else:
self.toast("Failed to create user.", kind="error")
self.reg_msg.configure(text="❌ Failed to create user")
# ------------- Chain tab
def build_chain_tab(self):
p = self.tab_chain
for i in range(8):
p.grid_rowconfigure(i, weight=0)
p.grid_rowconfigure(8, weight=1)
p.grid_columnconfigure(0, weight=1)
p.grid_columnconfigure(1, weight=1)
top = ctk.CTkFrame(p)
top.grid(row=0, column=0, columnspan=2, sticky="ew", padx=20, pady=(20, 10))
top.grid_columnconfigure(1, weight=1)
self.chain_status = ctk.CTkLabel(top, text="Status: —")
self.chain_status.grid(row=0, column=0, padx=(10, 10), pady=10, sticky="w")
verify_btn = ctk.CTkButton(top, text="Verify Chain", command=self.on_verify_chain, width=150)
verify_btn.grid(row=0, column=2, padx=10, pady=10)
export_btn = ctk.CTkButton(top, text="Export chain.json", command=self.on_export_chain, width=150)
export_btn.grid(row=0, column=3, padx=10, pady=10)
self.chain_list = ctk.CTkTextbox(p, height=360)
self.chain_list.grid(row=1, column=0, columnspan=2, sticky="nsew", padx=20, pady=(0, 20))
self.refresh_chain_view()
def on_verify_chain(self):
ok, msg = validate_chain()
self.chain_status.configure(text=f"Status: {'✅' if ok else '❌'} {msg}")
self.toast(("Chain OK" if ok else "Chain invalid") + f": {msg}", kind=("ok" if ok else "error"))
def on_export_chain(self):
# Already exported continuously; just show location
self.toast(f"Exported at {CHAIN_PATH}", kind="ok")
def refresh_chain_view(self):
chain = load_chain()
self.chain_list.configure(state="normal")
self.chain_list.delete("1.0", "end")
for blk in chain:
pretty = json.dumps(blk, indent=2)
self.chain_list.insert("end", pretty + "\n")
self.chain_list.configure(state="disabled")
# ------------- Settings tab
def build_settings_tab(self):
p = self.tab_settings
p.grid_columnconfigure(0, weight=1)
p.grid_rowconfigure(5, weight=1)
theme_lbl = ctk.CTkLabel(p, text="Appearance", font=("Segoe UI", 16, "bold"))
theme_lbl.grid(row=0, column=0, padx=20, pady=(24, 6), sticky="w")
theme_row = ctk.CTkFrame(p)
theme_row.grid(row=1, column=0, padx=20, pady=6, sticky="w")
ctk.CTkLabel(theme_row, text="Theme:").grid(row=0, column=0, padx=(10,8), pady=10)
self.theme_var = ctk.StringVar(value="Dark")
theme_opt = ctk.CTkOptionMenu(theme_row, values=["Dark", "Light", "System"], variable=self.theme_var, command=self.on_theme)
theme_opt.grid(row=0, column=1, padx=8, pady=10)
diff_lbl = ctk.CTkLabel(p, text="Security", font=("Segoe UI", 16, "bold"))
diff_lbl.grid(row=2, column=0, padx=20, pady=(16, 6), sticky="w")
info = ctk.CTkLabel(p, text="Password hashing: PBKDF2-SHA256 · Chain PoW difficulty: 3", text_color=("gray70","gray70"))
info.grid(row=3, column=0, padx=20, pady=(0, 12), sticky="w")
refresh_btn = ctk.CTkButton(p, text="Reload Chain View", command=self.refresh_chain_view)
refresh_btn.grid(row=4, column=0, padx=20, pady=(6, 6), sticky="w")
def on_theme(self, choice: str):
choice = choice.lower()
if choice == "dark":
ctk.set_appearance_mode("dark")
elif choice == "light":
ctk.set_appearance_mode("light")
else:
ctk.set_appearance_mode("system")
# ------------- Helpers
def toast(self, msg: str, kind: str = "info"):
# Simple inline notification
colors = {
"info": ("#4b5563", "#1f2937"),
"ok": ("#16a34a", "#064e3b"),
"warn": ("#f59e0b", "#7c2d12"),
"error": ("#ef4444", "#7f1d1d"),
}
fg, bg = colors.get(kind, colors["info"])
if hasattr(self, "_toast"):
self._toast.destroy()
self._toast = ctk.CTkLabel(self.header, text=msg, fg_color=bg, text_color="white", corner_radius=12, padx=12, pady=8)
self._toast.grid(row=0, column=4, padx=10)
# Auto-hide
self.after(2600, lambda: (self._toast.destroy() if hasattr(self, "_toast") else None))
def refresh_chain_status(self):
ok, msg = validate_chain()
self.status_lbl.configure(text=f"Chain: {'✅' if ok else '❌'} {msg}")
self.chain_status.configure(text=f"Status: {'✅' if ok else '❌'} {msg}")
self.refresh_chain_view()
# --------------------------
# Boot
# --------------------------
def main():
ensure_data_dir()
# ensure users/chain exist
_ = load_chain()
_ = load_users()
app = App()
app.mainloop()
if __name__ == "__main__":
main()
Comments
Post a Comment