From ee278d8c071e38632d8c44bbdd24851fd15ac710 Mon Sep 17 00:00:00 2001 From: Jakob Husu Date: Wed, 20 May 2026 11:53:38 +0200 Subject: [PATCH] =?UTF-8?q?Remove=20timestamp=20=E2=80=94=2024-word=20mnem?= =?UTF-8?q?onic=20alone=20is=20sufficient=20for=20recovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keygen.py | 47 ++++++++--------------------------------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/keygen.py b/keygen.py index 2d09bbb..4855883 100644 --- a/keygen.py +++ b/keygen.py @@ -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",