mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Merge pull request #236 from PR0M3TH3AN/codex/update-ssh-key-handling-and-add-utilities
Add SSH key entry support
This commit is contained in:
@@ -209,16 +209,42 @@ class EntryManager:
|
||||
logger.error(f"Failed to generate otpauth URI: {e}")
|
||||
raise
|
||||
|
||||
def add_ssh_key(self, notes: str = "") -> int:
|
||||
"""Placeholder for adding an SSH key entry."""
|
||||
index = self.get_next_index()
|
||||
def add_ssh_key(
|
||||
self, parent_seed: str, index: int | None = None, notes: str = ""
|
||||
) -> int:
|
||||
"""Add a new SSH key pair entry.
|
||||
|
||||
The provided ``index`` serves both as the vault entry identifier and
|
||||
derivation index for the key. If not supplied, the next available index
|
||||
is used. Only metadata is stored – keys are derived on demand.
|
||||
"""
|
||||
|
||||
if index is None:
|
||||
index = self.get_next_index()
|
||||
|
||||
data = self.vault.load_index()
|
||||
data.setdefault("entries", {})
|
||||
data["entries"][str(index)] = {"type": EntryType.SSH.value, "notes": notes}
|
||||
data["entries"][str(index)] = {
|
||||
"type": EntryType.SSH.value,
|
||||
"index": index,
|
||||
"notes": notes,
|
||||
}
|
||||
self._save_index(data)
|
||||
self.update_checksum()
|
||||
self.backup_manager.create_backup()
|
||||
raise NotImplementedError("SSH key entry support not implemented yet")
|
||||
return index
|
||||
|
||||
def get_ssh_key_pair(self, index: int, parent_seed: str) -> tuple[str, str]:
|
||||
"""Return the PEM formatted SSH key pair for the given entry."""
|
||||
|
||||
entry = self.retrieve_entry(index)
|
||||
if not entry or entry.get("type") != EntryType.SSH.value:
|
||||
raise ValueError("Entry is not an SSH key entry")
|
||||
|
||||
from password_manager.password_generation import derive_ssh_key_pair
|
||||
|
||||
key_index = int(entry.get("index", index))
|
||||
return derive_ssh_key_pair(parent_seed, key_index)
|
||||
|
||||
def add_seed(self, notes: str = "") -> int:
|
||||
"""Placeholder for adding a seed entry."""
|
||||
|
@@ -1093,6 +1093,34 @@ class PasswordManager:
|
||||
logging.error(f"Error generating TOTP code: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to generate TOTP code: {e}", "red"))
|
||||
return
|
||||
if entry_type == EntryType.SSH.value:
|
||||
notes = entry.get("notes", "")
|
||||
try:
|
||||
priv_pem, pub_pem = self.entry_manager.get_ssh_key_pair(
|
||||
index, self.parent_seed
|
||||
)
|
||||
if self.secret_mode_enabled:
|
||||
copy_to_clipboard(priv_pem, self.clipboard_clear_delay)
|
||||
print(
|
||||
colored(
|
||||
f"[+] SSH private key copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
|
||||
"green",
|
||||
)
|
||||
)
|
||||
print(colored("Public Key:", "cyan"))
|
||||
print(pub_pem)
|
||||
else:
|
||||
print(colored("\n[+] Retrieved SSH Key Pair:\n", "green"))
|
||||
print(colored("Public Key:", "cyan"))
|
||||
print(pub_pem)
|
||||
print(colored("Private Key:", "cyan"))
|
||||
print(priv_pem)
|
||||
if notes:
|
||||
print(colored(f"Notes: {notes}", "cyan"))
|
||||
except Exception as e:
|
||||
logging.error(f"Error deriving SSH key pair: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to derive SSH keys: {e}", "red"))
|
||||
return
|
||||
|
||||
website_name = entry.get("website")
|
||||
length = entry.get("length")
|
||||
|
@@ -25,8 +25,10 @@ from termcolor import colored
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from bip_utils import Bip39SeedGenerator
|
||||
|
||||
from local_bip85.bip85 import BIP85
|
||||
|
||||
@@ -340,6 +342,29 @@ def derive_ssh_key(bip85: BIP85, idx: int) -> bytes:
|
||||
return bip85.derive_entropy(index=idx, bytes_len=32, app_no=32)
|
||||
|
||||
|
||||
def derive_ssh_key_pair(parent_seed: str, index: int) -> tuple[str, str]:
|
||||
"""Derive an Ed25519 SSH key pair from the seed phrase and index."""
|
||||
|
||||
seed_bytes = Bip39SeedGenerator(parent_seed).Generate()
|
||||
bip85 = BIP85(seed_bytes)
|
||||
entropy = derive_ssh_key(bip85, index)
|
||||
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(entropy)
|
||||
priv_pem = private_key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption(),
|
||||
).decode()
|
||||
|
||||
public_key = private_key.public_key()
|
||||
pub_pem = public_key.public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
).decode()
|
||||
|
||||
return priv_pem, pub_pem
|
||||
|
||||
|
||||
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)
|
||||
|
@@ -61,9 +61,12 @@ def test_round_trip_entry_types(method, expected_type):
|
||||
entry_mgr.add_totp("example", TEST_SEED)
|
||||
index = 0
|
||||
else:
|
||||
with pytest.raises(NotImplementedError):
|
||||
getattr(entry_mgr, method)()
|
||||
index = 0
|
||||
if method == "add_ssh_key":
|
||||
index = entry_mgr.add_ssh_key(TEST_SEED)
|
||||
else:
|
||||
with pytest.raises(NotImplementedError):
|
||||
getattr(entry_mgr, method)()
|
||||
index = 0
|
||||
|
||||
entry = entry_mgr.retrieve_entry(index)
|
||||
assert entry["type"] == expected_type
|
||||
|
30
src/tests/test_ssh_entry.py
Normal file
30
src/tests/test_ssh_entry.py
Normal file
@@ -0,0 +1,30 @@
|
||||
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.vault import Vault
|
||||
from password_manager.config_manager import ConfigManager
|
||||
|
||||
|
||||
def test_add_and_retrieve_ssh_key_pair():
|
||||
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)
|
||||
|
||||
index = entry_mgr.add_ssh_key(TEST_SEED)
|
||||
entry = entry_mgr.retrieve_entry(index)
|
||||
assert entry == {"type": "ssh", "index": index, "notes": ""}
|
||||
|
||||
priv1, pub1 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)
|
||||
priv2, pub2 = entry_mgr.get_ssh_key_pair(index, TEST_SEED)
|
||||
assert priv1 == priv2
|
||||
assert pub1 == pub2
|
Reference in New Issue
Block a user