Complete security item 3 hardening and redaction sweep

This commit is contained in:
thePR0M3TH3AN committed 2026-02-25 18:23:40 -05:00
1 parent fcbe2b2943
commit 56b68bd0ac
11 files changed
+201 -55

No files matched your search

+4 -5
View File
@@ -96,11 +96,10 @@ class BIP85:
try:
child_key = self.bip32_ctx.DerivePath(path)
k = child_key.PrivateKey().Raw().ToBytes()
logging.debug(f"Derived child key at path {path}: {k.hex()}")
logging.debug("Derived child key at path %s", path)
hmac_key = b"bip-entropy-from-k"
hmac_result = hmac.new(hmac_key, k, hashlib.sha512).digest()
logging.debug(f"HMAC-SHA512 result: {hmac_result.hex()}")
entropy = hmac_result[:entropy_bytes]
@@ -115,7 +114,7 @@ class BIP85:
f"Derived entropy length is {len(entropy)} bytes; expected {entropy_bytes} bytes."
)
logging.debug(f"Derived entropy: {entropy.hex()}")
logging.debug("Derived entropy successfully.")
return entropy
except Exception as e:
logging.error(f"Error deriving entropy: {e}", exc_info=True)
@@ -139,7 +138,7 @@ class BIP85:
mnemonic = Bip39MnemonicGenerator(Bip39Languages.ENGLISH).FromEntropy(
entropy
)
logging.debug(f"Derived mnemonic: {mnemonic}")
logging.debug("Derived mnemonic successfully.")
return mnemonic.ToStr()
except Exception as e:
logging.error(f"Error generating mnemonic: {e}", exc_info=True)
@@ -150,7 +149,7 @@ class BIP85:
"""Derive 32 bytes of entropy for symmetric key usage."""
try:
key = self.derive_entropy(index=index, entropy_bytes=32, app_no=app_no)
logging.debug(f"Derived symmetric key: {key.hex()}")
logging.debug("Derived symmetric key successfully.")
return key
except Exception as e:
logging.error(f"Error deriving symmetric key: {e}", exc_info=True)
+1
View File
@@ -659,6 +659,7 @@ async def import_vault(
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(data)
tmp_path = Path(tmp.name)
os.chmod(tmp_path, 0o600)
try:
pm.handle_import_database(tmp_path)
finally:
+3 -10
View File
@@ -1042,10 +1042,7 @@ class PasswordManager:
self.encryption_manager = EncryptionManager(key_b64, fingerprint_dir)
self.vault = Vault(self.encryption_manager, fingerprint_dir)
# Log the type and content of parent_seed
logger.debug(
f"Decrypted parent_seed: {self.parent_seed} (type: {type(self.parent_seed)})"
)
logger.debug("Parent seed decrypted successfully.")
# Validate the decrypted seed
if not self.validate_bip85_seed(self.parent_seed):
@@ -1222,9 +1219,7 @@ class PasswordManager:
logging.info("User password hashed and stored successfully.")
self.parent_seed = parent_seed
logger.debug(
f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})"
)
logger.debug("Parent seed initialized successfully.")
self.initialize_bip85()
self.initialize_managers()
@@ -1422,9 +1417,7 @@ class PasswordManager:
logging.info("Parent seed encrypted and saved successfully.")
self.parent_seed = seed # Ensure this is a string
logger.debug(
f"parent_seed set to: {self.parent_seed} (type: {type(self.parent_seed)})"
)
logger.debug("Parent seed initialized successfully.")
self.initialize_bip85()
self.initialize_managers()
+72
View File
@@ -284,6 +284,78 @@ async def test_vault_import_via_upload(client, tmp_path):
assert called.get("sync") is True
@pytest.mark.anyio
async def test_vault_import_upload_sets_secure_temp_permissions(client, monkeypatch):
cl, token = client
called = {}
chmod_calls = []
unlink_calls = []
def import_db(path):
called["path"] = path
api.app.state.pm.handle_import_database = import_db
api.app.state.pm.sync_vault = lambda: called.setdefault("sync", True)
real_chmod = api.os.chmod
real_unlink = api.os.unlink
def tracking_chmod(path, mode):
chmod_calls.append((Path(path), mode))
return real_chmod(path, mode)
def tracking_unlink(path):
unlink_calls.append(Path(path))
return real_unlink(path)
monkeypatch.setattr(api.os, "chmod", tracking_chmod)
monkeypatch.setattr(api.os, "unlink", tracking_unlink)
headers = {"Authorization": f"Bearer {token}"}
res = await cl.post(
"/api/v1/vault/import",
files={"file": ("c.json.enc", b"{}")},
headers=headers,
)
assert res.status_code == 200
assert res.json() == {"status": "ok"}
tmp_path_used = called["path"]
assert any(path == tmp_path_used and mode == 0o600 for path, mode in chmod_calls)
assert tmp_path_used in unlink_calls
@pytest.mark.anyio
async def test_vault_import_upload_temp_file_removed_on_failure(client, monkeypatch):
cl, token = client
called = {}
unlink_calls = []
def import_db(path):
called["path"] = path
raise ValueError("boom")
api.app.state.pm.handle_import_database = import_db
api.app.state.pm.sync_vault = lambda: called.setdefault("sync", True)
real_unlink = api.os.unlink
def tracking_unlink(path):
unlink_calls.append(Path(path))
return real_unlink(path)
monkeypatch.setattr(api.os, "unlink", tracking_unlink)
headers = {"Authorization": f"Bearer {token}"}
with pytest.raises(ValueError):
await cl.post(
"/api/v1/vault/import",
files={"file": ("c.json.enc", b"{}")},
headers=headers,
)
assert called["path"] in unlink_calls
assert not called.get("sync", False)
@pytest.mark.anyio
async def test_vault_import_upload_too_large(client):
cl, token = client
+14
View File
@@ -1,3 +1,5 @@
import logging
from local_bip85.bip85 import BIP85
@@ -50,3 +52,15 @@ def test_default_word_count_from_entropy_bytes():
bip85.derive_entropy(index=5, entropy_bytes=20, app_no=39)
assert ctx.last_path == "m/83696968'/39'/0'/20'/5'"
def test_derive_entropy_logs_do_not_expose_key_material(caplog):
bip85 = BIP85(b"\x00" * 64)
ctx = DummyCtx()
bip85.bip32_ctx = ctx
sensitive_hex = "00" * 32
with caplog.at_level(logging.DEBUG):
bip85.derive_entropy(index=0, entropy_bytes=16, app_no=39, word_count=12)
assert sensitive_hex not in caplog.text
+8
View File
@@ -1,4 +1,5 @@
import hashlib
import logging
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -23,6 +24,13 @@ def test_generate_fingerprint_deterministic():
assert fp1 == fp2
def test_generate_fingerprint_does_not_log_seed_phrase(caplog):
seed = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
with caplog.at_level(logging.DEBUG):
generate_fingerprint(seed)
assert seed not in caplog.text
def test_encryption_round_trip():
with TemporaryDirectory() as tmpdir:
key = base64.urlsafe_b64encode(os.urandom(32))
+1 -3
View File
@@ -30,15 +30,13 @@ def generate_fingerprint(seed_phrase: str, length: int = 16) -> Optional[str]:
try:
# Normalize the seed phrase
normalized_seed = seed_phrase.strip().lower()
logger.debug(f"Normalized seed: {normalized_seed}")
# Compute SHA-256 hash
sha256_hash = hashlib.sha256(normalized_seed.encode("utf-8")).hexdigest()
logger.debug(f"SHA-256 Hash: {sha256_hash}")
# Truncate to desired length
fingerprint = sha256_hash[:length].upper()
logger.debug(f"Generated Fingerprint: {fingerprint}")
logger.debug("Generated seed fingerprint successfully.")
return fingerprint
except Exception as e: