Files
password-security-python/salt.py

112 lines
3.2 KiB
Python

#!/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())