Files
password-security-python/docs/plans/2025-11-13-multi-algorithm-support.md

1094 lines
30 KiB
Markdown

# 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 <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**
```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