81 lines
2.1 KiB
Python
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
|