Files
key-generator/main.py
bcjang bc0a546d6f first
2026-03-12 13:31:31 +09:00

379 lines
14 KiB
Python

import secrets
import base64
import uuid
import string
import pyperclip
import customtkinter as ctk
from tkinter import messagebox
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
KEY_TYPES = [
("JWT Secret (HS256)", "jwt_hs256", "32 bytes · Hex", 32, "hex"),
("JWT Secret (HS384)", "jwt_hs384", "48 bytes · Hex", 48, "hex"),
("JWT Secret (HS512)", "jwt_hs512", "64 bytes · Hex", 64, "hex"),
("JWT Secret (Base64)", "jwt_base64", "32 bytes · Base64URL", 32, "base64url"),
("API Key (sk-...)", "api_key", "32 bytes · Base64URL", 32, "api_key"),
("Operation Key (ops-)", "op_key", "24 bytes · Base64URL", 24, "op_key"),
("Random Hex 256-bit", "hex_256", "32 bytes · Hex", 32, "hex"),
("Random Hex 512-bit", "hex_512", "64 bytes · Hex", 64, "hex"),
("Alphanumeric", "alphanumeric","32 chars · A-Za-z0-9", 32, "alphanumeric"),
("UUID v4", "uuid_v4", "128-bit · Standard UUID", 16, "uuid"),
("Custom", "custom", "직접 설정", 32, "hex"),
]
FORMATS = ["hex", "base64url", "alphanumeric", "api_key (sk-)", "op_key (ops-)"]
FORMAT_MAP = {
"hex": "hex",
"base64url": "base64url",
"alphanumeric": "alphanumeric",
"api_key (sk-)": "api_key",
"op_key (ops-)": "op_key",
}
def generate_key(byte_length: int, fmt: str) -> str:
raw = secrets.token_bytes(byte_length)
if fmt == "hex":
return raw.hex()
elif fmt == "base64url":
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
elif fmt == "api_key":
return "sk-" + base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
elif fmt == "op_key":
return "ops-" + base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
elif fmt == "alphanumeric":
alpha = string.ascii_letters + string.digits
return "".join(secrets.choice(alpha) for _ in range(byte_length))
elif fmt == "uuid":
return str(uuid.uuid4())
return raw.hex()
class App(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Key Generator")
self.geometry("760x700")
self.resizable(False, False)
self._build_ui()
def _build_ui(self):
# ── Title ──────────────────────────────────────────────
title = ctk.CTkLabel(
self, text=" Key Generator",
font=ctk.CTkFont(size=22, weight="bold"),
anchor="w",
)
title.pack(padx=28, pady=(24, 2), anchor="w")
sub = ctk.CTkLabel(
self,
text="JWT Secret · API Key · Operation Key · UUID · Random Hex",
font=ctk.CTkFont(size=12),
text_color="#8892a4",
anchor="w",
)
sub.pack(padx=30, pady=(0, 16), anchor="w")
# ── Key Type ───────────────────────────────────────────
self._section("키 타입")
self._type_var = ctk.StringVar(value="jwt_hs256")
type_frame = ctk.CTkFrame(self, fg_color="transparent")
type_frame.pack(padx=28, pady=(4, 0), fill="x")
self._type_combo = ctk.CTkComboBox(
type_frame,
values=[t[0] for t in KEY_TYPES],
width=340,
height=36,
font=ctk.CTkFont(size=13),
command=self._on_type_change,
)
self._type_combo.set(KEY_TYPES[0][0])
self._type_combo.pack(side="left")
self._desc_label = ctk.CTkLabel(
type_frame,
text=KEY_TYPES[0][2],
font=ctk.CTkFont(size=11),
text_color="#8892a4",
)
self._desc_label.pack(side="left", padx=(14, 0))
# ── Custom Options ─────────────────────────────────────
self._custom_frame = ctk.CTkFrame(self, fg_color=("#1a1d27", "#1a1d27"), corner_radius=10)
self._custom_frame.pack(padx=28, pady=(12, 0), fill="x")
ctk.CTkLabel(
self._custom_frame, text="바이트 수",
font=ctk.CTkFont(size=11), text_color="#8892a4",
).grid(row=0, column=0, padx=(16, 0), pady=(12, 2), sticky="w")
ctk.CTkLabel(
self._custom_frame, text="출력 포맷",
font=ctk.CTkFont(size=11), text_color="#8892a4",
).grid(row=0, column=1, padx=(20, 0), pady=(12, 2), sticky="w")
self._bytes_entry = ctk.CTkEntry(
self._custom_frame, width=100, height=34,
font=ctk.CTkFont(size=13), placeholder_text="32",
)
self._bytes_entry.insert(0, "32")
self._bytes_entry.grid(row=1, column=0, padx=(16, 0), pady=(0, 14), sticky="w")
self._fmt_combo = ctk.CTkComboBox(
self._custom_frame, values=FORMATS,
width=200, height=34, font=ctk.CTkFont(size=13),
)
self._fmt_combo.set("hex")
self._fmt_combo.grid(row=1, column=1, padx=(20, 0), pady=(0, 14), sticky="w")
self._custom_frame.pack_forget() # hidden by default
# ── Bulk ───────────────────────────────────────────────
self._section("옵션")
bulk_frame = ctk.CTkFrame(self, fg_color="transparent")
bulk_frame.pack(padx=28, pady=(4, 0), fill="x")
self._bulk_var = ctk.BooleanVar(value=False)
bulk_chk = ctk.CTkCheckBox(
bulk_frame, text="대량 생성",
font=ctk.CTkFont(size=13),
variable=self._bulk_var,
command=self._on_bulk_toggle,
)
bulk_chk.pack(side="left")
self._bulk_label = ctk.CTkLabel(
bulk_frame, text="개수",
font=ctk.CTkFont(size=11), text_color="#8892a4",
)
self._bulk_count = ctk.CTkEntry(
bulk_frame, width=64, height=30,
font=ctk.CTkFont(size=13), placeholder_text="5",
)
self._bulk_count.insert(0, "5")
# hidden initially
# ── Generate Button ────────────────────────────────────
self._gen_btn = ctk.CTkButton(
self,
text="Generate",
height=44,
font=ctk.CTkFont(size=14, weight="bold"),
corner_radius=10,
command=self._on_generate,
)
self._gen_btn.pack(padx=28, pady=(20, 0), fill="x")
# ── Result ─────────────────────────────────────────────
self._section("생성된 키")
# Single result
self._result_frame = ctk.CTkFrame(self, fg_color="transparent")
self._result_frame.pack(padx=28, pady=(4, 0), fill="x")
self._key_box = ctk.CTkTextbox(
self._result_frame,
height=72,
font=ctk.CTkFont(family="Consolas", size=12),
fg_color=("#1a1d27", "#1a1d27"),
text_color="#a5f3fc",
corner_radius=10,
wrap="word",
state="disabled",
)
self._key_box.pack(fill="x")
# Meta row
self._meta_label = ctk.CTkLabel(
self._result_frame, text="",
font=ctk.CTkFont(size=11), text_color="#8892a4", anchor="w",
)
self._meta_label.pack(pady=(6, 0), anchor="w")
# Copy button
copy_row = ctk.CTkFrame(self._result_frame, fg_color="transparent")
copy_row.pack(fill="x", pady=(8, 0))
self._copy_btn = ctk.CTkButton(
copy_row,
text="Copy",
width=100, height=34,
font=ctk.CTkFont(size=12),
fg_color=("#22263a", "#22263a"),
hover_color=("#2e3250", "#2e3250"),
border_width=1,
border_color="#2e3250",
corner_radius=8,
command=self._copy_single,
)
self._copy_btn.pack(side="right")
# Bulk result
self._bulk_frame_result = ctk.CTkScrollableFrame(
self, height=200,
fg_color=("#1a1d27", "#1a1d27"),
corner_radius=10,
)
self._bulk_frame_result.pack(padx=28, pady=(4, 0), fill="x")
self._bulk_frame_result.pack_forget()
self._bulk_copy_all_btn = ctk.CTkButton(
self,
text="Copy All",
height=34,
font=ctk.CTkFont(size=12),
fg_color=("#22263a", "#22263a"),
hover_color=("#2e3250", "#2e3250"),
border_width=1,
border_color="#2e3250",
corner_radius=8,
command=self._copy_all,
)
self._bulk_keys = []
# Keyboard shortcut
self.bind("<Control-Return>", lambda e: self._on_generate())
def _section(self, text: str):
lbl = ctk.CTkLabel(
self, text=text.upper(),
font=ctk.CTkFont(size=10, weight="bold"),
text_color="#8892a4", anchor="w",
)
lbl.pack(padx=30, pady=(16, 0), anchor="w")
def _on_type_change(self, value: str):
entry = next((t for t in KEY_TYPES if t[0] == value), None)
if not entry:
return
self._desc_label.configure(text=entry[2])
if entry[1] == "custom":
self._custom_frame.pack(padx=28, pady=(12, 0), fill="x")
else:
self._custom_frame.pack_forget()
def _on_bulk_toggle(self):
if self._bulk_var.get():
self._bulk_label.pack(side="left", padx=(16, 4))
self._bulk_count.pack(side="left")
else:
self._bulk_label.pack_forget()
self._bulk_count.pack_forget()
def _get_selected_type(self):
label = self._type_combo.get()
return next((t for t in KEY_TYPES if t[0] == label), KEY_TYPES[0])
def _on_generate(self):
entry = self._get_selected_type()
_, key_id, _, byte_len, fmt = entry
if key_id == "custom":
try:
byte_len = int(self._bytes_entry.get())
byte_len = max(8, min(512, byte_len))
except ValueError:
byte_len = 32
fmt = FORMAT_MAP.get(self._fmt_combo.get(), "hex")
if self._bulk_var.get():
try:
count = int(self._bulk_count.get())
count = max(1, min(20, count))
except ValueError:
count = 5
self._generate_bulk(byte_len, fmt, count, entry)
else:
self._generate_single(byte_len, fmt, entry)
def _generate_single(self, byte_len, fmt, entry):
key = generate_key(byte_len, fmt)
bits = byte_len * 8
self._key_box.configure(state="normal")
self._key_box.delete("1.0", "end")
self._key_box.insert("1.0", key)
self._key_box.configure(state="disabled")
self._meta_label.configure(
text=f"{entry[0]} · {bits}-bit · {fmt.upper()} · {len(key)} chars"
)
# Reset copy button
self._copy_btn.configure(text="Copy", fg_color=("#22263a", "#22263a"))
# Show single, hide bulk
self._result_frame.pack(padx=28, pady=(4, 0), fill="x")
self._bulk_frame_result.pack_forget()
self._bulk_copy_all_btn.pack_forget()
def _generate_bulk(self, byte_len, fmt, count, entry):
self._bulk_keys = [generate_key(byte_len, fmt) for _ in range(count)]
# Clear old rows
for w in self._bulk_frame_result.winfo_children():
w.destroy()
for i, key in enumerate(self._bulk_keys):
row = ctk.CTkFrame(self._bulk_frame_result, fg_color="transparent")
row.pack(fill="x", pady=3)
ctk.CTkLabel(
row, text=f"#{i+1:02d}",
font=ctk.CTkFont(family="Consolas", size=11),
text_color="#4a5568", width=32, anchor="w",
).pack(side="left", padx=(4, 0))
ctk.CTkLabel(
row, text=key,
font=ctk.CTkFont(family="Consolas", size=11),
text_color="#a5f3fc", anchor="w",
wraplength=500,
).pack(side="left", padx=(6, 0), fill="x", expand=True)
btn = ctk.CTkButton(
row, text="Copy", width=60, height=26,
font=ctk.CTkFont(size=11),
fg_color=("#22263a", "#22263a"),
hover_color=("#2e3250", "#2e3250"),
border_width=1, border_color="#2e3250",
corner_radius=6,
command=lambda k=key, b=None: self._copy_item(k),
)
btn.pack(side="right", padx=(0, 4))
# Show bulk, hide single
self._result_frame.pack_forget()
self._bulk_frame_result.pack(padx=28, pady=(4, 0), fill="x")
self._bulk_copy_all_btn.pack(padx=28, pady=(8, 0), anchor="e")
def _copy_single(self):
key = self._key_box.get("1.0", "end").strip()
if not key:
return
pyperclip.copy(key)
self._copy_btn.configure(text="Copied!", fg_color=("#1a3a2a", "#1a3a2a"))
self.after(2000, lambda: self._copy_btn.configure(
text="Copy", fg_color=("#22263a", "#22263a")
))
def _copy_item(self, key: str):
pyperclip.copy(key)
def _copy_all(self):
if self._bulk_keys:
pyperclip.copy("\n".join(self._bulk_keys))
self._bulk_copy_all_btn.configure(text="Copied!")
self.after(2000, lambda: self._bulk_copy_all_btn.configure(text="Copy All"))
if __name__ == "__main__":
app = App()
app.mainloop()