mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
Add test for friendly decryption message?
This commit is contained in:
@@ -29,6 +29,7 @@ from utils.file_lock import exclusive_lock
|
|||||||
from mnemonic import Mnemonic
|
from mnemonic import Mnemonic
|
||||||
from utils.password_prompt import prompt_existing_password
|
from utils.password_prompt import prompt_existing_password
|
||||||
from utils.key_derivation import KdfConfig, CURRENT_KDF_VERSION
|
from utils.key_derivation import KdfConfig, CURRENT_KDF_VERSION
|
||||||
|
from .errors import DecryptionError
|
||||||
|
|
||||||
# Instantiate the logger
|
# Instantiate the logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -137,12 +138,13 @@ class EncryptionManager:
|
|||||||
ciphertext = encrypted_data[15:]
|
ciphertext = encrypted_data[15:]
|
||||||
if len(ciphertext) < 16:
|
if len(ciphertext) < 16:
|
||||||
logger.error("AES-GCM payload too short")
|
logger.error("AES-GCM payload too short")
|
||||||
raise InvalidToken("AES-GCM payload too short")
|
raise DecryptionError("Incorrect password or corrupt file")
|
||||||
return self.cipher.decrypt(nonce, ciphertext, None)
|
return self.cipher.decrypt(nonce, ciphertext, None)
|
||||||
except InvalidTag as e:
|
except InvalidTag as e:
|
||||||
msg = f"Failed to decrypt{ctx}: invalid key or corrupt file"
|
logger.error(
|
||||||
logger.error(msg)
|
f"Failed to decrypt{ctx}: incorrect password or corrupt file"
|
||||||
raise InvalidToken(msg) from e
|
)
|
||||||
|
raise DecryptionError("Incorrect password or corrupt file") from e
|
||||||
|
|
||||||
# Next try the older V2 format
|
# Next try the older V2 format
|
||||||
if encrypted_data.startswith(b"V2:"):
|
if encrypted_data.startswith(b"V2:"):
|
||||||
@@ -151,7 +153,7 @@ class EncryptionManager:
|
|||||||
ciphertext = encrypted_data[15:]
|
ciphertext = encrypted_data[15:]
|
||||||
if len(ciphertext) < 16:
|
if len(ciphertext) < 16:
|
||||||
logger.error("AES-GCM payload too short")
|
logger.error("AES-GCM payload too short")
|
||||||
raise InvalidToken("AES-GCM payload too short")
|
raise DecryptionError("Incorrect password or corrupt file")
|
||||||
return self.cipher.decrypt(nonce, ciphertext, None)
|
return self.cipher.decrypt(nonce, ciphertext, None)
|
||||||
except InvalidTag as e:
|
except InvalidTag as e:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -164,9 +166,12 @@ class EncryptionManager:
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
except InvalidToken:
|
except InvalidToken:
|
||||||
msg = f"Failed to decrypt{ctx}: invalid key or corrupt file"
|
logger.error(
|
||||||
logger.error(msg)
|
f"Failed to decrypt{ctx}: incorrect password or corrupt file"
|
||||||
raise InvalidToken(msg) from e
|
)
|
||||||
|
raise DecryptionError(
|
||||||
|
"Incorrect password or corrupt file"
|
||||||
|
) from e
|
||||||
|
|
||||||
# If it's neither V3 nor V2, assume legacy Fernet format
|
# If it's neither V3 nor V2, assume legacy Fernet format
|
||||||
logger.warning("Data is in legacy Fernet format. Attempting migration.")
|
logger.warning("Data is in legacy Fernet format. Attempting migration.")
|
||||||
@@ -176,18 +181,19 @@ class EncryptionManager:
|
|||||||
logger.error(
|
logger.error(
|
||||||
"Legacy Fernet decryption failed. Vault may be corrupt or key is incorrect."
|
"Legacy Fernet decryption failed. Vault may be corrupt or key is incorrect."
|
||||||
)
|
)
|
||||||
raise e
|
raise DecryptionError("Incorrect password or corrupt file") from e
|
||||||
|
|
||||||
except (InvalidToken, InvalidTag) as e:
|
except DecryptionError as e:
|
||||||
if encrypted_data.startswith(b"V3|") or encrypted_data.startswith(b"V2:"):
|
if (
|
||||||
# Already determined not to be legacy; re-raise
|
encrypted_data.startswith(b"V3|")
|
||||||
raise
|
or encrypted_data.startswith(b"V2:")
|
||||||
if isinstance(e, InvalidToken) and str(e) == "AES-GCM payload too short":
|
or not self._legacy_migrate_flag
|
||||||
raise
|
):
|
||||||
if not self._legacy_migrate_flag:
|
|
||||||
raise
|
raise
|
||||||
logger.debug(f"Could not decrypt data{ctx}: {e}")
|
logger.debug(f"Could not decrypt data{ctx}: {e}")
|
||||||
raise LegacyFormatRequiresMigrationError(context)
|
raise LegacyFormatRequiresMigrationError(context) from e
|
||||||
|
except (InvalidToken, InvalidTag) as e: # pragma: no cover - safety net
|
||||||
|
raise DecryptionError("Incorrect password or corrupt file") from e
|
||||||
|
|
||||||
def decrypt_legacy(
|
def decrypt_legacy(
|
||||||
self, encrypted_data: bytes, password: str, context: Optional[str] = None
|
self, encrypted_data: bytes, password: str, context: Optional[str] = None
|
||||||
@@ -224,9 +230,7 @@ class EncryptionManager:
|
|||||||
except Exception as e2: # pragma: no cover - try next iteration
|
except Exception as e2: # pragma: no cover - try next iteration
|
||||||
last_exc = e2
|
last_exc = e2
|
||||||
logger.error(f"Failed legacy decryption attempt: {last_exc}", exc_info=True)
|
logger.error(f"Failed legacy decryption attempt: {last_exc}", exc_info=True)
|
||||||
raise InvalidToken(
|
raise DecryptionError(f"Incorrect password or corrupt file") from last_exc
|
||||||
f"Could not decrypt{ctx} with any available method."
|
|
||||||
) from last_exc
|
|
||||||
|
|
||||||
# --- All functions below this point now use the smart `decrypt_data` method ---
|
# --- All functions below this point now use the smart `decrypt_data` method ---
|
||||||
|
|
||||||
@@ -409,10 +413,14 @@ class EncryptionManager:
|
|||||||
if return_kdf:
|
if return_kdf:
|
||||||
return data, kdf
|
return data, kdf
|
||||||
return data
|
return data
|
||||||
except (InvalidToken, InvalidTag) as e:
|
except DecryptionError as e:
|
||||||
msg = f"Failed to decrypt or parse data from {file_path}: {e}"
|
msg = f"Failed to decrypt or parse data from {file_path}: {e}"
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
raise InvalidToken(msg) from e
|
raise
|
||||||
|
except (InvalidToken, InvalidTag) as e: # pragma: no cover - legacy safety
|
||||||
|
msg = f"Failed to decrypt or parse data from {file_path}: {e}"
|
||||||
|
logger.error(msg)
|
||||||
|
raise DecryptionError("Incorrect password or corrupt file") from e
|
||||||
except JSONDecodeError as e:
|
except JSONDecodeError as e:
|
||||||
msg = f"Failed to parse JSON data from {file_path}: {e}"
|
msg = f"Failed to parse JSON data from {file_path}: {e}"
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
@@ -484,7 +492,7 @@ class EncryptionManager:
|
|||||||
logger.info("Index file from Nostr was processed and saved successfully.")
|
logger.info("Index file from Nostr was processed and saved successfully.")
|
||||||
self.last_migration_performed = is_legacy
|
self.last_migration_performed = is_legacy
|
||||||
return True
|
return True
|
||||||
except (InvalidToken, LegacyFormatRequiresMigrationError):
|
except (DecryptionError, LegacyFormatRequiresMigrationError):
|
||||||
try:
|
try:
|
||||||
password = prompt_existing_password(
|
password = prompt_existing_password(
|
||||||
"Enter your master password for legacy decryption: "
|
"Enter your master password for legacy decryption: "
|
||||||
|
@@ -18,4 +18,8 @@ class SeedPassError(ClickException):
|
|||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["SeedPassError"]
|
class DecryptionError(SeedPassError):
|
||||||
|
"""Raised when encrypted data cannot be decrypted."""
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["SeedPassError", "DecryptionError"]
|
||||||
|
@@ -37,8 +37,7 @@ from .password_generation import PasswordGenerator
|
|||||||
from .backup import BackupManager
|
from .backup import BackupManager
|
||||||
from .vault import Vault
|
from .vault import Vault
|
||||||
from .portable_backup import export_backup, import_backup, PortableMode
|
from .portable_backup import export_backup, import_backup, PortableMode
|
||||||
from cryptography.fernet import InvalidToken
|
from .errors import SeedPassError, DecryptionError
|
||||||
from .errors import SeedPassError
|
|
||||||
from .totp import TotpManager
|
from .totp import TotpManager
|
||||||
from .entry_types import EntryType
|
from .entry_types import EntryType
|
||||||
from .pubsub import bus
|
from .pubsub import bus
|
||||||
@@ -752,13 +751,11 @@ class PasswordManager:
|
|||||||
):
|
):
|
||||||
self.config_manager.set_kdf_iterations(iter_try)
|
self.config_manager.set_kdf_iterations(iter_try)
|
||||||
break
|
break
|
||||||
except InvalidToken:
|
except DecryptionError:
|
||||||
seed_mgr = None
|
seed_mgr = None
|
||||||
|
|
||||||
if seed_mgr is None:
|
if seed_mgr is None:
|
||||||
msg = (
|
msg = "Incorrect password or corrupt file"
|
||||||
"Invalid password for selected seed profile. Please try again."
|
|
||||||
)
|
|
||||||
print(colored(msg, "red"))
|
print(colored(msg, "red"))
|
||||||
attempts += 1
|
attempts += 1
|
||||||
password = None
|
password = None
|
||||||
@@ -830,6 +827,10 @@ class PasswordManager:
|
|||||||
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate()
|
||||||
self.derive_key_hierarchy(seed_bytes)
|
self.derive_key_hierarchy(seed_bytes)
|
||||||
self.bip85 = BIP85(seed_bytes)
|
self.bip85 = BIP85(seed_bytes)
|
||||||
|
except DecryptionError as e:
|
||||||
|
logger.error(f"Failed to load parent seed: {e}", exc_info=True)
|
||||||
|
print(colored("Incorrect password or corrupt file", "red"))
|
||||||
|
raise SeedPassError("Incorrect password or corrupt file") from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load parent seed: {e}", exc_info=True)
|
logger.error(f"Failed to load parent seed: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to load parent seed: {e}", "red"))
|
print(colored(f"Error: Failed to load parent seed: {e}", "red"))
|
||||||
@@ -4246,7 +4247,7 @@ class PasswordManager:
|
|||||||
src,
|
src,
|
||||||
parent_seed=self.parent_seed,
|
parent_seed=self.parent_seed,
|
||||||
)
|
)
|
||||||
except InvalidToken:
|
except DecryptionError:
|
||||||
logging.error("Invalid backup token during import", exc_info=True)
|
logging.error("Invalid backup token during import", exc_info=True)
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
@@ -4488,7 +4489,7 @@ class PasswordManager:
|
|||||||
else:
|
else:
|
||||||
logging.warning("Password verification failed.")
|
logging.warning("Password verification failed.")
|
||||||
return is_correct
|
return is_correct
|
||||||
except InvalidToken as e:
|
except DecryptionError as e:
|
||||||
logging.error(f"Failed to decrypt config: {e}")
|
logging.error(f"Failed to decrypt config: {e}")
|
||||||
print(
|
print(
|
||||||
colored(
|
colored(
|
||||||
|
23
src/tests/test_invalid_password_message.py
Normal file
23
src/tests/test_invalid_password_message.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from seedpass.core.manager import PasswordManager
|
||||||
|
from seedpass.core.config_manager import ConfigManager
|
||||||
|
from seedpass.core.errors import SeedPassError
|
||||||
|
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_password_shows_friendly_message_once(capsys):
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
tmp_path = Path(tmpdir)
|
||||||
|
vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
|
||||||
|
pm = PasswordManager.__new__(PasswordManager)
|
||||||
|
pm.config_manager = ConfigManager(vault, tmp_path)
|
||||||
|
pm.fingerprint_dir = tmp_path
|
||||||
|
pm.parent_seed = ""
|
||||||
|
with pytest.raises(SeedPassError):
|
||||||
|
pm.load_parent_seed(tmp_path, password="wrongpass")
|
||||||
|
captured = capsys.readouterr().out
|
||||||
|
assert captured.count("Incorrect password or corrupt file") == 1
|
Reference in New Issue
Block a user