From 97bdd2483d788303e2b21d08ffea2999acc11fb1 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 16 Jul 2025 22:09:58 -0400 Subject: [PATCH 1/7] Add runtime requirements file --- src/runtime_requirements.txt | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/runtime_requirements.txt diff --git a/src/runtime_requirements.txt b/src/runtime_requirements.txt new file mode 100644 index 0000000..8c7adb1 --- /dev/null +++ b/src/runtime_requirements.txt @@ -0,0 +1,28 @@ +# Runtime dependencies for vendoring/packaging only (tests excluded) +colorama>=0.4.6 +termcolor>=1.1.0 +cryptography>=40.0.2 +bip-utils>=2.5.0 +bech32==1.2.0 +coincurve>=18.0.0 +mnemonic +aiohttp>=3.12.14 +bcrypt +portalocker>=2.8 +nostr-sdk>=0.42.1 +websocket-client==1.7.0 + +websockets>=15.0.0 +tomli +pgpy==0.6.0 +pyotp>=2.8.0 +pyperclip +qrcode>=8.2 +typer>=0.12.3 +fastapi>=0.116.0 +uvicorn>=0.35.0 +httpx>=0.28.1 +requests>=2.32 +python-multipart +orjson +argon2-cffi From 182085b6396fa4feb7be755fafae2f6349ebcd87 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 08:33:23 -0400 Subject: [PATCH 2/7] Clarify runtime requirements comment --- src/runtime_requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime_requirements.txt b/src/runtime_requirements.txt index 8c7adb1..38cf46e 100644 --- a/src/runtime_requirements.txt +++ b/src/runtime_requirements.txt @@ -1,4 +1,5 @@ -# Runtime dependencies for vendoring/packaging only (tests excluded) +# Runtime dependencies for vendoring/packaging only +# Generated from requirements.txt with all test-only packages removed colorama>=0.4.6 termcolor>=1.1.0 cryptography>=40.0.2 From f7eaf2897fc64285818b23cfb8cba0f1ddba3dc1 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 08:39:22 -0400 Subject: [PATCH 3/7] Add vendor dependencies script --- .gitignore | 5 ++++- scripts/vendor_dependencies.sh | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100755 scripts/vendor_dependencies.sh diff --git a/.gitignore b/.gitignore index 5e92e6f..4946766 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ src/seedpass.egg-info/PKG-INFO src/seedpass.egg-info/SOURCES.txt src/seedpass.egg-info/dependency_links.txt src/seedpass.egg-info/entry_points.txt -src/seedpass.egg-info/top_level.txt \ No newline at end of file +src/seedpass.egg-info/top_level.txt + +# Allow vendored dependencies to be committed +!src/vendor/ diff --git a/scripts/vendor_dependencies.sh b/scripts/vendor_dependencies.sh new file mode 100755 index 0000000..96191e6 --- /dev/null +++ b/scripts/vendor_dependencies.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +VENDOR_DIR="src/vendor" + +# Clean vendor directory +rm -rf "$VENDOR_DIR" +mkdir -p "$VENDOR_DIR" + +pip download --no-binary :all: -r src/runtime_requirements.txt -d "$VENDOR_DIR" + +echo "Vendored dependencies installed in $VENDOR_DIR" From 5826e18189ebd2e249715bad155c380a1e1e2502 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 08:48:07 -0400 Subject: [PATCH 4/7] Add vendor directory to sys.path --- src/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 565ade0..9119d5a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,13 @@ # main.py -import os from pathlib import Path import sys + +# Add bundled vendor directory to sys.path so bundled dependencies can be imported +vendor_dir = Path(__file__).parent / "vendor" +if vendor_dir.exists(): + sys.path.insert(0, str(vendor_dir)) + +import os import logging import signal import getpass From 87215a10cc56a1de0b2048a4e9b6c5b154db42ad Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:06:54 -0400 Subject: [PATCH 5/7] Add PyInstaller spec and documentation --- README.md | 17 +++++++++++++++++ SeedPass.spec | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 SeedPass.spec diff --git a/README.md b/README.md index 0f63233..6c522c7 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No - [Running the Application](#running-the-application) - [Managing Multiple Seeds](#managing-multiple-seeds) - [Additional Entry Types](#additional-entry-types) +- [Building a standalone executable](#building-a-standalone-executable) - [Security Considerations](#security-considerations) - [Contributing](#contributing) - [License](#license) @@ -503,6 +504,22 @@ python -m mutmut results Mutation testing is disabled in the GitHub workflow due to reliability issues and should be run on a desktop environment instead. +## Building a standalone executable + +1. Run the vendoring script to bundle runtime dependencies: + +```bash +scripts/vendor_dependencies.sh +``` + +2. Build the binary with PyInstaller: + +```bash +pyinstaller SeedPass.spec +``` + +The standalone executable will appear in the `dist/` directory. This process works on Windows, macOS and Linux but you must build on each platform for a native binary. + ## Security Considerations **Important:** The password you use to encrypt your parent seed is also required to decrypt the seed index data retrieved from Nostr. **It is imperative to remember this password** and be sure to use it with the same seed, as losing it means you won't be able to access your stored index. Secure your 12-word seed **and** your master password. diff --git a/SeedPass.spec b/SeedPass.spec new file mode 100644 index 0000000..8d69efa --- /dev/null +++ b/SeedPass.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['src/main.py'], + pathex=['src', 'src/vendor'], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='SeedPass', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) From d415eca8bda19aa4f69ad901f003ea5962ed71c6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:13:07 -0400 Subject: [PATCH 6/7] docs: outline development workflow --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 6c522c7..7b39a4d 100644 --- a/README.md +++ b/README.md @@ -503,6 +503,25 @@ python -m mutmut results ``` Mutation testing is disabled in the GitHub workflow due to reliability issues and should be run on a desktop environment instead. +## Development Workflow + +1. Install all development dependencies: +```bash +pip install -r src/requirements.txt +``` + +2. When `src/runtime_requirements.txt` changes, rerun: +```bash +scripts/vendor_dependencies.sh +``` +Commit the updated `src/vendor/` directory. The application automatically adds this folder to `sys.path` so the bundled packages are found. + +3. Before committing, format and test the code: +```bash +black . +pytest +``` + ## Building a standalone executable From 764631b8ba9e5d075b54df34e98d86fc335d37c5 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 17 Jul 2025 10:04:06 -0400 Subject: [PATCH 7/7] Use masked input for all sensitive prompts --- src/main.py | 1 - src/password_manager/config_manager.py | 4 ++-- src/password_manager/manager.py | 5 ++--- src/tests/test_password_prompt.py | 8 +++----- src/utils/password_prompt.py | 9 ++++----- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main.py b/src/main.py index 9119d5a..6219ca5 100644 --- a/src/main.py +++ b/src/main.py @@ -10,7 +10,6 @@ if vendor_dir.exists(): import os import logging import signal -import getpass import time import argparse import asyncio diff --git a/src/password_manager/config_manager.py b/src/password_manager/config_manager.py index 269a10a..eb0e0cf 100644 --- a/src/password_manager/config_manager.py +++ b/src/password_manager/config_manager.py @@ -6,7 +6,7 @@ import logging from pathlib import Path from typing import List, Optional -import getpass +from utils.seed_prompt import masked_input import bcrypt @@ -93,7 +93,7 @@ class ConfigManager: self.save_config(data) if require_pin and data.get("pin_hash"): for _ in range(3): - pin = getpass.getpass("Enter settings PIN: ").strip() + pin = masked_input("Enter settings PIN: ").strip() if bcrypt.checkpw(pin.encode(), data["pin_hash"].encode()): break print("Invalid PIN") diff --git a/src/password_manager/manager.py b/src/password_manager/manager.py index b8ebd09..8434808 100644 --- a/src/password_manager/manager.py +++ b/src/password_manager/manager.py @@ -12,7 +12,6 @@ with the password manager functionalities. import sys import json import logging -import getpass import os import hashlib from typing import Optional, Literal @@ -668,8 +667,8 @@ class PasswordManager: Prompts the user for the master password to decrypt the seed. """ try: - # Prompt for password - password = getpass.getpass(prompt="Enter your login password: ").strip() + # Prompt for password using masked input + password = prompt_existing_password("Enter your login password: ") # Derive encryption key from password iterations = ( diff --git a/src/tests/test_password_prompt.py b/src/tests/test_password_prompt.py index e9d04d4..54eda13 100644 --- a/src/tests/test_password_prompt.py +++ b/src/tests/test_password_prompt.py @@ -9,16 +9,14 @@ from utils import password_prompt def test_prompt_new_password(monkeypatch): responses = cycle(["goodpass", "goodpass"]) - monkeypatch.setattr( - password_prompt.getpass, "getpass", lambda prompt: next(responses) - ) + monkeypatch.setattr(password_prompt, "masked_input", lambda prompt: next(responses)) result = password_prompt.prompt_new_password() assert result == "goodpass" def test_prompt_new_password_retry(monkeypatch, caplog): seq = iter(["pass1", "pass2", "passgood", "passgood"]) - monkeypatch.setattr(password_prompt.getpass, "getpass", lambda prompt: next(seq)) + monkeypatch.setattr(password_prompt, "masked_input", lambda prompt: next(seq)) caplog.set_level(logging.WARNING) result = password_prompt.prompt_new_password() assert "User entered a password shorter" in caplog.text @@ -26,7 +24,7 @@ def test_prompt_new_password_retry(monkeypatch, caplog): def test_prompt_existing_password(monkeypatch): - monkeypatch.setattr(password_prompt.getpass, "getpass", lambda prompt: "mypassword") + monkeypatch.setattr(password_prompt, "masked_input", lambda prompt: "mypassword") assert password_prompt.prompt_existing_password() == "mypassword" diff --git a/src/utils/password_prompt.py b/src/utils/password_prompt.py index 065ea0a..498e6b5 100644 --- a/src/utils/password_prompt.py +++ b/src/utils/password_prompt.py @@ -11,11 +11,10 @@ this module enhances code reuse, security, and maintainability across the applic Ensure that all dependencies are installed and properly configured in your environment. """ -import getpass +from utils.seed_prompt import masked_input import logging import sys import unicodedata -import traceback from termcolor import colored from colorama import init as colorama_init @@ -53,8 +52,8 @@ def prompt_new_password() -> str: while attempts < max_retries: try: - password = getpass.getpass(prompt="Enter a new password: ").strip() - confirm_password = getpass.getpass(prompt="Confirm your password: ").strip() + password = masked_input("Enter a new password: ").strip() + confirm_password = masked_input("Confirm your password: ").strip() if not password: print( @@ -128,7 +127,7 @@ def prompt_existing_password( attempts = 0 while attempts < max_retries: try: - password = getpass.getpass(prompt=prompt_message).strip() + password = masked_input(prompt_message).strip() if not password: print(