aere-quantum/crypto-inventory/lib/detect-java.mjs

252 lines
16 KiB
JavaScript

// Detectorul Java: JCA getInstance("..."), SSLContext, protocoale activate, Bouncy Castle.
import { argumente, imparteArgumente, valoareArgument, necunoscut } from './context.mjs';
import { hashCanonic, grupTls, curbaCanonica } from './catalog.mjs';
const TLSV = { TLSv1: '1.0', 'TLSv1.1': '1.1', 'TLSv1.2': '1.2', 'TLSv1.3': '1.3', SSLv3: 'ssl3', SSLv2Hello: 'ssl2', SSL: 'negotiated', TLS: 'negotiated', Default: 'negotiated' };
// transforma numele JCA intr-un activ de catalog, dupa clasa care il cere
export function activJca(clasa, val, dupa = '', variabila = null) {
const peVar = (metoda, rest) => (variabila ? new RegExp(`(?<![\\w.])${variabila}\\s*\\.\\s*${metoda}\\s*\\(\\s*${rest}`).exec(dupa) : null);
const v = String(val).trim();
const u = v.toUpperCase();
let m;
const pq = pqNume(v);
if (pq) return pq;
if (clasa === 'MessageDigest') return { grup: 'HASH', hash: hashCanonic(v) || v, functii: ['digest'] };
if (clasa === 'Mac') {
if ((m = /^HMAC-?(\S+)$/i.exec(v))) return { grup: 'MAC', hash: hashCanonic(m[1]) || m[1], functii: ['tag'] };
return { grup: 'UNKNOWN', motiv: `Mac "${v}" is not in this tool's catalog.` };
}
if (clasa === 'SecretKeyFactory') {
if ((m = /^PBKDF2WithHmac(\w+)$/i.exec(v))) return { grup: 'KDF', nume: `PBKDF2-HMAC-${m[1].toUpperCase()}`, hash: hashCanonic(m[1]) || m[1], functii: ['keyderive'] };
if (/^PBEWith(MD5|SHA1)AndDES/i.test(v)) return { grup: 'CIPHER', nume: 'DES', nota: `${v} derives a DES key with a weak hash.` };
if (/^DESede$/i.test(v)) return { grup: 'CIPHER', nume: '3DES' };
if (/^DES$/i.test(v)) return { grup: 'CIPHER', nume: 'DES' };
return { grup: 'UNKNOWN', motiv: `SecretKeyFactory "${v}" is not in this tool's catalog.` };
}
if (clasa === 'KeyGenerator') {
const init = peVar('init', '(\\d+)');
if (u === 'AES') return { grup: 'CIPHER', nume: 'AES', param: init ? init[1] : undefined, functii: ['keygen'] };
if ((m = /^HMAC(\w+)$/i.exec(v))) return { grup: 'MAC', hash: hashCanonic(m[1]) || m[1], functii: ['keygen'] };
if (u === 'DES' || u === 'DESEDE' || u === 'CHACHA20' || u === 'BLOWFISH' || u === 'RC4' || u === 'ARCFOUR') return { grup: 'CIPHER', nume: u === 'DESEDE' ? '3DES' : v, functii: ['keygen'] };
return { grup: 'UNKNOWN', motiv: `KeyGenerator "${v}" is not in this tool's catalog.` };
}
if (clasa === 'KeyPairGenerator' || clasa === 'KeyFactory' || clasa === 'AlgorithmParameterGenerator') {
const init = peVar('initialize', '(\\d+)');
const bits = clasa === 'KeyPairGenerator' && init ? init[1] : undefined;
const f = clasa === 'KeyPairGenerator' ? ['keygen'] : undefined;
if (u === 'RSA') return { grup: 'RSA', param: bits, functii: f };
if (u === 'RSASSA-PSS') return { grup: 'RSA', param: bits, primitiv: 'signature', padding: 'other', functii: f };
if (u === 'DSA') return { grup: 'DSA', param: bits, functii: f };
if (u === 'DH' || u === 'DIFFIEHELLMAN') return { grup: 'DH', param: bits, functii: f };
if (u === 'EC' || u === 'ECDSA' || u === 'ECDH') {
const spec = peVar('initialize', 'new\\s+ECGenParameterSpec\\s*\\(\\s*"([^"]+)"');
return { grup: u === 'ECDH' ? 'ECDH' : 'EC', curba: spec ? spec[1] : undefined, functii: f };
}
if (u === 'ED25519' || u === 'ED448' || u === 'EDDSA') return { grup: 'EDDSA', nume: u === 'ED448' ? 'Ed448' : 'Ed25519', functii: f };
if (u === 'X25519' || u === 'X448' || u === 'XDH') return { grup: 'XDH', nume: u === 'X448' ? 'X448' : 'X25519', functii: f };
}
if (clasa === 'Signature') {
if ((m = /^(\w+?)with(RSA|ECDSA|DSA|PLAIN-ECDSA|CVC-ECDSA|RSAandMGF1)(?:\/(PSS|ISO9796-2|X9\.31))?(?:inP1363Format)?$/i.exec(v))) {
const hash = hashCanonic(m[1]) || (m[1].toUpperCase() === 'NONE' ? undefined : m[1]);
const fam = m[2].toUpperCase();
if (fam.startsWith('RSA')) return { grup: 'RSA', primitiv: 'signature', hash, padding: m[3] || /MGF1/i.test(fam) ? 'other' : 'pkcs1v15', functii: ['sign', 'verify'] };
if (fam === 'DSA') return { grup: 'DSA', hash, functii: ['sign', 'verify'] };
return { grup: 'ECDSA', hash, functii: ['sign', 'verify'] };
}
if (u === 'RSASSA-PSS') return { grup: 'RSA', primitiv: 'signature', padding: 'other', functii: ['sign', 'verify'] };
if (u === 'ED25519' || u === 'ED448' || u === 'EDDSA') return { grup: 'EDDSA', nume: u === 'ED448' ? 'Ed448' : 'Ed25519', functii: ['sign', 'verify'] };
}
if (clasa === 'KeyAgreement') {
if (u === 'ECDH' || u === 'ECMQV' || u === 'ECCDH') return { grup: 'ECDH', functii: ['keygen'] };
if (u === 'DH' || u === 'DIFFIEHELLMAN') return { grup: 'DH', functii: ['keygen'] };
if (u === 'X25519' || u === 'X448' || u === 'XDH') return { grup: 'XDH', nume: u === 'X448' ? 'X448' : 'X25519', functii: ['keygen'] };
}
if (clasa === 'KEM') {
if (u === 'DHKEM') return { grup: 'ECDH', nume: 'DHKEM', primitiv: 'kem', functii: ['encapsulate', 'decapsulate'] };
}
if (clasa === 'Cipher') return cifruJca(v);
return { grup: 'UNKNOWN', motiv: `${clasa} "${v}" is not in this tool's catalog.` };
}
function cifruJca(v) {
const [alg0, mod0, pad0] = v.split('/').map((x) => (x || '').trim());
const alg = alg0.toUpperCase();
const mod = mod0 ? mod0.toLowerCase() : undefined;
const pad = pad0 ? pad0.toUpperCase() : '';
let m;
if (alg === 'RSA') {
const padding = /OAEP/.test(pad) ? 'oaep' : /PKCS1/.test(pad) ? 'pkcs1v15' : /NOPADDING/.test(pad) ? 'raw' : (pad ? 'other' : 'pkcs1v15');
const nota = pad ? undefined : 'Cipher "RSA" without padding means RSA/ECB/PKCS1Padding in the standard JCA providers.';
return { grup: 'RSA', primitiv: 'pke', padding, nota, functii: ['encrypt', 'decrypt'] };
}
if ((m = /^AES(?:_(128|192|256))?$/.exec(alg))) {
if (!mod) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'ecb', nota: 'Cipher "AES" without mode and padding means AES/ECB/PKCS5Padding in the standard JCA providers.', functii: ['encrypt', 'decrypt'] };
return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: mod === 'nopadding' ? undefined : mod, functii: ['encrypt', 'decrypt'] };
}
if (alg === 'DES') return { grup: 'CIPHER', nume: 'DES', mod, functii: ['encrypt', 'decrypt'] };
if (alg === 'DESEDE' || alg === 'TRIPLEDES') return { grup: 'CIPHER', nume: '3DES', mod, functii: ['encrypt', 'decrypt'] };
if (alg === 'RC4' || alg === 'ARCFOUR') return { grup: 'CIPHER', nume: 'RC4', functii: ['encrypt', 'decrypt'] };
if (alg === 'RC2') return { grup: 'CIPHER', nume: 'RC2', functii: ['encrypt', 'decrypt'] };
if (alg === 'BLOWFISH') return { grup: 'CIPHER', nume: 'Blowfish', mod, functii: ['encrypt', 'decrypt'] };
if (alg === 'CHACHA20-POLY1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt', 'decrypt'] };
if (alg === 'CHACHA20') return { grup: 'CIPHER', nume: 'ChaCha20', functii: ['encrypt', 'decrypt'] };
if (/^PBEWITH(MD5|SHA1)AND(DES|TRIPLEDES|RC2|RC4)/.test(alg)) return { grup: 'CIPHER', nume: /TRIPLEDES/.test(alg) ? '3DES' : /RC2/.test(alg) ? 'RC2' : /RC4/.test(alg) ? 'RC4' : 'DES', nota: `${v} derives the key with a weak hash.` };
if (/^ECIES/.test(alg)) return { grup: 'ECDH', nume: 'ECIES', primitiv: 'pke', functii: ['encrypt', 'decrypt'] };
return { grup: 'UNKNOWN', motiv: `Cipher transformation "${v}" is not in this tool's catalog.` };
}
// nume post-cuantice in JCA sau Bouncy Castle
export function pqNume(v) {
const t = String(v).trim();
let m;
if ((m = /^ML-?KEM(?:-(512|768|1024))?$/i.exec(t))) return { grup: 'MLKEM', param: m[1] };
if ((m = /^ML-?DSA(?:-(44|65|87))?$/i.exec(t))) return { grup: 'MLDSA', param: m[1] };
if ((m = /^SLH-?DSA(?:-(SHA2|SHAKE)-(128|192|256)([sfSF]))?$/i.exec(t))) return { grup: 'SLHDSA', param: m[1] ? `${m[1].toUpperCase()}-${m[2]}${m[3].toLowerCase()}` : undefined };
if ((m = /^(?:Falcon|FN-DSA)(?:-(512|1024))?$/i.exec(t))) return { grup: 'FALCON', param: m[1] };
if (/^(Kyber\d*|Dilithium\d*|SPHINCSPlus|SPHINCS\+)$/i.test(t)) return { grup: 'PREPQ', nume: t, primitiv: /kyber/i.test(t) ? 'kem' : 'signature' };
if (/^(XMSS|XMSSMT|XMSS\^MT|LMS|HSS)$/i.test(t)) return { grup: 'HASHSIG', nume: t.toUpperCase() };
if (/^(Rainbow|SIKE)$/i.test(t)) return { grup: 'BROKENPQ', nume: t, primitiv: /rainbow/i.test(t) ? 'signature' : 'kem' };
if (/^HQC(-\d+)?$/i.test(t)) return { grup: 'HQC', nume: t.toUpperCase() };
return null;
}
const BC_CLASA = [
[/^(ECDSASigner|ECNRSigner|DSTU4145Signer)$/, { grup: 'ECDSA' }],
[/^(DSASigner)$/, { grup: 'DSA' }],
[/^(RSADigestSigner|PSSSigner|RSAEngine|RSABlindedEngine|OAEPEncoding|PKCS1Encoding|RSAKeyPairGenerator|ISO9796d2Signer)$/, { grup: 'RSA' }],
[/^(ECKeyPairGenerator|ECNamedCurveTable|SECNamedCurves|ECDomainParameters)$/, { grup: 'EC' }],
[/^(ECDHBasicAgreement|ECDHCBasicAgreement|ECDHUnifiedAgreement)$/, { grup: 'ECDH' }],
[/^(DHBasicAgreement|DHAgreement|DHKeyPairGenerator)$/, { grup: 'DH' }],
[/^(Ed25519Signer|Ed25519ctxSigner|Ed25519phSigner|Ed25519KeyPairGenerator)$/, { grup: 'EDDSA', nume: 'Ed25519' }],
[/^(Ed448Signer|Ed448KeyPairGenerator)$/, { grup: 'EDDSA', nume: 'Ed448' }],
[/^(X25519Agreement|X25519KeyPairGenerator)$/, { grup: 'XDH', nume: 'X25519' }],
[/^(X448Agreement|X448KeyPairGenerator)$/, { grup: 'XDH', nume: 'X448' }],
[/^SecP256K1Curve$/, { grup: 'SECP256K1' }],
[/^MD5Digest$/, { grup: 'HASH', hash: 'MD5' }],
[/^SHA1Digest$/, { grup: 'HASH', hash: 'SHA1' }],
[/^SHA256Digest$/, { grup: 'HASH', hash: 'SHA256' }],
[/^SHA512Digest$/, { grup: 'HASH', hash: 'SHA512' }],
[/^(DESEngine)$/, { grup: 'CIPHER', nume: 'DES' }],
[/^(DESedeEngine)$/, { grup: 'CIPHER', nume: '3DES' }],
[/^(RC4Engine)$/, { grup: 'CIPHER', nume: 'RC4' }],
[/^(BlowfishEngine)$/, { grup: 'CIPHER', nume: 'Blowfish' }],
[/^(AESEngine|AESFastEngine|AESLightEngine)$/, { grup: 'CIPHER', nume: 'AES' }],
];
function bcActiv(cale) {
const clasa = cale.split('.').pop();
let m;
if ((m = /^(MLKEM|MLDSA|SLHDSA|Falcon|Kyber|Dilithium|SPHINCSPlus|XMSS|XMSSMT|LMS|HSS|Rainbow|SIKE|HQC|NTRU|NTRUPrime|BIKE|Frodo|CMCE|Picnic|Saber)/.exec(clasa))) {
const fam = m[1];
const map = { MLKEM: 'ML-KEM', MLDSA: 'ML-DSA', SLHDSA: 'SLH-DSA', SPHINCSPlus: 'SPHINCSPlus', XMSSMT: 'XMSSMT' };
const pq = pqNume(map[fam] || fam);
if (pq) return pq;
return { grup: 'UNKNOWN', motiv: `Bouncy Castle post-quantum class ${clasa}: scheme "${fam}" is not classified by this tool (not a finalized NIST standard).` };
}
if (/\.pqc\./.test(cale)) {
if (/BouncyCastlePQCProvider$/.test(clasa)) return { grup: 'UNKNOWN', motiv: 'Bouncy Castle PQC provider registered; the scheme is chosen elsewhere (not visible in this import).' };
return { grup: 'UNKNOWN', motiv: `Bouncy Castle post-quantum package class ${clasa}; scheme not recognized by this tool.` };
}
for (const [re, act] of BC_CLASA) if (re.test(clasa)) return act;
return null;
}
// Variabila din `v = <aici>` pe acelasi rand, citita INAPOI de la pozitie (ce dadea /([A-Za-z_$][\w$]*)\s*=\s*$/ pe textul randului
// de dinainte). Forma veche taia la fiecare apel tot randul pana la pozitie, deci un rand lung cu multe apeluri costa patratic
// (2026-09-29, revizuirea adversariala); cititul inapoi se opreste la primul caracter care nu poate face parte din tipar.
export function variabilaAtribuita(c, pos) {
const sp = (ch) => ch !== '\n' && /\s/.test(ch);
let j = pos - 1;
while (j >= 0 && sp(c[j])) j--;
if (j < 0 || c[j] !== '=') return null;
j--;
while (j >= 0 && sp(c[j])) j--;
const sf = j + 1;
while (j >= 0 && /[\w$]/.test(c[j])) j--;
let st = j + 1;
while (st < sf && /[0-9]/.test(c[st])) st++;
return st < sf ? c.slice(st, sf) : null;
}
export function detecteazaJava(ctx) {
const c = ctx.code;
const consumate = new Set();
for (const m of ctx.potriviri(/(?<![\w.])(KeyPairGenerator|Signature|Cipher|KeyAgreement|MessageDigest|Mac|KeyGenerator|KeyFactory|SecretKeyFactory|KEM|SSLContext|AlgorithmParameterGenerator)\s*\.\s*getInstance\s*\(/g)) {
const a = argumente(ctx, m.index + m[0].length);
const p = imparteArgumente(ctx, a.start, a.end);
const api = `${m[1]}.getInstance`;
const v = valoareArgument(ctx, p[0]);
if (v.fel === 'necunoscut') { ctx.adauga(m.index, api, 'call', necunoscut(v.expresie, api), { bucata: m[0] + v.expresie }); continue; }
if (v.fel !== 'literal' && v.fel !== 'rezolvat') continue;
const ex = v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {};
if (m[1] === 'SSLContext') {
const ver = TLSV[v.valoare];
ctx.adauga(m.index, api, 'config', ver ? { grup: 'TLS', param: ver, rol: 'only' } : { grup: 'UNKNOWN', motiv: `SSLContext protocol "${v.valoare}" is not in this tool's catalog.` }, { ...ex, bucata: `${m[0]}"${v.valoare}"` });
continue;
}
const dupa = c.slice(a.end, a.end + 2000);
const variabila = variabilaAtribuita(c, m.index);
if (variabila && m[1] === 'KeyPairGenerator' && /^(EC|ECDSA|ECDH)$/i.test(v.valoare)) {
const s = new RegExp(`(?<![\\w.])${variabila}\\s*\\.\\s*initialize\\s*\\(\\s*(new\\s+ECGenParameterSpec\\s*\\(\\s*")`).exec(dupa);
if (s) consumate.add(a.end + s.index + s[0].indexOf(s[1]));
}
ctx.adauga(m.index, api, 'call', activJca(m[1], v.valoare, dupa, variabila), { ...ex, bucata: `${m[0]}"${v.valoare}"` });
}
// curbe numite in afara unui KeyPairGenerator (ECGenParameterSpec, ECNamedCurveTable)
for (const m of ctx.potriviri(/(?<![\w.])(?:new\s+ECGenParameterSpec|ECNamedCurveTable\s*\.\s*getParameterSpec|SECNamedCurves\s*\.\s*getByName|CustomNamedCurves\s*\.\s*getByName)\s*\(\s*"([^"]+)"/g)) {
if (consumate.has(m.index)) continue;
ctx.adauga(m.index, m[0].replace(/\s*\(.*$/, ''), 'call', { grup: 'EC', curba: m[1] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])NamedParameterSpec\s*\.\s*(X25519|X448|ED25519|ED448|ML_DSA_44|ML_DSA_65|ML_DSA_87|ML_KEM_512|ML_KEM_768|ML_KEM_1024)\b/g)) {
const t = m[1].replace(/_/g, '-');
const act = pqNume(t) || (/^X/.test(t) ? { grup: 'XDH', nume: t } : { grup: 'EDDSA', nume: t === 'ED448' ? 'Ed448' : 'Ed25519' });
ctx.adauga(m.index, `NamedParameterSpec.${m[1]}`, 'call', act, { bucata: m[0] });
}
// protocoale TLS activate explicit
for (const m of ctx.potriviri(/(?<![\w.])set(?:Enabled)?Protocols\s*\(\s*new\s+String\s*\[\s*\]\s*\{/g)) {
const a = argumente(ctx, m.index + m[0].length);
const vers = [...a.text.matchAll(/"([^"]+)"/g)];
const lista = vers.map((x) => TLSV[x[1]]).filter(Boolean);
for (const x of vers) {
const ver = TLSV[x[1]];
if (!ver) continue;
const rol = ver === '1.2' && lista.includes('1.3') ? 'min' : 'only';
ctx.adauga(a.start + x.index, 'setEnabledProtocols', 'config', { grup: 'TLS', param: ver, rol }, { bucata: `"${x[1]}"` });
}
}
// proprietati de sistem ale JSSE
for (const m of ctx.potriviri(/(?<![\w.])System\s*\.\s*setProperty\s*\(\s*"(jdk\.tls\.(?:client|server)\.protocols|jdk\.tls\.namedGroups|https\.protocols)"\s*,\s*"([^"]*)"/g)) {
const off = m[0].lastIndexOf('"' + m[2] + '"') + 1;
let k = 0;
for (const bucata of m[2].split(',')) {
const t = bucata.trim();
const pos = m.index + off + k + bucata.indexOf(t);
k += bucata.length + 1;
if (!t) continue;
if (/namedGroups/.test(m[1])) ctx.adauga(pos, m[1], 'config', { ...grupTls(t), functii: ['keygen'] }, { bucata: `${m[1]}=${t}` });
else if (TLSV[t]) ctx.adauga(pos, m[1], 'config', { grup: 'TLS', param: TLSV[t], rol: 'only' }, { bucata: `${m[1]}=${t}` });
}
}
// Bouncy Castle: importurile si parametrii post-cuantici
for (const m of ctx.potriviri(/^[ \t]*import\s+(?:static\s+)?(org\.bouncycastle\.[\w.]+)\s*;/gm)) {
const act = bcActiv(m[1]);
if (act) ctx.adauga(m.index + m[0].indexOf('import'), `import ${m[1]}`, 'import', act, { suprimaDe: act.grup === 'EC' ? ['EC', 'ECDSA', 'ECDH', 'SECP256K1'] : 'AUTO', bucata: `import ${m[1]}` });
}
for (const m of ctx.potriviri(/(?<![\w.])(MLKEM|MLDSA|SLHDSA|Falcon|Kyber|Dilithium|SPHINCSPlus)Parameter(?:s|Spec)\s*\.\s*(\w+)/g)) {
const act = bcParam(m[1], m[2]);
if (act) ctx.adauga(m.index, `${m[1]}Parameters.${m[2]}`, 'call', act, { bucata: m[0] });
}
void curbaCanonica;
}
function bcParam(fam, p) {
let m;
if (fam === 'MLKEM' && (m = /(512|768|1024)/.exec(p))) return { grup: 'MLKEM', param: m[1] };
if (fam === 'MLDSA' && (m = /(44|65|87)/.exec(p))) return { grup: 'MLDSA', param: m[1] };
if (fam === 'SLHDSA' && (m = /(sha2|shake)_(128|192|256)([sf])/i.exec(p))) return { grup: 'SLHDSA', param: `${m[1].toUpperCase()}-${m[2]}${m[3].toLowerCase()}` };
if (fam === 'Falcon' && (m = /(512|1024)/.exec(p))) return { grup: 'FALCON', param: m[1] };
if (fam === 'Kyber' || fam === 'Dilithium' || fam === 'SPHINCSPlus') return { grup: 'PREPQ', nume: `${fam} ${p}`, primitiv: fam === 'Kyber' ? 'kem' : 'signature' };
return null;
}