mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-09 07:48:57 +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
|
nostr-sdk==0.42.1
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
parso==0.8.4
|
parso==0.8.4
|
||||||
|
pgpy==0.6.0
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
pony==0.7.19
|
pony==0.7.19
|
||||||
portalocker==3.2.0
|
portalocker==3.2.0
|
||||||
|
@@ -259,6 +259,55 @@ class EntryManager:
|
|||||||
key_index = int(entry.get("index", index))
|
key_index = int(entry.get("index", index))
|
||||||
return derive_ssh_key_pair(parent_seed, key_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(
|
def add_seed(
|
||||||
self,
|
self,
|
||||||
parent_seed: str,
|
parent_seed: str,
|
||||||
|
@@ -11,3 +11,4 @@ class EntryType(str, Enum):
|
|||||||
TOTP = "totp"
|
TOTP = "totp"
|
||||||
SSH = "ssh"
|
SSH = "ssh"
|
||||||
SEED = "seed"
|
SEED = "seed"
|
||||||
|
PGP = "pgp"
|
||||||
|
@@ -1212,6 +1212,30 @@ class PasswordManager:
|
|||||||
logging.error(f"Error deriving seed phrase: {e}", exc_info=True)
|
logging.error(f"Error deriving seed phrase: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to derive seed phrase: {e}", "red"))
|
print(colored(f"Error: Failed to derive seed phrase: {e}", "red"))
|
||||||
return
|
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")
|
website_name = entry.get("website")
|
||||||
length = entry.get("length")
|
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:
|
def derive_seed_phrase(bip85: BIP85, idx: int, words: int = 24) -> str:
|
||||||
"""Derive a new BIP39 seed phrase using BIP85."""
|
"""Derive a new BIP39 seed phrase using BIP85."""
|
||||||
return bip85.derive_mnemonic(index=idx, words_num=words)
|
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
|
tomli
|
||||||
hypothesis
|
hypothesis
|
||||||
mutmut==2.4.4
|
mutmut==2.4.4
|
||||||
|
pgpy==0.6.0
|
||||||
pyotp>=2.8.0
|
pyotp>=2.8.0
|
||||||
|
|
||||||
freezegun
|
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