aere-quantum/crypto-inventory/test/echivalenta-0.1.0.mjs

108 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Echivalenta formelor liniare din 0.2.0 cu formele din 0.1.0 (2026-09-29, revizuirea adversariala): aceleasi gasiri si biblioteci
// pe arbori reali, si aceleasi rezultate ale lexerului si ale cautarii PEM pe texte generate din jetoanele care conteaza.
// 0.1.0 se ia din git (revizia AERE_INV_REV_VECHE, implicit db9d9543), in forma comisa; intr-o copie fara istoric iese SARIT.
// node test/echivalenta-0.1.0.mjs [--iteratii N] [dosar...] (implicit: fixturile si dosarul uneltei)
// -> 0 identic, 1 o diferenta (tiparita), 2 NEMASURAT (revizia veche lipseste sau nimic comparat)
// Singura diferenta asteptata e textul motivului pentru "unknown" (spune acum si plafonul de variabile); se normalizeaza.
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
const AICI = dirname(fileURLToPath(import.meta.url));
const RAD = join(AICI, '..');
const REV = process.env.AERE_INV_REV_VECHE || 'db9d9543';
let ITER = 20000;
const dosare = [];
for (let k = 2; k < process.argv.length; k++) {
if (process.argv[k] === '--iteratii') ITER = Number(process.argv[++k]);
else dosare.push(process.argv[k]);
}
const DOSARE = dosare.length ? dosare : [join(AICI, 'fixturi'), RAD];
const git = (...a) => spawnSync('git', ['-c', 'core.autocrlf=false', ...a], { cwd: RAD, encoding: 'buffer', maxBuffer: 64 << 20 });
const ls = git('ls-tree', '-r', '--name-only', '--full-tree', REV, 'tools/crypto-inventory/inventar.mjs', 'tools/crypto-inventory/lib');
const fis = ls.status === 0 ? ls.stdout.toString().split('\n').filter(Boolean) : [];
if (!fis.length) { console.log(`SARIT: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); process.exit(2); }
const vechi = mkdtempSync(join(tmpdir(), 'ci-v010-'));
let dif = 0, comparate = 0;
let versiuneNoua = '?';
try {
for (const f of fis) {
const b = git('show', `${REV}:${f}`);
if (b.status !== 0) { console.log(`NEMASURAT: git show ${f}`); process.exit(2); }
const t = join(vechi, f.slice('tools/crypto-inventory/'.length));
mkdirSync(dirname(t), { recursive: true });
writeFileSync(t, b.stdout);
}
const imp = (r, m) => import(pathToFileURL(join(r, 'lib', m)).href);
const [SV, SN, LV, LN, PN] = await Promise.all([imp(vechi, 'scan.mjs'), imp(RAD, 'scan.mjs'), imp(vechi, 'lexer.mjs'), imp(RAD, 'lexer.mjs'), imp(RAD, 'pem.mjs')]);
versiuneNoua = SN.VERSIUNE;
if (SV.VERSIUNE !== '0.1.0') { console.log(`NEMASURAT: revizia ${REV} are versiunea ${SV.VERSIUNE}, nu 0.1.0`); process.exit(2); }
// 1. arbori reali
const norm = (g) => JSON.stringify({ ...g, reason: String(g.reason).replace(/, which (cannot be resolved statically|this tool does not resolve statically).*$/, ', which [motiv]') });
for (const d of DOSARE) {
const a = SV.scaneaza(d), b = SN.scaneaza(d);
// 0.3.0 (2026-10-18): Rust si Cargo.toml nu existau in 0.1.0, deci gasirile lor nu intra in echivalenta; se numara si se spun.
// 0.4.0 (2026-10-18): la fel Swift si Kotlin, manifestele Swift Package Manager / CocoaPods / libs.versions.toml, si grupurile Maven
// adaugate atunci (org.signal, org.whispersystems, org.mindrot, de.mkammerer, com.lambdaworks, org.bitcoinj).
const LIMBAJE_NOI = new Set(['rust', 'swift', 'kotlin']);
const libNoua = (x) => x.ecosystem === 'cargo' || x.ecosystem === 'swift' || x.ecosystem === 'cocoapods' || /(^|\/)libs\.versions\.toml$/.test(x.file) || /^(org\.signal|org\.whispersystems|org\.mindrot|de\.mkammerer|com\.lambdaworks|org\.bitcoinj):/.test(x.name);
const peLimbaj = {};
for (const g of b.findings) if (LIMBAJE_NOI.has(g.language)) peLimbaj[g.language] = (peLimbaj[g.language] || 0) + 1;
const libN = b.libraries.filter(libNoua).length;
b.findings = b.findings.filter((g) => !LIMBAJE_NOI.has(g.language));
b.libraries = b.libraries.filter((x) => !libNoua(x));
if (Object.keys(peLimbaj).length || libN) console.log(` (in afara echivalentei: gasiri ${JSON.stringify(peLimbaj)}, ${libN} biblioteci Cargo/Swift/CocoaPods/grupuri Maven noi; 0.1.0 nu le stia)`);
const ka = a.findings.map(norm), kb = b.findings.map(norm);
const sa = new Set(ka), sb = new Set(kb);
// 0.3.0 (2026-10-18, libsignal): detectoarele noi pe limbaje pe care 0.1.0 le STIA (API-ul libsignal in Java si Node) sunt adaugiri,
// nu rescrieri: o gasire numai in versiunea noua, cu un API din lista de mai jos, se numara si se spune, nu e diferenta. O gasire
// numai in 0.1.0 ramane intotdeauna diferenta: echivalenta pazeste ca rescrierea nu a PIERDUT nimic.
// 0.4.0: si regulile JVM adaugate pentru Kotlin, care se aplica si in Java (Android Keystore, Tink, OkHttp, PKIX, puntea libsignal).
const ADAUGIRI = /"api":"(?:(?:ECKeyPair|IdentityKeyPair|ECPrivateKey|PrivateKey|KEMKeyPair)\.generate|Curve\.generateKeyPair|HKDF\.deriveSecrets|new (?:Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32|PreKeyBundle|KEMPublicKey|SealedSessionCipher)|import org\.signal\.libsignal|hkdf|Aes256GcmSiv\.new|PreKeyBundle\.new|processPreKeyBundle|import @signalapp\/libsignal-client(?:\/[^"]*)?|Native\.\w+|KeyGenParameterSpec\.Builder|KeyTemplates\.get|\w+KeyTemplates\.\w+|Predefined\w+Parameters\.\w+|\w+Config\.register|import com\.google\.crypto\.tink|ConnectionSpec\.\w+|CertificatePinner\.Builder|(?:CertPathValidator|TrustManagerFactory)\.getInstance|KeyStore\.getInstance\("AndroidCAStore"\)|X509TrustManager|checkServerTrusted)"/;
const doarV = ka.filter((x) => !sb.has(x)), doarN0 = kb.filter((x) => !sa.has(x));
const adaugate = doarN0.filter((x) => ADAUGIRI.test(x)), doarN = doarN0.filter((x) => !ADAUGIRI.test(x));
if (adaugate.length) console.log(` (in afara echivalentei: ${adaugate.length} gasiri ale detectoarelor adaugate dupa 0.1.0, API-ul libsignal Java/Node si regulile JVM din 0.4.0)`);
const ok = !doarV.length && !doarN.length && ka.length + adaugate.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries);
comparate += ka.length;
if (!ok) dif++;
console.log(`${ok ? 'IDENTIC ' : 'DIFERIT '} ${d}: ${a.stats.filesSeen} fisiere, gasiri ${ka.length}/${kb.length}, biblioteci ${a.libraries.length}/${b.libraries.length}`);
for (const x of [...doarV.slice(0, 3).map((x) => 'numai 0.1.0: ' + x), ...doarN.slice(0, 3).map((x) => `numai ${SN.VERSIUNE}: ` + x)]) console.log(' ' + x.slice(0, 260));
}
// 2. texte generate: lexerul pe patru limbaje si cautarea PEM (fata de regexul din 0.1.0)
const RE_010 = /-----BEGIN ((?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)-----([A-Za-z0-9+/=\s:,\-\r\n]*?)-----END \1-----/g;
let seed = 12345;
const rnd = (n) => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed % n; };
const gen = (jet, max) => { let s = ''; const L = rnd(max); for (let i = 0; i < L; i++) s += jet[rnd(jet.length)]; return s; };
const JET = {
python: ['"', "'", '"""', "'''", '\\', '\n', ' ', '\t', 'r', 'b', 'f', 'u', 'x', '#', '=', '(', ')', 'rb', '\r\n', 'a1'],
javascript: ['=/[', '=/', ' x/', '"', "'", '`', '${', '}', '{', '/', '//', '/*', '*/', '[', ']', '\\', '\n', ' ', '=', '(', ')', 'return', 'x', '1', '.', ',', 'typeof'],
java: ['"', "'", '"""', '\\', '\n', ' ', '/', '//', '/*', '*/', 'x', '=', '(', ')'],
go: ['"', "'", '`', '\\', '\n', ' ', '/', '//', '/*', '*/', 'x', '=', '(', ')'],
};
const JET_PEM = ['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----', '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----',
'-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----', '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----',
'BEGIN CERTIFICATE-----', 'BEGIN PRIVATE KEY-----', 'AAAA', 'QUJD', '\n', ' ', '.', '-', '--', '-----', '"', 'BEGIN', '=', ':', ' ', '\r\n'];
const cat = {};
for (let k = 0; k < ITER; k++) {
for (const [limbaj, jet] of Object.entries(JET)) {
const t = gen(jet, 40);
const a = LV.curata(t, limbaj), b = LN.curata(t, limbaj);
comparate++;
if (a.code !== b.code || JSON.stringify(a.siruri) !== JSON.stringify(b.siruri)) { cat[limbaj] = (cat[limbaj] || 0) + 1; if (dif++ < 5) console.log(`DIFERIT lexer ${limbaj}: ${JSON.stringify(t)}`); }
}
const t = gen(JET_PEM, 30);
const a = [...t.matchAll(RE_010)].map((m) => [m.index, m[1], m[2]]);
const b = [...PN.blocuriPem(t)].map((x) => [x.index, x.tip, x.corp]);
comparate++;
if (JSON.stringify(a) !== JSON.stringify(b)) { cat.pem = (cat.pem || 0) + 1; if (dif++ < 5) console.log(`DIFERIT pem: ${JSON.stringify(t)}`); }
}
console.log(`texte generate: ${ITER} runde (lexer x4 + PEM), diferente ${JSON.stringify(cat)}`);
} finally { rmSync(vechi, { recursive: true, force: true }); }
console.log(`\n${dif ? 'DIFERIT' : 'IDENTIC'}: ${dif} diferente in ${comparate} comparatii (0.1.0 din ${REV} fata de ${versiuneNoua})`);
process.exitCode = dif ? 1 : comparate ? 0 : 2;