commit bc0a546d6f79f7532851e6d295fff765c4dff3a7 Author: bcjang Date: Thu Mar 12 13:31:31 2026 +0900 first diff --git a/README.md b/README.md new file mode 100644 index 0000000..68a7ab2 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Key Generator + +JWT Secret, API Key, Operation Key 등 다양한 보안 키를 생성하는 Python 데스크톱 애플리케이션. + +## 실행 + +```bash +pip install -r requirements.txt +python main.py +``` + +--- + +## 지원 키 타입 + +| 타입 | 비트 | 포맷 | +|------|------|------| +| JWT Secret (HS256) | 256-bit | Hex | +| JWT Secret (HS384) | 384-bit | Hex | +| JWT Secret (HS512) | 512-bit | Hex | +| JWT Secret (Base64URL) | 256-bit | Base64URL | +| API Key `sk-...` | 256-bit | Base64URL | +| Operation Key `ops-...` | 192-bit | Base64URL | +| Random Hex 256-bit | 256-bit | Hex | +| Random Hex 512-bit | 512-bit | Hex | +| Alphanumeric | 256-bit | A-Za-z0-9 | +| UUID v4 | 128-bit | UUID | +| Custom | 자유 | 직접 선택 | + +--- + +## 기능 + +- **Generate** 버튼 또는 `Ctrl+Enter`로 즉시 생성 +- **Copy** 버튼으로 클립보드 복사 +- **대량 생성** 체크박스 활성화 시 최대 20개 한번에 생성, **Copy All**로 전체 복사 +- **Custom** 타입 선택 시 바이트 수(8~512)와 출력 포맷 직접 지정 +- 모든 키는 Python `secrets` 모듈(암호학적 난수) 사용 + +--- + +## 의존성 + +- Python 3.8+ +- customtkinter 5.2+ +- pyperclip 1.9+ diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..4f7112f Binary files /dev/null and b/__pycache__/app.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..e7c08d6 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..a27247e --- /dev/null +++ b/app.py @@ -0,0 +1,161 @@ +import secrets +import hashlib +import hmac +import base64 +import uuid +import os +import string +from flask import Flask, render_template, request, jsonify + +app = Flask(__name__) + + +KEY_CONFIGS = { + "jwt_hs256": { + "label": "JWT Secret (HS256)", + "description": "HMAC-SHA256용 JWT 시크릿 키", + "bytes": 32, + "format": "hex", + }, + "jwt_hs384": { + "label": "JWT Secret (HS384)", + "description": "HMAC-SHA384용 JWT 시크릿 키", + "bytes": 48, + "format": "hex", + }, + "jwt_hs512": { + "label": "JWT Secret (HS512)", + "description": "HMAC-SHA512용 JWT 시크릿 키", + "bytes": 64, + "format": "hex", + }, + "jwt_base64": { + "label": "JWT Secret (Base64URL)", + "description": "Base64URL 인코딩 JWT 시크릿 (256-bit)", + "bytes": 32, + "format": "base64url", + }, + "api_key": { + "label": "API Key", + "description": "sk- 접두사 포함 API 키", + "bytes": 32, + "format": "api_key", + }, + "op_key": { + "label": "Operation Key", + "description": "ops- 접두사 포함 운영 키", + "bytes": 24, + "format": "op_key", + }, + "hex_256": { + "label": "Random Hex (256-bit)", + "description": "순수 랜덤 Hex 문자열", + "bytes": 32, + "format": "hex", + }, + "hex_512": { + "label": "Random Hex (512-bit)", + "description": "순수 랜덤 Hex 문자열 (512-bit)", + "bytes": 64, + "format": "hex", + }, + "alphanumeric": { + "label": "Alphanumeric Key", + "description": "영숫자 조합 랜덤 키", + "bytes": 32, + "format": "alphanumeric", + }, + "uuid_v4": { + "label": "UUID v4", + "description": "표준 UUID v4", + "bytes": 16, + "format": "uuid", + }, + "custom": { + "label": "Custom Length", + "description": "직접 바이트 수 지정", + "bytes": None, + "format": "hex", + }, +} + + +def generate_key(key_type: str, custom_bytes: int = 32, custom_format: str = "hex") -> dict: + config = KEY_CONFIGS.get(key_type) + if not config: + raise ValueError(f"Unknown key type: {key_type}") + + byte_length = config["bytes"] if config["bytes"] is not None else custom_bytes + fmt = custom_format if key_type == "custom" else config["format"] + + raw = secrets.token_bytes(byte_length) + + if fmt == "hex": + key = raw.hex() + elif fmt == "base64url": + key = base64.urlsafe_b64encode(raw).rstrip(b"=").decode() + elif fmt == "api_key": + encoded = base64.urlsafe_b64encode(raw).rstrip(b"=").decode() + key = f"sk-{encoded}" + elif fmt == "op_key": + encoded = base64.urlsafe_b64encode(raw).rstrip(b"=").decode() + key = f"ops-{encoded}" + elif fmt == "alphanumeric": + alphabet = string.ascii_letters + string.digits + key = "".join(secrets.choice(alphabet) for _ in range(byte_length)) + elif fmt == "uuid": + key = str(uuid.uuid4()) + else: + key = raw.hex() + + return { + "key": key, + "type": key_type, + "label": config["label"], + "bits": byte_length * 8, + "length": len(key), + "algorithm": fmt.upper(), + } + + +@app.route("/") +def index(): + return render_template("index.html", key_configs=KEY_CONFIGS) + + +@app.route("/api/generate", methods=["POST"]) +def api_generate(): + data = request.get_json() or {} + key_type = data.get("type", "jwt_hs256") + custom_bytes = int(data.get("custom_bytes", 32)) + custom_format = data.get("custom_format", "hex") + + if custom_bytes < 8: + custom_bytes = 8 + elif custom_bytes > 512: + custom_bytes = 512 + + try: + result = generate_key(key_type, custom_bytes, custom_format) + return jsonify({"success": True, "data": result}) + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 400 + + +@app.route("/api/generate/bulk", methods=["POST"]) +def api_generate_bulk(): + data = request.get_json() or {} + key_type = data.get("type", "jwt_hs256") + count = min(int(data.get("count", 5)), 20) + custom_bytes = int(data.get("custom_bytes", 32)) + custom_format = data.get("custom_format", "hex") + + try: + results = [generate_key(key_type, custom_bytes, custom_format) for _ in range(count)] + return jsonify({"success": True, "data": results}) + except Exception as e: + return jsonify({"success": False, "error": str(e)}), 400 + + +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=5050) diff --git a/main.py b/main.py new file mode 100644 index 0000000..d2b805d --- /dev/null +++ b/main.py @@ -0,0 +1,378 @@ +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("", 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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eb15e38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +customtkinter>=5.2.0 +pyperclip>=1.9.0 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..b0bcc6f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,631 @@ + + + + + + Key Generator + + + + +
+

+ + + + + Key Generator +

+

JWT Secret · API Key · Operation Key · UUID · Random Hex

+
+ +
+ + + +
+ {% for key, cfg in key_configs.items() %} + + {% endfor %} +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + + + +
+ + + + + + + +
+ + + + +