first
This commit is contained in:
46
README.md
Normal file
46
README.md
Normal file
@@ -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+
|
||||||
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-312.pyc
Normal file
BIN
__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
161
app.py
Normal file
161
app.py
Normal file
@@ -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)
|
||||||
378
main.py
Normal file
378
main.py
Normal file
@@ -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("<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()
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
customtkinter>=5.2.0
|
||||||
|
pyperclip>=1.9.0
|
||||||
631
templates/index.html
Normal file
631
templates/index.html
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Key Generator</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0f1117;
|
||||||
|
--surface: #1a1d27;
|
||||||
|
--surface2: #22263a;
|
||||||
|
--border: #2e3250;
|
||||||
|
--accent: #6c63ff;
|
||||||
|
--accent-hover: #7c74ff;
|
||||||
|
--accent-dim: rgba(108, 99, 255, 0.15);
|
||||||
|
--success: #22c55e;
|
||||||
|
--success-dim: rgba(34, 197, 94, 0.12);
|
||||||
|
--text: #e2e8f0;
|
||||||
|
--text-muted: #8892a4;
|
||||||
|
--text-dim: #4a5568;
|
||||||
|
--mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 16px 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
background: linear-gradient(135deg, #6c63ff, #a78bfa);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
header p {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main card */
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section label */
|
||||||
|
.section-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Key type grid */
|
||||||
|
.type-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-btn {
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.15s;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.type-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
.type-btn.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent);
|
||||||
|
}
|
||||||
|
.type-btn .btn-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.type-btn .btn-desc {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 3px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom options */
|
||||||
|
.custom-options {
|
||||||
|
display: none;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.custom-options.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.field-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.field-group label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.field-group input,
|
||||||
|
.field-group select {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.field-group input:focus,
|
||||||
|
.field-group select:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.field-group select option {
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Divider */
|
||||||
|
.divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 8px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulk toggle */
|
||||||
|
.bulk-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.bulk-row label {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.bulk-row input[type=number] {
|
||||||
|
width: 64px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
outline: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 38px;
|
||||||
|
height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toggle-switch input { display: none; }
|
||||||
|
.toggle-track {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked ~ .toggle-track { background: var(--accent); }
|
||||||
|
.toggle-thumb {
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-switch input:checked ~ .toggle-track .toggle-thumb {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate button */
|
||||||
|
.generate-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
transition: background 0.15s, transform 0.1s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.generate-btn:hover { background: var(--accent-hover); }
|
||||||
|
.generate-btn:active { transform: scale(0.99); }
|
||||||
|
.generate-btn .spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(255,255,255,0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.6s linear infinite;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.generate-btn.loading .spinner { display: block; }
|
||||||
|
.generate-btn.loading .btn-text { opacity: 0.7; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* Result area */
|
||||||
|
.result-area {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.result-single {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.key-display {
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px 16px 14px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #a5f3fc;
|
||||||
|
min-height: 56px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.key-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.meta-tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.meta-tag.highlight {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy button */
|
||||||
|
.copy-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 7px 14px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
.copy-btn.copied {
|
||||||
|
border-color: var(--success);
|
||||||
|
color: var(--success);
|
||||||
|
background: var(--success-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulk results */
|
||||||
|
.bulk-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.bulk-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
.bulk-index {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--mono);
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
.bulk-key {
|
||||||
|
flex: 1;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #a5f3fc;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.bulk-copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.bulk-copy:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.bulk-copy.copied {
|
||||||
|
border-color: var(--success);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
.bulk-copy-all {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon SVG */
|
||||||
|
svg { display: inline-block; vertical-align: middle; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="url(#g)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:8px">
|
||||||
|
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#6c63ff"/><stop offset="100%" stop-color="#a78bfa"/></linearGradient></defs>
|
||||||
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||||
|
</svg>
|
||||||
|
Key Generator
|
||||||
|
</h1>
|
||||||
|
<p>JWT Secret · API Key · Operation Key · UUID · Random Hex</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
|
||||||
|
<!-- Key Type Selection -->
|
||||||
|
<div class="section-label">키 타입 선택</div>
|
||||||
|
<div class="type-grid" id="typeGrid">
|
||||||
|
{% for key, cfg in key_configs.items() %}
|
||||||
|
<button class="type-btn {% if key == 'jwt_hs256' %}active{% endif %}"
|
||||||
|
data-type="{{ key }}"
|
||||||
|
onclick="selectType('{{ key }}', this)">
|
||||||
|
<div class="btn-label">{{ cfg.label }}</div>
|
||||||
|
<div class="btn-desc">{{ cfg.description }}</div>
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Options (visible when custom is selected) -->
|
||||||
|
<div class="custom-options" id="customOptions">
|
||||||
|
<div class="field-group">
|
||||||
|
<label>바이트 수 (8 ~ 512)</label>
|
||||||
|
<input type="number" id="customBytes" value="32" min="8" max="512" />
|
||||||
|
</div>
|
||||||
|
<div class="field-group">
|
||||||
|
<label>출력 포맷</label>
|
||||||
|
<select id="customFormat">
|
||||||
|
<option value="hex">Hex</option>
|
||||||
|
<option value="base64url">Base64URL</option>
|
||||||
|
<option value="alphanumeric">Alphanumeric</option>
|
||||||
|
<option value="api_key">API Key (sk- prefix)</option>
|
||||||
|
<option value="op_key">Op Key (ops- prefix)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="divider" />
|
||||||
|
|
||||||
|
<!-- Bulk toggle -->
|
||||||
|
<div class="bulk-row">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="bulkToggle" onchange="toggleBulk()" />
|
||||||
|
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||||
|
</label>
|
||||||
|
<label for="bulkToggle" style="cursor:pointer">대량 생성</label>
|
||||||
|
<input type="number" id="bulkCount" value="5" min="1" max="20"
|
||||||
|
style="display:none" />
|
||||||
|
<label id="bulkCountLabel" style="display:none">개</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate Button -->
|
||||||
|
<button class="generate-btn" id="generateBtn" onclick="generate()">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span class="btn-text">Generate</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div class="result-area" id="resultArea" style="display:none">
|
||||||
|
<div class="section-label" style="margin-bottom:12px">생성된 키</div>
|
||||||
|
|
||||||
|
<!-- Single result -->
|
||||||
|
<div class="result-single" id="resultSingle">
|
||||||
|
<div class="key-display" id="keyDisplay"></div>
|
||||||
|
<div class="key-meta" id="keyMeta"></div>
|
||||||
|
<div class="copy-row">
|
||||||
|
<button class="copy-btn" id="copyBtn" onclick="copyKey()">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk result -->
|
||||||
|
<div id="resultBulk" style="display:none">
|
||||||
|
<div class="bulk-list" id="bulkList"></div>
|
||||||
|
<div class="bulk-copy-all">
|
||||||
|
<button class="copy-btn" onclick="copyAll()">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg>
|
||||||
|
Copy All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let selectedType = 'jwt_hs256';
|
||||||
|
let bulkMode = false;
|
||||||
|
let lastBulkKeys = [];
|
||||||
|
|
||||||
|
function selectType(type, el) {
|
||||||
|
selectedType = type;
|
||||||
|
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
el.classList.add('active');
|
||||||
|
|
||||||
|
const customOpts = document.getElementById('customOptions');
|
||||||
|
if (type === 'custom') {
|
||||||
|
customOpts.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
customOpts.classList.remove('visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBulk() {
|
||||||
|
bulkMode = document.getElementById('bulkToggle').checked;
|
||||||
|
const countInput = document.getElementById('bulkCount');
|
||||||
|
const countLabel = document.getElementById('bulkCountLabel');
|
||||||
|
countInput.style.display = bulkMode ? 'block' : 'none';
|
||||||
|
countLabel.style.display = bulkMode ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
const btn = document.getElementById('generateBtn');
|
||||||
|
btn.classList.add('loading');
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: selectedType,
|
||||||
|
custom_bytes: parseInt(document.getElementById('customBytes').value) || 32,
|
||||||
|
custom_format: document.getElementById('customFormat').value,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bulkMode) {
|
||||||
|
const count = parseInt(document.getElementById('bulkCount').value) || 5;
|
||||||
|
const res = await fetch('/api/generate/bulk', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...payload, count }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.success) renderBulk(json.data);
|
||||||
|
} else {
|
||||||
|
const res = await fetch('/api/generate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (json.success) renderSingle(json.data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
btn.classList.remove('loading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSingle(data) {
|
||||||
|
document.getElementById('resultArea').style.display = 'block';
|
||||||
|
document.getElementById('resultSingle').style.display = 'block';
|
||||||
|
document.getElementById('resultBulk').style.display = 'none';
|
||||||
|
|
||||||
|
document.getElementById('keyDisplay').textContent = data.key;
|
||||||
|
|
||||||
|
const meta = document.getElementById('keyMeta');
|
||||||
|
meta.innerHTML = `
|
||||||
|
<span class="meta-tag highlight">${data.label}</span>
|
||||||
|
<span class="meta-tag">${data.bits} bit</span>
|
||||||
|
<span class="meta-tag">${data.algorithm}</span>
|
||||||
|
<span class="meta-tag">${data.length} chars</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
|
copyBtn.classList.remove('copied');
|
||||||
|
copyBtn.innerHTML = `
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg> Copy`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBulk(items) {
|
||||||
|
lastBulkKeys = items.map(i => i.key);
|
||||||
|
document.getElementById('resultArea').style.display = 'block';
|
||||||
|
document.getElementById('resultSingle').style.display = 'none';
|
||||||
|
document.getElementById('resultBulk').style.display = 'block';
|
||||||
|
|
||||||
|
const list = document.getElementById('bulkList');
|
||||||
|
list.innerHTML = items.map((item, idx) => `
|
||||||
|
<div class="bulk-item">
|
||||||
|
<span class="bulk-index">#${idx + 1}</span>
|
||||||
|
<span class="bulk-key">${item.key}</span>
|
||||||
|
<button class="bulk-copy" onclick="copyBulkItem(this, '${escHtml(item.key)}')">Copy</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return s.replace(/'/g, "\\'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyKey() {
|
||||||
|
const key = document.getElementById('keyDisplay').textContent;
|
||||||
|
navigator.clipboard.writeText(key).then(() => {
|
||||||
|
const btn = document.getElementById('copyBtn');
|
||||||
|
btn.classList.add('copied');
|
||||||
|
btn.innerHTML = `
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg> Copied!`;
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
btn.innerHTML = `
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg> Copy`;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyBulkItem(btn, key) {
|
||||||
|
navigator.clipboard.writeText(key).then(() => {
|
||||||
|
btn.classList.add('copied');
|
||||||
|
btn.textContent = 'Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
btn.textContent = 'Copy';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyAll() {
|
||||||
|
const text = lastBulkKeys.join('\n');
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
alert('전체 키가 클립보드에 복사되었습니다!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter key shortcut
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) generate();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user