diff --git a/src/local_bip85/bip85.py b/src/local_bip85/bip85.py index b863d6d..beac8de 100644 --- a/src/local_bip85/bip85.py +++ b/src/local_bip85/bip85.py @@ -32,9 +32,13 @@ logger = logging.getLogger(__name__) class BIP85: - def __init__(self, seed_bytes: bytes): + def __init__(self, seed_bytes: bytes | str): + """Initialize from BIP39 seed bytes or BIP32 xprv string.""" try: - self.bip32_ctx = Bip32Slip10Secp256k1.FromSeed(seed_bytes) + if isinstance(seed_bytes, (bytes, bytearray)): + self.bip32_ctx = Bip32Slip10Secp256k1.FromSeed(seed_bytes) + else: + self.bip32_ctx = Bip32Slip10Secp256k1.FromExtendedKey(seed_bytes) logging.debug("BIP32 context initialized successfully.") except Exception as e: logging.error(f"Error initializing BIP32 context: {e}") @@ -42,7 +46,9 @@ class BIP85: print(f"{Fore.RED}Error initializing BIP32 context: {e}") sys.exit(1) - def derive_entropy(self, index: int, bytes_len: int, app_no: int = 39) -> bytes: + def derive_entropy( + self, index: int, bytes_len: int, app_no: int = 39, words_len: int | None = None + ) -> bytes: """ Derives entropy using BIP-85 HMAC-SHA512 method. @@ -58,7 +64,9 @@ class BIP85: SystemExit: If derivation fails or entropy length is invalid. """ if app_no == 39: - path = f"m/83696968'/{app_no}'/0'/{bytes_len}'/{index}'" + if words_len is None: + words_len = bytes_len + path = f"m/83696968'/{app_no}'/0'/{words_len}'/{index}'" elif app_no == 32: path = f"m/83696968'/{app_no}'/{index}'" else: @@ -100,49 +108,29 @@ class BIP85: print(f"{Fore.RED}Error: Unsupported number of words: {words_num}") sys.exit(1) - entropy = self.derive_entropy(index=index, bytes_len=bytes_len, app_no=39) + entropy = self.derive_entropy( + index=index, bytes_len=bytes_len, app_no=39, words_len=words_num + ) try: mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy( entropy ) logging.debug(f"Derived mnemonic: {mnemonic}") - return mnemonic + return mnemonic.ToStr() except Exception as e: logging.error(f"Error generating mnemonic: {e}") logging.error(traceback.format_exc()) # Log full traceback print(f"{Fore.RED}Error generating mnemonic: {e}") sys.exit(1) - def derive_symmetric_key(self, app_no: int = 48, index: int = 0) -> bytes: - """ - Derives a symmetric encryption key using BIP85. - - Parameters: - app_no (int): Application number for key derivation (48 chosen arbitrarily). - index (int): Index for key derivation. - - Returns: - bytes: Derived symmetric key (32 bytes for AES-256). - - Raises: - SystemExit: If symmetric key derivation fails. - """ - entropy = self.derive_entropy( - app_no, language_code=0, words_num=24, index=index - ) + def derive_symmetric_key(self, index: int = 0, app_no: int = 2) -> bytes: + """Derive 32 bytes of entropy for symmetric key usage.""" try: - hkdf = HKDF( - algorithm=hashes.SHA256(), - length=32, # 256 bits for AES-256 - salt=None, - info=b"seedos-encryption-key", - backend=default_backend(), - ) - symmetric_key = hkdf.derive(entropy) - logging.debug(f"Derived symmetric key: {symmetric_key.hex()}") - return symmetric_key + key = self.derive_entropy(index=index, bytes_len=32, app_no=app_no) + logging.debug(f"Derived symmetric key: {key.hex()}") + return key except Exception as e: logging.error(f"Error deriving symmetric key: {e}") - logging.error(traceback.format_exc()) # Log full traceback + logging.error(traceback.format_exc()) print(f"{Fore.RED}Error deriving symmetric key: {e}") sys.exit(1) diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index 79afff5..45d9b78 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -656,11 +656,8 @@ class PasswordManager: try: master_seed = os.urandom(32) # Generate a random 32-byte seed bip85 = BIP85(master_seed) - mnemonic_obj = bip85.derive_mnemonic(index=0, words_num=12) - mnemonic_str = ( - mnemonic_obj.ToStr() - ) # Convert Bip39Mnemonic object to string - return mnemonic_str + mnemonic = bip85.derive_mnemonic(index=0, words_num=12) + return mnemonic except Exception as e: logging.error(f"Failed to generate BIP-85 seed: {e}") logging.error(traceback.format_exc()) diff --git a/src/tests/test_bip85_vectors.py b/src/tests/test_bip85_vectors.py new file mode 100644 index 0000000..8e77459 --- /dev/null +++ b/src/tests/test_bip85_vectors.py @@ -0,0 +1,39 @@ +import sys +from pathlib import Path +import pytest + +sys.path.append(str(Path(__file__).resolve().parents[1])) + +from local_bip85.bip85 import BIP85 + +MASTER_XPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb" + +EXPECTED_12 = "girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose" + +EXPECTED_24 = "puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano" + +EXPECTED_SYMM_KEY = "7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1" + + +@pytest.fixture(scope="module") +def bip85(): + return BIP85(MASTER_XPRV) + + +def test_bip85_mnemonic_12(bip85): + assert bip85.derive_mnemonic(index=0, words_num=12) == EXPECTED_12 + + +def test_bip85_mnemonic_24(bip85): + assert bip85.derive_mnemonic(index=0, words_num=24) == EXPECTED_24 + + +def test_bip85_symmetric_key(bip85): + assert bip85.derive_symmetric_key(index=0).hex() == EXPECTED_SYMM_KEY + + +def test_invalid_params(bip85): + with pytest.raises(SystemExit): + bip85.derive_mnemonic(index=0, words_num=15) + with pytest.raises(SystemExit): + bip85.derive_mnemonic(index=-1, words_num=12)