Update Readme Files

This commit is contained in:
2025-11-13 23:56:05 +00:00
parent e51c5184e9
commit 5c33d45112
11 changed files with 155 additions and 153 deletions

173
AGENTS.md
View File

@@ -2,7 +2,7 @@
## Project Overview ## Project Overview
This is a Python password hashing utility supporting multiple cryptographic algorithms (PBKDF2, Argon2, bcrypt). The project provides both library functions and two CLI implementations with different UX patterns. This is a Python password hashing utility supporting multiple cryptographic algorithms (PBK-DF2, Argon2, bcrypt). The project provides both library functions and two CLI implementations with different UX patterns.
**Key Architecture:** **Key Architecture:**
- **Algorithm Registry Pattern**: Pluggable hash algorithms implementing a common `Algorithm` protocol - **Algorithm Registry Pattern**: Pluggable hash algorithms implementing a common `Algorithm` protocol
@@ -21,7 +21,9 @@ bcrypt_algorithm.py # bcrypt implementation
tests/ tests/
test_hashing.py # Tests for core hash/verify functions test_hashing.py # Tests for core hash/verify functions
test_cli.py # Tests for salt.py CLI behavior test_cli.py # Tests for salt.py CLI behavior
test_cli2.py # Tests for salt2.py CLI behavior
test_algorithms.py # Tests for algorithm implementations test_algorithms.py # Tests for algorithm implementations
test_integration.py # End-to-end tests for CLI tools
``` ```
## Essential Commands ## Essential Commands
@@ -37,20 +39,22 @@ python3 salt2.py generate "MyPassword" # Alternative CLI
# Hash with specific algorithm # Hash with specific algorithm
python3 salt.py hash --algorithm argon2 "MyPassword" python3 salt.py hash --algorithm argon2 "MyPassword"
python3 salt.py --algorithm bcrypt "MyPassword" # Shortcut with algorithm python3 salt.py --algorithm bcrypt "MyPassword" # Shortcut with algorithm
python3 salt2.py generate "MyPassword" # Note: salt2.py doesn't support --algorithm yet python3 salt2.py generate --algorithm argon2 "MyPassword"
# Verify a password # Verify a password
python3 salt.py verify "MyPassword" <salt_b64> <hash_b64> python3 salt.py verify "MyPassword" <salt_b64> <hash_b64>
python3 salt.py verify --algorithm argon2 "MyPassword" "" <hash_b64> python3 salt.py verify --algorithm argon2 "MyPassword" "" <hash_b64>
python3 salt2.py verify --algorithm bcrypt "MyPassword" "" <hash_b64>
# List available algorithms # List available algorithms
python3 salt.py list-algorithms # Not implemented yet, would be useful addition python3 salt.py list-algorithms
python3 salt2.py list-algorithms
``` ```
### Testing ### Testing
```bash ```bash
# Run all tests (20 tests total as of now) # Run all tests (28 tests total as of now)
python3 -m pytest python3 -m pytest
# Verbose output # Verbose output
@@ -60,6 +64,7 @@ python3 -m pytest -v
python3 -m pytest tests/test_hashing.py python3 -m pytest tests/test_hashing.py
python3 -m pytest tests/test_cli.py python3 -m pytest tests/test_cli.py
python3 -m pytest tests/test_algorithms.py python3 -m pytest tests/test_algorithms.py
python3 -m pytest tests/test_integration.py
# Run specific test patterns # Run specific test patterns
python3 -m pytest -k verify python3 -m pytest -k verify
@@ -155,7 +160,7 @@ python3 salt.py --algorithm argon2 "mypassword"
``` ```
**Normalization rules:** **Normalization rules:**
1. If first arg is `hash` or `verify` → pass through unchanged 1. If first arg is `hash`, `verify`, or `list-algorithms` → pass through unchanged
2. If first arg is `-h` or `--help` → pass through unchanged 2. If first arg is `-h` or `--help` → pass through unchanged
3. If first arg is `--algorithm` or `-a` → prepend `hash` 3. If first arg is `--algorithm` or `-a` → prepend `hash`
4. Otherwise (plain password) → prepend `hash` 4. Otherwise (plain password) → prepend `hash`
@@ -216,7 +221,9 @@ DEFAULT_ITERATIONS = int(os.environ.get("PBKDF2_ITERATIONS", "200000"))
**Test organization:** **Test organization:**
- `test_hashing.py`: Core `hash_password()` and `verify_password()` functions - `test_hashing.py`: Core `hash_password()` and `verify_password()` functions
- `test_cli.py`: CLI behavior for `salt.py` (including shortcut syntax) - `test_cli.py`: CLI behavior for `salt.py` (including shortcut syntax)
- `test_cli2.py`: CLI behavior for `salt2.py`
- `test_algorithms.py`: Individual algorithm implementations - `test_algorithms.py`: Individual algorithm implementations
- `test_integration.py`: End-to-end integration tests for CLI commands
### Testing Patterns ### Testing Patterns
@@ -246,8 +253,8 @@ Exit codes: 0 = success, 1 = verification failure
### Test Execution Notes ### Test Execution Notes
- 20 tests total (as of current state) - 28 tests total (as of current state)
- Test execution time: ~5 seconds (mostly from Argon2/bcrypt hashing) - Test execution time: ~7 seconds (mostly from Argon2/bcrypt hashing)
- All tests must pass before committing - All tests must pass before committing
- Use `pytest -v` for verbose output - Use `pytest -v` for verbose output
- Use `pytest -k <pattern>` to run subset - Use `pytest -k <pattern>` to run subset
@@ -270,7 +277,7 @@ def test_<algo>_algorithm_identifier():
assert algo.identifier == "<algo>" assert algo.identifier == "<algo>"
``` ```
And to `tests/test_cli.py`: And to `tests/test_cli.py` or `tests/test_cli2.py`:
```python ```python
def test_main_hash_with_algorithm_<algo>(): def test_main_hash_with_algorithm_<algo>():
"""Test hash command with --algorithm <algo>.""" """Test hash command with --algorithm <algo>."""
@@ -285,7 +292,7 @@ The imports at the bottom of `algorithms.py` trigger registration. If you import
### 2. CLI Output to Stdout (Tests Must Capture) ### 2. CLI Output to Stdout (Tests Must Capture)
The CLI functions print to stdout. Tests use `main([...])` to check exit codes but don't capture output. If you need to test output content, use `capsys` fixture: The CLI functions print to stdout. Tests use `main([...])` to check exit codes but don't capture output. If you need to test output content, use `capsys` fixture. Subprocess-based integration tests also capture stdout.
```python ```python
def test_output_format(capsys): def test_output_format(capsys):
@@ -315,21 +322,11 @@ DEFAULT_ITERATIONS = int(os.environ.get("PBKDF2_ITERATIONS", "200000"))
CLI output and help text are in German: CLI output and help text are in German:
- `"✓ Passwort korrekt"` / `"✗ Passwort falsch"` - `"✓ Passwort korrekt"` / `"✗ Passwort falsch"`
- Help text uses German descriptions - Help text uses German descriptions
- Comments in code are mixed German/English
- When adding CLI features, maintain German for user-facing text - When adding CLI features, maintain German for user-facing text
### 6. Exception Handling in Verify ### 6. Exception Handling in Verify
`verify_password()` catches exceptions and returns `False` rather than raising: `verify_password()` and algorithm implementations catch exceptions and return `False` rather than raising. This is intentional for security to avoid leaking information via exception types.
```python
try:
salt = base64.b64decode(salt_b64, validate=True)
stored_hash = base64.b64decode(hash_b64, validate=True)
except (binascii.Error, ValueError):
return False
```
This is intentional for security (avoid leaking information via exception types).
### 7. Dynamic Algorithm List ### 7. Dynamic Algorithm List
@@ -346,15 +343,7 @@ hash_parser.add_argument(
This means adding a new algorithm automatically adds it to CLI choices. This means adding a new algorithm automatically adds it to CLI choices.
### 8. salt2.py Missing Algorithm Support ### 8. Relative Imports vs Absolute Imports
**Current limitation:** `salt2.py` does NOT yet support the `--algorithm` flag. It only uses the default (PBKDF2). To add support:
1. Update `_build_parser()` to add `--algorithm` argument to both subparsers
2. Update `_command_generate()` to pass `algorithm=args.algorithm`
3. Update `_command_verify()` to pass `algorithm=args.algorithm`
4. Add tests to `tests/test_cli.py` or create `tests/test_cli2.py`
### 9. Relative Imports vs Absolute Imports
The code uses **absolute imports** (not relative): The code uses **absolute imports** (not relative):
```python ```python
@@ -367,7 +356,6 @@ This works because modules are in the root directory, not a package. Keep this p
### Cryptographic Randomness ### Cryptographic Randomness
- Always use `os.urandom()` for salt generation (never `random` module) - Always use `os.urandom()` for salt generation (never `random` module)
- Never use predictable values or timestamps
### Timing Attack Protection ### Timing Attack Protection
- Use `hmac.compare_digest()` for hash comparison (PBKDF2) - Use `hmac.compare_digest()` for hash comparison (PBKDF2)
@@ -381,12 +369,10 @@ This works because modules are in the root directory, not a package. Keep this p
### Iteration Count ### Iteration Count
- Default 200,000 for PBKDF2 (OWASP recommended as of 2023) - Default 200,000 for PBKDF2 (OWASP recommended as of 2023)
- Configurable via `PBKDF2_ITERATIONS` environment variable - Configurable via `PBKDF2_ITERATIONS` environment variable
- Always parameterize in functions (don't hardcode)
### No Logging of Secrets ### No Logging of Secrets
- Never log passwords, salts, or hashes - Never log passwords, salts, or hashes
- Print statements only in CLI code for user output - Print statements only in CLI code for user output
- Tests don't need to capture sensitive values
## Configuration and Environment ## Configuration and Environment
@@ -394,7 +380,7 @@ This works because modules are in the root directory, not a package. Keep this p
**`PBKDF2_ITERATIONS`** **`PBKDF2_ITERATIONS`**
- Default: `"200000"` (as string, converted to int) - Default: `"200000"` (as string, converted to int)
- Used by: `pbkdf2_algorithm.py` and `salt.py` - Used by: `pbkdf2_algorithm.py`
- Example: `export PBKDF2_ITERATIONS=300000` - Example: `export PBKDF2_ITERATIONS=300000`
### Dependencies (requirements.txt) ### Dependencies (requirements.txt)
@@ -405,149 +391,38 @@ argon2-cffi>=23.1.0 # Argon2id implementation
bcrypt>=4.1.0 # bcrypt implementation bcrypt>=4.1.0 # bcrypt implementation
``` ```
**Standard library only:** `hashlib`, `hmac`, `os`, `base64`, `binascii`, `argparse`, `sys`
### Python Version Requirements ### Python Version Requirements
- **Minimum:** Python 3.11 (for `|` union type syntax) - **Minimum:** Python 3.11 (for `|` union type syntax)
- **Tested on:** Python 3.12.3 - **Tested on:** Python 3.12.3
- **Type hints:** Using modern syntax (`int | None`, not `Optional[int]`)
## Commit Guidelines ## Commit Guidelines
### Commit Message Format ### Commit Message Format
Use conventional commit format with imperative mood: Use conventional commit format with imperative mood:
``` ```
<type>: <short description> <type>: <short description>
<optional longer description>
<optional footer>
```
**Types in use:**
- `feat:` - New features (e.g., "feat: add argon2 algorithm")
- `fix:` - Bug fixes
- `test:` - Test additions/changes
- `refactor:` - Code restructuring without behavior change
- `docs:` - Documentation updates
### Examples from Git History
```
docs: add README.md for repository
docs: translate CLAUDE.md to German
first commit
``` ```
**Types in use:** `feat`, `fix`, `test`, `refactor`, `docs`.
### Before Committing ### Before Committing
1. Run full test suite: `python3 -m pytest -v` 1. Run full test suite: `python3 -m pytest -v`
2. Ensure all tests pass (20/20) 2. Ensure all tests pass (28/28)
3. Verify no regressions in existing functionality 3. Verify no regressions
4. Check that new code follows style conventions 4. Add tests for new functionality
5. Add tests for new functionality
### Pull Request Guidelines
- Keep PRs focused on single feature/fix
- Include motivation in description
- Reference issues with `Fixes #ID`
- For security changes: include CLI output or reproduction
- All tests must pass
- Maintain >90% coverage
## Planned Enhancements and Known Gaps
Based on existing documentation and code:
### Missing Features (mentioned in plans but not implemented)
1. **`list-algorithms` command** - Mentioned in plan (Task 9) but not in current code
2. **salt2.py algorithm support** - `salt2.py` doesn't support `--algorithm` flag yet
3. **Integration tests** - `tests/test_integration.py` mentioned in plan but not created
4. **Backward compatibility test** - Specific test mentioned in Task 12 plan
### Documentation Gaps
1. README.md is in German but doesn't mention multi-algorithm support yet
2. CLAUDE.md is German translation but may be outdated vs English version
3. No API docs for algorithm developers
### Potential Improvements
1. Add `list-algorithms` subcommand to CLIs
2. Complete algorithm support in `salt2.py`
3. Add integration tests with subprocess
4. Document algorithm addition process more clearly
5. Consider packaging as proper Python package (`salt/` directory structure)
## Working with Plans and Documentation ## Working with Plans and Documentation
### Plan Execution Pattern ### Plan Execution Pattern
The repository includes a detailed plan at `docs/plans/2025-11-13-multi-algorithm-support.md`: The repository includes a detailed plan at `docs/plans/2025-11-13-multi-algorithm-support.md`. When implementing from plans, follow the TDD approach outlined in the tasks.
- 12 tasks with step-by-step TDD approach
- Each task has verification steps
- Plan includes commit message examples
- Some tasks completed, others not yet
**If implementing from plans:**
1. Read the full task before starting
2. Follow TDD: failing test first, then implementation
3. Run tests after each step
4. Use suggested commit messages
5. Mark off checklist items as you complete them
### Multiple Documentation Files ### Multiple Documentation Files
- **AGENTS.md**: This file - agent/developer guidelines - **AGENTS.md**: This file - agent/developer guidelines
- **CLAUDE.md**: German translation, more detailed code examples - **CLAUDE.md**: German translation, more detailed code examples
- **README.md**: User-facing documentation in German - **README.md**: User-facing documentation in German
- **docs/plans/*.md**: Implementation plans with TDD steps - **docs/plans/*.md**: Implementation plans with TDD steps
Keep these in sync when making significant changes. Keep these in sync when making significant changes.
## Quick Reference
### File a Bug or Add a Feature
1. Create test demonstrating issue/feature
2. Implement fix/feature
3. Run `python3 -m pytest -v`
4. Commit with conventional message
5. Update docs if needed
### Debug a Test Failure
```bash
# Run specific test with verbose output
python3 -m pytest tests/test_file.py::test_name -vv
# Run with pdb on failure
python3 -m pytest --pdb tests/test_file.py::test_name
# See print statements
python3 -m pytest -s tests/test_file.py
```
### Verify All Systems
```bash
# Comprehensive check
python3 -m pytest -v && \
python3 salt.py "test" && \
python3 salt.py hash --algorithm argon2 "test" && \
python3 salt.py hash --algorithm bcrypt "test" && \
python3 salt2.py generate "test" && \
echo "All systems operational"
```
---
**Document Version:** 2025-11-13
**Last Updated:** Initial comprehensive analysis based on repository state
**Test Count:** 20 tests (all passing)
**Python Version:** 3.12.3

Binary file not shown.

Binary file not shown.

15
salt.py
View File

@@ -78,6 +78,11 @@ def _build_parser() -> argparse.ArgumentParser:
default="pbkdf2", default="pbkdf2",
help="Hash-Algorithmus (Standard: pbkdf2)", help="Hash-Algorithmus (Standard: pbkdf2)",
) )
subparsers.add_parser(
"list-algorithms", help="Zeigt alle verfügbaren Hash-Algorithmen an."
)
return parser return parser
@@ -86,7 +91,7 @@ def _normalize_args(argv: Sequence[str] | None) -> list[str]:
if not argv: if not argv:
return [] return []
# If it starts with a subcommand, leave as-is # If it starts with a subcommand, leave as-is
if argv[0] in {"hash", "verify"}: if argv[0] in {"hash", "verify", "list-algorithms"}:
return list(argv) return list(argv)
# If it starts with help flags, leave as-is # If it starts with help flags, leave as-is
if argv[0] in {"-h", "--help"}: if argv[0] in {"-h", "--help"}:
@@ -104,6 +109,14 @@ def main(argv: Sequence[str] | None = None) -> int:
arg_list = list(argv) if argv is not None else sys.argv[1:] arg_list = list(argv) if argv is not None else sys.argv[1:]
args = parser.parse_args(_normalize_args(arg_list)) args = parser.parse_args(_normalize_args(arg_list))
if args.command == "list-algorithms":
from algorithms import list_algorithms
print("Verfügbare Algorithmen:")
for algo in list_algorithms():
print(f" - {algo}")
return 0
if args.command == "verify": if args.command == "verify":
if verify_password(args.password, args.salt, args.hash, algorithm=args.algorithm): if verify_password(args.password, args.salt, args.hash, algorithm=args.algorithm):
print("✓ Passwort korrekt") print("✓ Passwort korrekt")

View File

@@ -10,6 +10,8 @@ from salt import hash_password, verify_password
def _build_parser() -> argparse.ArgumentParser: def _build_parser() -> argparse.ArgumentParser:
from algorithms import list_algorithms
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="CLI für Password-Hashing (PBKDF2, Argon2, bcrypt) inklusive Verify-Modus." description="CLI für Password-Hashing (PBKDF2, Argon2, bcrypt) inklusive Verify-Modus."
) )
@@ -19,6 +21,13 @@ def _build_parser() -> argparse.ArgumentParser:
"generate", help="Erzeugt ein neues Salt+Hash Paar." "generate", help="Erzeugt ein neues Salt+Hash Paar."
) )
generate_parser.add_argument("password", help="Klartext-Passwort zum Hashen.") generate_parser.add_argument("password", help="Klartext-Passwort zum Hashen.")
generate_parser.add_argument(
"--algorithm",
"-a",
choices=list_algorithms(),
default="pbkdf2",
help="Hash-Algorithmus (Standard: pbkdf2)",
)
verify_parser = subparsers.add_parser( verify_parser = subparsers.add_parser(
"verify", "verify",
@@ -27,29 +36,52 @@ def _build_parser() -> argparse.ArgumentParser:
verify_parser.add_argument("password", help="Passwort zum Prüfen.") verify_parser.add_argument("password", help="Passwort zum Prüfen.")
verify_parser.add_argument("salt", help="Base64-kodiertes Salt.") verify_parser.add_argument("salt", help="Base64-kodiertes Salt.")
verify_parser.add_argument("hash", help="Base64-kodierter Hash.") verify_parser.add_argument("hash", help="Base64-kodierter Hash.")
verify_parser.add_argument(
"--algorithm",
"-a",
choices=list_algorithms(),
default="pbkdf2",
help="Hash-Algorithmus (Standard: pbkdf2)",
)
subparsers.add_parser(
"list-algorithms", help="Zeigt alle verfügbaren Hash-Algorithmen an."
)
return parser return parser
def _command_generate(args: argparse.Namespace) -> int: def _command_generate(args: argparse.Namespace) -> int:
salt, hash_value = hash_password(args.password) salt, hash_value = hash_password(args.password, algorithm=args.algorithm)
print(f"Salt: {salt}") print(f"Salt: {salt}")
print(f"Hash: {hash_value}") print(f"Hash: {hash_value}")
return 0 return 0
def _command_verify(args: argparse.Namespace) -> int: def _command_verify(args: argparse.Namespace) -> int:
if verify_password(args.password, args.salt, args.hash): if verify_password(args.password, args.salt, args.hash, algorithm=args.algorithm):
print("✓ Passwort korrekt") print("✓ Passwort korrekt")
return 0 return 0
print("✗ Passwort falsch") print("✗ Passwort falsch")
return 1 return 1
def _command_list_algorithms() -> int:
from algorithms import list_algorithms
print("Verfügbare Algorithmen:")
for algo in list_algorithms():
print(f" - {algo}")
return 0
def main(argv: Sequence[str] | None = None) -> int: def main(argv: Sequence[str] | None = None) -> int:
parser = _build_parser() parser = _build_parser()
args = parser.parse_args(argv) args = parser.parse_args(argv)
if args.command == "generate": if args.command == "generate":
return _command_generate(args) return _command_generate(args)
if args.command == "list-algorithms":
return _command_list_algorithms()
return _command_verify(args) return _command_verify(args)

18
tests/test_cli2.py Normal file
View File

@@ -0,0 +1,18 @@
from salt import hash_password
from salt2 import main
def test_main_generate_with_algorithm_flag():
"""Verify salt2 CLI accepts --algorithm flag."""
assert main(["generate", "--algorithm", "pbkdf2", "secret"]) == 0
def test_main_verify_with_algorithm_flag():
"""Verify salt2 CLI verify accepts --algorithm flag."""
salt, hashed = hash_password("secret", algorithm="pbkdf2")
assert main(["verify", "--algorithm", "pbkdf2", "secret", salt, hashed]) == 0
def test_main_list_algorithms_command():
"""Verify salt2 CLI has list-algorithms command."""
assert main(["list-algorithms"]) == 0

View File

@@ -24,3 +24,16 @@ def test_hash_password_with_algorithm_parameter():
"""Verify hash_password accepts algorithm parameter.""" """Verify hash_password accepts algorithm parameter."""
salt, hashed = hash_password("test", algorithm="pbkdf2") salt, hashed = hash_password("test", algorithm="pbkdf2")
assert verify_password("test", salt, hashed, algorithm="pbkdf2") assert verify_password("test", salt, hashed, algorithm="pbkdf2")
def test_backward_compatibility_with_old_pbkdf2_hashes():
"""Verify existing PBKDF2 hashes still work without algorithm parameter."""
# Simulate old hash created before algorithm parameter existed
salt, hashed = hash_password("legacy-password")
# Verify using old API (no algorithm parameter)
assert verify_password("legacy-password", salt, hashed)
assert not verify_password("wrong", salt, hashed)
# Verify using new API with explicit pbkdf2
assert verify_password("legacy-password", salt, hashed, algorithm="pbkdf2")

51
tests/test_integration.py Normal file
View File

@@ -0,0 +1,51 @@
"""Integration tests for multi-algorithm password hashing."""
import subprocess
import sys
def test_pbkdf2_cli_integration():
"""Test PBKDF2 end-to-end via CLI."""
result = subprocess.run(
[sys.executable, "salt.py", "hash", "--algorithm", "pbkdf2", "test123"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "Salt:" in result.stdout
assert "Hash:" in result.stdout
def test_argon2_cli_integration():
"""Test Argon2 end-to-end via CLI."""
result = subprocess.run(
[sys.executable, "salt.py", "hash", "--algorithm", "argon2", "test123"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "Hash:" in result.stdout
def test_bcrypt_cli_integration():
"""Test bcrypt end-to-end via CLI."""
result = subprocess.run(
[sys.executable, "salt.py", "hash", "--algorithm", "bcrypt", "test123"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "Hash:" in result.stdout
def test_list_algorithms_integration():
"""Test list-algorithms command."""
result = subprocess.run(
[sys.executable, "salt.py", "list-algorithms"],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert "argon2" in result.stdout
assert "bcrypt" in result.stdout
assert "pbkdf2" in result.stdout