mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||||
|
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)
|
- [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
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
|
# 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
|
||||||
|
@@ -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")
|
||||||
|
@@ -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 = (
|
||||||
|
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):
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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(
|
||||||
|
Reference in New Issue
Block a user