crypto-inventory 0.3.0: Rust detector (use-resolved names, static calls, generics, ring constants, Noise patterns), Cargo.toml manifests, BLAKE3; 44/44, negative control 21/21

This commit is contained in:
Aere Network 2026-10-18 05:35:11 +03:00
parent 86779c8334
commit f2f6217b4c
16 changed files with 942 additions and 21 deletions

View File

@ -1,7 +1,7 @@
# Citations in this repository that you cannot open # Citations in this repository that you cannot open
This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working
tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-14, of every path cited in this tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-18, of every path cited in this
repository that does not resolve to a published file. repository that does not resolve to a published file.
A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates

View File

@ -10,7 +10,7 @@ notarization command of the verification layer and for the agents' x402 wallet (
| [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates | | [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates |
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 | | [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL | | [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, PEM blocks, dependency manifests): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow | | [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, Rust, PEM blocks, dependency manifests including Cargo.toml): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log | | [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log |
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content | | [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content |
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
@ -39,7 +39,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
| pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) | | pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) |
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) | | pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) | | pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
| crypto-inventory | 37/37 (`node test/proba.mjs`); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 18/18 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) | | crypto-inventory | 44/44 (`node test/proba.mjs`, measured 2026-10-18 with the Rust detector); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 21/21 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
| verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) | | verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) |
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | | proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) | | readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
## Licence ## Licence
MIT, see [LICENSE](LICENSE). Files: 700 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 479, device-identity 11, exposure-report 18, readiness 4). MIT, see [LICENSE](LICENSE). Files: 705 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 47, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 479, device-identity 11, exposure-report 18, readiness 4).

View File

