// test/proba.mjs - proba Aere PQ KMS, fara framework. // // Fiecare afirmatie are perechea ei negativa, iar refuzurile se judeca dupa MOTIV (codul // erorii), nu doar dupa faptul ca a aruncat ceva. Interoperabilitatea se masoara cu o a doua // implementare a formatului, scrisa aici independent (HKDF de mana, peste HMAC din node:crypto), // care decapsuleaza si construieste plicuri direct cu primitivele din node:crypto. // // Iesire: tabel pe stdout; cod 0 daca toate trec, 1 daca una pica, 2 daca harnessul a cazut. // Cu AERE_KMS_PROBA_JSON= scrie si rezultatul complet ca JSON (il citeste // test/control-negativ.mjs; "completed: true" inseamna ca s-a ajuns la capat). import crypto from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import http from 'node:http'; import { spawn, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { openKms, KmsError } from '../kms.mjs'; import { createServer, configFromEnv } from '../server.mjs'; const HERE = path.dirname(fileURLToPath(import.meta.url)); const SERVER_PATH = path.join(HERE, '..', 'server.mjs'); // ---------------------------------------------------------------------------------- harness const probe = []; function proba(nume, fn) { if (probe.some((p) => p.nume === nume)) throw new Error(`proba dublata: ${nume}`); probe.push({ nume, fn }); } class Esec extends Error {} function adevarat(c, ce) { if (!c) throw new Esec(ce); } function egal(a, b, ce) { if (a !== b) throw new Esec(`${ce}: asteptat ${JSON.stringify(b)}, primit ${JSON.stringify(a)}`); } function refuza(fn, cod, ce) { try { fn(); } catch (e) { if (!(e instanceof KmsError)) throw new Esec(`${ce}: a aruncat altceva decat KmsError: ${e && e.message}`); if (e.code !== cod) throw new Esec(`${ce}: motiv asteptat ${cod}, primit ${e.code} (${e.message})`); return e; } throw new Esec(`${ce}: trebuia refuzat cu ${cod}, a trecut`); } const tmpDirs = []; function tmpDir(tag = 'd') { const d = fs.mkdtempSync(path.join(os.tmpdir(), `aerekms-proba-${tag}-`)); tmpDirs.push(d); return d; } function rootHex() { return crypto.randomBytes(32).toString('hex'); } function nouKms(dir = tmpDir(), root = rootHex()) { return { kms: openKms({ dataDir: dir, rootKey: root }), dir, root }; } // ---------------------------------------------------------------------------------- a doua implementare a formatului const L = { fp: 'aerekms/v1/fingerprint', kem: 'aerekms/v1/hybrid-kem', commit: 'aerekms/v1/commit', aad: 'aerekms/v1/aad', dek: 'aerekms/v1/dek', sig: 'aerekms/v1/hybrid-sig', sigAlg: 'ed25519+ml-dsa-65', sigCtx: 'aerekms/v1', }; const OFF = { ver: 5, fp: 9, ePub: 17, ctK: 49, aadTag: 1137, commit: 1153, payload: 1169 }; const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; }; const sha256 = (...p) => { const h = crypto.createHash('sha256'); p.forEach((x) => h.update(x)); return h.digest(); }; const hmac = (k, ...p) => { const h = crypto.createHmac('sha256', k); p.forEach((x) => h.update(x)); return h.digest(); }; // HKDF-SHA-256 (RFC 5869) scris de mana, nu crypto.hkdfSync: a doua implementare, nu aceeasi. function hkdfManual(ikm, salt, info, len) { const prk = hmac(salt, ikm); const out = []; let t = Buffer.alloc(0); for (let i = 1; out.reduce((s, b) => s + b.length, 0) < len; i++) { t = hmac(prk, t, info, Buffer.from([i])); out.push(t); } return Buffer.concat(out).subarray(0, len); } const x25519Pub = (raw) => crypto.createPublicKey({ key: { kty: 'OKP', crv: 'X25519', x: Buffer.from(raw).toString('base64url') }, format: 'jwk' }); const ed25519Pub = (raw) => crypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: Buffer.from(raw).toString('base64url') }, format: 'jwk' }); const pkcs8 = (b64) => crypto.createPrivateKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'pkcs8' }); const spki = (b64) => crypto.createPublicKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'spki' }); function deriveManual({ name, ver, fp, xPubRaw, ekRaw, ePub, ctK, ikm, aad }) { const tr = Buffer.concat([Buffer.from(L.kem + '\0'), u32(Buffer.byteLength(name)), Buffer.from(name), u32(ver), fp, xPubRaw, sha256(ekRaw), ePub, ctK]); const salt = sha256(tr); const commitKey = hkdfManual(ikm, salt, Buffer.from(L.commit), 32); const aadKey = hkdfManual(ikm, salt, Buffer.from(L.aad), 32); const dek = hkdfManual(ikm, salt, Buffer.concat([Buffer.from(L.dek + '\0'), sha256(aad)]), 44); return { commitKey, aadTag: hmac(aadKey, aad).subarray(0, 16), key: dek.subarray(0, 32), iv: dek.subarray(32, 44) }; } function parseManual(ct) { const m = /^aerekms:v(\d+):(.+)$/.exec(ct); if (!m) throw new Error('forma'); const body = Buffer.from(m[2], 'base64'); return { prefixVer: Number(m[1]), body, ver: body.readUInt32BE(OFF.ver), fp: body.subarray(OFF.fp, OFF.fp + 8), ePub: body.subarray(OFF.ePub, OFF.ePub + 32), ctK: body.subarray(OFF.ctK, OFF.ctK + 1088), aadTag: body.subarray(OFF.aadTag, OFF.aadTag + 16), commit: body.subarray(OFF.commit, OFF.commit + 16), }; } // Deschide un plic cu un IKM dat (ambele secrete, sau doar unul, pentru proba atacatorului). function openManual(ct, { name, pubVer, ikm, aad }) { const p = parseManual(ct); const xPubRaw = Buffer.from(pubVer.public.x25519, 'base64'); const ekRaw = Buffer.from(pubVer.public.ml_kem_768, 'base64').subarray(22); const d = deriveManual({ name, ver: p.ver, fp: p.fp, xPubRaw, ekRaw, ePub: p.ePub, ctK: p.ctK, ikm, aad }); const commit = hmac(d.commitKey, p.body.subarray(0, OFF.commit)).subarray(0, 16); if (!commit.equals(p.commit)) throw new Error('commit'); if (!d.aadTag.equals(p.aadTag)) throw new Error('aad'); const dc = crypto.createDecipheriv('aes-256-gcm', d.key, d.iv); dc.setAAD(p.body.subarray(0, OFF.payload)); dc.setAuthTag(p.body.subarray(p.body.length - 16)); return Buffer.concat([dc.update(p.body.subarray(OFF.payload, p.body.length - 16)), dc.final()]); } // Secretele unui plic, calculate cu cheile private exportate (direct cu node:crypto). function secreteManual(ct, exp) { const p = parseManual(ct); const ssX = crypto.diffieHellman({ privateKey: pkcs8(exp.private.x25519_pkcs8), publicKey: x25519Pub(p.ePub) }); const ssK = crypto.decapsulate(pkcs8(exp.private.ml_kem_768_pkcs8), p.ctK); return { ssX, ssK }; } // Construieste un plic numai din cheile publice. mod: 'ambele' | 'doar-x' | 'doar-k'. function sealManual({ name, ver, pubVer, pt, aad, mod = 'ambele' }) { const xPubRaw = Buffer.from(pubVer.public.x25519, 'base64'); const kSpki = Buffer.from(pubVer.public.ml_kem_768, 'base64'); const ekRaw = kSpki.subarray(22); const fp = Buffer.from(pubVer.fingerprint, 'hex'); const eph = crypto.generateKeyPairSync('x25519'); const ePub = Buffer.from(eph.publicKey.export({ format: 'jwk' }).x, 'base64url'); const ssX = crypto.diffieHellman({ privateKey: eph.privateKey, publicKey: x25519Pub(xPubRaw) }); const { sharedKey: ssK, ciphertext: ctK } = crypto.encapsulate(crypto.createPublicKey({ key: kSpki, format: 'der', type: 'spki' })); const ikm = mod === 'ambele' ? Buffer.concat([ssK, ssX]) : mod === 'doar-x' ? ssX : ssK; const d = deriveManual({ name, ver, fp, xPubRaw, ekRaw, ePub, ctK, ikm, aad }); const pre = Buffer.concat([Buffer.from('AKM1'), Buffer.from([0x45]), u32(ver), fp, ePub, ctK, d.aadTag]); const hdr = Buffer.concat([pre, hmac(d.commitKey, pre).subarray(0, 16)]); const c = crypto.createCipheriv('aes-256-gcm', d.key, d.iv); c.setAAD(hdr); const body = Buffer.concat([hdr, c.update(pt), c.final(), c.getAuthTag()]); return `aerekms:v${ver}:${body.toString('base64')}`; } function reencode(ct, fn) { const p = parseManual(ct); const body = Buffer.from(p.body); const prefixVer = fn(body) ?? p.prefixVer; return `aerekms:v${prefixVer}:${body.toString('base64')}`; } function sigMessageManual(fpHex, ver, msg) { return Buffer.concat([Buffer.from(L.sig + '\0' + L.sigAlg + '\0'), Buffer.from(fpHex, 'hex'), u32(ver), Buffer.from(msg)]); } function sigParts(sig) { const m = /^aerekms:v(\d+):(.+)$/.exec(sig); const body = Buffer.from(m[2], 'base64'); return { body, ver: body.readUInt32BE(5), ed: body.subarray(17, 81), ml: body.subarray(81) }; } function sigWith(sig, fn) { const m = /^aerekms:v(\d+):(.+)$/.exec(sig); const body = Buffer.from(m[2], 'base64'); fn(body); return `aerekms:v${m[1]}:${body.toString('base64')}`; } // ---------------------------------------------------------------------------------- scanerul de material privat // Cauta octetii ac in fisier si in tot ce se poate decoda din el (base64, hex, JSON), recursiv. function haystacks(buf, depth = 0, out = []) { out.push(buf); if (depth > 4) return out; const text = buf.toString('utf8'); let obj; try { obj = JSON.parse(text); } catch { obj = undefined; } const strings = []; const walk = (v) => { if (typeof v === 'string') strings.push(v); else if (v && typeof v === 'object') Object.values(v).forEach(walk); }; if (obj !== undefined) walk(obj); else strings.push(text); for (const s of strings) { if (s.length >= 16 && /^[A-Za-z0-9+/]+={0,2}$/.test(s)) haystacks(Buffer.from(s, 'base64'), depth + 1, out); if (s.length >= 16 && /^[A-Za-z0-9_-]+$/.test(s)) haystacks(Buffer.from(s, 'base64url'), depth + 1, out); if (s.length >= 16 && /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0) haystacks(Buffer.from(s, 'hex'), depth + 1, out); } return out; } function needlesDin(exp) { const n = []; for (const b64 of Object.values(exp.private)) { const der = Buffer.from(b64, 'base64'); n.push(der); n.push(der.subarray(der.length - 32)); // scalar X25519 / samanta Ed25519 / coada cheii PQ if (der.length > 200) n.push(der.subarray(100, 132)); } return n; } function gasesteMaterial(files, needles) { const gasite = []; for (const f of files) { const hs = haystacks(fs.readFileSync(f)); for (const nd of needles) if (hs.some((h) => h.indexOf(nd) !== -1)) gasite.push(path.basename(f)); } return gasite; } function toateFisierele(dir) { const out = []; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, e.name); if (e.isDirectory()) out.push(...toateFisierele(p)); else out.push(p); } return out; } // ---------------------------------------------------------------------------------- HTTP function cerere(port, method, p, { headers = {}, body, chunked = false } = {}) { return new Promise((resolve, reject) => { const data = body === undefined ? null : Buffer.isBuffer(body) ? body : Buffer.from(typeof body === 'string' ? body : JSON.stringify(body)); const h = { ...headers }; if (data && !chunked) h['content-length'] = data.length; if (data && h['content-type'] === undefined) h['content-type'] = 'application/json'; const req = http.request({ host: '127.0.0.1', port, method, path: p, headers: h, agent: false }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const text = Buffer.concat(chunks).toString('utf8'); let json = null; try { json = JSON.parse(text); } catch { json = null; } resolve({ status: res.statusCode, json, text }); }); }); req.on('error', reject); if (data) { if (chunked) for (let i = 0; i < data.length; i += 1024) req.write(data.subarray(i, i + 1024)); else req.write(data); } req.end(); }); } async function cuServer(fn, { maxBody = 4096 } = {}) { const { kms, dir } = nouKms(); const token = crypto.randomBytes(24).toString('base64url') + 'x'.repeat(8); const server = createServer({ kms, token, maxBody }); await new Promise((r) => server.listen(0, '127.0.0.1', r)); const port = server.address().port; const auth = { authorization: `Bearer ${token}` }; try { await fn({ port, kms, token, auth, dir }); } finally { await new Promise((r) => server.close(r)); kms.close(); } } const b64 = (s) => Buffer.from(s).toString('base64'); const envCurat = () => Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('AERE_KMS_'))); // ================================================================================== PROBELE proba('mediu: node:crypto are ml-kem-768, ml-dsa-65, x25519, ed25519', () => { const k = crypto.generateKeyPairSync('ml-kem-768'); const { sharedKey, ciphertext } = crypto.encapsulate(k.publicKey); adevarat(crypto.decapsulate(k.privateKey, ciphertext).equals(sharedKey), 'ML-KEM-768 dus-intors'); const d = crypto.generateKeyPairSync('ml-dsa-65'); const s = crypto.sign(null, Buffer.from('m'), d.privateKey); adevarat(crypto.verify(null, Buffer.from('m'), d.publicKey, s), 'ML-DSA-65 verifica'); adevarat(!crypto.verify(null, Buffer.from('n'), d.publicKey, s), 'ML-DSA-65 refuza alt mesaj'); egal(crypto.generateKeyPairSync('x25519').publicKey.asymmetricKeyType, 'x25519', 'x25519'); egal(crypto.generateKeyPairSync('ed25519').publicKey.asymmetricKeyType, 'ed25519', 'ed25519'); }); // ---------------------------------------------------------------------------------- cheia radacina proba('radacina: cheie valida porneste serviciul', () => { const { kms, dir } = nouKms(); adevarat(fs.existsSync(path.join(dir, 'root-check.json')), 'root-check.json scris'); kms.createKey('k', { type: 'encrypt' }); kms.close(); }); proba('radacina: lipsa cheii refuza pornirea cu ROOT_KEY_MISSING si nu creeaza nimic pe disc', () => { for (const v of [undefined, null, '', ' ', '\r\n']) { const dir = path.join(tmpDir('lipsa'), 'nu-exista'); refuza(() => openKms({ dataDir: dir, rootKey: v }), 'ROOT_KEY_MISSING', `rootKey=${JSON.stringify(v)}`); adevarat(!fs.existsSync(dir), 'dosarul de date nu trebuie creat fara cheie radacina'); } }); proba('radacina: cheie malformata refuzata cu ROOT_KEY_INVALID fara sa tipareasca valoarea', () => { const valid = rootHex(); for (const v of ['zz' + valid.slice(2), valid.slice(1), valid + 'a', 'nu-e-hex-deloc-nu-e-hex-deloc-nu-e-hex-deloc-nu-e-hex-deloc-1234']) { const e = refuza(() => openKms({ dataDir: tmpDir(), rootKey: v }), 'ROOT_KEY_INVALID', 'forma'); adevarat(!e.message.includes(v) && !JSON.stringify({ ...e }).includes(v), 'mesajul contine valoarea cheii'); adevarat(!e.message.includes(v.slice(0, 16)), 'mesajul contine un prefix al cheii'); } }); proba('radacina: cheie de zerouri refuzata cu ROOT_KEY_WEAK', () => { refuza(() => openKms({ dataDir: tmpDir(), rootKey: '0'.repeat(64) }), 'ROOT_KEY_WEAK', 'zerouri'); }); proba('radacina: CRLF la coada e taiat, aceeasi cheie redeschide depozitul', () => { const root = rootHex(); const dir = tmpDir(); const a = openKms({ dataDir: dir, rootKey: root }); const c = a.encrypt(a.createKey('k', { type: 'encrypt' }).name, 'x').ciphertext; a.close(); const b = openKms({ dataDir: dir, rootKey: root.toUpperCase() + '\r\n' }); egal(b.decrypt('k', c).plaintext.toString(), 'x', 'decriptare dupa redeschidere'); b.close(); }); proba('radacina: alta cheie radacina pe acelasi depozit refuzata cu ROOT_KEY_MISMATCH', () => { const { kms, dir } = nouKms(); kms.createKey('k', { type: 'encrypt' }); kms.close(); refuza(() => openKms({ dataDir: dir, rootKey: rootHex() }), 'ROOT_KEY_MISMATCH', 'alta radacina'); }); proba('radacina: depozit cu chei dar fara root-check.json refuzat cu ROOT_CHECK_MISSING', () => { const { kms, dir, root } = nouKms(); kms.createKey('k', { type: 'encrypt' }); kms.close(); fs.rmSync(path.join(dir, 'root-check.json')); refuza(() => openKms({ dataDir: dir, rootKey: root }), 'ROOT_CHECK_MISSING', 'fara root-check'); }); // ---------------------------------------------------------------------------------- stocare proba('stocare: cheia se reincarca dupa repornire, decripteaza si verifica', () => { const { kms, dir, root } = nouKms(); kms.createKey('e', { type: 'encrypt' }); kms.createKey('s', { type: 'sign' }); const c = kms.encrypt('e', 'text', 'ctx').ciphertext; const s = kms.sign('s', 'mesaj').signature; kms.close(); const k2 = openKms({ dataDir: dir, rootKey: root }); egal(k2.decrypt('e', c, 'ctx').plaintext.toString(), 'text', 'decriptare dupa repornire'); egal(k2.verify('s', 'mesaj', s).valid, true, 'verificare dupa repornire'); k2.close(); }); proba('stocare: materialul privat nu apare in clar pe disc (cu controlul pozitiv al scanerului)', () => { const { kms, dir } = nouKms(); kms.createKey('e', { type: 'encrypt', exportable: true }); kms.createKey('s', { type: 'sign', exportable: true }); kms.encrypt('e', 'x'); kms.sign('s', 'x'); const needles = [...needlesDin(kms.exportKey('e')), ...needlesDin(kms.exportKey('s'))]; // control pozitiv: scanerul gaseste materialul ascuns pe doua niveluri (base64 de JSON cu base64) const exp = kms.exportKey('e'); const momeala = path.join(tmpDir('momeala'), 'm.json'); fs.writeFileSync(momeala, JSON.stringify({ x: Buffer.from(JSON.stringify({ a: exp.private.x25519_pkcs8 })).toString('base64') })); adevarat(gasesteMaterial([momeala], needles).length > 0, 'CONTROL POZITIV: scanerul nu gaseste materialul plantat, deci nu masoara nimic'); const fisiere = toateFisierele(dir); egal(fisiere.filter((f) => f.endsWith('.json')).length >= 3, true, 'fisierele de chei exista'); const gasite = gasesteMaterial(fisiere, needles); egal(gasite.length, 0, `material privat gasit in clar in: ${[...new Set(gasite)].join(', ')}`); kms.close(); }); proba('stocare: min_decryption_version coborat pe disc refuzat cu KEY_FILE_TAMPERED', () => { const { kms, dir, root } = nouKms(); kms.createKey('k', { type: 'encrypt' }); kms.rotate('k'); kms.setMinDecryptionVersion('k', 2); kms.close(); const f = path.join(dir, 'keys', 'k.json'); const rec = JSON.parse(fs.readFileSync(f, 'utf8')); egal(rec.min_decryption_version, 2, 'minimul scris'); rec.min_decryption_version = 1; fs.writeFileSync(f, JSON.stringify(rec, null, 2)); const k2 = openKms({ dataDir: dir, rootKey: root }); refuza(() => k2.getKey('k'), 'KEY_FILE_TAMPERED', 'minim coborat pe disc'); k2.close(); }); proba('stocare: cheie publica inlocuita pe disc refuzata cu KEY_FILE_TAMPERED', () => { const { kms, dir, root } = nouKms(); kms.createKey('k', { type: 'encrypt' }); kms.close(); const f = path.join(dir, 'keys', 'k.json'); const rec = JSON.parse(fs.readFileSync(f, 'utf8')); const atacator = crypto.generateKeyPairSync('x25519'); rec.versions['1'].public.x25519 = Buffer.from(atacator.publicKey.export({ format: 'jwk' }).x, 'base64url').toString('base64'); fs.writeFileSync(f, JSON.stringify(rec, null, 2)); const k2 = openKms({ dataDir: dir, rootKey: root }); refuza(() => k2.encrypt('k', 'x'), 'KEY_FILE_TAMPERED', 'cheie publica inlocuita'); k2.close(); }); // ---------------------------------------------------------------------------------- chei si politica proba('chei: nume valid acceptat, nume invalid refuzat cu INVALID_KEY_NAME', () => { const { kms } = nouKms(); kms.createKey('a-b_1', { type: 'encrypt' }); for (const n of ['../x', 'A', '', 'a/b', 'a'.repeat(65), '-a', 'a.json']) { refuza(() => kms.createKey(n, { type: 'encrypt' }), 'INVALID_KEY_NAME', `nume ${JSON.stringify(n)}`); } kms.close(); }); proba('chei: creare dubla refuzata cu KEY_EXISTS, cheie lipsa cu KEY_NOT_FOUND, tip invalid cu INVALID_KEY_TYPE', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); refuza(() => kms.createKey('k', { type: 'sign' }), 'KEY_EXISTS', 'dubla'); refuza(() => kms.encrypt('lipsa', 'x'), 'KEY_NOT_FOUND', 'lipsa'); refuza(() => kms.createKey('t', { type: 'aes' }), 'INVALID_KEY_TYPE', 'tip'); kms.close(); }); proba('chei: operatie pe tipul gresit de cheie refuzata cu WRONG_KEY_TYPE', () => { const { kms } = nouKms(); kms.createKey('e', { type: 'encrypt' }); kms.createKey('s', { type: 'sign' }); refuza(() => kms.encrypt('s', 'x'), 'WRONG_KEY_TYPE', 'encrypt cu cheie de semnare'); refuza(() => kms.sign('e', 'x'), 'WRONG_KEY_TYPE', 'sign cu cheie de criptare'); refuza(() => kms.datakey('s'), 'WRONG_KEY_TYPE', 'datakey cu cheie de semnare'); kms.close(); }); proba('politica: implicit neexportabila, exportul refuzat cu KEY_NOT_EXPORTABLE', () => { const { kms } = nouKms(); const d = kms.createKey('k', { type: 'encrypt' }); egal(d.policy.exportable, false, 'politica implicita'); refuza(() => kms.exportKey('k'), 'KEY_NOT_EXPORTABLE', 'export'); refuza(() => kms.createKey('z', { type: 'encrypt', exportable: 'da' }), 'INVALID_POLICY', 'exportable ne-boolean'); kms.close(); }); proba('politica: cheie exportabila exporta chei private care corespund cheilor publice', () => { const { kms } = nouKms(); const e = kms.createKey('e', { type: 'encrypt', exportable: true }); const s = kms.createKey('s', { type: 'sign', exportable: true }); const xe = kms.exportKey('e'); const xs = kms.exportKey('s'); const rawX = Buffer.from(crypto.createPublicKey(pkcs8(xe.private.x25519_pkcs8)).export({ format: 'jwk' }).x, 'base64url'); egal(rawX.toString('base64'), e.versions['1'].public.x25519, 'X25519 privata -> publica'); const kPub = crypto.createPublicKey(pkcs8(xe.private.ml_kem_768_pkcs8)).export({ format: 'der', type: 'spki' }); egal(kPub.toString('base64'), e.versions['1'].public.ml_kem_768, 'ML-KEM privata -> publica'); const rawEd = Buffer.from(crypto.createPublicKey(pkcs8(xs.private.ed25519_pkcs8)).export({ format: 'jwk' }).x, 'base64url'); egal(rawEd.toString('base64'), s.versions['1'].public.ed25519, 'Ed25519 privata -> publica'); const dPub = crypto.createPublicKey(pkcs8(xs.private.ml_dsa_65_pkcs8)).export({ format: 'der', type: 'spki' }); egal(dPub.toString('base64'), s.versions['1'].public.ml_dsa_65, 'ML-DSA privata -> publica'); kms.close(); }); // ---------------------------------------------------------------------------------- criptare proba('criptare: dus-intors cu si fara aad, formatul aerekms:v:, plicuri diferite', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const a = kms.encrypt('k', 'secret', 'ctx'); const b = kms.encrypt('k', 'secret', 'ctx'); adevarat(/^aerekms:v1:[A-Za-z0-9+/]+={0,2}$/.test(a.ciphertext), 'forma prefixului'); adevarat(a.ciphertext !== b.ciphertext, 'doua criptari ale aceluiasi text trebuie sa difere'); egal(kms.decrypt('k', a.ciphertext, 'ctx').plaintext.toString(), 'secret', 'cu aad'); const c = kms.encrypt('k', Buffer.alloc(0)); egal(kms.decrypt('k', c.ciphertext).plaintext.length, 0, 'text gol fara aad'); const mare = crypto.randomBytes(100000); adevarat(kms.decrypt('k', kms.encrypt('k', mare).ciphertext).plaintext.equals(mare), '100 KB'); kms.close(); }); proba('criptare: un octet schimbat in fiecare camp e refuzat cu motivul campului', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const ct = kms.encrypt('k', 'text cu ceva lungime', 'ctx').ciphertext; egal(kms.decrypt('k', ct, 'ctx').plaintext.toString(), 'text cu ceva lungime', 'plicul neatins trece'); const len = parseManual(ct).body.length; const campuri = [ ['magic', 0, 'MALFORMED_CIPHERTEXT'], ['tip', 4, 'MALFORMED_CIPHERTEXT'], ['versiune', 8, 'VERSION_MISMATCH'], ['amprenta', 9, 'KEY_MISMATCH'], ['X25519 efemer', OFF.ePub + 5, 'HEADER_AUTH_FAILED'], ['ML-KEM ct', OFF.ctK + 500, 'HEADER_AUTH_FAILED'], ['eticheta aad', OFF.aadTag + 3, 'HEADER_AUTH_FAILED'], ['angajament', OFF.commit + 3, 'HEADER_AUTH_FAILED'], ['continut', OFF.payload + 2, 'PAYLOAD_AUTH_FAILED'], ['eticheta GCM', len - 1, 'PAYLOAD_AUTH_FAILED'], ]; for (const [camp, poz, cod] of campuri) { const rau = reencode(ct, (b) => { b[poz] ^= 0x01; }); refuza(() => kms.decrypt('k', rau, 'ctx'), cod, `octet schimbat in ${camp} (pozitia ${poz})`); } // trunchiat sub antet: forma rupta; trunchiat in continut: GCM prinde lipsa const scurt = `aerekms:v1:${parseManual(ct).body.subarray(0, 1100).toString('base64')}`; refuza(() => kms.decrypt('k', scurt, 'ctx'), 'MALFORMED_CIPHERTEXT', 'trunchiat sub antet'); refuza(() => kms.decrypt('k', ct.slice(0, -8), 'ctx'), 'PAYLOAD_AUTH_FAILED', 'trunchiat in continut'); refuza(() => kms.decrypt('k', 'vault:v1:AAAA', 'ctx'), 'MALFORMED_CIPHERTEXT', 'alt prefix'); kms.close(); }); proba('criptare: ORICE octet schimbat (toate pozitiile) e refuzat', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const ct = kms.encrypt('k', 'abcdefgh', 'ctx').ciphertext; const len = parseManual(ct).body.length; const coduri = new Set(['MALFORMED_CIPHERTEXT', 'VERSION_MISMATCH', 'KEY_MISMATCH', 'UNKNOWN_VERSION', 'HEADER_AUTH_FAILED', 'AAD_MISMATCH', 'PAYLOAD_AUTH_FAILED']); let refuzate = 0; for (let i = 0; i < len; i++) { const rau = reencode(ct, (b) => { b[i] ^= 0x80; }); try { kms.decrypt('k', rau, 'ctx'); throw new Esec(`octetul ${i} schimbat a trecut decriptarea`); } catch (e) { if (e instanceof Esec) throw e; adevarat(e instanceof KmsError && coduri.has(e.code), `octetul ${i}: motiv neasteptat ${e.code}`); refuzate++; } } egal(refuzate, len, 'toate pozitiile refuzate'); kms.close(); }); proba('criptare: alt aad refuzat cu AAD_MISMATCH', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const cu = kms.encrypt('k', 'x', 'tenant-1').ciphertext; const fara = kms.encrypt('k', 'x').ciphertext; egal(kms.decrypt('k', cu, 'tenant-1').plaintext.toString(), 'x', 'aad corect'); refuza(() => kms.decrypt('k', cu, 'tenant-2'), 'AAD_MISMATCH', 'alt aad'); refuza(() => kms.decrypt('k', cu), 'AAD_MISMATCH', 'aad lipsa'); refuza(() => kms.decrypt('k', fara, 'tenant-1'), 'AAD_MISMATCH', 'aad in plus'); kms.close(); }); proba('criptare: alta cheie refuzata cu KEY_MISMATCH', () => { const { kms } = nouKms(); kms.createKey('a', { type: 'encrypt' }); const b = kms.createKey('b', { type: 'encrypt' }); const ct = kms.encrypt('a', 'x').ciphertext; egal(kms.decrypt('a', ct).plaintext.toString(), 'x', 'cheia proprie'); refuza(() => kms.decrypt('b', ct), 'KEY_MISMATCH', 'alta cheie'); // si cu amprenta rescrisa pe a cheii b: legatura criptografica tot refuza const fals = reencode(ct, (body) => { Buffer.from(b.versions['1'].fingerprint, 'hex').copy(body, OFF.fp); }); refuza(() => kms.decrypt('b', fals), 'HEADER_AUTH_FAILED', 'amprenta falsificata'); kms.close(); }); proba('criptare: alta versiune refuzata cu VERSION_MISMATCH', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const ct1 = kms.encrypt('k', 'x').ciphertext; const d2 = kms.rotate('k'); egal(kms.decrypt('k', ct1).plaintext.toString(), 'x', 'v1 decripteaza dupa rotire'); const doarPrefix = ct1.replace(/^aerekms:v1:/, 'aerekms:v2:'); refuza(() => kms.decrypt('k', doarPrefix), 'VERSION_MISMATCH', 'prefix schimbat'); const prefixSiCorp = reencode(ct1, (b) => { b.writeUInt32BE(2, OFF.ver); return 2; }); refuza(() => kms.decrypt('k', prefixSiCorp), 'VERSION_MISMATCH', 'prefix si corp schimbate, amprenta v1'); const totul = reencode(ct1, (b) => { b.writeUInt32BE(2, OFF.ver); Buffer.from(d2.versions['2'].fingerprint, 'hex').copy(b, OFF.fp); return 2; }); refuza(() => kms.decrypt('k', totul), 'HEADER_AUTH_FAILED', 'versiune si amprenta falsificate'); const inexistenta = reencode(ct1, (b) => { b.writeUInt32BE(7, OFF.ver); return 7; }); refuza(() => kms.decrypt('k', inexistenta), 'UNKNOWN_VERSION', 'versiune inexistenta'); kms.close(); }); proba('criptare: base64 necanonic refuzat cu MALFORMED_CIPHERTEXT', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const ct = kms.encrypt('k', 'a').ciphertext; // corp 1186 octeti -> "==" la coada const ALF = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; adevarat(ct.endsWith('=='), 'forma asteptata a probei (corp cu padding)'); const i = ct.length - 3; const rau = ct.slice(0, i) + ALF[ALF.indexOf(ct[i]) ^ 1] + ct.slice(i + 1); adevarat(Buffer.from(rau.split(':')[2], 'base64').equals(Buffer.from(ct.split(':')[2], 'base64')), 'CONTROL: variantele decodeaza la aceiasi octeti'); adevarat(rau !== ct, 'CONTROL: textul difera'); refuza(() => kms.decrypt('k', rau), 'MALFORMED_CIPHERTEXT', 'base64 necanonic'); kms.close(); }); proba('criptare: atacatorul cu doar secretul X25519 nu poate deriva cheia plicului', () => { const { kms } = nouKms(); const d = kms.createKey('k', { type: 'encrypt', exportable: true }); const exp = kms.exportKey('k'); const ct = kms.encrypt('k', 'tinta', 'ctx').ciphertext; const { ssX, ssK } = secreteManual(ct, exp); const z = Buffer.alloc(32); for (const ikm of [ssX, Buffer.concat([z, ssX]), Buffer.concat([ssX, z])]) { let deschis = false; try { openManual(ct, { name: 'k', pubVer: d.versions['1'], ikm, aad: Buffer.from('ctx') }); deschis = true; } catch { /* asteptat */ } adevarat(!deschis, 'plicul s-a deschis numai cu secretul X25519'); } // control pozitiv al metodei: cu AMBELE secrete, aceeasi cale deschide plicul egal(openManual(ct, { name: 'k', pubVer: d.versions['1'], ikm: Buffer.concat([ssK, ssX]), aad: Buffer.from('ctx') }).toString(), 'tinta', 'CONTROL POZITIV'); kms.close(); }); proba('criptare: atacatorul cu doar secretul ML-KEM nu poate deriva cheia plicului', () => { const { kms } = nouKms(); const d = kms.createKey('k', { type: 'encrypt', exportable: true }); const exp = kms.exportKey('k'); const ct = kms.encrypt('k', 'tinta', 'ctx').ciphertext; const { ssX, ssK } = secreteManual(ct, exp); const z = Buffer.alloc(32); for (const ikm of [ssK, Buffer.concat([ssK, z]), Buffer.concat([z, ssK])]) { let deschis = false; try { openManual(ct, { name: 'k', pubVer: d.versions['1'], ikm, aad: Buffer.from('ctx') }); deschis = true; } catch { /* asteptat */ } adevarat(!deschis, 'plicul s-a deschis numai cu secretul ML-KEM'); } egal(openManual(ct, { name: 'k', pubVer: d.versions['1'], ikm: Buffer.concat([ssK, ssX]), aad: Buffer.from('ctx') }).toString(), 'tinta', 'CONTROL POZITIV'); kms.close(); }); // ---------------------------------------------------------------------------------- interoperabilitate proba('interop: plicul serviciului decapsulat si decriptat manual cu node:crypto', () => { const { kms } = nouKms(); const d = kms.createKey('k', { type: 'encrypt', exportable: true }); const exp = kms.exportKey('k'); const ct = kms.encrypt('k', 'interoperabil', 'aad-1').ciphertext; const { ssX, ssK } = secreteManual(ct, exp); const pt = openManual(ct, { name: 'k', pubVer: d.versions['1'], ikm: Buffer.concat([ssK, ssX]), aad: Buffer.from('aad-1') }); egal(pt.toString(), 'interoperabil', 'decriptare manuala'); let err = ''; try { openManual(ct, { name: 'k', pubVer: d.versions['1'], ikm: Buffer.concat([ssK, ssX]), aad: Buffer.from('aad-2') }); } catch (e) { err = e.message; } egal(err, 'aad', 'implementarea manuala refuza alt aad pe eticheta aad'); kms.close(); }); proba('interop: plic construit manual din cheile publice e decriptat de serviciu', () => { const { kms } = nouKms(); const d = kms.createKey('k', { type: 'encrypt' }); const ct = sealManual({ name: 'k', ver: 1, pubVer: d.versions['1'], pt: Buffer.from('de la client'), aad: Buffer.from('a') }); egal(kms.decrypt('k', ct, 'a').plaintext.toString(), 'de la client', 'serviciul decripteaza plicul clientului'); refuza(() => kms.decrypt('k', ct, 'b'), 'AAD_MISMATCH', 'alt aad pe plicul clientului'); kms.close(); }); proba('interop: plic construit fara unul din secrete e refuzat de serviciu cu HEADER_AUTH_FAILED', () => { const { kms } = nouKms(); const d = kms.createKey('k', { type: 'encrypt' }); for (const mod of ['doar-x', 'doar-k']) { const ct = sealManual({ name: 'k', ver: 1, pubVer: d.versions['1'], pt: Buffer.from('x'), aad: Buffer.alloc(0), mod }); refuza(() => kms.decrypt('k', ct), 'HEADER_AUTH_FAILED', `KDF ${mod}`); } kms.close(); }); // ---------------------------------------------------------------------------------- rotire si minim proba('rotire: versiune noua, criptarea foloseste ultima, versiunile vechi decripteaza', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const c1 = kms.encrypt('k', 'unu').ciphertext; const r = kms.rotate('k'); egal(r.latest_version, 2, 'ultima versiune'); const c2 = kms.encrypt('k', 'doi').ciphertext; adevarat(c2.startsWith('aerekms:v2:'), 'criptarea foloseste v2'); egal(kms.decrypt('k', c1).plaintext.toString(), 'unu', 'v1 decripteaza'); egal(kms.decrypt('k', c2).plaintext.toString(), 'doi', 'v2 decripteaza'); adevarat(r.versions['1'].fingerprint !== r.versions['2'].fingerprint, 'amprente diferite'); kms.close(); }); proba('rotire: sub min_decryption_version decriptarea e refuzata cu VERSION_BELOW_MINIMUM', () => { const { kms, dir, root } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const c1 = kms.encrypt('k', 'unu').ciphertext; kms.rotate('k'); const c2 = kms.encrypt('k', 'doi').ciphertext; kms.setMinDecryptionVersion('k', 2); refuza(() => kms.decrypt('k', c1), 'VERSION_BELOW_MINIMUM', 'v1 sub minim'); refuza(() => kms.rewrap('k', c1), 'VERSION_BELOW_MINIMUM', 'rewrap v1 sub minim'); egal(kms.decrypt('k', c2).plaintext.toString(), 'doi', 'v2 la minim decripteaza'); kms.close(); const k2 = openKms({ dataDir: dir, rootKey: root }); refuza(() => k2.decrypt('k', c1), 'VERSION_BELOW_MINIMUM', 'minimul persista dupa repornire'); k2.close(); }); proba('rotire: sub min_decryption_version verificarea semnaturii e refuzata cu VERSION_BELOW_MINIMUM', () => { const { kms } = nouKms(); kms.createKey('s', { type: 'sign' }); const s1 = kms.sign('s', 'm').signature; kms.rotate('s'); const s2 = kms.sign('s', 'm').signature; adevarat(s2.startsWith('aerekms:v2:'), 'semnarea foloseste v2'); egal(kms.verify('s', 'm', s1).valid, true, 'v1 verifica inainte de minim'); kms.setMinDecryptionVersion('s', 2); const r = kms.verify('s', 'm', s1); egal(r.valid, false, 'v1 sub minim'); egal(r.reason, 'VERSION_BELOW_MINIMUM', 'motivul'); egal(kms.verify('s', 'm', s2).valid, true, 'v2 verifica'); kms.close(); }); proba('rotire: minimul nu coboara (MIN_VERSION_NOT_MONOTONIC) si nu trece de ultima (VERSION_OUT_OF_RANGE)', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); kms.rotate('k'); kms.setMinDecryptionVersion('k', 2); refuza(() => kms.setMinDecryptionVersion('k', 1), 'MIN_VERSION_NOT_MONOTONIC', 'coborare'); refuza(() => kms.setMinDecryptionVersion('k', 3), 'VERSION_OUT_OF_RANGE', 'peste ultima'); refuza(() => kms.setMinDecryptionVersion('k', 0), 'VERSION_OUT_OF_RANGE', 'zero'); refuza(() => kms.setMinDecryptionVersion('k', 1.5), 'VERSION_OUT_OF_RANGE', 'fractie'); egal(kms.setMinDecryptionVersion('k', 2).min_decryption_version, 2, 'acelasi minim e permis'); kms.close(); }); // ---------------------------------------------------------------------------------- rewrap si datakey proba('rewrap: reincapsuleaza la ultima versiune, acelasi text clar, raspunsul nu contine text clar', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const MARCAJ = 'marcaj-rewrap-' + crypto.randomBytes(6).toString('hex'); const c1 = kms.encrypt('k', MARCAJ, 'ctx').ciphertext; kms.rotate('k'); const r = kms.rewrap('k', c1, 'ctx'); egal(Object.keys(r).sort().join(','), 'ciphertext,version', 'campurile raspunsului'); egal(r.version, 2, 'versiunea noua'); adevarat(r.ciphertext.startsWith('aerekms:v2:'), 'prefix v2'); adevarat(!JSON.stringify(r).includes(MARCAJ) && !JSON.stringify(r).includes(b64(MARCAJ)), 'textul clar in raspuns'); egal(kms.decrypt('k', r.ciphertext, 'ctx').plaintext.toString(), MARCAJ, 'acelasi text clar'); refuza(() => kms.rewrap('k', c1, 'alt'), 'AAD_MISMATCH', 'rewrap cu alt aad'); refuza(() => kms.decrypt('k', r.ciphertext, 'alt'), 'AAD_MISMATCH', 'aad-ul ramane legat dupa rewrap'); kms.close(); }); proba('datakey: cheia clara si cea invelita corespund; forma doar-invelita nu intoarce cheia clara', () => { const { kms } = nouKms(); kms.createKey('k', { type: 'encrypt' }); const dk = kms.datakey('k', { aad: 'fisier-7' }); const clar = Buffer.from(dk.plaintext, 'base64'); egal(clar.length, 32, '256 de biti'); adevarat(kms.decrypt('k', dk.ciphertext, 'fisier-7').plaintext.equals(clar), 'invelita decripteaza la cea clara'); refuza(() => kms.decrypt('k', dk.ciphertext, 'fisier-8'), 'AAD_MISMATCH', 'cheia de date legata de aad'); const doar = kms.datakey('k', { includePlaintext: false }); adevarat(!('plaintext' in doar), 'forma doar-invelita are camp plaintext'); egal(kms.decrypt('k', doar.ciphertext).plaintext.length, 32, 'forma doar-invelita decripteaza'); egal(Buffer.from(kms.datakey('k', { bits: 512 }).plaintext, 'base64').length, 64, '512 biti'); refuza(() => kms.datakey('k', { bits: 100 }), 'INVALID_BITS', 'biti invalizi'); kms.close(); }); // ---------------------------------------------------------------------------------- semnare proba('semnare: semnatura hibrida se verifica, cu ambele jumatati raportate', () => { const { kms } = nouKms(); kms.createKey('s', { type: 'sign' }); const s = kms.sign('s', 'mesaj'); adevarat(/^aerekms:v1:/.test(s.signature), 'forma'); const r = kms.verify('s', 'mesaj', s.signature); egal(r.valid, true, 'valida'); egal(r.classical, true, 'Ed25519'); egal(r.post_quantum, true, 'ML-DSA-65'); egal(r.reason, null, 'fara motiv'); kms.close(); }); proba('semnare: jumatatea Ed25519 stricata e refuzata cu CLASSICAL_SIGNATURE_INVALID', () => { const { kms } = nouKms(); kms.createKey('s', { type: 'sign' }); const s = kms.sign('s', 'mesaj').signature; const rau = sigWith(s, (b) => { b[17 + 10] ^= 0x01; }); const r = kms.verify('s', 'mesaj', rau); egal(r.valid, false, 'valid'); egal(r.reason, 'CLASSICAL_SIGNATURE_INVALID', 'motivul'); egal(r.post_quantum, true, 'jumatatea PQ ramasa buna'); kms.close(); }); proba('semnare: jumatatea ML-DSA-65 stricata e refuzata cu PQ_SIGNATURE_INVALID', () => { const { kms } = nouKms(); kms.createKey('s', { type: 'sign' }); const s = kms.sign('s', 'mesaj').signature; const rau = sigWith(s, (b) => { b[81 + 1000] ^= 0x01; }); const r = kms.verify('s', 'mesaj', rau); egal(r.valid, false, 'valid'); egal(r.reason, 'PQ_SIGNATURE_INVALID', 'motivul'); egal(r.classical, true, 'jumatatea clasica ramasa buna'); kms.close(); }); proba('semnare: ambele jumatati stricate sau mesaj schimbat refuzate cu BOTH_SIGNATURES_INVALID', () => { const { kms } = nouKms(); kms.createKey('s', { type: 'sign' }); const s = kms.sign('s', 'mesaj').signature; const rau = sigWith(s, (b) => { b[17 + 10] ^= 0x01; b[81 + 1000] ^= 0x01; }); egal(kms.verify('s', 'mesaj', rau).reason, 'BOTH_SIGNATURES_INVALID', 'ambele stricate'); const r = kms.verify('s', 'mesaj!', s); egal(r.valid, false, 'mesaj schimbat'); egal(r.reason, 'BOTH_SIGNATURES_INVALID', 'motivul la mesaj schimbat'); kms.close(); }); proba('semnare: jumatati amestecate din doua semnaturi valide sunt refuzate', () => { const { kms } = nouKms(); kms.createKey('s', { type: 'sign' }); const s1 = kms.sign('s', 'unu').signature; const s2 = kms.sign('s', 'doi').signature; const p2 = sigParts(s2); const amestec = sigWith(s1, (b) => { p2.ml.copy(b, 81); }); const r1 = kms.verify('s', 'unu', amestec); egal(r1.valid, false, 'amestec peste "unu"'); egal(r1.reason, 'PQ_SIGNATURE_INVALID', 'motivul peste "unu"'); const r2 = kms.verify('s', 'doi', amestec); egal(r2.valid, false, 'amestec peste "doi"'); egal(r2.reason, 'CLASSICAL_SIGNATURE_INVALID', 'motivul peste "doi"'); kms.close(); }); proba('semnare: alta cheie refuzata cu KEY_MISMATCH, semnatura malformata cu MALFORMED_SIGNATURE', () => { const { kms } = nouKms(); kms.createKey('a', { type: 'sign' }); kms.createKey('b', { type: 'sign' }); const s = kms.sign('a', 'm').signature; egal(kms.verify('a', 'm', s).valid, true, 'cheia proprie'); const r = kms.verify('b', 'm', s); egal(r.valid, false, 'alta cheie'); egal(r.reason, 'KEY_MISMATCH', 'motivul'); egal(kms.verify('a', 'm', s.slice(0, -12)).reason, 'MALFORMED_SIGNATURE', 'trunchiata'); egal(kms.verify('a', 'm', 'nu-e-semnatura').reason, 'MALFORMED_SIGNATURE', 'gunoi'); egal(kms.verify('a', 'm', s.replace('aerekms:v1:', 'aerekms:v2:')).reason, 'VERSION_MISMATCH', 'prefix schimbat'); kms.close(); }); proba('interop: semnatura verificata manual cu node:crypto; jumatatile nu sunt semnaturi ale mesajului brut', () => { const { kms } = nouKms(); const d = kms.createKey('s', { type: 'sign' }); const pub = d.versions['1']; const s = kms.sign('s', 'document').signature; const p = sigParts(s); const m = sigMessageManual(pub.fingerprint, 1, 'document'); const edPub = ed25519Pub(Buffer.from(pub.public.ed25519, 'base64')); const mlPub = spki(pub.public.ml_dsa_65); adevarat(crypto.verify(null, m, edPub, p.ed), 'Ed25519 verifica manual'); adevarat(crypto.verify(null, m, { key: mlPub, context: Buffer.from(L.sigCtx) }, p.ml), 'ML-DSA-65 verifica manual'); // neseparabilitate: nicio jumatate nu e o semnatura valida peste mesajul brut adevarat(!crypto.verify(null, Buffer.from('document'), edPub, p.ed), 'Ed25519 peste mesajul brut'); adevarat(!crypto.verify(null, Buffer.from('document'), mlPub, p.ml), 'ML-DSA peste mesajul brut fara context'); adevarat(!crypto.verify(null, m, mlPub, p.ml), 'ML-DSA fara contextul aerekms'); kms.close(); }); // ---------------------------------------------------------------------------------- jurnalul de audit function jurnalCu(n) { const { kms, dir, root } = nouKms(); kms.createKey('k', { type: 'encrypt' }); for (let i = 0; i < n; i++) kms.encrypt('k', `t${i}`); return { kms, dir, root, f: path.join(dir, 'audit.log') }; } const randuri = (f) => fs.readFileSync(f, 'utf8').split('\n').filter((l) => l !== ''); const scrieRanduri = (f, rs) => fs.writeFileSync(f, rs.join('\n') + '\n'); // A4 (revizuirea din 2026-09-25): cand randul de audit nu se poate scrie, MUTATIA nu are voie sa ramana pe disc; inainte, rotirea si // retragerea versiunii ramaneau in fisierul cheii si lantul de audit iesea valid fara nicio urma a lor. proba('audit: o mutatie al carei rand de audit nu se poate scrie NU ramane pe disc (rotire, minim, creare), iar lantul ramane valid', () => { const { kms, dir, f } = jurnalCu(1); const cheie = path.join(dir, 'keys', 'k.json'); const inainte = fs.readFileSync(cheie); fs.renameSync(f, f + '.deoparte'); fs.mkdirSync(f); // in locul jurnalului, un dosar: append-ul cade try { refuza(() => kms.rotate('k'), 'AUDIT_WRITE_FAILED', 'rotire cu jurnal nescriibil'); egal(fs.readFileSync(cheie).equals(inainte), true, 'rotirea a ramas pe disc fara rand de audit'); refuza(() => kms.createKey('nou', { type: 'encrypt' }), 'AUDIT_WRITE_FAILED', 'creare cu jurnal nescriibil'); egal(fs.existsSync(path.join(dir, 'keys', 'nou.json')), false, 'cheia creata a ramas pe disc fara rand de audit'); } finally { fs.rmdirSync(f); fs.renameSync(f + '.deoparte', f); } // jurnalul la loc: starea vizibila e cea de dinaintea esecurilor, si lantul se verifica egal(JSON.parse(fs.readFileSync(cheie, 'utf8')).latest_version, 1, 'versiunea dupa esec'); egal(kms.getKey('k').latest_version, 1, 'versiunea citita prin serviciu dupa esec'); egal(kms.verifyAudit().ok, true, 'lantul de audit dupa esecuri'); // controlul pozitiv al fixturii: cu jurnalul scriibil, aceeasi rotire chiar se face si se vede in fisier kms.rotate('k'); egal(JSON.parse(fs.readFileSync(cheie, 'utf8')).latest_version, 2, 'rotirea cu jurnal scriibil'); kms.close(); }); proba('audit: jurnalul neatins se verifica OK, un rand pe operatie, si refuzurile au motivul lor', () => { const { kms, f } = jurnalCu(3); try { kms.decrypt('k', kms.encrypt('k', 'x', 'a').ciphertext, 'b'); } catch { /* refuz asteptat */ } const v = kms.verifyAudit(); egal(v.ok, true, `jurnal neatins (${v.reason} la randul ${v.line})`); egal(v.rows, 6, 'randuri: create + 3 encrypt + encrypt + decrypt refuzat'); const ultim = JSON.parse(randuri(f).at(-1)); egal(ultim.op, 'decrypt', 'ultima operatie'); egal(ultim.ok, false, 'refuzul e scris ca refuz'); egal(ultim.reason, 'AAD_MISMATCH', 'motivul refuzului'); egal(JSON.stringify(kms.auditHead()), JSON.stringify(v.head), 'capul din memorie = capul din fisier'); kms.close(); }); proba('audit: rand sters detectat cu AUDIT_CHAIN_BROKEN pe randul lui', () => { const { kms, f } = jurnalCu(5); const rs = randuri(f); rs.splice(3, 1); scrieRanduri(f, rs); const v = kms.verifyAudit(); egal(v.ok, false, 'rand sters'); egal(v.reason, 'AUDIT_CHAIN_BROKEN', 'motivul'); egal(v.line, 4, 'randul'); kms.close(); }); proba('audit: rand schimbat detectat cu AUDIT_ROW_MODIFIED', () => { const { kms, f } = jurnalCu(5); const rs = randuri(f); const r = JSON.parse(rs[2]); r.op = 'decrypt'; rs[2] = JSON.stringify(r); scrieRanduri(f, rs); const v = kms.verifyAudit(); egal(v.ok, false, 'rand schimbat'); egal(v.reason, 'AUDIT_ROW_MODIFIED', 'motivul'); egal(v.line, 3, 'randul'); kms.close(); }); proba('audit: randuri reordonate detectate cu AUDIT_CHAIN_BROKEN', () => { const { kms, f } = jurnalCu(5); const rs = randuri(f); [rs[1], rs[2]] = [rs[2], rs[1]]; scrieRanduri(f, rs); const v = kms.verifyAudit(); egal(v.ok, false, 'reordonare'); egal(v.reason, 'AUDIT_CHAIN_BROKEN', 'motivul'); egal(v.line, 2, 'randul'); kms.close(); }); proba('audit: coada taiata se vede numai fata de un cap ancorat (AUDIT_TRUNCATED)', () => { const { kms, f } = jurnalCu(4); const cap = kms.auditHead(); egal(kms.verifyAudit({ expectedHead: cap }).ok, true, 'cu capul ancorat, jurnal neatins'); const rs = randuri(f); scrieRanduri(f, rs.slice(0, -1)); egal(kms.verifyAudit().ok, true, 'LIMITA DOCUMENTATA: fara cap ancorat, taierea cozii nu se vede'); const v = kms.verifyAudit({ expectedHead: cap }); egal(v.ok, false, 'coada taiata fata de cap'); egal(v.reason, 'AUDIT_TRUNCATED', 'motivul'); kms.close(); }); proba('audit: pornirea refuza un jurnal stricat cu AUDIT_CHAIN_INVALID', () => { const { kms, f, dir, root } = jurnalCu(3); kms.close(); const k0 = openKms({ dataDir: dir, rootKey: root }); k0.close(); const rs = randuri(f); rs.splice(1, 1); scrieRanduri(f, rs); refuza(() => openKms({ dataDir: dir, rootKey: root }), 'AUDIT_CHAIN_INVALID', 'pornire pe jurnal stricat'); }); proba('audit: jurnalul si fisierele nu contin text clar, aad, mesaje sau chei', () => { const { kms, dir } = nouKms(); const PT = 'MARCAJ-PT-' + crypto.randomBytes(6).toString('hex'); const AAD = 'MARCAJ-AAD-' + crypto.randomBytes(6).toString('hex'); const MSG = 'MARCAJ-MSG-' + crypto.randomBytes(6).toString('hex'); kms.createKey('e', { type: 'encrypt', exportable: true }); kms.createKey('s', { type: 'sign', exportable: true }); const c = kms.encrypt('e', PT, AAD).ciphertext; kms.decrypt('e', c, AAD); kms.rotate('e'); kms.rewrap('e', c, AAD); kms.datakey('e', { aad: AAD }); kms.verify('s', MSG, kms.sign('s', MSG).signature); try { kms.decrypt('e', c, AAD + 'x'); } catch { /* asteptat */ } const needles = [...needlesDin(kms.exportKey('e')), ...needlesDin(kms.exportKey('s'))]; const log = fs.readFileSync(path.join(dir, 'audit.log'), 'utf8'); for (const m of [PT, AAD, MSG]) { for (const forma of [m, b64(m), Buffer.from(m).toString('hex')]) adevarat(!log.includes(forma), `jurnalul contine ${m.slice(0, 11)}`); } egal(gasesteMaterial([path.join(dir, 'audit.log')], needles).length, 0, 'material de cheie in jurnal'); for (const f of toateFisierele(dir)) { const t = fs.readFileSync(f, 'utf8'); for (const m of [PT, AAD, MSG]) adevarat(!t.includes(m) && !t.includes(b64(m)), `${path.basename(f)} contine un marcaj`); } kms.close(); }); // ---------------------------------------------------------------------------------- erori proba('erori: niciun mesaj de eroare nu contine material de cheie', () => { const root = rootHex(); const { kms } = nouKms(tmpDir(), root); kms.createKey('e', { type: 'encrypt', exportable: true }); kms.createKey('s', { type: 'sign', exportable: true }); const ne = needlesDin(kms.exportKey('e')); const ns = needlesDin(kms.exportKey('s')); const forme = [root, root.toUpperCase(), Buffer.from(root, 'hex').toString('base64')]; for (const n of [...ne, ...ns]) forme.push(n.toString('base64'), n.toString('hex'), n.toString('base64url')); const ct = kms.encrypt('e', 'x', 'a').ciphertext; const sig = kms.sign('s', 'm').signature; const erori = []; const prinde = (f) => { try { f(); } catch (e) { erori.push(e); } }; prinde(() => kms.decrypt('e', ct, 'b')); prinde(() => kms.decrypt('e', reencode(ct, (b) => void (b[OFF.ctK] ^= 1)))); prinde(() => kms.decrypt('e', reencode(ct, (b) => void (b[OFF.payload] ^= 1)), 'a')); prinde(() => kms.decrypt('e', 'aerekms:v1:AAAA')); prinde(() => kms.sign('e', 'm')); prinde(() => kms.exportKey('e', 9)); prinde(() => kms.setMinDecryptionVersion('e', 5)); prinde(() => openKms({ dataDir: tmpDir(), rootKey: root.slice(2) })); prinde(() => openKms({ dataDir: kms.dataDir, rootKey: rootHex() })); egal(erori.length, 9, 'toate cele noua operatii trebuiau refuzate'); const vs = kms.verify('s', 'n', sig); const texte = [...erori.map((e) => `${e.message} ${JSON.stringify({ ...e })}`), JSON.stringify(vs)]; for (const t of texte) for (const f of forme) adevarat(!t.includes(f), 'un mesaj de eroare contine material de cheie'); kms.close(); }); // ---------------------------------------------------------------------------------- server proba('server: configurarea implicita asculta pe 127.0.0.1 si cere token si cheie radacina', () => { const token = crypto.randomBytes(24).toString('hex'); const root = rootHex(); const c = configFromEnv({ AERE_KMS_TOKEN: token, AERE_KMS_ROOT_KEY: root }); egal(c.host, '127.0.0.1', 'gazda implicita'); egal(c.port, 8420, 'portul implicit'); refuza(() => configFromEnv({ AERE_KMS_ROOT_KEY: root }), 'TOKEN_MISSING', 'fara token'); refuza(() => configFromEnv({ AERE_KMS_TOKEN: 'scurt', AERE_KMS_ROOT_KEY: root }), 'TOKEN_TOO_SHORT', 'token scurt'); refuza(() => configFromEnv({ AERE_KMS_TOKEN: token }), 'ROOT_KEY_MISSING', 'fara cheie radacina'); refuza(() => configFromEnv({ AERE_KMS_TOKEN: root, AERE_KMS_ROOT_KEY: root }), 'TOKEN_EQUALS_ROOT_KEY', 'token = radacina'); }); proba('server: pornit din linia de comanda refuza fara AERE_KMS_ROOT_KEY si nu tipareste tokenul', () => { const dir = path.join(tmpDir('cli'), 'date'); const token = 'tok-' + crypto.randomBytes(20).toString('hex'); const r = spawnSync(process.execPath, [SERVER_PATH], { env: { ...envCurat(), AERE_KMS_TOKEN: token, AERE_KMS_DATA_DIR: dir, AERE_KMS_PORT: '0' }, encoding: 'utf8', timeout: 20000, }); egal(r.status, 1, `codul de iesire (stderr: ${String(r.stderr).slice(0, 200)})`); adevarat(r.stderr.includes('ROOT_KEY_MISSING'), 'motivul pe stderr'); adevarat(!r.stderr.includes(token) && !r.stdout.includes(token), 'tokenul tiparit'); adevarat(!fs.existsSync(dir), 'dosarul de date creat fara cheie radacina'); }); proba('server: pornit din linia de comanda asculta pe 127.0.0.1 si raspunde', async () => { const token = 'tok-' + crypto.randomBytes(20).toString('hex'); const root = rootHex(); const child = spawn(process.execPath, [SERVER_PATH], { env: { ...envCurat(), AERE_KMS_TOKEN: token, AERE_KMS_ROOT_KEY: root, AERE_KMS_DATA_DIR: path.join(tmpDir('cli'), 'date'), AERE_KMS_PORT: '0' }, stdio: ['ignore', 'pipe', 'pipe'], }); let out = ''; let err = ''; child.stderr.on('data', (c) => (err += c)); try { const adresa = await new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Esec(`serverul nu a pornit in 15 s (stderr: ${err.slice(0, 200)})`)), 15000); child.stdout.on('data', (c) => { out += c; const m = /listening on ([0-9.]+):(\d+)/.exec(out); if (m) { clearTimeout(t); resolve({ host: m[1], port: Number(m[2]) }); } }); child.on('exit', (code) => reject(new Esec(`serverul a iesit cu ${code}: ${err.slice(0, 200)}`))); }); egal(adresa.host, '127.0.0.1', 'adresa de ascultare'); const h = await cerere(adresa.port, 'GET', '/v1/keys', { headers: { authorization: `Bearer ${token}` } }); egal(h.status, 200, 'cerere autentificata'); const n = await cerere(adresa.port, 'GET', '/v1/keys'); egal(n.status, 401, 'cerere neautentificata'); adevarat(!out.includes(token) && !out.includes(root) && !err.includes(token) && !err.includes(root), 'secret tiparit'); } finally { child.kill(); await new Promise((r) => (child.exitCode !== null ? r() : child.on('exit', r))); } }); proba('server: fara Authorization raspunde 401 UNAUTHENTICATED', async () => { await cuServer(async ({ port, auth }) => { const ok = await cerere(port, 'GET', '/v1/keys', { headers: auth }); egal(ok.status, 200, 'cu token'); for (const [m, p, body] of [['GET', '/v1/keys'], ['POST', '/v1/keys/k', { type: 'encrypt' }], ['POST', '/v1/encrypt/k', { plaintext: b64('x') }]]) { const r = await cerere(port, m, p, { body }); egal(r.status, 401, `${m} ${p} fara token`); egal(r.json && r.json.error, 'UNAUTHENTICATED', `${m} ${p} motivul`); } }); }); proba('server: token gresit raspunde 401 INVALID_TOKEN, fara sa-l repete', async () => { await cuServer(async ({ port, token }) => { const gresit = token.slice(0, -1) + (token.endsWith('a') ? 'b' : 'a'); for (const h of [`Bearer ${gresit}`, `Basic ${token}`, `Bearer`, `bearer ${token}`]) { const r = await cerere(port, 'GET', '/v1/keys', { headers: { authorization: h } }); egal(r.status, 401, `antet ${h.slice(0, 7)}`); egal(r.json && r.json.error, 'INVALID_TOKEN', `motivul pentru ${h.slice(0, 7)}`); adevarat(!r.text.includes(token) && !r.text.includes(gresit), 'raspunsul repeta tokenul'); } }); }); proba('server: cerere peste limita cu Content-Length raspunde 413 BODY_TOO_LARGE', async () => { await cuServer(async ({ port, auth }) => { await cerere(port, 'POST', '/v1/keys/k', { headers: auth, body: { type: 'encrypt' } }); const sub = await cerere(port, 'POST', '/v1/encrypt/k', { headers: auth, body: { plaintext: b64('x'.repeat(2000)) } }); egal(sub.status, 200, 'sub limita trece'); const peste = await cerere(port, 'POST', '/v1/encrypt/k', { headers: auth, body: { plaintext: b64('x'.repeat(6000)) } }); egal(peste.status, 413, 'peste limita'); egal(peste.json && peste.json.error, 'BODY_TOO_LARGE', 'motivul'); }); }); proba('server: cerere peste limita fara Content-Length (chunked) raspunde 413 BODY_TOO_LARGE', async () => { await cuServer(async ({ port, auth }) => { await cerere(port, 'POST', '/v1/keys/k', { headers: auth, body: { type: 'encrypt' } }); const sub = await cerere(port, 'POST', '/v1/encrypt/k', { headers: auth, body: { plaintext: b64('y'.repeat(2000)) }, chunked: true }); egal(sub.status, 200, 'sub limita, chunked, trece'); const peste = await cerere(port, 'POST', '/v1/encrypt/k', { headers: auth, body: { plaintext: b64('y'.repeat(9000)) }, chunked: true }); egal(peste.status, 413, 'peste limita, chunked'); egal(peste.json && peste.json.error, 'BODY_TOO_LARGE', 'motivul'); }); }); proba('server: JSON invalid 400 INVALID_JSON, base64 invalid 400 INVALID_BASE64, ruta necunoscuta 404 NOT_FOUND', async () => { await cuServer(async ({ port, auth }) => { await cerere(port, 'POST', '/v1/keys/k', { headers: auth, body: { type: 'encrypt' } }); const j = await cerere(port, 'POST', '/v1/encrypt/k', { headers: auth, body: '{"plaintext":' }); egal(j.status, 400, 'JSON invalid'); egal(j.json.error, 'INVALID_JSON', 'motivul JSON'); const b = await cerere(port, 'POST', '/v1/encrypt/k', { headers: auth, body: { plaintext: 'nu e base64!' } }); egal(b.json.error, 'INVALID_BASE64', 'motivul base64'); const n = await cerere(port, 'GET', '/v1/nimic', { headers: auth }); egal(n.status, 404, 'ruta'); egal(n.json.error, 'NOT_FOUND', 'motivul rutei'); const nm = await cerere(port, 'GET', '/v1/keys/..%2Fx', { headers: auth }); egal(nm.json.error, 'INVALID_KEY_NAME', 'nume cu traversare in cale'); }); }); proba('server: flux complet prin HTTP (creare, criptare, decriptare, rotire, rewrap, datakey, semnare, verificare, audit)', async () => { await cuServer(async ({ port, auth }) => { const P = (p, body) => cerere(port, 'POST', p, { headers: auth, body }); const G = (p) => cerere(port, 'GET', p, { headers: auth }); egal((await P('/v1/keys/e', { type: 'encrypt' })).status, 200, 'creare e'); egal((await P('/v1/keys/s', { type: 'sign' })).status, 200, 'creare s'); const dup = await P('/v1/keys/e', { type: 'encrypt' }); egal(dup.status, 409, 'dubla'); egal(dup.json.error, 'KEY_EXISTS', 'motivul dublei'); const enc = await P('/v1/encrypt/e', { plaintext: b64('prin http'), aad: b64('ctx') }); egal(enc.status, 200, 'encrypt'); const dec = await P('/v1/decrypt/e', { ciphertext: enc.json.ciphertext, aad: b64('ctx') }); egal(Buffer.from(dec.json.plaintext, 'base64').toString(), 'prin http', 'decrypt'); const rau = await P('/v1/decrypt/e', { ciphertext: enc.json.ciphertext, aad: b64('alt') }); egal(rau.status, 400, 'decrypt cu alt aad'); egal(rau.json.error, 'AAD_MISMATCH', 'motivul prin HTTP'); egal((await P('/v1/keys/e/rotate', {})).json.latest_version, 2, 'rotire'); const rw = await P('/v1/rewrap/e', { ciphertext: enc.json.ciphertext, aad: b64('ctx') }); egal(rw.json.version, 2, 'rewrap'); adevarat(!('plaintext' in rw.json), 'rewrap nu intoarce text clar'); egal((await P('/v1/keys/e/config', { min_decryption_version: 2 })).json.min_decryption_version, 2, 'config'); const vechi = await P('/v1/decrypt/e', { ciphertext: enc.json.ciphertext, aad: b64('ctx') }); egal(vechi.json.error, 'VERSION_BELOW_MINIMUM', 'sub minim prin HTTP'); const dk = await P('/v1/datakey/e', { include_plaintext: false }); adevarat(!('plaintext' in dk.json) && dk.json.ciphertext.startsWith('aerekms:v2:'), 'datakey doar invelita'); const ex = await G('/v1/keys/e/export'); egal(ex.status, 403, 'export refuzat'); egal(ex.json.error, 'KEY_NOT_EXPORTABLE', 'motivul exportului'); const sg = await P('/v1/sign/s', { message: b64('doc') }); egal((await P('/v1/verify/s', { message: b64('doc'), signature: sg.json.signature })).json.valid, true, 'verify'); const vr = await P('/v1/verify/s', { message: b64('doc2'), signature: sg.json.signature }); egal(vr.json.valid, false, 'verify alt mesaj'); egal(vr.json.reason, 'BOTH_SIGNATURES_INVALID', 'motivul verify'); const k = await G('/v1/keys/e'); egal(k.json.latest_version, 2, 'citire cheie'); adevarat(!JSON.stringify(k.json).includes('private'), 'citirea cheii nu expune material privat'); egal(JSON.stringify((await G('/v1/keys')).json.keys), JSON.stringify(['e', 's']), 'lista'); const au = await G('/v1/audit/verify'); egal(au.json.ok, true, 'audit prin HTTP'); }, { maxBody: 8192 }); }); proba('server: limita minima a corpului incape o verificare de semnatura (AERE_KMS_MAX_BODY >= 8192)', async () => { const token = crypto.randomBytes(24).toString('hex'); const root = rootHex(); refuza(() => configFromEnv({ AERE_KMS_TOKEN: token, AERE_KMS_ROOT_KEY: root, AERE_KMS_MAX_BODY: '4096' }), 'INVALID_MAX_BODY', 'limita sub 8192'); const min = configFromEnv({ AERE_KMS_TOKEN: token, AERE_KMS_ROOT_KEY: root, AERE_KMS_MAX_BODY: '8192' }).maxBody; await cuServer(async ({ port, auth }) => { await cerere(port, 'POST', '/v1/keys/s', { headers: auth, body: { type: 'sign' } }); const msg = b64('m'.repeat(1024)); const sg = await cerere(port, 'POST', '/v1/sign/s', { headers: auth, body: { message: msg } }); const vr = await cerere(port, 'POST', '/v1/verify/s', { headers: auth, body: { message: msg, signature: sg.json.signature } }); egal(vr.status, 200, `verificare la limita minima (${min})`); egal(vr.json.valid, true, 'valida'); }, { maxBody: min }); }); proba('server: raspunsurile nu contin tokenul', async () => { await cuServer(async ({ port, auth, token }) => { const rs = []; rs.push(await cerere(port, 'POST', '/v1/keys/k', { headers: auth, body: { type: 'encrypt' } })); rs.push(await cerere(port, 'GET', '/v1/keys/k', { headers: auth })); rs.push(await cerere(port, 'POST', '/v1/decrypt/k', { headers: auth, body: { ciphertext: 'x' } })); rs.push(await cerere(port, 'GET', '/v1/audit/verify', { headers: auth })); for (const r of rs) adevarat(!r.text.includes(token), 'tokenul in raspuns'); }); }); // ================================================================================== rulare async function main() { const t0 = Date.now(); const rez = []; for (const p of probe) { const t = Date.now(); try { await p.fn(); rez.push({ nume: p.nume, ok: true, ms: Date.now() - t }); } catch (e) { rez.push({ nume: p.nume, ok: false, ms: Date.now() - t, motiv: String((e && e.message) || e).slice(0, 500), tip: e instanceof Esec ? 'afirmatie' : 'exceptie' }); } } for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* dosar temporar al probei; ramane in %TEMP% daca Windows il tine ocupat */ } } const picate = rez.filter((r) => !r.ok); for (const r of rez) process.stdout.write(`${r.ok ? 'TRECE' : 'PICA '} ${r.nume}${r.ok ? '' : `\n -> ${r.motiv}`}\n`); process.stdout.write(`\n${rez.length - picate.length}/${rez.length} probe trecute, ${picate.length} picate, ${Date.now() - t0} ms\n`); if (process.env.AERE_KMS_PROBA_JSON) { fs.writeFileSync(process.env.AERE_KMS_PROBA_JSON, JSON.stringify({ completed: true, total: rez.length, passed: rez.length - picate.length, failed: picate.map((r) => r.nume), results: rez, node: process.versions.node, openssl: process.versions.openssl, }, null, 2)); } process.exitCode = picate.length === 0 ? 0 : 1; } main().catch((e) => { process.stderr.write(`harnessul probei a cazut: ${(e && e.stack) || e}\n`); process.exitCode = 2; });