mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Merge pull request #241 from PR0M3TH3AN/codex/implement-pgp-key-management-functionality
Add PGP key generation
This commit is contained in:
@@ -33,6 +33,7 @@ mutmut==2.4.4
|
||||
nostr-sdk==0.42.1
|
||||
packaging==25.0
|
||||
parso==0.8.4
|
||||
pgpy==0.6.0
|
||||
pluggy==1.6.0
|
||||
pony==0.7.19
|
||||
portalocker==3.2.0
|
||||
|
@@ -259,6 +259,55 @@ class EntryManager:
|
||||
key_index = int(entry.get("index", index))
|
||||
return derive_ssh_key_pair(parent_seed, key_index)
|
||||
|
||||
def add_pgp_key(
|
||||
self,
|
||||
parent_seed: str,
|
||||
index: int | None = None,
|
||||
key_type: str = "ed25519",
|
||||
user_id: str = "",
|
||||
notes: str = "",
|
||||
) -> int:
|
||||
"""Add a new PGP key entry."""
|
||||
|
||||
if index is None:
|
||||
index = self.get_next_index()
|
||||
|
||||
data = self.vault.load_index()
|
||||
data.setdefault("entries", {})
|
||||
data["entries"][str(index)] = {
|
||||
"type": EntryType.PGP.value,
|
||||
"kind": EntryType.PGP.value,
|
||||
"index": index,
|
||||
"key_type": key_type,
|
||||
"user_id": user_id,
|
||||
"notes": notes,
|
||||
}
|
||||
self._save_index(data)
|
||||
self.update_checksum()
|
||||
self.backup_manager.create_backup()
|
||||
return index
|
||||
|
||||
def get_pgp_key(self, index: int, parent_seed: str) -> tuple[str, str]:
|
||||
"""Return the armored PGP private key and fingerprint for the entry."""
|
||||
|
||||
entry = self.retrieve_entry(index)
|
||||
etype = entry.get("type") if entry else None
|
||||
kind = entry.get("kind") if entry else None
|
||||
if not entry or (etype != EntryType.PGP.value and kind != EntryType.PGP.value):
|
||||
raise ValueError("Entry is not a PGP key entry")
|
||||
|
||||
from password_manager.password_generation import derive_pgp_key
|
||||
from local_bip85.bip85 import BIP85
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
|
||||
seed_bytes = Bip39SeedGenerator(parent_seed).Generate()
|
||||
bip85 = BIP85(seed_bytes)
|
||||
|
||||
key_idx = int(entry.get("index", index))
|
||||
key_type = entry.get("key_type", "ed25519")
|
||||
user_id = entry.get("user_id", "")
|
||||
return derive_pgp_key(bip85, key_idx, key_type, user_id)
|
||||
|
||||
def add_seed(
|
||||
self,
|
||||
parent_seed: str,
|
||||
|
@@ -11,3 +11,4 @@ class EntryType(str, Enum):
|
||||
TOTP = "totp"
|
||||
SSH = "ssh"
|
||||
SEED = "seed"
|
||||
PGP = "pgp"
|
||||
|
@@ -1212,6 +1212,30 @@ class PasswordManager:
|
||||
logging.error(f"Error deriving seed phrase: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to derive seed phrase: {e}", "red"))
|
||||
return
|
||||
if entry_type == EntryType.PGP.value:
|
||||
notes = entry.get("notes", "")
|
||||
try:
|
||||
priv_key, fingerprint = self.entry_manager.get_pgp_key(
|
||||
index, self.parent_seed
|
||||
)
|
||||
if self.secret_mode_enabled:
|
||||
copy_to_clipboard(priv_key, self.clipboard_clear_delay)
|
||||
print(
|
||||
colored(
|
||||
f"[+] PGP key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
||||
"green",
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(colored("\n[+] Retrieved PGP Key:\n", "green"))
|
||||
print(colored(f"Fingerprint: {fingerprint}", "cyan"))
|
||||
print(priv_key)
|
||||
if notes:
|
||||
print(colored(f"Notes: {notes}", "cyan"))
|
||||
except Exception as e:
|
||||
logging.error(f"Error deriving PGP key: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to derive PGP key: {e}", "red"))
|
||||
return
|
||||
|
||||
website_name = entry.get("website")
|
||||
length = entry.get("length")
|
||||
|
@@ -368,3 +368,97 @@ def derive_ssh_key_pair(parent_seed: str, index: int) -> tuple[str, str]:
|
||||
def derive_seed_phrase(bip85: BIP85, idx: int, words: int = 24) -> str:
|
||||
"""Derive a new BIP39 seed phrase using BIP85."""
|
||||
return bip85.derive_mnemonic(index=idx, words_num=words)
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
from pgpy import PGPKey, PGPUID
|
||||
from pgpy.packet.packets import PrivKeyV4
|
||||
from pgpy.packet.fields import (
|
||||
EdDSAPriv,
|
||||
RSAPriv,
|
||||
ECPoint,
|
||||
ECPointFormat,
|
||||
EllipticCurveOID,
|
||||
MPI,
|
||||
)
|
||||
from pgpy.constants import (
|
||||
PubKeyAlgorithm,
|
||||
KeyFlags,
|
||||
HashAlgorithm,
|
||||
SymmetricKeyAlgorithm,
|
||||
CompressionAlgorithm,
|
||||
)
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Util.number import inverse
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
entropy = bip85.derive_entropy(index=idx, bytes_len=32, app_no=32)
|
||||
created = datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
|
||||
if key_type.lower() == "rsa":
|
||||
|
||||
class DRNG:
|
||||
def __init__(self, seed: bytes) -> None:
|
||||
self.seed = seed
|
||||
|
||||
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
|
||||
return out[:n]
|
||||
|
||||
rsa_key = RSA.generate(2048, randfunc=DRNG(entropy))
|
||||
keymat = RSAPriv()
|
||||
keymat.n = MPI(rsa_key.n)
|
||||
keymat.e = MPI(rsa_key.e)
|
||||
keymat.d = MPI(rsa_key.d)
|
||||
keymat.p = MPI(rsa_key.p)
|
||||
keymat.q = MPI(rsa_key.q)
|
||||
keymat.u = MPI(inverse(keymat.p, keymat.q))
|
||||
keymat._compute_chksum()
|
||||
|
||||
pkt = PrivKeyV4()
|
||||
pkt.pkalg = PubKeyAlgorithm.RSAEncryptOrSign
|
||||
pkt.keymaterial = keymat
|
||||
else:
|
||||
priv = ed25519.Ed25519PrivateKey.from_private_bytes(entropy)
|
||||
public = priv.public_key().public_bytes(
|
||||
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
||||
)
|
||||
keymat = EdDSAPriv()
|
||||
keymat.oid = EllipticCurveOID.Ed25519
|
||||
keymat.s = MPI(int.from_bytes(entropy, "big"))
|
||||
keymat.p = ECPoint.from_values(
|
||||
keymat.oid.key_size, ECPointFormat.Native, public
|
||||
)
|
||||
keymat._compute_chksum()
|
||||
|
||||
pkt = PrivKeyV4()
|
||||
pkt.pkalg = PubKeyAlgorithm.EdDSA
|
||||
pkt.keymaterial = keymat
|
||||
|
||||
pkt.created = created
|
||||
pkt.update_hlen()
|
||||
key = PGPKey()
|
||||
key._key = pkt
|
||||
uid = PGPUID.new(user_id)
|
||||
key.add_uid(
|
||||
uid,
|
||||
usage={
|
||||
KeyFlags.Sign,
|
||||
KeyFlags.EncryptCommunications,
|
||||
KeyFlags.EncryptStorage,
|
||||
},
|
||||
hashes=[HashAlgorithm.SHA256],
|
||||
ciphers=[SymmetricKeyAlgorithm.AES256],
|
||||
compression=[CompressionAlgorithm.ZLIB],
|
||||
)
|
||||
return str(key), key.fingerprint
|
||||
|
@@ -18,6 +18,7 @@ websockets>=15.0.0
|
||||
tomli
|
||||
hypothesis
|
||||
mutmut==2.4.4
|
||||
pgpy==0.6.0
|
||||
pyotp>=2.8.0
|
||||
|
||||
freezegun
|
||||
|
27
src/tests/test_pgp_entry.py
Normal file
27
src/tests/test_pgp_entry.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from password_manager.entry_management import EntryManager
|
||||
from password_manager.backup import BackupManager
|
||||
from password_manager.config_manager import ConfigManager
|
||||
|
||||
|
||||
def test_pgp_key_determinism():
|
||||
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(TEST_SEED, key_type="ed25519", 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
|
Reference in New Issue
Block a user