import secrets import base64 import uuid import string import pyperclip from cryptography.hazmat.primitives.asymmetric import rsa, ec from cryptography.hazmat.primitives import serialization import customtkinter as ctk from tkinter import messagebox ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") KEY_TYPES = [ ("JWT Key Pair (RS256)", "jwt_rs256", "RSA 2048-bit · PEM", 256, "rsa_keypair"), ("JWT Key Pair (ES256)", "jwt_es256", "EC P-256 · PEM", 32, "ec_keypair"), ("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: if fmt == "rsa_keypair": private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) priv_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ).decode() pub_pem = private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ).decode() return priv_pem + "\n" + pub_pem if fmt == "ec_keypair": private_key = ec.generate_private_key(ec.SECP256R1()) priv_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ).decode() pub_pem = private_key.public_key().public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, ).decode() return priv_pem + "\n" + pub_pem 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("", 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") fmt_display = {"rsa_keypair": "RS256", "ec_keypair": "ES256"}.get(fmt, fmt.upper()) self._meta_label.configure( text=f"{entry[0]} · {bits}-bit · {fmt_display} · {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()