chore: initial commit with existing codebase

This commit is contained in:
2025-11-13 13:08:49 +00:00
commit 18c991181a
11 changed files with 1424 additions and 0 deletions

25
AGENTS.md Normal file
View File

@@ -0,0 +1,25 @@
# Repository Guidelines
## Project Structure & Module Organization
The repo is intentionally small: `salt.py` is a reference implementation that demonstrates secure PBKDF2 hashing, while `salt2.py` wraps the same logic in a CLI with verify mode. Keep new helpers alongside these modules only if they remain single-file utilities; otherwise scaffold a package under `salt/` and mirror tests in `tests/`. Generate derived assets (sample salts, fixtures) in `artifacts/` and never commit secrets or real user data.
## Build, Test, and Development Commands
Run quick manual checks directly:
```bash
python3 salt.py "MySecret" # prints salt + hash
python3 salt2.py verify <pw> <salt> <hash> # exit 0 on success
```
Add dependencies to `requirements.txt` if you introduce new libraries. For automated validation add pytest and expose a default target:
```bash
python3 -m pytest # runs unit tests
python3 -m pytest -k verify --maxfail=1 -vv
```
## Coding Style & Naming Conventions
Target Python 3.11+, 4-space indentation, and type-annotate public functions. Use descriptive, lowercase_with_underscores names for variables and helper functions; reserve CapWords for classes if you introduce them. Favor stdlib modules (hashlib, secrets, base64) before external choices. Keep scripts executable (`#!/usr/bin/env python3`) and guard entry points with `if __name__ == "__main__":`.
## Testing Guidelines
Pin tests under `tests/test_<feature>.py` and mirror module names (e.g., `tests/test_hashing.py`). Cover both happy paths and failure handling (invalid base64, mismatched hashes). Include property-style assertions such as round-tripping `hash_password` output into `verify_password`. When adding algorithms, require regression tests with representative salts and ensure coverage stays above 90% for the hashing utilities.
## Commit & Pull Request Guidelines
Use short, imperative subject lines (e.g., `feat: add argon2 hashing option`) and describe motivation plus key changes in the body. Reference related issues with `Fixes #ID` when applicable. Open PRs only after tests pass, include reproduction or CLI output for security changes, and add screenshots or logs if the behavior affects tooling UX. Keep PRs focused; split large refactors into reviewable chunks.

104
CLAUDE.md Normal file
View File

