252 lines
10 KiB
JavaScript
252 lines
10 KiB
JavaScript
/*
|
|
* vault1984 — crypto test suite
|
|
* Runs in both QuickJS (CLI) and browser.
|
|
*
|
|
* CLI: vault1984-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(vault1984.crypto.encrypt_field(K8, 'username', 'johanj'), function(ct, err) {
|
|
if (err) { fail(name, err.message); done(); return; }
|
|
resolve(vault1984.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(vault1984.crypto.encrypt_field(K16, 'password', 's3cret!P@ss'), function(ct, err) {
|
|
if (err) { fail(name, err.message); done(); return; }
|
|
resolve(vault1984.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(vault1984.crypto.encrypt_field(K32, 'passport', 'NL12345678'), function(ct, err) {
|
|
if (err) { fail(name, err.message); done(); return; }
|
|
resolve(vault1984.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(vault1984.crypto.encrypt_field(K32, 'passport', 'NL12345678'), function(ct, err) {
|
|
if (err) { fail(name, 'encrypt failed: ' + err.message); done(); return; }
|
|
safe(function() { return vault1984.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(vault1984.crypto.encrypt_field(K16, 'password', 'secret'), function(ct, err) {
|
|
if (err) { fail(name, 'encrypt failed: ' + err.message); done(); return; }
|
|
safe(function() { return vault1984.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(vault1984.crypto.encrypt_field(K16, 'secret', 'value'), function(ct, err) {
|
|
if (err) { fail(name, 'encrypt failed'); done(); return; }
|
|
safe(function() { return vault1984.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(vault1984.crypto.encrypt_field(K16, 'labelA', 'value'), function(ct, err) {
|
|
if (err) { fail(name, 'encrypt failed'); done(); return; }
|
|
safe(function() { return vault1984.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(vault1984.crypto.encrypt_field(K16, 'empty', ''), function(ct, err) {
|
|
if (err) { fail(name, err.message); done(); return; }
|
|
resolve(vault1984.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(vault1984.crypto.encrypt_field(K16, 'intl', unicode), function(ct, err) {
|
|
if (err) { fail(name, err.message); done(); return; }
|
|
resolve(vault1984.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(vault1984.crypto.encrypt_field(K8, 'field', 'test'), function(ct1, err) {
|
|
if (err) { fail(name, 'L1 encrypt: ' + err.message); done(); return; }
|
|
safe(function() { return vault1984.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 = vault1984.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 = vault1984.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();
|
|
});
|
|
});
|
|
|
|
/* --- 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._v1984_test_result = summary;
|
|
return;
|
|
}
|
|
tests[idx](function() { run(idx + 1); });
|
|
}
|
|
|
|
run(0);
|
|
|
|
/* For sync environments (QuickJS), result is available immediately */
|
|
if (typeof globalThis._v1984_test_result !== 'undefined') {
|
|
/* Used by CLI eval */
|
|
}
|
|
})();
|
|
|
|
/* Return result for jsbridge_eval */
|
|
globalThis._v1984_test_result || 'RUNNING (async — check console)';
|