Remove timestamp — 24-word mnemonic alone is sufficient for recovery
This commit is contained in:
@@ -2,35 +2,22 @@
|
||||
"""
|
||||
SSH key generator with BIP39 mnemonic backup.
|
||||
|
||||
generate — creates a new Ed25519 SSH key pair and prints 24 recovery words + timestamp
|
||||
recover — re-creates the exact same key pair from those 24 words + timestamp
|
||||
generate — creates a new Ed25519 SSH key pair and prints 24 recovery words
|
||||
recover — re-creates the exact same key pair from those 24 words
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from mnemonic import Mnemonic
|
||||
|
||||
|
||||
ENTROPY_BITS = 256 # → 24 BIP39 words
|
||||
TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S UTC"
|
||||
|
||||
|
||||
def derive_seed(entropy: bytes, timestamp: str) -> bytes:
|
||||
"""Derive 32-byte Ed25519 seed from mnemonic entropy + timestamp."""
|
||||
return HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=timestamp.encode(),
|
||||
info=b"ssh-seed-keygen v1",
|
||||
).derive(entropy)
|
||||
|
||||
|
||||
def save_keypair(
|
||||
@@ -113,25 +100,11 @@ def ask_passphrase(confirm: bool = True) -> bytes | None:
|
||||
return passphrase.encode()
|
||||
|
||||
|
||||
def ask_timestamp() -> str:
|
||||
print(f"Generation timestamp (format: YYYY-MM-DD HH:MM:SS UTC):")
|
||||
raw = input("Timestamp: ").strip()
|
||||
try:
|
||||
datetime.strptime(raw, TIMESTAMP_FORMAT)
|
||||
except ValueError:
|
||||
print(f"Error: timestamp must match format '{TIMESTAMP_FORMAT}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return raw
|
||||
|
||||
|
||||
def cmd_generate(args: argparse.Namespace) -> None:
|
||||
mnemo = Mnemonic("english")
|
||||
entropy = os.urandom(ENTROPY_BITS // 8)
|
||||
words = mnemo.to_mnemonic(entropy)
|
||||
timestamp = datetime.now(timezone.utc).strftime(TIMESTAMP_FORMAT)
|
||||
|
||||
seed = derive_seed(entropy, timestamp)
|
||||
private_key = Ed25519PrivateKey.from_private_bytes(seed)
|
||||
private_key = Ed25519PrivateKey.from_private_bytes(entropy)
|
||||
|
||||
passphrase = ask_passphrase(confirm=True) if args.passphrase else None
|
||||
|
||||
@@ -144,10 +117,9 @@ def cmd_generate(args: argparse.Namespace) -> None:
|
||||
if passphrase:
|
||||
print(" Passphrase : set")
|
||||
|
||||
print(f"\nWrite ALL of the following down — you need both to recover your key:\n")
|
||||
print(f" Timestamp : {timestamp}\n")
|
||||
print(f"\nYour 24-word recovery mnemonic — write these down and store offline:\n")
|
||||
print(format_mnemonic(words))
|
||||
print("\nKeep these secret. Both the words and the timestamp are required to recover.")
|
||||
print("\nThese words reconstruct your exact private key. Keep them secret.")
|
||||
|
||||
|
||||
def cmd_recover(args: argparse.Namespace) -> None:
|
||||
@@ -165,11 +137,8 @@ def cmd_recover(args: argparse.Namespace) -> None:
|
||||
print("Error: invalid mnemonic — check spelling or word count.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
timestamp = ask_timestamp()
|
||||
|
||||
entropy = bytes(mnemo.to_entropy(words))
|
||||
seed = derive_seed(entropy, timestamp)
|
||||
private_key = Ed25519PrivateKey.from_private_bytes(seed)
|
||||
private_key = Ed25519PrivateKey.from_private_bytes(entropy)
|
||||
|
||||
passphrase = ask_passphrase(confirm=True) if args.passphrase else None
|
||||
|
||||
@@ -210,7 +179,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
"generate",
|
||||
aliases=["gen"],
|
||||
parents=[shared],
|
||||
help="generate a new SSH key pair and print its 24-word mnemonic + timestamp",
|
||||
help="generate a new SSH key pair and print its 24-word mnemonic",
|
||||
)
|
||||
sub.add_parser(
|
||||
"recover",
|
||||
|
||||
Reference in New Issue
Block a user