# Multi-Algorithm Password Hashing Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Extend the password hashing utility to support multiple algorithms (PBKDF2, Argon2, bcrypt) with backward compatibility. **Architecture:** Create an algorithm registry pattern with pluggable hashers. Each algorithm implements a common interface. The CLI and core functions accept an algorithm parameter. Hash outputs include algorithm identifiers for verification. Maintain 100% backward compatibility with existing PBKDF2 hashes. **Tech Stack:** Python 3.11+, hashlib (PBKDF2), argon2-cffi (Argon2), bcrypt (bcrypt), pytest --- ## Task 1: Add Dependencies **Files:** - Modify: `requirements.txt` **Step 1: Add algorithm dependencies to requirements.txt** Add the following lines to `requirements.txt`: ``` argon2-cffi>=23.1.0 bcrypt>=4.1.0 ``` **Step 2: Install dependencies** Run: `pip install -r requirements.txt` Expected: Successfully installs argon2-cffi and bcrypt packages **Step 3: Commit dependency changes** ```bash git add requirements.txt git commit -m "feat: add argon2-cffi and bcrypt dependencies" ``` --- ## Task 2: Create Algorithm Interface **Files:** - Create: `salt/algorithms.py` - Create: `tests/test_algorithms.py` **Step 1: Write failing test for algorithm interface** Create `tests/test_algorithms.py`: ```python import pytest from salt.algorithms import Algorithm, get_algorithm def test_algorithm_has_required_methods(): """Verify Algorithm protocol defines required methods.""" algo = get_algorithm("pbkdf2") assert hasattr(algo, "hash") assert hasattr(algo, "verify") assert hasattr(algo, "identifier") def test_get_algorithm_returns_pbkdf2(): """Verify default algorithm is PBKDF2.""" algo = get_algorithm("pbkdf2") assert algo.identifier == "pbkdf2" def test_get_algorithm_unknown_raises_error(): """Verify unknown algorithm raises ValueError.""" with pytest.raises(ValueError, match="Unknown algorithm"): get_algorithm("unknown") ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_algorithms.py -v` Expected: FAIL with "ModuleNotFoundError: No module named 'salt.algorithms'" **Step 3: Create algorithm interface** Create `salt/algorithms.py`: ```python """Algorithm interface and registry for password hashing.""" from __future__ import annotations from typing import Protocol class Algorithm(Protocol): """Protocol defining the interface for password hashing algorithms.""" identifier: str def hash(self, password: str, **kwargs) -> tuple[str, str]: """Hash a password and return (salt_b64, hash_b64).""" ... def verify(self, password: str, salt_b64: str, hash_b64: str, **kwargs) -> bool: """Verify a password against stored salt and hash.""" ... # Algorithm registry _ALGORITHMS: dict[str, Algorithm] = {} def register_algorithm(algo: Algorithm) -> None: """Register an algorithm in the global registry.""" _ALGORITHMS[algo.identifier] = algo def get_algorithm(name: str) -> Algorithm: """Get an algorithm by name from the registry.""" if name not in _ALGORITHMS: raise ValueError(f"Unknown algorithm: {name}") return _ALGORITHMS[name] def list_algorithms() -> list[str]: """Return list of registered algorithm identifiers.""" return sorted(_ALGORITHMS.keys()) ``` **Step 4: Run test to verify it passes** Run: `pytest tests/test_algorithms.py::test_get_algorithm_unknown_raises_error -v` Expected: PASS **Step 5: Commit algorithm interface** ```bash git add salt/algorithms.py tests/test_algorithms.py git commit -m "feat: add algorithm interface and registry" ``` --- ## Task 3: Implement PBKDF2 Algorithm **Files:** - Create: `salt/pbkdf2_algorithm.py` - Modify: `tests/test_algorithms.py` **Step 1: Write failing test for PBKDF2 algorithm** Add to `tests/test_algorithms.py`: ```python def test_pbkdf2_algorithm_hash_round_trip(): """Verify PBKDF2 algorithm can hash and verify passwords.""" algo = get_algorithm("pbkdf2") salt, hashed = algo.hash("test password") assert algo.verify("test password", salt, hashed) assert not algo.verify("wrong password", salt, hashed) def test_pbkdf2_algorithm_respects_iterations(): """Verify PBKDF2 algorithm respects custom iterations.""" algo = get_algorithm("pbkdf2") salt1, hash1 = algo.hash("test", iterations=100000) salt2, hash2 = algo.hash("test", iterations=200000) # Different iterations should produce different hashes even with same password assert hash1 != hash2 ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_algorithms.py::test_pbkdf2_algorithm_hash_round_trip -v` Expected: FAIL with "ValueError: Unknown algorithm: pbkdf2" **Step 3: Implement PBKDF2 algorithm** Create `salt/pbkdf2_algorithm.py`: ```python """PBKDF2 algorithm implementation.""" from __future__ import annotations import base64 import binascii import hashlib import hmac import os DEFAULT_SALT_BYTES = 16 DEFAULT_ITERATIONS = int(os.environ.get("PBKDF2_ITERATIONS", "200000")) class PBKDF2Algorithm: """PBKDF2-HMAC-SHA256 password hashing algorithm.""" identifier = "pbkdf2" def hash( self, password: str, *, iterations: int | None = None, salt_bytes: int = DEFAULT_SALT_BYTES, ) -> tuple[str, str]: """Hash a password using PBKDF2-HMAC-SHA256.""" iterations = iterations or DEFAULT_ITERATIONS salt = os.urandom(salt_bytes) derived = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, iterations, ) return ( base64.b64encode(salt).decode("utf-8"), base64.b64encode(derived).decode("utf-8"), ) def verify( self, password: str, salt_b64: str, hash_b64: str, *, iterations: int | None = None, ) -> bool: """Verify a password against PBKDF2 salt and hash.""" iterations = iterations or DEFAULT_ITERATIONS try: salt = base64.b64decode(salt_b64, validate=True) stored_hash = base64.b64decode(hash_b64, validate=True) except (binascii.Error, ValueError): return False derived = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, iterations, ) return hmac.compare_digest(derived, stored_hash) ``` **Step 4: Register PBKDF2 algorithm** Add to end of `salt/pbkdf2_algorithm.py`: ```python from salt.algorithms import register_algorithm # Auto-register when module is imported register_algorithm(PBKDF2Algorithm()) ``` **Step 5: Import PBKDF2 in algorithms module** Add to `salt/algorithms.py` at the end: ```python # Import to auto-register algorithms from salt import pbkdf2_algorithm # noqa: E402, F401 ``` **Step 6: Run tests to verify they pass** Run: `pytest tests/test_algorithms.py -v` Expected: All tests PASS **Step 7: Commit PBKDF2 algorithm** ```bash git add salt/pbkdf2_algorithm.py tests/test_algorithms.py salt/algorithms.py git commit -m "feat: implement PBKDF2 algorithm with registry" ``` --- ## Task 4: Implement Argon2 Algorithm **Files:** - Create: `salt/argon2_algorithm.py` - Modify: `tests/test_algorithms.py` **Step 1: Write failing test for Argon2 algorithm** Add to `tests/test_algorithms.py`: ```python def test_argon2_algorithm_hash_round_trip(): """Verify Argon2 algorithm can hash and verify passwords.""" algo = get_algorithm("argon2") salt, hashed = algo.hash("test password") assert algo.verify("test password", salt, hashed) assert not algo.verify("wrong password", salt, hashed) def test_argon2_algorithm_identifier(): """Verify Argon2 algorithm has correct identifier.""" algo = get_algorithm("argon2") assert algo.identifier == "argon2" ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_algorithms.py::test_argon2_algorithm_hash_round_trip -v` Expected: FAIL with "ValueError: Unknown algorithm: argon2" **Step 3: Implement Argon2 algorithm** Create `salt/argon2_algorithm.py`: ```python """Argon2 algorithm implementation.""" from __future__ import annotations import base64 import binascii from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError, InvalidHashError class Argon2Algorithm: """Argon2id password hashing algorithm.""" identifier = "argon2" def __init__(self): """Initialize with default Argon2 parameters.""" self._hasher = PasswordHasher() def hash(self, password: str, **kwargs) -> tuple[str, str]: """Hash a password using Argon2id. Note: Argon2 generates its own salt internally and returns a complete hash string that includes the salt and parameters. We return empty string for salt_b64 and the full hash as hash_b64. """ hash_string = self._hasher.hash(password) # Return empty salt since Argon2 embeds salt in hash return ("", base64.b64encode(hash_string.encode("utf-8")).decode("utf-8")) def verify( self, password: str, salt_b64: str, hash_b64: str, **kwargs, ) -> bool: """Verify a password against Argon2 hash. Note: salt_b64 is ignored since Argon2 embeds salt in the hash. """ try: hash_string = base64.b64decode(hash_b64, validate=True).decode("utf-8") self._hasher.verify(hash_string, password) return True except (VerifyMismatchError, InvalidHashError, binascii.Error, ValueError, UnicodeDecodeError): return False ``` **Step 4: Register Argon2 algorithm** Add to end of `salt/argon2_algorithm.py`: ```python from salt.algorithms import register_algorithm # Auto-register when module is imported register_algorithm(Argon2Algorithm()) ``` **Step 5: Import Argon2 in algorithms module** Modify `salt/algorithms.py` to import argon2: ```python # Import to auto-register algorithms from salt import pbkdf2_algorithm # noqa: E402, F401 from salt import argon2_algorithm # noqa: E402, F401 ``` **Step 6: Run tests to verify they pass** Run: `pytest tests/test_algorithms.py::test_argon2_algorithm_hash_round_trip -v` Expected: PASS **Step 7: Commit Argon2 algorithm** ```bash git add salt/argon2_algorithm.py tests/test_algorithms.py salt/algorithms.py git commit -m "feat: implement Argon2 algorithm" ``` --- ## Task 5: Implement bcrypt Algorithm **Files:** - Create: `salt/bcrypt_algorithm.py` - Modify: `tests/test_algorithms.py` **Step 1: Write failing test for bcrypt algorithm** Add to `tests/test_algorithms.py`: ```python def test_bcrypt_algorithm_hash_round_trip(): """Verify bcrypt algorithm can hash and verify passwords.""" algo = get_algorithm("bcrypt") salt, hashed = algo.hash("test password") assert algo.verify("test password", salt, hashed) assert not algo.verify("wrong password", salt, hashed) def test_bcrypt_algorithm_identifier(): """Verify bcrypt algorithm has correct identifier.""" algo = get_algorithm("bcrypt") assert algo.identifier == "bcrypt" ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_algorithms.py::test_bcrypt_algorithm_hash_round_trip -v` Expected: FAIL with "ValueError: Unknown algorithm: bcrypt" **Step 3: Implement bcrypt algorithm** Create `salt/bcrypt_algorithm.py`: ```python """bcrypt algorithm implementation.""" from __future__ import annotations import base64 import binascii import bcrypt as bcrypt_lib class BcryptAlgorithm: """bcrypt password hashing algorithm.""" identifier = "bcrypt" def hash(self, password: str, **kwargs) -> tuple[str, str]: """Hash a password using bcrypt. Note: bcrypt generates its own salt internally and returns a complete hash string that includes the salt. We return empty string for salt_b64 and the full hash as hash_b64. """ hashed = bcrypt_lib.hashpw(password.encode("utf-8"), bcrypt_lib.gensalt()) # Return empty salt since bcrypt embeds salt in hash return ("", base64.b64encode(hashed).decode("utf-8")) def verify( self, password: str, salt_b64: str, hash_b64: str, **kwargs, ) -> bool: """Verify a password against bcrypt hash. Note: salt_b64 is ignored since bcrypt embeds salt in the hash. """ try: hashed = base64.b64decode(hash_b64, validate=True) return bcrypt_lib.checkpw(password.encode("utf-8"), hashed) except (binascii.Error, ValueError): return False ``` **Step 4: Register bcrypt algorithm** Add to end of `salt/bcrypt_algorithm.py`: ```python from salt.algorithms import register_algorithm # Auto-register when module is imported register_algorithm(BcryptAlgorithm()) ``` **Step 5: Import bcrypt in algorithms module** Modify `salt/algorithms.py` to import bcrypt: ```python # Import to auto-register algorithms from salt import pbkdf2_algorithm # noqa: E402, F401 from salt import argon2_algorithm # noqa: E402, F401 from salt import bcrypt_algorithm # noqa: E402, F401 ``` **Step 6: Run tests to verify they pass** Run: `pytest tests/test_algorithms.py::test_bcrypt_algorithm_hash_round_trip -v` Expected: PASS **Step 7: Commit bcrypt algorithm** ```bash git add salt/bcrypt_algorithm.py tests/test_algorithms.py salt/algorithms.py git commit -m "feat: implement bcrypt algorithm" ``` --- ## Task 6: Refactor salt.py to Use Algorithm Interface **Files:** - Modify: `salt.py` - Modify: `tests/test_hashing.py` **Step 1: Write test for algorithm parameter in hash_password** Add to `tests/test_hashing.py`: ```python 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") ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_hashing.py::test_hash_password_with_algorithm_parameter -v` Expected: FAIL with "TypeError: hash_password() got an unexpected keyword argument 'algorithm'" **Step 3: Update hash_password to accept algorithm parameter** Modify `salt.py`: ```python def hash_password( password: str, *, algorithm: str = "pbkdf2", iterations: int | None = None, salt_bytes: int = DEFAULT_SALT_BYTES, ) -> tuple[str, str]: """Return a base64 encoded salt and hash for ``password``.""" from salt.algorithms import get_algorithm algo = get_algorithm(algorithm) return algo.hash(password, iterations=iterations, salt_bytes=salt_bytes) ``` **Step 4: Update verify_password to accept algorithm parameter** Modify `salt.py`: ```python def verify_password( password: str, salt_b64: str, hash_b64: str, *, algorithm: str = "pbkdf2", iterations: int | None = None, ) -> bool: """Validate ``password`` against the provided base64 salt + hash pair.""" from salt.algorithms import get_algorithm algo = get_algorithm(algorithm) return algo.verify(password, salt_b64, hash_b64, iterations=iterations) ``` **Step 5: Run all tests to verify backward compatibility** Run: `pytest tests/test_hashing.py -v` Expected: All tests PASS (existing tests still work) **Step 6: Commit refactored core functions** ```bash git add salt.py tests/test_hashing.py git commit -m "refactor: update hash_password and verify_password to use algorithm interface" ``` --- ## Task 7: Add CLI Algorithm Support to salt.py **Files:** - Modify: `salt.py` - Modify: `tests/test_cli.py` **Step 1: Write test for CLI algorithm parameter** Add to `tests/test_cli.py`: ```python def test_main_hash_with_algorithm_flag(): """Verify CLI accepts --algorithm flag.""" assert main(["hash", "--algorithm", "pbkdf2", "secret"]) == 0 def test_main_verify_with_algorithm_flag(): """Verify CLI verify accepts --algorithm flag.""" from salt import hash_password salt, hashed = hash_password("secret", algorithm="pbkdf2") assert main(["verify", "--algorithm", "pbkdf2", "secret", salt, hashed]) == 0 ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_cli.py::test_main_hash_with_algorithm_flag -v` Expected: FAIL with "unrecognized arguments: --algorithm" **Step 3: Add --algorithm flag to CLI parser** Modify `_build_parser()` in `salt.py`: ```python def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Erstellt sichere PBKDF2-Hashes oder verifiziert bestehende Werte." ) subparsers = parser.add_subparsers(dest="command") hash_parser = subparsers.add_parser( "hash", help="Erstellt ein Salt und einen Hash für ein Passwort." ) hash_parser.add_argument("password", help="Klartext-Passwort zum Hashen.") hash_parser.add_argument( "--algorithm", "-a", default="pbkdf2", help="Hash-Algorithmus (pbkdf2, argon2, bcrypt). Standard: pbkdf2", ) verify_parser = subparsers.add_parser("verify", help="Überprüft ein Passwort.") verify_parser.add_argument("password", help="Klartext-Passwort zur Überprüfung.") verify_parser.add_argument("salt", help="Base64-kodiertes Salt.") verify_parser.add_argument("hash", help="Base64-kodierter Hash.") verify_parser.add_argument( "--algorithm", "-a", default="pbkdf2", help="Hash-Algorithmus (pbkdf2, argon2, bcrypt). Standard: pbkdf2", ) return parser ``` **Step 4: Update main() to use algorithm parameter** Modify `main()` in `salt.py`: ```python def main(argv: Sequence[str] | None = None) -> int: parser = _build_parser() arg_list = list(argv) if argv is not None else sys.argv[1:] args = parser.parse_args(_normalize_args(arg_list)) if args.command == "verify": if verify_password(args.password, args.salt, args.hash, algorithm=args.algorithm): print("✓ Passwort korrekt") return 0 print("✗ Passwort falsch") return 1 if args.command != "hash": parser.error('Bitte einen Hash generieren oder "verify" verwenden.') salt, hash_value = hash_password(args.password, algorithm=args.algorithm) print(f"Salt: {salt}") print(f"Hash: {hash_value}") return 0 ``` **Step 5: Run tests to verify they pass** Run: `pytest tests/test_cli.py -v` Expected: All tests PASS **Step 6: Commit CLI algorithm support** ```bash git add salt.py tests/test_cli.py git commit -m "feat: add --algorithm flag to CLI" ``` --- ## Task 8: Add CLI Algorithm Support to salt2.py **Files:** - Modify: `salt2.py` - Create: `tests/test_cli2.py` **Step 1: Write test for salt2.py CLI algorithm parameter** Create `tests/test_cli2.py`: ```python 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 ``` **Step 2: Run test to verify it fails** Run: `pytest tests/test_cli2.py::test_main_generate_with_algorithm_flag -v` Expected: FAIL with "unrecognized arguments: --algorithm" **Step 3: Add --algorithm flag to salt2.py parser** Modify `_build_parser()` in `salt2.py`: ```python def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="CLI für PBKDF2-Hashing inklusive Verify-Modus." ) subparsers = parser.add_subparsers(dest="command", required=True) generate_parser = subparsers.add_parser( "generate", help="Erzeugt ein neues Salt+Hash Paar." ) generate_parser.add_argument("password", help="Klartext-Passwort zum Hashen.") generate_parser.add_argument( "--algorithm", "-a", default="pbkdf2", help="Hash-Algorithmus (pbkdf2, argon2, bcrypt). Standard: pbkdf2", ) verify_parser = subparsers.add_parser( "verify", help="Validiert ein Passwort anhand von Salt und Hash.", ) 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", default="pbkdf2", help="Hash-Algorithmus (pbkdf2, argon2, bcrypt). Standard: pbkdf2", ) return parser ``` **Step 4: Update command handlers to use algorithm parameter** Modify `_command_generate()` and `_command_verify()` in `salt2.py`: ```python def _command_generate(args: argparse.Namespace) -> int: 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, algorithm=args.algorithm): print("✓ Passwort korrekt") return 0 print("✗ Passwort falsch") return 1 ``` **Step 5: Run tests to verify they pass** Run: `pytest tests/test_cli2.py -v` Expected: All tests PASS **Step 6: Commit salt2.py algorithm support** ```bash git add salt2.py tests/test_cli2.py git commit -m "feat: add --algorithm flag to salt2 CLI" ``` --- ## Task 9: Add Algorithm Selection to CLI Help **Files:** - Modify: `salt.py` - Modify: `salt2.py` **Step 1: Add algorithm list command to salt.py** Modify `_build_parser()` in `salt.py` to add a list subcommand: ```python def _build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Erstellt sichere PBKDF2-Hashes oder verifiziert bestehende Werte." ) subparsers = parser.add_subparsers(dest="command") # ... existing hash and verify parsers ... subparsers.add_parser( "list-algorithms", help="Zeigt alle verfügbaren Hash-Algorithmen an.", ) return parser ``` **Step 2: Add list-algorithms handler to main()** Modify `main()` in `salt.py`: ```python def main(argv: Sequence[str] | None = None) -> int: parser = _build_parser() 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 salt.algorithms import list_algorithms print("Verfügbare Algorithmen:") for algo in list_algorithms(): print(f" - {algo}") return 0 if args.command == "verify": # ... existing verify code ... if args.command != "hash": parser.error('Bitte einen Hash generieren oder "verify" verwenden.') # ... existing hash code ... ``` **Step 3: Test list-algorithms command** Run: `python3 salt.py list-algorithms` Expected: Output showing "pbkdf2", "argon2", "bcrypt" **Step 4: Add same functionality to salt2.py** Apply similar changes to `salt2.py` `_build_parser()` and add a `_command_list_algorithms()` function. **Step 5: Commit algorithm listing** ```bash git add salt.py salt2.py git commit -m "feat: add list-algorithms command to CLI" ``` --- ## Task 10: Update Documentation **Files:** - Modify: `CLAUDE.md` **Step 1: Update CLAUDE.md with algorithm documentation** Add section to `CLAUDE.md` after "## Common Commands": ```markdown ### Algorithm Support The utility now supports multiple hashing algorithms: - **pbkdf2** (default): PBKDF2-HMAC-SHA256 with 200,000 iterations - **argon2**: Argon2id with default parameters from argon2-cffi - **bcrypt**: bcrypt with default cost factor ```bash # List available algorithms python3 salt.py list-algorithms # Hash with specific algorithm python3 salt.py hash --algorithm argon2 "MyPassword" python3 salt2.py generate --algorithm bcrypt "MyPassword" # Verify with specific algorithm python3 salt.py verify --algorithm pbkdf2 ``` **Note:** Argon2 and bcrypt embed the salt within their hash output, so the salt field may be empty for these algorithms. ``` **Step 2: Update Key Functions section** Update the function signatures in CLAUDE.md: ```markdown **`hash_password(password, *, algorithm="pbkdf2", iterations=None, salt_bytes=16) -> tuple[str, str]`** - Supports multiple algorithms: pbkdf2, argon2, bcrypt - Generates a cryptographically secure random salt (or uses algorithm's embedded salt) - Returns base64-encoded (salt, hash) tuple **`verify_password(password, salt_b64, hash_b64, *, algorithm="pbkdf2", iterations=None) -> bool`** - Validates a password against stored salt/hash using specified algorithm - Uses timing-safe comparison when applicable - Returns `False` for invalid base64 without raising exceptions ``` **Step 3: Commit documentation updates** ```bash git add CLAUDE.md git commit -m "docs: update documentation for multi-algorithm support" ``` --- ## Task 11: Integration Testing **Files:** - Create: `tests/test_integration.py` **Step 1: Write integration tests** Create `tests/test_integration.py`: ```python """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 ``` **Step 2: Run integration tests** Run: `pytest tests/test_integration.py -v` Expected: All tests PASS **Step 3: Run full test suite** Run: `pytest -v` Expected: All tests PASS, coverage >90% **Step 4: Commit integration tests** ```bash git add tests/test_integration.py git commit -m "test: add integration tests for multi-algorithm support" ``` --- ## Task 12: Backward Compatibility Verification **Files:** - Modify: `tests/test_hashing.py` **Step 1: Write backward compatibility test** Add to `tests/test_hashing.py`: ```python 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") ``` **Step 2: Run backward compatibility test** Run: `pytest tests/test_hashing.py::test_backward_compatibility_with_old_pbkdf2_hashes -v` Expected: PASS **Step 3: Verify all existing tests still pass** Run: `pytest tests/test_hashing.py tests/test_cli.py -v` Expected: All existing tests PASS (no regressions) **Step 4: Commit backward compatibility test** ```bash git add tests/test_hashing.py git commit -m "test: add backward compatibility verification" ``` --- ## Verification Checklist Run these commands to verify the implementation: - [ ] `pytest -v` - All tests pass - [ ] `pytest --cov=salt --cov-report=term-missing` - Coverage >90% - [ ] `python3 salt.py list-algorithms` - Shows all 3 algorithms - [ ] `python3 salt.py hash --algorithm pbkdf2 "test"` - Works - [ ] `python3 salt.py hash --algorithm argon2 "test"` - Works - [ ] `python3 salt.py hash --algorithm bcrypt "test"` - Works - [ ] `python3 salt.py "test"` - Shortcut still works (uses pbkdf2) - [ ] `python3 salt2.py generate --algorithm argon2 "test"` - Works - [ ] Verify no regressions in existing functionality --- ## Architecture Notes for Engineer **Algorithm Registration Pattern:** - Each algorithm implements the `Algorithm` protocol (hash, verify, identifier) - Algorithms auto-register themselves on import via `register_algorithm()` - Registry is in `salt/algorithms.py` with `get_algorithm()` and `list_algorithms()` - Import side-effects ensure all algorithms are registered at module load time **Backward Compatibility:** - Default algorithm is "pbkdf2" - All existing code works without modification - `algorithm` parameter is optional with default value - Existing PBKDF2 hashes verify correctly **Salt Handling:** - PBKDF2: Generates separate salt, returns as first tuple element - Argon2: Embeds salt in hash string, returns empty string for salt - bcrypt: Embeds salt in hash string, returns empty string for salt **Testing Strategy:** - Unit tests per algorithm in `tests/test_algorithms.py` - Integration tests in `tests/test_integration.py` - CLI tests in `tests/test_cli.py` and `tests/test_cli2.py` - Backward compatibility test in `tests/test_hashing.py` **Design Principles:** - DRY: Common interface, no duplication - YAGNI: Only implement requested algorithms - TDD: Write failing test first, minimal implementation, then pass - Frequent commits: One logical change per commit