Files

76 lines
3.7 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
require_once __DIR__ . '/../app/Domain/Credential/TechnicalInformation.php';
require_once __DIR__ . '/../app/Domain/Credential/CredentialVault.php';
use App\Domain\Credential\CredentialVault;
use App\Domain\Credential\TechnicalInformation;
function credential_assert_same(mixed $expected, mixed $actual, string $message): void
{
if ($expected !== $actual) {
throw new RuntimeException($message . "\nExpected: " . var_export($expected, true) . "\nActual: " . var_export($actual, true));
}
}
$key = base64_encode(random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES));
$vault = new CredentialVault($key);
$secret = 'P@ssword & token 123';
$ciphertext = $vault->encrypt($secret);
if ($ciphertext === $secret || $vault->decrypt($ciphertext) !== $secret) {
throw new RuntimeException('Credentials must round-trip through authenticated encryption.');
}
try {
(new CredentialVault(base64_encode(random_bytes(32))))->decrypt($ciphertext);
throw new RuntimeException('Decrypting with the wrong key should fail.');
} catch (RuntimeException $expected) {
if (!str_contains($expected->getMessage(), 'decrypt')) {
throw new RuntimeException('Wrong-key failure should be explicit.');
}
}
$masked = $vault->mask($secret);
credential_assert_same('••••••••••••••••••••', $masked, 'Secrets must never be shown in plaintext.');
credential_assert_same('••••••••••••••••••••', $vault->display(['secret' => $secret])['secret'], 'Display must mask secret fields.');
$credential = [
'id' => 7,
'category' => 'hosting',
'label' => ' Production Host ',
'username' => ' deploy ',
'notes' => ' SSH access ',
'secret' => $secret,
'internal_token' => 'do not expose',
];
$encrypted = $vault->encryptCredential($credential);
if (array_key_exists('secret', $encrypted) || !isset($encrypted['secret_ciphertext'])) {
throw new RuntimeException('Stored credential records must contain ciphertext, not plaintext.');
}
credential_assert_same($secret, $vault->decryptCredential($encrypted)['secret'], 'Credential records must decrypt their secret.');
$projection = $vault->projectMetadata($encrypted);
credential_assert_same(['id' => 7, 'category' => 'hosting', 'label' => 'Production Host', 'username' => 'deploy', 'notes' => 'SSH access'], $projection, 'Metadata projection must be allow-listed and plaintext-free.');
$info = new TechnicalInformation();
$valid = $info->validate(['category' => 'vpn', 'label' => ' Office VPN ', 'username' => ' alice ', 'notes' => ' MFA enabled ']);
credential_assert_same(true, $valid['valid'], 'Valid technical information should pass.');
credential_assert_same('Office VPN', $valid['label'], 'Labels should be normalized.');
$invalid = $info->validate(['category' => 'unknown', 'label' => ' ', 'username' => "bad\nname", 'notes' => str_repeat('x', 2001)]);
if ($invalid['valid'] || !isset($invalid['errors']['category'], $invalid['errors']['label'], $invalid['errors']['username'], $invalid['errors']['notes'])) {
throw new RuntimeException('Technical information must validate category, label, username, and notes.');
}
foreach (['missing' => null, 'short' => 'short', 'placeholder' => 'generate-a-long-random-secret'] as $name => $badKey) {
try {
new CredentialVault($badKey);
throw new RuntimeException("{$name} APP_KEY material should fail explicitly.");
} catch (RuntimeException $expected) {
if (!str_contains($expected->getMessage(), 'APP_KEY')) {
throw new RuntimeException("{$name} APP_KEY error should mention APP_KEY.");
}
}
}
printf("Credential vault tests: 6 passed\n");