mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
* Select `3` to change your master password.
|
||||||
* Choose `4` to verify the script checksum.
|
* Choose `4` to verify the script checksum.
|
||||||
* Choose `5` to back up the parent seed.
|
* Select `5` to generate a new script checksum.
|
||||||
* Select `6` to export the database to an encrypted file.
|
* Choose `6` to back up the parent seed.
|
||||||
* Choose `7` to import a database from a backup file.
|
* Select `7` to export the database to an encrypted file.
|
||||||
* Select `8` to export all 2FA codes.
|
* Choose `8` to import a database from a backup file.
|
||||||
* Choose `9` to set an additional backup location.
|
* Select `9` to export all 2FA codes.
|
||||||
* Select `10` to change the inactivity timeout.
|
* Choose `10` to set an additional backup location.
|
||||||
* Choose `11` to toggle Secret Mode and set the clipboard clear delay.
|
* Select `11` to change the inactivity timeout.
|
||||||
* Select `12` to lock the vault and require re-entry of your password.
|
* Choose `12` to lock the vault and require re-entry of your password.
|
||||||
* Choose `13` to return to the main menu.
|
* Select `13` to view seed profile stats.
|
||||||
* Select `14` 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
|
## Running Tests
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ SRC_DIR = PROJECT_ROOT / "src"
|
|||||||
if str(SRC_DIR) not in sys.path:
|
if str(SRC_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(SRC_DIR))
|
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
|
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."""
|
"""Calculate checksum for the main script and write it to SCRIPT_CHECKSUM_FILE."""
|
||||||
initialize_app()
|
initialize_app()
|
||||||
script_path = SRC_DIR / "password_manager" / "manager.py"
|
script_path = SRC_DIR / "password_manager" / "manager.py"
|
||||||
checksum = calculate_checksum(str(script_path))
|
if not update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)):
|
||||||
if checksum is None:
|
raise SystemExit(f"Failed to update checksum for {script_path}")
|
||||||
raise SystemExit(f"Failed to calculate checksum for {script_path}")
|
|
||||||
|
|
||||||
SCRIPT_CHECKSUM_FILE.write_text(checksum)
|
|
||||||
print(f"Updated checksum written to {SCRIPT_CHECKSUM_FILE}")
|
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("2. Nostr")
|
||||||
print("3. Change password")
|
print("3. Change password")
|
||||||
print("4. Verify Script Checksum")
|
print("4. Verify Script Checksum")
|
||||||
print("5. Backup Parent Seed")
|
print("5. Generate Script Checksum")
|
||||||
print("6. Export database")
|
print("6. Backup Parent Seed")
|
||||||
print("7. Import database")
|
print("7. Export database")
|
||||||
print("8. Export 2FA codes")
|
print("8. Import database")
|
||||||
print("9. Set additional backup location")
|
print("9. Export 2FA codes")
|
||||||
print("10. Set inactivity timeout")
|
print("10. Set additional backup location")
|
||||||
print("11. Lock Vault")
|
print("11. Set inactivity timeout")
|
||||||
print("12. Stats")
|
print("12. Lock Vault")
|
||||||
print("13. Toggle Secret Mode")
|
print("13. Stats")
|
||||||
print("14. Back")
|
print("14. Toggle Secret Mode")
|
||||||
|
print("15. Back")
|
||||||
choice = input("Select an option: ").strip()
|
choice = input("Select an option: ").strip()
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
handle_profiles_menu(password_manager)
|
handle_profiles_menu(password_manager)
|
||||||
@@ -637,28 +638,30 @@ def handle_settings(password_manager: PasswordManager) -> None:
|
|||||||
elif choice == "4":
|
elif choice == "4":
|
||||||
password_manager.handle_verify_checksum()
|
password_manager.handle_verify_checksum()
|
||||||
elif choice == "5":
|
elif choice == "5":
|
||||||
password_manager.handle_backup_reveal_parent_seed()
|
password_manager.handle_update_script_checksum()
|
||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
password_manager.handle_export_database()
|
password_manager.handle_backup_reveal_parent_seed()
|
||||||
elif choice == "7":
|
elif choice == "7":
|
||||||
|
password_manager.handle_export_database()
|
||||||
|
elif choice == "8":
|
||||||
path = input("Enter path to backup file: ").strip()
|
path = input("Enter path to backup file: ").strip()
|
||||||
if path:
|
if path:
|
||||||
password_manager.handle_import_database(Path(path))
|
password_manager.handle_import_database(Path(path))
|
||||||
elif choice == "8":
|
|
||||||
password_manager.handle_export_totp_codes()
|
|
||||||
elif choice == "9":
|
elif choice == "9":
|
||||||
handle_set_additional_backup_location(password_manager)
|
password_manager.handle_export_totp_codes()
|
||||||
elif choice == "10":
|
elif choice == "10":
|
||||||
handle_set_inactivity_timeout(password_manager)
|
handle_set_additional_backup_location(password_manager)
|
||||||
elif choice == "11":
|
elif choice == "11":
|
||||||
|
handle_set_inactivity_timeout(password_manager)
|
||||||
|
elif choice == "12":
|
||||||
password_manager.lock_vault()
|
password_manager.lock_vault()
|
||||||
print(colored("Vault locked. Please re-enter your password.", "yellow"))
|
print(colored("Vault locked. Please re-enter your password.", "yellow"))
|
||||||
password_manager.unlock_vault()
|
password_manager.unlock_vault()
|
||||||
elif choice == "12":
|
|
||||||
handle_display_stats(password_manager)
|
|
||||||
elif choice == "13":
|
elif choice == "13":
|
||||||
handle_toggle_secret_mode(password_manager)
|
handle_display_stats(password_manager)
|
||||||
elif choice == "14":
|
elif choice == "14":
|
||||||
|
handle_toggle_secret_mode(password_manager)
|
||||||
|
elif choice == "15":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print(colored("Invalid choice.", "red"))
|
print(colored("Invalid choice.", "red"))
|
||||||
|
@@ -34,7 +34,13 @@ from utils.key_derivation import (
|
|||||||
derive_index_key,
|
derive_index_key,
|
||||||
EncryptionMode,
|
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 (
|
from utils.password_prompt import (
|
||||||
prompt_for_password,
|
prompt_for_password,
|
||||||
prompt_existing_password,
|
prompt_existing_password,
|
||||||
@@ -89,6 +95,7 @@ class PasswordManager:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the PasswordManager."""
|
"""Initialize the PasswordManager."""
|
||||||
initialize_app()
|
initialize_app()
|
||||||
|
self.ensure_script_checksum()
|
||||||
self.encryption_mode: EncryptionMode = EncryptionMode.SEED_ONLY
|
self.encryption_mode: EncryptionMode = EncryptionMode.SEED_ONLY
|
||||||
self.encryption_manager: Optional[EncryptionManager] = None
|
self.encryption_manager: Optional[EncryptionManager] = None
|
||||||
self.entry_manager: Optional[EntryManager] = None
|
self.entry_manager: Optional[EntryManager] = None
|
||||||
@@ -119,6 +126,23 @@ class PasswordManager:
|
|||||||
# Set the current fingerprint directory
|
# Set the current fingerprint directory
|
||||||
self.fingerprint_dir = self.fingerprint_manager.get_current_fingerprint_dir()
|
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
|
@property
|
||||||
def parent_seed(self) -> Optional[str]:
|
def parent_seed(self) -> Optional[str]:
|
||||||
"""Return the decrypted parent seed if set."""
|
"""Return the decrypted parent seed if set."""
|
||||||
@@ -1473,7 +1497,7 @@ class PasswordManager:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print(
|
print(
|
||||||
colored(
|
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",
|
"yellow",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -1495,6 +1519,26 @@ class PasswordManager:
|
|||||||
logging.error(f"Error during checksum verification: {e}", exc_info=True)
|
logging.error(f"Error during checksum verification: {e}", exc_info=True)
|
||||||
print(colored(f"Error: Failed to verify checksum: {e}", "red"))
|
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]:
|
def get_encrypted_data(self) -> Optional[bytes]:
|
||||||
"""
|
"""
|
||||||
Retrieves the encrypted password index data.
|
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)
|
monkeypatch.setattr("password_manager.manager.verify_checksum", raise_missing)
|
||||||
pm.handle_verify_checksum()
|
pm.handle_verify_checksum()
|
||||||
out = capsys.readouterr().out.lower()
|
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):
|
def test_backup_and_restore_database(monkeypatch, capsys):
|
||||||
|
@@ -93,7 +93,7 @@ def test_settings_menu_additional_backup(monkeypatch):
|
|||||||
tmp_path = Path(tmpdir)
|
tmp_path = Path(tmpdir)
|
||||||
pm, cfg_mgr, fp_mgr = setup_pm(tmp_path, monkeypatch)
|
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("main.handle_set_additional_backup_location") as handler:
|
||||||
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
||||||
main.handle_settings(pm)
|
main.handle_settings(pm)
|
||||||
|
@@ -21,6 +21,8 @@ try:
|
|||||||
verify_checksum,
|
verify_checksum,
|
||||||
json_checksum,
|
json_checksum,
|
||||||
canonical_json_dumps,
|
canonical_json_dumps,
|
||||||
|
initialize_checksum,
|
||||||
|
update_checksum_file,
|
||||||
)
|
)
|
||||||
from .password_prompt import prompt_for_password
|
from .password_prompt import prompt_for_password
|
||||||
from .input_utils import timed_input
|
from .input_utils import timed_input
|
||||||
@@ -45,6 +47,8 @@ __all__ = [
|
|||||||
"verify_checksum",
|
"verify_checksum",
|
||||||
"json_checksum",
|
"json_checksum",
|
||||||
"canonical_json_dumps",
|
"canonical_json_dumps",
|
||||||
|
"initialize_checksum",
|
||||||
|
"update_checksum_file",
|
||||||
"exclusive_lock",
|
"exclusive_lock",
|
||||||
"shared_lock",
|
"shared_lock",
|
||||||
"prompt_for_password",
|
"prompt_for_password",
|
||||||
|
@@ -198,3 +198,29 @@ def initialize_checksum(file_path: str, checksum_file_path: str) -> bool:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return False
|
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