1094 lines
30 KiB
Markdown
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
|