diff --git a/src/password_manager/entry_management.py b/src/password_manager/entry_management.py index 1732f21..4409fab 100644 --- a/src/password_manager/entry_management.py +++ b/src/password_manager/entry_management.py @@ -27,6 +27,7 @@ from termcolor import colored from password_manager.migrations import LATEST_VERSION from password_manager.entry_types import EntryType from password_manager.totp import TotpManager +from utils.fingerprint import generate_fingerprint from password_manager.vault import Vault from password_manager.backup import BackupManager @@ -474,6 +475,75 @@ class EntryManager: seed_index = int(entry.get("index", index)) return derive_seed_phrase(bip85, seed_index, words) + def add_managed_account( + self, + label: str, + parent_seed: str, + *, + index: int | None = None, + word_count: int = 24, + notes: str = "", + archived: bool = False, + ) -> int: + """Add a new managed account seed entry.""" + + if index is None: + index = self.get_next_index() + + from password_manager.password_generation import derive_seed_phrase + from local_bip85.bip85 import BIP85 + from bip_utils import Bip39SeedGenerator + + seed_bytes = Bip39SeedGenerator(parent_seed).Generate() + bip85 = BIP85(seed_bytes) + + seed_phrase = derive_seed_phrase(bip85, index, word_count) + fingerprint = generate_fingerprint(seed_phrase) + + account_dir = self.fingerprint_dir / "accounts" / fingerprint + account_dir.mkdir(parents=True, exist_ok=True) + + data = self.vault.load_index() + data.setdefault("entries", {}) + data["entries"][str(index)] = { + "type": EntryType.MANAGED_ACCOUNT.value, + "kind": EntryType.MANAGED_ACCOUNT.value, + "index": index, + "label": label, + "word_count": word_count, + "notes": notes, + "fingerprint": fingerprint, + "archived": archived, + } + + self._save_index(data) + self.update_checksum() + self.backup_manager.create_backup() + return index + + def get_managed_account_seed(self, index: int, parent_seed: str) -> str: + """Return the seed phrase for a managed account 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.MANAGED_ACCOUNT.value + and kind != EntryType.MANAGED_ACCOUNT.value + ): + raise ValueError("Entry is not a managed account entry") + + from password_manager.password_generation import derive_seed_phrase + from local_bip85.bip85 import BIP85 + from bip_utils import Bip39SeedGenerator + + seed_bytes = Bip39SeedGenerator(parent_seed).Generate() + bip85 = BIP85(seed_bytes) + + words = int(entry.get("word_count", 24)) + seed_index = int(entry.get("index", index)) + return derive_seed_phrase(bip85, seed_index, words) + def get_totp_code( self, index: int, parent_seed: str | None = None, timestamp: int | None = None ) -> str: diff --git a/src/tests/test_managed_account_entry.py b/src/tests/test_managed_account_entry.py new file mode 100644 index 0000000..88f40f0 --- /dev/null +++ b/src/tests/test_managed_account_entry.py @@ -0,0 +1,50 @@ +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +from helpers import create_vault, TEST_SEED, TEST_PASSWORD +from utils.fingerprint import generate_fingerprint + +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 +from password_manager.password_generation import derive_seed_phrase +from local_bip85.bip85 import BIP85 +from bip_utils import Bip39SeedGenerator + + +def setup_mgr(tmp_path: Path) -> EntryManager: + vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg = ConfigManager(vault, tmp_path) + backup = BackupManager(tmp_path, cfg) + return EntryManager(vault, backup) + + +def test_add_and_get_managed_account_seed(): + with TemporaryDirectory() as tmpdir: + tmp_path = Path(tmpdir) + mgr = setup_mgr(tmp_path) + + idx = mgr.add_managed_account("acct", TEST_SEED, word_count=12) + entry = mgr.retrieve_entry(idx) + assert entry["type"] == "managed_account" + assert entry["kind"] == "managed_account" + assert entry["index"] == idx + assert entry["label"] == "acct" + assert entry["word_count"] == 12 + assert entry["archived"] is False + fp = entry.get("fingerprint") + assert fp + assert (tmp_path / "accounts" / fp).exists() + + phrase_a = mgr.get_managed_account_seed(idx, TEST_SEED) + phrase_b = mgr.get_managed_account_seed(idx, TEST_SEED) + assert phrase_a == phrase_b + + seed_bytes = Bip39SeedGenerator(TEST_SEED).Generate() + bip85 = BIP85(seed_bytes) + expected = derive_seed_phrase(bip85, idx, 12) + assert phrase_a == expected + assert generate_fingerprint(phrase_a) == fp