Add legacy Fernet migration

This commit is contained in:
thePR0M3TH3AN
2025-07-12 22:24:15 -04:00
parent ec61243c0c
commit c7cb9aa6ec
3 changed files with 91 additions and 9 deletions

View File

@@ -25,6 +25,8 @@ from typing import Optional
import base64 import base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag from cryptography.exceptions import InvalidTag
from cryptography.fernet import Fernet, InvalidToken
from cryptography.fernet import Fernet, InvalidToken
from termcolor import colored from termcolor import colored
from utils.file_lock import ( from utils.file_lock import (
exclusive_lock, exclusive_lock,
@@ -34,6 +36,16 @@ from utils.file_lock import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def decrypt_legacy_fernet(encryption_key: bytes | str, payload: bytes) -> bytes:
"""Decrypt *payload* using legacy Fernet."""
if isinstance(encryption_key, str):
key = encryption_key.encode()
else:
key = encryption_key
f = Fernet(key)
return f.decrypt(payload)
class EncryptionManager: class EncryptionManager:
""" """
EncryptionManager Class EncryptionManager Class
@@ -55,6 +67,7 @@ class EncryptionManager:
try: try:
if isinstance(encryption_key, str): if isinstance(encryption_key, str):
encryption_key = encryption_key.encode() encryption_key = encryption_key.encode()
self.key_b64 = encryption_key
self.key = base64.urlsafe_b64decode(encryption_key) self.key = base64.urlsafe_b64decode(encryption_key)
self.cipher = AESGCM(self.key) self.cipher = AESGCM(self.key)
logger.debug( logger.debug(
@@ -304,16 +317,31 @@ class EncryptionManager:
data = json.loads(json_content) data = json.loads(json_content)
logger.debug(f"JSON data loaded and decrypted from '{file_path}': {data}") logger.debug(f"JSON data loaded and decrypted from '{file_path}': {data}")
return data return data
except json.JSONDecodeError as e: except (InvalidTag, json.JSONDecodeError):
logger.error( logger.info(
f"Failed to decode JSON data from '{file_path}': {e}", exc_info=True f"AES-GCM decryption failed for '{file_path}', attempting legacy format"
) )
raise with exclusive_lock(file_path) as fh:
except InvalidTag: fh.seek(0)
logger.error( legacy_bytes = fh.read()
"Invalid encryption key or corrupted data while decrypting JSON data." try:
) legacy_plain = decrypt_legacy_fernet(self.key_b64, legacy_bytes)
raise data = json.loads(legacy_plain.decode("utf-8").strip())
except (InvalidToken, json.JSONDecodeError) as e:
logger.error(
f"Legacy decryption failed for '{file_path}': {e}", exc_info=True
)
raise
legacy_path = file_path.with_suffix(file_path.suffix + ".fernet")
os.rename(file_path, legacy_path)
chk = file_path.parent / f"{file_path.stem}_checksum.txt"
if chk.exists():
chk.rename(chk.with_suffix(chk.suffix + ".fernet"))
self.save_json_data(data, relative_path)
self.update_checksum(relative_path)
return data
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Failed to load JSON data from '{file_path}': {e}", exc_info=True f"Failed to load JSON data from '{file_path}': {e}", exc_info=True

View File

@@ -30,6 +30,17 @@ class Vault:
# ----- Password index helpers ----- # ----- Password index helpers -----
def load_index(self) -> dict: def load_index(self) -> dict:
"""Return decrypted password index data as a dict, applying migrations.""" """Return decrypted password index data as a dict, applying migrations."""
legacy_file = self.fingerprint_dir / "seedpass_passwords_db.json.enc"
if legacy_file.exists() and not self.index_file.exists():
legacy_checksum = (
self.fingerprint_dir / "seedpass_passwords_db_checksum.txt"
)
legacy_file.rename(self.index_file)
if legacy_checksum.exists():
legacy_checksum.rename(
self.fingerprint_dir / "seedpass_entries_db_checksum.txt"
)
data = self.encryption_manager.load_json_data(self.index_file) data = self.encryption_manager.load_json_data(self.index_file)
from .migrations import apply_migrations, LATEST_VERSION from .migrations import apply_migrations, LATEST_VERSION

View File

@@ -0,0 +1,43 @@
import json
import hashlib
from pathlib import Path
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
from utils.key_derivation import derive_index_key
from cryptography.fernet import Fernet
def test_legacy_index_migrates(tmp_path: Path):
vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
key = derive_index_key(TEST_SEED)
data = {
"schema_version": 4,
"entries": {
"0": {
"label": "a",
"length": 8,
"type": "password",
"kind": "password",
"notes": "",
"custom_fields": [],
"origin": "",
"tags": [],
}
},
}
enc = Fernet(key).encrypt(json.dumps(data).encode())
legacy_file = tmp_path / "seedpass_passwords_db.json.enc"
legacy_file.write_bytes(enc)
(tmp_path / "seedpass_passwords_db_checksum.txt").write_text(
hashlib.sha256(enc).hexdigest()
)
loaded = vault.load_index()
assert loaded == data
new_file = tmp_path / "seedpass_entries_db.json.enc"
assert new_file.exists()
assert not legacy_file.exists()
assert not (tmp_path / "seedpass_passwords_db_checksum.txt").exists()
assert (tmp_path / ("seedpass_entries_db.json.enc.fernet")).exists()