diff --git a/src/seedpass/core/password_generation.py b/src/seedpass/core/password_generation.py index 1c166b6..43b1800 100644 --- a/src/seedpass/core/password_generation.py +++ b/src/seedpass/core/password_generation.py @@ -482,7 +482,13 @@ def derive_seed_phrase(bip85: BIP85, idx: int, words: int = 24) -> str: def derive_pgp_key( bip85: BIP85, idx: int, key_type: str = "ed25519", user_id: str = "" ) -> tuple[str, str]: - """Derive a deterministic PGP private key and return it with its fingerprint.""" + """Derive a deterministic PGP private key and return it with its fingerprint. + + For RSA keys the randomness required during key generation is provided by + an HMAC-SHA256 based deterministic generator seeded from the BIP-85 + entropy. This avoids use of Python's ``random`` module while ensuring the + output remains stable across Python versions. + """ from pgpy import PGPKey, PGPUID from pgpy.packet.packets import PrivKeyV4 @@ -514,14 +520,18 @@ def derive_pgp_key( if key_type.lower() == "rsa": class DRNG: + """HMAC-SHA256 based deterministic random generator.""" + def __init__(self, seed: bytes) -> None: - self.seed = seed + self.key = seed + self.counter = 0 def __call__(self, n: int) -> bytes: # pragma: no cover - deterministic out = b"" while len(out) < n: - self.seed = hashlib.sha256(self.seed).digest() - out += self.seed + msg = self.counter.to_bytes(4, "big") + out += hmac.new(self.key, msg, hashlib.sha256).digest() + self.counter += 1 return out[:n] rsa_key = RSA.generate(2048, randfunc=DRNG(entropy)) diff --git a/src/tests/test_pgp_entry.py b/src/tests/test_pgp_entry.py index c1fd37f..9cb7c28 100644 --- a/src/tests/test_pgp_entry.py +++ b/src/tests/test_pgp_entry.py @@ -39,3 +39,21 @@ def test_pgp_key_determinism(): entry = data["entries"][str(idx)] assert entry["key_type"] == "ed25519" assert entry["user_id"] == "Test" + + +def test_pgp_rsa_key_determinism(): + """RSA PGP keys should be derived deterministically.""" + + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + idx = entry_mgr.add_pgp_key("pgp", TEST_SEED, key_type="rsa", user_id="Test") + key1, fp1 = entry_mgr.get_pgp_key(idx, TEST_SEED) + key2, fp2 = entry_mgr.get_pgp_key(idx, TEST_SEED) + + assert fp1 == fp2 + assert key1 == key2