diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..f509f12 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "mcp__serena__activate_project", + "mcp__serena__list_dir", + "mcp__serena__get_symbols_overview", + "mcp__serena__find_symbol", + "Bash(python -c \"\nfrom app import generate_key\nr = generate_key\\('jwt_rs256'\\)\nprint\\('RS256 keypair:', r['keypair'], r['algorithm'], r['bits'], 'bit'\\)\nprint\\('private key starts with:', r['key'][:27]\\)\nprint\\('public key starts with:', r['public_key'][:26]\\)\n\ne = generate_key\\('jwt_es256'\\)\nprint\\('ES256 keypair:', e['keypair'], e['algorithm'], e['bits'], 'bit'\\)\nprint\\('private key starts with:', e['key'][:27]\\)\n\nh = generate_key\\('jwt_hs256'\\)\nprint\\('HS256:', h['keypair'], h['algorithm'], h['bits'], 'bit'\\)\n\")" + ] + } +} diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..2e510af --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..43b2d10 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,135 @@ +# the name by which the project can be referenced within Serena +project_name: "key-generator" + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# php_phpactor powershell python python_jedi r +# rego ruby ruby_solargraph rust scala +# swift terraform toml typescript typescript_vts +# vue yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- python + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# The language backend to use for this project. +# If not set, the global setting from serena_config.yml is used. +# Valid values: LSP, JetBrains +# Note: the backend is fixed at startup. If a project with a different backend +# is activated post-init, an error will be returned. +language_backend: + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Note: global ignored_paths from serena_config.yml are also applied additively. +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +# time budget (seconds) per tool call for the retrieval of additional symbol information +# such as docstrings or parameter information. +# This overrides the corresponding setting in the global configuration; see the documentation there. +# If null or missing, use the setting from the global configuration. +symbol_info_budget: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] diff --git a/=41.0.0 b/=41.0.0 new file mode 100644 index 0000000..9f0cb12 --- /dev/null +++ b/=41.0.0 @@ -0,0 +1,3 @@ + +[notice] A new release of pip is available: 25.0.1 -> 26.0.1 +[notice] To update, run: python.exe -m pip install --upgrade pip diff --git a/README.md b/README.md index 68a7ab2..29acf0c 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,21 @@ 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 | 자유 | 직접 선택 | +| 타입 | 비트 | 포맷 | 비고 | +|------|------|------|------| +| JWT Key Pair (RS256) | 2048-bit | PEM | RSA 비대칭 키 쌍 (Private + Public) | +| JWT Key Pair (ES256) | 256-bit | PEM | EC P-256 비대칭 키 쌍 (Private + Public) | +| JWT Secret (HS256) | 256-bit | Hex | HMAC 대칭 키 | +| JWT Secret (HS384) | 384-bit | Hex | HMAC 대칭 키 | +| JWT Secret (HS512) | 512-bit | Hex | HMAC 대칭 키 | +| 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 | 자유 | 직접 선택 | | --- @@ -36,6 +38,7 @@ python main.py - **대량 생성** 체크박스 활성화 시 최대 20개 한번에 생성, **Copy All**로 전체 복사 - **Custom** 타입 선택 시 바이트 수(8~512)와 출력 포맷 직접 지정 - 모든 키는 Python `secrets` 모듈(암호학적 난수) 사용 +- **RS256 / ES256** 선택 시 Private Key와 Public Key를 각각 개별 복사 가능 (웹 UI) --- @@ -44,3 +47,4 @@ python main.py - Python 3.8+ - customtkinter 5.2+ - pyperclip 1.9+ +- cryptography 41.0+ (RS256 / ES256 키 쌍 생성) diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc index 4f7112f..889f287 100644 Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ diff --git a/app.py b/app.py index a27247e..8eef6f1 100644 --- a/app.py +++ b/app.py @@ -5,12 +5,26 @@ import base64 import uuid import os import string +from cryptography.hazmat.primitives.asymmetric import rsa, ec +from cryptography.hazmat.primitives import serialization from flask import Flask, render_template, request, jsonify app = Flask(__name__) KEY_CONFIGS = { + "jwt_rs256": { + "label": "JWT Key Pair (RS256)", + "description": "RSA 2048-bit 비대칭 키 쌍 (PEM)", + "bytes": None, + "format": "rsa_keypair", + }, + "jwt_es256": { + "label": "JWT Key Pair (ES256)", + "description": "EC P-256 비대칭 키 쌍 (PEM)", + "bytes": None, + "format": "ec_keypair", + }, "jwt_hs256": { "label": "JWT Secret (HS256)", "description": "HMAC-SHA256용 JWT 시크릿 키", @@ -85,9 +99,53 @@ def generate_key(key_type: str, custom_bytes: int = 32, custom_format: str = "he 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"] + if fmt == "rsa_keypair": + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + priv_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode() + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode() + return { + "key": priv_pem, + "public_key": pub_pem, + "keypair": True, + "type": key_type, + "label": config["label"], + "bits": 2048, + "length": len(priv_pem), + "algorithm": "RS256", + } + + if fmt == "ec_keypair": + private_key = ec.generate_private_key(ec.SECP256R1()) + priv_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode() + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode() + return { + "key": priv_pem, + "public_key": pub_pem, + "keypair": True, + "type": key_type, + "label": config["label"], + "bits": 256, + "length": len(priv_pem), + "algorithm": "ES256", + } + + byte_length = config["bytes"] if config["bytes"] is not None else custom_bytes raw = secrets.token_bytes(byte_length) if fmt == "hex": @@ -110,6 +168,7 @@ def generate_key(key_type: str, custom_bytes: int = 32, custom_format: str = "he return { "key": key, + "keypair": False, "type": key_type, "label": config["label"], "bits": byte_length * 8, diff --git a/main.py b/main.py index d2b805d..6d62b40 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,8 @@ import base64 import uuid import string import pyperclip +from cryptography.hazmat.primitives.asymmetric import rsa, ec +from cryptography.hazmat.primitives import serialization import customtkinter as ctk from tkinter import messagebox @@ -10,6 +12,8 @@ ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") KEY_TYPES = [ + ("JWT Key Pair (RS256)", "jwt_rs256", "RSA 2048-bit · PEM", 256, "rsa_keypair"), + ("JWT Key Pair (ES256)", "jwt_es256", "EC P-256 · PEM", 32, "ec_keypair"), ("JWT Secret (HS256)", "jwt_hs256", "32 bytes · Hex", 32, "hex"), ("JWT Secret (HS384)", "jwt_hs384", "48 bytes · Hex", 48, "hex"), ("JWT Secret (HS512)", "jwt_hs512", "64 bytes · Hex", 64, "hex"), @@ -34,6 +38,32 @@ FORMAT_MAP = { def generate_key(byte_length: int, fmt: str) -> str: + if fmt == "rsa_keypair": + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + priv_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode() + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode() + return priv_pem + "\n" + pub_pem + + if fmt == "ec_keypair": + private_key = ec.generate_private_key(ec.SECP256R1()) + priv_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ).decode() + pub_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode() + return priv_pem + "\n" + pub_pem + raw = secrets.token_bytes(byte_length) if fmt == "hex": return raw.hex() @@ -301,8 +331,9 @@ class App(ctk.CTk): self._key_box.insert("1.0", key) self._key_box.configure(state="disabled") + fmt_display = {"rsa_keypair": "RS256", "ec_keypair": "ES256"}.get(fmt, fmt.upper()) self._meta_label.configure( - text=f"{entry[0]} · {bits}-bit · {fmt.upper()} · {len(key)} chars" + text=f"{entry[0]} · {bits}-bit · {fmt_display} · {len(key)} chars" ) # Reset copy button diff --git a/requirements.txt b/requirements.txt index eb15e38..b344acd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ customtkinter>=5.2.0 pyperclip>=1.9.0 +cryptography>=41.0.0 diff --git a/templates/index.html b/templates/index.html index b0bcc6f..74fdf60 100644 --- a/templates/index.html +++ b/templates/index.html @@ -375,6 +375,18 @@ margin-top: 10px; } + /* Keypair display */ + .keypair-section { margin-bottom: 16px; } + .keypair-section:last-child { margin-bottom: 0; } + .keypair-section-label { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 6px; + } + /* Icon SVG */ svg { display: inline-block; vertical-align: middle; } @@ -389,7 +401,7 @@ Key Generator -
JWT Secret · API Key · Operation Key · UUID · Random Hex
+JWT Secret · RS256 · ES256 · API Key · Operation Key · UUID · Random Hex