76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""Cross-language tests: the bundled JS solver and the Python validator must agree.
|
|
|
|
These tests execute the actual bundled easypow.js with Node.js, guarding
|
|
against the two implementations drifting apart (e.g. mismatched masks).
|
|
Skipped when node is not available.
|
|
"""
|
|
|
|
import json
|
|
import secrets
|
|
import shutil
|
|
import subprocess
|
|
from importlib.resources import files
|
|
|
|
import base64url
|
|
import pytest
|
|
|
|
from easypow import generate, solve, validate
|
|
from easypow.pow import _decode
|
|
|
|
NODE = shutil.which("node")
|
|
SOLVER_PATH = files("easypow").joinpath("easypow.js")
|
|
|
|
pytestmark = pytest.mark.skipif(NODE is None, reason="node is not available")
|
|
|
|
|
|
def _run_node(script: str) -> str:
|
|
result = subprocess.run(
|
|
[NODE, "--input-type=module", "-e", script],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _js_solves(challenge: str) -> None:
|
|
script = (
|
|
f"import solve from {json.dumps(str(SOLVER_PATH))}\n"
|
|
f"console.log(await solve({json.dumps(challenge)}))"
|
|
)
|
|
validate(challenge, _run_node(script).splitlines()[-1])
|
|
|
|
|
|
def test_js_solution_validates_in_python() -> None:
|
|
_js_solves(generate(seconds=0.2))
|
|
|
|
|
|
def test_js_solution_validates_in_python_custom_params() -> None:
|
|
_js_solves(base64url.enc(bytes([2, 9 << 4 | 6]) + secrets.token_bytes(7)))
|
|
|
|
|
|
def test_python_solution_validates_in_js() -> None:
|
|
challenge = generate(seconds=0.2)
|
|
_, _, mask, iterations = _decode(challenge)
|
|
script = f"""
|
|
const B64URL = {{ alphabet: 'base64url', omitPadding: true }}
|
|
const challenge = Uint8Array.fromBase64({json.dumps(challenge)}, B64URL)
|
|
const key = await crypto.subtle.importKey(
|
|
'raw', challenge, 'PBKDF2', false, ['deriveBits']
|
|
)
|
|
const args = {{
|
|
name: 'PBKDF2', salt: null, hash: 'SHA-512', iterations: {iterations}
|
|
}}
|
|
for (const fragment of {json.dumps(solve(challenge))}.split('.')) {{
|
|
const nonce = Uint8Array.fromBase64(fragment.padEnd(6, 'A'), B64URL)
|
|
const result = new Uint32Array(await crypto.subtle.deriveBits(
|
|
{{ ...args, salt: nonce }}, key, 32
|
|
))
|
|
if (result[0] & {mask}) {{
|
|
console.error('nonce failed mask check')
|
|
process.exit(1)
|
|
}}
|
|
}}
|
|
"""
|
|
_run_node(script)
|