clavitor/clavis/clavis-crypto/test_crypto.js

309 lines
13 KiB
JavaScript

/*
* clavitor — crypto test suite
* Runs in both QuickJS (CLI) and browser.
*
* CLI: clavitor-cli test-roundtrip
* Web: open browser console, paste: fetch('/app/test_crypto.js').then(r=>r.text()).then(eval)
* or load as <script> in a test page
*
* All tests must produce identical results on both platforms.
* If any test fails on one but passes on the other, the shared crypto is broken.
*/
(function() {
var R = []; /* results */
var FAIL = false;
function pass(name) { R.push('PASS ' + name); }
function fail(name, detail) { R.push('FAIL ' + name + (detail ? ' — ' + detail : '')); FAIL = true; }
/* Wrap a test that might be sync (QuickJS) or async (browser) */
function resolve(val, cb) {
if (val && typeof val.then === 'function') {
val.then(function(r) { cb(r); }).catch(function(e) { cb(null, e); });
} else {
cb(val);
}
}
/* Safe call: catches sync throws (QuickJS) and async rejections (browser) */
function safe(fn, cb) {
try {
var result = fn();
if (result && typeof result.then === 'function') {
result.then(function(r) { cb(r); }).catch(function(e) { cb(null, e); });
} else {
cb(result);
}
} catch(e) {
cb(null, e);
}
}
/* --- Key fixtures --- */
var K8 = new Uint8Array([11,22,33,44,55,66,77,88]);
var K16 = new Uint8Array([11,22,33,44,55,66,77,88,99,110,111,112,113,114,115,116]);
var K32 = new Uint8Array([11,22,33,44,55,66,77,88,99,110,111,112,113,114,115,116,
201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216]);
/* K16 is the first 16 bytes of K32 — by design (truncation model) */
/* K8 is the first 8 bytes of K16 — by design */
var WRONG_K16 = new Uint8Array([255,22,33,44,55,66,77,88,99,110,111,112,113,114,115,116]);
var tests = [];
/* --- Test 1: L1 (8-byte) roundtrip --- */
tests.push(function(done) {
var name = 'L1 (8B) encrypt/decrypt roundtrip';
resolve(clavitor.crypto.encrypt_field(K8, 'username', 'johanj'), function(ct, err) {
if (err) { fail(name, err.message); done(); return; }
resolve(clavitor.crypto.decrypt_field(K8, 'username', ct), function(pt, err2) {
if (err2) fail(name, err2.message);
else if (pt === 'johanj') pass(name);
else fail(name, 'got "' + pt + '"');
done();
});
});
});
/* --- Test 2: L2 (16-byte) roundtrip --- */
tests.push(function(done) {
var name = 'L2 (16B) encrypt/decrypt roundtrip';
resolve(clavitor.crypto.encrypt_field(K16, 'password', 's3cret!P@ss'), function(ct, err) {
if (err) { fail(name, err.message); done(); return; }
resolve(clavitor.crypto.decrypt_field(K16, 'password', ct), function(pt, err2) {
if (err2) fail(name, err2.message);
else if (pt === 's3cret!P@ss') pass(name);
else fail(name, 'got "' + pt + '"');
done();
});
});
});
/* --- Test 3: L3 (32-byte) roundtrip --- */
tests.push(function(done) {
var name = 'L3 (32B) encrypt/decrypt roundtrip';
resolve(clavitor.crypto.encrypt_field(K32, 'passport', 'NL12345678'), function(ct, err) {
if (err) { fail(name, err.message); done(); return; }
resolve(clavitor.crypto.decrypt_field(K32, 'passport', ct), function(pt, err2) {
if (err2) fail(name, err2.message);
else if (pt === 'NL12345678') pass(name);
else fail(name, 'got "' + pt + '"');
done();
});
});
});
/* --- Test 4: L2 key cannot decrypt L3 ciphertext --- */
tests.push(function(done) {
var name = 'L2 key rejects L3 ciphertext';
resolve(clavitor.crypto.encrypt_field(K32, 'passport', 'NL12345678'), function(ct, err) {
if (err) { fail(name, 'encrypt failed: ' + err.message); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(K16, 'passport', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'L2 key decrypted L3 data to "' + pt + '"');
done();
});
});
});
/* --- Test 5: L3 key cannot decrypt L2 ciphertext --- */
tests.push(function(done) {
var name = 'L3 key rejects L2 ciphertext';
resolve(clavitor.crypto.encrypt_field(K16, 'password', 'secret'), function(ct, err) {
if (err) { fail(name, 'encrypt failed: ' + err.message); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(K32, 'password', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'L3 key decrypted L2 data to "' + pt + '"');
done();
});
});
});
/* --- Test 6: Wrong key rejection --- */
tests.push(function(done) {
var name = 'wrong key rejected';
resolve(clavitor.crypto.encrypt_field(K16, 'secret', 'value'), function(ct, err) {
if (err) { fail(name, 'encrypt failed'); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(WRONG_K16, 'secret', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'wrong key decrypted to "' + pt + '"');
done();
});
});
});
/* --- Test 7: Wrong label rejection --- */
tests.push(function(done) {
var name = 'wrong label rejected';
resolve(clavitor.crypto.encrypt_field(K16, 'labelA', 'value'), function(ct, err) {
if (err) { fail(name, 'encrypt failed'); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(K16, 'labelB', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'wrong label decrypted to "' + pt + '"');
done();
});
});
});
/* --- Test 8: Empty string roundtrip --- */
tests.push(function(done) {
var name = 'empty string roundtrip';
resolve(clavitor.crypto.encrypt_field(K16, 'empty', ''), function(ct, err) {
if (err) { fail(name, err.message); done(); return; }
resolve(clavitor.crypto.decrypt_field(K16, 'empty', ct), function(pt, err2) {
if (err2) fail(name, err2.message);
else if (pt === '') pass(name);
else fail(name, 'got "' + pt + '"');
done();
});
});
});
/* --- Test 9: Unicode roundtrip --- */
tests.push(function(done) {
var name = 'unicode roundtrip';
var unicode = 'pässwörd 密码 🔑';
resolve(clavitor.crypto.encrypt_field(K16, 'intl', unicode), function(ct, err) {
if (err) { fail(name, err.message); done(); return; }
resolve(clavitor.crypto.decrypt_field(K16, 'intl', ct), function(pt, err2) {
if (err2) fail(name, err2.message);
else if (pt === unicode) pass(name);
else fail(name, 'mismatch');
done();
});
});
});
/* --- Test 10: L1 key (8B) produces different ciphertext than L2 key (16B) --- */
tests.push(function(done) {
var name = 'L1 and L2 keys produce different ciphertexts';
/* K8 doubled = [11,22,33,44,55,66,77,88,11,22,33,44,55,66,77,88]
* K16 = [11,22,33,44,55,66,77,88,99,110,111,112,113,114,115,116]
* These are different keys after normalization, so HKDF produces different field keys.
* L1 ciphertext must NOT be decryptable with L2 key. */
resolve(clavitor.crypto.encrypt_field(K8, 'field', 'test'), function(ct1, err) {
if (err) { fail(name, 'L1 encrypt: ' + err.message); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(K16, 'field', ct1); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'L2 key decrypted L1 data');
done();
});
});
});
/* --- Test 11: TOTP generation (RFC 6238 test vector) --- */
tests.push(function(done) {
var name = 'TOTP RFC 6238 test vector';
var result = clavitor.totp.generate_totp('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 59, 30, 6);
resolve(result, function(code, err) {
if (err) fail(name, err.message);
else if (code === '287082') pass(name);
else fail(name, 'got ' + code + ', expected 287082');
done();
});
});
/* --- Test 12: TOTP second test vector (time=1111111109) --- */
tests.push(function(done) {
var name = 'TOTP RFC 6238 test vector #2';
var result = clavitor.totp.generate_totp('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 1111111109, 30, 8);
resolve(result, function(code, err) {
if (err) fail(name, err.message);
else if (code === '07081804') pass(name);
else fail(name, 'got ' + code + ', expected 07081804');
done();
});
});
/* --- Test 13: L3 TOTP seed cannot be read with L2 key --- */
tests.push(function(done) {
var name = 'L3 TOTP seed inaccessible to agent (L2 key)';
var seed = 'JBSWY3DPEHPK3PXP'; // a TOTP seed
resolve(clavitor.crypto.encrypt_field(K32, 'totp', seed), function(ct, err) {
if (err) { fail(name, 'encrypt failed'); done(); return; }
// Agent has K16 (L2), tries to decrypt L3 TOTP seed
safe(function() { return clavitor.crypto.decrypt_field(K16, 'totp', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'L2 key decrypted L3 TOTP seed to "' + pt + '"');
done();
});
});
});
/* --- Test 14: L2 TOTP seed IS accessible with L2 key --- */
tests.push(function(done) {
var name = 'L2 TOTP seed accessible to agent (L2 key)';
var seed = 'JBSWY3DPEHPK3PXP';
resolve(clavitor.crypto.encrypt_field(K16, 'totp', seed), function(ct, err) {
if (err) { fail(name, 'encrypt failed'); done(); return; }
resolve(clavitor.crypto.decrypt_field(K16, 'totp', ct), function(pt, err2) {
if (err2) fail(name, err2.message);
else if (pt === seed) pass(name);
else fail(name, 'got "' + pt + '"');
done();
});
});
});
/* --- Test 15: L3 card number cannot be read with L2 key --- */
tests.push(function(done) {
var name = 'L3 card number inaccessible to agent (L2 key)';
resolve(clavitor.crypto.encrypt_field(K32, 'Number', '5452120017212208'), function(ct, err) {
if (err) { fail(name, 'encrypt failed'); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(K16, 'Number', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'L2 key decrypted L3 card number');
done();
});
});
});
/* --- Test 16: Truncation model — L2 key is prefix of L3 but cannot derive L3 --- */
tests.push(function(done) {
var name = 'truncation: L2 prefix of L3, still cannot decrypt L3';
// K16 = K32[0..16] by design. Encrypt with full K32, try with K16.
resolve(clavitor.crypto.encrypt_field(K32, 'secret', 'classified'), function(ct, err) {
if (err) { fail(name, 'encrypt failed'); done(); return; }
safe(function() { return clavitor.crypto.decrypt_field(K16, 'secret', ct); }, function(pt, err2) {
if (err2) pass(name);
else fail(name, 'L2 (prefix of L3) decrypted L3 data');
done();
});
});
});
/* --- Runner --- */
function run(idx) {
if (idx >= tests.length) {
/* Summary */
var summary = '\n' + R.join('\n') + '\n\n' +
(FAIL ? 'FAILED' : 'ALL ' + tests.length + ' TESTS PASSED');
if (typeof globalThis.document !== 'undefined') {
/* Browser: log to console */
console.log(summary);
R.forEach(function(line) {
if (line.indexOf('FAIL') === 0) console.error(line);
else console.log(line);
});
}
/* Return result string (for QuickJS eval or browser display) */
globalThis._clavitor_test_result = summary;
return;
}
tests[idx](function() { run(idx + 1); });
}
run(0);
/* For sync environments (QuickJS), result is available immediately */
if (typeof globalThis._clavitor_test_result !== 'undefined') {
/* Used by CLI eval */
}
})();
/* Return result for jsbridge_eval */
globalThis._clavitor_test_result || 'RUNNING (async — check console)';