@ -29,12 +29,15 @@ function ruleaza(pagina) {
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'cons-web-ctl-')); const T = fs.mkdtempSync(path.join(os.tmpdir(), 'cons-web-ctl-'));
let rele = 0; let rele = 0;
try { try {
const m0 = path.join(T, 'martor.html'); fs.copyFileSync(PAGINA, m0); // (2026-10-16) capetele de rand se aduc la LF inainte de orice: copia de pe disc a unui checkout Windows (core.autocrlf) are CRLF,
// iar doua tipare cer '\n'; asa au iesit STRICAT ("apare de 0 ori") si poarta D-459 a stat NEMASURAT. Martorul e scris din ACEEASI
// forma ca plantarile, deci o plantare difera de martor numai prin defectul ei.
const src = fs.readFileSync(PAGINA, 'utf8').replace(/\r\n/g, '\n');
const m0 = path.join(T, 'martor.html'); fs.writeFileSync(m0, src);
const m = await ruleaza(m0); const m = await ruleaza(m0);
if (m.nemasurat) { console.log('NEMASURAT: proba nu poate rula aici (fara browser)'); process.exit(2); } if (m.nemasurat) { console.log('NEMASURAT: proba nu poate rula aici (fara browser)'); process.exit(2); }
if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde'); if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde');
else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); } else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); }
const src = fs.readFileSync(PAGINA, 'utf8');
const planteaza = async ([nume, din, inl, tinta], i) => { const planteaza = async ([nume, din, inl, tinta], i) => {
if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`]; if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`];
const f = path.join(T, `p${i}.html`); fs.writeFileSync(f, src.replace(din, inl)); const f = path.join(T, `p${i}.html`); fs.writeFileSync(f, src.replace(din, inl));

View File

@ -23,13 +23,23 @@ An API name that appears only inside a comment or inside a string literal is not
| Python | `cryptography.hazmat` (`rsa`, `ec`, `dsa`, `dh`, `ed25519`, `x25519`, `padding`, `hashes`, `algorithms`/`modes`, AEAD classes); PyCryptodome (`Crypto`/`Cryptodome`: `RSA`, `DSA`, `ECC`, ciphers and modes, `PKCS1_OAEP`, `pss`, `DSS`, hash `.new`); `hashlib` (including `hashlib.new` and `pbkdf2_hmac`); `hmac` with `digestmod`; `ecdsa`; PyJWT / python-jose algorithms; `oqs` (liboqs-python); `ssl` protocol constants and `TLSVersion` | | Python | `cryptography.hazmat` (`rsa`, `ec`, `dsa`, `dh`, `ed25519`, `x25519`, `padding`, `hashes`, `algorithms`/`modes`, AEAD classes); PyCryptodome (`Crypto`/`Cryptodome`: `RSA`, `DSA`, `ECC`, ciphers and modes, `PKCS1_OAEP`, `pss`, `DSS`, hash `.new`); `hashlib` (including `hashlib.new` and `pbkdf2_hmac`); `hmac` with `digestmod`; `ecdsa`; PyJWT / python-jose algorithms; `oqs` (liboqs-python); `ssl` protocol constants and `TLSVersion` |
| Java | `KeyPairGenerator`, `Signature`, `Cipher`, `KeyAgreement`, `MessageDigest`, `Mac`, `KeyGenerator`, `KeyFactory`, `SecretKeyFactory`, `KEM`, `SSLContext` `getInstance("...")`; `ECGenParameterSpec`, `NamedParameterSpec`; `setEnabledProtocols`; JSSE system properties (`jdk.tls.namedGroups`, protocol lists); Bouncy Castle imports and post-quantum parameter sets (`org.bouncycastle.pqc`, `MLKEMParameters`, `MLDSAParameters`, `SLHDSAParameters`, `FalconParameters`, ...) | | Java | `KeyPairGenerator`, `Signature`, `Cipher`, `KeyAgreement`, `MessageDigest`, `Mac`, `KeyGenerator`, `KeyFactory`, `SecretKeyFactory`, `KEM`, `SSLContext` `getInstance("...")`; `ECGenParameterSpec`, `NamedParameterSpec`; `setEnabledProtocols`; JSSE system properties (`jdk.tls.namedGroups`, protocol lists); Bouncy Castle imports and post-quantum parameter sets (`org.bouncycastle.pqc`, `MLKEMParameters`, `MLDSAParameters`, `SLHDSAParameters`, `FalconParameters`, ...) |
| Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) | | Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) |
| Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr` |
| Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys | | Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys |
| Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`: cryptographic libraries that are declared | | Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`, `Cargo.toml`: cryptographic libraries that are declared |
In Go an unused import does not compile, so an import of a crypto package is evidence of use; the In Go an unused import does not compile, so an import of a crypto package is evidence of use; the
import-level finding is dropped when the same file has a call-level finding that covers it. A blank import-level finding is dropped when the same file has a call-level finding that covers it. A blank
import (`_ "crypto/sha512"`) is reported with a note that it only registers an implementation. import (`_ "crypto/sha512"`) is reported with a note that it only registers an implementation.
In Rust (added in 0.3.0, 2026-10-18, after the first run on a tree of 1,320 Rust files returned zero Rust
findings) the `use` declarations decide what a name means: `SigningKey` is Ed25519 when it comes from
`ed25519-dalek`, ML-DSA from `ml-dsa`, SLH-DSA from `slh-dsa`, ECDSA from `p256`/`k256`, RSA from `rsa`.
A name like that with no `use` naming its crate is reported as `unknown` with the candidates listed, not
guessed. An unused import is only a warning in Rust, so an imported crypto crate is reported as an
`import` finding only when the file has no recognized call from it. Not recognized: methods on values
(`.sign()`, `.encrypt()`) without their constructor in the same file, macros, and a project's own wrapper
types (the file that calls the crate directly is the one reported).
When the algorithm is given by a variable, the tool resolves it only when the file assigns that name When the algorithm is given by a variable, the tool resolves it only when the file assigns that name
exactly once, to a plain string literal, and the name is not a function parameter or loop variable exactly once, to a plain string literal, and the name is not a function parameter or loop variable
(template literals whose `${...}` parts all resolve this way are also resolved). The finding then (template literals whose `${...}` parts all resolve this way are also resolved). The finding then
@ -176,8 +186,8 @@ console.log(renderSummary(result));
configuration files at run time. Those appear as `unknown` at best, or not at all. configuration files at run time. Those appear as `unknown` at best, or not at all.
- It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key - It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key
is reported like MD5 used for signatures; the reason text tells you what to check. is reported like MD5 used for signatures; the reason text tells you what to check.
- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java and Go. - It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java, Go and Rust.
Kotlin, Scala, C#, Rust, C and C++ files are only checked for PEM blocks. Files that are not UTF-8 Kotlin, Scala, C#, C and C++ files are only checked for PEM blocks. Files that are not UTF-8
text (including UTF-16) are treated as binary and counted as not read. text (including UTF-16) are treated as binary and counted as not read.
- It does not execute anything from the scanned tree, and it makes no network requests. - It does not execute anything from the scanned tree, and it makes no network requests.

View File

@ -79,6 +79,7 @@ const HASH = {
SHAKE256: { n: 'SHAKE256', c: CLS.S, xof: true }, SHAKE256: { n: 'SHAKE256', c: CLS.S, xof: true },
BLAKE2B: { n: 'BLAKE2b', c: CLS.S }, BLAKE2S: { n: 'BLAKE2s', c: CLS.S }, BLAKE2B: { n: 'BLAKE2b', c: CLS.S }, BLAKE2S: { n: 'BLAKE2s', c: CLS.S },
BLAKE2B512: { n: 'BLAKE2b-512', c: CLS.S }, BLAKE2S256: { n: 'BLAKE2s-256', c: CLS.S }, BLAKE2B512: { n: 'BLAKE2b-512', c: CLS.S }, BLAKE2S256: { n: 'BLAKE2s-256', c: CLS.S },
BLAKE3: { n: 'BLAKE3', c: CLS.S, q: 2, cl: 128 },
SM3: { n: 'SM3', c: CLS.S }, SM3: { n: 'SM3', c: CLS.S },
KECCAK256: { n: 'Keccak-256', c: CLS.S }, KECCAK256: { n: 'Keccak-256', c: CLS.S },
}; };
@ -97,6 +98,7 @@ export function hashCanonic(s) {
if (/^BLAKE2B-?512$/.test(t)) return 'BLAKE2B512'; if (/^BLAKE2B-?512$/.test(t)) return 'BLAKE2B512';
if (/^BLAKE2S-?256$/.test(t)) return 'BLAKE2S256'; if (/^BLAKE2S-?256$/.test(t)) return 'BLAKE2S256';
if (/^BLAKE2[BS]$/.test(t)) return t; if (/^BLAKE2[BS]$/.test(t)) return t;
if (/^BLAKE3$/.test(t)) return 'BLAKE3';
if (/^SM3$/.test(t)) return 'SM3'; if (/^SM3$/.test(t)) return 'SM3';
if (/^KECCAK-?256$/.test(t)) return 'KECCAK256'; if (/^KECCAK-?256$/.test(t)) return 'KECCAK256';
return null; return null;

View File

@ -0,0 +1,583 @@
// Detectorul Rust (0.3.0, 2026-10-18; scris dupa prima rulare a inventarului pe un arbore strain, cu 1.320 de fisiere Rust si
// zero constatari). Ce se recunoaste:
// - declaratiile `use` si `extern crate`: ele spun din ce crate vine fiecare nume din scop. Acelasi nume inseamna lucruri
// diferite (SigningKey e Ed25519 in ed25519-dalek, ML-DSA in ml-dsa, ECDSA in p256/k256, RSA in rsa), deci un nume
// ambiguu fara `use` nu se ghiceste: iese unknown, cu motivul scris;
// - apelurile statice Tip::metoda (Sha256::new, SigningKey::generate, Aes256Gcm::new_from_slice, RsaPrivateKey::new(rng, 2048)),
// parametrii generici (Hmac::<Sha256>, Hkdf::<Sha512>, type HmacSha256 = Hmac<Sha256>), functiile pe cale (blake3::hash,
// pbkdf2::pbkdf2_hmac::<Sha256>, pqcrypto_kyber::kyber1024::keypair), constantele lui ring (aead::AES_256_GCM),
// tiparele Noise ale lui snow, furnizorul criptografic al lui rustls, variantele Algorithm ale lui jsonwebtoken si oqs,
// parametrii ML-DSA / SLH-DSA / ML-KEM (MlDsa65, SlhDsaSha2_128s, MlKem768) si prekey-urile Kyber ale lui libsignal;
// - in Rust un import nefolosit e un avertisment, nu o eroare, deci importul e dovada SLABA: un crate criptografic importat se
// raporteaza ca 'import' (unknown) numai cand fisierul nu are niciun apel recunoscut din el.
// Nu se recunosc: metodele pe valori (.sign(), .update(), .encrypt()) fara constructorul lor in acelasi fisier, macro-urile,
// tipurile proprii care invelesc un crate (acolo se raporteaza fisierul care il cheama direct).
import { argumente, escapeRe } from './context.mjs';
import { jws } from './catalog.mjs';
const RADACINI_PROPRII = new Set(['crate', 'self', 'super', 'std', 'core', 'alloc']);
// crate-urile criptografice cunoscute (numele din `use`, cu liniuta jos) si grupurile pe care le pot produce (suprimarea importului)
const CRATE = {
sha2: { ofera: 'SHA-224/256/384/512', grupuri: ['HASH', 'MAC', 'KDF'] },
sha1: { ofera: 'SHA-1', grupuri: ['HASH', 'MAC', 'KDF'] },
md5: { ofera: 'MD5', grupuri: ['HASH'] },
sha3: { ofera: 'SHA3, Keccak, SHAKE', grupuri: ['HASH'] },
blake2: { ofera: 'BLAKE2b/BLAKE2s', grupuri: ['HASH', 'MAC'] },
blake3: { ofera: 'BLAKE3', grupuri: ['HASH'] },
hmac: { ofera: 'HMAC', grupuri: ['MAC'] },
hkdf: { ofera: 'HKDF', grupuri: ['KDF'] },
argon2: { ofera: 'Argon2', grupuri: ['KDF'] },
pbkdf2: { ofera: 'PBKDF2', grupuri: ['KDF'] },
scrypt: { ofera: 'scrypt', grupuri: ['KDF'] },
bcrypt: { ofera: 'bcrypt', grupuri: ['KDF'] },
ed25519_dalek: { ofera: 'Ed25519 signatures', grupuri: ['EDDSA'] },
x25519_dalek: { ofera: 'X25519 key agreement', grupuri: ['XDH'] },
curve25519_dalek: { ofera: 'Curve25519 group arithmetic (Ristretto, Edwards, Montgomery)', grupuri: ['EC', 'XDH', 'EDDSA'] },
chacha20poly1305: { ofera: 'ChaCha20-Poly1305 and XChaCha20-Poly1305', grupuri: ['CIPHER'] },
aes_gcm: { ofera: 'AES-GCM', grupuri: ['CIPHER'] },
aes_gcm_siv: { ofera: 'AES-GCM-SIV', grupuri: ['CIPHER'] },
aes_siv: { ofera: 'AES-SIV', grupuri: ['CIPHER'] },
aes: { ofera: 'the AES block cipher (mode chosen by the caller)', grupuri: ['CIPHER'] },
cbc: { ofera: 'the CBC mode', grupuri: ['CIPHER'] },
ctr: { ofera: 'the CTR mode', grupuri: ['CIPHER'] },
ecb: { ofera: 'the ECB mode', grupuri: ['CIPHER'] },
rsa: { ofera: 'RSA', grupuri: ['RSA'] },
p256: { ofera: 'NIST P-256 (ECDSA, ECDH)', grupuri: ['EC', 'ECDSA', 'ECDH'] },
p384: { ofera: 'NIST P-384 (ECDSA, ECDH)', grupuri: ['EC', 'ECDSA', 'ECDH'] },
p521: { ofera: 'NIST P-521 (ECDSA, ECDH)', grupuri: ['EC', 'ECDSA', 'ECDH'] },
k256: { ofera: 'secp256k1 (ECDSA, ECDH)', grupuri: ['SECP256K1', 'ECDH'] },
secp256k1: { ofera: 'secp256k1 (ECDSA, Schnorr)', grupuri: ['SECP256K1'] },
ml_kem: { ofera: 'ML-KEM (FIPS 203)', grupuri: ['MLKEM'] },
ml_dsa: { ofera: 'ML-DSA (FIPS 204)', grupuri: ['MLDSA'] },
slh_dsa: { ofera: 'SLH-DSA (FIPS 205)', grupuri: ['SLHDSA'] },
pqcrypto_kyber: { ofera: 'Kyber (pre-standard ML-KEM)', grupuri: ['PREPQ'] },
pqcrypto_mlkem: { ofera: 'ML-KEM (PQClean)', grupuri: ['MLKEM'] },
pqcrypto_dilithium: { ofera: 'Dilithium (pre-standard ML-DSA)', grupuri: ['PREPQ'] },
pqcrypto_mldsa: { ofera: 'ML-DSA (PQClean)', grupuri: ['MLDSA'] },
pqcrypto_sphincsplus: { ofera: 'SPHINCS+ (pre-standard SLH-DSA)', grupuri: ['PREPQ'] },
pqcrypto_falcon: { ofera: 'Falcon', grupuri: ['FALCON'] },
pqcrypto_hqc: { ofera: 'HQC', grupuri: ['HQC'] },
pqcrypto_classicmceliece: { ofera: 'Classic McEliece (NIST round 4, not standardized)', grupuri: ['UNKNOWN'] },
oqs: { ofera: 'liboqs: post-quantum KEMs and signatures', grupuri: ['MLKEM', 'MLDSA', 'FALCON', 'PREPQ', 'HQC', 'UNKNOWN'] },
libsignal_protocol: { ofera: 'Signal sessions: Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet', grupuri: ['XDH', 'PREPQ', 'MLKEM', 'LIB-MULTI'] },
ring: { ofera: 'ring: AES-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, PBKDF2, Ed25519, ECDSA P-256/P-384, RSA, X25519, ECDH', grupuri: ['HASH', 'MAC', 'KDF', 'CIPHER', 'EDDSA', 'ECDSA', 'RSA', 'XDH', 'ECDH'] },
openssl: { ofera: 'OpenSSL bindings', grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
rustls: { ofera: 'TLS (versions and key-exchange groups come from the crypto provider and the Cargo features)', grupuri: ['TLS', 'XDH', 'UNKNOWN'] },
tokio_rustls: { ofera: 'TLS over tokio (rustls)', grupuri: ['TLS'] },
hyper_rustls: { ofera: 'HTTPS for hyper (rustls)', grupuri: ['TLS'] },
native_tls: { ofera: 'the platform TLS stack', grupuri: ['TLS'] },
snow: { ofera: 'the Noise protocol framework', grupuri: ['XDH', 'CIPHER', 'HASH'] },
jsonwebtoken: { ofera: 'JWS HS*/RS*/PS*/ES*/EdDSA', grupuri: ['JWT-HMAC', 'RSA', 'ECDSA', 'EDDSA', 'JWT-NONE'] },
age: { ofera: 'age file encryption: X25519 recipients, scrypt passphrases, ChaCha20-Poly1305', grupuri: ['XDH', 'KDF', 'CIPHER'] },
crypto_box: { ofera: 'NaCl crypto_box: X25519 + XSalsa20/XChaCha20-Poly1305', grupuri: ['XDH', 'CIPHER'] },
arti_client: { ofera: 'a Tor client (circuit cryptography negotiated with relays)', grupuri: ['UNKNOWN'] },
};
const NUME_CARGO = (c) => c.replace(/_/g, '-');
// tipuri care au un singur proprietar printre crate-urile cunoscute: se recunosc si fara `use` (de ex. dupa un `use sha2::*`)
const UNIC = {};
for (const [crate, tipuri] of Object.entries({
sha2: ['Sha224', 'Sha256', 'Sha384', 'Sha512', 'Sha512_224', 'Sha512_256'],
sha1: ['Sha1'], md5: ['Md5'],
sha3: ['Sha3_224', 'Sha3_256', 'Sha3_384', 'Sha3_512', 'Keccak256', 'Shake128', 'Shake256'],
blake2: ['Blake2b512', 'Blake2s256', 'Blake2b', 'Blake2s', 'Blake2bMac', 'Blake2sMac'],
hmac: ['Hmac', 'SimpleHmac'], hkdf: ['Hkdf'], argon2: ['Argon2'],
chacha20poly1305: ['ChaCha20Poly1305', 'XChaCha20Poly1305', 'ChaCha8Poly1305', 'ChaCha12Poly1305', 'XChaCha8Poly1305', 'XChaCha12Poly1305'],
aes_gcm: ['Aes128Gcm', 'Aes256Gcm'], aes_gcm_siv: ['Aes128GcmSiv', 'Aes256GcmSiv'], aes_siv: ['Aes128SivAead', 'Aes256SivAead'],
aes: ['Aes128', 'Aes192', 'Aes256'],
rsa: ['RsaPrivateKey', 'RsaPublicKey', 'Pkcs1v15Sign', 'Pkcs1v15Encrypt', 'Oaep', 'Pss'],
x25519_dalek: ['StaticSecret', 'EphemeralSecret', 'ReusableSecret'],
curve25519_dalek: ['RistrettoPoint', 'EdwardsPoint', 'MontgomeryPoint'],
ml_kem: ['MlKem512', 'MlKem768', 'MlKem1024'], ml_dsa: ['MlDsa44', 'MlDsa65', 'MlDsa87'],
libsignal_protocol: ['PreKeyBundle', 'IdentityKeyPair', 'SignalProtocolAddress', 'KyberPreKeyRecord'],
arti_client: ['TorClient'],
})) for (const t of tipuri) UNIC[t] = crate;
const HASH_TIP = {
Sha224: 'SHA224', Sha256: 'SHA256', Sha384: 'SHA384', Sha512: 'SHA512', Sha512_224: 'SHA512/224', Sha512_256: 'SHA512/256',
Sha1: 'SHA1', Md5: 'MD5',
Sha3_224: 'SHA3-224', Sha3_256: 'SHA3-256', Sha3_384: 'SHA3-384', Sha3_512: 'SHA3-512', Keccak256: 'KECCAK256', Shake128: 'SHAKE128', Shake256: 'SHAKE256',
Blake2b512: 'BLAKE2B512', Blake2s256: 'BLAKE2S256', Blake2b: 'BLAKE2B', Blake2s: 'BLAKE2S',
};
const hashDinTip = (t) => HASH_TIP[String(t || '').split('::').pop().trim()] || null;
// crate-ul care detine tipul de hash dintr-un parametru generic (sha2::Sha256 -> sha2), ca `use sha2` sa conteze ca folosit
const crateHashDinTip = (t) => { const n = String(t || '').split('::').pop().trim(); return UNIC[n] || null; };
const CTOR = /^(new|default|new_from_slice|new_varkey|new_with_prefix|digest|digest_variable|new_variable|generate|generate_key|generate_nonce|random|random_from_rng|from|from_bytes|from_slice|from_keypair_bytes|from_pkcs8_der|from_pkcs8_pem|from_pkcs1_der|from_pkcs1_pem|from_public_key_der|from_public_key_pem|from_components|try_from|new_with_secret|from_seed|key_gen|keypair|try_sign|sign|verify)$/;
// --- `use` ---------------------------------------------------------------------------------------------------------------------
export function useuriRust(ctx) {
const r = []; // { crate, cale: [segmente], local, glob, pos }
for (const m of ctx.potriviri(/(?<![\w:])(?:pub(?:\([^)]*\))?\s+)?use\s+([^;{}]*(?:\{[^;]*\})?[^;]*);/g)) {
dezvolta(m[1].trim(), [], (cale, local, glob) => r.push({ crate: cale[0], cale, local, glob, pos: m.index, sf: m.index + m[0].length }));
}
for (const m of ctx.potriviri(/(?<![\w:])extern\s+crate\s+([A-Za-z_]\w*)(?:\s+as\s+([A-Za-z_]\w*))?\s*;/g)) {
r.push({ crate: m[1], cale: [m[1]], local: m[2] || m[1], glob: false, pos: m.index, sf: m.index + m[0].length });
}
return r.filter((u) => u.cale.length && !RADACINI_PROPRII.has(u.crate));
}
function dezvolta(text, prefix, emite) {
let t = text.trim();
if (t.startsWith('::')) t = t.slice(2);
const k = t.indexOf('{');
if (k >= 0) {
const baza = t.slice(0, k).replace(/::\s*$/, '').trim();
const inauntru = t.slice(k + 1, t.lastIndexOf('}'));
const pre = baza ? [...prefix, ...baza.split('::').map((s) => s.trim()).filter(Boolean)] : prefix;
for (const parte of imparteVirgule(inauntru)) if (parte.trim()) dezvolta(parte, pre, emite);
return;
}
const alias = /^(.*?)\s+as\s+([A-Za-z_]\w*|_)$/.exec(t);
const seg = (alias ? alias[1] : t).trim().split('::').map((s) => s.trim()).filter(Boolean);
if (!seg.length) return;
const cale = [...prefix, ...seg];
if (cale[cale.length - 1] === '*') { if (cale.length > 1) emite(cale.slice(0, -1), null, true); return; }
if (cale[cale.length - 1] === 'self') cale.pop();
if (!cale.length) return;
emite(cale, alias ? alias[2] : cale[cale.length - 1], false);
}
function imparteVirgule(s) {
const r = [];
let ad = 0;
let inc = 0;
for (let i = 0; i < s.length; i++) {
const c = s[i];
if (c === '{') ad++;
else if (c === '}') ad--;
else if (c === ',' && ad === 0) { r.push(s.slice(inc, i)); inc = i + 1; }
}
r.push(s.slice(inc));
return r;
}
// --- detectorul -----------------------------------------------------------------------------------------------------------------
export function detecteazaRust(ctx) {
const useuri = useuriRust(ctx);
const locale = new Map(); // numele din scop -> { crate, cale }
const globuri = new Set(); // crate-uri aduse cu `use crate::*` (numele lor UNIC se recunosc)
for (const u of useuri) {
if (u.glob) { globuri.add(u.crate); continue; }
if (u.local && u.local !== '_' && !locale.has(u.local)) locale.set(u.local, { crate: u.crate, cale: u.cale });
}
// numele aduse din propriul crate (use crate::chei::KeyPair, use super::*) nu sunt ambigue intre crate-urile criptografice
const proprii = new Set();
for (const m of ctx.potriviri(/(?<![\w:])(?:pub(?:\([^)]*\))?\s+)?use\s+((?:crate|self|super)::[^;]*);/g)) dezvolta(m[1].trim(), [], (cale, local) => { if (local) proprii.add(local); });
const folosite = new Set(); // crate-urile cu cel putin un apel recunoscut in acest fisier
// o potrivire din interiorul unei declaratii `use` nu e o folosire (MlDsa65 din `use ml_dsa::MlDsa65` nu e un apel)
const inUse = (pos) => useuri.some((u) => pos >= u.pos && pos < u.sf);
const potriviri = (re) => ctx.potriviri(re).filter((m) => !inUse(m.index));
// parametrul PQ al unui rand se raporteaza o singura data, oricate forme il poarta (SigningKey::<MlDsa65>::new si MlDsa65 din generic)
const pqPeRand = new Set();
const adauga = (crate, pos, api, fel, activ, bucata) => {
if (/^(MLDSA|MLKEM|SLHDSA)$/.test(activ.grup)) {
const k = `${ctx.poz(pos).line}|${activ.grup}|${activ.param || ''}`;
if (pqPeRand.has(k)) return;
pqPeRand.add(k);
}
if (crate) folosite.add(crate);
if (activ.hash && activ.crateHash) folosite.add(activ.crateHash);
const curat = { ...activ };
delete curat.crateHash;
delete curat.bucataExtra;
ctx.adauga(pos, api, fel, curat, { bucata });
};
// o cale scrisa in cod (prefixul `a::b::`) sau un nume de tip -> calea completa prin `use`
const rezolvaCale = (prefix, tip) => {
const seg = prefix ? prefix.split('::').filter(Boolean) : [];
if (seg.length) {
if (RADACINI_PROPRII.has(seg[0])) return null;
const u = locale.get(seg[0]);
const cale = u ? [...u.cale, ...seg.slice(1)] : seg;
return CRATE[cale[0]] ? { crate: cale[0], cale: [...cale, tip] } : null;
}
const u = locale.get(tip);
if (u) return CRATE[u.crate] ? { crate: u.crate, cale: u.cale } : null;
if (UNIC[tip]) return { crate: UNIC[tip], cale: [UNIC[tip], tip] };
return null;
};
// 1. apelurile statice Tip::metoda, cu sau fara turbofish (Hmac::<Sha256>::new_from_slice)
const RE_STATIC = /(?<![\w:])((?:[A-Za-z_]\w*::)*)([A-Z]\w*)\s*(?:::\s*<([^;{}()=]*?)>)?\s*::\s*([a-z_]\w*)\b/g;
for (const m of ctx.potriviri(RE_STATIC)) {
const [intreg, prefix, tip, generic, metoda] = m;
const r = rezolvaCale(prefix, tip);
if (!r) {
if (!prefix && /^(SigningKey|VerifyingKey|KeyPair|Keypair|SecretKey|PublicKey|Signature)$/.test(tip) && CTOR.test(metoda) && !locale.has(tip) && !proprii.has(tip)) {
// nume ambiguu, fara `use` care sa spuna crate-ul: nu se ghiceste
const tipuriLocale = new RegExp(`\\b(struct|enum|type|trait)\\s+${tip}\\b`).test(ctx.code);
if (!tipuriLocale) ctx.adauga(m.index, `${tip}::${metoda}`, 'call', { grup: 'UNKNOWN', nume: `${tip} (crate not resolved)`, motiv: `${tip}::${metoda} with no \`use\` in this file naming its crate: it may be Ed25519 (ed25519-dalek), ML-DSA (ml-dsa), SLH-DSA (slh-dsa), ECDSA (p256, k256) or RSA (rsa).` }, { bucata: intreg });
}
continue;
}
const activ = activStatic(r, metoda, generic, ctx, m);
if (activ) adauga(r.crate, m.index, `${prefix}${tip}::${metoda}`, 'call', activ, intreg + (activ.bucataExtra || ''));
}
// 2. aliasuri de tip cu parametru generic: type HmacSha256 = Hmac<Sha256>; type Kdf = Hkdf<Sha512>;
for (const m of ctx.potriviri(/(?<![\w:])type\s+\w+\s*=\s*((?:[A-Za-z_]\w*::)*)(Hmac|SimpleHmac|Hkdf)\s*<\s*([^>;]+)>/g)) {
const r = rezolvaCale(m[1], m[2]);
if (!r) continue;
const h = hashDinTip(m[3]);
const activ = m[2] === 'Hkdf' ? { grup: 'KDF', nume: 'HKDF', hash: h || undefined, functii: ['derive'], crateHash: crateHashDinTip(m[3]) }
: (h ? { grup: 'MAC', hash: h, functii: ['tag'], crateHash: crateHashDinTip(m[3]) } : { grup: 'UNKNOWN', motiv: `HMAC over "${m[3].trim()}", a hash this tool does not resolve.` });
adauga(r.crate, m.index, `type = ${m[2]}<${m[3].trim()}>`, 'config', activ, m[0]);
}
// 3. functii pe cale: blake3::hash(...), pbkdf2::pbkdf2_hmac::<Sha256>(...), pqcrypto_kyber::kyber1024::keypair(), scrypt::scrypt(...),
// rustls::crypto::ring::default_provider(), libsignal_protocol::process_prekey_bundle(...)
for (const m of ctx.potriviri(/(?<![\w:.])((?:[a-z_]\w*::)+)([a-z_]\w*)\s*(?:::\s*<([^;{}()=]*?)>)?\s*\(/g)) {
const seg = m[1].split('::').filter(Boolean);
const u = locale.get(seg[0]);
const cale = u ? [...u.cale, ...seg.slice(1), m[2]] : [...seg, m[2]];
if (RADACINI_PROPRII.has(cale[0]) || !CRATE[cale[0]]) continue;
const activ = activFunctie(cale, m[3], ctx, m);
if (activ) adauga(cale[0], m.index, cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || ''));
}
// functii importate direct: use pbkdf2::pbkdf2_hmac; use blake3::hash; use libsignal_protocol::process_prekey_bundle;
for (const [local, u] of locale) {
if (!/^[a-z_]\w*$/.test(local) || !CRATE[u.crate] || u.cale.length < 2) continue;
for (const m of ctx.potriviri(new RegExp(`(?<![\\w:.])${escapeRe(local)}\\s*(?:::\\s*<([^;{}()=]*?)>)?\\s*\\(`, 'g'))) {
const activ = activFunctie(u.cale, m[1], ctx, m);
if (activ) adauga(u.crate, m.index, u.cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || ''));
}
}
// 4. constantele lui ring (aead::AES_256_GCM, signature::ED25519) si variantele enum (Algorithm::RS256, KeyType::Kyber1024,
// SslVersion::TLS1_2), rezolvate prin `use`
for (const m of ctx.potriviri(/(?<![\w:])((?:[A-Za-z_]\w*::)+)([A-Z][A-Za-z0-9_]*)\b(?!\s*::|\s*\()/g)) {
const seg = m[1].split('::').filter(Boolean);
if (RADACINI_PROPRII.has(seg[0])) continue;
const u = locale.get(seg[0]);
const cale = u ? [...u.cale, ...seg.slice(1)] : seg;
if (!CRATE[cale[0]]) continue;
const activ = activConstanta(cale, m[2], ctx, m);
if (activ) adauga(cale[0], m.index, `${cale.join('::')}::${m[2]}`, activ.fel || 'config', activ, m[0]);
}
// variante importate direct (use jsonwebtoken::Algorithm; Algorithm::RS256) si tipuri parametru PQ scrise singure
for (const m of ctx.potriviri(/(?<![\w:])([A-Z]\w*)::([A-Z][A-Za-z0-9_]*)\b(?!\s*::|\s*\()/g)) {
const u = locale.get(m[1]);
if (!u || !CRATE[u.crate]) continue;
const activ = activConstanta(u.cale, m[2], ctx, m);
if (activ) adauga(u.crate, m.index, `${u.cale.join('::')}::${m[2]}`, activ.fel || 'config', activ, m[0]);
}
const peRand = new Set();
for (const m of potriviri(/(?<!\w)(MlDsa(44|65|87)|MlKem(512|768|1024)|SlhDsa(Sha2|Shake)_(128|192|256)([sf]))\b(?!\s*::)/g)) {
const linie = ctx.poz(m.index).line;
const k = `${linie}|${m[1]}`;
if (peRand.has(k)) continue;
peRand.add(k);
const r = rezolvaCale('', m[1]) || (m[1].startsWith('SlhDsa') ? { crate: 'slh_dsa' } : null);
if (!r) continue;
const activ = m[2] ? { grup: 'MLDSA', param: m[2] } : m[3] ? { grup: 'MLKEM', param: m[3] } : { grup: 'SLHDSA', param: `${m[4].toUpperCase()}-${m[5]}${m[6]}` };
adauga(r.crate, m.index, m[1], 'call', activ, m[0]);
}
// SLH-DSA cu numele scurte ale crate-ului (slh_dsa::Sha2_128s), numai cand crate-ul e in scop
if ([...locale.values()].some((u) => u.crate === 'slh_dsa') || globuri.has('slh_dsa')) {
for (const m of potriviri(/(?<!\w)(?:slh_dsa::)?(Sha2|Shake)_(128|192|256)([sf])\b(?!\s*::)/g)) {
const k = `${ctx.poz(m.index).line}|${m[0]}`;
if (peRand.has(k)) continue;
peRand.add(k);
adauga('slh_dsa', m.index, `slh_dsa::${m[1]}_${m[2]}${m[3]}`, 'call', { grup: 'SLHDSA', param: `${m[1].toUpperCase()}-${m[2]}${m[3]}` }, m[0]);
}
}
// 5. modurile de operare ale RustCrypto: cbc::Encryptor<Aes256>, ecb::Decryptor<aes::Aes128>, ctr::Ctr128BE<Aes256>
for (const m of ctx.potriviri(/(?<![\w:])(cbc|ecb|ctr|cfb_mode|ofb|ccm)::(Encryptor|Decryptor|Ctr\w*|Ccm)\s*(?:::)?\s*<\s*(?:aes::)?Aes(128|192|256)\b/g)) {
const mod = m[1] === 'cfb_mode' ? 'cfb' : m[1];
adauga('aes', m.index, `${m[1]}::${m[2]}<Aes${m[3]}>`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[3], mod, functii: [m[2] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]);
}
// 6. tiparul Noise al lui snow, scris ca sir: Noise_XX_25519_ChaChaPoly_SHA256
if ([...locale.values()].some((u) => u.crate === 'snow') || /(?<![\w:])snow::/.test(ctx.code)) {
for (const m of ctx.code.matchAll(/Noise_(\w+?)_(25519|448)_(ChaChaPoly|AESGCM)_(SHA256|SHA512|BLAKE2s|BLAKE2b)\b/g)) {
if (!ctx.inSir(m.index)) continue;
const pos = m.index;
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'XDH', nume: m[2] === '448' ? 'X448' : 'X25519', functii: ['keygen'] }, m[0]);
adauga('snow', pos, 'snow Noise pattern', 'config', m[3] === 'AESGCM' ? { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm', functii: ['encrypt'] } : { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] }, m[0]);
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'HASH', hash: m[4] === 'SHA256' ? 'SHA256' : m[4] === 'SHA512' ? 'SHA512' : m[4] === 'BLAKE2s' ? 'BLAKE2S' : 'BLAKE2B', functii: ['digest'] }, m[0]);
}
}
// 7. metodele pe valori ale lui secp256k1 (sign_ecdsa, sign_schnorr), cand crate-ul e in scop
if ([...locale.values()].some((u) => u.crate === 'secp256k1')) {
for (const m of ctx.potriviri(/\.\s*(sign_ecdsa|sign_ecdsa_recoverable|sign_schnorr|verify_ecdsa|verify_schnorr)\s*\(/g)) {
const schnorr = /schnorr/.test(m[1]);
adauga('secp256k1', m.index, `secp256k1 .${m[1]}`, 'call', { grup: 'SECP256K1', nume: schnorr ? 'Schnorr-secp256k1' : undefined, functii: [/^sign/.test(m[1]) ? 'sign' : 'verify'] }, m[0]);
}
}
// 8. openssl: versiunea minima/maxima TLS
if ([...locale.values()].some((u) => u.crate === 'openssl') || /(?<![\w:])openssl::/.test(ctx.code)) {
const vers = { SSL3: 'ssl3', TLS1: '1.0', TLS1_1: '1.1', TLS1_2: '1.2', TLS1_3: '1.3' };
for (const m of ctx.potriviri(/\.\s*set_(min|max)_proto_version\s*\(\s*Some\s*\(\s*(?:openssl::ssl::)?SslVersion::(SSL3|TLS1_3|TLS1_2|TLS1_1|TLS1)\b/g)) {
adauga('openssl', m.index, `openssl set_${m[1]}_proto_version`, 'config', { grup: 'TLS', param: vers[m[2]], rol: m[1] }, m[0]);
}
}
// 9. importurile: dovada slaba, numai pentru crate-urile fara niciun apel recunoscut in fisier (o singura data pe crate)
const raportate = new Set();
for (const u of useuri) {
const c = CRATE[u.crate];
if (!c || folosite.has(u.crate) || raportate.has(u.crate)) continue;
raportate.add(u.crate);
const nume = NUME_CARGO(u.crate);
const activ = c.grupuri[0] === 'TLS'
? { grup: 'UNKNOWN', nume, motiv: `Imports ${nume}: the TLS versions and key-exchange groups come from the crypto provider and the Cargo features (ring has no ML-KEM; aws-lc-rs offers X25519MLKEM768 from rustls 0.23.22), not visible in this file.` }
: c.grupuri[0] === 'UNKNOWN'
? { grup: 'UNKNOWN', nume, motiv: `Imports ${nume} (${c.ofera}); the cryptography is negotiated at run time, not visible here.` }
: { grup: 'LIB-MULTI', nume, motiv: `Imports ${nume} (${c.ofera}); no specific call recognized in this file.` };
// fara suprimare pe grup: un apel sha2 nu spune nimic despre importul blake2; crate-ul cu apel e deja exclus prin `folosite`
ctx.adauga(u.pos, `use ${u.crate}`, 'import', activ, { bucata: `use ${u.cale.join('::')}` });
}
}
// textul argumentelor apelului de dupa o potrivire: '(' trebuie sa urmeze imediat (spatii permise), altfel sir gol
function argsDupa(ctx, m) {
const dupa = /^\s*\(/.exec(ctx.code.slice(m.index + m[0].length, m.index + m[0].length + 8));
if (!dupa) return '';
const a = argumente(ctx, m.index + m[0].length + dupa[0].length);
return ctx.code.slice(a.start, a.end);
}
// --- clasificarea unui apel static Tip::metoda -----------------------------------------------------------------------------------
function activStatic(r, metoda, generic, ctx, m) {
const { crate, cale } = r;
const tip = cale[cale.length - 1]; // numele ORIGINAL al tipului, nu aliasul local din `use ... as`
const fn = (k) => [k];
const rol = /^(generate|random|random_from_rng|key_gen|keypair|new|default|from_seed)$/.test(metoda) ? 'keygen' : /^(try_sign|sign)$/.test(metoda) ? 'sign' : /^verify/.test(metoda) ? 'verify' : 'other';
switch (crate) {
case 'sha2': case 'sha1': case 'md5': case 'sha3': case 'blake2': {
const h = HASH_TIP[tip];
if (!h || !/^(new|default|digest|new_with_prefix|digest_variable|new_variable|new_keyed)$/.test(metoda)) return null;
if (/Mac$/.test(tip)) return { grup: 'MAC', hash: h, functii: ['tag'] };
return { grup: 'HASH', hash: h, functii: ['digest'] };
}
case 'blake3':
return tip === 'Hasher' && /^(new|new_keyed|new_derive_key|default)$/.test(metoda) ? { grup: 'HASH', hash: 'BLAKE3', functii: ['digest'] } : null;
case 'hmac': {
if (!/^(Hmac|SimpleHmac)$/.test(tip) || !/^(new|new_from_slice|new_varkey|default)$/.test(metoda)) return null;
const h = hashDinTip(generic);
return h ? { grup: 'MAC', hash: h, functii: ['tag'], crateHash: crateHashDinTip(generic) } : { grup: 'UNKNOWN', motiv: generic ? `HMAC over "${generic.trim()}", a hash this tool does not resolve.` : 'HMAC whose hash is a type alias or an inferred type parameter, not visible at this call.' };
}
case 'hkdf': {
if (tip !== 'Hkdf' || !/^(new|extract|from_prk)$/.test(metoda)) return null;
return { grup: 'KDF', nume: 'HKDF', hash: hashDinTip(generic) || undefined, functii: ['derive'], crateHash: crateHashDinTip(generic) };
}
case 'argon2': {
if (tip !== 'Argon2' || !/^(new|default|new_with_secret)$/.test(metoda)) return null;
const varianta = /Argon2(id|i|d)\b/.exec(ctx.code.slice(m.index, m.index + m[0].length + 200) || '');
return { grup: 'KDF', nume: varianta ? `Argon2${varianta[1]}` : 'Argon2', functii: ['derive'], bucataExtra: varianta ? ` ${varianta[0]}` : '' };
}
case 'ed25519_dalek':
if (/^(SigningKey|Keypair|ExpandedSecretKey)$/.test(tip)) return { grup: 'EDDSA', nume: 'Ed25519', functii: fn(rol === 'other' ? 'keygen' : rol) };
if (tip === 'VerifyingKey' || tip === 'PublicKey') return { grup: 'EDDSA', nume: 'Ed25519', functii: ['verify'] };
return null;
case 'x25519_dalek':
if (/^(StaticSecret|EphemeralSecret|ReusableSecret)$/.test(tip)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'] };
if (tip === 'PublicKey') return { grup: 'XDH', nume: 'X25519', functii: ['other'] };
return null;
case 'curve25519_dalek':
if (/^(RistrettoPoint|EdwardsPoint|MontgomeryPoint|Scalar)$/.test(tip)) return { grup: 'EC', curba: 'Curve25519', nume: 'Curve25519 group arithmetic', primitiv: 'other', nota: 'curve25519-dalek: Ristretto/Edwards/Montgomery group operations; the protocol built on them (signatures, key agreement, VRF) decides the primitive.' };
return null;
case 'chacha20poly1305': {
const n = { ChaCha20Poly1305: 'ChaCha20-Poly1305', XChaCha20Poly1305: 'XChaCha20-Poly1305', ChaCha8Poly1305: 'ChaCha8-Poly1305', ChaCha12Poly1305: 'ChaCha12-Poly1305', XChaCha8Poly1305: 'XChaCha8-Poly1305', XChaCha12Poly1305: 'XChaCha12-Poly1305' }[tip];
return n && /^(new|new_from_slice|generate_key|generate_nonce)$/.test(metoda) ? { grup: 'CIPHER', nume: n, functii: ['encrypt'] } : null;
}
case 'aes_gcm': case 'aes_gcm_siv': case 'aes_siv': case 'aes': {
const k = /^Aes(128|192|256)(Gcm|GcmSiv|SivAead)?$/.exec(tip);
if (!k || !/^(new|new_from_slice|generate_key|generate_nonce)$/.test(metoda)) return null;
const mod = k[2] === 'Gcm' ? 'gcm' : k[2] === 'GcmSiv' ? 'gcm-siv' : k[2] === 'SivAead' ? 'siv' : undefined;
return { grup: 'CIPHER', nume: 'AES', param: k[1], mod, functii: ['encrypt'], nota: mod ? undefined : 'Raw block cipher (one block at a time); the mode of operation is chosen by the caller.' };
}
case 'rsa': {
if (tip === 'RsaPrivateKey') {
let bits;
if (metoda === 'new' || metoda === 'new_with_exp') {
const parti = argsDupa(ctx, m).split(',');
const b = parti[1] && /^\s*(\d{3,5})\s*$/.exec(parti[1]);
if (b) bits = b[1];
}
return { grup: 'RSA', param: bits, functii: fn(rol === 'other' ? 'other' : rol), bucataExtra: bits ? ` ${bits}` : '' };
}
if (tip === 'RsaPublicKey') return { grup: 'RSA', functii: ['other'] };
if (tip === 'Pkcs1v15Sign') return { grup: 'RSA', primitiv: 'signature', padding: 'pkcs1v15', hash: hashDinTip(generic) || undefined, functii: ['sign', 'verify'] };
if (tip === 'Pss') return { grup: 'RSA', primitiv: 'signature', padding: 'pss', hash: hashDinTip(generic) || undefined, functii: ['sign', 'verify'] };
if (tip === 'Oaep') return { grup: 'RSA', primitiv: 'pke', padding: 'oaep', hash: hashDinTip(generic) || undefined, functii: ['encrypt'] };
if (tip === 'Pkcs1v15Encrypt') return { grup: 'RSA', primitiv: 'pke', padding: 'pkcs1v15', functii: ['encrypt'] };
return null;
}
case 'p256': case 'p384': case 'p521': case 'k256': {
const curba = { p256: 'secp256r1', p384: 'secp384r1', p521: 'secp521r1', k256: 'secp256k1' }[crate];
const g = crate === 'k256' ? 'SECP256K1' : 'ECDSA';
if (cale.includes('ecdh') || /^(EphemeralSecret)$/.test(tip)) return { grup: 'ECDH', curba, functii: ['keygen'] };
if (cale.includes('ecdsa') || /^(SigningKey|VerifyingKey|Signature)$/.test(tip)) return { grup: g, curba, functii: fn(rol === 'other' ? (tip === 'VerifyingKey' ? 'verify' : 'keygen') : rol) };
if (/^(SecretKey|PublicKey|NonZeroScalar|AffinePoint|ProjectivePoint)$/.test(tip)) return { grup: crate === 'k256' ? 'SECP256K1' : 'EC', curba, functii: ['keygen'] };
return null;
}
case 'secp256k1':
if (/^(Secp256k1|SecretKey|Keypair|KeyPair|PublicKey|XOnlyPublicKey)$/.test(tip)) return { grup: 'SECP256K1', functii: ['keygen'] };
return null;
case 'ml_kem': {
const k = /^MlKem(512|768|1024)$/.exec(tip);
return k ? { grup: 'MLKEM', param: k[1], functii: fn(/^generate/.test(metoda) ? 'keygen' : /encaps/.test(metoda) ? 'encapsulate' : /decaps/.test(metoda) ? 'decapsulate' : 'other') } : null;
}
case 'ml_dsa': {
const k = /^MlDsa(44|65|87)$/.exec(tip);
if (k) return { grup: 'MLDSA', param: k[1], functii: fn(rol) };
const g = /MlDsa(44|65|87)/.exec(generic || '');
if (/^(SigningKey|VerifyingKey|KeyPair|Signature)$/.test(tip)) return { grup: 'MLDSA', param: g ? g[1] : undefined, functii: fn(rol === 'other' ? (tip === 'VerifyingKey' ? 'verify' : 'keygen') : rol) };
return null;
}
case 'slh_dsa': {
const k = /^(?:SlhDsa)?(Sha2|Shake)_(128|192|256)([sf])$/.exec(tip);
if (k) return { grup: 'SLHDSA', param: `${k[1].toUpperCase()}-${k[2]}${k[3]}`, functii: fn(rol) };
const g = /(?:SlhDsa)?(Sha2|Shake)_(128|192|256)([sf])/.exec(generic || '');
if (/^(SigningKey|VerifyingKey|Signature)$/.test(tip)) return { grup: 'SLHDSA', param: g ? `${g[1].toUpperCase()}-${g[2]}${g[3]}` : undefined, functii: fn(rol === 'other' ? (tip === 'VerifyingKey' ? 'verify' : 'keygen') : rol) };
return null;
}
case 'libsignal_protocol':
if (tip === 'PreKeyBundle' && metoda === 'new') return { grup: 'LIB-MULTI', nume: 'libsignal session setup (X3DH/PQXDH)', motiv: 'libsignal prekey bundle: Curve25519 identity and signed prekey; a Kyber prekey in the bundle makes the session PQXDH (hybrid). Which prekeys the bundle carries is decided at run time.' };
if (/^(IdentityKeyPair|KeyPair|PrivateKey)$/.test(tip) && /^(generate|new|from_private_key_bytes|deserialize)$/.test(metoda)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: 'libsignal identity and prekey keys are Curve25519 (signatures over them are XEdDSA).' };
if (tip === 'KyberPreKeyRecord' || tip === 'KeyType') return null;
return null;
case 'ring':
if (tip === 'Ed25519KeyPair') return { grup: 'EDDSA', nume: 'Ed25519', functii: fn(/generate/.test(metoda) ? 'keygen' : 'other') };
if (tip === 'RsaKeyPair') return { grup: 'RSA', primitiv: 'signature', functii: ['sign'] };
if (tip === 'EcdsaKeyPair') { const c = /P(256|384)/.exec(ctx.code.slice(m.index, m.index + 200)); return { grup: 'ECDSA', curba: c ? `secp${c[1]}r1` : undefined, functii: fn(/generate/.test(metoda) ? 'keygen' : 'other') }; }
if (tip === 'EphemeralPrivateKey' && metoda === 'generate') { const c = /(X25519|ECDH_P256|ECDH_P384)/.exec(ctx.code.slice(m.index, m.index + 200)); return c && c[1] !== 'X25519' ? { grup: 'ECDH', curba: c[1] === 'ECDH_P256' ? 'secp256r1' : 'secp384r1', functii: ['keygen'] } : { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; }
return null;
case 'openssl': {
if (tip === 'MessageDigest') { const h = { md5: 'MD5', sha1: 'SHA1', sha224: 'SHA224', sha256: 'SHA256', sha384: 'SHA384', sha512: 'SHA512', sha3_224: 'SHA3-224', sha3_256: 'SHA3-256', sha3_384: 'SHA3-384', sha3_512: 'SHA3-512', ripemd160: 'RIPEMD160', shake_128: 'SHAKE128', shake_256: 'SHAKE256' }[metoda]; return h ? { grup: 'HASH', hash: h, functii: ['digest'] } : null; }
if (tip === 'Cipher') { const k = /^(aes|des_ede3|des|rc4|rc2|bf|chacha20_poly1305|chacha20|camellia|sm4|aria)_?(\d{3})?_?(\w+)?$/.exec(metoda); if (!k) return null; const nume = k[1] === 'des_ede3' ? '3DES' : k[1] === 'bf' ? 'Blowfish' : k[1] === 'chacha20_poly1305' ? 'ChaCha20-Poly1305' : k[1] === 'chacha20' ? 'ChaCha20' : k[1].toUpperCase(); return { grup: 'CIPHER', nume, param: k[2], mod: k[3] ? k[3].replace(/_/g, '-') : undefined, functii: ['encrypt'] }; }
if (tip === 'Rsa' && /^(generate|generate_with_e)$/.test(metoda)) { const b = /^\s*(\d{3,5})\s*(?:,|$)/.exec(argsDupa(ctx, m)); return { grup: 'RSA', param: b ? b[1] : undefined, functii: ['keygen'], bucataExtra: b ? ` ${b[1]}` : '' }; }
if (tip === 'EcKey' && /^(generate|from_curve_name)$/.test(metoda)) { const c = /Nid::(X9_62_PRIME256V1|SECP256K1|SECP384R1|SECP521R1|SECP224R1|BRAINPOOL_P256R1|BRAINPOOL_P384R1|BRAINPOOL_P512R1)/.exec(ctx.code.slice(m.index, m.index + 300)); const curba = c ? ({ X9_62_PRIME256V1: 'secp256r1', SECP256K1: 'secp256k1', SECP384R1: 'secp384r1', SECP521R1: 'secp521r1', SECP224R1: 'secp224r1', BRAINPOOL_P256R1: 'brainpoolP256r1', BRAINPOOL_P384R1: 'brainpoolP384r1', BRAINPOOL_P512R1: 'brainpoolP512r1' })[c[1]] : undefined; return { grup: 'EC', curba, functii: ['keygen'] }; }
if (tip === 'PKey') { const k = /^generate_(ed25519|ed448|x25519|x448)$/.exec(metoda); if (!k) return null; return /^ed/.test(k[1]) ? { grup: 'EDDSA', nume: k[1] === 'ed448' ? 'Ed448' : 'Ed25519', functii: ['keygen'] } : { grup: 'XDH', nume: k[1] === 'x448' ? 'X448' : 'X25519', functii: ['keygen'] }; }
if (/^(SslConnector|SslAcceptor|SslContext)$/.test(tip) && /^(builder|mozilla_modern|mozilla_intermediate|mozilla_modern_v5|mozilla_intermediate_v5)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `openssl ${tip}` };
return null;
}
case 'rustls': case 'tokio_rustls': case 'hyper_rustls': case 'native_tls':
if (/^(ClientConfig|ServerConfig)$/.test(tip) && /^(builder|builder_with_provider|builder_with_protocol_versions)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `rustls ${tip}` };
if (/^(TlsConnector|TlsAcceptor|HttpsConnectorBuilder)$/.test(tip)) return { grup: 'TLS', param: 'negotiated', nume: `${NUME_CARGO(crate)} ${tip}` };
return null;
case 'pqcrypto_kyber': case 'pqcrypto_mlkem': case 'pqcrypto_dilithium': case 'pqcrypto_mldsa': case 'pqcrypto_sphincsplus': case 'pqcrypto_falcon': case 'pqcrypto_hqc': case 'pqcrypto_classicmceliece':
return null; // functiile lor sunt pe cale (pasul 3)
case 'oqs':
return null; // variantele Algorithm (pasul 4)
case 'age':
if (cale.includes('x25519') && /^(Identity|Recipient)$/.test(tip)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: 'age: X25519 recipients; the file key is wrapped and the payload encrypted with ChaCha20-Poly1305.' };
if (cale.includes('scrypt') && /^(Identity|Recipient)$/.test(tip)) return { grup: 'KDF', nume: 'scrypt', functii: ['derive'] };
return null;
case 'crypto_box':
if (/^(SalsaBox|ChaChaBox)$/.test(tip) && metoda === 'new') return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: `crypto_box: X25519 key agreement with ${tip === 'SalsaBox' ? 'XSalsa20-Poly1305' : 'XChaCha20-Poly1305'}.` };
if (tip === 'SecretKey' && /^(generate|from_bytes)$/.test(metoda)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'] };
return null;
case 'arti_client':
if (tip === 'TorClient') return { grup: 'UNKNOWN', nume: 'Tor client (arti)', motiv: 'Tor circuit and channel cryptography is negotiated with the relays by the Tor protocol version in use, not visible in this file.' };
return null;
case 'jsonwebtoken':
return null; // variantele Algorithm (pasul 4)
default:
return null;
}
}
// --- functii pe cale ------------------------------------------------------------------------------------------------------------
function activFunctie(cale, generic, ctx, m) {
const crate = cale[0];
const fnume = cale[cale.length - 1];
const modul = cale.length >= 3 ? cale[cale.length - 2] : null;
switch (crate) {
case 'blake3':
return /^(hash|keyed_hash|derive_key)$/.test(fnume) ? { grup: 'HASH', hash: 'BLAKE3', functii: ['digest'] } : null;
case 'pbkdf2': {
if (!/^pbkdf2(_hmac|_hmac_array|_array|_check|_simple)?$/.test(fnume)) return null;
const h = hashDinTip(generic ? generic.replace(/^.*?<\s*/, '').replace(/\s*>.*$/, '') : '');
return { grup: 'KDF', nume: 'PBKDF2', hash: h || undefined, functii: ['derive'], crateHash: crateHashDinTip(generic ? generic.replace(/^.*?<\s*/, '').replace(/\s*>.*$/, '') : '') };
}
case 'scrypt':
return /^(scrypt|scrypt_simple|scrypt_check)$/.test(fnume) ? { grup: 'KDF', nume: 'scrypt', functii: ['derive'] } : null;
case 'bcrypt':
return /^(hash|hash_with_salt|verify|hash_with_result)$/.test(fnume) ? { grup: 'KDF', nume: 'bcrypt', functii: ['derive'] } : null;
case 'rustls':
if (fnume === 'default_provider' && modul === 'ring') return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: 'rustls with the ring provider: ring implements no ML-KEM, so the key-exchange groups are X25519, P-256 and P-384 only (no X25519MLKEM768).' };
if (fnume === 'default_provider' && modul === 'aws_lc_rs') return { grup: 'UNKNOWN', nume: 'rustls aws-lc-rs provider', motiv: 'rustls with the aws-lc-rs provider: X25519MLKEM768 is offered by default from rustls 0.23.22 (and only with the prefer-post-quantum feature before 0.23.22); the rustls version is in Cargo.lock, not here.' };
return null;
case 'libsignal_protocol':
if (fnume === 'process_prekey_bundle') return { grup: 'LIB-MULTI', nume: 'libsignal session setup (X3DH/PQXDH)', motiv: 'libsignal session setup from a prekey bundle: Curve25519 identity and signed prekey; a Kyber prekey in the bundle makes the session PQXDH (hybrid). Which prekeys the bundle carries is decided at run time.' };
return null;
case 'pqcrypto_kyber': case 'pqcrypto_mlkem': case 'pqcrypto_dilithium': case 'pqcrypto_mldsa': case 'pqcrypto_sphincsplus': case 'pqcrypto_falcon': case 'pqcrypto_hqc': case 'pqcrypto_classicmceliece': {
if (!/^(keypair|encapsulate|decapsulate|sign|open|detached_sign|verify_detached_signature)$/.test(fnume) || !modul) return null;
const f = /^(keypair)$/.test(fnume) ? 'keygen' : /encaps/.test(fnume) ? 'encapsulate' : /decaps/.test(fnume) ? 'decapsulate' : /sign$/.test(fnume) ? 'sign' : /open|verify/.test(fnume) ? 'verify' : 'other';
let k;
if ((k = /^kyber(512|768|1024)(90s)?$/.exec(modul))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem', functii: [f] };
if ((k = /^mlkem(512|768|1024)$/.exec(modul))) return { grup: 'MLKEM', param: k[1], functii: [f] };
if ((k = /^dilithium(2|3|5)(aes)?$/.exec(modul))) return { grup: 'PREPQ', nume: `Dilithium${k[1]}`, primitiv: 'signature', functii: [f] };
if ((k = /^mldsa(44|65|87)$/.exec(modul))) return { grup: 'MLDSA', param: k[1], functii: [f] };
if (/^sphincs/.test(modul)) return { grup: 'PREPQ', nume: `SPHINCS+ (${modul})`, primitiv: 'signature', functii: [f] };
if ((k = /^falcon(?:padded)?(512|1024)$/.exec(modul))) return { grup: 'FALCON', param: k[1], functii: [f] };
if ((k = /^hqc(128|192|256)$/.exec(modul))) return { grup: 'HQC', nume: `HQC-${k[1]}`, functii: [f] };
if (/^mceliece/.test(modul)) return { grup: 'UNKNOWN', nume: `Classic McEliece (${modul})`, motiv: 'Classic McEliece: code-based KEM, NIST round-4 candidate, not standardized.' };
return null;
}
default:
return null;
}
}
// --- constante si variante enum (ring, jsonwebtoken, oqs, libsignal kem::KeyType) ----------------------------------------------------
function activConstanta(cale, nume, ctx, m) {
const crate = cale[0];
const modul = cale[cale.length - 1];
let k;
if (crate === 'ring') {
if (modul === 'signature') {
if (nume === 'ED25519') return { grup: 'EDDSA', nume: 'Ed25519', functii: ['verify'] };
if ((k = /^ECDSA_P(256|384)_SHA(256|384)_(ASN1|FIXED)(_SIGNING)?$/.exec(nume))) return { grup: 'ECDSA', curba: `secp${k[1]}r1`, hash: `SHA${k[2]}`, functii: [k[4] ? 'sign' : 'verify'] };
if ((k = /^RSA_(PKCS1|PSS)_(?:\d+_\d+_)?SHA(1|256|384|512)(_LEGACY_KEY)?$/.exec(nume))) return { grup: 'RSA', primitiv: 'signature', padding: k[1] === 'PSS' ? 'pss' : 'pkcs1v15', hash: `SHA${k[2]}`, functii: ['sign', 'verify'] };
return null;
}
if (modul === 'agreement') {
if (nume === 'X25519') return { grup: 'XDH', nume: 'X25519', functii: ['keygen'] };
if ((k = /^ECDH_P(256|384)$/.exec(nume))) return { grup: 'ECDH', curba: `secp${k[1]}r1`, functii: ['keygen'] };
return null;
}
if (modul === 'digest') {
if ((k = /^SHA(1_FOR_LEGACY_USE_ONLY|256|384|512|512_256)$/.exec(nume))) return { grup: 'HASH', hash: k[1].startsWith('1') ? 'SHA1' : k[1] === '512_256' ? 'SHA512/256' : `SHA${k[1]}`, functii: ['digest'] };
return null;
}
if (modul === 'aead') {
if ((k = /^AES_(128|256)_GCM$/.exec(nume))) return { grup: 'CIPHER', nume: 'AES', param: k[1], mod: 'gcm', functii: ['encrypt'] };
if (nume === 'CHACHA20_POLY1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] };
return null;
}
if (modul === 'hmac' && (k = /^HMAC_SHA(1_FOR_LEGACY_USE_ONLY|256|384|512)$/.exec(nume))) return { grup: 'MAC', hash: k[1].startsWith('1') ? 'SHA1' : `SHA${k[1]}`, functii: ['tag'] };
if (modul === 'hkdf' && (k = /^HKDF_SHA(1_FOR_LEGACY_USE_ONLY|256|384|512)$/.exec(nume))) return { grup: 'KDF', nume: 'HKDF', hash: k[1].startsWith('1') ? 'SHA1' : `SHA${k[1]}`, functii: ['derive'] };
if (modul === 'pbkdf2' && (k = /^PBKDF2_HMAC_SHA(1|256|384|512)$/.exec(nume))) return { grup: 'KDF', nume: 'PBKDF2', hash: `SHA${k[1]}`, functii: ['derive'] };
return null;
}
if (crate === 'jsonwebtoken' && modul === 'Algorithm') {
return /^(HS256|HS384|HS512|RS256|RS384|RS512|PS256|PS384|PS512|ES256|ES384|EdDSA)$/.test(nume) ? { ...jws(nume), functii: ['sign', 'verify'] } : null;
}
if (crate === 'oqs' && modul === 'Algorithm') {
if ((k = /^MlKem(512|768|1024)$/.exec(nume))) return { grup: 'MLKEM', param: k[1] };
if ((k = /^Kyber(512|768|1024)/.exec(nume))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem' };
if ((k = /^MlDsa(44|65|87)$/.exec(nume))) return { grup: 'MLDSA', param: k[1] };
if ((k = /^Dilithium(2|3|5)/.exec(nume))) return { grup: 'PREPQ', nume: `Dilithium${k[1]}`, primitiv: 'signature' };
if ((k = /^Falcon(512|1024)$/.exec(nume))) return { grup: 'FALCON', param: k[1] };
if (/^Sphincs/.test(nume)) return { grup: 'PREPQ', nume: `SPHINCS+ (${nume})`, primitiv: 'signature' };
if ((k = /^Hqc(128|192|256)$/.exec(nume))) return { grup: 'HQC', nume: `HQC-${k[1]}` };
if (/^(BikeL|ClassicMcEliece|FrodoKem|Ntru|Mayo|Cross|Uov|Snova)/.test(nume)) return { grup: 'UNKNOWN', nume: `liboqs ${nume}`, motiv: `${nume}: a post-quantum scheme that is not a NIST standard (liboqs experimental or round-4 algorithm).` };
return null;
}
if (crate === 'libsignal_protocol' && modul === 'KeyType') {
if ((k = /^Kyber(768|1024)$/.exec(nume))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem', nota: 'Signal PQXDH prekeys: CRYSTALS-Kyber (the pre-standard form of ML-KEM).' };
if ((k = /^M[lL][kK][eE][mM](768|1024)$/.exec(nume))) return { grup: 'MLKEM', param: k[1] };
return null;
}
return null;
}
export { CRATE as CRATE_RUST };

View File

@ -15,6 +15,7 @@ export function curata(text, limbaj) {
case 'java': return curataC(text, { textBlock: true }); case 'java': return curataC(text, { textBlock: true });
case 'go': return curataC(text, { raw: true }); case 'go': return curataC(text, { raw: true });
case 'python': return curataPython(text); case 'python': return curataPython(text);
case 'rust': return curataRust(text);
default: return { code: text, siruri: [] }; default: return { code: text, siruri: [] };
} }
} }
@ -199,6 +200,73 @@ function curataPython(text) {
return { code: out.join(''), siruri }; return { code: out.join(''), siruri };
} }
// Rust (0.3.0): comentariile bloc se IMBRICA; sirurile pot trece peste randuri si au forme brute (r"...", r#"..."#, br"...",
// cr#"..."#) si de octeti (b"..."); apostroful NU deschide un sir decat ca literal char ('a', '\n', '\u{1F600}', b'x'): altfel e
// o durata de viata ('a, 'static) sau o eticheta ('outer:), si un lexer C ar fi deschis acolo un "sir" pana la urmatorul apostrof.
function curataRust(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (d === '/' && c === '/') { // (scris invers fata de lexerul C: controlul negativ isi ancoreaza ciotul pe acela)
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
let j = i + 2;
let ad = 1;
while (j < n && ad > 0) {
if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; }
if (text[j] === '*' && text[j + 1] === '/') { ad--; j += 2; continue; }
j++;
}
goleste(i, j); i = j; continue;
}
const dupaCuvant = i > 0 && /\w/.test(text[i - 1]);
if (!dupaCuvant && (c === 'r' || ((c === 'b' || c === 'c') && d === 'r'))) {
let j = c === 'r' ? i + 1 : i + 2;
let diezi = 0;
while (text[j] === '#') { diezi++; j++; }
if (text[j] === '"') {
const inchidere = '"' + '#'.repeat(diezi);
const k = text.indexOf(inchidere, j + 1);
const sf = k < 0 ? n : k + inchidere.length;
siruri.push([i, sf]); i = sf; continue;
}
}
if (c === '"' || (!dupaCuvant && (c === 'b' || c === 'c') && d === '"')) {
const start = i;
let j = c === '"' ? i + 1 : i + 2;
while (j < n && text[j] !== '"') { if (text[j] === '\\') j++; j++; }
j = Math.min(n, j + 1);
siruri.push([start, j]); i = j; continue;
}
if (c === "'") {
if (d === '\\') {
let j = i + 2;
while (j < n && text[j] !== "'" && text[j] !== '\n' && j - i < 16) j++;
if (text[j] === "'") { siruri.push([i, j + 1]); i = j + 1; continue; }
i++; continue;
}
if (d !== undefined && d !== '\n' && text[i + 2] === "'") { siruri.push([i, i + 3]); i += 3; continue; }
if (d !== undefined && d.charCodeAt(0) >= 0xd800 && d.charCodeAt(0) <= 0xdbff && text[i + 3] === "'") { siruri.push([i, i + 4]); i += 4; continue; }
i++; continue;
}
if (/[A-Za-z_]/.test(c)) {
let j = i + 1;
while (j < n && /\w/.test(text[j])) j++;
i = j; continue;
}
i++;
}
return { code: out.join(''), siruri };
}
// cautare binara: pozitia e inauntrul unui sir? // cautare binara: pozitia e inauntrul unui sir?
export function inSir(siruri, pos) { export function inSir(siruri, pos) {
let lo = 0; let lo = 0;

View File

@ -43,14 +43,32 @@ const GO = [
[/^filippo\.io\/edwards25519$/, 'Edwards25519 group arithmetic'], [/^filippo\.io\/edwards25519$/, 'Edwards25519 group arithmetic'],
[/^github\.com\/open-quantum-safe\/liboqs-go$/, 'post-quantum KEMs and signatures (liboqs)'], [/^github\.com\/open-quantum-safe\/liboqs-go$/, 'post-quantum KEMs and signatures (liboqs)'],
]; ];
// Cargo (0.3.0): crate-urile criptografice cunoscute de detectorul Rust, cu numele lor din registru (liniuta, nu liniuta jos), plus
// cele care se vad numai in manifest (furnizori, legaturi). Versiunea citita e CERINTA declarata ("0.10", "^1.2"), nu versiunea
// rezolvata din Cargo.lock; purl-ul o poarta numai cand e un numar simplu.
import { CRATE_RUST } from './detect-rust.mjs';
const CARGO = [
...Object.entries(CRATE_RUST).map(([c, v]) => [new RegExp('^' + c.replace(/_/g, '-') + '$'), v.ofera]),
[/^md-5$/, 'MD5'],
[/^cfb-mode$/, 'the CFB mode'],
[/^aws-lc-rs$/, 'AWS-LC: AES-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, Ed25519, ECDSA, RSA, X25519, ML-KEM, ML-DSA'],
[/^rustls-(webpki|pki-types|native-certs|platform-verifier)$/, 'certificate handling for rustls'],
[/^webpki(-roots)?$/, 'WebPKI certificate validation (roots or verifier)'],
[/^openssl-sys$/, 'OpenSSL bindings'],
[/^pqcrypto$/, 'PQClean post-quantum KEMs and signatures (meta-crate)'],
[/^(x509-parser|x509-cert|rcgen)$/, 'X.509 certificates'],
[/^(ed25519|ecdsa|elliptic-curve|signature|aead|cipher|digest|kem)$/, 'RustCrypto trait crate (algorithm chosen by the implementation crate)'],
[/^(sodiumoxide|libsodium-sys|dryoc)$/, 'libsodium (NaCl): X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2'],
];
export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod)$/; export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod|Cargo\.toml)$/;
function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) { function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) {
const { line } = randul(inceputuri, pos); const { line } = randul(inceputuri, pos);
const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
: ecosistem === 'pypi' ? `pkg:pypi/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` : ecosistem === 'pypi' ? `pkg:pypi/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
: ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` : ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
: ecosistem === 'cargo' ? `pkg:cargo/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
: `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`; : `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`;
return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl }; return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl };
} }
@ -129,6 +147,55 @@ export function citesteManifest(text, numeFisier, rel) {
} }
pos += linie.length + 1; pos += linie.length + 1;
} }
} else if (numeFisier === 'Cargo.toml') {
// sectiunile [dependencies], [dev-dependencies], [build-dependencies], [workspace.dependencies], [target.'cfg(...)'.dependencies]
// si forma tabel [dependencies.nume] cu `version =` / `package =` pe randurile urmatoare
let pos = 0;
let inDependente = false;
let tabel = null; // { nume, pos, versiune, pachet } pentru [dependencies.nume]
const inchide = () => {
if (!tabel) return;
const nume = tabel.pachet || tabel.nume;
const intrare = CARGO.find(([re]) => re.test(nume));
if (intrare) r.push(lib('cargo', nume, tabel.versiune, intrare[1], tabel.pos, inceputuri, rel));
tabel = null;
};
const valoare = (s) => { const v = /^\s*["']([^"']*)["']/.exec(s); return v ? v[1].trim() : undefined; };
for (const linie of text.split('\n')) {
const t = linie.replace(/\s+#.*$/, '').replace(/^#.*$/, '').trim();
let h;
if ((h = /^\[\s*(.+?)\s*\]$/.exec(t))) {
inchide();
const s = h[1].replace(/\s+/g, '');
const dep = /^(?:workspace\.|target\.(?:'[^']*'|"[^"]*"|[^.]+)\.)?(dependencies|dev-dependencies|build-dependencies)(?:\.(?:"([^"]+)"|([\w.-]+)))?$/.exec(s);
inDependente = !!dep;
const numeTabel = dep && (dep[2] || dep[3]);
if (numeTabel) tabel = { nume: numeTabel, pos: pos + linie.indexOf(numeTabel), versiune: undefined, pachet: null };
} else if (tabel) {
let m;
if ((m = /^version\s*=\s*(.+)$/.exec(t))) tabel.versiune = valoare(m[1]);
else if ((m = /^package\s*=\s*(.+)$/.exec(t))) tabel.pachet = valoare(m[1]);
} else if (inDependente && t) {
const m = /^(?:"([^"]+)"|([\w.-]+))\s*=\s*(.+)$/.exec(t);
if (m) {
let nume = m[1] || m[2];
let versiune;
const val = m[3].trim();
if (val.startsWith('{')) {
const v = /\bversion\s*=\s*["']([^"']*)["']/.exec(val);
const pk = /\bpackage\s*=\s*["']([^"']*)["']/.exec(val);
if (pk) nume = pk[1];
versiune = v ? v[1].trim() : undefined;
} else {
versiune = valoare(val);
}
const intrare = CARGO.find(([re]) => re.test(nume));
if (intrare) r.push(lib('cargo', nume, versiune, intrare[1], pos + linie.indexOf(m[1] || m[2]), inceputuri, rel));
}
}
pos += linie.length + 1;
}
inchide();
} }
return { biblioteci: r }; return { biblioteci: r };
} }

View File

@ -7,11 +7,12 @@ import { detecteazaJs } from './detect-js.mjs';
import { detecteazaPy } from './detect-py.mjs'; import { detecteazaPy } from './detect-py.mjs';
import { detecteazaJava } from './detect-java.mjs'; import { detecteazaJava } from './detect-java.mjs';
import { detecteazaGo } from './detect-go.mjs'; import { detecteazaGo } from './detect-go.mjs';
import { detecteazaRust } from './detect-rust.mjs';
import { detecteazaPem } from './pem.mjs'; import { detecteazaPem } from './pem.mjs';
import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs'; import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs';
export const VERSIUNE = '0.2.0'; export const VERSIUNE = '0.3.0';
export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache']; export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache', '.claude'];
const LIMBAJ = { const LIMBAJ = {
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript', '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
@ -19,8 +20,9 @@ const LIMBAJ = {
'.py': 'python', '.pyw': 'python', '.py': 'python', '.pyw': 'python',
'.java': 'java', '.java': 'java',
'.go': 'go', '.go': 'go',
'.rs': 'rust',
}; };
const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo }; const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo, rust: detecteazaRust };
const EXEMPLE = 5; const EXEMPLE = 5;
export function limbajul(cale) { export function limbajul(cale) {

View File

@ -16,6 +16,30 @@ const SUITA = join(AICI, 'proba.mjs');
const B = 'negativ: fisierul binar e sarit si numarat'; const B = 'negativ: fisierul binar e sarit si numarat';
const MUTATII = [ const MUTATII = [
{
nume: 'comentariile bloc Rust nu se mai imbrica (lexerul)',
fisier: 'lib/lexer.mjs',
ancora: "if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; }",
cu: "if (false) { ad++; j += 2; continue; }",
tinta: 'negativ: comentariile, sirurile brute, literalele char si lifetime-urile Rust nu produc gasiri',
martor: B,
},
{
nume: 'SigningKey clasificat dupa nume, nu dupa crate-ul din use (Rust)',
fisier: 'lib/detect-rust.mjs',
ancora: "if (u) return CRATE[u.crate] ? { crate: u.crate, cale: u.cale } : null;",
cu: "if (u) return { crate: 'ed25519_dalek', cale: ['ed25519_dalek', tip] };",
tinta: 'rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)',
martor: B,
},
{
nume: 'Cargo.toml scos din manifeste',
fisier: 'lib/manifeste.mjs',
ancora: '|go\\.mod|Cargo\\.toml)$/;',
cu: '|go\\.mod)$/;',
tinta: 'pozitiv: manifestele dau biblioteci declarate, nu gasiri',
martor: B,
},
{ {
nume: 'secp256k1 clasificat quantum-safe', nume: 'secp256k1 clasificat quantum-safe',
fisier: 'lib/catalog.mjs', fisier: 'lib/catalog.mjs',

View File

@ -27,6 +27,7 @@ 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); } 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-')); const vechi = mkdtempSync(join(tmpdir(), 'ci-v010-'));
let dif = 0, comparate = 0; let dif = 0, comparate = 0;
let versiuneNoua = '?';
try { try {
for (const f of fis) { for (const f of fis) {
const b = git('show', `${REV}:${f}`); const b = git('show', `${REV}:${f}`);
@ -37,12 +38,19 @@ try {
} }
const imp = (r, m) => import(pathToFileURL(join(r, 'lib', m)).href); 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')]); 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); } 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 // 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]') }); 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) { for (const d of DOSARE) {
const a = SV.scaneaza(d), b = SN.scaneaza(d); 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
const rustN = b.findings.filter((g) => g.language === 'rust').length;
const cargoN = b.libraries.filter((x) => x.ecosystem === 'cargo').length;
b.findings = b.findings.filter((g) => g.language !== 'rust');
b.libraries = b.libraries.filter((x) => x.ecosystem !== 'cargo');
if (rustN || cargoN) console.log(` (in afara echivalentei: ${rustN} gasiri Rust, ${cargoN} biblioteci Cargo; 0.1.0 nu le stia)`);
const ka = a.findings.map(norm), kb = b.findings.map(norm); const ka = a.findings.map(norm), kb = b.findings.map(norm);
const sa = new Set(ka), sb = new Set(kb); const sa = new Set(ka), sb = new Set(kb);
const doarV = ka.filter((x) => !sb.has(x)), doarN = kb.filter((x) => !sa.has(x)); const doarV = ka.filter((x) => !sb.has(x)), doarN = kb.filter((x) => !sa.has(x));
@ -50,7 +58,7 @@ try {
comparate += ka.length; comparate += ka.length;
if (!ok) dif++; 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}`); 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 0.2.0: ' + x)]) console.log(' ' + x.slice(0, 260)); 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) // 2. texte generate: lexerul pe patru limbaje si cautarea PEM (fata de regexul din 0.1.0)
@ -83,5 +91,5 @@ try {
} }
console.log(`texte generate: ${ITER} runde (lexer x4 + PEM), diferente ${JSON.stringify(cat)}`); console.log(`texte generate: ${ITER} runde (lexer x4 + PEM), diferente ${JSON.stringify(cat)}`);
} finally { rmSync(vechi, { recursive: true, force: true }); } } 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 0.2.0)`); 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; process.exitCode = dif ? 1 : comparate ? 0 : 2;

View File

@ -0,0 +1,21 @@
[package]
name = "fixtura"
version = "0.1.0"
edition = "2021"
[dependencies]
sha2 = "0.10"
ed25519-dalek = { version = "2", features = ["rand_core"] }
rustls = { version = "0.23", default-features = false, features = ["ring"] }
tokio = { version = "1", features = ["full"] }
aes-gcm = { workspace = true }
pq = { package = "pqcrypto-mlkem", version = "0.1" }
serde = "1"
[dependencies.ml-dsa]
version = "0.1.1"
default-features = false
[dev-dependencies]
hex-literal = "0.4"
argon2 = "0.5"

View File

@ -0,0 +1,23 @@
// Fixtura negativa Rust: nicio folosire reala. Comentarii (si imbricate), siruri obisnuite, brute si de octeti, literale char
// (inclusiv '"' si '\''), durate de viata si etichete: niciuna nu are voie sa produca o gasire sau sa deschida un sir.
use std::fmt;
/* use sha2::Sha256;
/* let h = Sha256::new(); */
let d = Sha1::digest(b"x");
*/
/// Aes256Gcm::new_from_slice(key) intr-un comentariu de documentatie nu e o folosire.
pub fn nimic<'a>(x: &'a str, y: &'a str) -> &'a str {
let ghilimea = '"';
let apostrof = '\'';
let brut = r#"Sha256::new() si "Hmac::<Sha256>::new_from_slice""#;
let octeti = b"RsaPrivateKey::new(&mut rng, 1024)";
let text = "use ed25519_dalek::SigningKey; SigningKey::generate(rng)";
'exterior: loop {
if ghilimea == apostrof { break 'exterior; }
break;
}
let _ = (brut, octeti, text, fmt::Error);
if x.len() > y.len() { x } else { y }
}

View File

@ -0,0 +1,6 @@
// Fixtura: Rust, fara niciun `use` care sa spuna din ce crate vine SigningKey. Numele e purtat de ed25519-dalek, ml-dsa, slh-dsa,
// p256, k256 si rsa, cu intelesuri diferite; unealta nu ghiceste, raporteaza unknown cu motivul scris.
pub fn cheie(b: &[u8; 32]) -> Vec<u8> {
let k = SigningKey::from_bytes(b); // EXPECT: SigningKey (crate not resolved) | unknown
k.to_bytes().to_vec()
}

View File

@ -0,0 +1,53 @@
// Fixtura: Rust. Numele din scop vin din `use`, si acelasi nume inseamna alt algoritm dupa crate (SigningKey e Ed25519 in
// ed25519-dalek, ML-DSA in ml-dsa, ECDSA in p256). Un import fara niciun apel recunoscut ramane dovada slaba (unknown).
use sha2::{Digest, Sha256, Sha512};
use sha1::Sha1;
use hmac::{Hmac, Mac};
use hkdf::Hkdf;
use argon2::{Algorithm, Argon2, Params, Version};
use chacha20poly1305::{aead::KeyInit, XChaCha20Poly1305};
use aes_gcm::Aes256Gcm;
use x25519_dalek::{PublicKey, StaticSecret};
use ed25519_dalek::SigningKey;
use ml_dsa::{KeyGen, MlDsa65};
use slh_dsa::{Sha2_128s, SigningKey as SlhSigningKey};
use ml_kem::{KemCore, MlKem768};
use rsa::RsaPrivateKey;
use p256::ecdsa::SigningKey as P256SigningKey;
use libsignal_protocol::kem;
use ring::aead;
use jsonwebtoken::Algorithm as JwtAlg;
use openssl::hash::MessageDigest;
use rustls::crypto::ring as rustls_ring;
use snow::Builder;
use blake2::Blake2b512; // EXPECT: blake2 | unknown
use zeroize::Zeroizing;
type HmacSha256 = Hmac<Sha256>; // EXPECT: HMAC-SHA-256 | quantum-safe
pub fn exemple(data: &[u8], key: &[u8; 32], rng: &mut impl rand_core::CryptoRngCore) {
let h = Sha256::new(); // EXPECT: SHA-256 | quantum-safe
let d = Sha1::digest(data); // EXPECT: SHA-1 | weak-now
let m = Hmac::<Sha512>::new_from_slice(key).unwrap(); // EXPECT: HMAC-SHA-512 | quantum-safe
let hk = Hkdf::<Sha256>::new(None, key); // EXPECT: HKDF | quantum-safe
let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default()); // EXPECT: Argon2id | quantum-safe
let aead1 = XChaCha20Poly1305::new_from_slice(key).unwrap(); // EXPECT: XChaCha20-Poly1305 | quantum-safe
let aead2 = Aes256Gcm::new_from_slice(key).unwrap(); // EXPECT: AES-256-GCM | quantum-safe
let bloc = ecb::Encryptor::<aes::Aes128>::new(key[..16].into()); // EXPECT: AES-128-ECB | weak-now
let secret = StaticSecret::random_from_rng(rng); // EXPECT: X25519 | quantum-vulnerable
let pk = PublicKey::from(&secret); // EXPECT: X25519 | quantum-vulnerable
let ed = SigningKey::generate(rng); // EXPECT: Ed25519 | quantum-vulnerable
let kp = MlDsa65::key_gen(rng); // EXPECT: ML-DSA-65 | quantum-safe
let slh = SlhSigningKey::<Sha2_128s>::new(rng); // EXPECT: SLH-DSA-SHA2-128s | quantum-safe
let (dk, ek) = MlKem768::generate(rng); // EXPECT: ML-KEM-768 | quantum-safe
let r = RsaPrivateKey::new(rng, 2048).unwrap(); // EXPECT: RSA-2048 | quantum-vulnerable
let p = P256SigningKey::random(rng); // EXPECT: ECDSA-secp256r1 | quantum-vulnerable
let kt = kem::KeyType::Kyber1024; // EXPECT: Kyber-1024 | quantum-safe
let sealing = aead::UnboundKey::new(&aead::AES_256_GCM, key).unwrap(); // EXPECT: AES-256-GCM | quantum-safe
let alg = JwtAlg::RS256; // EXPECT: JWS RS256 | quantum-vulnerable
let md = MessageDigest::md5(); // EXPECT: MD5 | weak-now
let provider = rustls_ring::default_provider(); // EXPECT: X25519 | quantum-vulnerable
let noise = Builder::new("Noise_XX_25519_ChaChaPoly_SHA256".parse().unwrap()); // EXPECT: X25519 | quantum-vulnerable ; ChaCha20-Poly1305 | quantum-safe ; SHA-256 | quantum-safe
let _ = (h, d, m, hk, argon, aead1, aead2, bloc, pk, ed, kp, slh, dk, ek, r, p, kt, sealing, alg, md, provider, noise, Zeroizing::new(0u8));
let _t: Option<HmacSha256> = None;
}

View File

@ -14,11 +14,12 @@ const AICI = dirname(fileURLToPath(import.meta.url));
const FIX = join(AICI, 'fixturi'); const FIX = join(AICI, 'fixturi');
const CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs'); const CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs');
const inv = await import(pathToFileURL(CALE_MODUL).href); const inv = await import(pathToFileURL(CALE_MODUL).href);
const lx = await import(pathToFileURL(join(dirname(CALE_MODUL), 'lib', 'lexer.mjs')).href);
const CA_JSON = process.argv.includes('--json'); const CA_JSON = process.argv.includes('--json');
// numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient // numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient
const MARCAJE_ASTEPTATE = 96; const MARCAJE_ASTEPTATE = 123;
const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'python/crypto_py.py', 'java/Crypto.java', 'go/crypto.go', 'negative/variabile.js']; const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'python/crypto_py.py', 'java/Crypto.java', 'go/crypto.go', 'rust/cripto.rs', 'rust/ambiguu.rs', 'negative/variabile.js'];
const probe = []; const probe = [];
const proba = (nume, fn) => probe.push({ nume, fn }); const proba = (nume, fn) => probe.push({ nume, fn });
@ -89,11 +90,12 @@ proba('pozitiv: manifestele dau biblioteci declarate, nu gasiri', () => {
const libs = rez.libraries.map((b) => `${b.file}:${b.line} ${b.name}`).sort(); const libs = rez.libraries.map((b) => `${b.file}:${b.line} ${b.name}`).sort();
const ast = ['go.mod:6 github.com/cloudflare/circl', 'go.mod:7 golang.org/x/crypto', 'package.json:10 @noble/post-quantum', const ast = ['go.mod:6 github.com/cloudflare/circl', 'go.mod:7 golang.org/x/crypto', 'package.json:10 @noble/post-quantum',
'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on', 'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on',
'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt'].sort(); 'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt', 'Cargo.toml:7 sha2', 'Cargo.toml:8 ed25519-dalek', 'Cargo.toml:9 rustls',
'Cargo.toml:11 aes-gcm', 'Cargo.toml:12 pqcrypto-mlkem', 'Cargo.toml:15 ml-dsa', 'Cargo.toml:21 argon2'].sort();
asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`); asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`);
const bom = inv.buildCbom(rez, { deterministic: true }); const bom = inv.buildCbom(rez, { deterministic: true });
const c = bom.components.filter((x) => x.type === 'library'); const c = bom.components.filter((x) => x.type === 'library');
asigura(c.length === 8 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), 'componentele library nu poarta declared-only'); asigura(c.length === 15 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), 'componentele library nu poarta declared-only');
}); });
proba('pozitiv: material PEM (certificat si cheie publica)', () => { proba('pozitiv: material PEM (certificat si cheie publica)', () => {
@ -192,9 +194,58 @@ proba('negativ: comentariile si sirurile Java si Go nu produc gasiri', () => {
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`); asigura(g.length === 0, `au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`);
}); });
proba('negativ: comentariile, sirurile brute, literalele char si lifetime-urile Rust nu produc gasiri', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'rust/cripto.rs').some((x) => x.name === 'MD5'), 'controlul pozitiv: MD5 din fixtura Rust lipseste');
const g = gasiriDin(rez, 'negative/comentarii.rs');
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
});
proba('rust: lexerul: lifetime-urile si literalele char nu deschid siruri, comentariile bloc se imbrica, sirurile brute se inchid la diezi', () => {
const t1 = "fn f<'a>(x: &'a str) { let c = '\"'; let s = \"Sha256::new()\"; Sha1::digest(x) }";
const c1 = lx.curata(t1, 'rust');
asigura(lx.inSir(c1.siruri, t1.indexOf('Sha256')) && !lx.inSir(c1.siruri, t1.indexOf('Sha1')), `t1: siruri ${JSON.stringify(c1.siruri)}`);
const t2 = '/* a /* b */ Sha256::new() */ Md5::new()';
const c2 = lx.curata(t2, 'rust');
asigura(!c2.code.includes('Sha256') && c2.code.includes('Md5') && c2.code.length === t2.length, `t2: ${JSON.stringify(c2.code)}`);
const t3 = 'let s = r#"a"b"#; let o = b"x\\"y"; Sha1::digest(d)';
const c3 = lx.curata(t3, 'rust');
asigura(c3.siruri.length === 2 && !lx.inSir(c3.siruri, t3.indexOf('Sha1')), `t3: ${JSON.stringify(c3.siruri)}`);
const t4 = "let a = '\\''; let e = '\\u{1F600}'; 'outer: loop { break 'outer; } Md5::new()";
const c4 = lx.curata(t4, 'rust');
asigura(c4.siruri.length === 2 && !lx.inSir(c4.siruri, t4.indexOf('Md5')), `t4: ${JSON.stringify(c4.siruri)}`);
});
proba('rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)', () => {
const g = gasiriDin(scan(FIX), 'rust/cripto.rs');
const nume = (api) => g.filter((x) => x.api === api).map((x) => x.name);
asigura(JSON.stringify(nume('SigningKey::generate')) === '["Ed25519"]', `ed25519_dalek: ${nume('SigningKey::generate')}`);
asigura(JSON.stringify(nume('P256SigningKey::random')) === '["ECDSA-secp256r1"]', `p256 prin alias: ${nume('P256SigningKey::random')}`);
asigura(JSON.stringify(nume('SlhSigningKey::new')) === '["SLH-DSA-SHA2-128s"]', `slh_dsa prin alias + generic: ${nume('SlhSigningKey::new')}`);
const a = gasiriDin(scan(FIX), 'rust/ambiguu.rs');
asigura(a.length === 1 && a[0].classification === 'unknown' && /ed25519-dalek/.test(a[0].reason) && /ml-dsa/.test(a[0].reason), `fara use: ${JSON.stringify(a.map((x) => [x.name, x.classification]))}`);
});
proba('rust: importul fara apel e dovada slaba (unknown), importul cu apel in acelasi fisier nu se raporteaza', () => {
const g = gasiriDin(scan(FIX), 'rust/cripto.rs');
const imp = g.filter((x) => x.evidenceKind === 'import');
asigura(imp.length === 1 && imp[0].name === 'blake2' && imp[0].classification === 'unknown', `importuri: ${JSON.stringify(imp.map((x) => [x.name, x.classification]))}`);
asigura(!g.some((x) => x.api === 'use sha2' || x.api === 'use ed25519_dalek'), 'sha2 / ed25519_dalek au apeluri, importul lor nu trebuie raportat');
});
proba('rust: Cargo.toml: forma tabel, workspace = true fara versiune, redenumire prin package, purl pkg:cargo', () => {
const libs = scan(join(FIX, 'manifeste')).libraries.filter((b) => b.ecosystem === 'cargo');
const l = (n) => libs.find((b) => b.name === n);
asigura(l('ml-dsa') && l('ml-dsa').version === '0.1.1' && l('ml-dsa').line === 15, `tabel: ${JSON.stringify(l('ml-dsa'))}`);
asigura(l('aes-gcm') && l('aes-gcm').version === undefined && l('aes-gcm').purl === 'pkg:cargo/aes-gcm', `workspace: ${JSON.stringify(l('aes-gcm'))}`);
asigura(l('pqcrypto-mlkem') && l('pqcrypto-mlkem').version === '0.1' && !l('pq'), `package =: ${JSON.stringify(l('pqcrypto-mlkem'))}`);
asigura(l('sha2') && l('sha2').purl === 'pkg:cargo/sha2@0.10' && l('argon2') && l('argon2').line === 21, `purl / dev-dependencies: ${JSON.stringify([l('sha2'), l('argon2')])}`);
asigura(!l('tokio') && !l('serde') && !l('hex-literal'), 'crate-urile fara criptografie nu sunt biblioteci declarate');
});
proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => { proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => {
const rez = scan(join(FIX, 'negative')); const rez = scan(join(FIX, 'negative'));
asigura(rez.stats.filesSeen === 7 && rez.stats.codeFilesTotal === 7, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`); asigura(rez.stats.filesSeen === 8 && rez.stats.codeFilesTotal === 8, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`);
const g = gasiriDin(rez, 'fara-cripto.js'); const g = gasiriDin(rez, 'fara-cripto.js');
asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`); asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`);
}); });