@@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Python PBKDF2 password hashing utility with two implementations:
- `salt.py`: Reference implementation with CLI that supports both `hash` and `verify` subcommands, plus a shortcut where running `python3 salt.py <password>` automatically invokes the hash command
- `salt2.py`: Alternative CLI that imports from `salt.py` and requires explicit subcommands (`generate` and `verify`)
Both implementations use PBKDF2-HMAC-SHA256 with configurable iteration counts (default: 200,000, overridable via `PBKDF2_ITERATIONS` env var).
## Common Commands
### Running the CLI
```bash
# Hash a password (salt.py supports shortcut syntax)
python3 salt.py "MyPassword"
python3 salt.py hash "MyPassword"
# Verify a password
python3 salt.py verify <password> <salt_b64> <hash_b64>
# Alternative CLI (requires explicit subcommands)
python3 salt2.py generate "MyPassword"
python3 salt2.py verify <password> <salt_b64> <hash_b64>
```
### Testing
```bash
# Run all tests
python3 -m pytest
# Run specific tests with verbose output
python3 -m pytest -k verify --maxfail=1 -vv
# Run tests for a specific module
python3 -m pytest tests/test_hashing.py
```
### Dependencies
Dependencies are listed in `requirements.txt`. Install with:
```bash
pip install -r requirements.txt
```
## Code Architecture
### Module Structure
- **salt.py**: Core module containing `hash_password()` and `verify_password()` functions, plus an integrated CLI via `main()`
- **salt2.py**: Wrapper CLI that imports from `salt.py` and provides an alternative command structure
- **tests/**: Test suite mirroring the module structure
- `test_hashing.py`: Tests for core hashing/verification functions
- `test_cli.py`: Tests for CLI behavior (specifically `salt.py`'s shortcut syntax)
### Key Functions
**`hash_password(password, *, iterations=None, salt_bytes=16) -> tuple[str, str]`**
- Generates a cryptographically secure random salt
- Derives a hash using PBKDF2-HMAC-SHA256
- Returns base64-encoded (salt, hash) tuple
**`verify_password(password, salt_b64, hash_b64, *, iterations=None) -> bool`**
- Validates a password against stored salt/hash
- Uses timing-safe comparison via `hmac.compare_digest()`
- Returns `False` for invalid base64 without raising exceptions
### CLI Argument Handling
The `salt.py` module includes `_normalize_args()` which allows shortcut syntax: if the first argument is not a subcommand (`hash`/`verify`) and not a flag, it's automatically prefixed with `hash`. This is tested in `tests/test_cli.py:test_main_supports_hash_shortcut()`.
## Development Guidelines
### Python Version and Style
- Target Python 3.11+
- Use type annotations on public functions
- 4-space indentation
- Follow `lowercase_with_underscores` naming for functions and variables
### Security Practices
- Always use `os.urandom()` for salt generation (never predictable values)
- Use `hmac.compare_digest()` for hash comparison to prevent timing attacks
- Validate base64 input with `validate=True` parameter
- Handle encoding errors gracefully by returning `False` rather than raising exceptions
### Testing Requirements
- Place tests in `tests/test_<module>.py` matching the module name
- Cover happy paths and error handling (invalid base64, wrong passwords)
- Include round-trip tests: hash → verify with correct/incorrect passwords
- Maintain >90% coverage for hashing utilities
- When adding new hash algorithms, include regression tests with known salt/hash pairs
### Configuration
- Iteration count defaults to 200,000 but respects `PBKDF2_ITERATIONS` environment variable
- Salt size defaults to 16 bytes (configurable via `salt_bytes` parameter)
## Commit Guidelines
Use imperative subject lines with conventional commit prefixes:
- `feat:` for new features
- `fix:` for bug fixes
- `test:` for test additions/changes
- `refactor:` for code restructuring
Reference issues with `Fixes #ID` when applicable. PRs should only be opened after tests pass.

Binary file not shown.

File diff suppressed because it is too large Load Diff

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pytest>=7.4
argon2-cffi>=23.1.0
bcrypt>=4.1.0

111
salt.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""PBKDF2 helper utilities for hashing and verifying passwords."""
from __future__ import annotations
import argparse
import base64
import binascii
import hashlib
import hmac
import os
import sys
from typing import Sequence
DEFAULT_SALT_BYTES = 16
DEFAULT_ITERATIONS = int(os.environ.get("PBKDF2_ITERATIONS", "200000"))
def hash_password(
password: str,
*,
iterations: int | None = None,
salt_bytes: int = DEFAULT_SALT_BYTES,
) -> tuple[str, str]:
"""Return a base64 encoded salt and hash for ``password``."""
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_password(
password: str,
salt_b64: str,
hash_b64: str,
*,
iterations: int | None = None,
) -> bool:
"""Validate ``password`` against the provided base64 salt + hash pair."""
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)
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.")
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.")
return parser
def _normalize_args(argv: Sequence[str] | None) -> list[str]:
"""Erlaube ``python3 salt.py <passwort>`` als Abkürzung für ``hash``."""
if not argv:
return []
if argv[0] in {"hash", "verify"} or argv[0].startswith("-"):
return list(argv)
return ["hash", *argv]
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):
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)
print(f"Salt: {salt}")
print(f"Hash: {hash_value}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

57
salt2.py Normal file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Command line interface around the salt hashing helpers."""
from __future__ import annotations
import argparse
from typing import Sequence
from salt import hash_password, verify_password
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.")
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.")
return parser
def _command_generate(args: argparse.Namespace) -> int:
salt, hash_value = hash_password(args.password)
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):
print("✓ Passwort korrekt")
return 0
print("✗ Passwort falsch")
return 1
def main(argv: Sequence[str] | None = None) -> int:
parser = _build_parser()
args = parser.parse_args(argv)
if args.command == "generate":
return _command_generate(args)
return _command_verify(args)
if __name__ == "__main__":
raise SystemExit(main())

11
tests/test_cli.py Normal file
View File

@@ -0,0 +1,11 @@
from salt import hash_password, main
def test_main_supports_hash_shortcut() -> None:
assert main(["secret"]) == 0
def test_main_verify_round_trip() -> None:
salt, hashed = hash_password("secret-value")
assert main(["verify", "secret-value", salt, hashed]) == 0
assert main(["verify", "wrong", salt, hashed]) == 1

20
tests/test_hashing.py Normal file
View File

@@ -0,0 +1,20 @@
import re
from salt import hash_password, verify_password
def test_hash_password_round_trip() -> None:
salt, hashed = hash_password("correct horse battery staple")
assert verify_password("correct horse battery staple", salt, hashed)
assert not verify_password("wrong", salt, hashed)
def test_hash_password_returns_base64() -> None:
salt, hashed = hash_password("secret")
base64_pattern = re.compile(r"^[A-Za-z0-9+/]+={0,2}$")
assert base64_pattern.fullmatch(salt)
assert base64_pattern.fullmatch(hashed)
def test_verify_password_handles_invalid_base64() -> None:
assert verify_password("secret", "**invalid**", "???") is False