// Material criptografic in text: blocuri PEM complete (antet, corp base64, subsol). // Cheile private se raporteaza DOAR ca locatie si tip; valoarea nu se tipareste niciodata. // Tipul cheii se afla parsand blocul in memorie cu node:crypto (date, nu cod executat). import { createPublicKey, createPrivateKey, X509Certificate } from 'node:crypto'; // Cautarea blocurilor e LINIARA in marimea textului (2026-09-29, revizuirea adversariala): forma veche, un singur regex cu // corp lenes si referinta inapoi, relua cautarea sfarsitului de la FIECARE antet BEGIN, deci un fisier de 1 MB cu antete fara // sfarsit costa minute. Acum pentru fiecare antet se cere PRIMUL `-----END -----` de dupa el si ca intre ele sa nu fie niciun // caracter din afara alfabetului corpului, adica exact ce potrivea regexul vechi; ambele pozitii se tin minte si se cauta numai // inainte, deci fiecare caracter se citeste de un numar marginit de ori. const SURSA_BEGIN = '-----BEGIN ((?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)-----'; const SURSA_AFARA_DIN_CORP = '[^A-Za-z0-9+/=\\s:,\\-\\r\\n]'; export function* blocuriPem(text) { const reBegin = new RegExp(SURSA_BEGIN, 'g'); const reRau = new RegExp(SURSA_AFARA_DIN_CORP, 'g'); // o cautare tinuta minte e buna pentru pozitia `de` daca a pornit la sau inainte de `de` si nu a gasit ceva inainte de `de` const valabil = (c, de) => c && c.la <= de && (c.poz < 0 || c.poz >= de); const sfarsit = new Map(); // tip -> { la, poz }: primul END al tipului la pozitia >= la (poz -1 = niciunul) let rau = null; // { la, poz }: primul caracter din afara alfabetului corpului, la pozitia >= la const urmatorulSfarsit = (tip, de) => { let c = sfarsit.get(tip); if (!valabil(c, de)) { c = { la: de, poz: text.indexOf(`-----END ${tip}-----`, de) }; sfarsit.set(tip, c); } return c.poz; }; const urmatorulRau = (de) => { if (!valabil(rau, de)) { reRau.lastIndex = de; const x = reRau.exec(text); rau = { la: de, poz: x ? x.index : -1 }; } return rau.poz; }; let m; while ((m = reBegin.exec(text))) { const tip = m[1]; const corpDe = m.index + m[0].length; const e = urmatorulSfarsit(tip, corpDe); const r = e < 0 ? -1 : urmatorulRau(corpDe); // fara potrivire: cautarea continua de la caracterul urmator, ca la regexul vechi (un antet poate incepe in liniutele celui de dinainte) if (e < 0 || (r >= 0 && r < e)) { reBegin.lastIndex = m.index + 1; continue; } const capat = e + `-----END ${tip}-----`.length; yield { index: m.index, tip, corp: text.slice(corpDe, e), bloc: text.slice(m.index, capat) }; reBegin.lastIndex = capat; } } function activCheie(ko) { const t = ko.asymmetricKeyType; const d = ko.asymmetricKeyDetails || {}; if (t === 'rsa' || t === 'rsa-pss') return { grup: 'RSA', param: d.modulusLength ? String(d.modulusLength) : undefined }; if (t === 'dsa') return { grup: 'DSA', param: d.modulusLength ? String(d.modulusLength) : undefined }; if (t === 'dh') return { grup: 'DH' }; if (t === 'ec') return { grup: 'EC', curba: d.namedCurve }; if (t === 'ed25519' || t === 'ed448') return { grup: 'EDDSA', nume: t === 'ed25519' ? 'Ed25519' : 'Ed448' }; if (t === 'x25519' || t === 'x448') return { grup: 'XDH', nume: t === 'x25519' ? 'X25519' : 'X448' }; if (/^ml-dsa-(44|65|87)$/.test(t)) return { grup: 'MLDSA', param: t.slice(7) }; if (/^ml-kem-(512|768|1024)$/.test(t)) return { grup: 'MLKEM', param: t.slice(7) }; if (/^slh-dsa/.test(t)) return { grup: 'SLHDSA', param: t.slice(8) }; return { grup: 'UNKNOWN', motiv: `Key type "${t}" is not in this tool's catalog.` }; } export function detecteazaPem(text, ctx) { for (const b of blocuriPem(text)) { const m = [b.bloc, b.tip, b.corp]; m.index = b.index; const corp = m[2].replace(/[\s]/g, '').replace(/^Proc-Type:.*?DEK-Info:[^A-Za-z0-9+/]*/, ''); if (corp.replace(/[^A-Za-z0-9+/=]/g, '').length < 64) continue; // un antet singur (de ex. intr-un parser) nu e material const tip = m[1]; let act; let assetType; if (tip === 'CERTIFICATE') { assetType = 'certificate'; try { const x = new X509Certificate(m[0]); act = { ...activCheie(x.publicKey), nota: 'Certificate public key.' }; act.certificat = { subjectName: x.subject.replace(/\n/g, ', '), issuerName: x.issuer.replace(/\n/g, ', '), notValidBefore: new Date(x.validFrom).toISOString(), notValidAfter: new Date(x.validTo).toISOString(), certificateFormat: 'X.509', certificateExtension: 'pem', fingerprintSha256: x.fingerprint256, }; } catch { act = { grup: 'UNKNOWN', motiv: 'PEM certificate block that could not be parsed.', certificat: { certificateFormat: 'X.509', certificateExtension: 'pem' } }; } } else if (/PUBLIC KEY/.test(tip)) { assetType = 'related-crypto-material'; try { act = activCheie(createPublicKey(m[0])); } catch { act = tip === 'RSA PUBLIC KEY' ? { grup: 'RSA' } : { grup: 'UNKNOWN', motiv: 'PEM public key block that could not be parsed.' }; } act.material = { type: 'public-key', format: 'PEM' }; } else { assetType = 'related-crypto-material'; if (/ENCRYPTED/.test(tip) || /Proc-Type:\s*4,ENCRYPTED/.test(m[2])) act = { grup: 'UNKNOWN', motiv: 'Encrypted private key: algorithm not visible without the passphrase.' }; else if (tip === 'OPENSSH PRIVATE KEY') act = { grup: 'UNKNOWN', motiv: 'OpenSSH private key: algorithm not parsed by this tool.' }; else { try { act = activCheie(createPrivateKey(m[0])); } catch { act = tip.startsWith('RSA') ? { grup: 'RSA' } : tip.startsWith('EC') ? { grup: 'EC' } : tip.startsWith('DSA') ? { grup: 'DSA' } : { grup: 'UNKNOWN', motiv: 'PEM private key block that could not be parsed.' }; } } act.material = { type: 'private-key', format: 'PEM' }; act.nota = 'Private key material stored in the scanned tree: if it is real, treat it as exposed and rotate it.'; } ctx.adauga(m.index, `PEM ${tip}`, 'pem', { ...act, assetType }, { bucata: `-----BEGIN ${tip}-----` }); } }