Merge pull request #602 from PR0M3TH3AN/beta

Beta
This commit is contained in:
thePR0M3TH3AN
2025-07-17 10:38:45 -04:00
committed by GitHub
10 changed files with 137 additions and 18 deletions

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ src/seedpass.egg-info/SOURCES.txt
src/seedpass.egg-info/dependency_links.txt src/seedpass.egg-info/dependency_links.txt
src/seedpass.egg-info/entry_points.txt src/seedpass.egg-info/entry_points.txt
src/seedpass.egg-info/top_level.txt src/seedpass.egg-info/top_level.txt
# Allow vendored dependencies to be committed
!src/vendor/

View File

@@ -31,6 +31,7 @@ SeedPass now uses the `portalocker` library for cross-platform file locking. No
- [Running the Application](#running-the-application) - [Running the Application](#running-the-application)
- [Managing Multiple Seeds](#managing-multiple-seeds) - [Managing Multiple Seeds](#managing-multiple-seeds)
- [Additional Entry Types](#additional-entry-types) - [Additional Entry Types](#additional-entry-types)
- [Building a standalone executable](#building-a-standalone-executable)
- [Security Considerations](#security-considerations) - [Security Considerations](#security-considerations)
- [Contributing](#contributing) - [Contributing](#contributing)
- [License](#license) - [License](#license)
@@ -502,6 +503,41 @@ 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. 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
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 ## Security Considerations

38
SeedPass.spec Normal file
View File

@@ -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,
)

12
scripts/vendor_dependencies.sh Executable file
View File

@@ -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"

View File

@@ -1,10 +1,15 @@
# main.py # main.py
import os
from pathlib import Path from pathlib import Path
import sys 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 logging
import signal import signal
import getpass
import time import time
import argparse import argparse
import asyncio import asyncio

View File

@@ -6,7 +6,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
import getpass from utils.seed_prompt import masked_input
import bcrypt import bcrypt
@@ -93,7 +93,7 @@ class ConfigManager:
self.save_config(data) self.save_config(data)
if require_pin and data.get("pin_hash"): if require_pin and data.get("pin_hash"):
for _ in range(3): 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()): if bcrypt.checkpw(pin.encode(), data["pin_hash"].encode()):
break break
print("Invalid PIN") print("Invalid PIN")

View File

@@ -12,7 +12,6 @@ with the password manager functionalities.
import sys import sys
import json import json
import logging import logging
import getpass
import os import os
import hashlib import hashlib
from typing import Optional, Literal from typing import Optional, Literal
@@ -668,8 +667,8 @@ class PasswordManager:
Prompts the user for the master password to decrypt the seed. Prompts the user for the master password to decrypt the seed.
""" """
try: try:
# Prompt for password # Prompt for password using masked input
password = getpass.getpass(prompt="Enter your login password: ").strip() password = prompt_existing_password("Enter your login password: ")
# Derive encryption key from password # Derive encryption key from password
iterations = ( iterations = (

View File

@@ -0,0 +1,29 @@
# 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
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

View File

@@ -9,16 +9,14 @@ from utils import password_prompt
def test_prompt_new_password(monkeypatch): def test_prompt_new_password(monkeypatch):
responses = cycle(["goodpass", "goodpass"]) responses = cycle(["goodpass", "goodpass"])
monkeypatch.setattr( monkeypatch.setattr(password_prompt, "masked_input", lambda prompt: next(responses))
password_prompt.getpass, "getpass", lambda prompt: next(responses)
)
result = password_prompt.prompt_new_password() result = password_prompt.prompt_new_password()
assert result == "goodpass" assert result == "goodpass"
def test_prompt_new_password_retry(monkeypatch, caplog): def test_prompt_new_password_retry(monkeypatch, caplog):
seq = iter(["pass1", "pass2", "passgood", "passgood"]) 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) caplog.set_level(logging.WARNING)
result = password_prompt.prompt_new_password() result = password_prompt.prompt_new_password()
assert "User entered a password shorter" in caplog.text 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): 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" assert password_prompt.prompt_existing_password() == "mypassword"

View File

@@ -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. Ensure that all dependencies are installed and properly configured in your environment.
""" """
import getpass from utils.seed_prompt import masked_input
import logging import logging
import sys import sys
import unicodedata import unicodedata
import traceback
from termcolor import colored from termcolor import colored
from colorama import init as colorama_init from colorama import init as colorama_init
@@ -53,8 +52,8 @@ def prompt_new_password() -> str:
while attempts < max_retries: while attempts < max_retries:
try: try:
password = getpass.getpass(prompt="Enter a new password: ").strip() password = masked_input("Enter a new password: ").strip()
confirm_password = getpass.getpass(prompt="Confirm your password: ").strip() confirm_password = masked_input("Confirm your password: ").strip()
if not password: if not password:
print( print(
@@ -128,7 +127,7 @@ def prompt_existing_password(
attempts = 0 attempts = 0
while attempts < max_retries: while attempts < max_retries:
try: try:
password = getpass.getpass(prompt=prompt_message).strip() password = masked_input(prompt_message).strip()
if not password: if not password:
print( print(