70 lines
3.2 KiB
Python
70 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate independent Laravel CBC envelopes and optionally verify using PHP OpenSSL.
|
|
|
|
Run: python tests/crypto_interop.py [--check-php]
|
|
Requires Python cryptography. The optional PHP check requires PHP's openssl extension.
|
|
Fixtures exercise Laravel's encryptString envelope, not the entire Laravel framework.
|
|
"""
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
from cryptography.hazmat.primitives.padding import PKCS7
|
|
|
|
|
|
def generate():
|
|
fixtures = []
|
|
for key in (b"0123456789abcdef", b"0123456789abcdef0123456789abcdef"):
|
|
for plaintext in ("", "Laravel encrypted field", "Unicode 🔐 café"):
|
|
iv = bytes(range(16))
|
|
padder = PKCS7(128).padder()
|
|
padded = padder.update(plaintext.encode()) + padder.finalize()
|
|
encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
|
|
value = base64.b64encode(encryptor.update(padded) + encryptor.finalize()).decode()
|
|
encoded_iv = base64.b64encode(iv).decode()
|
|
mac = hmac.new(key, (encoded_iv + value).encode(), hashlib.sha256).hexdigest()
|
|
payload = dict(iv=encoded_iv, value=value, mac=mac, tag="")
|
|
envelope = base64.b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode()
|
|
fixtures.append(dict(cipher=f"AES-{len(key)*8}-CBC", app_key="base64:" + base64.b64encode(key).decode(), plaintext=plaintext, envelope=envelope))
|
|
return fixtures
|
|
|
|
|
|
def check_php(fixtures):
|
|
php = shutil.which("php")
|
|
if not php:
|
|
print("SKIP PHP OpenSSL: php executable unavailable")
|
|
return
|
|
code = r'''
|
|
$rows = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR);
|
|
foreach ($rows as $row) {
|
|
$key = base64_decode(substr($row['app_key'], 7), true);
|
|
$payload = json_decode(base64_decode($row['envelope']), true);
|
|
$mac = hash_hmac('sha256', $payload['iv'].$payload['value'], $key);
|
|
if (!hash_equals($mac, $payload['mac'])) { throw new Exception('MAC mismatch'); }
|
|
$plain = openssl_decrypt($payload['value'], $row['cipher'], $key, 0, base64_decode($payload['iv']));
|
|
if ($plain !== $row['plaintext']) { throw new Exception('decrypt mismatch'); }
|
|
$encrypted = openssl_encrypt($row['plaintext'], $row['cipher'], $key, 0, base64_decode($payload['iv']));
|
|
if ($encrypted !== $payload['value']) { throw new Exception('encrypt mismatch'); }
|
|
}
|
|
echo 'PASS PHP OpenSSL: '.count($rows).' fixtures'.PHP_EOL;
|
|
'''
|
|
subprocess.run([php, "-r", code], input=json.dumps(fixtures), text=True, check=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--check-php", action="store_true")
|
|
args = parser.parse_args()
|
|
rows = generate()
|
|
target = Path(__file__).parent / "fixtures" / "laravel_cbc.json"
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(json.dumps(rows, indent=2, ensure_ascii=False) + "\n")
|
|
print(f"Generated {len(rows)} independent Python AES CBC / HMAC fixtures: {target}")
|
|
if args.check_php:
|
|
check_php(rows)
|