mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-07 21:32:25 +00:00
Merge pull request #838 from PR0M3TH3AN/codex/add-manual-export-without-encryption
Add option to export and import database without encryption
This commit is contained in:
8 files changed
+151
-32
No files matched your search
@@ -36,7 +36,7 @@ from .entry_management import EntryManager
|
||||
from .password_generation import PasswordGenerator
|
||||
from .backup import BackupManager
|
||||
from .vault import Vault
|
||||
from .portable_backup import export_backup, import_backup
|
||||
from .portable_backup import export_backup, import_backup, PortableMode
|
||||
from cryptography.fernet import InvalidToken
|
||||
from .totp import TotpManager
|
||||
from .entry_types import EntryType
|
||||
@@ -4102,8 +4102,15 @@ class PasswordManager:
|
||||
def handle_export_database(
|
||||
self,
|
||||
dest: Path | None = None,
|
||||
*,
|
||||
encrypt: bool | None = None,
|
||||
) -> Path | None:
|
||||
"""Export the current database to an encrypted portable file."""
|
||||
"""Export the current database to a portable file.
|
||||
|
||||
If ``encrypt`` is ``True`` (default) the payload is encrypted. When
|
||||
``encrypt`` is ``False`` the export contains plaintext data. When
|
||||
``encrypt`` is ``None`` the user is prompted interactively.
|
||||
"""
|
||||
try:
|
||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
||||
clear_header_with_notification(
|
||||
@@ -4113,11 +4120,16 @@ class PasswordManager:
|
||||
parent_fingerprint=parent_fp,
|
||||
child_fingerprint=child_fp,
|
||||
)
|
||||
if encrypt is None:
|
||||
encrypt = not confirm_action(
|
||||
"Export database without encryption? (Y/N): "
|
||||
)
|
||||
path = export_backup(
|
||||
self.vault,
|
||||
self.backup_manager,
|
||||
dest,
|
||||
parent_seed=self.parent_seed,
|
||||
encrypt=encrypt,
|
||||
)
|
||||
print(colored(f"Database exported to '{path}'.", "green"))
|
||||
audit_logger = getattr(self, "audit_logger", None)
|
||||
@@ -4132,15 +4144,26 @@ class PasswordManager:
|
||||
def handle_import_database(self, src: Path) -> None:
|
||||
"""Import a portable database file, replacing the current index."""
|
||||
|
||||
if not src.name.endswith(".json.enc"):
|
||||
if not (src.name.endswith(".json.enc") or src.name.endswith(".json")):
|
||||
print(
|
||||
colored(
|
||||
"Error: Selected file must be a SeedPass database backup (.json.enc).",
|
||||
"Error: Selected file must be a SeedPass database backup (.json or .json.enc).",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Determine encryption mode for post-processing
|
||||
mode = None
|
||||
try:
|
||||
raw = src.read_bytes()
|
||||
if src.suffix.endswith(".enc"):
|
||||
raw = self.vault.encryption_manager.decrypt_data(raw, context=str(src))
|
||||
wrapper = json.loads(raw.decode("utf-8"))
|
||||
mode = wrapper.get("encryption_mode")
|
||||
except Exception:
|
||||
mode = None
|
||||
|
||||
fp, parent_fp, child_fp = self.header_fingerprint_args
|
||||
clear_header_with_notification(
|
||||
self,
|
||||
@@ -4180,6 +4203,23 @@ class PasswordManager:
|
||||
)
|
||||
return
|
||||
|
||||
if mode == PortableMode.NONE.value:
|
||||
try:
|
||||
password = prompt_new_password()
|
||||
iterations = self.config_manager.get_kdf_iterations()
|
||||
seed_key = derive_key_from_password(
|
||||
password, self.current_fingerprint, iterations=iterations
|
||||
)
|
||||
seed_mgr = EncryptionManager(seed_key, self.fingerprint_dir)
|
||||
seed_mgr.encrypt_parent_seed(self.parent_seed)
|
||||
self.store_hashed_password(password)
|
||||
except Exception as e:
|
||||
logging.error(
|
||||
f"Failed to set master password after import: {e}", exc_info=True
|
||||
)
|
||||
print(colored(f"Error: Failed to set master password: {e}", "red"))
|
||||
return
|
||||
|
||||
print(colored("Database imported successfully.", "green"))
|
||||
self.sync_vault()
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ class PortableMode(Enum):
|
||||
"""Encryption mode for portable exports."""
|
||||
|
||||
SEED_ONLY = EncryptionMode.SEED_ONLY.value
|
||||
NONE = "none"
|
||||
|
||||
|
||||
def _derive_export_key(seed: str) -> bytes:
|
||||
@@ -47,8 +48,15 @@ def export_backup(
|
||||
*,
|
||||
publish: bool = False,
|
||||
parent_seed: str | None = None,
|
||||
encrypt: bool = True,
|
||||
) -> Path:
|
||||
"""Export the current vault state to a portable encrypted file."""
|
||||
"""Export the current vault state to a portable file.
|
||||
|
||||
When ``encrypt`` is ``True`` (the default) the payload is encrypted with a
|
||||
key derived from the parent seed. When ``encrypt`` is ``False`` the payload
|
||||
is written in plaintext and the wrapper records an ``encryption_mode`` of
|
||||
:data:`PortableMode.NONE`.
|
||||
"""
|
||||
|
||||
if dest_path is None:
|
||||
ts = int(time.time())
|
||||
@@ -57,24 +65,32 @@ def export_backup(
|
||||
dest_path = dest_dir / EXPORT_NAME_TEMPLATE.format(ts=ts)
|
||||
|
||||
index_data = vault.load_index()
|
||||
seed = (
|
||||
parent_seed
|
||||
if parent_seed is not None
|
||||
else vault.encryption_manager.decrypt_parent_seed()
|
||||
)
|
||||
key = _derive_export_key(seed)
|
||||
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
||||
|
||||
canonical = canonical_json_dumps(index_data)
|
||||
payload_bytes = enc_mgr.encrypt_data(canonical.encode("utf-8"))
|
||||
|
||||
if encrypt:
|
||||
seed = (
|
||||
parent_seed
|
||||
if parent_seed is not None
|
||||
else vault.encryption_manager.decrypt_parent_seed()
|
||||
)
|
||||
key = _derive_export_key(seed)
|
||||
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
||||
payload_bytes = enc_mgr.encrypt_data(canonical.encode("utf-8"))
|
||||
mode = PortableMode.SEED_ONLY
|
||||
cipher = "aes-gcm"
|
||||
else:
|
||||
payload_bytes = canonical.encode("utf-8")
|
||||
mode = PortableMode.NONE
|
||||
cipher = "none"
|
||||
|
||||
checksum = json_checksum(index_data)
|
||||
|
||||
wrapper = {
|
||||
"format_version": FORMAT_VERSION,
|
||||
"created_at": int(time.time()),
|
||||
"fingerprint": vault.fingerprint_dir.name,
|
||||
"encryption_mode": PortableMode.SEED_ONLY.value,
|
||||
"cipher": "aes-gcm",
|
||||
"encryption_mode": mode.value,
|
||||
"cipher": cipher,
|
||||
"checksum": checksum,
|
||||
"payload": base64.b64encode(payload_bytes).decode("utf-8"),
|
||||
}
|
||||
@@ -118,19 +134,24 @@ def import_backup(
|
||||
if wrapper.get("format_version") != FORMAT_VERSION:
|
||||
raise ValueError("Unsupported backup format")
|
||||
|
||||
if wrapper.get("encryption_mode") != PortableMode.SEED_ONLY.value:
|
||||
raise ValueError("Unsupported encryption mode")
|
||||
mode = wrapper.get("encryption_mode")
|
||||
payload = base64.b64decode(wrapper["payload"])
|
||||
|
||||
seed = (
|
||||
parent_seed
|
||||
if parent_seed is not None
|
||||
else vault.encryption_manager.decrypt_parent_seed()
|
||||
)
|
||||
key = _derive_export_key(seed)
|
||||
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
||||
enc_mgr._legacy_migrate_flag = False
|
||||
index_bytes = enc_mgr.decrypt_data(payload, context="backup payload")
|
||||
if mode == PortableMode.SEED_ONLY.value:
|
||||
seed = (
|
||||
parent_seed
|
||||
if parent_seed is not None
|
||||
else vault.encryption_manager.decrypt_parent_seed()
|
||||
)
|
||||
key = _derive_export_key(seed)
|
||||
enc_mgr = EncryptionManager(key, vault.fingerprint_dir)
|
||||
enc_mgr._legacy_migrate_flag = False
|
||||
index_bytes = enc_mgr.decrypt_data(payload, context="backup payload")
|
||||
elif mode == PortableMode.NONE.value:
|
||||
index_bytes = payload
|
||||
else:
|
||||
raise ValueError("Unsupported encryption mode")
|
||||
|
||||
index = json.loads(index_bytes.decode("utf-8"))
|
||||
|
||||
checksum = json_checksum(index)
|
||||
|
||||
Reference in new issue
Block a user