mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||
src/seedpass.egg-info/top_level.txt
|
||||
|
||||
# Allow vendored dependencies to be committed
|
||||
!src/vendor/
|
||||
|
36
README.md
36
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)
|
||||
@@ -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.
|
||||
## 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
|
||||
|
||||
|
38
SeedPass.spec
Normal file
38
SeedPass.spec
Normal 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
12
scripts/vendor_dependencies.sh
Executable 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"
|
@@ -1,10 +1,15 @@
|
||||
# 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
|
||||
import time
|
||||
import argparse
|
||||
import asyncio
|
||||
|
@@ -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")
|
||||
|
@@ -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 = (
|
||||
|
29
src/runtime_requirements.txt
Normal file
29
src/runtime_requirements.txt
Normal 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
|
@@ -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"
|
||||
|
||||
|
||||
|
@@ -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(
|
||||
|
Reference in New Issue
Block a user