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