30 KiB
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
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:
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:
"""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
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:
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:
"""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:
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:
# 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
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:
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:
"""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:
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:
# 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
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:
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:
"""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:
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:
# 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
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:
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:
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:
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
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:
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:
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:
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
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:
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:
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:
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
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:
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:
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
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":
### 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 <password> <salt> <hash>
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
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:
"""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
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:
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
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 passpytest --cov=salt --cov-report=term-missing- Coverage >90%python3 salt.py list-algorithms- Shows all 3 algorithmspython3 salt.py hash --algorithm pbkdf2 "test"- Workspython3 salt.py hash --algorithm argon2 "test"- Workspython3 salt.py hash --algorithm bcrypt "test"- Workspython3 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
Algorithmprotocol (hash, verify, identifier) - Algorithms auto-register themselves on import via
register_algorithm() - Registry is in
salt/algorithms.pywithget_algorithm()andlist_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
algorithmparameter 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.pyandtests/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