unwind/unwind/utils.py

81 lines
2.1 KiB
Python

import base64
import hashlib
import secrets
from typing import Literal
def b64encode(b: bytes) -> str:
return base64.b64encode(b).decode().rstrip("=")
def b64decode(s: str) -> bytes:
return base64.b64decode(b64padded(s))
def b64padded(s: str) -> str:
return s + "=" * (4 - len(s) % 4)
def phc_scrypt(
secret: bytes,
*,
salt: bytes | None = None,
params: dict[Literal["n", "r", "p"], int] = {},
) -> str:
"""Return the scrypt expanded secret in PHC string format.
Uses somewhat sane defaults.
For more information on the PHC string format, see
https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
"""
if salt is None:
salt = secrets.token_bytes(16)
n = params.get("n", 2**14) # CPU/Memory cost factor
r = params.get("r", 8) # block size
p = params.get("p", 1) # parallelization factor
# maxmem = 2 * 128 * n * r * p
hashed_secret = hashlib.scrypt(secret, salt=salt, n=n, r=r, p=p)
encoded_params = ",".join(f"{k}={v}" for k, v in {"n": n, "r": r, "p": p}.items())
phc = "".join(
f"${x}"
for x in ["scrypt", encoded_params, b64encode(salt), b64encode(hashed_secret)]
)
return phc
def phc_compare(*, secret: str, phc_string: str) -> bool:
args = parse_phc(phc_string)
if args["id"] != "scrypt":
raise ValueError(f"Algorithm not supported: {args['id']}")
assert type(args["params"]) is dict
encoded = phc_scrypt(b64decode(secret), salt=args["salt"], params=args["params"])
return secrets.compare_digest(encoded, phc_string)
def parse_phc(s: str):
parts = dict.fromkeys(["id", "version", "params", "salt", "hash"])
_, parts["id"], *rest = s.split("$")
if rest and rest[0].startswith("v="):
parts["version"] = rest.pop(0)
if rest and "=" in rest[0]:
parts["params"] = {
kv[0]: int(kv[1])
for p in rest.pop(0).split(",")
if len(kv := p.split("=", 2)) == 2
}
if rest:
parts["salt"] = b64decode(rest.pop(0))
if rest:
parts["hash"] = b64decode(rest.pop(0))
return parts