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

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 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