Files
key-generator/main.py
2026-03-12 14:17:39 +09:00

410 lines
15 KiB
Python

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("<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")
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()