1.2.0
This commit is contained in:
105
README.md
105
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
|
- **Drag‑and‑drop friendly**: on macOS and Linux you can simply drop the file into the Terminal window.
|
||||||
- Outputs:
|
- **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.
|
||||||
- `nsec` (private key)
|
- **No external binaries** – it just uses the pure‑Python `ecdsa` and `bech32` packages.
|
||||||
- `npub` (public key)
|
- **Portable**: you can ship it as a single Python file or compile to a binary with tools such as PyInstaller.
|
||||||
- Drag and drop file support on macOS/Linux Terminal
|
|
||||||
|
|
||||||
---
|
## Install
|
||||||
|
|
||||||
## 🚀 Install
|
|
||||||
|
|
||||||
##
|
|
||||||
|
|
||||||
### 1. Clone & Set Up
|
|
||||||
|
|
||||||
|
### 1. Clone the repo
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/btcforplebs/nostr-keygen.git
|
git clone https://github.com/btcforplebs/nostr-keygen.git
|
||||||
cd nostr-keygen
|
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
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
# Install required packages globally
|
### 3. Install dependencies
|
||||||
pip3 install ecdsa bech32
|
```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
|
nostr-keygen
|
||||||
|
```
|
||||||
|
You’ll be guided through the following steps:
|
||||||
|
|
||||||
Returns
|
1. Welcome screen – press **Enter** to continue.
|
||||||
🔐 Drop a file to use as entropy (or enter path):
|
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).
|
||||||
|
|
||||||
|
|||||||
79
main.py
79
main.py
@@ -1,79 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate a fresh Nostr key (npub/nsec) using a file as entropy source.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
nostr-keygen <path-to-file>
|
|
||||||
|
|
||||||
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()
|
|
||||||
149
nostr-keygen
Executable file
149
nostr-keygen
Executable file
@@ -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()
|
||||||
Reference in New Issue
Block a user