379 lines
14 KiB
Python
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()
|