From bc0a546d6f79f7532851e6d295fff765c4dff3a7 Mon Sep 17 00:00:00 2001 From: bcjang Date: Thu, 12 Mar 2026 13:31:31 +0900 Subject: [PATCH] first --- README.md | 46 +++ __pycache__/app.cpython-312.pyc | Bin 0 -> 6075 bytes __pycache__/main.cpython-312.pyc | Bin 0 -> 19477 bytes app.py | 161 ++++++++ main.py | 378 ++++++++++++++++++ requirements.txt | 2 + templates/index.html | 631 +++++++++++++++++++++++++++++++ 7 files changed, 1218 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/app.cpython-312.pyc create mode 100644 __pycache__/main.cpython-312.pyc create mode 100644 app.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 templates/index.html 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 0000000000000000000000000000000000000000..4f7112fc142270609b2d76dcaf2cff68312b6957 GIT binary patch literal 6075 zcmcgwZ){W76~FI2+t2@<*a=BUA#Q;Xhb93MC{RiX2@w7iPy@6WsOR{;Bu@M%z3;gs zo>Q5&QUmQoveG5$Xq9T!8U)j_l@F-ew3bTKqH~Uhuu=+$F|qC*MFwkZQq_93a6yB^ZsU`i=obl2t=zl!X1tVmpmnnjW+R z9ZTC;4|W!7Z`!c&L2c+*T6-teURJorU4B{G+;r^ugPzzr7|gTo%=_0ff4q46;^NKL zz!B7S1Qm~J>R`Gt5{iq>NA|f7NCUCQU=F#r&Yc+_xjizRdF`tE&V{RY-koq$0XQdB z^!EjQz0yG3cDPR>etI@c(G7~xK6wev%zM`|V`teRmxGXMi1x7wN}l&ea5U_0lltBH zMa3#XBr|j=Gc@MD{lTT%Kl_M{-@bZ1^TBXt^jiD@EjTB(mDrm9ujQ`MB;!TRp}uZ^ zBo>xPFu+#MERLPc3|#>e?#zdiciz3M*`_sArno-P&(;<&9PCM&SHgHohR>CW!zM>NicJNTI!~^c2eF`VvSBHiJS! zknM^-Hvr)|fqseJ=e6;oOITDEEJpqf_KeKlmLI_$JXHxJJ@-K|@+nw0|A>|?dCP-f*~%>MR!X#^=~<>x z3vIzD4Ht__9QgAlZ0n(@T~vllogB8mVN_OV0}CF}nt%4RJA%__gf%(XnpP@RnvU|g zM)f?QQ46hhiv_b3{g8(x!~_B#E}}l1(Ba}ZAVQ%Libzora|ka!OvZCzl$4P`;eYU3Q;jUZc1Vg$!-?CS{QmLQZKu{Z)yvTs2}q}4 zn{{1MguXUWa?0P=SKl|Fnzqxf{ywrJUX>e^=(AG4JroW2L-LOLyg(3UmFd~s{};NS zL{p^)rnl^$+;FjLG%^wye{ptMJ@DVjbnmZMJ;9MuAczU7Tl5$`233H0RUJKP)~)K` z`GkFhSZr5K{!mwx1eNZvr$n_J*nPs+a`@1meS6zgp-WO!^KpMDChaC9N=P-#r&^R~ zuN29~Uv^QdAsg1!Vt9&Wf2ZW@*xV#V0#PhMK$aCqK~w{lvOHxYU8>D52ZBLgNKzDu zz*=yyi>^u66AePHLi~{~NfkhrZAR78=b%XyV8uZ9!9F}@vXX9-x;=5zWf=m|NN2E1 zwa|wFCZ_L}Ds+YwRUrN|q@K!%*;Qdu3!F?dbGKG@l66$VT6h;^A9S@-(GhSRIQltT z`32uO-`I;OTXk~J-z>#*_VTI9x{2-y?^MN>lzr=zaqG=B>n8So=$l%zbGUk}?B@-i zg^~x-m8-`4XDS=c)lDA1-Zs5yce3RhbMdTs#k6_FSZ~T)n{2r$nuny<5@~z!g|2g5 zZw1c_X_Ix>GGnTovzN@;tEcVN<8sQrZq~kW+P-nJDP`X>Ykzjy{%p$Le2&SQW=s`x z&hlAj?XNK6;GhW8F{9g1evjOVK!O{--1 zh2ecu_LW&R`;2MjoU3BiwRYOI_Gn_(#yPg^{rtL*^1F6yjf6!m={c_8Gpi=e& zStY~y6?69Di>pU#M{3`BGG(tiC*0IFJS5B;P?0NLRGuy_8xu#W(nXbH{t?HVvvfG{ zPEESBeDuV~iFZ!kwTV{4JQDSWdvG&I^dTlWDL@qHiqA z$F^TP`K6_KO4rP!K9HR2<`Smqyh8^eaN%Hgq{zJ zDA`A-4%i@kOQWNg=^GFW1SA>a6!t6rJ(qcM6oys3M5E)aVYoDpk6c$a0fwHFE{Q-%6myw;odko|lH2)LPh$P2Cipohc)yhVyT$rL?F`7@2mtgc& z_{pC@MV-(&YpI;JRHiH|W-P15oypd;!*!wmT>qG1+>&xUnQWVLEgM!w`$zi6?UQ_} ztTE+kN*+v?l#jNJw2k>Dq*O^$^0}L$YuG;{mZi(e&so!^(lG&|QMzpTkab?C(>HT- z#Vf|TC#;jIEX;*`A!&<(pR=Ba6qjaqv>bUa(7&UanLcI?_ zhdy--_kaEopi|(=OG9o65Mg~DD*O-X!yjohY0a$xU`yMA{8*D6)B()$@JQrg)QT?X z1AG?>@TUPV0HOR>0zgrKER&ublo9^zVjUVZfJ7cx*qkt6%L1g@VHOU4gZeWRob*$U z6$VbRK+meUw6TR85!)7K!uC8k#aBfJPG}3j=_mlFst-hC5#<&|*DacWFNM~lq#Z`~ zkmGdxMW`T6pbF#!t+GfA2P4Ew*=K1*UwDQ|a*|Rzp#lgmpz$ZPH=kq>53uyFHWZgK zISrgl9ZyRlvfwN(1gDRBDv>~Fgi6mq^=JsSIxY&stqhPajQdk1HK`)c#Kx4hKDqmm z(3`{Pkz-TGPkwpiRLXgJ`pBujY`D06+&J;%b=z02JySH4BXfrpqyW|q>HXERZ; z+b?&Af*l|i_6NvQu!wBF$VX-JG;pfElm6k2Q1{NhZ9QWtRy_Wh_DtQFb9GFm4Y=aV#Y5ATkWl%tQVi)DY!3 z?rUUc|1Do5+t&zc6A*t#+fr!TKMa0(#fS$5Z?cN*sHkJ?q6 zE^B3qD?ri36d_n|VqKNcRm8d;*QQ?%T@Kb&lP+t@Tg*y9*N#P9t2HS%D3)r|uVqr@ zh10`arPy4x_bc7Tc>(E`vn%~C-a)#0 literal 0 HcmV?d00001 diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7c08d601e533223c4208baee14efab4ee2227dd GIT binary patch literal 19477 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 literal 0 HcmV?d00001 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 %} +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + + + +
+ + + + + + + +
+ + + + +