From 28532bd6d3b94b76c02e909e0e2ed202a2e13fa8 Mon Sep 17 00:00:00 2001 From: BTCforPlebs Date: Thu, 2 Oct 2025 14:14:21 -0400 Subject: [PATCH] 1.2.0 --- README.md | 105 +++++++++++++++++++++++++++--------- main.py | 79 --------------------------- nostr-keygen | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 103 deletions(-) delete mode 100644 main.py create mode 100755 nostr-keygen diff --git a/README.md b/README.md index 48c582d..1763a4a 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,99 @@ -# nostr-keygen +# Nostr‑KeyGen -🔐 A minimal CLI tool to generate Nostr-compatible key pairs using file-based entropy. +A tiny, interactive command‑line tool that lets you generate a Nostr key pair from any file you choose. It works on macOS, Linux and (with a few tweaks) Windows. The script is intentionally lightweight – no fancy GUI, just an easy‑to‑follow wizard run from your terminal. -## ✨ Features +## Features -- Uses the contents of any file as a secure entropy source -- Outputs: - - `nsec` (private key) - - `npub` (public key) -- Drag and drop file support on macOS/Linux Terminal +- **Drag‑and‑drop friendly**: on macOS and Linux you can simply drop the file into the Terminal window. +- **Clear, step‑by‑step tutorial**: every run prints a short intro, an explanation, a quick demo, asks for your file and finishes with the key‑pair in *both* Nostr‑Bech32 and raw hex. +- **No external binaries** – it just uses the pure‑Python `ecdsa` and `bech32` packages. +- **Portable**: you can ship it as a single Python file or compile to a binary with tools such as PyInstaller. ---- - -## 🚀 Install - -## - -### 1. Clone & Set Up +## Install +### 1. Clone the repo ```bash git clone https://github.com/btcforplebs/nostr-keygen.git cd nostr-keygen -chmod +x nostr-keygen -sudo ln -s "$PWD/nostr-keygen" /usr/local/bin/nostr-keygen +``` -# Activate virtual environment +### 2. Create and activate a virtual environment +```bash python3 -m venv venv source venv/bin/activate +``` -# Install required packages globally -pip3 install ecdsa bech32 +### 3. Install dependencies +```bash +pip install ecdsa bech32 +``` -Now just run: +### 4. Make it executable and optional symlink +```bash +chmod +x nostr-keygen +``` + +## Usage + +Run the script directly from the project directory: +```bash +./nostr-keygen +``` +or, if you added the symlink: +```bash nostr-keygen +``` +You’ll be guided through the following steps: -Returns -🔐 Drop a file to use as entropy (or enter path): +1. Welcome screen – press **Enter** to continue. +2. Tool overview – press **Enter** to continue. +3. Demo key generation using a temporary file – press **Enter** to continue. +4. Prompt for the path to your entropy file. You can drop a file into the terminal on macOS/Linux or type the absolute/relative path. +5. The script shows you the generated `nsec`, `npub`, and the hex representation of both keys. +6. Press **Enter** again, the terminal is cleared and the program exits. -Drop file to use as entropy: /Users/username/Desktop/file.txt +## Example Output +``` +🛠️ Welcome to Nostr‑KeyGen + +This tool will: +- Pick any file as your entropy source +- Hash the file → 32‑byte seed +- Derive a secp256k1 key pair +- Print the Nostr `nsec`/`npub` and the raw hex values + +💡 Demo (using a random file in /tmp)... + Demo nsec: nsec1q4... + Demo npub: npub1p8... + Demo priv hex: 1f4c... + Demo pub hex: 03a1... + +📂 Enter the path to your entropy file: /Users/me/secret.txt + +✅ Your new key pair: + - nsec : nsec1q8... + - npub : npub1p9... + - priv hex : 7c2d... + - pub hex : 029b... + +🧹 Cleaning up... +🚀 Done. Goodbye! +``` + +## FAQs + +| Question | Answer | +|----------|--------| +| Why does it read *any* file as entropy? | Nostr requires a 32‑byte seed. Hashing a file guarantees a deterministic, pseudo‑random 32‑byte output irrespective of the file size or content. | +| Will it always produce the same key for the same file? | Yes – the seed is derived from a SHA‑256 hash of the file contents. | +| Can I use a password instead? | The current script accepts files only, but you could easily pipe a password string using `echo -n 'mypassword' > pwd.txt` first. | + +## Contributing + +Pull requests are welcome! Just make sure the code passes `flake8`/`black` and contains an updated README. + +## License + +MIT – see [LICENSE](LICENSE). diff --git a/main.py b/main.py deleted file mode 100644 index beea519..0000000 --- a/main.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -"""Generate a fresh Nostr key (npub/nsec) using a file as entropy source. - -Usage: - nostr-keygen - -The file is read in binary mode and its bytes are hashed with SHA‑256 to -produce the 32‑byte seed which is then used to generate a secp256k1 -private key. The private key is hex‑encoded as an nsec, and the -corresponding public key is converted to a Bech32 npub. The example -uses the official Nostr prefix "nsec"/ -""" - -import argparse -import hashlib -import os -import sys - -from ecdsa import SigningKey, SECP256k1 -from bech32 import bech32_encode, convertbits - -# Constants for Nostr bech32 encoding -NSEC_PREFIX = "nsec" -NPUB_PREFIX = "npub" - -# convertbits helper adapted from bech32 library; using provided function for clarity - -def _to_bech32(data: bytes, hrp: str) -> str: - """Encode raw bytes into a Bech32 string with the given human‑readable part.""" - # Convert 8‑bit bytes to 5‑bit groups - five_bits = convertbits(list(data), 8, 5, True) - if five_bits is None: - raise ValueError("Error converting data to 5‑bit groups") - return bech32_encode(hrp, five_bits) - - -def _entropy_to_pri_key(entropy: bytes) -> SigningKey: - """Return an ECDSA SECP256k1 private key derived from entropy.""" - # Use SHA‑256 of the provided entropy for deterministic key generation - seed = hashlib.sha256(entropy).digest() - return SigningKey.from_string(seed, curve=SECP256k1) - - -def generate_key_from_file(file_path: str) -> (str, str): - """Return (nsec, npub) for the key derived from the file content.""" - if not os.path.isfile(file_path): - raise FileNotFoundError(f"File not found: {file_path}") - with open(file_path, "rb") as f: - data = f.read() - - sk = _entropy_to_pri_key(data) - vk = sk.get_verifying_key() - # Private key bytes - private_bytes = sk.to_string() - # Public key bytes, compressed (33 bytes) - public_bytes = vk.to_string("compressed") - nsec = _to_bech32(private_bytes, NSEC_PREFIX) - npub = _to_bech32(public_bytes, NPUB_PREFIX) - return nsec, npub - - - -def main(): - parser = argparse.ArgumentParser(description="Generate Nostr key pair from file entropy") - parser.add_argument("file", help="Path to file used as entropy source") - args = parser.parse_args() - - try: - nsec, npub = generate_key_from_file(args.file) - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - - print("nsec:", nsec) - print("npub:", npub) - - -if __name__ == "__main__": - main() diff --git a/nostr-keygen b/nostr-keygen new file mode 100755 index 0000000..017c050 --- /dev/null +++ b/nostr-keygen @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""A friendly CLI that walks you through generating a Nostr key pair + from a file you pick on the fly. + + Run it with: + ./nostr-keygen +""" + +import time +import argparse +import hashlib +import os +import sys +from pathlib import Path + +# ----- IMPORTS --------------------------------------------------------------- +try: + from ecdsa import SigningKey, SECP256k1 + from bech32 import bech32_encode, convertbits +except ImportError as e: + print("⚠️ Required packages are missing.\nRun 'pip install ecdsa bech32' first.", file=sys.stderr) + sys.exit(1) + +# ----- CONSTANTS ------------------------------------------------------------- +NSEC_PREFIX = "nsec" +NPUB_PREFIX = "npub" + +# ----- HELPER FUNCTIONS ------------------------------------------------------ +def _to_bech32(data: bytes, hrp: str) -> str: + """Encode raw bytes into a Bech32 string with the given human‑readable part.""" + five_bits = convertbits(list(data), 8, 5, True) + if five_bits is None: + raise ValueError("Error converting data to 5‑bit groups") + return bech32_encode(hrp, five_bits) + +def _entropy_to_pri_key(entropy: bytes) -> SigningKey: + """Return a SECP256k1 private key derived from SHA‑256 of the entropy.""" + seed = hashlib.sha256(entropy).digest() + return SigningKey.from_string(seed, curve=SECP256k1) + +def generate_key_from_file(file_path: Path): + """Return (nsec, npub, private_hex, public_hex).""" + if not file_path.is_file(): + raise FileNotFoundError(f"File not found: {file_path}") + + with file_path.open("rb") as f: + data = f.read() + + sk = _entropy_to_pri_key(data) + vk = sk.get_verifying_key() + + private_bytes = sk.to_string() + public_bytes = vk.to_string("compressed") + + nsec = _to_bech32(private_bytes, NSEC_PREFIX) + npub = _to_bech32(public_bytes, NPUB_PREFIX) + + return nsec, npub, private_bytes.hex(), public_bytes.hex() + +# ----- CLI / UI --------------------------------------------------------------- +def welcome(): + clear_terminal() + print("\n" + "="*30) + print("🛠️ Welcome to Nostr‑KeyGen") + print("="*30 + "\n") + + +def info(): + clear_terminal() + print("This tool will help you:") + print("- Pick any file as your entropy source") + print("- Hash the file → 32‑byte seed") + print("- Derive a secp256k1 key pair") + print("- Print the Nostr `nsec`/`npub` and the raw hex values") + print() + +def demo(): + clear_terminal() + print("💡 EXAMPLE (using a random file in /tmp)...") + example = Path("/tmp/nostr_demo_entropy.txt") + example.write_bytes(os.urandom(256)) + try: + nsec, npub, priv_hex, pub_hex = generate_key_from_file(example) + print(f" DEMO nsec: {nsec}") + print(f" DEMO npub: {npub}") + print(f" DEMO priv hex: {priv_hex}") + print(f" DEMO pub hex: {pub_hex}") + except Exception as e: + print(f" Cannot generate demo key: {e}") + finally: + example.unlink(missing_ok=True) + print() + +def ask_file() -> Path: + clear_terminal() + while True: + path = input("📂 Enter the path or drop your file: ").strip() + if not path: + print(" ✨ Nothing entered, try again.") + continue + p = Path(path).expanduser().resolve() + if p.is_file(): + return p + print(" ❌ That path isn’t a file. Try again.") + +def show_keys(nsec, npub, priv_hex, pub_hex): + clear_terminal() + print("\n✅ Your new key pair:") + print(f" - nsec : {nsec}") + print(f" - npub : {npub}") + print(f" - priv hex : {priv_hex}") + print(f" - pub hex : {pub_hex}\n") + +def clear_terminal(): + """Clear the screen for a clean exit.""" + os.system("clear" if os.name != "nt" else "cls") + +def pause(prompt="[Enter] to continue"): + """Wait for the user to hit Enter.""" + input(f"\n{prompt}") + +def main(): + welcome() + pause("Press [Enter] to continue...") + + info() + pause("Press [Enter] to continue...") + + demo() + pause("Press [Enter] to continue...") + + entropy_file = ask_file() + + try: + nsec, npub, priv_hex, pub_hex = generate_key_from_file(entropy_file) + except Exception as e: + print(f"\n❌ Failed to generate keys: {e}", file=sys.stderr) + sys.exit(1) + + show_keys(nsec, npub, priv_hex, pub_hex) + pause("Press [Enter] to clear the screen and exit...") + + clear_terminal() + print("🚀 Done. Goodbye!\n") + sys.exit(0) + +if __name__ == "__main__": + # If the script is run directly, invoke main() + main() \ No newline at end of file