Remove timestamp — 24-word mnemonic alone is sufficient for recovery

This commit is contained in:
2026-05-20 11:53:38 +02:00
parent f76a773433
commit ee278d8c07
+8 -39
View File
@@ -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",