diff --git a/AGENTS.md b/AGENTS.md index b905553..8a9cb40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## 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:** - **Algorithm Registry Pattern**: Pluggable hash algorithms implementing a common `Algorithm` protocol @@ -21,7 +21,9 @@ bcrypt_algorithm.py # bcrypt implementation tests/ test_hashing.py # Tests for core hash/verify functions 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_integration.py # End-to-end tests for CLI tools ``` ## Essential Commands @@ -36,21 +38,23 @@ python3 salt2.py generate "MyPassword" # Alternative CLI # Hash with specific algorithm python3 salt.py hash --algorithm argon2 "MyPassword" -python3 salt.py --algorithm bcrypt "MyPassword" # Shortcut with algorithm -python3 salt2.py generate "MyPassword" # Note: salt2.py doesn't support --algorithm yet +python3 salt.py --algorithm bcrypt "MyPassword" # Shortcut with algorithm +python3 salt2.py generate --algorithm argon2 "MyPassword" # Verify a password python3 salt.py verify "MyPassword" python3 salt.py verify --algorithm argon2 "MyPassword" "" +python3 salt2.py verify --algorithm bcrypt "MyPassword" "" # 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 ```bash -# Run all tests (20 tests total as of now) +# Run all tests (28 tests total as of now) python3 -m pytest # Verbose output @@ -60,6 +64,7 @@ python3 -m pytest -v python3 -m pytest tests/test_hashing.py python3 -m pytest tests/test_cli.py python3 -m pytest tests/test_algorithms.py +python3 -m pytest tests/test_integration.py # Run specific test patterns python3 -m pytest -k verify @@ -155,7 +160,7 @@ python3 salt.py --algorithm argon2 "mypassword" ``` **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 3. If first arg is `--algorithm` or `-a` → prepend `hash` 4. Otherwise (plain password) → prepend `hash` @@ -216,7 +221,9 @@ DEFAULT_ITERATIONS = int(os.environ.get("PBKDF2_ITERATIONS", "200000")) **Test organization:** - `test_hashing.py`: Core `hash_password()` and `verify_password()` functions - `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_integration.py`: End-to-end integration tests for CLI commands ### Testing Patterns @@ -246,8 +253,8 @@ Exit codes: 0 = success, 1 = verification failure ### Test Execution Notes -- 20 tests total (as of current state) -- Test execution time: ~5 seconds (mostly from Argon2/bcrypt hashing) +- 28 tests total (as of current state) +- Test execution time: ~7 seconds (mostly from Argon2/bcrypt hashing) - All tests must pass before committing - Use `pytest -v` for verbose output - Use `pytest -k ` to run subset @@ -270,7 +277,7 @@ def test__algorithm_identifier(): assert algo.identifier == "" ``` -And to `tests/test_cli.py`: +And to `tests/test_cli.py` or `tests/test_cli2.py`: ```python def test_main_hash_with_algorithm_(): """Test hash command with --algorithm .""" @@ -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) -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 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: - `"✓ Passwort korrekt"` / `"✗ Passwort falsch"` - Help text uses German descriptions -- Comments in code are mixed German/English - When adding CLI features, maintain German for user-facing text ### 6. Exception Handling in Verify -`verify_password()` catches exceptions and returns `False` rather than raising: -```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). +`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. ### 7. Dynamic Algorithm List @@ -346,15 +343,7 @@ hash_parser.add_argument( This means adding a new algorithm automatically adds it to CLI choices. -### 8. salt2.py Missing Algorithm Support - -**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 +### 8. Relative Imports vs Absolute Imports The code uses **absolute imports** (not relative): ```python @@ -367,7 +356,6 @@ This works because modules are in the root directory, not a package. Keep this p ### Cryptographic Randomness - Always use `os.urandom()` for salt generation (never `random` module) -- Never use predictable values or timestamps ### Timing Attack Protection - 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 - Default 200,000 for PBKDF2 (OWASP recommended as of 2023) - Configurable via `PBKDF2_ITERATIONS` environment variable -- Always parameterize in functions (don't hardcode) ### No Logging of Secrets - Never log passwords, salts, or hashes - Print statements only in CLI code for user output -- Tests don't need to capture sensitive values ## Configuration and Environment @@ -394,7 +380,7 @@ This works because modules are in the root directory, not a package. Keep this p **`PBKDF2_ITERATIONS`** - 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` ### Dependencies (requirements.txt) @@ -405,149 +391,38 @@ argon2-cffi>=23.1.0 # Argon2id implementation bcrypt>=4.1.0 # bcrypt implementation ``` -**Standard library only:** `hashlib`, `hmac`, `os`, `base64`, `binascii`, `argparse`, `sys` - ### Python Version Requirements - **Minimum:** Python 3.11 (for `|` union type syntax) - **Tested on:** Python 3.12.3 -- **Type hints:** Using modern syntax (`int | None`, not `Optional[int]`) ## Commit Guidelines ### Commit Message Format Use conventional commit format with imperative mood: - ``` : - - - - -``` - -**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 1. Run full test suite: `python3 -m pytest -v` -2. Ensure all tests pass (20/20) -3. Verify no regressions in existing functionality -4. Check that new code follows style conventions -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) +2. Ensure all tests pass (28/28) +3. Verify no regressions +4. Add tests for new functionality ## Working with Plans and Documentation ### Plan Execution Pattern -The repository includes a detailed plan at `docs/plans/2025-11-13-multi-algorithm-support.md`: -- 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 +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. ### Multiple Documentation Files - - **AGENTS.md**: This file - agent/developer guidelines - **CLAUDE.md**: German translation, more detailed code examples - **README.md**: User-facing documentation in German - **docs/plans/*.md**: Implementation plans with TDD steps 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 diff --git a/__pycache__/salt.cpython-312.pyc b/__pycache__/salt.cpython-312.pyc index 8132b80..5033081 100644 Binary files a/__pycache__/salt.cpython-312.pyc and b/__pycache__/salt.cpython-312.pyc differ diff --git a/__pycache__/salt2.cpython-312.pyc b/__pycache__/salt2.cpython-312.pyc new file mode 100644 index 0000000..5ecbc1a Binary files /dev/null and b/__pycache__/salt2.cpython-312.pyc differ diff --git a/salt.py b/salt.py index 87071b9..a9f31fd 100644 --- a/salt.py +++ b/salt.py @@ -78,6 +78,11 @@ def _build_parser() -> argparse.ArgumentParser: default="pbkdf2", help="Hash-Algorithmus (Standard: pbkdf2)", ) + + subparsers.add_parser( + "list-algorithms", help="Zeigt alle verfügbaren Hash-Algorithmen an." + ) + return parser @@ -86,7 +91,7 @@ def _normalize_args(argv: Sequence[str] | None) -> list[str]: if not argv: return [] # 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) # If it starts with help flags, leave as-is 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:] 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 verify_password(args.password, args.salt, args.hash, algorithm=args.algorithm): print("✓ Passwort korrekt") diff --git a/salt2.py b/salt2.py index 2d19fe7..a49e628 100644 --- a/salt2.py +++ b/salt2.py @@ -10,6 +10,8 @@ from salt import hash_password, verify_password def _build_parser() -> argparse.ArgumentParser: + from algorithms import list_algorithms + parser = argparse.ArgumentParser( 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_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", @@ -27,29 +36,52 @@ def _build_parser() -> argparse.ArgumentParser: verify_parser.add_argument("password", help="Passwort zum Prüfen.") verify_parser.add_argument("salt", help="Base64-kodiertes Salt.") 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 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"Hash: {hash_value}") return 0 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") return 0 print("✗ Passwort falsch") 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: parser = _build_parser() args = parser.parse_args(argv) if args.command == "generate": return _command_generate(args) + if args.command == "list-algorithms": + return _command_list_algorithms() return _command_verify(args) diff --git a/tests/__pycache__/test_cli2.cpython-312-pytest-9.0.1.pyc b/tests/__pycache__/test_cli2.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000..fc8109b Binary files /dev/null and b/tests/__pycache__/test_cli2.cpython-312-pytest-9.0.1.pyc differ diff --git a/tests/__pycache__/test_hashing.cpython-312-pytest-9.0.1.pyc b/tests/__pycache__/test_hashing.cpython-312-pytest-9.0.1.pyc index 3c68c5b..7c6d815 100644 Binary files a/tests/__pycache__/test_hashing.cpython-312-pytest-9.0.1.pyc and b/tests/__pycache__/test_hashing.cpython-312-pytest-9.0.1.pyc differ diff --git a/tests/__pycache__/test_integration.cpython-312-pytest-9.0.1.pyc b/tests/__pycache__/test_integration.cpython-312-pytest-9.0.1.pyc new file mode 100644 index 0000000..c64752c Binary files /dev/null and b/tests/__pycache__/test_integration.cpython-312-pytest-9.0.1.pyc differ diff --git a/tests/test_cli2.py b/tests/test_cli2.py new file mode 100644 index 0000000..9b6c5ca --- /dev/null +++ b/tests/test_cli2.py @@ -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 diff --git a/tests/test_hashing.py b/tests/test_hashing.py index 27f3bb2..183cf3a 100644 --- a/tests/test_hashing.py +++ b/tests/test_hashing.py @@ -24,3 +24,16 @@ def test_hash_password_with_algorithm_parameter(): """Verify hash_password accepts algorithm parameter.""" salt, hashed = hash_password("test", 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") diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..ba84879 --- /dev/null +++ b/tests/test_integration.py @@ -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