web>app
This commit is contained in:
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@@ -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\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.serena/.gitignore
vendored
Normal file
2
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/cache
|
||||||
|
/project.local.yml
|
||||||
135
.serena/project.yml
Normal file
135
.serena/project.yml
Normal file
@@ -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: []
|
||||||
3
=41.0.0
Normal file
3
=41.0.0
Normal file
@@ -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
|
||||||
30
README.md
30
README.md
@@ -13,19 +13,21 @@ python main.py
|
|||||||
|
|
||||||
## 지원 키 타입
|
## 지원 키 타입
|
||||||
|
|
||||||
| 타입 | 비트 | 포맷 |
|
| 타입 | 비트 | 포맷 | 비고 |
|
||||||
|------|------|------|
|
|------|------|------|------|
|
||||||
| JWT Secret (HS256) | 256-bit | Hex |
|
| JWT Key Pair (RS256) | 2048-bit | PEM | RSA 비대칭 키 쌍 (Private + Public) |
|
||||||
| JWT Secret (HS384) | 384-bit | Hex |
|
| JWT Key Pair (ES256) | 256-bit | PEM | EC P-256 비대칭 키 쌍 (Private + Public) |
|
||||||
| JWT Secret (HS512) | 512-bit | Hex |
|
| JWT Secret (HS256) | 256-bit | Hex | HMAC 대칭 키 |
|
||||||
| JWT Secret (Base64URL) | 256-bit | Base64URL |
|
| JWT Secret (HS384) | 384-bit | Hex | HMAC 대칭 키 |
|
||||||
| API Key `sk-...` | 256-bit | Base64URL |
|
| JWT Secret (HS512) | 512-bit | Hex | HMAC 대칭 키 |
|
||||||
| Operation Key `ops-...` | 192-bit | Base64URL |
|
| JWT Secret (Base64URL) | 256-bit | Base64URL | |
|
||||||
| Random Hex 256-bit | 256-bit | Hex |
|
| API Key `sk-...` | 256-bit | Base64URL | |
|
||||||
| Random Hex 512-bit | 512-bit | Hex |
|
| Operation Key `ops-...` | 192-bit | Base64URL | |
|
||||||
| Alphanumeric | 256-bit | A-Za-z0-9 |
|
| Random Hex 256-bit | 256-bit | Hex | |
|
||||||
| UUID v4 | 128-bit | UUID |
|
| Random Hex 512-bit | 512-bit | Hex | |
|
||||||
| Custom | 자유 | 직접 선택 |
|
| Alphanumeric | 256-bit | A-Za-z0-9 | |
|
||||||
|
| UUID v4 | 128-bit | UUID | |
|
||||||
|
| Custom | 자유 | 직접 선택 | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ python main.py
|
|||||||
- **대량 생성** 체크박스 활성화 시 최대 20개 한번에 생성, **Copy All**로 전체 복사
|
- **대량 생성** 체크박스 활성화 시 최대 20개 한번에 생성, **Copy All**로 전체 복사
|
||||||
- **Custom** 타입 선택 시 바이트 수(8~512)와 출력 포맷 직접 지정
|
- **Custom** 타입 선택 시 바이트 수(8~512)와 출력 포맷 직접 지정
|
||||||
- 모든 키는 Python `secrets` 모듈(암호학적 난수) 사용
|
- 모든 키는 Python `secrets` 모듈(암호학적 난수) 사용
|
||||||
|
- **RS256 / ES256** 선택 시 Private Key와 Public Key를 각각 개별 복사 가능 (웹 UI)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -44,3 +47,4 @@ python main.py
|
|||||||
- Python 3.8+
|
- Python 3.8+
|
||||||
- customtkinter 5.2+
|
- customtkinter 5.2+
|
||||||
- pyperclip 1.9+
|
- pyperclip 1.9+
|
||||||
|
- cryptography 41.0+ (RS256 / ES256 키 쌍 생성)
|
||||||
|
|||||||
Binary file not shown.
61
app.py
61
app.py
@@ -5,12 +5,26 @@ import base64
|
|||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
KEY_CONFIGS = {
|
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": {
|
"jwt_hs256": {
|
||||||
"label": "JWT Secret (HS256)",
|
"label": "JWT Secret (HS256)",
|
||||||
"description": "HMAC-SHA256용 JWT 시크릿 키",
|
"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:
|
if not config:
|
||||||
raise ValueError(f"Unknown key type: {key_type}")
|
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"]
|
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)
|
raw = secrets.token_bytes(byte_length)
|
||||||
|
|
||||||
if fmt == "hex":
|
if fmt == "hex":
|
||||||
@@ -110,6 +168,7 @@ def generate_key(key_type: str, custom_bytes: int = 32, custom_format: str = "he
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"key": key,
|
"key": key,
|
||||||
|
"keypair": False,
|
||||||
"type": key_type,
|
"type": key_type,
|
||||||
"label": config["label"],
|
"label": config["label"],
|
||||||
"bits": byte_length * 8,
|
"bits": byte_length * 8,
|
||||||
|
|||||||
33
main.py
33
main.py
@@ -3,6 +3,8 @@ import base64
|
|||||||
import uuid
|
import uuid
|
||||||
import string
|
import string
|
||||||
import pyperclip
|
import pyperclip
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
|
||||||
@@ -10,6 +12,8 @@ ctk.set_appearance_mode("dark")
|
|||||||
ctk.set_default_color_theme("blue")
|
ctk.set_default_color_theme("blue")
|
||||||
|
|
||||||
KEY_TYPES = [
|
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 (HS256)", "jwt_hs256", "32 bytes · Hex", 32, "hex"),
|
||||||
("JWT Secret (HS384)", "jwt_hs384", "48 bytes · Hex", 48, "hex"),
|
("JWT Secret (HS384)", "jwt_hs384", "48 bytes · Hex", 48, "hex"),
|
||||||
("JWT Secret (HS512)", "jwt_hs512", "64 bytes · Hex", 64, "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:
|
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)
|
raw = secrets.token_bytes(byte_length)
|
||||||
if fmt == "hex":
|
if fmt == "hex":
|
||||||
return raw.hex()
|
return raw.hex()
|
||||||
@@ -301,8 +331,9 @@ class App(ctk.CTk):
|
|||||||
self._key_box.insert("1.0", key)
|
self._key_box.insert("1.0", key)
|
||||||
self._key_box.configure(state="disabled")
|
self._key_box.configure(state="disabled")
|
||||||
|
|
||||||
|
fmt_display = {"rsa_keypair": "RS256", "ec_keypair": "ES256"}.get(fmt, fmt.upper())
|
||||||
self._meta_label.configure(
|
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
|
# Reset copy button
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
customtkinter>=5.2.0
|
customtkinter>=5.2.0
|
||||||
pyperclip>=1.9.0
|
pyperclip>=1.9.0
|
||||||
|
cryptography>=41.0.0
|
||||||
|
|||||||
@@ -375,6 +375,18 @@
|
|||||||
margin-top: 10px;
|
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 */
|
/* Icon SVG */
|
||||||
svg { display: inline-block; vertical-align: middle; }
|
svg { display: inline-block; vertical-align: middle; }
|
||||||
</style>
|
</style>
|
||||||
@@ -389,7 +401,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Key Generator
|
Key Generator
|
||||||
</h1>
|
</h1>
|
||||||
<p>JWT Secret · API Key · Operation Key · UUID · Random Hex</p>
|
<p>JWT Secret · RS256 · ES256 · API Key · Operation Key · UUID · Random Hex</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -463,6 +475,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Keypair result -->
|
||||||
|
<div id="resultKeypair" style="display:none">
|
||||||
|
<div class="keypair-section">
|
||||||
|
<div class="keypair-section-label">Private Key</div>
|
||||||
|
<div class="key-display" id="privateKeyDisplay" style="white-space:pre;font-size:0.75rem;"></div>
|
||||||
|
<div class="copy-row">
|
||||||
|
<button class="copy-btn" id="copyPrivateBtn" onclick="copyPrivateKey()">
|
||||||
|
<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 Private Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="keypair-section">
|
||||||
|
<div class="keypair-section-label">Public Key</div>
|
||||||
|
<div class="key-display" id="publicKeyDisplay" style="white-space:pre;font-size:0.75rem;"></div>
|
||||||
|
<div class="copy-row">
|
||||||
|
<button class="copy-btn" id="copyPublicBtn" onclick="copyPublicKey()">
|
||||||
|
<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 Public Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="key-meta" id="keyMetaKeypair"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bulk result -->
|
<!-- Bulk result -->
|
||||||
<div id="resultBulk" style="display:none">
|
<div id="resultBulk" style="display:none">
|
||||||
<div class="bulk-list" id="bulkList"></div>
|
<div class="bulk-list" id="bulkList"></div>
|
||||||
@@ -483,6 +524,8 @@
|
|||||||
let selectedType = 'jwt_hs256';
|
let selectedType = 'jwt_hs256';
|
||||||
let bulkMode = false;
|
let bulkMode = false;
|
||||||
let lastBulkKeys = [];
|
let lastBulkKeys = [];
|
||||||
|
let lastBulkData = [];
|
||||||
|
let lastKeypair = null;
|
||||||
|
|
||||||
function selectType(type, el) {
|
function selectType(type, el) {
|
||||||
selectedType = type;
|
selectedType = type;
|
||||||
@@ -541,11 +584,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COPY_ICON = `<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>`;
|
||||||
|
const CHECK_ICON = `<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>`;
|
||||||
|
|
||||||
function renderSingle(data) {
|
function renderSingle(data) {
|
||||||
document.getElementById('resultArea').style.display = 'block';
|
document.getElementById('resultArea').style.display = 'block';
|
||||||
document.getElementById('resultSingle').style.display = 'block';
|
|
||||||
document.getElementById('resultBulk').style.display = 'none';
|
document.getElementById('resultBulk').style.display = 'none';
|
||||||
|
|
||||||
|
if (data.keypair) {
|
||||||
|
lastKeypair = data;
|
||||||
|
document.getElementById('resultSingle').style.display = 'none';
|
||||||
|
document.getElementById('resultKeypair').style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('privateKeyDisplay').textContent = data.key;
|
||||||
|
document.getElementById('publicKeyDisplay').textContent = data.public_key;
|
||||||
|
|
||||||
|
const meta = document.getElementById('keyMetaKeypair');
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
document.getElementById('copyPrivateBtn').classList.remove('copied');
|
||||||
|
document.getElementById('copyPublicBtn').classList.remove('copied');
|
||||||
|
document.getElementById('copyPrivateBtn').innerHTML = `${COPY_ICON} Copy Private Key`;
|
||||||
|
document.getElementById('copyPublicBtn').innerHTML = `${COPY_ICON} Copy Public Key`;
|
||||||
|
} else {
|
||||||
|
lastKeypair = null;
|
||||||
|
document.getElementById('resultSingle').style.display = 'block';
|
||||||
|
document.getElementById('resultKeypair').style.display = 'none';
|
||||||
|
|
||||||
document.getElementById('keyDisplay').textContent = data.key;
|
document.getElementById('keyDisplay').textContent = data.key;
|
||||||
|
|
||||||
const meta = document.getElementById('keyMeta');
|
const meta = document.getElementById('keyMeta');
|
||||||
@@ -558,52 +626,69 @@
|
|||||||
|
|
||||||
const copyBtn = document.getElementById('copyBtn');
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
copyBtn.classList.remove('copied');
|
copyBtn.classList.remove('copied');
|
||||||
copyBtn.innerHTML = `
|
copyBtn.innerHTML = `${COPY_ICON} Copy`;
|
||||||
<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) {
|
function renderBulk(items) {
|
||||||
|
lastBulkData = items;
|
||||||
lastBulkKeys = items.map(i => i.key);
|
lastBulkKeys = items.map(i => i.key);
|
||||||
document.getElementById('resultArea').style.display = 'block';
|
document.getElementById('resultArea').style.display = 'block';
|
||||||
document.getElementById('resultSingle').style.display = 'none';
|
document.getElementById('resultSingle').style.display = 'none';
|
||||||
|
document.getElementById('resultKeypair').style.display = 'none';
|
||||||
document.getElementById('resultBulk').style.display = 'block';
|
document.getElementById('resultBulk').style.display = 'block';
|
||||||
|
|
||||||
const list = document.getElementById('bulkList');
|
const list = document.getElementById('bulkList');
|
||||||
list.innerHTML = items.map((item, idx) => `
|
list.innerHTML = items.map((item, idx) => {
|
||||||
|
const displayKey = item.keypair
|
||||||
|
? item.key.split('\n').slice(0, 2).join('\n') + '\n...'
|
||||||
|
: item.key;
|
||||||
|
return `
|
||||||
<div class="bulk-item">
|
<div class="bulk-item">
|
||||||
<span class="bulk-index">#${idx + 1}</span>
|
<span class="bulk-index">#${idx + 1}</span>
|
||||||
<span class="bulk-key">${item.key}</span>
|
<span class="bulk-key" style="${item.keypair ? 'white-space:pre;font-size:0.72rem;' : ''}">${escHtmlDisplay(displayKey)}</span>
|
||||||
<button class="bulk-copy" onclick="copyBulkItem(this, '${escHtml(item.key)}')">Copy</button>
|
<button class="bulk-copy" data-index="${idx}" onclick="copyBulkItem(this)">Copy</button>
|
||||||
</div>
|
</div>`;
|
||||||
`).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escHtml(s) {
|
function escHtmlDisplay(s) {
|
||||||
return s.replace(/'/g, "\\'");
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function flashCopied(btn, resetLabel) {
|
||||||
|
btn.classList.add('copied');
|
||||||
|
btn.innerHTML = `${CHECK_ICON} Copied!`;
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
btn.innerHTML = `${COPY_ICON} ${resetLabel}`;
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyKey() {
|
function copyKey() {
|
||||||
const key = document.getElementById('keyDisplay').textContent;
|
const key = document.getElementById('keyDisplay').textContent;
|
||||||
navigator.clipboard.writeText(key).then(() => {
|
navigator.clipboard.writeText(key).then(() => {
|
||||||
const btn = document.getElementById('copyBtn');
|
flashCopied(document.getElementById('copyBtn'), 'Copy');
|
||||||
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) {
|
function copyPrivateKey() {
|
||||||
|
if (!lastKeypair) return;
|
||||||
|
navigator.clipboard.writeText(lastKeypair.key).then(() => {
|
||||||
|
flashCopied(document.getElementById('copyPrivateBtn'), 'Copy Private Key');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPublicKey() {
|
||||||
|
if (!lastKeypair) return;
|
||||||
|
navigator.clipboard.writeText(lastKeypair.public_key).then(() => {
|
||||||
|
flashCopied(document.getElementById('copyPublicBtn'), 'Copy Public Key');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyBulkItem(btn) {
|
||||||
|
const idx = parseInt(btn.dataset.index);
|
||||||
|
const key = lastBulkData[idx].key;
|
||||||
navigator.clipboard.writeText(key).then(() => {
|
navigator.clipboard.writeText(key).then(() => {
|
||||||
btn.classList.add('copied');
|
btn.classList.add('copied');
|
||||||
btn.textContent = 'Copied!';
|
btn.textContent = 'Copied!';
|
||||||
|
|||||||
Reference in New Issue
Block a user