mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-07 14:58:56 +00:00
Automate script checksum handling
This commit is contained in:
23
README.md
23
README.md
@@ -43,7 +43,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No
|
||||
- **Encrypted Storage:** All seeds, login passwords, and sensitive index data are encrypted locally.
|
||||
- **Nostr Integration:** Post and retrieve your encrypted password index to/from the Nostr network.
|
||||
- **Chunked Snapshots:** Encrypted vaults are compressed and split into 50 KB chunks published as `kind 30071` events with a `kind 30070` manifest and `kind 30072` deltas.
|
||||
- **Checksum Verification:** Ensure the integrity of the script with checksum verification.
|
||||
- **Automatic Checksum Generation:** The script generates and verifies a SHA-256 checksum to detect tampering.
|
||||
- **Multiple Seed Profiles:** Manage separate seed profiles and switch between them seamlessly.
|
||||
- **Interactive TUI:** Navigate through menus to add, retrieve, and modify entries as well as configure Nostr settings.
|
||||
- **SeedPass 2FA:** Generate TOTP codes with a real-time countdown progress bar.
|
||||
@@ -279,16 +279,17 @@ Back in the Settings menu you can:
|
||||
|
||||
* Select `3` to change your master password.
|
||||
* Choose `4` to verify the script checksum.
|
||||
* Choose `5` to back up the parent seed.
|
||||
* Select `6` to export the database to an encrypted file.
|
||||
* Choose `7` to import a database from a backup file.
|
||||
* Select `8` to export all 2FA codes.
|
||||
* Choose `9` to set an additional backup location.
|
||||
* Select `10` to change the inactivity timeout.
|
||||
* Choose `11` to toggle Secret Mode and set the clipboard clear delay.
|
||||
* Select `12` to lock the vault and require re-entry of your password.
|
||||
* Choose `13` to return to the main menu.
|
||||
* Select `14` to view seed profile stats.
|
||||
* Select `5` to generate a new script checksum.
|
||||
* Choose `6` to back up the parent seed.
|
||||
* Select `7` to export the database to an encrypted file.
|
||||
* Choose `8` to import a database from a backup file.
|
||||
* Select `9` to export all 2FA codes.
|
||||
* Choose `10` to set an additional backup location.
|
||||
* Select `11` to change the inactivity timeout.
|
||||
* Choose `12` to lock the vault and require re-entry of your password.
|
||||
* Select `13` to view seed profile stats.
|
||||
* Choose `14` to toggle Secret Mode and set the clipboard clear delay.
|
||||
* Select `15` to return to the main menu.
|
||||
|
||||
## Running Tests
|
||||
|
||||
|
@@ -7,7 +7,7 @@ SRC_DIR = PROJECT_ROOT / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
from utils.checksum import calculate_checksum
|
||||
from utils.checksum import update_checksum_file
|
||||
from constants import SCRIPT_CHECKSUM_FILE, initialize_app
|
||||
|
||||
|
||||
@@ -15,11 +15,8 @@ def main() -> None:
|
||||
"""Calculate checksum for the main script and write it to SCRIPT_CHECKSUM_FILE."""
|
||||
initialize_app()
|
||||
script_path = SRC_DIR / "password_manager" / "manager.py"
|
||||
checksum = calculate_checksum(str(script_path))
|
||||
if checksum is None:
|
||||
raise SystemExit(f"Failed to calculate checksum for {script_path}")
|
||||
|
||||
SCRIPT_CHECKSUM_FILE.write_text(checksum)
|
||||
if not update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)):
|
||||
raise SystemExit(f"Failed to update checksum for {script_path}")
|
||||
print(f"Updated checksum written to {SCRIPT_CHECKSUM_FILE}")
|
||||
|
||||
|
||||
|
41
src/main.py
41
src/main.py
@@ -617,16 +617,17 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
||||
print("2. Nostr")
|
||||
print("3. Change password")
|
||||
print("4. Verify Script Checksum")
|
||||
print("5. Backup Parent Seed")
|
||||
print("6. Export database")
|
||||
print("7. Import database")
|
||||
print("8. Export 2FA codes")
|
||||
print("9. Set additional backup location")
|
||||
print("10. Set inactivity timeout")
|
||||
print("11. Lock Vault")
|
||||
print("12. Stats")
|
||||
print("13. Toggle Secret Mode")
|
||||
print("14. Back")
|
||||
print("5. Generate Script Checksum")
|
||||
print("6. Backup Parent Seed")
|
||||
print("7. Export database")
|
||||
print("8. Import database")
|
||||
print("9. Export 2FA codes")
|
||||
print("10. Set additional backup location")
|
||||
print("11. Set inactivity timeout")
|
||||
print("12. Lock Vault")
|
||||
print("13. Stats")
|
||||
print("14. Toggle Secret Mode")
|
||||
print("15. Back")
|
||||
choice = input("Select an option: ").strip()
|
||||
if choice == "1":
|
||||
handle_profiles_menu(password_manager)
|
||||
@@ -637,28 +638,30 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
||||
elif choice == "4":
|
||||
password_manager.handle_verify_checksum()
|
||||
elif choice == "5":
|
||||
password_manager.handle_backup_reveal_parent_seed()
|
||||
password_manager.handle_update_script_checksum()
|
||||
elif choice == "6":
|
||||
password_manager.handle_export_database()
|
||||
password_manager.handle_backup_reveal_parent_seed()
|
||||
elif choice == "7":
|
||||
password_manager.handle_export_database()
|
||||
elif choice == "8":
|
||||
path = input("Enter path to backup file: ").strip()
|
||||
if path:
|
||||
password_manager.handle_import_database(Path(path))
|
||||
elif choice == "8":
|
||||
password_manager.handle_export_totp_codes()
|
||||
elif choice == "9":
|
||||
handle_set_additional_backup_location(password_manager)
|
||||
password_manager.handle_export_totp_codes()
|
||||
elif choice == "10":
|
||||
handle_set_inactivity_timeout(password_manager)
|
||||
handle_set_additional_backup_location(password_manager)
|
||||
elif choice == "11":
|
||||
handle_set_inactivity_timeout(password_manager)
|
||||
elif choice == "12":
|
||||
password_manager.lock_vault()
|
||||
print(colored("Vault locked. Please re-enter your password.", "yellow"))
|
||||
password_manager.unlock_vault()
|
||||
elif choice == "12":
|
||||
handle_display_stats(password_manager)
|
||||
elif choice == "13":
|
||||
handle_toggle_secret_mode(password_manager)
|
||||
handle_display_stats(password_manager)
|
||||
elif choice == "14":
|
||||
handle_toggle_secret_mode(password_manager)
|
||||
elif choice == "15":
|
||||
break
|
||||
else:
|
||||
print(colored("Invalid choice.", "red"))
|
||||
|
@@ -34,7 +34,13 @@ from utils.key_derivation import (
|
||||
derive_index_key,
|
||||
EncryptionMode,
|
||||
)
|
||||
from utils.checksum import calculate_checksum, verify_checksum, json_checksum
|
||||
from utils.checksum import (
|
||||
calculate_checksum,
|
||||
verify_checksum,
|
||||
json_checksum,
|
||||
initialize_checksum,
|
||||
update_checksum_file,
|
||||
)
|
||||
from utils.password_prompt import (
|
||||
prompt_for_password,
|
||||
prompt_existing_password,
|
||||
@@ -89,6 +95,7 @@ class PasswordManager:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the PasswordManager."""
|
||||
initialize_app()
|
||||
self.ensure_script_checksum()
|
||||
self.encryption_mode: EncryptionMode = EncryptionMode.SEED_ONLY
|
||||
self.encryption_manager: Optional[EncryptionManager] = None
|
||||
self.entry_manager: Optional[EntryManager] = None
|
||||
@@ -119,6 +126,23 @@ class PasswordManager:
|
||||
# Set the current fingerprint directory
|
||||
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
|
||||
|
||||
def ensure_script_checksum(self) -> None:
|
||||
"""Initialize or verify the checksum of the manager script."""
|
||||
script_path = Path(__file__).resolve()
|
||||
if not SCRIPT_CHECKSUM_FILE.exists():
|
||||
initialize_checksum(str(script_path), SCRIPT_CHECKSUM_FILE)
|
||||
return
|
||||
checksum = calculate_checksum(str(script_path))
|
||||
if checksum and not verify_checksum(checksum, SCRIPT_CHECKSUM_FILE):
|
||||
logging.warning("Script checksum mismatch detected on startup")
|
||||
print(
|
||||
colored(
|
||||
"Warning: script checksum mismatch. "
|
||||
"Run 'Generate Script Checksum' in Settings if you've updated the app.",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def parent_seed(self) -> Optional[str]:
|
||||
"""Return the decrypted parent seed if set."""
|
||||
@@ -1473,7 +1497,7 @@ class PasswordManager:
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
colored(
|
||||
"Checksum file missing. Run scripts/update_checksum.py to generate it.",
|
||||
"Checksum file missing. Run scripts/update_checksum.py or choose 'Generate Script Checksum' in Settings.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
@@ -1495,6 +1519,26 @@ class PasswordManager:
|
||||
logging.error(f"Error during checksum verification: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to verify checksum: {e}", "red"))
|
||||
|
||||
def handle_update_script_checksum(self) -> None:
|
||||
"""Generate a new checksum for the manager script."""
|
||||
if not confirm_action("Generate new script checksum? (Y/N): "):
|
||||
print(colored("Operation cancelled.", "yellow"))
|
||||
return
|
||||
try:
|
||||
script_path = Path(__file__).resolve()
|
||||
if update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)):
|
||||
print(
|
||||
colored(
|
||||
f"Checksum updated at '{SCRIPT_CHECKSUM_FILE}'.",
|
||||
"green",
|
||||
)
|
||||
)
|
||||
else:
|
||||
print(colored("Failed to update checksum.", "red"))
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating checksum: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to update checksum: {e}", "red"))
|
||||
|
||||
def get_encrypted_data(self) -> Optional[bytes]:
|
||||
"""
|
||||
Retrieves the encrypted password index data.
|
||||
|
@@ -57,7 +57,7 @@ def test_handle_verify_checksum_missing(monkeypatch, tmp_path, capsys):
|
||||
monkeypatch.setattr("password_manager.manager.verify_checksum", raise_missing)
|
||||
pm.handle_verify_checksum()
|
||||
out = capsys.readouterr().out.lower()
|
||||
assert "update_checksum.py" in out
|
||||
assert "generate script checksum" in out
|
||||
|
||||
|
||||
def test_backup_and_restore_database(monkeypatch, capsys):
|
||||
|
@@ -93,7 +93,7 @@ def test_settings_menu_additional_backup(monkeypatch):
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch)
|
||||
|
||||
inputs = iter(["9", "14"])
|
||||
inputs = iter(["10", "15"])
|
||||
with patch("main.handle_set_additional_backup_location") as handler:
|
||||
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
||||
main.handle_settings(pm)
|
||||
|
@@ -21,6 +21,8 @@ try:
|
||||
verify_checksum,
|
||||
json_checksum,
|
||||
canonical_json_dumps,
|
||||
initialize_checksum,
|
||||
update_checksum_file,
|
||||
)
|
||||
from .password_prompt import prompt_for_password
|
||||
from .input_utils import timed_input
|
||||
@@ -45,6 +47,8 @@ __all__ = [
|
||||
"verify_checksum",
|
||||
"json_checksum",
|
||||
"canonical_json_dumps",
|
||||
"initialize_checksum",
|
||||
"update_checksum_file",
|
||||
"exclusive_lock",
|
||||
"shared_lock",
|
||||
"prompt_for_password",
|
||||
|
@@ -198,3 +198,29 @@ def initialize_checksum(file_path: str, checksum_file_path: str) -> bool:
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def update_checksum_file(file_path: str, checksum_file_path: str) -> bool:
|
||||
"""Update ``checksum_file_path`` with the SHA-256 checksum of ``file_path``."""
|
||||
checksum = calculate_checksum(file_path)
|
||||
if checksum is None:
|
||||
return False
|
||||
try:
|
||||
with open(checksum_file_path, "w") as f:
|
||||
f.write(checksum)
|
||||
logging.debug(
|
||||
f"Updated checksum for '{file_path}' to '{checksum}' at '{checksum_file_path}'."
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logging.error(
|
||||
f"Failed to update checksum file '{checksum_file_path}': {exc}",
|
||||
exc_info=True,
|
||||
)
|
||||
print(
|
||||
colored(
|
||||
f"Error: Failed to update checksum file '{checksum_file_path}': {exc}",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
Reference in New Issue
Block a user