Borg/php/borg-stmf/tests/InteropTest.php
Snider b3755da69d feat: Add STMF form encryption and SMSG secure message packages
STMF (Sovereign Form Encryption):
- X25519 ECDH + ChaCha20-Poly1305 hybrid encryption
- Go library (pkg/stmf/) with encrypt/decrypt and HTTP middleware
- WASM module for client-side browser encryption
- JavaScript wrapper with TypeScript types (js/borg-stmf/)
- PHP library for server-side decryption (php/borg-stmf/)
- Full cross-platform interoperability (Go <-> PHP)

SMSG (Secure Message):
- Password-based ChaCha20-Poly1305 message encryption
- Support for attachments, metadata, and PKI reply keys
- WASM bindings for browser-based decryption

Demos:
- index.html: Form encryption demo with modern dark UI
- support-reply.html: Decrypt password-protected messages
- examples/smsg-reply/: CLI tool for creating encrypted replies

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 00:49:07 +00:00

238 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Borg\STMF\Tests;
require_once __DIR__ . '/../src/FormField.php';
require_once __DIR__ . '/../src/FormData.php';
require_once __DIR__ . '/../src/KeyPair.php';
require_once __DIR__ . '/../src/DecryptionException.php';
require_once __DIR__ . '/../src/InvalidPayloadException.php';
require_once __DIR__ . '/../src/STMF.php';
use Borg\STMF\STMF;
use Borg\STMF\KeyPair;
/**
* Interoperability test - decrypts payloads encrypted by Go
*/
class InteropTest
{
private array $vectors;
private int $passed = 0;
private int $failed = 0;
public function __construct(string $vectorsFile)
{
$json = file_get_contents($vectorsFile);
$this->vectors = json_decode($json, true);
if ($this->vectors === null) {
throw new \RuntimeException("Failed to parse test vectors: " . json_last_error_msg());
}
}
public function run(): bool
{
echo "Running STMF Interoperability Tests\n";
echo "===================================\n\n";
foreach ($this->vectors as $vector) {
$this->runVector($vector);
}
echo "\n===================================\n";
echo "Results: {$this->passed} passed, {$this->failed} failed\n";
return $this->failed === 0;
}
private function runVector(array $vector): void
{
$name = $vector['name'];
echo "Testing: {$name}... ";
try {
// Create STMF instance with private key
$stmf = new STMF($vector['private_key']);
// Decrypt the payload
$formData = $stmf->decrypt($vector['encrypted_b64']);
// Verify fields
$expectedFields = $vector['expected_fields'] ?? [];
foreach ($expectedFields as $key => $expectedValue) {
$actualValue = $formData->get($key);
if ($actualValue !== $expectedValue) {
throw new \RuntimeException(
"Field '{$key}': expected " . json_encode($expectedValue) .
", got " . json_encode($actualValue)
);
}
}
// Verify metadata if present
$expectedMeta = $vector['expected_meta'] ?? [];
if ($expectedMeta) {
$actualMeta = $formData->getMetadata();
foreach ($expectedMeta as $key => $expectedValue) {
$actualValue = $actualMeta[$key] ?? null;
if ($actualValue !== $expectedValue) {
throw new \RuntimeException(
"Metadata '{$key}': expected " . json_encode($expectedValue) .
", got " . json_encode($actualValue)
);
}
}
}
// Verify field count
$expectedCount = count($expectedFields);
$actualCount = count($formData->fields());
if ($actualCount !== $expectedCount) {
throw new \RuntimeException(
"Field count: expected {$expectedCount}, got {$actualCount}"
);
}
echo "PASS\n";
$this->passed++;
} catch (\Exception $e) {
echo "FAIL\n";
echo " Error: " . $e->getMessage() . "\n";
$this->failed++;
}
}
}
// Additional standalone tests
class StandaloneTests
{
public static function runAll(): bool
{
echo "\nRunning Standalone PHP Tests\n";
echo "============================\n\n";
$passed = 0;
$failed = 0;
// Test 1: KeyPair generation
echo "Testing: KeyPair generation... ";
try {
$kp = KeyPair::generate();
if (strlen($kp->getPublicKey()) !== 32) {
throw new \RuntimeException("Public key wrong length");
}
if (strlen($kp->getPrivateKey()) !== 32) {
throw new \RuntimeException("Private key wrong length");
}
echo "PASS\n";
$passed++;
} catch (\Exception $e) {
echo "FAIL: " . $e->getMessage() . "\n";
$failed++;
}
// Test 2: KeyPair from private key
echo "Testing: KeyPair from private key... ";
try {
$kp1 = KeyPair::generate();
$kp2 = KeyPair::fromPrivateKeyBase64($kp1->getPrivateKeyBase64());
if ($kp1->getPublicKeyBase64() !== $kp2->getPublicKeyBase64()) {
throw new \RuntimeException("Public keys don't match");
}
echo "PASS\n";
$passed++;
} catch (\Exception $e) {
echo "FAIL: " . $e->getMessage() . "\n";
$failed++;
}
// Test 3: Invalid payload validation
echo "Testing: Invalid payload detection... ";
try {
$kp = KeyPair::generate();
$stmf = STMF::fromKeyPair($kp);
$isValid = $stmf->validate("not-valid-base64!!!");
if ($isValid) {
throw new \RuntimeException("Should have rejected invalid payload");
}
$isValid2 = $stmf->validate(base64_encode("FAKE" . str_repeat("\x00", 100)));
if ($isValid2) {
throw new \RuntimeException("Should have rejected fake STMF");
}
echo "PASS\n";
$passed++;
} catch (\Exception $e) {
echo "FAIL: " . $e->getMessage() . "\n";
$failed++;
}
// Test 4: FormData methods
echo "Testing: FormData methods... ";
try {
$fields = [
\Borg\STMF\FormField::fromArray(['name' => 'email', 'value' => 'test@test.com']),
\Borg\STMF\FormField::fromArray(['name' => 'tag', 'value' => 'one']),
\Borg\STMF\FormField::fromArray(['name' => 'tag', 'value' => 'two']),
];
$fd = new \Borg\STMF\FormData($fields, ['origin' => 'https://example.com']);
if ($fd->get('email') !== 'test@test.com') {
throw new \RuntimeException("get() failed");
}
if (!$fd->has('email')) {
throw new \RuntimeException("has() failed");
}
if ($fd->has('nonexistent')) {
throw new \RuntimeException("has() false positive");
}
$tags = $fd->getAll('tag');
if (count($tags) !== 2 || $tags[0] !== 'one' || $tags[1] !== 'two') {
throw new \RuntimeException("getAll() failed");
}
if ($fd->getOrigin() !== 'https://example.com') {
throw new \RuntimeException("getOrigin() failed");
}
echo "PASS\n";
$passed++;
} catch (\Exception $e) {
echo "FAIL: " . $e->getMessage() . "\n";
$failed++;
}
echo "\n============================\n";
echo "Standalone: {$passed} passed, {$failed} failed\n";
return $failed === 0;
}
}
// Run tests
if (php_sapi_name() === 'cli') {
$vectorsFile = __DIR__ . '/test_vectors.json';
if (!file_exists($vectorsFile)) {
echo "Error: test_vectors.json not found.\n";
echo "Generate it with: go run tests/generate_test_vectors.go > tests/test_vectors.json\n";
exit(1);
}
// Check sodium extension
if (!extension_loaded('sodium')) {
echo "Error: sodium extension not loaded.\n";
echo "Enable it in php.ini or install php-sodium.\n";
exit(1);
}
$interop = new InteropTest($vectorsFile);
$interopPassed = $interop->run();
$standalonePassed = StandaloneTests::runAll();
exit(($interopPassed && $standalonePassed) ? 0 : 1);
}