Add BIP85 TOTP derivation

This commit is contained in:
thePR0M3TH3AN
2025-07-02 23:50:56 -04:00
parent fe758eee35
commit 2a72464268
4 changed files with 26 additions and 12 deletions

View File

@@ -335,12 +335,6 @@ class PasswordGenerator:
raise raise
def derive_totp_secret(bip85: BIP85, idx: int) -> str:
"""Derive a TOTP secret for the given index using BIP85."""
entropy = bip85.derive_entropy(index=idx, bytes_len=10, app_no=2)
return base64.b32encode(entropy).decode("utf-8")
def derive_ssh_key(bip85: BIP85, idx: int) -> bytes: def derive_ssh_key(bip85: BIP85, idx: int) -> bytes:
"""Derive 32 bytes of entropy suitable for an SSH key.""" """Derive 32 bytes of entropy suitable for an SSH key."""
return bip85.derive_entropy(index=idx, bytes_len=32, app_no=32) return bip85.derive_entropy(index=idx, bytes_len=32, app_no=32)

View File

@@ -6,10 +6,10 @@ sys.path.append(str(Path(__file__).resolve().parents[1]))
from local_bip85.bip85 import BIP85, Bip85Error from local_bip85.bip85 import BIP85, Bip85Error
from password_manager.password_generation import ( from password_manager.password_generation import (
derive_totp_secret,
derive_ssh_key, derive_ssh_key,
derive_seed_phrase, derive_seed_phrase,
) )
from utils.key_derivation import derive_totp_secret
MASTER_XPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb" MASTER_XPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
@@ -18,7 +18,7 @@ EXPECTED_12 = "girl mad pet galaxy egg matter matrix prison refuse sense ordinar
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_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" EXPECTED_SYMM_KEY = "7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1"
EXPECTED_TOTP_SECRET = "OBALWUYQJ4TTM7ZR" EXPECTED_TOTP_SECRET = "VQYTWDNEWYBY2G3LOGGCEKR4LZ3LNEYY"
EXPECTED_SSH_KEY = "52405cd0dd21c5be78314a7c1a3c65ffd8d896536cc7dee3157db5824f0c92e2" EXPECTED_SSH_KEY = "52405cd0dd21c5be78314a7c1a3c65ffd8d896536cc7dee3157db5824f0c92e2"
@@ -39,8 +39,8 @@ def test_bip85_symmetric_key(bip85):
assert bip85.derive_symmetric_key(index=0).hex() == EXPECTED_SYMM_KEY assert bip85.derive_symmetric_key(index=0).hex() == EXPECTED_SYMM_KEY
def test_derive_totp_secret(bip85): def test_derive_totp_secret():
assert derive_totp_secret(bip85, 0) == EXPECTED_TOTP_SECRET assert derive_totp_secret(EXPECTED_24, 0) == EXPECTED_TOTP_SECRET
def test_derive_ssh_key(bip85): def test_derive_ssh_key(bip85):

View File

@@ -11,8 +11,10 @@ try:
derive_key_from_password, derive_key_from_password,
derive_key_from_parent_seed, derive_key_from_parent_seed,
derive_index_key, derive_index_key,
derive_totp_secret,
EncryptionMode, EncryptionMode,
DEFAULT_ENCRYPTION_MODE, DEFAULT_ENCRYPTION_MODE,
TOTP_PURPOSE,
) )
from .checksum import ( from .checksum import (
calculate_checksum, calculate_checksum,
@@ -33,8 +35,10 @@ __all__ = [
"derive_key_from_password", "derive_key_from_password",
"derive_key_from_parent_seed", "derive_key_from_parent_seed",
"derive_index_key", "derive_index_key",
"derive_totp_secret",
"EncryptionMode", "EncryptionMode",
"DEFAULT_ENCRYPTION_MODE", "DEFAULT_ENCRYPTION_MODE",
"TOTP_PURPOSE",
"calculate_checksum", "calculate_checksum",
"verify_checksum", "verify_checksum",
"json_checksum", "json_checksum",

View File

@@ -20,6 +20,7 @@ import base64
import unicodedata import unicodedata
import logging import logging
import traceback import traceback
import hmac
from enum import Enum from enum import Enum
from typing import Optional, Union from typing import Optional, Union
from bip_utils import Bip39SeedGenerator from bip_utils import Bip39SeedGenerator
@@ -40,6 +41,9 @@ class EncryptionMode(Enum):
DEFAULT_ENCRYPTION_MODE = EncryptionMode.SEED_ONLY DEFAULT_ENCRYPTION_MODE = EncryptionMode.SEED_ONLY
# Purpose constant for TOTP secret derivation using BIP85
TOTP_PURPOSE = 39
def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes: def derive_key_from_password(password: str, iterations: int = 100_000) -> bytes:
""" """
@@ -159,10 +163,22 @@ def derive_totp_secret(seed: str, index: int) -> str:
try: try:
from local_bip85 import BIP85 from local_bip85 import BIP85
# Initialize BIP85 from the BIP39 seed bytes
seed_bytes = Bip39SeedGenerator(seed).Generate() seed_bytes = Bip39SeedGenerator(seed).Generate()
bip85 = BIP85(seed_bytes) bip85 = BIP85(seed_bytes)
entropy = bip85.derive_entropy(index=index, bytes_len=10, app_no=2)
secret = base64.b32encode(entropy).decode("utf-8") # Build the BIP32 path m/83696968'/39'/TOTP'/{index}'
totp_int = int.from_bytes(b"TOTP", "big")
path = f"m/83696968'/{TOTP_PURPOSE}'/{totp_int}'/{index}'"
# Derive entropy using the same scheme as BIP85
child_key = bip85.bip32_ctx.DerivePath(path)
key_bytes = child_key.PrivateKey().Raw().ToBytes()
entropy = hmac.new(b"bip-entropy-from-k", key_bytes, hashlib.sha512).digest()
# Hash the first 32 bytes of entropy and encode the first 20 bytes
hashed = hashlib.sha256(entropy[:32]).digest()
secret = base64.b32encode(hashed[:20]).decode("utf-8")
logger.debug(f"Derived TOTP secret for index {index}: {secret}") logger.debug(f"Derived TOTP secret for index {index}: {secret}")
return secret return secret
except Exception as e: except Exception as e: