Add crypto-inventory: a cryptographic inventory of source code as a CycloneDX 1.6 CBOM, with linear cost on hostile input; PKI DER messages in English

This commit is contained in:
Aere Network 2026-09-29 17:42:36 +03:00
parent 7d814d1bb8
commit 0f081f0847
49 changed files with 4342 additions and 33 deletions

View File

@ -1,6 +1,6 @@
# Aere Quantum
Self-hosted post-quantum infrastructure from Aere Network. Three components, each a few files with **no dependencies**:
Self-hosted post-quantum infrastructure from Aere Network. Four components, each a few files with **no dependencies**:
Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry.
| component | what it does |
@ -8,6 +8,7 @@ Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a pac
| [`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-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 |
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
@ -22,9 +23,11 @@ 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-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`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`) |
| 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) |
Test names and control messages are in Romanian; identifiers, error codes, messages and documentation are in English.
Code comments, internal names, test names and control messages are in Romanian, and so are the two command words of the KMS
HSM tool (explained in its README); error codes, error messages, exported names and documentation are in English.
## Licence
MIT, see [LICENSE](LICENSE). Files: 24 (pq-gateway 6, pq-kms 10, pq-pki 6).
MIT, see [LICENSE](LICENSE). Files: 66 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42).

202
crypto-inventory/README.md Normal file
View File

@ -0,0 +1,202 @@
# Aere crypto inventory
A cryptographic inventory of source code. It reads a directory, finds where cryptography is used
(algorithms, key sizes, curves, modes, TLS settings, JWT algorithms, key and certificate files),
classifies each use by its exposure to a quantum computer, suggests a migration target, and writes
the result as a **CBOM** (Cryptography Bill of Materials) in **CycloneDX 1.6** format.
It is a single Node.js script with no dependencies: only built-in `node:*` modules. Nothing from the
scanned tree is executed, and no network request is made.
```
node tools/crypto-inventory/inventar.mjs scan ./my-service --out cbom.json --summary
```
## What it detects
Detection is pattern-based on the source text, after comments (and Python docstrings) are removed.
An API name that appears only inside a comment or inside a string literal is not reported as a use.
| Language | What is recognized |
|---|---|
| JavaScript / TypeScript | `node:crypto` (`createHash`, `createHmac`, `createCipheriv`, `createSign`/`createVerify`, `generateKeyPair(Sync)` for `rsa`, `rsa-pss`, `dsa`, `ec`, `ed25519`, `x25519`, `dh`, `ml-dsa-*`, `ml-kem-*`, `slh-dsa-*`, `createECDH`, `getDiffieHellman`, `publicEncrypt`, `pbkdf2`, `hkdf`, `crypto.sign`/`verify`, `createPrivateKey`/`createPublicKey`, `X509Certificate`); WebCrypto `subtle` (RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, ECDH, Ed25519, X25519, AES-GCM/CBC/CTR/KW, HMAC, HKDF, PBKDF2, `digest`); TLS options `minVersion`, `maxVersion`, `secureProtocol`, `ecdhCurve`; libraries from imports: `ethers`, `web3`, `viem` (secp256k1 ECDSA), `secp256k1`, `elliptic`, `node-forge`, `jsonwebtoken`/`jose` (JWS algorithms), `@noble/curves`, `@noble/post-quantum`, `@noble/hashes`, `crypto-js`, `tweetnacl`/libsodium, `node-rsa`, bcrypt/argon2 |
| 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`, ...) |
| 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`) |
| 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 |
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 (`_ "crypto/sha512"`) is reported with a note that it only registers an implementation.
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
(template literals whose `${...}` parts all resolve this way are also resolved). The finding then
records where the value came from (`resolvedFrom`). In every other case the finding is `unknown`: the
tool does not guess. At most 64 distinct variables are resolved per file; past that, variables in that
file are reported as `unknown`, and the file is counted (`files-over-resolution-limit` in the CBOM, a
line in the text summary).
## Cost on hostile input
The scanner reads repositories it did not write, so its cost is kept linear in the size of each file,
also for input built to be slow. Version 0.1.0, which was never published, had six places where a
crafted file cost quadratic time, minutes for a single file of the default maximum size: PEM block
search, JavaScript regex literals, Python triple-quoted strings on a long line, variable resolution
repeated for every call, open-ended parameter-list patterns used by that resolution, and the
assigned-variable lookup in Java. An adversarial review found them on 2026-09-29 and they were
rewritten for 0.2.0; the rewritten forms give the same findings as the old ones: on 2026-09-29,
`test/echivalenta-0.1.0.mjs` (which takes 0.1.0 from git history) found no difference on about 4,500
source files of the repository where the tool is developed, including three large npm packages, nor
on 300,000 generated inputs to the lexer and the PEM search. `test/proba-timp.mjs` keeps the cost
linear, and `test/control-negativ-timp.mjs` shows that it turns red when a quadratic form comes back.
One cost remains bounded rather than linear: the arguments of a call are read for at most 4,000
characters, so a file made of thousands of unclosed calls is slower (about 10 seconds per megabyte on
the machine where it was measured) but still finishes.
## Classification
Every finding carries one class, a written reason and, when action is needed, a recommendation.
| Class | Meaning |
|---|---|
| `quantum-vulnerable` | Broken by Shor's algorithm on a cryptographically relevant quantum computer: RSA, DSA, finite-field DH, ECDSA, ECDH, EdDSA and X25519/X448 on any classical curve, secp256k1, pairing-based schemes. Also TLS configurations that allow TLS 1.2, which has no standardized post-quantum key exchange. |
| `weak-now` | Unsafe against classical attackers today: MD5, SHA-1, DES, 3DES, RC4, RC2, 64-bit-block ciphers, ECB mode, SSL and TLS below 1.2, RSA/DH below 2048 bits, curves below 112-bit security, JWT `none`. When the algorithm is also quantum-vulnerable (for example RSA-1024, or an RSA signature over SHA-1), the finding says so (`quantumVulnerable: true`). |
| `quantum-safe` | ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205), Falcon, hybrid key-exchange groups such as X25519MLKEM768, AES, ChaCha20-Poly1305, SHA-2, SHA-3, HMAC and KDFs over them. For AES-128 the reason notes that Grover's algorithm gives at most a quadratic speed-up (NIST category 1) and that AES-256 is preferred for long-lived data. |
| `unknown` | The algorithm cannot be determined statically: it comes from a parameter, from configuration, from a key loaded at run time, or from a library that offers many algorithms without a recognized call. |
Recommendations are concrete but do not pretend a migration is simple. Examples: key exchange in TLS
moves to X25519MLKEM768; signatures move to ML-DSA-65 or a hybrid classical+ML-DSA signature (the
X.509 composite signature format was still an IETF draft when the rules were written); secp256k1 in a
wallet moves at the account level to an account with a post-quantum owner, which is a protocol change
and not a library swap; for JWT the tool says plainly that no final post-quantum JWS standard exists
yet. Statements about standards and runtime defaults reflect the time the rules were written; check
the current status before acting on them.
A finding states which algorithm appears where. It does not judge intent: a TLS scanner that
deliberately offers classical groups to test a server will be reported like any other client.
## Output
### CBOM (CycloneDX 1.6)
- `bomFormat: "CycloneDX"`, `specVersion: "1.6"`, `serialNumber` (`urn:uuid:...`), `version: 1`.
- `metadata.timestamp`, `metadata.tools.components` (this tool and its version),
`metadata.component` (the scanned directory), `metadata.properties` (scan statistics: files seen,
analyzed, not read and why, excluded directories, limits).
- One component of `type: "cryptographic-asset"` per distinct asset, with a stable `bom-ref`
derived from its content:
- `cryptoProperties.assetType`: `algorithm`, `protocol`, `certificate` or `related-crypto-material`.
- `algorithmProperties`: `primitive`, `parameterSetIdentifier`, `curve`, `mode`, `padding`,
`cryptoFunctions`, `classicalSecurityLevel`, `nistQuantumSecurityLevel` (0 for quantum-vulnerable
and weak algorithms; omitted when not defined).
- `protocolProperties`: `type: "tls"`, `version`.
- `certificateProperties`: subject, issuer, validity, format.
- `relatedCryptoMaterialProperties`: `type` (`private-key` or `public-key`), `format`, `size`.
**The value of key material is never written.**
- `oid` where it is standard and certain.
- `evidence.occurrences[]`: `location` (path relative to the scanned directory), `line`,
`symbol` (the API), `additionalContext` (the matched call, not the source line).
- `properties` in the `aere:crypto-inventory:` namespace: `classification`,
`quantum-vulnerable`, `reason`, `recommendation`, `languages`, `evidence-kinds`, `resolved-from`.
- Declared libraries from manifests are components of `type: "library"` with `purl`, the manifest
location, and `aere:crypto-inventory:declared-only = true`. **Declaration is not use**: a library
can be declared and never called, and the cryptographic-asset components are the evidence of use.
`--deterministic` derives the serial number from the content and omits the timestamp, so two runs on
the same tree produce identical files.
### Text summary (`--summary`)
Files seen, analyzed per language, checked for PEM only, and **not read, with the reason** (binary,
too large, over the file limit, unreadable), plus excluded directories and symlinks not followed.
Nothing is skipped silently: the scan fails with an accounting error if the categories do not add up
to the number of files seen. Then counts per class and per language, the most frequent assets, the
files with quantum-vulnerable findings, and the declared libraries.
## Command line
```
node inventar.mjs scan <dir> [options]
--out <file> write the CBOM to <file> (default: stdout, unless --summary is given)
--summary print the text summary
--fail-on <list> exit 1 when findings match: vulnerable, weak, unknown, any (comma-separated)
--max-file-bytes <n> skip and count files larger than n bytes (default 1048576)
--max-files <n> read at most n files; the rest are counted, not read (default 50000)
--exclude-dir <name> do not descend into directories with this name (repeatable)
--no-default-excludes also descend into .git, node_modules, __pycache__, .venv, venv, ...
--deterministic reproducible output (content-derived serial number, no timestamp)
--findings-json <file> write the raw findings, one object per occurrence
```
Exit codes: `0` success, `1` the `--fail-on` condition is met, `2` usage or scan error.
`--fail-on vulnerable` matches every finding a quantum computer would break, including `weak-now`
findings that are also quantum-vulnerable.
### In CI
```yaml
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Cryptographic inventory
run: node tools/crypto-inventory/inventar.mjs scan . --out cbom.json --summary --fail-on vulnerable
- uses: actions/upload-artifact@v4
if: always()
with:
name: cbom
path: cbom.json
```
Start with `--fail-on weak` if the goal is to block known-broken algorithms first, and add
`vulnerable` once a migration plan exists; failing a build on every ECDSA use from day one usually
only teaches people to disable the check.
### As a module
```js
import { scan, buildCbom, renderSummary, checkFailOn, classify } from './inventar.mjs';
const result = scan('./src', { maxFileBytes: 1 << 20 });
const cbom = buildCbom(result, { deterministic: true });
console.log(renderSummary(result));
```
## What it does NOT do
- It does not see cryptography inside compiled binaries (`.class`, `.jar`, `.so`, `.dll`, executables)
or inside transitive dependencies. It reads the source you point it at, and lists the libraries your
manifests declare.
- It does not find hand-written implementations (for example a local Keccak or AES routine), calls
made through wrappers or reflection, dynamically computed imports, or algorithms chosen in
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
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.
Kotlin, Scala, C#, Rust, 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.
- It does not execute anything from the scanned tree, and it makes no network requests.
## Tests
```
node test/proba.mjs # 37 probes: fixtures per language with the exact expected findings
node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (18 mutations), and
# requires the matching probe to fail while the rest of the suite still runs
node test/proba-timp.mjs # 8 hostile inputs, each at two sizes: the cost must grow linearly
node test/control-negativ-timp.mjs # puts each quadratic form back and requires the cost probe to turn red
node test/echivalenta-0.1.0.mjs # same findings as 0.1.0 (needs the git history; skipped without it)
```
Fixtures mark each expected finding on its own line (`EXPECT: <name> | <class>`), and a probe fails on
a missing finding, an extra finding, a wrong line or a wrong class. Negative fixtures cover comments,
docstrings, strings that mention crypto APIs, a file without cryptography, algorithms from
unresolvable variables, a binary file and the size and count limits. The private-key probe generates
a key at run time (none is stored in the repository) and checks that no part of it appears in any
output.
Developed and tested on Node.js 24; older versions are not tested.

View File

@ -0,0 +1,98 @@
#!/usr/bin/env node
// Inventarul criptografic al unui cod sursa (CBOM CycloneDX 1.6). Fara dependinte externe.
// Folosire: node inventar.mjs scan <dosar> [--out cbom.json] [--summary] [--fail-on vulnerable]
// Codul de iesire: 0 = bine, 1 = conditia --fail-on indeplinita, 2 = eroare de folosire sau de scanare.
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import { scaneaza, VERSIUNE, DOSARE_EXCLUSE_IMPLICIT } from './lib/scan.mjs';
import { construiesteCbom } from './lib/cbom.mjs';
import { rezumatText, verificaFailOn, numarPeClase } from './lib/rezumat.mjs';
import { evalueaza, CLS } from './lib/catalog.mjs';
export { scaneaza as scan, construiesteCbom as buildCbom, rezumatText as renderSummary, verificaFailOn as checkFailOn, numarPeClase as countByClass, evalueaza as classify, CLS as CLASSES, VERSIUNE as VERSION, DOSARE_EXCLUSE_IMPLICIT as DEFAULT_EXCLUDED_DIRS };
const AJUTOR = `aere-crypto-inventory ${VERSIUNE}
Usage:
node inventar.mjs scan <dir> [options]
Options:
--out <file> write the CycloneDX 1.6 CBOM to <file> (default: stdout, unless --summary is given)
--summary print a text summary to stdout
--fail-on <list> exit with code 1 if findings match: vulnerable, weak, unknown, any (comma-separated)
--max-file-bytes <n> skip (and count) files larger than n bytes (default 1048576)
--max-files <n> read at most n files; the rest are counted, not read (default 50000)
--exclude-dir <name> do not descend into directories with this name (repeatable)
--no-default-excludes also descend into ${DOSARE_EXCLUSE_IMPLICIT.join(', ')}
--deterministic serial number derived from content, no timestamp (reproducible output)
--findings-json <file> write the raw findings (one object per occurrence) as JSON
Exit codes: 0 ok, 1 --fail-on condition met, 2 usage or scan error.`;
export function main(argv) {
const a = argv.slice();
const cmd = a.shift();
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(AJUTOR + '\n'); return cmd ? 0 : 2; }
if (cmd === '--version') { process.stdout.write(VERSIUNE + '\n'); return 0; }
if (cmd !== 'scan') { process.stderr.write(`unknown command: ${cmd}\n${AJUTOR}\n`); return 2; }
let dir = null;
const o = { excludeDirs: [] };
let out = null;
let summary = false;
let failOn = null;
let determinist = false;
let findingsJson = null;
const numar = (x, nume) => {
if (!/^\d+$/.test(String(x))) throw new Error(`${nume} needs a non-negative integer`);
return Number(x);
};
try {
while (a.length) {
const x = a.shift();
if (x === '--out') out = a.shift();
else if (x === '--summary') summary = true;
else if (x === '--fail-on') failOn = a.shift();
else if (x === '--max-file-bytes') o.maxFileBytes = numar(a.shift(), x);
else if (x === '--max-files') o.maxFiles = numar(a.shift(), x);
else if (x === '--exclude-dir') o.excludeDirs.push(a.shift());
else if (x === '--no-default-excludes') o.noDefaultExcludes = true;
else if (x === '--deterministic') determinist = true;
else if (x === '--findings-json') findingsJson = a.shift();
else if (x.startsWith('--')) throw new Error(`unknown option: ${x}`);
else if (!dir) dir = x;
else throw new Error(`unexpected argument: ${x}`);
}
if (!dir) throw new Error('missing <dir>');
if (out === undefined || findingsJson === undefined || failOn === undefined) throw new Error('option is missing its value');
verificaFailOn([], failOn);
} catch (err) {
process.stderr.write(`error: ${err.message}\n${AJUTOR}\n`);
return 2;
}
let rez;
try {
rez = scaneaza(dir, o);
} catch (err) {
process.stderr.write(`error: ${err.message}\n`);
return 2;
}
const bom = construiesteCbom(rez, { deterministic: determinist });
const json = JSON.stringify(bom, null, 2) + '\n';
if (out) {
writeFileSync(out, json);
process.stderr.write(`CBOM written: ${resolve(out)} (${bom.components.length} components)\n`);
}
if (findingsJson) writeFileSync(findingsJson, JSON.stringify(rez.findings, null, 2) + '\n');
if (summary) process.stdout.write(rezumatText(rez) + '\n');
if (!out && !summary) process.stdout.write(json);
const f = verificaFailOn(rez.findings, failOn);
if (f.esec) {
process.stderr.write(`fail-on ${failOn}: ${f.motive.join('; ')}\n`);
return 1;
}
return 0;
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
process.exitCode = main(process.argv.slice(2));
}

View File

@ -0,0 +1,418 @@
// Catalogul de clasificare. Fiecare gasire primeste o clasa, un motiv scris si, cand e
// vulnerabila, o recomandare de migrare. Textele pentru utilizator sunt in engleza.
//
// Reguli: o clasa nu se ghiceste. "unknown" inseamna ca valoarea nu se poate afla static.
export const CLS = Object.freeze({
V: 'quantum-vulnerable',
W: 'weak-now',
S: 'quantum-safe',
U: 'unknown',
});
export const REC = Object.freeze({
SIG: 'Migrate signatures to ML-DSA-65 (FIPS 204), or to a hybrid (composite) classical+ML-DSA signature during the transition so that both would have to be broken. For long-lived roots of trust consider SLH-DSA (FIPS 205). Keys and signatures grow (ML-DSA-65: 1,952-byte public key, 3,309-byte signature): check protocol, storage and column limits. Composite signatures for X.509 were still an IETF draft when this rule was written.',
KEX: 'For TLS, offer the hybrid group X25519MLKEM768 in TLS 1.3 (built into Go 1.24+ and OpenSSL 3.5+). For application-level key establishment use ML-KEM-768 (FIPS 203), ideally combined with X25519 during the transition. Traffic protected by a classical key exchange can be recorded now and decrypted later, so start with data that must stay confidential for years.',
PKE: 'Replace RSA encryption and key transport with ML-KEM-768 (FIPS 203) used as a KEM in front of an AEAD such as AES-256-GCM, ideally hybrid with X25519 during the transition. Ciphertexts recorded today become readable once a cryptographically relevant quantum computer exists; re-encrypt data that must stay confidential for years.',
SECP256K1: 'secp256k1 ECDSA is fixed by the account and transaction format of Ethereum-style and Bitcoin-style chains, so this is not a library swap. Plan the migration at the account level: move control of funds and roles to an account whose owner is a post-quantum key (for example an ML-DSA or Falcon key checked by a smart-contract account or by the chain itself), and stop reusing addresses whose public key is already exposed on chain.',
JWT: 'There is no final standard for post-quantum JWS/JWT yet: ML-DSA for JOSE and COSE was an IETF draft when this rule was written, and mainstream JWT libraries do not ship it by default. What you can do now: keep token lifetimes short, use HS256/HS512 where issuer and verifier can share a secret, inventory where verification keys live so they can be rotated, and plan a hybrid (classical + ML-DSA) signature once the specification is final and your library supports it.',
JWT_NONE: 'An unsigned JWT ("none") must never be accepted: remove it from the accepted algorithms.',
HASH_WEAK: 'Replace with SHA-256, SHA-384 or SHA3-256. If this is a non-security checksum (cache key, deduplication), document that; for passwords use a password hash (Argon2id, scrypt, bcrypt, or PBKDF2-HMAC-SHA-256 with a high iteration count).',
CIPHER_WEAK: 'Replace with AES-256-GCM or ChaCha20-Poly1305, with a unique nonce per key.',
ECB: 'ECB leaks plaintext patterns. Use an authenticated mode: AES-256-GCM (unique nonce per key) or ChaCha20-Poly1305.',
TLS_OLD: 'Disable SSL, TLS 1.0 and TLS 1.1; require at least TLS 1.2 and prefer TLS 1.3 with the hybrid group X25519MLKEM768.',
TLS12: 'TLS 1.2 has no standardized post-quantum key exchange. Prefer TLS 1.3 and offer X25519MLKEM768; keep TLS 1.2 only for clients that require it, and measure how many still negotiate it.',
TLS13: 'Check the negotiated key-exchange groups: offer X25519MLKEM768 first. Go 1.24+ does this by default when CurvePreferences is unset, and OpenSSL 3.5+ includes it in its default groups; older runtimes negotiate a classical group.',
AES128: 'Grover\'s algorithm gives at most a quadratic speed-up, which NIST treats as security category 1 for AES-128. Frameworks such as CNSA 2.0 require AES-256; prefer AES-256 for data that must stay confidential for decades.',
AES_SIZE: 'Key size is set at run time by the key length. AES-128 is NIST category 1 against a quantum attacker, AES-256 category 5; prefer AES-256 for long-lived data.',
PREPQ: 'Pre-standard variant: migrate to the final NIST standard (ML-KEM FIPS 203, ML-DSA FIPS 204, SLH-DSA FIPS 205, or FN-DSA for Falcon once published). Keys and outputs are not interoperable with the final versions.',
BROKEN_PQ: 'Broken by classical attacks in 2022. Remove it; use ML-KEM (FIPS 203) or ML-DSA (FIPS 204).',
UNKNOWN: 'Find where the value comes from (configuration, environment, caller) and review it there; this tool does not guess.',
LIB_MULTI: 'The library offers both classical and post-quantum-safe primitives; no specific call was recognized in this file. Review how it is used.',
});
// numele canonice ale curbelor
const ALIAS_CURBE = [
[/^(p-?256|prime256v1|secp256r1|nist256p|curvep256|p256)$/i, 'secp256r1'],
[/^(p-?384|secp384r1|nist384p|curvep384|p384)$/i, 'secp384r1'],
[/^(p-?521|secp521r1|nist521p|curvep521|p521)$/i, 'secp521r1'],
[/^(p-?224|secp224r1|nist224p|p224)$/i, 'secp224r1'],
[/^(p-?192|prime192v1|secp192r1|nist192p|p192)$/i, 'secp192r1'],
[/^(secp256k1|k256|k-256)$/i, 'secp256k1'],
[/^brainpoolp256r1$/i, 'brainpoolP256r1'],
[/^brainpoolp384r1$/i, 'brainpoolP384r1'],
[/^brainpoolp512r1$/i, 'brainpoolP512r1'],
[/^(curve25519|x25519)$/i, 'Curve25519'],
[/^(ed25519|edwards25519)$/i, 'Edwards25519'],
[/^(curve448|x448)$/i, 'Curve448'],
[/^(ed448|edwards448)$/i, 'Edwards448'],
];
const NIVEL_CURBA = {
secp256r1: 128, secp384r1: 192, secp521r1: 256, secp224r1: 112, secp192r1: 96, secp256k1: 128,
brainpoolP256r1: 128, brainpoolP384r1: 192, brainpoolP512r1: 256,
};
export function curbaCanonica(s) {
if (!s) return null;
const t = String(s).trim();
for (const [re, nume] of ALIAS_CURBE) if (re.test(t)) return nume;
return t;
}
// hash-uri: nume canonic, clasa, nivel NIST (categoria de coliziune), OID
const HASH = {
MD2: { n: 'MD2', c: CLS.W }, MD4: { n: 'MD4', c: CLS.W },
MD5: { n: 'MD5', c: CLS.W, oid: '1.2.840.113549.2.5' },
SHA1: { n: 'SHA-1', c: CLS.W, oid: '1.3.14.3.2.26' },
RIPEMD160: { n: 'RIPEMD-160', c: CLS.W },
SHA224: { n: 'SHA-224', c: CLS.S },
SHA256: { n: 'SHA-256', c: CLS.S, q: 2, cl: 128, oid: '2.16.840.1.101.3.4.2.1' },
SHA384: { n: 'SHA-384', c: CLS.S, q: 4, cl: 192, oid: '2.16.840.1.101.3.4.2.2' },
SHA512: { n: 'SHA-512', c: CLS.S, q: 4, cl: 256, oid: '2.16.840.1.101.3.4.2.3' },
'SHA512/256': { n: 'SHA-512/256', c: CLS.S, q: 2 },
'SHA512/224': { n: 'SHA-512/224', c: CLS.S },
'SHA3-224': { n: 'SHA3-224', c: CLS.S },
'SHA3-256': { n: 'SHA3-256', c: CLS.S, q: 2 },
'SHA3-384': { n: 'SHA3-384', c: CLS.S, q: 4 },
'SHA3-512': { n: 'SHA3-512', c: CLS.S, q: 4 },
SHAKE128: { n: 'SHAKE128', 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 },
BLAKE2B512: { n: 'BLAKE2b-512', c: CLS.S }, BLAKE2S256: { n: 'BLAKE2s-256', c: CLS.S },
SM3: { n: 'SM3', c: CLS.S },
KECCAK256: { n: 'Keccak-256', c: CLS.S },
};
export function hashCanonic(s) {
if (!s) return null;
let t = String(s).trim().toUpperCase().replace(/^RSA-/, '');
t = t.replace(/_/g, '-');
if (/^SHA-?1$|^SHA$/.test(t)) return 'SHA1';
if (/^SHA-?(224|256|384|512)$/.test(t)) return 'SHA' + t.replace(/\D/g, '');
if (/^SHA-?512[/-](224|256)$/.test(t)) return 'SHA512/' + t.slice(-3);
if (/^SHA3-?(224|256|384|512)$/.test(t)) return 'SHA3-' + t.slice(-3);
if (/^SHAKE-?(128|256)$/.test(t)) return 'SHAKE' + t.slice(-3);
if (/^MD[245]$/.test(t)) return t;
if (/^RIPEMD-?160$|^RMD160$/.test(t)) return 'RIPEMD160';
if (/^BLAKE2B-?512$/.test(t)) return 'BLAKE2B512';
if (/^BLAKE2S-?256$/.test(t)) return 'BLAKE2S256';
if (/^BLAKE2[BS]$/.test(t)) return t;
if (/^SM3$/.test(t)) return 'SM3';
if (/^KECCAK-?256$/.test(t)) return 'KECCAK256';
return null;
}
function rez(o) {
return {
classification: o.c,
quantumVulnerable: !!o.qv,
reason: o.motiv,
recommendation: o.rec || null,
nistQuantumSecurityLevel: o.q,
classicalSecurityLevel: o.cl,
oid: o.oid,
};
}
const SHOR = 'Shor\'s algorithm on a cryptographically relevant quantum computer recovers the private key from the public key';
// intrarea: { grup, nume?, param?, curba?, hash?, mod?, primitiv?, context? }
// iesirea: descrierea completa (nume canonic, primitiv, clasa, motiv, recomandare, niveluri)
export function evalueaza(a) {
const g = a.grup;
const f = (x) => Object.assign({ grup: g, nume: a.nume, primitiv: a.primitiv || 'unknown', param: a.param, curba: a.curba, mod: a.mod, padding: a.padding }, x);
if (g === 'UNKNOWN') {
return f(rez({ c: CLS.U, motiv: a.motiv || 'Algorithm could not be determined statically.', rec: REC.UNKNOWN }));
}
if (g === 'RSA') {
const bits = a.param ? Number(a.param) : null;
const nume = a.nume || (bits ? `RSA-${bits}` : 'RSA');
const prim = a.primitiv || 'unknown';
const rec = prim === 'pke' ? REC.PKE : (a.context === 'jwt' ? REC.JWT : (prim === 'signature' ? REC.SIG : `${REC.SIG} If the key is used for encryption: ${REC.PKE}`));
const hashSlab = a.hash && HASH[a.hash] && HASH[a.hash].c === CLS.W;
if (bits && bits < 2048) {
return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `RSA with a ${bits}-bit modulus is below the 2048-bit minimum (NIST SP 800-131A); also quantum-vulnerable: ${SHOR}.`, rec, q: 0, oid: '1.2.840.113549.1.1.1' }) });
}
if (hashSlab) {
return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `RSA signature over ${HASH[a.hash].n}, which has practical collision attacks; also quantum-vulnerable: ${SHOR}.`, rec: `${REC.HASH_WEAK} ${rec}`, q: 0 }) });
}
const cl = bits === 2048 ? 112 : bits === 3072 ? 128 : bits === 7680 ? 192 : bits === 15360 ? 256 : undefined;
return f({ nume, ...rez({ c: CLS.V, qv: true, motiv: `RSA: ${SHOR}.`, rec, q: 0, cl, oid: '1.2.840.113549.1.1.1' }) });
}
if (g === 'DSA') {
const bits = a.param ? Number(a.param) : null;
const nume = a.nume || (bits ? `DSA-${bits}` : 'DSA');
if ((bits && bits < 2048) || (a.hash && HASH[a.hash] && HASH[a.hash].c === CLS.W)) {
return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `DSA with ${bits ? bits + '-bit parameters' : 'a weak hash'} is below current minimums; FIPS 186-5 no longer approves DSA for generating signatures; also ${SHOR}.`, rec: REC.SIG, q: 0 }) });
}
return f({ nume, ...rez({ c: CLS.V, qv: true, motiv: `DSA: ${SHOR} (discrete logarithm). FIPS 186-5 no longer approves DSA for generating signatures.`, rec: REC.SIG, q: 0 }) });
}
if (g === 'DH') {
const bits = a.param && /^\d+$/.test(a.param) ? Number(a.param) : null;
const nume = a.nume || (a.param ? `DH-${a.param}` : 'DH');
if (bits && bits < 2048) {
return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `Finite-field Diffie-Hellman with a ${bits}-bit group is below the 2048-bit minimum; also ${SHOR} (discrete logarithm).`, rec: REC.KEX, q: 0 }) });
}
return f({ nume, ...rez({ c: CLS.V, qv: true, motiv: `Finite-field Diffie-Hellman: ${SHOR} (discrete logarithm), so recorded key exchanges can be decrypted later.`, rec: REC.KEX, q: 0 }) });
}
if (g === 'EC' || g === 'ECDSA' || g === 'ECDH' || g === 'SECP256K1') {
const curba = curbaCanonica(a.curba) || (g === 'SECP256K1' ? 'secp256k1' : null);
const e256k1 = curba === 'secp256k1';
const eticheta = g === 'SECP256K1' ? 'ECDSA' : g;
const nume = a.nume || (curba ? `${eticheta}-${curba}` : eticheta);
const cl = curba ? NIVEL_CURBA[curba] : undefined;
const prim = a.primitiv || (g === 'ECDH' ? 'key-agree' : (g === 'EC' ? 'other' : 'signature'));
let rec = g === 'ECDH' ? REC.KEX : (g === 'EC' ? `Signatures: ${REC.SIG} Key agreement: ${REC.KEX}` : REC.SIG);
if (e256k1 && g !== 'ECDH') rec = REC.SECP256K1;
if (a.context === 'jwt') rec = REC.JWT;
const baza = { nume, primitiv: prim, curba: curba || undefined, grup: e256k1 ? 'SECP256K1' : g };
if (cl !== undefined && cl < 112) {
return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `Elliptic curve ${curba} gives about ${cl}-bit classical security, below the 112-bit minimum; also ${SHOR} (elliptic-curve discrete logarithm).`, rec, q: 0, cl }) });
}
if (a.hash && HASH[a.hash] && HASH[a.hash].c === CLS.W) {
return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `ECDSA over ${HASH[a.hash].n}, which has practical collision attacks; also ${SHOR}.`, rec: `${REC.HASH_WEAK} ${rec}`, q: 0 }) });
}
const pe = curba ? ` on ${curba}` : '';
const motiv = e256k1
? `ECDSA on secp256k1: ${SHOR} (elliptic-curve discrete logarithm). On public blockchains the public key is visible on chain after the first signed transaction.`
: `${eticheta}${pe}: ${SHOR} (elliptic-curve discrete logarithm).`;
return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });
}
if (g === 'EDDSA') {
const nume = a.nume || 'Ed25519';
const e448 = /448/.test(nume);
const rec = a.context === 'jwt' ? REC.JWT : REC.SIG;
return f({ nume, primitiv: 'signature', curba: e448 ? 'Edwards448' : 'Edwards25519', ...rez({ c: CLS.V, qv: true, motiv: `${nume}: ${SHOR} (elliptic-curve discrete logarithm).`, rec, q: 0, cl: e448 ? 224 : 128, oid: e448 ? '1.3.101.113' : '1.3.101.112' }) });
}
if (g === 'XDH') {
const nume = a.nume || 'X25519';
const e448 = /448/.test(nume);
return f({ nume, primitiv: 'key-agree', curba: e448 ? 'Curve448' : 'Curve25519', ...rez({ c: CLS.V, qv: true, motiv: `${nume} key agreement: ${SHOR} (elliptic-curve discrete logarithm), so recorded key exchanges can be decrypted later.`, rec: REC.KEX, q: 0, cl: e448 ? 224 : 128, oid: e448 ? '1.3.101.111' : '1.3.101.110' }) });
}
if (g === 'PAIRING') {
return f({ nume: a.nume || 'BLS12-381', primitiv: a.primitiv || 'signature', ...rez({ c: CLS.V, qv: true, motiv: `${a.nume || 'Pairing-based cryptography'}: ${SHOR} (discrete logarithm in the pairing groups).`, rec: REC.SIG, q: 0 }) });
}
if (g === 'CLASSIC-SIG') {
// semnatura clasica al carei tip de cheie nu se vede (RSA, DSA sau ECDSA)
const h = a.hash && HASH[a.hash];
const nume = a.nume || `signature-with-${h ? h.n : 'unknown-hash'}`;
if (h && h.c === CLS.W) {
return f({ nume, primitiv: 'signature', ...rez({ c: CLS.W, qv: true, motiv: `${a.motiv || 'Classical signature'} over ${h.n}, which has practical collision attacks; the key type (RSA, DSA or ECDSA) is also quantum-vulnerable.`, rec: `${REC.HASH_WEAK} ${REC.SIG}`, q: 0 }) });
}
return f({ nume, primitiv: 'signature', ...rez({ c: CLS.V, qv: true, motiv: `${a.motiv || 'Classical signature (RSA, DSA or ECDSA key)'}: ${SHOR}.`, rec: REC.SIG, q: 0 }) });
}
if (g === 'HASH') {
const h = HASH[a.hash];
if (!h) return f({ nume: a.nume || String(a.hash), primitiv: 'hash', ...rez({ c: CLS.U, motiv: `Hash "${a.hash}" is not in this tool's catalog.`, rec: REC.UNKNOWN }) });
const nota = a.nota ? ` ${a.nota}` : '';
if (h.c === CLS.W) {
const motiv = a.hash === 'RIPEMD160'
? `RIPEMD-160 has a 160-bit output (about 80-bit collision resistance), below the 112-bit minimum for new designs.${nota}`
: `${h.n} has practical collision attacks; unsafe for signatures, certificates and integrity against an adversary.${nota}`;
return f({ nume: h.n, primitiv: 'hash', ...rez({ c: CLS.W, motiv, rec: REC.HASH_WEAK, q: 0, oid: h.oid }) });
}
return f({ nume: h.n, primitiv: h.xof ? 'xof' : 'hash', ...rez({ c: CLS.S, motiv: `${h.n}: quantum attacks give at most a polynomial speed-up for collisions and preimages (Grover, BHT); output size keeps it within NIST categories.${nota}`, q: h.q, cl: h.cl, oid: h.oid }) });
}
if (g === 'MAC') {
if (!a.hash) return f({ nume: 'HMAC', primitiv: 'mac', ...rez({ c: CLS.U, motiv: 'HMAC whose hash function is bound to the key object (set where the key is imported or generated), not visible at this call.', rec: REC.UNKNOWN }) });
const h = HASH[a.hash];
const nume = `HMAC-${h ? h.n : String(a.hash || 'unknown')}`;
if (!h) return f({ nume, primitiv: 'mac', ...rez({ c: CLS.U, motiv: `HMAC hash "${a.hash}" is not in this tool's catalog.`, rec: REC.UNKNOWN }) });
if (h.c === CLS.W) {
return f({ nume, primitiv: 'mac', ...rez({ c: CLS.W, motiv: `${nume}: HMAC does not depend on collision resistance and has no practical break, but ${h.n} is deprecated for new designs and should be retired.`, rec: 'Move to HMAC-SHA-256 or HMAC-SHA-384.', q: 0 }) });
}
return f({ nume, primitiv: 'mac', ...rez({ c: CLS.S, motiv: `${nume}: symmetric; a quantum attacker gains at most a quadratic speed-up (Grover) on key search.` }) });
}
if (g === 'KDF') {
const h = a.hash ? HASH[a.hash] : null;
const nume = a.nume || 'KDF';
if (h && h.c === CLS.W) {
return f({ nume, primitiv: 'kdf', ...rez({ c: CLS.W, motiv: `${nume} uses ${h.n}; not practically broken as a KDF, but deprecated for new designs.`, rec: 'Use PBKDF2-HMAC-SHA-256 (high iteration count), scrypt or Argon2id.', q: 0 }) });
}
return f({ nume, primitiv: 'kdf', ...rez({ c: CLS.S, motiv: `${nume}: symmetric key derivation; a quantum attacker gains at most a quadratic speed-up (Grover).` }) });
}
if (g === 'CIPHER') return cifru(a, f);
if (g === 'MLKEM') {
const p = String(a.param || '');
const q = p === '512' ? 1 : p === '768' ? 3 : p === '1024' ? 5 : undefined;
const oid = p === '512' ? '2.16.840.1.101.3.4.4.1' : p === '768' ? '2.16.840.1.101.3.4.4.2' : p === '1024' ? '2.16.840.1.101.3.4.4.3' : undefined;
const nume = p ? `ML-KEM-${p}` : 'ML-KEM';
return f({ nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${nume}: module-lattice KEM standardized in FIPS 203${q ? `, NIST category ${q}` : ' (parameter set not visible)'}.`, q, oid }) });
}
if (g === 'MLDSA') {
const p = String(a.param || '');
const q = p === '44' ? 2 : p === '65' ? 3 : p === '87' ? 5 : undefined;
const oid = p === '44' ? '2.16.840.1.101.3.4.3.17' : p === '65' ? '2.16.840.1.101.3.4.3.18' : p === '87' ? '2.16.840.1.101.3.4.3.19' : undefined;
const nume = p ? `ML-DSA-${p}` : 'ML-DSA';
return f({ nume, primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${nume}: module-lattice signature standardized in FIPS 204${q ? `, NIST category ${q}` : ' (parameter set not visible)'}.`, q, oid }) });
}
if (g === 'SLHDSA') {
const p = String(a.param || '');
const m = /(128|192|256)/.exec(p);
const q = m ? ({ 128: 1, 192: 3, 256: 5 })[m[1]] : undefined;
const nume = p ? `SLH-DSA-${p}` : 'SLH-DSA';
return f({ nume, primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${nume}: stateless hash-based signature standardized in FIPS 205${q ? `, NIST category ${q}` : ''}.`, q }) });
}
if (g === 'FALCON') {
const p = String(a.param || '');
const q = p === '512' ? 1 : p === '1024' ? 5 : undefined;
const nume = p ? `Falcon-${p}` : 'Falcon';
return f({ nume, primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${nume}: NTRU-lattice signature selected by NIST (to be standardized as FN-DSA)${q ? `, NIST category ${q}` : ''}.`, q }) });
}
if (g === 'HASHSIG') {
return f({ nume: a.nume || 'XMSS/LMS', primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${a.nume || 'Stateful hash-based signature'}: approved in NIST SP 800-208; security depends on never reusing a one-time key state.` }) });
}
if (g === 'PREPQ') {
return f({ nume: a.nume, primitiv: a.primitiv, ...rez({ c: CLS.S, motiv: `${a.nume}: pre-standard version of a NIST-selected post-quantum scheme.`, rec: REC.PREPQ }) });
}
if (g === 'BROKENPQ') {
return f({ nume: a.nume, primitiv: a.primitiv, ...rez({ c: CLS.W, motiv: `${a.nume} was broken by classical attacks in 2022.`, rec: REC.BROKEN_PQ, q: 0 }) });
}
if (g === 'HQC') {
return f({ nume: a.nume || 'HQC', primitiv: 'kem', ...rez({ c: CLS.S, motiv: 'HQC: code-based KEM selected by NIST in 2025 as a second KEM; final standard pending.' }) });
}
if (g === 'HYBRID-KEX') {
const pq = /1024/.test(a.nume) ? 5 : 3;
return f({ nume: a.nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${a.nume}: hybrid key exchange (classical ECDH + ML-KEM); stays secure if either component holds. The NIST category given is that of the ML-KEM component.`, q: pq }) });
}
if (g === 'TLS') return tls(a, f);
if (g === 'JWT-HMAC') {
const h = HASH[a.hash];
return f({ nume: a.nume, primitiv: 'mac', ...rez({ c: CLS.S, motiv: `${a.nume} is HMAC-${h ? h.n : a.hash}: symmetric, so a quantum attacker gains at most a quadratic speed-up (Grover); the shared secret must be long and random.` }) });
}
if (g === 'JWT-NONE') {
return f({ nume: 'JWS none', primitiv: 'signature', ...rez({ c: CLS.W, motiv: 'The "none" algorithm means the token is not signed at all.', rec: REC.JWT_NONE, q: 0 }) });
}
if (g === 'LIB-MULTI') {
return f({ nume: a.nume, primitiv: 'unknown', ...rez({ c: CLS.U, motiv: a.motiv || `Imports ${a.nume}; no specific algorithm call recognized in this file.`, rec: REC.LIB_MULTI }) });
}
if (g === 'SSH') {
return f({ nume: a.nume || 'SSH', primitiv: 'unknown', ...rez({ c: CLS.U, motiv: a.motiv || 'SSH: key exchange and host-key algorithms are negotiated at run time and not visible here.', rec: 'Check the configured key-exchange algorithms: OpenSSH 9.9+ offers mlkem768x25519-sha256 and 9.0+ sntrup761x25519-sha512; host keys remain classical (Ed25519, ECDSA, RSA).' }) });
}
return f(rez({ c: CLS.U, motiv: `No classification rule for group "${g}".`, rec: REC.UNKNOWN }));
}
function cifru(a, f) {
const alg = String(a.nume || '').toUpperCase();
const mod = a.mod ? String(a.mod).toLowerCase() : undefined;
if (/^(DES|DES-?CBC|DES-?ECB|DES-?CFB|DES-?OFB)$/.test(alg) || alg === 'DES') {
return f({ nume: mod ? `DES-${mod.toUpperCase()}` : 'DES', primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: 'DES has a 56-bit key and is brute-forceable today.', rec: REC.CIPHER_WEAK, q: 0 }) });
}
if (/^(3DES|DESEDE|DES-?EDE3?|TRIPLEDES|TDEA|DES3)$/.test(alg)) {
return f({ nume: mod ? `3DES-${mod.toUpperCase()}` : '3DES', primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: '3DES (TDEA) has a 64-bit block (Sweet32 birthday attacks) and was disallowed by NIST after 2023.', rec: REC.CIPHER_WEAK, q: 0 }) });
}
if (/^(RC4|ARC4|ARCFOUR)$/.test(alg)) {
return f({ nume: 'RC4', primitiv: 'stream-cipher', ...rez({ c: CLS.W, motiv: 'RC4 has exploitable keystream biases and is prohibited in TLS (RFC 7465).', rec: REC.CIPHER_WEAK, q: 0 }) });
}
if (/^(RC2|ARC2)$/.test(alg)) {
return f({ nume: 'RC2', primitiv: 'block-cipher', ...rez({ c: CLS.W, motiv: 'RC2 is an obsolete cipher with a 64-bit block.', rec: REC.CIPHER_WEAK, q: 0 }) });
}
if (/^(BLOWFISH|BF)$/.test(alg)) {
return f({ nume: 'Blowfish', primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: 'Blowfish has a 64-bit block (Sweet32 birthday attacks).', rec: REC.CIPHER_WEAK, q: 0 }) });
}
if (/^(CAST5|CAST|IDEA|SEED)$/.test(alg)) {
return f({ nume: alg, primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: `${alg} has a 64-bit block (Sweet32 birthday attacks) or is obsolete.`, rec: REC.CIPHER_WEAK, q: 0 }) });
}
if (/^CHACHA20(-POLY1305)?$|^XCHACHA20-POLY1305$|^XSALSA20-POLY1305$|^SALSA20$/.test(alg)) {
const ae = /POLY1305/.test(alg);
return f({ nume: a.nume, primitiv: ae ? 'ae' : 'stream-cipher', ...rez({ c: CLS.S, motiv: `${a.nume}: 256-bit key; a quantum attacker gains at most a quadratic speed-up (Grover) on key search.` }) });
}
if (/^AES/.test(alg) || /^CAMELLIA/.test(alg) || /^SM4/.test(alg) || /^ARIA/.test(alg)) {
const familie = /^CAMELLIA/.test(alg) ? 'Camellia' : /^SM4/.test(alg) ? 'SM4' : /^ARIA/.test(alg) ? 'ARIA' : 'AES';
const bits = a.param ? Number(a.param) : (familie === 'SM4' ? 128 : null);
const numeMod = mod ? `-${mod.toUpperCase()}` : '';
const nume = `${familie}${bits ? '-' + bits : ''}${numeMod}`;
const ae = mod && /^(gcm|ccm|ocb|siv|gcm-siv|eax|poly1305)$/.test(mod);
const prim = ae ? 'ae' : 'block-cipher';
const modCdx = mod && ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr'].includes(mod) ? mod : (mod ? 'other' : undefined);
if (mod === 'ecb') {
return f({ nume, primitiv: prim, mod: modCdx, ...rez({ c: CLS.W, motiv: `${nume}: ECB mode encrypts equal blocks to equal ciphertext and leaks plaintext structure.${a.nota ? ' ' + a.nota : ''}`, rec: REC.ECB, q: 0 }) });
}
const q = bits === 128 ? 1 : bits === 192 ? 3 : bits === 256 ? 5 : undefined;
const cl = bits || undefined;
const rec = bits === 128 ? REC.AES128 : (bits ? null : REC.AES_SIZE);
const motiv = bits === 128
? `${nume}: symmetric; Grover's algorithm reduces key search at most quadratically, NIST category 1.`
: bits ? `${nume}: symmetric ${bits}-bit key; Grover's algorithm reduces key search at most quadratically, NIST category ${q}.`
: `${nume}: symmetric; key size not visible statically (AES-128 is NIST category 1, AES-256 category 5).`;
return f({ nume, primitiv: prim, mod: modCdx, param: bits ? String(bits) : undefined, ...rez({ c: CLS.S, motiv, rec, q, cl }) });
}
return f({ nume: a.nume, primitiv: 'unknown', ...rez({ c: CLS.U, motiv: `Cipher "${a.nume}" is not in this tool's catalog.`, rec: REC.UNKNOWN }) });
}
// a.param = versiunea ('1.0','1.1','1.2','1.3','ssl3','negotiated'); a.rol = 'min'|'max'|'only'
function tls(a, f) {
const v = String(a.param || '');
const rol = a.rol || 'only';
const numeV = v === 'ssl3' ? 'SSLv3' : v === 'ssl2' ? 'SSLv2' : (v === 'negotiated' ? 'TLS (negotiated)' : `TLSv${v}`);
const nume = a.nume || numeV;
const baza = { nume, primitiv: 'other', protocolType: 'tls', protocolVersion: /^1\.[0-3]$/.test(v) ? v : (v === 'ssl3' ? '3.0' : undefined), rol };
if (v === 'ssl2' || v === 'ssl3' || v === '1.0' || v === '1.1') {
if (rol === 'max') {
return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `${numeV} as the maximum version caps the connection at a deprecated protocol (RFC 8996).`, rec: REC.TLS_OLD, q: 0 }) });
}
return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `${numeV} is deprecated (RFC 8996) and ${rol === 'min' ? 'allowed as the minimum version' : 'selected'} here.`, rec: REC.TLS_OLD, q: 0 }) });
}
if (v === '1.2') {
if (rol === 'min') {
return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv: 'TLS 1.2 is allowed; TLS 1.2 has no standardized post-quantum key exchange, so a peer that negotiates it uses classical (EC)DHE or RSA key exchange.', rec: REC.TLS12, q: 0 }) });
}
return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv: `TLS 1.2 ${rol === 'max' ? 'is the maximum version' : 'is selected'}; TLS 1.2 has no standardized post-quantum key exchange, so key exchange is classical (EC)DHE or RSA.`, rec: REC.TLS12, q: 0 }) });
}
if (v === '1.3') {
if (rol === 'max') {
return f({ ...baza, ...rez({ c: CLS.U, motiv: 'TLS 1.3 is the maximum version; the minimum and the key-exchange groups are not visible here.', rec: REC.TLS13 }) });
}
return f({ ...baza, ...rez({ c: CLS.U, motiv: 'TLS 1.3 only: whether key exchange is post-quantum depends on the negotiated group (X25519MLKEM768 or a classical group), which depends on runtime defaults and the peer.', rec: REC.TLS13 }) });
}
return f({ ...baza, ...rez({ c: CLS.U, motiv: 'Protocol version is negotiated at run time from the runtime defaults; not visible statically.', rec: REC.TLS13 }) });
}
// grupurile de schimb de chei TLS (ecdhCurve, CurvePreferences, jdk.tls.namedGroups)
export function grupTls(nume) {
const t = String(nume).trim();
if (/^(X25519MLKEM768|X25519Kyber768Draft00|SecP256r1MLKEM768|SecP384r1MLKEM1024|p256_mlkem768|p384_mlkem1024|x25519_mlkem768)$/i.test(t)) {
if (/kyber/i.test(t)) return { grup: 'PREPQ', nume: t, primitiv: 'kem' };
return { grup: 'HYBRID-KEX', nume: t };
}
if (/^(X25519|x25519)$/.test(t)) return { grup: 'XDH', nume: 'X25519' };
if (/^(X448|x448)$/.test(t)) return { grup: 'XDH', nume: 'X448' };
if (/^(MLKEM(512|768|1024)|mlkem(512|768|1024))$/i.test(t)) return { grup: 'MLKEM', param: t.replace(/\D/g, '') };
if (/^(ffdhe\d+)$/i.test(t)) return { grup: 'DH', param: t.toLowerCase() };
const c = curbaCanonica(t);
if (c && /^secp|^brainpool/.test(c)) return { grup: 'ECDH', curba: c };
if (/^auto$/i.test(t)) return { grup: 'UNKNOWN', motiv: 'ecdhCurve "auto" selects groups from the runtime defaults; not visible statically.' };
return { grup: 'UNKNOWN', motiv: `Key-exchange group "${t}" is not in this tool's catalog.` };
}
// algoritmii JWS (RFC 7518 + RFC 8037 + RFC 8812)
export function jws(alg) {
const m = /^(HS|RS|PS|ES)(256|384|512)$/.exec(alg);
if (m) {
const hash = 'SHA' + m[2];
if (m[1] === 'HS') return { grup: 'JWT-HMAC', nume: `JWS ${alg}`, hash, context: 'jwt' };
if (m[1] === 'RS') return { grup: 'RSA', nume: `JWS ${alg}`, primitiv: 'signature', padding: 'pkcs1v15', hash, context: 'jwt' };
if (m[1] === 'PS') return { grup: 'RSA', nume: `JWS ${alg}`, primitiv: 'signature', padding: 'other', hash, context: 'jwt' };
const curba = m[2] === '256' ? 'secp256r1' : m[2] === '384' ? 'secp384r1' : 'secp521r1';
return { grup: 'ECDSA', nume: `JWS ${alg}`, curba, hash, context: 'jwt' };
}
if (alg === 'ES256K') return { grup: 'ECDSA', nume: 'JWS ES256K', curba: 'secp256k1', context: 'jwt' };
if (alg === 'EdDSA' || alg === 'Ed25519') return { grup: 'EDDSA', nume: `JWS ${alg}`, context: 'jwt' };
if (alg === 'none') return { grup: 'JWT-NONE' };
return null;
}
export const JWS_RE = /^(?:(?:HS|RS|PS|ES)(?:256|384|512)|ES256K|EdDSA|Ed25519|none)$/;

View File

@ -0,0 +1,158 @@
// Constructia CBOM-ului CycloneDX 1.6 din rezultatul scanarii.
import { createHash, randomUUID } from 'node:crypto';
export const ENUM = Object.freeze({
componentType: ['application', 'framework', 'library', 'container', 'platform', 'operating-system', 'device', 'device-driver', 'firmware', 'file', 'machine-learning-model', 'data', 'cryptographic-asset'],
assetType: ['algorithm', 'certificate', 'protocol', 'related-crypto-material'],
primitive: ['drbg', 'mac', 'block-cipher', 'stream-cipher', 'signature', 'hash', 'pke', 'xof', 'kdf', 'key-agree', 'kem', 'ae', 'combiner', 'other', 'unknown'],
mode: ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr', 'other', 'unknown'],
padding: ['pkcs5', 'pkcs7', 'pkcs1v15', 'oaep', 'raw', 'other', 'unknown'],
cryptoFunctions: ['generate', 'keygen', 'encrypt', 'decrypt', 'digest', 'tag', 'keyderive', 'sign', 'verify', 'encapsulate', 'decapsulate', 'other', 'unknown'],
protocolType: ['tls', 'ssh', 'ipsec', 'ike', 'sstp', 'wpa', 'other', 'unknown'],
materialType: ['private-key', 'public-key', 'secret-key', 'key', 'ciphertext', 'signature', 'digest', 'initialization-vector', 'nonce', 'seed', 'salt', 'shared-secret', 'tag', 'additional-data', 'password', 'credential', 'token', 'other', 'unknown'],
});
const P = 'aere:crypto-inventory:';
function slug(s) {
return String(s).toLowerCase().replace(/[^a-z0-9.+-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'x';
}
function cheieComponenta(g) {
return [g.assetType, g.name, g.primitive, g.classification, g.parameterSetIdentifier, g.curve, g.mode, g.padding,
g.protocolType, g.protocolVersion, g.material && g.material.type, g.certificate && g.certificate.fingerprintSha256, g.reason].map((x) => x ?? '').join('|');
}
function uuidDin(hex) {
const h = hex.slice(0, 32).split('');
h[12] = '5';
h[16] = '89ab'[parseInt(h[16], 16) & 3];
const s = h.join('');
return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
}
export function construiesteCbom(rez, optiuni = {}) {
const grupuri = new Map();
for (const g of rez.findings) {
const k = cheieComponenta(g);
if (!grupuri.has(k)) grupuri.set(k, []);
grupuri.get(k).push(g);
}
const componente = [];
for (const [k, lista] of grupuri) {
const g = lista[0];
const ref = `crypto:${g.assetType}:${slug(g.certificate ? 'x509-' + (g.certificate.subjectName || 'certificate') : g.name)}:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;
const cp = { assetType: g.assetType };
if (g.assetType === 'algorithm') {
const ap = { primitive: ENUM.primitive.includes(g.primitive) ? g.primitive : 'unknown' };
if (g.parameterSetIdentifier) ap.parameterSetIdentifier = g.parameterSetIdentifier;
if (g.curve) ap.curve = g.curve;
if (g.mode) ap.mode = ENUM.mode.includes(g.mode) ? g.mode : 'other';
if (g.padding) ap.padding = ENUM.padding.includes(g.padding) ? g.padding : 'other';
const f = [...new Set(lista.flatMap((x) => x.cryptoFunctions || []))].filter((x) => ENUM.cryptoFunctions.includes(x)).sort();
if (f.length) ap.cryptoFunctions = f;
if (Number.isInteger(g.classicalSecurityLevel)) ap.classicalSecurityLevel = g.classicalSecurityLevel;
if (Number.isInteger(g.nistQuantumSecurityLevel)) ap.nistQuantumSecurityLevel = g.nistQuantumSecurityLevel;
cp.algorithmProperties = ap;
} else if (g.assetType === 'protocol') {
cp.protocolProperties = { type: ENUM.protocolType.includes(g.protocolType) ? g.protocolType : 'unknown' };
if (g.protocolVersion) cp.protocolProperties.version = g.protocolVersion;
} else if (g.assetType === 'related-crypto-material') {
const m = { type: ENUM.materialType.includes(g.material && g.material.type) ? g.material.type : 'unknown' };
if (g.material && g.material.format) m.format = g.material.format;
if (g.parameterSetIdentifier && /^\d+$/.test(g.parameterSetIdentifier)) m.size = Number(g.parameterSetIdentifier);
cp.relatedCryptoMaterialProperties = m;
} else if (g.assetType === 'certificate') {
const c = g.certificate || {};
const cert = {};
for (const camp of ['subjectName', 'issuerName', 'notValidBefore', 'notValidAfter', 'certificateFormat', 'certificateExtension']) if (c[camp]) cert[camp] = c[camp];
cp.certificateProperties = cert;
}
if (g.oid) cp.oid = g.oid;
const props = [
{ name: `${P}classification`, value: g.classification },
{ name: `${P}quantum-vulnerable`, value: String(!!g.quantumVulnerable) },
{ name: `${P}reason`, value: g.reason },
];
if (g.recommendation) props.push({ name: `${P}recommendation`, value: g.recommendation });
if (g.assetType !== 'algorithm') props.push({ name: `${P}key-algorithm`, value: g.name });
if (g.certificate && g.certificate.fingerprintSha256) props.push({ name: `${P}certificate-sha256-fingerprint`, value: g.certificate.fingerprintSha256 });
props.push({ name: `${P}languages`, value: [...new Set(lista.map((x) => x.language))].sort().join(',') });
props.push({ name: `${P}evidence-kinds`, value: [...new Set(lista.map((x) => x.evidenceKind))].sort().join(',') });
const rezolvate = [...new Set(lista.filter((x) => x.resolvedFrom).map((x) => `${x.file}:${x.line} <- ${x.resolvedFrom}`))];
if (rezolvate.length) props.push({ name: `${P}resolved-from`, value: rezolvate.join('; ') });
const nume = g.assetType === 'certificate' ? `X.509 certificate${g.certificate && g.certificate.subjectName ? ' ' + g.certificate.subjectName : ''}`
: g.assetType === 'related-crypto-material' ? `${g.name} ${g.material ? g.material.type : 'key material'}` : g.name;
componente.push({
type: 'cryptographic-asset',
'bom-ref': ref,
name: nume,
cryptoProperties: cp,
evidence: {
occurrences: lista.map((x) => {
const oc = { location: x.file, line: x.line, symbol: x.api };
if (x.context) oc.additionalContext = x.context;
return oc;
}),
},
properties: props,
});
}
// bibliotecile declarate, unite pe (ecosistem, nume, versiune)
const libs = new Map();
for (const b of rez.libraries) {
const k = `${b.ecosystem}|${b.name}|${b.version || ''}`;
if (!libs.has(k)) libs.set(k, []);
libs.get(k).push(b);
}
for (const [k, lista] of libs) {
const b = lista[0];
const c = {
type: 'library',
'bom-ref': `library:${b.ecosystem}:${slug(b.name)}:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`,
name: b.name,
};
if (b.version) c.version = b.version;
if (b.purl) c.purl = b.purl;
c.evidence = { occurrences: lista.map((x) => ({ location: x.file, line: x.line })) };
c.properties = [
{ name: `${P}declared-only`, value: 'true' },
{ name: `${P}note`, value: 'Declared in a dependency manifest. Declaration is not use: see the cryptographic-asset components for code that calls it.' },
{ name: `${P}provides`, value: b.provides },
];
componente.push(c);
}
componente.sort((a, b) => (a['bom-ref'] < b['bom-ref'] ? -1 : a['bom-ref'] > b['bom-ref'] ? 1 : 0));
const s = rez.stats;
const metaProps = [
['files-seen', s.filesSeen], ['code-files-analyzed', s.codeFilesTotal], ['text-files-pem-only', s.textFilesPemOnly],
['not-read-binary', s.notRead.binary], ['not-read-too-large', s.notRead.tooLarge], ['not-read-file-limit', s.notRead.fileLimit],
['not-read-unreadable', s.notRead.unreadable], ['symlinks-not-followed', s.symlinksNotFollowed],
['files-over-resolution-limit', s.resolutionLimitFiles], ['max-resolved-variables-per-file', rez.options.maxResolvedVariablesPerFile],
['dirs-excluded', Object.entries(s.dirsExcluded).map(([n, c]) => `${n}:${c}`).join(',') || 'none'],
['max-file-bytes', rez.options.maxFileBytes], ['max-files', rez.options.maxFiles],
['method', 'static pattern analysis of source text; no code from the scanned tree is executed'],
].map(([n, v]) => ({ name: `${P}${n}`, value: String(v) }));
const bom = {
$schema: 'http://cyclonedx.org/schema/bom-1.6.schema.json',
bomFormat: 'CycloneDX',
specVersion: '1.6',
serialNumber: '',
version: 1,
metadata: {
tools: { components: [{ type: 'application', name: rez.tool.name, version: rez.tool.version, description: 'Static cryptographic inventory of source code (CBOM)' }] },
component: { type: 'application', 'bom-ref': 'scanned-target', name: rez.rootName },
properties: metaProps,
},
components: componente,
};
if (optiuni.deterministic) {
bom.serialNumber = `urn:uuid:${uuidDin(createHash('sha256').update(JSON.stringify(componente)).digest('hex'))}`;
} else {
bom.metadata = { timestamp: rez.finishedAt, ...bom.metadata };
bom.serialNumber = `urn:uuid:${randomUUID()}`;
}
return bom;
}

View File

@ -0,0 +1,253 @@
// Contextul unui fisier scanat si uneltele comune ale detectorilor.
import { curata, inSir, inceputuriDeRand, randul } from './lexer.mjs';
import { evalueaza } from './catalog.mjs';
export function escapeRe(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function facContext(text, limbaj, rel) {
const { code, siruri } = curata(text, limbaj);
const inceputuri = inceputuriDeRand(text);
const ctx = {
text, code, siruri, limbaj, rel,
gasiri: [],
inSir: (pos) => inSir(siruri, pos),
poz: (pos) => randul(inceputuri, pos),
// potriviri in cod: numele API-ului (inceputul potrivirii) nu are voie sa fie intr-un sir
potriviri(re) {
const r = [];
const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
for (const m of code.matchAll(g)) if (!inSir(siruri, m.index)) r.push(m);
return r;
},
adauga(pos, api, fel, activ, extra = {}) {
const ev = evalueaza(activ);
const { line, column } = randul(inceputuri, pos);
const bucata = extra.bucata !== undefined ? extra.bucata : '';
ctx.gasiri.push({
file: rel,
line,
column,
language: limbaj,
api,
evidenceKind: fel,
assetType: activ.assetType || (ev.protocolType ? 'protocol' : 'algorithm'),
group: ev.grup,
name: ev.nume || activ.nume || api,
primitive: ev.primitiv || 'unknown',
parameterSetIdentifier: ev.param !== undefined && ev.param !== null && ev.param !== '' ? String(ev.param) : undefined,
curve: ev.curba || undefined,
mode: ev.mod || undefined,
padding: ev.padding || activ.padding || undefined,
cryptoFunctions: activ.functii || undefined,
classification: ev.classification,
quantumVulnerable: ev.quantumVulnerable,
reason: activ.nota && !ev.reason.includes(activ.nota) ? `${ev.reason} ${activ.nota}` : ev.reason,
recommendation: ev.recommendation,
nistQuantumSecurityLevel: ev.nistQuantumSecurityLevel,
classicalSecurityLevel: ev.classicalSecurityLevel,
oid: ev.oid,
protocolType: ev.protocolType,
protocolVersion: ev.protocolVersion,
suppressedBy: extra.suprimaDe || undefined,
resolvedFrom: extra.rezolvatDin || undefined,
context: normalizeaza(bucata),
material: activ.material || undefined,
certificate: activ.certificat || undefined,
});
},
};
return ctx;
}
function normalizeaza(s) {
const t = String(s || '').replace(/\s+/g, ' ').trim();
return t.length > 120 ? t.slice(0, 117) + '...' : t;
}
// Argumentele unui apel: de la pozitia de dupa '(' pana la ')' echilibrata.
// Intoarce textul argumentelor (din vederea de cod) si limitele lui.
export function argumente(ctx, dupaParanteza) {
const c = ctx.code;
let adancime = 1;
let j = dupaParanteza;
while (j < c.length) {
if (ctx.inSir(j)) { j++; continue; }
const ch = c[j];
if (ch === '(' || ch === '[' || ch === '{') adancime++;
else if (ch === ')' || ch === ']' || ch === '}') { adancime--; if (adancime === 0) break; }
j++;
if (j - dupaParanteza > 4000) break;
}
return { text: c.slice(dupaParanteza, j), start: dupaParanteza, end: j };
}
// imparte argumentele pe virgulele de nivel zero
export function imparteArgumente(ctx, start, end) {
const c = ctx.code;
const r = [];
let adancime = 0;
let s = start;
for (let j = start; j < end; j++) {
if (ctx.inSir(j)) continue;
const ch = c[j];
if (ch === '(' || ch === '[' || ch === '{') adancime++;
else if (ch === ')' || ch === ']' || ch === '}') adancime--;
else if (ch === ',' && adancime === 0) { r.push({ text: c.slice(s, j), start: s }); s = j + 1; }
}
if (end > s) r.push({ text: c.slice(s, end), start: s });
return r.map((a) => {
const lead = a.text.length - a.text.trimStart().length;
return { text: a.text.trim(), start: a.start + lead };
}).filter((a) => a.text.length);
}
// un literal de sir simplu, fara interpolare; intoarce valoarea sau null
export function literal(s) {
const t = String(s).trim();
const m = /^(?:[rRbBuU]{0,2})(['"`])([^'"`\n$\\]*)\1$/.exec(t);
return m ? m[2] : null;
}
// identificator simplu (nu proprietate, nu apel)
export function identificator(s) {
const t = String(s).trim();
return /^[A-Za-z_$][\w$]*$/.test(t) ? t : null;
}
// Rezolvarea conservatoare a unei variabile la un literal: EXACT o atribuire in fisier,
// sub forma unei declaratii cu literal, si numele nu apare ca parametru de functie.
// Orice alta forma: null (gasirea iese "unknown", nu se ghiceste).
// Rezultatul depinde numai de fisier si de nume, deci se tine minte; si cel mult REZOLVARI_PE_FISIER nume distincte se rezolva
// intr-un fisier (2026-09-29, revizuirea adversariala): fiecare rezolvare citeste tot fisierul, deci un fisier cu mii de apeluri
// pe variabile costa patratic. Un nume peste plafon iese "unknown", si fisierul se numara in statistici (nu se sare in tacere).
export const REZOLVARI_PE_FISIER = 64;
export function rezolva(ctx, nume) {
if (!identificator(nume)) return null;
if (!ctx.rezolvari) ctx.rezolvari = new Map();
if (ctx.rezolvari.has(nume)) return ctx.rezolvari.get(nume);
if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER) { ctx.rezolvariPestePlafon = (ctx.rezolvariPestePlafon || 0) + 1; return null; }
const r = rezolvaOData(ctx, nume);
ctx.rezolvari.set(nume, r);
return r;
}
function rezolvaOData(ctx, nume) {
const c = ctx.code;
const e = escapeRe(nume);
const atribuiri = ctx.potriviri(new RegExp(`(?<![\\w$.])${e}\\s*(?:[-+*/%|&^]|\\?\\?|\\|\\||&&)?(?::=|=)(?![=>])`, 'g'));
if (atribuiri.length !== 1) return null;
const k = atribuiri[0].index;
const ls0 = c.lastIndexOf('\n', k) + 1;
const le0 = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k);
const linie = c.slice(ls0, le0);
let parametru = false;
if (ctx.limbaj === 'javascript') {
// intervalele fara capat ([^(]*, [^:]*, ...) sunt marginite: pornite de la fiecare cuvant cheie, ar citi pana la capatul
// fisierului pe un text fara paranteza (sau doua puncte) inchisa, deci un fisier facut anume ar costa patratic
parametru = new RegExp(`(?:function\\b[^(]{0,200}\\(|\\()([^()]{0,2000}(?<![\\w$.])${e}(?![\\w$])[^()]{0,2000})\\)\\s*(?:=>|\\{)`).test(c)
|| new RegExp(`\\bfor\\s*\\(\\s*(?:const|let|var)?\\s*${e}\\s+(?:of|in)\\b`).test(c)
|| new RegExp(`(?<![\\w$.])${e}\\s*=>`).test(c);
const m = new RegExp(`\\b(?:const|let|var)\\s+${e}\\s*=\\s*(['"\`])([^'"\`\\n$\\\\]*)\\1\\s*[;,]?\\s*$`).exec(linie);
return !parametru && m ? { valoare: m[2], line: ctx.poz(k).line } : null;
}
if (ctx.limbaj === 'python') {
parametru = new RegExp(`\\bdef\\s+\\w+\\s*\\([^)]{0,2000}(?<![\\w.])${e}\\b`).test(c) || new RegExp(`\\blambda\\b[^:]{0,500}\\b${e}\\b`).test(c)
|| new RegExp(`\\bfor\\s+[^:\\n]{0,500}(?<![\\w.])${e}\\b[^:\\n]{0,500}\\bin\\b`).test(c) || new RegExp(`\\bas\\s+${e}\\b`).test(c);
const m = new RegExp(`^[ \\t]*${e}\\s*(?::\\s*str\\s*)?=\\s*(['"])([^'"\\n\\\\]*)\\1\\s*$`, 'm').exec(c.slice(c.lastIndexOf('\n', k) + 1));
return !parametru && m && m.index === 0 ? { valoare: m[2], line: ctx.poz(k).line } : null;
}
if (ctx.limbaj === 'java') {
parametru = new RegExp(`\\(([^()]{0,2000}\\bString\\s+${e}\\b[^()]{0,2000})\\)`).test(c);
const ls = c.lastIndexOf('\n', k) + 1;
const le = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k);
const m = new RegExp(`\\bString\\s+${e}\\s*=\\s*"([^"\\n\\\\]*)"\\s*;`).exec(c.slice(ls, le));
return !parametru && m ? { valoare: m[1], line: ctx.poz(k).line } : null;
}
if (ctx.limbaj === 'go') {
parametru = new RegExp(`\\bfunc\\b[^{]{0,500}\\([^)]{0,2000}\\b${e}\\s+(?:\\w+\\s*,\\s*)*string\\b`).test(c) || new RegExp(`\\bfunc\\b[^{]{0,500}\\([^)]{0,2000}\\b${e}\\s+string\\b`).test(c);
const ls = c.lastIndexOf('\n', k) + 1;
const le = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k);
const m = new RegExp(`^\\s*(?:(?:const|var)\\s+)?${e}\\s*(?:string\\s*)?(?::=|=)\\s*"([^"\\n\\\\]*)"\\s*$`).exec(c.slice(ls, le));
return !parametru && m ? { valoare: m[1], line: ctx.poz(k).line } : null;
}
return null;
}
// valoarea unui argument: literal, variabila rezolvata sau necunoscuta
export function valoareArgument(ctx, arg) {
if (!arg) return { fel: 'lipsa' };
const l = literal(arg.text);
if (l !== null) return { fel: 'literal', valoare: l };
const id = identificator(arg.text);
if (id) {
const r = rezolva(ctx, id);
if (r) return { fel: 'rezolvat', valoare: r.valoare, din: `${id} (line ${r.line})` };
}
return { fel: 'necunoscut', expresie: arg.text.length > 60 ? arg.text.slice(0, 57) + '...' : arg.text };
}
export function necunoscut(expr, api) {
return { grup: 'UNKNOWN', motiv: `Algorithm for ${api} is given by the expression "${expr}", which this tool does not resolve statically (it resolves only a variable with exactly one literal assignment in this file, for at most ${REZOLVARI_PE_FISIER} distinct variables per file).` };
}
// Obiectul literal care contine pozitia (acolade echilibrate), pentru a citi parametri vecini.
export function obiectulDin(ctx, pos) {
const c = ctx.code;
let adancime = 0;
let s = pos;
while (s > 0 && pos - s < 2000) {
s--;
if (ctx.inSir(s)) continue;
if (c[s] === '}') adancime++;
else if (c[s] === '{') { if (adancime === 0) break; adancime--; }
}
if (c[s] !== '{') return null;
const a = argumente(ctx, s + 1);
return { text: c.slice(s, a.end + 1), start: s, end: a.end + 1 };
}
// Suprimarea: o gasire din import cade daca acelasi fisier are o gasire din apel care o acopera.
// Pentru hash-uri si MAC se compara numele intreg, pentru cifruri si KDF familia, altfel grupul.
function tokeni(g) {
if (g.group === 'HASH') return [`HASH:${g.name}`];
if (g.group === 'MAC') return g.name.startsWith('HMAC-') ? [`MAC:${g.name}`, `HASH:${g.name.slice(5)}`] : [`MAC:${g.name}`];
if (g.group === 'CIPHER' || g.group === 'KDF') return [`${g.group}:${g.name.split('-')[0]}`];
return [g.group];
}
export function aplicaSuprimarea(gasiri) {
const acoperite = new Set();
for (const g of gasiri) if (g.evidenceKind !== 'import') { acoperite.add(g.group); for (const t of tokeni(g)) acoperite.add(t); }
const r = gasiri.filter((g) => {
if (g.evidenceKind !== 'import' || !g.suppressedBy) return true;
const lista = g.suppressedBy === 'AUTO' ? tokeni(g) : g.suppressedBy;
return !lista.some((x) => acoperite.has(x));
});
// dubluri: acelasi rand, acelasi nume, acelasi API
const vazut = new Set();
return r.filter((g) => {
const k = `${g.line}|${g.name}|${g.api}|${g.classification}`;
if (vazut.has(k)) return false;
vazut.add(k);
return true;
}).map((g) => { const o = { ...g }; delete o.suppressedBy; return o; });
}
// numele unui cifru in stil OpenSSL (node:crypto): aes-256-gcm, des-ede3-cbc, bf-cbc, rc4, chacha20-poly1305
export function cifruOpenssl(s) {
const t = String(s).toLowerCase().trim();
let m;
if ((m = /^(?:id-)?aes-?(128|192|256)(?:-(\w+(?:-\w+)?))?$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: m[2] || 'cbc' };
if ((m = /^(camellia|aria|sm4)-?(128|192|256)?(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: m[1].toUpperCase(), param: m[2], mod: m[3] };
if (/^des-ede3(-\w+)?$|^des3$|^des-ede(-\w+)?$/.test(t)) return { grup: 'CIPHER', nume: '3DES', mod: (/-(cbc|ecb|cfb|ofb)$/.exec(t) || [])[1] || (t === 'des-ede3' || t === 'des-ede' ? 'ecb' : 'cbc') };
if ((m = /^des(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: 'DES', mod: m[1] || 'cbc' };
if (/^rc4(-\d+)?$|^rc4-hmac-md5$/.test(t)) return { grup: 'CIPHER', nume: 'RC4' };
if (/^rc2(-\w+)?$/.test(t)) return { grup: 'CIPHER', nume: 'RC2' };
if ((m = /^(?:bf|blowfish)(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: 'Blowfish', mod: m[1] };
if ((m = /^(cast5|cast|idea|seed)(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: m[1].toUpperCase(), mod: m[2] };
if (t === 'chacha20-poly1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' };
if (t === 'chacha20') return { grup: 'CIPHER', nume: 'ChaCha20' };
return null;
}

View File

@ -0,0 +1,207 @@
// Detectorul Go: importurile pachetelor crypto/* (in Go un import nefolosit nu compileaza, deci
// importul e o dovada de folosire, cu exceptia importului gol "_"), apelurile cu parametri si
// configuratia tls.Config.
import { argumente, imparteArgumente, escapeRe } from './context.mjs';
import { grupTls, jws } from './catalog.mjs';
const PACHETE = {
'crypto/rsa': { grup: 'RSA' },
'crypto/dsa': { grup: 'DSA' },
'crypto/ecdsa': { grup: 'ECDSA' },
'crypto/elliptic': { grup: 'EC' },
'crypto/ecdh': { grup: 'ECDH' },
'crypto/ed25519': { grup: 'EDDSA', nume: 'Ed25519' },
'golang.org/x/crypto/ed25519': { grup: 'EDDSA', nume: 'Ed25519' },
'golang.org/x/crypto/curve25519': { grup: 'XDH', nume: 'X25519' },
'crypto/md5': { grup: 'HASH', hash: 'MD5' },
'crypto/sha1': { grup: 'HASH', hash: 'SHA1' },
'golang.org/x/crypto/md4': { grup: 'HASH', hash: 'MD4' },
'golang.org/x/crypto/ripemd160': { grup: 'HASH', hash: 'RIPEMD160' },
'crypto/sha256': { grup: 'HASH', hash: 'SHA256' },
'crypto/sha512': { grup: 'HASH', hash: 'SHA512' },
'crypto/sha3': { grup: 'HASH', hash: 'SHA3-256' },
'golang.org/x/crypto/sha3': { grup: 'HASH', hash: 'SHA3-256' },
'crypto/des': { grup: 'CIPHER', nume: 'DES' },
'crypto/rc4': { grup: 'CIPHER', nume: 'RC4' },
'golang.org/x/crypto/blowfish': { grup: 'CIPHER', nume: 'Blowfish' },
'crypto/aes': { grup: 'CIPHER', nume: 'AES' },
'golang.org/x/crypto/chacha20poly1305': { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' },
'crypto/mlkem': { grup: 'MLKEM' },
'golang.org/x/crypto/bcrypt': { grup: 'KDF', nume: 'bcrypt' },
'golang.org/x/crypto/argon2': { grup: 'KDF', nume: 'Argon2' },
'golang.org/x/crypto/scrypt': { grup: 'KDF', nume: 'scrypt' },
'golang.org/x/crypto/pbkdf2': { grup: 'KDF', nume: 'PBKDF2' },
'crypto/pbkdf2': { grup: 'KDF', nume: 'PBKDF2' },
'crypto/hkdf': { grup: 'KDF', nume: 'HKDF' },
'golang.org/x/crypto/ssh': { grup: 'SSH' },
'github.com/ethereum/go-ethereum/crypto': { grup: 'SECP256K1', nota: 'go-ethereum crypto package: accounts and signatures are secp256k1 ECDSA.' },
'github.com/ethereum/go-ethereum/crypto/secp256k1': { grup: 'SECP256K1' },
'github.com/decred/dcrd/dcrec/secp256k1': { grup: 'SECP256K1' },
'github.com/btcsuite/btcd/btcec': { grup: 'SECP256K1' },
'github.com/cloudflare/circl/sign/ed25519': { grup: 'EDDSA', nume: 'Ed25519' },
'github.com/cloudflare/circl/sign/slhdsa': { grup: 'SLHDSA' },
'github.com/cloudflare/circl/kem/hybrid': { grup: 'HYBRID-KEX', nume: 'hybrid KEM (circl)' },
};
function pachetActiv(cale) {
if (PACHETE[cale]) return PACHETE[cale];
let m;
if ((m = /^github\.com\/cloudflare\/circl\/sign\/mldsa\/mldsa(44|65|87)$/.exec(cale))) return { grup: 'MLDSA', param: m[1] };
if ((m = /^github\.com\/cloudflare\/circl\/kem\/mlkem\/mlkem(512|768|1024)$/.exec(cale))) return { grup: 'MLKEM', param: m[1] };
if ((m = /^github\.com\/cloudflare\/circl\/sign\/dilithium\/mode(2|3|5)$/.exec(cale))) return { grup: 'PREPQ', nume: `Dilithium${m[1]}`, primitiv: 'signature' };
if ((m = /^github\.com\/cloudflare\/circl\/kem\/kyber\/kyber(512|768|1024)$/.exec(cale))) return { grup: 'PREPQ', nume: `Kyber${m[1]}`, primitiv: 'kem' };
if (/^github\.com\/btcsuite\/btcd\/btcec(\/v\d+)?$/.test(cale) || /^github\.com\/decred\/dcrd\/dcrec\/secp256k1(\/v\d+)?$/.test(cale)) return { grup: 'SECP256K1' };
if (/^github\.com\/golang-jwt\/jwt(\/v\d+)?$/.test(cale)) return { grup: 'LIB-MULTI', nume: 'golang-jwt', motiv: 'Imports golang-jwt; no SigningMethod reference recognized in this file.' };
if (cale === 'crypto/tls') return { grup: 'TLS', param: 'negotiated', motiv: 'crypto/tls with default settings' };
return null;
}
export function importuriGo(ctx) {
const r = [];
const c = ctx.code;
const adauga = (alias, cale, pos) => r.push({ alias: alias || cale.split('/').filter((x) => !/^v\d+$/.test(x)).pop(), gol: alias === '_', cale, pos });
for (const m of c.matchAll(/(^|\n)[ \t]*import[ \t]+(?:([\w.]+)[ \t]+)?("([^"\n]+)")/g)) {
adauga(m[2], m[4], m.index + m[0].indexOf(m[3]));
}
for (const m of c.matchAll(/(^|\n)[ \t]*import[ \t]*\(([^)]*)\)/g)) {
const bloc = m[2];
const baza = m.index + m[0].indexOf('(') + 1;
for (const x of bloc.matchAll(/(?:^|\n)[ \t]*(?:([\w.]+)[ \t]+)?("([^"\n]+)")/g)) {
adauga(x[1], x[3], baza + x.index + x[0].indexOf(x[2]));
}
}
return r;
}
export function detecteazaGo(ctx) {
const imp = importuriGo(ctx);
const alias = (cale) => { const i = imp.find((x) => x.cale === cale && !x.gol); return i ? escapeRe(i.alias) : null; };
const apel = (cale, functii) => {
const a = alias(cale);
if (!a) return [];
return ctx.potriviri(new RegExp(`(?<![\\w.])${a}\\s*\\.\\s*(${functii})\\b`, 'g'));
};
const args = (m) => {
const dupa = ctx.code.slice(m.index + m[0].length);
const p = /^\s*\(/.exec(dupa);
if (!p) return { text: '', parti: [] };
const a = argumente(ctx, m.index + m[0].length + p[0].length);
return { ...a, parti: imparteArgumente(ctx, a.start, a.end) };
};
const pachetHash = { md5: 'MD5', sha1: 'SHA1', sha256: 'SHA256', sha512: 'SHA512', sha3: 'SHA3-256' };
// apelurile cu parametri
for (const m of apel('crypto/rsa', 'GenerateKey|GenerateMultiPrimeKey')) {
const a = args(m);
const ult = a.parti[a.parti.length - 1];
ctx.adauga(m.index, `rsa.${m[1]}`, 'call', { grup: 'RSA', param: ult && /^\d+$/.test(ult.text) ? ult.text : undefined, functii: ['keygen'] }, { bucata: m[0] + (ult ? ult.text : '') });
}
for (const m of apel('crypto/rsa', 'EncryptOAEP|DecryptOAEP|EncryptPKCS1v15|DecryptPKCS1v15|SignPSS|SignPKCS1v15|VerifyPSS|VerifyPKCS1v15')) {
const a = args(m);
const h = /\bcrypto\s*\.\s*(MD5|SHA1|SHA224|SHA256|SHA384|SHA512)\b/.exec(a.text);
const sig = /^(Sign|Verify)/.test(m[1]);
ctx.adauga(m.index, `rsa.${m[1]}`, 'call', { grup: 'RSA', primitiv: sig ? 'signature' : 'pke', padding: /OAEP/.test(m[1]) ? 'oaep' : /PKCS1v15/.test(m[1]) ? 'pkcs1v15' : 'other', hash: h ? h[1] : undefined, functii: [/^Sign/.test(m[1]) ? 'sign' : /^Verify/.test(m[1]) ? 'verify' : /^Encrypt/.test(m[1]) ? 'encrypt' : 'decrypt'] }, { bucata: m[0] });
}
const ell = alias('crypto/elliptic');
const consumate = new Set();
for (const m of apel('crypto/ecdsa', 'GenerateKey|Sign|SignASN1|Verify|VerifyASN1')) {
const a = args(m);
let curba;
if (ell && m[1] === 'GenerateKey') {
const k = new RegExp(`${ell}\\s*\\.\\s*(P224|P256|P384|P521)\\s*\\(`).exec(a.text);
if (k) { curba = k[1]; consumate.add(a.start + k.index); }
}
ctx.adauga(m.index, `ecdsa.${m[1]}`, 'call', { grup: 'ECDSA', curba, functii: [m[1] === 'GenerateKey' ? 'keygen' : /^Sign/.test(m[1]) ? 'sign' : 'verify'] }, { bucata: m[0] + (curba ? ` ${curba}` : '') });
}
for (const m of apel('crypto/elliptic', 'P224|P256|P384|P521')) {
if (consumate.has(m.index)) continue;
ctx.adauga(m.index, `elliptic.${m[1]}`, 'call', { grup: 'EC', curba: m[1] }, { bucata: m[0] });
}
for (const m of apel('crypto/ecdh', 'X25519|P256|P384|P521')) {
ctx.adauga(m.index, `ecdh.${m[1]}`, 'call', m[1] === 'X25519' ? { grup: 'XDH', nume: 'X25519', functii: ['keygen'] } : { grup: 'ECDH', curba: m[1], functii: ['keygen'] }, { bucata: m[0] });
}
for (const cale of ['crypto/ed25519', 'golang.org/x/crypto/ed25519']) {
for (const m of apel(cale, 'GenerateKey|Sign|Verify|NewKeyFromSeed|VerifyWithOptions')) {
ctx.adauga(m.index, `ed25519.${m[1]}`, 'call', { grup: 'EDDSA', nume: 'Ed25519', functii: [m[1] === 'Sign' ? 'sign' : /^Verify/.test(m[1]) ? 'verify' : 'keygen'] }, { bucata: m[0] });
}
}
for (const [cale, fn] of [['crypto/md5', 'New|Sum'], ['crypto/sha1', 'New|Sum'], ['crypto/sha256', 'New|New224|Sum256|Sum224'], ['crypto/sha512', 'New|New384|Sum512|Sum384|New512_256'], ['crypto/sha3', 'New256|New384|New512|Sum256|Sum384|Sum512|NewSHAKE128|NewSHAKE256'], ['golang.org/x/crypto/sha3', 'New256|New384|New512|Sum256|Sum384|Sum512|NewLegacyKeccak256']]) {
for (const m of apel(cale, fn)) {
const dupa = ctx.code.slice(m.index + m[0].length, m.index + m[0].length + 3);
if (!/^\s*\(/.test(dupa)) continue; // referinta de functie (de ex. hmac.New(sha1.New, ...)) se raporteaza la hmac
const pk = cale.split('/').pop();
let h = pachetHash[pk];
if (/224/.test(m[1])) h = 'SHA224';
if (/384/.test(m[1])) h = pk === 'sha3' ? 'SHA3-384' : 'SHA384';
if (/512_256/.test(m[1])) h = 'SHA512/256';
if (pk === 'sha3' && /512/.test(m[1])) h = 'SHA3-512';
if (/SHAKE128/.test(m[1])) h = 'SHAKE128';
if (/SHAKE256/.test(m[1])) h = 'SHAKE256';
if (/Keccak/.test(m[1])) h = 'KECCAK256';
ctx.adauga(m.index, `${pk}.${m[1]}`, 'call', { grup: 'HASH', hash: h, functii: ['digest'] }, { bucata: m[0] });
}
}
for (const m of apel('crypto/hmac', 'New')) {
const a = args(m);
const k = /^(\w+)\s*\.\s*New(\d*)\b/.exec(a.parti[0] ? a.parti[0].text : '');
const pk = k ? imp.find((x) => x.alias === k[1]) : null;
const baza = pk ? pachetHash[pk.cale.split('/').pop()] : null;
const h = baza ? (k[2] === '384' ? 'SHA384' : k[2] === '224' ? 'SHA224' : baza) : null;
ctx.adauga(m.index, 'hmac.New', 'call', h ? { grup: 'MAC', hash: h, functii: ['tag'] } : { grup: 'UNKNOWN', motiv: `hmac.New hash "${a.parti[0] ? a.parti[0].text : '?'}" cannot be resolved statically.` }, { bucata: m[0] + (a.parti[0] ? `(${a.parti[0].text}` : '') });
}
for (const m of apel('crypto/des', 'NewCipher|NewTripleDESCipher')) {
ctx.adauga(m.index, `des.${m[1]}`, 'call', { grup: 'CIPHER', nume: m[1] === 'NewCipher' ? 'DES' : '3DES', functii: ['encrypt'] }, { bucata: m[0] });
}
for (const m of apel('crypto/rc4', 'NewCipher')) ctx.adauga(m.index, 'rc4.NewCipher', 'call', { grup: 'CIPHER', nume: 'RC4', functii: ['encrypt'] }, { bucata: m[0] });
for (const m of apel('crypto/aes', 'NewCipher')) ctx.adauga(m.index, 'aes.NewCipher', 'call', { grup: 'CIPHER', nume: 'AES', functii: ['encrypt'] }, { bucata: m[0] });
for (const m of apel('crypto/mlkem', 'GenerateKey768|GenerateKey1024|NewEncapsulationKey768|NewEncapsulationKey1024|NewDecapsulationKey768|NewDecapsulationKey1024')) {
ctx.adauga(m.index, `mlkem.${m[1]}`, 'call', { grup: 'MLKEM', param: /1024/.test(m[1]) ? '1024' : '768', functii: [/^Generate/.test(m[1]) ? 'keygen' : 'other'] }, { bucata: m[0] });
}
const eth = alias('github.com/ethereum/go-ethereum/crypto');
if (eth) {
for (const m of ctx.potriviri(new RegExp(`(?<![\\w.])${eth}\\s*\\.\\s*(Sign|GenerateKey|Ecrecover|SigToPub|VerifySignature|HexToECDSA|ToECDSA)\\s*\\(`, 'g'))) {
ctx.adauga(m.index, `${eth}.${m[1]}`, 'call', { grup: 'SECP256K1', functii: [m[1] === 'Sign' ? 'sign' : /Verify|recover|SigToPub/i.test(m[1]) ? 'verify' : 'keygen'] }, { bucata: m[0] });
}
}
const jwtI = imp.find((x) => /^github\.com\/golang-jwt\/jwt(\/v\d+)?$/.test(x.cale));
if (jwtI) {
for (const m of ctx.potriviri(new RegExp(`(?<![\\w.])${escapeRe(jwtI.alias)}\\s*\\.\\s*SigningMethod(RS256|RS384|RS512|PS256|PS384|PS512|ES256|ES384|ES512|HS256|HS384|HS512|EdDSA|None)\\b`, 'g'))) {
const alg = m[1] === 'None' ? 'none' : m[1];
ctx.adauga(m.index, `jwt.SigningMethod${m[1]}`, 'config', { ...jws(alg), functii: ['sign', 'verify'] }, { bucata: m[0] });
}
}
// tls.Config
const tlsA = alias('crypto/tls');
let tlsGasit = false;
if (tlsA) {
const vers = { VersionSSL30: 'ssl3', VersionTLS10: '1.0', VersionTLS11: '1.1', VersionTLS12: '1.2', VersionTLS13: '1.3' };
for (const m of ctx.potriviri(new RegExp(`(?<![\\w])(MinVersion|MaxVersion)\\s*(?::|=)\\s*${tlsA}\\s*\\.\\s*(VersionSSL30|VersionTLS1[0-3])\\b`, 'g'))) {
tlsGasit = true;
ctx.adauga(m.index, `tls.Config.${m[1]}`, 'config', { grup: 'TLS', param: vers[m[2]], rol: m[1] === 'MinVersion' ? 'min' : 'max' }, { bucata: m[0] });
}
for (const m of ctx.potriviri(new RegExp(`(?<![\\w])CurvePreferences\\s*(?::|=)\\s*\\[\\]\\s*${tlsA}\\s*\\.\\s*CurveID\\s*\\{`, 'g'))) {
tlsGasit = true;
const a = argumente(ctx, m.index + m[0].length);
for (const x of a.text.matchAll(new RegExp(`${tlsA}\\s*\\.\\s*(\\w+)`, 'g'))) {
const n = x[1].replace(/^Curve(P\d+)$/, '$1');
ctx.adauga(a.start + x.index, 'tls.Config.CurvePreferences', 'config', { ...grupTls(n), functii: ['keygen'] }, { bucata: `CurvePreferences ${x[1]}` });
}
}
}
// importurile: dovada de folosire in Go, cazute cand fisierul are un apel al aceluiasi grup
for (const i of imp) {
const act = pachetActiv(i.cale);
if (!act) continue;
if (i.cale === 'crypto/tls') {
if (tlsGasit) continue;
ctx.adauga(i.pos, 'import crypto/tls', 'import', { grup: 'UNKNOWN', motiv: 'crypto/tls with default key-exchange settings: Go 1.24 and later offer X25519MLKEM768 by default when Config.CurvePreferences is unset, earlier versions do not. The Go version is not determined by this scan (see the go directive in go.mod).' }, { bucata: 'import "crypto/tls"' });
continue;
}
const nota = i.gol ? `Blank import of ${i.cale}: registers the implementation; not a direct call.` : act.nota;
const supr = act.grup === 'EC' ? ['EC', 'ECDSA', 'ECDH', 'SECP256K1', 'XDH'] : act.grup === 'ECDSA' ? ['ECDSA', 'SECP256K1'] : act.grup === 'LIB-MULTI' ? ['RSA', 'ECDSA', 'EDDSA', 'JWT-HMAC', 'JWT-NONE', 'SECP256K1'] : i.cale === 'crypto/des' ? ['CIPHER:DES', 'CIPHER:3DES'] : 'AUTO';
ctx.adauga(i.pos, `import ${i.cale}`, 'import', { ...act, nota }, { suprimaDe: supr, bucata: `import "${i.cale}"` });
}
}

View File

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

View File

@ -0,0 +1,510 @@
// Detectorul JavaScript / TypeScript: node:crypto, WebCrypto, optiuni TLS, biblioteci din importuri.
import { argumente, imparteArgumente, valoareArgument, necunoscut, obiectulDin, cifruOpenssl, identificator, rezolva } from './context.mjs';
import { hashCanonic, grupTls, jws, JWS_RE } from './catalog.mjs';
const MODP = { modp1: 768, modp2: 1024, modp5: 1536, modp14: 2048, modp15: 3072, modp16: 4096, modp17: 6144, modp18: 8192 };
const TLSV = { TLSv1: '1.0', 'TLSv1.0': '1.0', 'TLSv1.1': '1.1', 'TLSv1.2': '1.2', 'TLSv1.3': '1.3', SSLv3: 'ssl3' };
export function importuriJs(ctx) {
const r = [];
for (const m of ctx.potriviri(/(?<![\w$.])import\s+((?:type\s+)?(?:[\w$*{},\s]|\bas\b)+?)\s*from\s*(['"])([^'"\n]+)\2/g)) {
r.push(descrieImport(m[3], m.index, m[1]));
}
for (const m of ctx.potriviri(/(?<![\w$.])import\s*(['"])([^'"\n]+)\1/g)) r.push(descrieImport(m[2], m.index, ''));
for (const m of ctx.potriviri(/(?<![\w$.])(?:(?:const|let|var)\s+([\w$]+|\{[^}]*\})\s*=\s*(?:await\s+)?)?(?:require|import)\s*\(\s*(['"])([^'"\n]+)\2\s*\)(?:\s*\.\s*([\w$]+))?/g)) {
const leg = m[1] || '';
r.push(descrieImport(m[3], m.index, leg.startsWith('{') ? leg : (leg ? leg : ''), !leg.startsWith('{') && leg ? leg : null, m[4]));
}
return r;
}
// un import printr-o cale relativa in node_modules (../x/node_modules/@noble/curves/secp256k1.js)
// e acelasi pachet: se pastreaza ce urmeaza dupa ultimul node_modules/, fara extensie
export function normalizeazaSpec(spec) {
let s = String(spec);
const k = s.lastIndexOf('node_modules/');
if (k >= 0) s = s.slice(k + 'node_modules/'.length);
if (/^(@[\w.-]+\/)?[\w.-]+\/.+\.(m?js|cjs)$/.test(s) && k >= 0) s = s.replace(/\.(m?js|cjs)$/, '');
else if (/^@noble\//.test(s)) s = s.replace(/\.(m?js|cjs)$/, '');
return s;
}
function descrieImport(spec0, pos, clauza, implicitDinRequire = null, proprietate = null) {
const spec = normalizeazaSpec(spec0);
const importate = [];
const locale = [];
let implicit = implicitDinRequire;
const acolade = /\{([^}]*)\}/.exec(clauza || '');
if (acolade) {
for (const bucata of acolade[1].split(',')) {
const t = bucata.trim().replace(/^type\s+/, '');
if (!t) continue;
const m = /^([\w$]+)(?:\s*(?:as|:)\s*([\w$]+))?$/.exec(t);
if (m) { importate.push(m[1]); locale.push(m[2] || m[1]); }
}
}
const rest = (clauza || '').replace(/\{[^}]*\}/, '').replace(/^type\s+/, '');
const ns = /\*\s*as\s+([\w$]+)/.exec(rest);
if (ns) implicit = ns[1];
const def = /^\s*([\w$]+)\s*(?:,|$)/.exec(rest);
if (!implicit && def && def[1] !== 'type') implicit = def[1];
if (proprietate) { importate.push(proprietate); if (implicit) locale.push(implicit); }
return { spec, pos, importate, locale, implicit };
}
export function detecteazaJs(ctx) {
const c = ctx.code;
const imp = importuriJs(ctx);
const importa = (re) => imp.filter((i) => re.test(i.spec));
const cryptoImp = importa(/^(node:)?crypto$/);
const aliasCrypto = new Set(['crypto']);
for (const i of cryptoImp) { if (i.implicit) aliasCrypto.add(i.implicit); }
const numeImportate = new Set(cryptoImp.flatMap((i) => i.locale));
const nodeCrypto = cryptoImp.length > 0 || ctx.potriviri(/(?<![\w$.])crypto\s*\.\s*(createHash|createHmac|createCipheriv|createDecipheriv|createSign|createVerify|createECDH|generateKeyPairSync|generateKeyPair|generateKeySync|publicEncrypt|privateDecrypt|getDiffieHellman|createDiffieHellman|pbkdf2Sync|pbkdf2|hkdfSync)\s*\(/g).length > 0;
const apeluri = (nume) => ctx.potriviri(new RegExp(`(?<![\\w$.])(?:([\\w$]+)\\s*\\.\\s*)?(${nume})\\s*\\(`, 'g'))
.filter((m) => m[1] !== 'subtle' && (m[1] ? aliasCrypto.has(m[1]) || nodeCrypto : (numeImportate.has(m[2]) || nodeCrypto)));
const argsDe = (m) => {
const a = argumente(ctx, m.index + m[0].length);
return imparteArgumente(ctx, a.start, a.end);
};
const cuValoare = (m, arg, api, fn) => {
const v = valoareArgument(ctx, arg);
if (v.fel === 'literal' || v.fel === 'rezolvat') return fn(v.valoare, v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {});
if (v.fel === 'necunoscut') ctx.adauga(m.index, api, 'call', necunoscut(v.expresie, api), { bucata: m[0] + v.expresie });
return null;
};
if (nodeCrypto) {
for (const m of apeluri('createHash|createHmac')) {
const api = m[2];
const p = argsDe(m);
cuValoare(m, p[0], api, (val, ex) => {
const h = hashCanonic(val) || val;
if (api === 'createHash') ctx.adauga(m.index, api, 'call', { grup: 'HASH', hash: h, functii: ['digest'] }, { ...ex, bucata: `${m[0]}'${val}'` });
else ctx.adauga(m.index, api, 'call', { grup: 'MAC', hash: h, functii: ['tag'] }, { ...ex, bucata: `${m[0]}'${val}'` });
});
}
for (const m of apeluri('createCipheriv|createDecipheriv|createCipher|createDecipher')) {
const api = m[2];
const p = argsDe(m);
cuValoare(m, p[0], api, (val, ex) => {
const cf = cifruOpenssl(val) || { grup: 'CIPHER', nume: val };
ctx.adauga(m.index, api, 'call', { ...cf, functii: [/Decipher/.test(api) ? 'decrypt' : 'encrypt'] }, { ...ex, bucata: `${m[0]}'${val}'` });
});
}
for (const m of apeluri('createSign|createVerify')) {
const api = m[2];
const p = argsDe(m);
cuValoare(m, p[0], api, (val, ex) => {
const f = [api === 'createSign' ? 'sign' : 'verify'];
const h = hashCanonic(val.replace(/^(RSA-|ecdsa-with-|DSA-|RSA-PSS-)/i, '').replace(/with(RSA|DSA)Encryption$/i, '').replace(/WithRSAEncryption$/i, ''));
let a;
if (/rsa/i.test(val)) a = { grup: 'RSA', primitiv: 'signature', hash: h };
else if (/ecdsa/i.test(val)) a = { grup: 'ECDSA', hash: h };
else if (/dsa/i.test(val)) a = { grup: 'DSA', hash: h };
else a = { grup: 'CLASSIC-SIG', hash: h, motiv: `${api} with digest ${val} (Sign/Verify objects take RSA, RSA-PSS, DSA or EC keys; the key type comes from the key object)` };
ctx.adauga(m.index, api, 'call', { ...a, functii: f }, { ...ex, bucata: `${m[0]}'${val}'` });
});
}
for (const m of apeluri('createECDH')) {
const p = argsDe(m);
cuValoare(m, p[0], 'createECDH', (val, ex) => ctx.adauga(m.index, 'createECDH', 'call', { grup: 'ECDH', curba: val, functii: ['keygen'] }, { ...ex, bucata: `${m[0]}'${val}'` }));
}
for (const m of apeluri('getDiffieHellman|createDiffieHellmanGroup')) {
const p = argsDe(m);
cuValoare(m, p[0], m[2], (val, ex) => ctx.adauga(m.index, m[2], 'call', { grup: 'DH', param: MODP[val] ? String(MODP[val]) : val, functii: ['keygen'] }, { ...ex, bucata: `${m[0]}'${val}'` }));
}
for (const m of apeluri('createDiffieHellman')) {
const p = argsDe(m);
const bits = p[0] && /^\d+$/.test(p[0].text) ? p[0].text : undefined;
ctx.adauga(m.index, 'createDiffieHellman', 'call', { grup: 'DH', param: bits, functii: ['keygen'] }, { bucata: m[0] + (bits || '') });
}
for (const m of apeluri('generateKeySync|generateKey')) {
const p = argsDe(m);
const v = valoareArgument(ctx, p[0]);
if ((v.fel === 'literal' || v.fel === 'rezolvat') && v.valoare.toLowerCase() === 'aes') {
const len = p[1] && /length\s*:\s*(\d+)/.exec(p[1].text);
ctx.adauga(m.index, m[2], 'call', { grup: 'CIPHER', nume: 'AES', param: len ? len[1] : undefined, functii: ['keygen'] }, { bucata: `${m[0]}'aes'` });
}
}
for (const m of apeluri('publicEncrypt|privateDecrypt|privateEncrypt|publicDecrypt')) {
const api = m[2];
const a = argumente(ctx, m.index + m[0].length);
const t = a.text;
const enc = api === 'publicEncrypt' || api === 'privateDecrypt';
let padding = enc ? 'oaep' : 'pkcs1v15';
if (/RSA_PKCS1_OAEP_PADDING|oaepHash/.test(t)) padding = 'oaep';
else if (/RSA_PKCS1_PADDING/.test(t)) padding = 'pkcs1v15';
else if (/RSA_NO_PADDING/.test(t)) padding = 'raw';
ctx.adauga(m.index, api, 'call', { grup: 'RSA', primitiv: enc ? 'pke' : 'signature', padding, functii: [api === 'publicEncrypt' || api === 'privateEncrypt' ? 'encrypt' : 'decrypt'] }, { bucata: m[0] });
}
for (const m of apeluri('pbkdf2Sync|pbkdf2')) {
const p = argsDe(m);
cuValoare(m, p[4], m[2], (val, ex) => {
const h = hashCanonic(val) || val;
ctx.adauga(m.index, m[2], 'call', { grup: 'KDF', nume: `PBKDF2-HMAC-${val.toUpperCase()}`, hash: h, functii: ['keyderive'] }, { ...ex, bucata: m[0] });
});
}
for (const m of apeluri('hkdfSync|hkdf')) {
const p = argsDe(m);
cuValoare(m, p[0], m[2], (val, ex) => ctx.adauga(m.index, m[2], 'call', { grup: 'KDF', nume: `HKDF-${val.toUpperCase()}`, hash: hashCanonic(val) || val, functii: ['keyderive'] }, { ...ex, bucata: m[0] }));
}
// chei si certificate incarcate la rulare: algoritmul vine din material, nu din sursa
for (const m of [...apeluri('createPrivateKey|createPublicKey'), ...ctx.potriviri(/(?<![\w$.])new\s+(?:([\w$]+)\s*\.\s*)?(X509Certificate)\s*\(/g).filter((x) => !x[1] || aliasCrypto.has(x[1]))]) {
const api = m[2];
ctx.adauga(m.index, api, 'call', { grup: 'UNKNOWN', motiv: `${api}: key material loaded at run time; its algorithm (RSA, EC, Ed25519, ML-DSA, ...) comes from the key or certificate, which is not in the scanned source. This is a place where keys enter the program: check which algorithm is deployed there.`, functii: ['other'] }, { bucata: m[0] });
}
// crypto.sign / crypto.verify (si importurile numite sign/verify)
for (const m of ctx.potriviri(/(?<![\w$.])(?:([\w$]+)\s*\.\s*)?(sign|verify)\s*\(/g)) {
if (m[1] ? !aliasCrypto.has(m[1]) : !numeImportate.has(m[2])) continue;
const p = argsDe(m);
const api = `crypto.${m[2]}`;
if (!p[0] || /^(null|undefined)$/.test(p[0].text)) {
ctx.adauga(m.index, api, 'call', { grup: 'UNKNOWN', motiv: `${api} with a null algorithm: the algorithm is determined by the key object (for example Ed25519, Ed448, ML-DSA, RSA or ECDSA), which is not visible statically.`, functii: [m[2]] }, { bucata: `${m[0]}null` });
continue;
}
cuValoare(m, p[0], api, (val, ex) => ctx.adauga(m.index, api, 'call', { grup: 'CLASSIC-SIG', hash: hashCanonic(val) || val, motiv: `${api} with digest ${val} (a digest name is used with RSA, RSA-PSS, DSA and EC keys; EdDSA and ML-DSA keys take null)`, functii: [m[2]] }, { ...ex, bucata: `${m[0]}'${val}'` }));
}
}
// generateKeyPair: node:crypto sau jose (acolo primul argument e un algoritm JWS)
const jwtLib = importa(/^(jsonwebtoken|jose|jwt-simple|express-jwt|@fastify\/jwt|passport-jwt|koa-jwt|fast-jwt|jws)$/);
for (const m of ctx.potriviri(/(?<![\w$.])(?:([\w$]+)\s*\.\s*)?(generateKeyPairSync|generateKeyPair)\s*\(/g)) {
if (!nodeCrypto && !jwtLib.length) continue;
const a = argumente(ctx, m.index + m[0].length);
const p = imparteArgumente(ctx, a.start, a.end);
const opt = p[1] ? p[1].text : '';
cuValoare(m, p[0], m[2], (val, ex) => {
if (JWS_RE.test(val) && jwtLib.length) return; // se raporteaza de regula JWT
const t = val.toLowerCase();
const ml = /modulusLength\s*:\s*(\d+)/.exec(opt);
const nc = /namedCurve\s*:\s*(['"])([^'"]+)\1/.exec(opt);
let act;
if (t === 'rsa') act = { grup: 'RSA', param: ml && ml[1] };
else if (t === 'rsa-pss') act = { grup: 'RSA', param: ml && ml[1], primitiv: 'signature', padding: 'other' };
else if (t === 'dsa') act = { grup: 'DSA', param: ml && ml[1] };
else if (t === 'ec') act = { grup: 'EC', curba: nc && nc[2] };
else if (t === 'ed25519' || t === 'ed448') act = { grup: 'EDDSA', nume: t === 'ed25519' ? 'Ed25519' : 'Ed448' };
else if (t === 'x25519' || t === 'x448') act = { grup: 'XDH', nume: t === 'x25519' ? 'X25519' : 'X448' };
else if (t === 'dh') {
const pl = /primeLength\s*:\s*(\d+)/.exec(opt);
const gr = /group\s*:\s*(['"])(modp\d+)\1/.exec(opt);
act = { grup: 'DH', param: pl ? pl[1] : gr ? String(MODP[gr[2]] || gr[2]) : undefined };
} else if (/^ml-dsa-(44|65|87)$/.test(t)) act = { grup: 'MLDSA', param: t.slice(7) };
else if (/^ml-kem-(512|768|1024)$/.test(t)) act = { grup: 'MLKEM', param: t.slice(7) };
else if (/^slh-dsa-(sha2|shake)-(128|192|256)[sf]$/.test(t)) act = { grup: 'SLHDSA', param: t.slice(8).toUpperCase().replace(/([SF])$/, (x) => x.toLowerCase()) };
else act = { grup: 'UNKNOWN', motiv: `Key type "${val}" is not in this tool's catalog.` };
ctx.adauga(m.index, m[2], 'call', { ...act, functii: ['keygen'] }, { ...ex, bucata: `${m[0]}'${val}'` });
});
}
detecteazaTlsJs(ctx);
detecteazaWebCrypto(ctx);
detecteazaBiblioteci(ctx, imp, importa, jwtLib);
}
function detecteazaTlsJs(ctx) {
for (const m of ctx.potriviri(/(?<![\w$])(minVersion|maxVersion|DEFAULT_MIN_VERSION|DEFAULT_MAX_VERSION)\s*[:=]\s*(['"])(TLSv1(?:\.[0-3])?|SSLv3)\2/g)) {
const rol = /min/i.test(m[1]) ? 'min' : 'max';
ctx.adauga(m.index, `tls ${m[1]}`, 'config', { grup: 'TLS', param: TLSV[m[3]], rol }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$])secureProtocol\s*:\s*(['"])(\w+)\1/g)) {
const map = { SSLv2_method: 'ssl2', SSLv3_method: 'ssl3', TLSv1_method: '1.0', TLSv1_1_method: '1.1', TLSv1_2_method: '1.2', TLS_method: 'negotiated', SSLv23_method: 'negotiated' };
const v = map[m[2].replace(/_(client|server)_method$/, '_method')];
if (v) ctx.adauga(m.index, 'tls secureProtocol', 'config', { grup: 'TLS', param: v, rol: 'only' }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$])ecdhCurve\s*:\s*/g)) {
const baza = m.index + m[0].length;
const expr = expresiePanaLaVirgula(ctx, baza);
const lit = /^(['"])([^'"\n]*)\1$/.exec(expr);
if (lit) {
// fiecare grup la coloana lui din sir
let off = 1;
for (const g of lit[2].split(/[:,/]/)) {
const t = g.trim().replace(/^[*?]+/, '');
if (t && t !== 'DEFAULT') ctx.adauga(baza + off + g.indexOf(t), 'tls ecdhCurve', 'config', { ...grupTls(t), functii: ['keygen'] }, { bucata: `ecdhCurve: ${t}` });
off += g.length + 1;
}
continue;
}
const v = valoareExpresie(ctx, expr);
if (!v) {
ctx.adauga(m.index, 'tls ecdhCurve', 'config', necunoscut(expr || '?', 'tls ecdhCurve'), { bucata: m[0] + expr });
continue;
}
for (const g of v.valoare.split(/[:,/]/)) {
const t = g.trim().replace(/^[*?]+/, '');
if (t && t !== 'DEFAULT') ctx.adauga(m.index, 'tls ecdhCurve', 'config', { ...grupTls(t), functii: ['keygen'] }, { bucata: `ecdhCurve: ${t}`, rezolvatDin: v.din });
}
}
}
// textul unei expresii pana la virgula, acolada sau paranteza de nivel zero (fara siruri), max 80 caractere
function expresiePanaLaVirgula(ctx, start) {
const c = ctx.code;
let adancime = 0;
let j = start;
while (j < c.length && j - start < 400) {
if (!ctx.inSir(j)) {
const ch = c[j];
if (ch === '(' || ch === '[' || ch === '{') adancime++;
else if (ch === ')' || ch === ']' || ch === '}') { if (adancime === 0) break; adancime--; }
else if ((ch === ',' || ch === '\n' || ch === ';') && adancime === 0) break;
}
j++;
}
const t = c.slice(start, j).trim();
return t.length > 80 ? t.slice(0, 77) + '...' : t;
}
// valoarea unei expresii: identificator rezolvat, sau template ale carui ${ID} se rezolva toate; altfel null
function valoareExpresie(ctx, expr) {
const id = identificator(expr);
if (id) {
const r = rezolva(ctx, id);
return r ? { valoare: r.valoare, din: `${id} (line ${r.line})` } : null;
}
const t = /^`((?:[^`$\\]|\$\{\s*[A-Za-z_$][\w$]*\s*\})*)`$/.exec(expr);
if (!t) return null;
const din = [];
let ok = true;
const val = t[1].replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (_, n) => {
const r = rezolva(ctx, n);
if (!r) { ok = false; return ''; }
din.push(`${n} (line ${r.line})`);
return r.valoare;
});
return ok ? { valoare: val, din: din.join(', ') } : null;
}
const WEBCRYPTO_ALG = /^(RSA-OAEP|RSASSA-PKCS1-v1_5|RSA-PSS|ECDSA|ECDH|Ed25519|Ed448|X25519|X448|AES-GCM|AES-CBC|AES-CTR|AES-KW|HMAC|HKDF|PBKDF2|ML-KEM-(?:512|768|1024)|ML-DSA-(?:44|65|87)|ChaCha20-Poly1305)$/i;
function activWebCrypto(nume, fereastra) {
const n = nume.toUpperCase();
const ml = /modulusLength\s*:\s*(\d+)/.exec(fereastra);
const nc = /namedCurve\s*:\s*(['"])([^'"]+)\1/.exec(fereastra);
const len = /(?<![\w$])length\s*:\s*(\d+)/.exec(fereastra);
const hm = /hash\s*:\s*(?:\{\s*name\s*:\s*)?(['"])([^'"]+)\1/.exec(fereastra);
const hash = hm ? hashCanonic(hm[2]) || hm[2] : undefined;
if (n === 'RSA-OAEP') return { grup: 'RSA', primitiv: 'pke', padding: 'oaep', param: ml && ml[1] };
if (n === 'RSASSA-PKCS1-V1_5') return { grup: 'RSA', primitiv: 'signature', padding: 'pkcs1v15', param: ml && ml[1], hash };
if (n === 'RSA-PSS') return { grup: 'RSA', primitiv: 'signature', padding: 'other', param: ml && ml[1], hash };
if (n === 'ECDSA') return { grup: 'ECDSA', curba: nc && nc[2], hash };
if (n === 'ECDH') return { grup: 'ECDH', curba: nc && nc[2] };
if (n === 'ED25519' || n === 'ED448') return { grup: 'EDDSA', nume: n === 'ED25519' ? 'Ed25519' : 'Ed448' };
if (n === 'X25519' || n === 'X448') return { grup: 'XDH', nume: n === 'X25519' ? 'X25519' : 'X448' };
if (/^AES-/.test(n)) return { grup: 'CIPHER', nume: 'AES', param: len && len[1], mod: n.slice(4).toLowerCase() };
if (n === 'HMAC') return { grup: 'MAC', hash };
if (n === 'HKDF') return { grup: 'KDF', nume: 'HKDF', hash };
if (n === 'PBKDF2') return { grup: 'KDF', nume: 'PBKDF2', hash };
if (/^ML-KEM-/.test(n)) return { grup: 'MLKEM', param: n.slice(7) };
if (/^ML-DSA-/.test(n)) return { grup: 'MLDSA', param: n.slice(7) };
if (n === 'CHACHA20-POLY1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' };
return null;
}
function detecteazaWebCrypto(ctx) {
if (!ctx.potriviri(/(?<![\w$])subtle\b/g).length) return;
for (const m of ctx.potriviri(/(?<![\w$])name\s*:\s*(['"])([\w-]+)\1/g)) {
if (!WEBCRYPTO_ALG.test(m[2])) continue;
const ob = obiectulDin(ctx, m.index);
const act = activWebCrypto(m[2], ob ? ob.text : '');
if (act) ctx.adauga(m.index, 'WebCrypto', 'call', act, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$])subtle\s*\.\s*(generateKey|importKey|sign|verify|encrypt|decrypt|deriveBits|deriveKey|digest|encapsulateKey|encapsulateBits|decapsulateKey|decapsulateBits)\s*\(/g)) {
const a = argumente(ctx, m.index + m[0].length);
const p = imparteArgumente(ctx, a.start, a.end);
const idx = m[1] === 'importKey' ? 2 : 0;
const arg = p[idx];
if (!arg) continue;
const v = valoareArgument(ctx, arg);
if (m[1] === 'digest') {
if (v.fel === 'literal' || v.fel === 'rezolvat') ctx.adauga(m.index, 'subtle.digest', 'call', { grup: 'HASH', hash: hashCanonic(v.valoare) || v.valoare, functii: ['digest'] }, { bucata: `${m[0]}'${v.valoare}'`, rezolvatDin: v.din });
else {
const ob = /name\s*:\s*(['"])([^'"]+)\1/.exec(arg.text);
if (ob) ctx.adauga(m.index, 'subtle.digest', 'call', { grup: 'HASH', hash: hashCanonic(ob[2]) || ob[2], functii: ['digest'] }, { bucata: `${m[0]}{name:'${ob[2]}'}` });
else if (v.fel === 'necunoscut') ctx.adauga(m.index, 'subtle.digest', 'call', necunoscut(v.expresie, 'subtle.digest'), { bucata: m[0] });
}
continue;
}
if ((v.fel === 'literal' || v.fel === 'rezolvat') && WEBCRYPTO_ALG.test(v.valoare)) {
const act = activWebCrypto(v.valoare, '');
if (act) ctx.adauga(arg.start, `subtle.${m[1]}`, 'call', act, { bucata: `subtle.${m[1]}('${v.valoare}')`, rezolvatDin: v.din });
}
}
}
const ETH = /^(ethers|web3|viem|viem\/accounts|@ethersproject\/[\w-]+|ethereumjs-[\w-]+|@ethereumjs\/[\w-]+|ethereum-cryptography(\/.*)?|web3-eth-accounts)$/;
const SECP_LIB = /^(secp256k1|tiny-secp256k1|@noble\/secp256k1|@bitcoinerlab\/secp256k1|ecpair|bitcoinjs-lib|eccrypto)$/;
function detecteazaBiblioteci(ctx, imp, importa, jwtLib) {
const c = ctx.code;
const primul = (lista) => lista[0];
// Ethereum: conturile si semnaturile sunt secp256k1 ECDSA
const eth = importa(ETH);
if (eth.length) {
const i = primul(eth);
ctx.adauga(i.pos, `import ${i.spec}`, 'import', { grup: 'SECP256K1', nota: `Imported library: ${i.spec}, whose accounts and transaction signatures are secp256k1 ECDSA; the import alone does not show that this file signs.` }, { suprimaDe: ['SECP256K1'], bucata: `import ${i.spec}` });
const re1 = /(?<![\w$.])(?:new\s+(?:[\w$]+\s*\.\s*)*(?:Wallet|SigningKey|HDNodeWallet)\s*\(|(?:[\w$]+\s*\.\s*)?(?:(?:Wallet|HDNodeWallet)\s*\.\s*(?:createRandom|fromPhrase|fromMnemonic|fromEncryptedJson|fromEncryptedJsonSync|fromSeed)|privateKeyToAccount|mnemonicToAccount|generatePrivateKey|recoverAddress|recoverMessageAddress|verifyMessage|verifyTypedData|recoverPublicKey|ecrecover|ecsign)\s*\()/g;
const re2 = /\.\s*(?:signMessage|signTransaction|signTypedData|_signTypedData|signAuthorization)\s*\(|\baccounts\s*\.\s*(?:create|sign|signTransaction|privateKeyToAccount|recover)\s*\(/g;
for (const m of [...ctx.potriviri(re1), ...ctx.potriviri(re2)]) {
const f = /verify|recover/i.test(m[0]) ? ['verify'] : /sign/i.test(m[0]) ? ['sign'] : ['keygen'];
ctx.adauga(m.index, m[0].replace(/[\s(]/g, ''), 'call', { grup: 'SECP256K1', functii: f }, { bucata: m[0] });
}
}
for (const i of importa(SECP_LIB)) {
ctx.adauga(i.pos, `import ${i.spec}`, 'import', { grup: 'SECP256K1', nota: `Imported library: ${i.spec} (secp256k1 only).` }, { bucata: `import ${i.spec}` });
}
// elliptic
const ell = importa(/^elliptic$/);
if (ell.length) {
for (const m of ctx.potriviri(/(?<![\w$.])new\s+(?:[\w$]+\s*\.\s*)?(ec|EC)\s*\(\s*(['"])([\w-]+)\2/g)) {
ctx.adauga(m.index, 'elliptic ec', 'call', { grup: 'EC', curba: m[3], functii: ['keygen'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$.])new\s+(?:[\w$]+\s*\.\s*)?(eddsa|EdDSA)\s*\(\s*(['"])([\w-]+)\2/g)) {
ctx.adauga(m.index, 'elliptic eddsa', 'call', { grup: 'EDDSA', nume: /448/.test(m[3]) ? 'Ed448' : 'Ed25519' }, { bucata: m[0] });
}
ctx.adauga(ell[0].pos, 'import elliptic', 'import', { grup: 'LIB-MULTI', nume: 'elliptic', motiv: 'Imports elliptic (classical elliptic-curve library, quantum-vulnerable in every mode); no curve construction recognized in this file.' }, { suprimaDe: ['EC', 'ECDSA', 'ECDH', 'SECP256K1', 'EDDSA'], bucata: 'import elliptic' });
}
// node-forge
const forge = importa(/^node-forge$/);
if (forge.length) {
for (const m of ctx.potriviri(/(?<![\w$])pki\s*\.\s*rsa\s*\.\s*generateKeyPair\s*\(/g)) {
const a = argumente(ctx, m.index + m[0].length);
const b = /bits\s*:\s*(\d+)/.exec(a.text) || /^\s*(\d+)/.exec(a.text);
ctx.adauga(m.index, 'forge.pki.rsa.generateKeyPair', 'call', { grup: 'RSA', param: b && b[1], functii: ['keygen'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$])md\s*\.\s*(md5|sha1|sha256|sha384|sha512)\s*\.\s*create\s*\(/g)) {
ctx.adauga(m.index, `forge.md.${m[1]}`, 'call', { grup: 'HASH', hash: hashCanonic(m[1]), functii: ['digest'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$])cipher\s*\.\s*create(?:De)?cipher\s*\(\s*(['"])([\w-]+)\1/g)) {
const t = /^(AES|3DES|DES|RC2)(?:-(\w+))?$/i.exec(m[2]);
ctx.adauga(m.index, 'forge.cipher', 'call', t ? { grup: 'CIPHER', nume: t[1].toUpperCase(), mod: t[2] } : { grup: 'CIPHER', nume: m[2] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$])ed25519\s*\.\s*(generateKeyPair|sign|verify)\s*\(/g)) {
ctx.adauga(m.index, `forge.ed25519.${m[1]}`, 'call', { grup: 'EDDSA', nume: 'Ed25519' }, { bucata: m[0] });
}
ctx.adauga(forge[0].pos, 'import node-forge', 'import', { grup: 'LIB-MULTI', nume: 'node-forge' }, { suprimaDe: ['RSA', 'HASH', 'CIPHER', 'EDDSA'], bucata: 'import node-forge' });
}
// JWT: literalele de algoritm JWS din fisierele care importa o biblioteca JWT
if (jwtLib.length) {
let gasit = false;
for (const [s, e] of ctx.siruri) {
if (!`'"`.includes(ctx.text[s]) || ctx.text[e - 1] !== ctx.text[s]) continue;
const v = ctx.text.slice(s + 1, e - 1);
if (!JWS_RE.test(v)) continue;
if (v === 'none') {
const inainte = ctx.code.slice(Math.max(0, s - 60), s);
if (!/alg(?:orithms?)?\s*:\s*\[?[^\]\n]*$/.test(inainte)) continue;
}
gasit = true;
ctx.adauga(s, 'JWT algorithm', 'config', { ...jws(v), functii: ['sign', 'verify'] }, { bucata: `'${v}'` });
}
if (!gasit) {
ctx.adauga(jwtLib[0].pos, `import ${jwtLib[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: jwtLib[0].spec, motiv: `Imports ${jwtLib[0].spec}; no JWS algorithm literal found in this file (the algorithm may come from configuration or library defaults).` }, { bucata: `import ${jwtLib[0].spec}` });
}
}
// @noble/curves
for (const i of importa(/^@noble\/curves(\/.*)?$/)) {
const map = {
secp256k1: { grup: 'SECP256K1' }, schnorr: { grup: 'SECP256K1', nume: 'Schnorr-secp256k1' },
ed25519: { grup: 'EDDSA', nume: 'Ed25519' }, ed25519ph: { grup: 'EDDSA', nume: 'Ed25519' }, ed25519ctx: { grup: 'EDDSA', nume: 'Ed25519' },
x25519: { grup: 'XDH', nume: 'X25519' }, ed448: { grup: 'EDDSA', nume: 'Ed448' }, x448: { grup: 'XDH', nume: 'X448' },
p256: { grup: 'EC', curba: 'secp256r1' }, secp256r1: { grup: 'EC', curba: 'secp256r1' },
p384: { grup: 'EC', curba: 'secp384r1' }, secp384r1: { grup: 'EC', curba: 'secp384r1' },
p521: { grup: 'EC', curba: 'secp521r1' }, secp521r1: { grup: 'EC', curba: 'secp521r1' },
bls12_381: { grup: 'PAIRING', nume: 'BLS12-381' }, bn254: { grup: 'PAIRING', nume: 'BN254' },
};
const sub = (/^@noble\/curves\/([\w-]+)/.exec(i.spec) || [])[1];
const nume = i.importate.length ? i.importate : (sub ? [sub.replace(/-/g, '_').replace('bls12_381', 'bls12_381')] : []);
for (const n of nume) {
const act = map[n] || (n === 'nist' ? { grup: 'EC' } : null);
if (act) ctx.adauga(i.pos, `import ${i.spec}`, 'import', act, { bucata: `import { ${n} } from '${i.spec}'` });
}
}
// @noble/post-quantum
for (const i of importa(/^@noble\/post-quantum(\/.*)?$/)) {
const sub = (/^@noble\/post-quantum\/([\w-]+)/.exec(i.spec) || [])[1] || '';
const lista = i.importate.length ? i.importate : [sub];
for (const n of lista) {
let m;
let act = null;
if ((m = /^ml_kem(512|768|1024)$/.exec(n))) act = { grup: 'MLKEM', param: m[1] };
else if ((m = /^ml_dsa(44|65|87)$/.exec(n))) act = { grup: 'MLDSA', param: m[1] };
else if ((m = /^slh_dsa_(sha2|shake)_(128|192|256)([sf])$/.exec(n))) act = { grup: 'SLHDSA', param: `${m[1].toUpperCase()}-${m[2]}${m[3]}` };
else if ((m = /^ml_kem(768|1024)_(x25519|p256|p384)$/i.exec(n))) act = { grup: 'HYBRID-KEX', nume: `ML-KEM-${m[1]}+${m[2].toUpperCase()}` };
else if (/^xwing$/i.test(n)) act = { grup: 'HYBRID-KEX', nume: 'X-Wing' };
else if (n === 'ml-kem') act = { grup: 'MLKEM' };
else if (n === 'ml-dsa') act = { grup: 'MLDSA' };
else if (n === 'slh-dsa') act = { grup: 'SLHDSA' };
if (act) ctx.adauga(i.pos, `import ${i.spec}`, 'import', act, { bucata: `import { ${n} } from '${i.spec}'` });
}
}
// @noble/hashes
for (const i of importa(/^@noble\/hashes(\/.*)?$/)) {
for (const n of i.importate) {
const h = hashCanonic(n.replace(/_/g, '-')) || ({ keccak_256: 'KECCAK256', sha3_256: 'SHA3-256', sha3_512: 'SHA3-512', ripemd160: 'RIPEMD160' })[n];
if (h) ctx.adauga(i.pos, `import ${i.spec}`, 'import', { grup: 'HASH', hash: h }, { bucata: `import { ${n} } from '${i.spec}'` });
}
}
// crypto-js
const cjs = importa(/^crypto-js(\/.*)?$/);
if (cjs.length) {
for (const m of ctx.potriviri(/(?<![\w$.])[\w$]+\s*\.\s*(MD5|SHA1|SHA224|SHA256|SHA384|SHA512|RIPEMD160|HmacMD5|HmacSHA1|HmacSHA256|HmacSHA384|HmacSHA512)\s*\(/g)) {
const hmac = /^Hmac/.test(m[1]);
const h = hashCanonic(m[1].replace(/^Hmac/, ''));
ctx.adauga(m.index, `CryptoJS.${m[1]}`, 'call', hmac ? { grup: 'MAC', hash: h, functii: ['tag'] } : { grup: 'HASH', hash: h, functii: ['digest'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$.])[\w$]+\s*\.\s*(AES|DES|TripleDES|RC4|RC4Drop|Rabbit|Blowfish)\s*\.\s*(encrypt|decrypt)\s*\(/g)) {
const nume = m[1] === 'TripleDES' ? '3DES' : m[1] === 'RC4Drop' ? 'RC4' : m[1];
ctx.adauga(m.index, `CryptoJS.${m[1]}.${m[2]}`, 'call', { grup: 'CIPHER', nume, functii: [m[2]] }, { bucata: m[0] });
}
ctx.adauga(cjs[0].pos, `import ${cjs[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: 'crypto-js' }, { suprimaDe: ['HASH', 'MAC', 'CIPHER'], bucata: `import ${cjs[0].spec}` });
}
// tweetnacl / libsodium
const nacl = importa(/^(tweetnacl|libsodium-wrappers|libsodium-wrappers-sumo|sodium-native)$/);
if (nacl.length) {
for (const m of ctx.potriviri(/(?<![\w$.])[\w$]+\s*\.\s*(sign(?:\s*\.\s*(?:keyPair|detached))?|crypto_sign\w*)\s*\(/g)) {
ctx.adauga(m.index, 'nacl.sign', 'call', { grup: 'EDDSA', nume: 'Ed25519', functii: ['sign'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$.])[\w$]+\s*\.\s*(box(?:\s*\.\s*(?:keyPair|before))?|crypto_box\w*|crypto_kx\w*|crypto_scalarmult\w*)\s*\(/g)) {
ctx.adauga(m.index, 'nacl.box', 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$.])[\w$]+\s*\.\s*(secretbox|crypto_secretbox\w*|crypto_aead_xchacha20poly1305\w*)\s*\(/g)) {
ctx.adauga(m.index, 'nacl.secretbox', 'call', { grup: 'CIPHER', nume: /xchacha/.test(m[1]) ? 'XChaCha20-Poly1305' : 'XSalsa20-Poly1305', functii: ['encrypt'] }, { bucata: m[0] });
}
ctx.adauga(nacl[0].pos, `import ${nacl[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: nacl[0].spec }, { suprimaDe: ['EDDSA', 'XDH', 'CIPHER'], bucata: `import ${nacl[0].spec}` });
}
// node-rsa
const nrsa = importa(/^node-rsa$/);
if (nrsa.length) {
for (const m of ctx.potriviri(/(?<![\w$.])new\s+[\w$]+\s*\(\s*\{\s*b\s*:\s*(\d+)/g)) {
ctx.adauga(m.index, 'new NodeRSA', 'call', { grup: 'RSA', param: m[1], functii: ['keygen'] }, { bucata: m[0] });
}
ctx.adauga(nrsa[0].pos, 'import node-rsa', 'import', { grup: 'RSA' }, { suprimaDe: ['RSA'], bucata: 'import node-rsa' });
}
// parole: bcrypt / argon2 / scrypt
for (const i of importa(/^(bcrypt|bcryptjs|argon2|@node-rs\/argon2|@node-rs\/bcrypt|scrypt-js)$/)) {
const nume = /argon2/.test(i.spec) ? 'Argon2' : /scrypt/.test(i.spec) ? 'scrypt' : 'bcrypt';
ctx.adauga(i.pos, `import ${i.spec}`, 'import', { grup: 'KDF', nume }, { bucata: `import ${i.spec}` });
}
// biblioteci cu multi algoritmi, fara apel recunoscut
for (const i of importa(/^(openpgp|jsrsasign|sshpk|@peculiar\/x509|pkijs|node-jose)$/)) {
ctx.adauga(i.pos, `import ${i.spec}`, 'import', { grup: 'LIB-MULTI', nume: i.spec }, { bucata: `import ${i.spec}` });
}
void c;
}

View File

@ -0,0 +1,208 @@
// Detectorul Python: cryptography (hazmat), PyCryptodome, hashlib, hmac, ecdsa, PyJWT/jose, oqs, ssl.
import { argumente, imparteArgumente, valoareArgument, necunoscut } from './context.mjs';
import { hashCanonic, jws, JWS_RE } from './catalog.mjs';
const CURBE_PY = {
SECP256R1: 'secp256r1', SECP384R1: 'secp384r1', SECP521R1: 'secp521r1', SECP224R1: 'secp224r1', SECP192R1: 'secp192r1',
SECP256K1: 'secp256k1', BrainpoolP256R1: 'brainpoolP256r1', BrainpoolP384R1: 'brainpoolP384r1', BrainpoolP512R1: 'brainpoolP512r1',
SECT571R1: 'sect571r1', SECT409R1: 'sect409r1', SECT283R1: 'sect283r1', SECT233R1: 'sect233r1', SECT163R2: 'sect163r2',
};
const CURBE_ECDSA_LIB = { SECP256k1: 'secp256k1', NIST192p: 'secp192r1', NIST224p: 'secp224r1', NIST256p: 'secp256r1', NIST384p: 'secp384r1', NIST521p: 'secp521r1', BRAINPOOLP256r1: 'brainpoolP256r1', BRAINPOOLP384r1: 'brainpoolP384r1', BRAINPOOLP512r1: 'brainpoolP512r1' };
function importaPy(ctx, re) {
return ctx.potriviri(/^[ \t]*(?:from\s+([\w.]+)\s+import\b|import\s+([\w.]+(?:\s*,\s*[\w.]+)*))/gm)
.filter((m) => (m[1] ? re.test(m[1]) : m[2].split(',').some((x) => re.test(x.trim()))));
}
export function detecteazaPy(ctx) {
const argsDe = (m) => { const a = argumente(ctx, m.index + m[0].length); return { ...a, parti: imparteArgumente(ctx, a.start, a.end) }; };
const cuValoare = (m, arg, api, fn) => {
const v = valoareArgument(ctx, arg);
if (v.fel === 'literal' || v.fel === 'rezolvat') return fn(v.valoare, v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {});
if (v.fel === 'necunoscut') ctx.adauga(m.index, api, 'call', necunoscut(v.expresie, api), { bucata: m[0] + v.expresie });
return null;
};
const numar = (a, cheie, poz) => {
const k = new RegExp(`\\b${cheie}\\s*=\\s*(\\d+)`).exec(a.text);
if (k) return k[1];
if (poz !== undefined && a.parti[poz] && /^\d+$/.test(a.parti[poz].text)) return a.parti[poz].text;
return undefined;
};
// --- cryptography.hazmat ---
if (importaPy(ctx, /^cryptography\b/).length) {
for (const m of ctx.potriviri(/(?<![\w.])(rsa|dsa|dh)\s*\.\s*(generate_private_key|generate_parameters)\s*\(/g)) {
const a = argsDe(m);
const bits = m[1] === 'rsa' ? numar(a, 'key_size', 1) : numar(a, 'key_size', m[1] === 'dh' ? 1 : 0);
ctx.adauga(m.index, `${m[1]}.${m[2]}`, 'call', { grup: m[1].toUpperCase(), param: bits, functii: ['keygen'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])ec\s*\.\s*(generate_private_key|derive_private_key)\s*\(/g)) {
const a = argsDe(m);
const k = /\bec\s*\.\s*(\w+)\s*\(/.exec(a.text);
ctx.adauga(m.index, `ec.${m[1]}`, 'call', { grup: 'EC', curba: k ? (CURBE_PY[k[1]] || k[1]) : undefined, functii: ['keygen'] }, { bucata: m[0] + (k ? k[0] : '') });
}
for (const m of ctx.potriviri(/(?<![\w.])ec\s*\.\s*ECDSA\s*\(/g)) {
const a = argsDe(m);
const h = /\bhashes\s*\.\s*(\w+)\s*\(/.exec(a.text);
ctx.adauga(m.index, 'ec.ECDSA', 'call', { grup: 'ECDSA', hash: h ? hashCanonic(h[1].replace(/_/g, '-')) : undefined, functii: ['sign', 'verify'] }, { bucata: m[0] + (h ? h[0] : '') });
}
for (const m of ctx.potriviri(/(?<![\w.])ec\s*\.\s*ECDH\s*\(/g)) {
ctx.adauga(m.index, 'ec.ECDH', 'call', { grup: 'ECDH', functii: ['keygen'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w])(?:\w+\s*\.\s*)?(Ed25519|Ed448|X25519|X448)(PrivateKey|PublicKey)\s*\.\s*(generate|from_private_bytes|from_public_bytes)\s*\(/g)) {
const ed = /^Ed/.test(m[1]);
ctx.adauga(m.index, `${m[1]}${m[2]}.${m[3]}`, 'call', { grup: ed ? 'EDDSA' : 'XDH', nume: m[1], functii: [m[3] === 'generate' ? 'keygen' : 'other'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])padding\s*\.\s*(OAEP|PSS|PKCS1v15)\s*\(/g)) {
const act = m[1] === 'OAEP' ? { grup: 'RSA', primitiv: 'pke', padding: 'oaep' } : m[1] === 'PSS' ? { grup: 'RSA', primitiv: 'signature', padding: 'other' } : { grup: 'RSA', primitiv: 'unknown', padding: 'pkcs1v15' };
ctx.adauga(m.index, `padding.${m[1]}`, 'call', act, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])hashes\s*\.\s*(MD5|SHA1|SHA224|SHA256|SHA384|SHA512|SHA512_224|SHA512_256|SHA3_224|SHA3_256|SHA3_384|SHA3_512|SHAKE128|SHAKE256|BLAKE2b|BLAKE2s|SM3)\s*\(/g)) {
const t = m[1].replace(/^SHA512_(224|256)$/, 'SHA512/$1').replace(/_/g, '-');
ctx.adauga(m.index, `hashes.${m[1]}`, 'call', { grup: 'HASH', hash: hashCanonic(t) || t, functii: ['digest'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])algorithms\s*\.\s*(AES|AES128|AES256|TripleDES|ARC4|Blowfish|CAST5|IDEA|SEED|ChaCha20|Camellia|SM4)\s*\(/g)) {
const map = { AES128: ['AES', '128'], AES256: ['AES', '256'], TripleDES: ['3DES'], ARC4: ['RC4'] };
const [nume, param] = map[m[1]] || [m[1]];
const a = argsDe(m);
const dupa = ctx.code.slice(a.end, a.end + 80);
const mod = /^\s*\)\s*,\s*modes\s*\.\s*(\w+)\s*\(/.exec(dupa);
ctx.adauga(m.index, `algorithms.${m[1]}`, 'call', { grup: 'CIPHER', nume, param, mod: mod ? mod[1].toLowerCase() : undefined, functii: ['encrypt'] }, { bucata: m[0] + (mod ? ` modes.${mod[1]}` : '') });
}
for (const m of ctx.potriviri(/(?<![\w.])(AESGCM|AESCCM|AESOCB3|AESSIV|AESGCMSIV|ChaCha20Poly1305)\s*(?:\.\s*generate_key\s*)?\(/g)) {
const a = argsDe(m);
const b = /generate_key/.test(m[0]) ? numar(a, 'bit_length', 0) : undefined;
const map = { AESGCM: 'gcm', AESCCM: 'ccm', AESOCB3: 'ocb', AESSIV: 'siv', AESGCMSIV: 'gcm-siv' };
const act = m[1] === 'ChaCha20Poly1305' ? { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' } : { grup: 'CIPHER', nume: 'AES', param: b, mod: map[m[1]] };
ctx.adauga(m.index, m[1], 'call', { ...act, functii: [/generate_key/.test(m[0]) ? 'keygen' : 'encrypt'] }, { bucata: m[0] });
}
}
// --- PyCryptodome (Crypto / Cryptodome) ---
if (importaPy(ctx, /^(Crypto|Cryptodome)\b/).length) {
for (const m of ctx.potriviri(/(?<![\w.])(RSA|DSA)\s*\.\s*generate\s*\(/g)) {
const a = argsDe(m);
ctx.adauga(m.index, `${m[1]}.generate`, 'call', { grup: m[1], param: numar(a, 'bits', 0), functii: ['keygen'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])(RSA|DSA|ECC)\s*\.\s*(import_key|importKey|construct)\s*\(/g)) {
ctx.adauga(m.index, `${m[1]}.${m[2]}`, 'call', m[1] === 'ECC' ? { grup: 'EC' } : { grup: m[1] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])ECC\s*\.\s*generate\s*\(/g)) {
const a = argsDe(m);
const k = /\bcurve\s*=\s*(['"])([^'"]+)\1/.exec(a.text);
const curba = k ? k[2] : undefined;
const act = curba && /^ed(25519|448)$/i.test(curba) ? { grup: 'EDDSA', nume: /448/.test(curba) ? 'Ed448' : 'Ed25519' } : { grup: 'EC', curba };
ctx.adauga(m.index, 'ECC.generate', 'call', { ...act, functii: ['keygen'] }, { bucata: m[0] + (k ? k[0] : '') });
}
for (const m of ctx.potriviri(/(?<![\w.])(AES|DES|DES3|ARC4|ARC2|Blowfish|CAST|ChaCha20|ChaCha20_Poly1305|Salsa20)\s*\.\s*new\s*\(/g)) {
const a = argsDe(m);
const mod = /\b(?:AES|DES|DES3|ARC2|Blowfish|CAST)\s*\.\s*MODE_(\w+)/.exec(a.text);
const map = { DES3: '3DES', ARC4: 'RC4', ARC2: 'RC2', ChaCha20_Poly1305: 'ChaCha20-Poly1305' };
ctx.adauga(m.index, `${m[1]}.new`, 'call', { grup: 'CIPHER', nume: map[m[1]] || m[1], mod: mod ? mod[1].toLowerCase() : undefined, functii: ['encrypt'] }, { bucata: m[0] + (mod ? mod[0] : '') });
}
for (const m of ctx.potriviri(/(?<![\w.])(PKCS1_OAEP|PKCS1_v1_5|pkcs1_15|pss|DSS|eddsa)\s*\.\s*new\s*\(/g)) {
const map = {
PKCS1_OAEP: { grup: 'RSA', primitiv: 'pke', padding: 'oaep' },
PKCS1_v1_5: { grup: 'RSA', primitiv: 'unknown', padding: 'pkcs1v15' },
pkcs1_15: { grup: 'RSA', primitiv: 'signature', padding: 'pkcs1v15' },
pss: { grup: 'RSA', primitiv: 'signature', padding: 'other' },
DSS: { grup: 'CLASSIC-SIG', nume: 'DSS (DSA or ECDSA)', motiv: 'DSS signature (DSA or ECDSA, key type from the key object)' },
eddsa: { grup: 'EDDSA', nume: 'EdDSA' },
};
ctx.adauga(m.index, `${m[1]}.new`, 'call', map[m[1]], { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])(MD2|MD4|MD5|SHA1|SHA224|SHA256|SHA384|SHA512|SHA3_256|SHA3_384|SHA3_512|RIPEMD160|RIPEMD|BLAKE2b|BLAKE2s)\s*\.\s*new\s*\(/g)) {
const t = m[1].replace(/_/g, '-').replace(/^RIPEMD$/, 'RIPEMD160');
ctx.adauga(m.index, `${m[1]}.new`, 'call', { grup: 'HASH', hash: hashCanonic(t) || t, functii: ['digest'] }, { bucata: m[0] });
}
}
// --- hashlib ---
for (const m of ctx.potriviri(/(?<![\w.])hashlib\s*\.\s*(md5|sha1|sha224|sha256|sha384|sha512|sha3_224|sha3_256|sha3_384|sha3_512|blake2b|blake2s|shake_128|shake_256)\b(\s*\()?/g)) {
let nota;
if (m[2]) {
const a = argumente(ctx, m.index + m[0].length);
if (/usedforsecurity\s*=\s*False/.test(a.text)) nota = 'Marked usedforsecurity=False: likely a non-security use (checksum); review and document.';
}
const t = m[1].replace(/^shake_/, 'SHAKE').replace(/_/g, '-');
ctx.adauga(m.index, `hashlib.${m[1]}`, 'call', { grup: 'HASH', hash: hashCanonic(t) || t, nota, functii: ['digest'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])hashlib\s*\.\s*new\s*\(/g)) {
const a = argsDe(m);
cuValoare(m, a.parti[0], 'hashlib.new', (val, ex) => ctx.adauga(m.index, 'hashlib.new', 'call', { grup: 'HASH', hash: hashCanonic(val) || val, functii: ['digest'] }, { ...ex, bucata: `hashlib.new('${val}')` }));
}
for (const m of ctx.potriviri(/(?<![\w.])hashlib\s*\.\s*pbkdf2_hmac\s*\(/g)) {
const a = argsDe(m);
cuValoare(m, a.parti[0], 'hashlib.pbkdf2_hmac', (val, ex) => ctx.adauga(m.index, 'hashlib.pbkdf2_hmac', 'call', { grup: 'KDF', nume: `PBKDF2-HMAC-${val.toUpperCase()}`, hash: hashCanonic(val) || val, functii: ['keyderive'] }, { ...ex, bucata: m[0] }));
}
for (const m of ctx.potriviri(/(?<![\w.])hmac\s*\.\s*(new|digest)\s*\(/g)) {
const a = argsDe(m);
const d = /\bdigestmod\s*=\s*(['"])(\w+)\1/.exec(a.text);
if (d) ctx.adauga(m.index, `hmac.${m[1]}`, 'call', { grup: 'MAC', hash: hashCanonic(d[2]) || d[2], functii: ['tag'] }, { bucata: m[0] + d[0] });
}
// --- ecdsa (python-ecdsa) ---
if (importaPy(ctx, /^ecdsa\b/).length) {
for (const m of ctx.potriviri(/(?<![\w.])(SigningKey|VerifyingKey)\s*\.\s*(generate|from_string|from_secret_exponent|from_pem|from_der|from_public_point)\s*\(/g)) {
const a = argsDe(m);
const k = /\bcurve\s*=\s*(?:ecdsa\s*\.\s*)?(\w+)/.exec(a.text);
let curba = k ? (CURBE_ECDSA_LIB[k[1]] || k[1]) : undefined;
let nota;
if (!k && m[2] === 'generate') { curba = 'secp192r1'; nota = 'No curve argument: python-ecdsa defaults to NIST192p.'; }
const act = curba && /^Ed(25519|448)$/.test(curba) ? { grup: 'EDDSA', nume: curba } : { grup: 'ECDSA', curba };
ctx.adauga(m.index, `${m[1]}.${m[2]}`, 'call', { ...act, nota, functii: [m[1] === 'SigningKey' ? 'sign' : 'verify'] }, { bucata: m[0] + (k ? k[0] : '') });
}
}
// --- JWT (PyJWT, python-jose, jwcrypto, authlib) ---
const jwtImp = importaPy(ctx, /^(jwt|jose|jwcrypto|authlib\.jose)\b/);
if (jwtImp.length) {
let gasit = false;
for (const [s, e] of ctx.siruri) {
if (!`'"`.includes(ctx.text[s]) || ctx.text[e - 1] !== ctx.text[s]) continue;
const v = ctx.text.slice(s + 1, e - 1);
if (!JWS_RE.test(v)) continue;
if (v === 'none') {
const inainte = ctx.code.slice(Math.max(0, s - 60), s);
if (!/alg(?:orithms?)?\s*=\s*\[?[^\]\n]*$/.test(inainte)) continue;
}
gasit = true;
ctx.adauga(s, 'JWT algorithm', 'config', { ...jws(v), functii: ['sign', 'verify'] }, { bucata: `'${v}'` });
}
if (!gasit) ctx.adauga(jwtImp[0].index, 'import jwt', 'import', { grup: 'LIB-MULTI', nume: 'JWT library', motiv: 'Imports a JWT library; no JWS algorithm literal found in this file (the algorithm may come from configuration or library defaults).' }, { bucata: jwtImp[0][0].trim() });
}
// --- liboqs-python ---
for (const m of ctx.potriviri(/(?<![\w.])oqs\s*\.\s*(KeyEncapsulation|Signature)\s*\(/g)) {
const a = argsDe(m);
cuValoare(m, a.parti[0], `oqs.${m[1]}`, (val, ex) => ctx.adauga(m.index, `oqs.${m[1]}`, 'call', oqsActiv(val), { ...ex, bucata: `oqs.${m[1]}('${val}')` }));
}
// --- ssl ---
for (const m of ctx.potriviri(/(?<![\w.])ssl\s*\.\s*PROTOCOL_(SSLv2|SSLv3|SSLv23|TLSv1_2|TLSv1_1|TLSv1|TLS_CLIENT|TLS_SERVER|TLS)\b/g)) {
const map = { SSLv2: 'ssl2', SSLv3: 'ssl3', TLSv1: '1.0', TLSv1_1: '1.1', TLSv1_2: '1.2' };
ctx.adauga(m.index, `ssl.PROTOCOL_${m[1]}`, 'config', { grup: 'TLS', param: map[m[1]] || 'negotiated', rol: 'only' }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])ssl\s*\.\s*TLSVersion\s*\.\s*(SSLv3|TLSv1_3|TLSv1_2|TLSv1_1|TLSv1)\b/g)) {
const map = { SSLv3: 'ssl3', TLSv1: '1.0', TLSv1_1: '1.1', TLSv1_2: '1.2', TLSv1_3: '1.3' };
const inainte = ctx.code.slice(Math.max(0, m.index - 40), m.index);
const rol = /maximum_version\s*=\s*$/.test(inainte) ? 'max' : 'min';
ctx.adauga(m.index, `ssl.TLSVersion.${m[1]}`, 'config', { grup: 'TLS', param: map[m[1]], rol }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/\.\s*set_ecdh_curve\s*\(/g)) {
const a = argsDe(m);
cuValoare(m, a.parti[0], 'set_ecdh_curve', (val, ex) => ctx.adauga(m.index, 'set_ecdh_curve', 'config', { grup: 'ECDH', curba: val, functii: ['keygen'] }, { ...ex, bucata: m[0] }));
}
}
export function oqsActiv(val) {
const t = String(val);
let m;
if ((m = /^ML-KEM-(512|768|1024)$/i.exec(t))) return { grup: 'MLKEM', param: m[1] };
if ((m = /^ML-DSA-(44|65|87)$/i.exec(t))) return { grup: 'MLDSA', param: m[1] };
if ((m = /^SLH[-_]DSA[-_](SHA2|SHAKE)[-_](128|192|256)([sf])/i.exec(t))) return { grup: 'SLHDSA', param: `${m[1].toUpperCase()}-${m[2]}${m[3].toLowerCase()}` };
if ((m = /^Falcon-(512|1024)$/i.exec(t))) return { grup: 'FALCON', param: m[1] };
if (/^(Kyber\d+|Dilithium\d|SPHINCS\+?-.*)$/i.test(t)) return { grup: 'PREPQ', nume: t, primitiv: /kyber/i.test(t) ? 'kem' : 'signature' };
if (/^HQC-\d+$/i.test(t)) return { grup: 'HQC', nume: t };
return { grup: 'UNKNOWN', motiv: `Post-quantum mechanism "${t}" is not in this tool's catalog.` };
}

View File

@ -0,0 +1,230 @@
// Lexer minimal pe limbaje: comentariile devin spatii (liniile si coloanele raman pe loc),
// sirurile raman in text, dar li se tin minte intervalele, ca un detector sa poata cere
// "numele API-ului e COD, nu text dintr-un sir".
//
// Nu e un parser: e cat trebuie ca un tipar sa nu se potriveasca in comentarii, in
// docstring-uri Python sau in textul unui sir.
const KW_REGEX = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
'throw', 'case', 'do', 'else', 'yield', 'await']);
const PUNCT_REGEX = '(,=:[!&|?{};+-*%<>~^';
export function curata(text, limbaj) {
switch (limbaj) {
case 'javascript': return curataC(text, { template: true, regex: true });
case 'java': return curataC(text, { textBlock: true });
case 'go': return curataC(text, { raw: true });
case 'python': return curataPython(text);
default: return { code: text, siruri: [] };
}
}
function golitor(out) {
return (a, b) => {
for (let k = a; k < b; k++) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' ';
};
}
function curataC(text, o) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
const stiva = [];
let acolade = 0;
let prevSig = '';
let prevWord = '';
let i = 0;
let esecRegex = null; // bit 1: din (pozitie, in afara clasei) nu se ajunge la sfarsitul literalului; bit 2: la fel, in clasa
// scaneaza un segment de template literal pornind de la j; intoarce pozitia si daca s-a deschis ${
const scanTpl = (inceput, j) => {
while (j < n) {
const ch = text[j];
if (ch === '\\') { j += 2; continue; }
if (ch === '`') { siruri.push([inceput, j + 1]); return { pos: j + 1, deschis: false }; }
if (ch === '$' && text[j + 1] === '{') { siruri.push([inceput, j + 2]); return { pos: j + 2, deschis: true }; }
j++;
}
siruri.push([inceput, n]);
return { pos: n, deschis: false };
};
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (c === '/' && d === '/') {
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
let j = text.indexOf('*/', i + 2);
j = j < 0 ? n : j + 2;
goleste(i, j); i = j; continue;
}
if (o.textBlock && c === '"' && text.startsWith('"""', i)) {
let j = text.indexOf('"""', i + 3);
j = j < 0 ? n : j + 3;
siruri.push([i, j]); i = j; prevSig = '"'; continue;
}
if (c === '"' || c === "'") {
let j = i + 1;
while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') j++; j++; }
j = Math.min(n, j + 1);
siruri.push([i, j]); i = j; prevSig = c; continue;
}
if (c === '`' && o.raw) {
let j = text.indexOf('`', i + 1);
j = j < 0 ? n : j + 1;
siruri.push([i, j]); i = j; prevSig = '`'; continue;
}
if (c === '`' && o.template) {
const r = scanTpl(i, i + 1);
i = r.pos;
if (r.deschis) { stiva.push(acolade); acolade++; }
prevSig = '`'; continue;
}
if (c === '/' && o.regex) {
const permis = prevSig === '' || PUNCT_REGEX.includes(prevSig) || (prevSig === 'a' && KW_REGEX.has(prevWord));
if (permis) {
// Cautarea sfarsitului unui literal regex e determinista din (pozitie, "in clasa"), deci o stare din care s-a ajuns o data
// la capatul randului fara sfarsit duce acolo si a doua oara. Starile acestea se tin minte (2026-09-29, revizuirea
// adversariala): fara ele un rand lung cu multe `/` nedeschise (de ex. `=/[` repetat) costa patratic, minute pe 1 MB.
let j = i + 1;
let inClasa = false;
let bun = false;
const vizitate = [];
while (j < n && text[j] !== '\n') {
if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;
vizitate.push(inClasa ? -j - 1 : j);
const ch = text[j];
if (ch === '\\') { j += 2; continue; }
if (ch === '[') inClasa = true;
else if (ch === ']') inClasa = false;
else if (ch === '/' && !inClasa) { bun = true; break; }
j++;
}
if (!bun) {
if (!esecRegex) esecRegex = new Uint8Array(n);
for (const v of vizitate) { if (v < 0) esecRegex[-v - 1] |= 2; else esecRegex[v] |= 1; }
}
if (bun) {
j++;
while (j < n && /[a-z]/i.test(text[j])) j++;
siruri.push([i, j]); i = j; prevSig = '/'; continue;
}
}
prevSig = '/'; i++; continue;
}
if (c === '{') { acolade++; prevSig = '{'; i++; continue; }
if (c === '}') {
acolade--;
if (stiva.length && acolade === stiva[stiva.length - 1]) {
stiva.pop();
const r = scanTpl(i, i + 1);
i = r.pos;
if (r.deschis) { stiva.push(acolade); acolade++; }
prevSig = '`'; continue;
}
prevSig = '}'; i++; continue;
}
if (/[A-Za-z_$]/.test(c)) {
let j = i + 1;
while (j < n && /[\w$]/.test(text[j])) j++;
prevWord = text.slice(i, j); prevSig = 'a'; i = j; continue;
}
if (/[0-9]/.test(c)) {
let j = i + 1;
while (j < n && /[\w.]/.test(text[j])) j++;
prevWord = ''; prevSig = '0'; i = j; continue;
}
if (!/\s/.test(c)) prevSig = c;
i++;
}
return { code: out.join(''), siruri };
}
function curataPython(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
// primul caracter care nu e spatiu pe randul curent (-1 = niciunul inca). Tinut din mers (2026-09-29, revizuirea adversariala):
// forma veche cauta inapoi inceputul randului la FIECARE sir triplu, deci un rand lung cu multe siruri triple costa patratic.
let primulPeRand = -1;
while (i < n) {
const c = text[i];
if (c === '\n') { primulPeRand = -1; i++; continue; }
if (c === '#') {
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '"' || c === "'") {
// prefixul (r, b, f, u, rb, ...) face parte din sir
let start = i;
while (start > 0 && /[rRbBuUfF]/.test(text[start - 1]) && i - start < 2) start--;
if (start < i && start > 0 && /[\w]/.test(text[start - 1])) start = i;
const triplu = text.startsWith(c.repeat(3), i);
let j;
// inainte de sir, pe randul lui, sunt numai spatii (prefixul r/b/f/u face parte din sir)?
const singurPeRand = primulPeRand < 0 || primulPeRand >= start;
let ultimaLinieNoua = -1;
if (triplu) {
j = i + 3;
while (j < n && !text.startsWith(c.repeat(3), j)) {
if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } else if (text[j] === '\n') ultimaLinieNoua = j;
j++;
}
j = Math.min(n, j + 3);
} else {
j = i + 1;
while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } j++; }
if (j < n && text[j] === '\n') ultimaLinieNoua = j; // sir neterminat: intervalul lui se opreste DUPA acest rand nou
j = Math.min(n, j + 1);
}
// dupa sir: daca sirul a trecut peste un rand nou, randul curent incepe in el (coada lui nu e spatiu), afara de cazul in
// care sirul se termina chiar cu randul nou; altfel randul curent are acum cel putin sirul
if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;
else if (primulPeRand < 0) primulPeRand = start;
// docstring: sirul triplu e singur pe randul lui, ca instructiune; se trateaza ca un comentariu
if (triplu && singurPeRand) { goleste(start, j); i = j; continue; }
siruri.push([start, j]); i = j; continue;
}
if (primulPeRand < 0 && c !== ' ' && c !== '\t') primulPeRand = i;
i++;
}
return { code: out.join(''), siruri };
}
// cautare binara: pozitia e inauntrul unui sir?
export function inSir(siruri, pos) {
let lo = 0;
let hi = siruri.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const [s, e] = siruri[mid];
if (pos < s) hi = mid - 1;
else if (pos >= e) lo = mid + 1;
else return true;
}
return false;
}
export function inceputuriDeRand(text) {
const r = [0];
for (let k = 0; k < text.length; k++) if (text.charCodeAt(k) === 10) r.push(k + 1);
return r;
}
export function randul(inceputuri, pos) {
let lo = 0;
let hi = inceputuri.length - 1;
while (lo < hi) {
const mid = (lo + hi + 1) >> 1;
if (inceputuri[mid] <= pos) lo = mid; else hi = mid - 1;
}
return { line: lo + 1, column: pos - inceputuri[lo] + 1 };
}

View File

@ -0,0 +1,134 @@
// Bibliotecile criptografice DECLARATE in manifeste. Declararea nu e folosire: aceste intrari
// ies ca componente "library", nu ca gasiri clasificate.
import { inceputuriDeRand, randul } from './lexer.mjs';
// nume -> ce ofera (text scurt, in engleza, pentru utilizator)
const NPM = {
'node-forge': 'RSA, AES, DES, MD5, SHA-1, SHA-2, X.509 (classical)',
elliptic: 'ECDSA/ECDH/EdDSA on classical curves including secp256k1',
secp256k1: 'secp256k1 ECDSA', 'tiny-secp256k1': 'secp256k1 ECDSA', '@noble/secp256k1': 'secp256k1 ECDSA',
ethers: 'secp256k1 ECDSA accounts and signatures', web3: 'secp256k1 ECDSA accounts and signatures', viem: 'secp256k1 ECDSA accounts and signatures',
'ethereum-cryptography': 'secp256k1, keccak, AES, scrypt',
jsonwebtoken: 'JWS HS*/RS*/PS*/ES*', jose: 'JWS/JWE (classical algorithms)', 'jwt-simple': 'JWS HS*/RS*',
'@noble/curves': 'secp256k1, P-256/384/521, Ed25519/X25519, Ed448, BLS12-381 (classical)',
'@noble/post-quantum': 'ML-KEM, ML-DSA, SLH-DSA (post-quantum)',
'@noble/hashes': 'SHA-2, SHA-3, BLAKE, legacy MD5/SHA-1/RIPEMD-160',
'@noble/ed25519': 'Ed25519',
'crypto-js': 'AES, DES, 3DES, RC4, MD5, SHA-1, SHA-2', tweetnacl: 'Ed25519, X25519, XSalsa20-Poly1305',
'libsodium-wrappers': 'Ed25519, X25519, XChaCha20-Poly1305, Argon2', 'node-rsa': 'RSA', jsrsasign: 'RSA, ECDSA, X.509',
openpgp: 'OpenPGP: RSA, ECC, AES', bcrypt: 'bcrypt password hashing', bcryptjs: 'bcrypt password hashing', argon2: 'Argon2 password hashing',
sshpk: 'SSH keys: RSA, ECDSA, Ed25519', '@peculiar/x509': 'X.509 certificates', pkijs: 'X.509/CMS',
};
const PYPI = {
cryptography: 'RSA, EC, DH, Ed25519/X25519, AES, hashes (pyca/cryptography)',
pycryptodome: 'RSA, DSA, ECC, AES, DES, hashes', pycryptodomex: 'RSA, DSA, ECC, AES, DES, hashes',
pycrypto: 'RSA, DSA, AES, DES (unmaintained since 2013)', ecdsa: 'ECDSA on classical curves', pyjwt: 'JWS HS*/RS*/ES*/PS*/EdDSA',
'python-jose': 'JWS/JWE (classical algorithms)', jwcrypto: 'JWS/JWE (classical algorithms)', pynacl: 'Ed25519, X25519',
pyopenssl: 'TLS and X.509 via OpenSSL', paramiko: 'SSH: RSA, ECDSA, Ed25519, classical key exchange', 'liboqs-python': 'post-quantum KEMs and signatures (liboqs)',
oqs: 'post-quantum KEMs and signatures (liboqs)', web3: 'secp256k1 ECDSA accounts', 'eth-account': 'secp256k1 ECDSA accounts', 'eth-keys': 'secp256k1 ECDSA',
coincurve: 'secp256k1 ECDSA', bcrypt: 'bcrypt password hashing', 'argon2-cffi': 'Argon2 password hashing', passlib: 'password hashing', rsa: 'RSA (pure Python)',
};
const MAVEN_GROUP = {
'org.bouncycastle': 'Bouncy Castle: classical and post-quantum algorithms',
'io.jsonwebtoken': 'JJWT: JWS HS*/RS*/ES*/PS*/EdDSA', 'com.nimbusds': 'Nimbus JOSE+JWT', 'com.auth0': 'java-jwt (if artifact is java-jwt)',
'org.web3j': 'secp256k1 ECDSA accounts (web3j)', 'com.google.crypto.tink': 'Tink: AEAD, signatures, hybrid encryption', 'org.conscrypt': 'Conscrypt TLS provider',
};
const GO = [
[/^golang\.org\/x\/crypto$/, 'extended crypto: ssh, chacha20poly1305, curve25519, bcrypt, argon2'],
[/^github\.com\/cloudflare\/circl$/, 'CIRCL: ML-KEM, ML-DSA, SLH-DSA, hybrid KEMs, classical curves'],
[/^github\.com\/ethereum\/go-ethereum$/, 'secp256k1 ECDSA accounts and signatures'],
[/^github\.com\/golang-jwt\/jwt(\/v\d+)?$/, 'JWS HS*/RS*/ES*/PS*/EdDSA'],
[/^github\.com\/decred\/dcrd\/dcrec\/secp256k1(\/v\d+)?$/, 'secp256k1 ECDSA'],
[/^github\.com\/btcsuite\/btcd\/btcec(\/v\d+)?$/, 'secp256k1 ECDSA'],
[/^filippo\.io\/edwards25519$/, 'Edwards25519 group arithmetic'],
[/^github\.com\/open-quantum-safe\/liboqs-go$/, 'post-quantum KEMs and signatures (liboqs)'],
];
export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod)$/;
function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) {
const { line } = randul(inceputuri, pos);
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 === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
: `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`;
return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl };
}
export function citesteManifest(text, numeFisier, rel) {
const inceputuri = inceputuriDeRand(text);
const r = [];
if (numeFisier === 'package.json') {
let j;
try { j = JSON.parse(text); } catch { return { biblioteci: [], eroare: 'package.json is not valid JSON' }; }
for (const sect of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
const d = j && j[sect];
if (!d || typeof d !== 'object') continue;
const ps = text.indexOf(`"${sect}"`);
for (const [nume, ver] of Object.entries(d)) {
const ofera = NPM[nume] || (/^@ethersproject\//.test(nume) ? 'secp256k1 ECDSA (ethers v5 module)' : null);
if (!ofera) continue;
const pos = text.indexOf(`"${nume}"`, ps < 0 ? 0 : ps);
r.push(lib('npm', nume, String(ver).replace(/^[\^~>=<\s]+/, ''), ofera, pos < 0 ? 0 : pos, inceputuri, rel));
}
}
} else if (/^requirements/.test(numeFisier)) {
let pos = 0;
for (const linie of text.split('\n')) {
const t = linie.replace(/#.*/, '').trim();
const m = /^([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:[=<>!~]=?\s*([\w.*+-]+))?/.exec(t);
if (m) {
const nume = m[1].toLowerCase().replace(/_/g, '-');
if (PYPI[nume]) r.push(lib('pypi', nume, m[2], PYPI[nume], pos + linie.indexOf(m[1]), inceputuri, rel));
}
pos += linie.length + 1;
}
} else if (numeFisier === 'pyproject.toml') {
let pos = 0;
let sectiune = '';
for (const linie of text.split('\n')) {
const h = /^\s*\[([^\]]+)\]/.exec(linie);
if (h) sectiune = h[1];
for (const m of linie.matchAll(/["']([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:[=<>!~]=?\s*([\w.*+-]+))?[^"']*["']/g)) {
const nume = m[1].toLowerCase().replace(/_/g, '-');
if (PYPI[nume]) r.push(lib('pypi', nume, m[2], PYPI[nume], pos + m.index + 1, inceputuri, rel));
}
const p = /^\s*([A-Za-z0-9_.-]+)\s*=\s*(?:["']([^"']*)["']|\{)/.exec(linie);
if (p && /dependencies/.test(sectiune)) {
const nume = p[1].toLowerCase().replace(/_/g, '-');
if (PYPI[nume]) r.push(lib('pypi', nume, p[2] ? p[2].replace(/^[\^~>=<\s]+/, '') : undefined, PYPI[nume], pos + linie.indexOf(p[1]), inceputuri, rel));
}
pos += linie.length + 1;
}
} else if (numeFisier === 'pom.xml') {
for (const m of text.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
const g = /<groupId>\s*([^<\s]+)\s*<\/groupId>/.exec(m[1]);
const a = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(m[1]);
const v = /<version>\s*([^<\s]+)\s*<\/version>/.exec(m[1]);
if (!g || !a) continue;
const ofera = MAVEN_GROUP[g[1]];
if (!ofera) continue;
r.push(lib('maven', `${g[1]}:${a[1]}`, v && !/\$\{/.test(v[1]) ? v[1] : undefined, ofera, m.index + m[0].indexOf(a[0]), inceputuri, rel));
}
} else if (/^build\.gradle/.test(numeFisier)) {
for (const m of text.matchAll(/["']([\w.-]+):([\w.-]+)(?::([\w.-]+))?["']/g)) {
const ofera = MAVEN_GROUP[m[1]];
if (ofera) r.push(lib('maven', `${m[1]}:${m[2]}`, m[3], ofera, m.index + 1, inceputuri, rel));
}
} else if (numeFisier === 'go.mod') {
let pos = 0;
let inRequire = false;
for (const linie of text.split('\n')) {
const t = linie.replace(/\/\/.*/, '').trim();
if (/^require\s*\($/.test(t)) inRequire = true;
else if (inRequire && t === ')') inRequire = false;
const m = inRequire ? /^([\w.\-/]+)\s+(v[\w.\-+]+)/.exec(t) : /^require\s+([\w.\-/]+)\s+(v[\w.\-+]+)/.exec(t);
if (m) {
const intrare = GO.find(([re]) => re.test(m[1]));
if (intrare) r.push(lib('golang', m[1], m[2], intrare[1], pos + linie.indexOf(m[1]), inceputuri, rel));
}
pos += linie.length + 1;
}
}
return { biblioteci: r };
}

View File

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

View File

@ -0,0 +1,76 @@
// Rezumatul text (engleza, pentru utilizator) si regula --fail-on.
import { CLS } from './catalog.mjs';
const ORDINE = [CLS.V, CLS.W, CLS.S, CLS.U];
export function numarPeClase(gasiri) {
const r = Object.fromEntries(ORDINE.map((c) => [c, 0]));
for (const g of gasiri) r[g.classification] = (r[g.classification] || 0) + 1;
return r;
}
// --fail-on: lista separata prin virgula din vulnerable, weak, unknown, any
// "vulnerable" = orice gasire pe care un calculator cuantic o sparge (inclusiv cele weak-now care sunt si Shor)
export function verificaFailOn(gasiri, spec) {
if (!spec) return { esec: false, motive: [] };
const cer = new Set(String(spec).split(',').map((x) => x.trim()).filter(Boolean));
const necunoscute = [...cer].filter((x) => !['vulnerable', 'weak', 'unknown', 'any'].includes(x));
if (necunoscute.length) throw new Error(`--fail-on: unknown value(s): ${necunoscute.join(', ')} (use vulnerable, weak, unknown or any)`);
const motive = [];
const vuln = gasiri.filter((g) => g.classification === CLS.V || (g.classification === CLS.W && g.quantumVulnerable));
const slabe = gasiri.filter((g) => g.classification === CLS.W);
const nec = gasiri.filter((g) => g.classification === CLS.U);
if ((cer.has('vulnerable') || cer.has('any')) && vuln.length) motive.push(`${vuln.length} quantum-vulnerable finding(s)`);
if ((cer.has('weak') || cer.has('any')) && slabe.length) motive.push(`${slabe.length} weak-now finding(s)`);
if (cer.has('unknown') && nec.length) motive.push(`${nec.length} unknown finding(s)`);
return { esec: motive.length > 0, motive };
}
export function rezumatText(rez) {
const s = rez.stats;
const g = rez.findings;
const L = [];
L.push(`Aere crypto inventory ${rez.tool.version}: ${rez.rootName}`);
const limbaje = Object.entries(s.codeFiles).sort().map(([k, v]) => `${k} ${v}`).join(', ') || 'none';
L.push(`Files: ${s.filesSeen} seen | ${s.codeFilesTotal} analyzed as code (${limbaje}) | ${s.textFilesPemOnly} other text files checked for PEM key/certificate blocks only`);
const nr = s.notRead;
L.push(`Not read: ${nr.binary} binary, ${nr.tooLarge} too large (> ${rez.options.maxFileBytes} bytes), ${nr.fileLimit} over the file limit (${rez.options.maxFiles}), ${nr.unreadable} unreadable; ${s.symlinksNotFollowed} symlinks not followed`);
for (const k of ['binary', 'tooLarge', 'fileLimit', 'unreadable']) {
if (s.notReadExamples[k].length) L.push(` e.g. ${k}: ${s.notReadExamples[k].join(', ')}${nr[k] > s.notReadExamples[k].length ? ', ...' : ''}`);
}
if (s.resolutionLimitFiles) L.push(`Variable resolution limit (${rez.options.maxResolvedVariablesPerFile} distinct variables per file) reached in ${s.resolutionLimitFiles} file(s); further variables there are reported as unknown: ${s.resolutionLimitExamples.join(', ')}${s.resolutionLimitFiles > s.resolutionLimitExamples.length ? ', ...' : ''}`);
const ex = Object.entries(s.dirsExcluded).sort().map(([n, c]) => `${n} (${c})`).join(', ');
L.push(`Directories not descended: ${ex || 'none'}${s.dirsUnreadable ? `; ${s.dirsUnreadable} unreadable` : ''}`);
const pk = {};
for (const x of g) pk[x.evidenceKind] = (pk[x.evidenceKind] || 0) + 1;
L.push(`Findings: ${g.length} (${Object.entries(pk).sort().map(([k, v]) => `${k} ${v}`).join(', ') || 'none'})`);
const cl = numarPeClase(g);
const wq = g.filter((x) => x.classification === CLS.W && x.quantumVulnerable).length;
for (const c of ORDINE) L.push(` ${c.padEnd(19)} ${String(cl[c]).padStart(5)}${c === CLS.W && wq ? ` (${wq} also quantum-vulnerable)` : ''}`);
const pl = {};
for (const x of g) pl[x.language] = (pl[x.language] || 0) + 1;
L.push(`By language: ${Object.entries(pl).sort().map(([k, v]) => `${k} ${v}`).join(', ') || 'none'}`);
const frecv = {};
for (const x of g) { const k = `${x.name} [${x.classification}]`; frecv[k] = (frecv[k] || 0) + 1; }
const top = Object.entries(frecv).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 10);
if (top.length) {
L.push('Most frequent:');
top.forEach(([k, v], i) => L.push(` ${String(i + 1).padStart(2)}. ${k} x${v}`));
}
const vf = new Map();
for (const x of g) {
if (!(x.classification === CLS.V || (x.classification === CLS.W && x.quantumVulnerable))) continue;
if (!vf.has(x.file)) vf.set(x.file, []);
vf.get(x.file).push(x);
}
L.push(`Files with quantum-vulnerable findings: ${vf.size}`);
for (const [f, lista] of [...vf].sort()) {
L.push(` ${f} (${lista.length}: ${[...new Set(lista.map((x) => x.name))].join(', ')})`);
}
L.push(`Declared crypto libraries (manifests; declaration is not use): ${rez.libraries.length}`);
for (const b of rez.libraries) L.push(` ${b.file}:${b.line} ${b.name}${b.version ? ' ' + b.version : ''} (${b.provides})`);
if (s.manifestErrors.length) L.push(`Manifest errors: ${s.manifestErrors.join('; ')}`);
L.push('Scope: static pattern analysis of source text. Not covered: compiled binaries, transitive dependencies, cryptography chosen at run time.');
// numele de fisiere si textele vin din arborele scanat: caracterele de control (secvente ANSI) nu ajung in terminal
return L.map((l) => l.replace(/[\u0000-\u001f\u007f-\u009f]/g, '?')).join('\n');
}

View File

@ -0,0 +1,141 @@
// Parcurgerea dosarului si orchestrarea detectorilor. Nimic nu se sare in tacere: fiecare
// fisier vazut ajunge intr-o singura categorie numarata, si suma se verifica la sfarsit.
import { readdirSync, statSync, readFileSync, lstatSync } from 'node:fs';
import { join, relative, sep, basename, resolve, extname } from 'node:path';
import { facContext, aplicaSuprimarea, REZOLVARI_PE_FISIER } from './context.mjs';
import { detecteazaJs } from './detect-js.mjs';
import { detecteazaPy } from './detect-py.mjs';
import { detecteazaJava } from './detect-java.mjs';
import { detecteazaGo } from './detect-go.mjs';
import { detecteazaPem } from './pem.mjs';
import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs';
export const VERSIUNE = '0.2.0';
export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache'];
const LIMBAJ = {
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
'.ts': 'javascript', '.tsx': 'javascript', '.mts': 'javascript', '.cts': 'javascript',
'.py': 'python', '.pyw': 'python',
'.java': 'java',
'.go': 'go',
};
const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo };
const EXEMPLE = 5;
export function limbajul(cale) {
return LIMBAJ[extname(cale).toLowerCase()] || null;
}
// binar = contine un octet NUL in primii 8192 octeti
export function eBinar(buf) {
const n = Math.min(buf.length, 8192);
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
return false;
}
export function scaneaza(radacina, optiuni = {}) {
const o = {
maxFileBytes: optiuni.maxFileBytes ?? 1048576,
maxFiles: optiuni.maxFiles ?? 50000,
excludeDirs: new Set([...(optiuni.noDefaultExcludes ? [] : DOSARE_EXCLUSE_IMPLICIT), ...(optiuni.excludeDirs || [])]),
};
const root = resolve(radacina);
const st = statSync(root);
if (!st.isDirectory()) throw new Error(`not a directory: ${radacina}`);
const inceput = new Date();
const stats = {
filesSeen: 0,
codeFiles: {},
textFilesPemOnly: 0,
manifests: 0,
manifestErrors: [],
notRead: { binary: 0, tooLarge: 0, fileLimit: 0, unreadable: 0 },
notReadExamples: { binary: [], tooLarge: [], fileLimit: [], unreadable: [] },
symlinksNotFollowed: 0,
resolutionLimitFiles: 0,
resolutionLimitExamples: [],
dirsExcluded: {},
dirsUnreadable: 0,
};
const gasiri = [];
const biblioteci = [];
let cititeSauIncercate = 0;
const noteaza = (motiv, rel, extra) => {
stats.notRead[motiv]++;
if (stats.notReadExamples[motiv].length < EXEMPLE) stats.notReadExamples[motiv].push(extra ? `${rel} (${extra})` : rel);
};
const stiva = [root];
while (stiva.length) {
const dir = stiva.pop();
let intrari;
try { intrari = readdirSync(dir, { withFileTypes: true }); } catch { stats.dirsUnreadable++; continue; }
intrari.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
const subdosare = [];
for (const e of intrari) {
const abs = join(dir, e.name);
if (e.isSymbolicLink()) { stats.symlinksNotFollowed++; continue; }
if (e.isDirectory()) {
if (o.excludeDirs.has(e.name)) { stats.dirsExcluded[e.name] = (stats.dirsExcluded[e.name] || 0) + 1; continue; }
subdosare.push(abs);
continue;
}
if (!e.isFile()) continue;
stats.filesSeen++;
const rel = relative(root, abs).split(sep).join('/');
if (cititeSauIncercate >= o.maxFiles) { noteaza('fileLimit', rel); continue; }
cititeSauIncercate++;
let marime;
try { marime = lstatSync(abs).size; } catch (err) { noteaza('unreadable', rel, err.code); continue; }
if (marime > o.maxFileBytes) { noteaza('tooLarge', rel, `${marime} bytes`); continue; }
let buf;
try { buf = readFileSync(abs); } catch (err) { noteaza('unreadable', rel, err.code); continue; }
if (eBinar(buf)) { noteaza('binary', rel); continue; }
const text = buf.toString('utf8');
const limbaj = limbajul(abs);
const ctx = facContext(text, limbaj || 'text', rel);
if (limbaj) {
stats.codeFiles[limbaj] = (stats.codeFiles[limbaj] || 0) + 1;
DETECTOR[limbaj](ctx);
if (ctx.rezolvariPestePlafon) {
stats.resolutionLimitFiles++;
if (stats.resolutionLimitExamples.length < EXEMPLE) stats.resolutionLimitExamples.push(rel);
}
} else {
stats.textFilesPemOnly++;
}
detecteazaPem(text, ctx);
gasiri.push(...aplicaSuprimarea(ctx.gasiri));
if (NUME_MANIFEST.test(basename(abs))) {
stats.manifests++;
const r = citesteManifest(text, basename(abs), rel);
if (r.eroare) stats.manifestErrors.push(`${rel}: ${r.eroare}`);
biblioteci.push(...r.biblioteci);
}
}
for (let i = subdosare.length - 1; i >= 0; i--) stiva.push(subdosare[i]);
}
// garda: fiecare fisier vazut e intr-o singura categorie
const cod = Object.values(stats.codeFiles).reduce((a, b) => a + b, 0);
const nr = stats.notRead;
const suma = cod + stats.textFilesPemOnly + nr.binary + nr.tooLarge + nr.fileLimit + nr.unreadable;
if (suma !== stats.filesSeen) throw new Error(`internal accounting error: ${stats.filesSeen} files seen, ${suma} accounted for`);
stats.codeFilesTotal = cod;
gasiri.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.column - b.column));
biblioteci.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
return {
tool: { name: 'aere-crypto-inventory', version: VERSIUNE },
root,
rootName: basename(root),
options: { maxFileBytes: o.maxFileBytes, maxFiles: o.maxFiles, excludeDirs: [...o.excludeDirs].sort(), maxResolvedVariablesPerFile: REZOLVARI_PE_FISIER },
startedAt: inceput.toISOString(),
finishedAt: new Date().toISOString(),
findings: gasiri,
libraries: biblioteci,
stats,
};
}

View File

@ -0,0 +1,80 @@
// Controlul negativ al probei de cost (test/proba-timp.mjs): proba trebuie sa iasa ROSIE pe cazul numit cand forma patratica e
// pusa inapoi. Trei stari pe caz: PRINS (proba a masurat si cazul tinta e ROSU), SCAPAT (a ramas VERDE), STRICAT (copia nu s-a
// putut face, ancora nu apare exact o data, sau proba nu a masurat).
// V0 inventarul de dinainte de reparatie, din git (sarit si spus daca revizia nu e in istoricul depozitului)
// -> rosii: pem, py-triplu, js-regex, js-linie, java-linie, java-kpg
// P1 rezolvarea variabilelor fara memorie (fiecare apel reciteste fisierul) -> js-linie rosu
// P2 esecurile literalului regex JS netinute minte -> js-regex rosu
// P3 cautarea PEM fara pozitiile tinute minte (sfarsitul cautat de la fiecare antet) -> pem rosu
// node test/control-negativ-timp.mjs -> 0 toate prinse, 1 unul scapat, 2 STRICAT
import { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
const AICI = dirname(fileURLToPath(import.meta.url));
const RADACINA = join(AICI, '..');
const PROBA = join(AICI, 'proba-timp.mjs');
const REV = process.env.AERE_INV_REV_VECHE || 'db9d9543';
let prinse = 0, stricate = 0, total = 0;
const copiaza = () => {
const d = mkdtempSync(join(tmpdir(), 'ci-timp-control-'));
cpSync(join(RADACINA, 'inventar.mjs'), join(d, 'inventar.mjs'));
cpSync(join(RADACINA, 'lib'), join(d, 'lib'), { recursive: true });
return d;
};
function ruleaza(id, d, tinte, doar) {
const r = spawnSync(process.execPath, [PROBA, ...(doar ? ['--doar', doar.join(',')] : [])], { encoding: 'utf8', timeout: 600000, env: { ...process.env, CRYPTO_INVENTORY_MODULE: join(d, 'inventar.mjs') } });
const out = r.stdout || '';
if (!out.includes(`(modul: ${join(d, 'inventar.mjs')})`) || /^STRICAT/m.test(out)) { stricate++; console.log(` STRICAT ${id}: proba nu a masurat copia (cod ${r.status}) ${out.split('\n').filter((l) => /^STRICAT/.test(l)).join(' | ').slice(0, 160)}`); return; }
const rosii = out.split('\n').filter((l) => /^ROSU/.test(l)).map((l) => l.split(/\s+/)[1].replace(/:$/, ''));
const lipsa = tinte.filter((t) => !rosii.includes(t));
if (r.status === 1 && !lipsa.length) { prinse++; console.log(` PRINS ${id}: rosii ${rosii.join(', ')}`); }
else console.log(` SCAPAT ${id}: cod ${r.status}, au ramas verzi: ${lipsa.join(', ')}`);
}
// V0: codul de dinainte, din git, in forma comisa (fara conversia capetelor de rand)
total++;
const git = (...a) => spawnSync('git', ['-c', 'core.autocrlf=false', ...a], { cwd: RADACINA, encoding: 'buffer', maxBuffer: 64 << 20 });
const ls = git('ls-tree', '-r', '--name-only', '--full-tree', REV, 'tools/crypto-inventory/inventar.mjs', 'tools/crypto-inventory/lib');
const fisiere = ls.status === 0 ? ls.stdout.toString().split('\n').filter(Boolean) : [];
if (!fisiere.length) { total--; console.log(` SARIT V0: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); }
else {
const d = mkdtempSync(join(tmpdir(), 'ci-timp-v0-'));
try {
let ok = true;
for (const f of fisiere) {
const b = git('show', `${REV}:${f}`);
if (b.status !== 0) { ok = false; break; }
const t = join(d, f.slice('tools/crypto-inventory/'.length));
mkdirSync(dirname(t), { recursive: true });
writeFileSync(t, b.stdout);
}
if (!ok || readFileSync(join(d, 'lib', 'pem.mjs'), 'utf8').includes('blocuriPem')) { stricate++; console.log(' STRICAT V0: revizia veche nu se citeste sau are deja reparatia'); }
else ruleaza('V0', d, ['pem.txt', 'py-triplu.py', 'js-regex.js', 'js-linie.js', 'java-linie.java', 'java-kpg.java']);
} finally { rmSync(d, { recursive: true, force: true }); }
}
const P = [
['P1', 'lib/context.mjs', 'if (ctx.rezolvari.has(nume)) return ctx.rezolvari.get(nume);', "if (ctx.rezolvari.has(nume) && process.env.AERE_PLANTA_NICIODATA === 'da') return ctx.rezolvari.get(nume);", ['js-linie.js']],
['P2', 'lib/lexer.mjs', 'if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;', "if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1) && process.env.AERE_PLANTA_NICIODATA === 'da') break;", ['js-regex.js']],
['P3', 'lib/pem.mjs', 'const valabil = (c, de) => c && c.la <= de && (c.poz < 0 || c.poz >= de);', "const valabil = (c, de) => process.env.AERE_PLANTA_NICIODATA === 'da' && c && c.la <= de && (c.poz < 0 || c.poz >= de);", ['pem.txt']],
];
for (const [id, fisier, ancora, cu, tinte] of P) {
total++;
const d = copiaza();
try {
const cale = join(d, fisier);
const text = readFileSync(cale, 'utf8');
if (text.split(ancora).length !== 2) { stricate++; console.log(` STRICAT ${id}: ancora nu apare exact o data in ${fisier}`); continue; }
writeFileSync(cale, text.replace(ancora, cu));
const dif = ['inventar.mjs', ...readdirSync(join(d, 'lib')).map((f) => `lib/${f}`)].filter((f) => readFileSync(join(d, f), 'utf8') !== readFileSync(join(RADACINA, f), 'utf8'));
if (dif.length !== 1 || dif[0] !== fisier) { stricate++; console.log(` STRICAT ${id}: copia difera in ${dif.join(', ') || 'nimic'}`); continue; }
ruleaza(id, d, tinte, tinte);
} finally { rmSync(d, { recursive: true, force: true }); }
}
console.log(`\ncontrolul negativ al probei de cost: prinse ${prinse}/${total}, stricate ${stricate}`);
process.exitCode = stricate ? 2 : prinse === total ? 0 : 1;

View File

@ -0,0 +1,242 @@
// Controlul negativ al suitei: strica, intr-o COPIE a modulului, cate o regula sau un detector si
// cere ca proba tinta sa iasa ROSIE pe numele ei, ca suita sa fi rulat INTREAGA (aceleasi nume ca
// linia de baza) si ca martorul neatins sa ramana VERDE. Altfel verdictul e STRICAT, nu "prins".
// Un ciot care nu se poate pune (ancora lipsa sau dubla) e tot STRICAT: un control care nu a
// plantat nimic nu a masurat nimic.
// Folosire: node test/control-negativ.mjs (cod 0 doar daca toate mutatiile sunt PRINSE)
import { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
const AICI = dirname(fileURLToPath(import.meta.url));
const RADACINA = join(AICI, '..');
const SUITA = join(AICI, 'proba.mjs');
const B = 'negativ: fisierul binar e sarit si numarat';
const MUTATII = [
{
nume: 'secp256k1 clasificat quantum-safe',
fisier: 'lib/catalog.mjs',
ancora: "return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });",
cu: "return f({ ...baza, ...rez({ c: e256k1 ? CLS.S : CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });",
tinta: 'clasificare: secp256k1 este quantum-vulnerable, cu recomandare de cont post-cuantic',
martor: B,
},
{
nume: 'comentariile // nu mai sunt ignorate (JS/Java/Go)',
fisier: 'lib/lexer.mjs',
ancora: "if (c === '/' && d === '/') {",
cu: "if (false && c === '/' && d === '/') {",
tinta: 'negativ: comentariile JS nu produc gasiri',
martor: B,
},
{
nume: 'linia raportata cu unu mai mult',
fisier: 'lib/lexer.mjs',
ancora: 'return { line: lo + 1, column: pos - inceputuri[lo] + 1 };',
cu: 'return { line: lo + 2, column: pos - inceputuri[lo] + 1 };',
tinta: 'pozitiv: js/node-crypto.mjs',
martor: 'negativ: fisier fara criptografie da zero gasiri, si a fost citit',
},
{
nume: 'variabila reasignata e "rezolvata" la prima valoare (ghicit)',
fisier: 'lib/context.mjs',
ancora: 'if (atribuiri.length !== 1) return null;',
cu: 'if (atribuiri.length < 1) return null;',
tinta: 'negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit',
martor: B,
},
{
nume: 'fisierele binare nu mai sunt sarite',
fisier: 'lib/scan.mjs',
ancora: "if (eBinar(buf)) { noteaza('binary', rel); continue; }",
cu: "if (false && eBinar(buf)) { noteaza('binary', rel); continue; }",
tinta: B,
martor: 'negativ: comentariile JS nu produc gasiri',
},
{
nume: 'comentariile # nu mai sunt ignorate (Python)',
fisier: 'lib/lexer.mjs',
ancora: " if (c === '#') {",
cu: " if (false && c === '#') {",
tinta: 'negativ: comentariile si docstring-urile Python nu produc gasiri',
martor: 'pozitiv: js/node-crypto.mjs',
},
// 2026-09-29, revizuirea adversariala: formele liniare trebuie sa dea exact ce dadeau cele vechi
{
nume: 'cautarea PEM nu mai reia de la caracterul urmator dupa un antet fara sfarsit',
fisier: 'lib/pem.mjs',
ancora: '{ reBegin.lastIndex = m.index + 1; continue; }',
cu: "{ if (process.env.AERE_PLANTA_NICIODATA === 'da') reBegin.lastIndex = m.index + 1; continue; }",
tinta: 'limite: un bloc PEM care incepe in liniutele unui antet fara sfarsit e gasit',
martor: B,
},
{
nume: 'esecul unui literal regex JS tinut minte fara starea "in clasa"',
fisier: 'lib/lexer.mjs',
ancora: 'if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;',
cu: "if (esecRegex && (esecRegex[j] & (inClasa ? 2 : 1) || (process.env.AERE_PLANTA_NICIODATA !== 'da' && esecRegex[j]))) break;",
tinta: 'limite: literalul regex JS se cauta din starea lui, nu din ce a esuat alt literal pe acelasi rand',
martor: B,
},
{
nume: 'sirul neterminat Python nu mai muta inceputul randului',
fisier: 'lib/lexer.mjs',
ancora: 'if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;',
cu: "if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 && process.env.AERE_PLANTA_NICIODATA === 'da' ? -1 : ultimaLinieNoua + 1;",
tinta: 'limite: docstring Python pe randul lui (si dupa un sir neterminat), dar nu dupa cod pe acelasi rand',
martor: B,
},
{
nume: 'plafonul de variabile pe fisier scos',
fisier: 'lib/context.mjs',
ancora: 'if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER) {',
cu: "if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER && process.env.AERE_PLANTA_NICIODATA === 'da') {",
tinta: 'limite: peste plafonul de variabile pe fisier numele ies unknown si fisierul se numara',
martor: B,
},
{
nume: 'caracterele de control din rezumat nu mai sunt inlocuite',
fisier: 'lib/rezumat.mjs',
ancora: "return L.map((l) => l.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '?')).join('\\n');",
cu: "return L.map((l) => (process.env.AERE_PLANTA_NICIODATA === 'da' ? l.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '?') : l)).join('\\n');",
tinta: 'rezumat: caracterele de control din numele de fisiere nu ajung in terminal',
martor: B,
},
{
nume: 'MD5 clasificat quantum-safe',
fisier: 'lib/catalog.mjs',
ancora: "MD5: { n: 'MD5', c: CLS.W, oid: '1.2.840.113549.2.5' },",
cu: "MD5: { n: 'MD5', c: CLS.S, oid: '1.2.840.113549.2.5' },",
tinta: 'clasificare: MD5 si SHA-1 sunt weak-now, SHA-256 quantum-safe',
martor: 'negativ: fisier prea mare sarit si numarat',
},
{
nume: 'bom-ref fara amprenta (coliziuni)',
fisier: 'lib/cbom.mjs',
ancora: ":${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;\n const cp",
cu: '`;\n const cp',
tinta: 'cbom: bom-ref unice si stabile intre doua rulari',
martor: B,
},
{
nume: '--fail-on vulnerable ignorat',
fisier: 'lib/rezumat.mjs',
ancora: "if ((cer.has('vulnerable') || cer.has('any')) && vuln.length)",
cu: "if (false && (cer.has('vulnerable') || cer.has('any')) && vuln.length)",
tinta: 'cli: --fail-on vulnerable iese 1 pe cod vulnerabil si 0 pe cod fara criptografie',
martor: 'cbom: structura CycloneDX 1.6 (campuri obligatorii, tipuri, enumerari)',
},
{
nume: 'apelurile din siruri numarate ca folosire',
fisier: 'lib/context.mjs',
ancora: 'for (const m of code.matchAll(g)) if (!inSir(siruri, m.index)) r.push(m);',
cu: 'for (const m of code.matchAll(g)) r.push(m);',
tinta: 'negativ: apelul scris intr-un sir JS nu e folosire',
martor: B,
},
{
nume: 'fisierul prea mare sarit fara sa fie numarat',
fisier: 'lib/scan.mjs',
ancora: "if (marime > o.maxFileBytes) { noteaza('tooLarge', rel, `${marime} bytes`); continue; }",
cu: 'if (marime > o.maxFileBytes) { continue; }',
tinta: 'negativ: fisier prea mare sarit si numarat',
martor: 'pozitiv: js/node-crypto.mjs',
},
{
nume: 'suprimarea importurilor acoperite de apeluri scoasa (numarare dubla)',
fisier: 'lib/context.mjs',
ancora: " if (g.evidenceKind !== 'import' || !g.suppressedBy) return true;",
cu: ' return true;',
tinta: 'pozitiv: go/crypto.go',
martor: B,
},
{
nume: 'blocul PEM intreg (cu cheia privata) pus in contextul gasirii',
fisier: 'lib/pem.mjs',
ancora: '{ bucata: `-----BEGIN ${tip}-----` }',
cu: '{ bucata: m[0] }',
tinta: 'pozitiv: cheie privata generata la rulare e raportata, iar valoarea ei nu apare in iesire',
martor: B,
},
];
function copiaza() {
const d = mkdtempSync(join(tmpdir(), 'ci-control-'));
cpSync(join(RADACINA, 'inventar.mjs'), join(d, 'inventar.mjs'));
cpSync(join(RADACINA, 'lib'), join(d, 'lib'), { recursive: true });
return d;
}
function fisiereModul(d) {
return ['inventar.mjs', ...readdirSync(join(d, 'lib')).map((f) => `lib/${f}`)].filter((f) => statSync(join(d, f)).isFile()).sort();
}
function ruleaza(d) {
const r = spawnSync(process.execPath, [SUITA, '--json'], {
encoding: 'utf8',
timeout: 300000,
env: { ...process.env, CRYPTO_INVENTORY_MODULE: join(d, 'inventar.mjs') },
});
const ultima = (r.stdout || '').trim().split('\n').pop() || '';
try {
const j = JSON.parse(ultima);
return { ok: true, j, status: r.status };
} catch {
return { ok: false, motiv: `suita nu a raportat JSON (status ${r.status}): ${(r.stderr || '').trim().split('\n').slice(0, 3).join(' | ')}` };
}
}
// linia de baza: o copie neatinsa, toata verde, si suita chiar a masurat COPIA
const baza = copiaza();
let linieBaza;
try {
const r = ruleaza(baza);
if (!r.ok) { console.log(`STRICAT linia de baza: ${r.motiv}`); process.exitCode = 3; }
else if (r.j.modul !== join(baza, 'inventar.mjs')) { console.log(`STRICAT linia de baza: suita a masurat ${r.j.modul}, nu copia`); process.exitCode = 3; }
else if (r.j.verzi !== r.j.rulate) { console.log(`STRICAT linia de baza: ${r.j.rosii} probe rosii pe copia neatinsa: ${Object.keys(r.j.erori).join('; ')}`); process.exitCode = 3; }
else linieBaza = r.j;
} finally { rmSync(baza, { recursive: true, force: true }); }
if (linieBaza) {
const numeBaza = Object.keys(linieBaza.rezultate).sort();
console.log(`linia de baza: ${linieBaza.rulate} probe, toate verzi, pe o copie neatinsa a modulului\n`);
const verdicte = [];
for (const mu of MUTATII) {
const d = copiaza();
let verdict;
let detaliu = '';
try {
const cale = join(d, mu.fisier);
const text = readFileSync(cale, 'utf8');
const n = text.split(mu.ancora).length - 1;
if (!numeBaza.includes(mu.tinta) || !numeBaza.includes(mu.martor)) {
verdict = 'STRICAT'; detaliu = 'tinta sau martorul nu exista in suita';
} else if (n !== 1) {
verdict = 'STRICAT'; detaliu = `ciotul nu s-a putut pune: ancora apare de ${n} ori`;
} else {
writeFileSync(cale, text.replace(mu.ancora, mu.cu));
// cmp: exact un fisier difera de original, si acela e cel mutat
const diferite = fisiereModul(d).filter((f) => readFileSync(join(d, f), 'utf8') !== readFileSync(join(RADACINA, f), 'utf8'));
if (diferite.length !== 1 || diferite[0] !== mu.fisier) {
verdict = 'STRICAT'; detaliu = `copia difera in: ${diferite.join(', ') || 'nimic'}`;
} else {
const r = ruleaza(d);
if (!r.ok) { verdict = 'STRICAT'; detaliu = r.motiv; }
else if (r.j.modul !== join(d, 'inventar.mjs')) { verdict = 'STRICAT'; detaliu = `suita a masurat ${r.j.modul}`; }
else if (JSON.stringify(Object.keys(r.j.rezultate).sort()) !== JSON.stringify(numeBaza)) { verdict = 'STRICAT'; detaliu = `suita nu a rulat intreaga: ${r.j.rulate} din ${linieBaza.rulate}`; }
else if (r.j.rezultate[mu.tinta] !== 'ROSU') { verdict = 'NEPRINS'; detaliu = 'proba tinta a ramas verde'; }
else if (r.j.rezultate[mu.martor] !== 'VERDE') { verdict = 'STRICAT'; detaliu = `martorul a iesit rosu: ${r.j.erori[mu.martor]}`; }
else { verdict = 'PRINS'; detaliu = `tinta rosie (${r.j.erori[mu.tinta].slice(0, 110)}); ${r.j.rosii} din ${r.j.rulate} rosii in total; martor verde`; }
}
}
} finally { rmSync(d, { recursive: true, force: true }); }
verdicte.push(verdict);
console.log(`${verdict.padEnd(8)} ${mu.nume}\n tinta: ${mu.tinta}\n ${detaliu}`);
}
const prinse = verdicte.filter((v) => v === 'PRINS').length;
console.log(`\n${prinse} din ${MUTATII.length} mutatii PRINSE; ${verdicte.filter((v) => v === 'NEPRINS').length} neprinse; ${verdicte.filter((v) => v === 'STRICAT').length} stricate`);
process.exitCode = prinse === MUTATII.length ? 0 : 1;
}

View File

@ -0,0 +1,87 @@
// Echivalenta formelor liniare din 0.2.0 cu formele din 0.1.0 (2026-09-29, revizuirea adversariala): aceleasi gasiri si biblioteci
// pe arbori reali, si aceleasi rezultate ale lexerului si ale cautarii PEM pe texte generate din jetoanele care conteaza.
// 0.1.0 se ia din git (revizia AERE_INV_REV_VECHE, implicit db9d9543), in forma comisa; intr-o copie fara istoric iese SARIT.
// node test/echivalenta-0.1.0.mjs [--iteratii N] [dosar...] (implicit: fixturile si dosarul uneltei)
// -> 0 identic, 1 o diferenta (tiparita), 2 NEMASURAT (revizia veche lipseste sau nimic comparat)
// Singura diferenta asteptata e textul motivului pentru "unknown" (spune acum si plafonul de variabile); se normalizeaza.
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
const AICI = dirname(fileURLToPath(import.meta.url));
const RAD = join(AICI, '..');
const REV = process.env.AERE_INV_REV_VECHE || 'db9d9543';
let ITER = 20000;
const dosare = [];
for (let k = 2; k < process.argv.length; k++) {
if (process.argv[k] === '--iteratii') ITER = Number(process.argv[++k]);
else dosare.push(process.argv[k]);
}
const DOSARE = dosare.length ? dosare : [join(AICI, 'fixturi'), RAD];
const git = (...a) => spawnSync('git', ['-c', 'core.autocrlf=false', ...a], { cwd: RAD, encoding: 'buffer', maxBuffer: 64 << 20 });
const ls = git('ls-tree', '-r', '--name-only', '--full-tree', REV, 'tools/crypto-inventory/inventar.mjs', 'tools/crypto-inventory/lib');
const fis = ls.status === 0 ? ls.stdout.toString().split('\n').filter(Boolean) : [];
if (!fis.length) { console.log(`SARIT: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); process.exit(2); }
const vechi = mkdtempSync(join(tmpdir(), 'ci-v010-'));
let dif = 0, comparate = 0;
try {
for (const f of fis) {
const b = git('show', `${REV}:${f}`);
if (b.status !== 0) { console.log(`NEMASURAT: git show ${f}`); process.exit(2); }
const t = join(vechi, f.slice('tools/crypto-inventory/'.length));
mkdirSync(dirname(t), { recursive: true });
writeFileSync(t, b.stdout);
}
const imp = (r, m) => import(pathToFileURL(join(r, 'lib', m)).href);
const [SV, SN, LV, LN, PN] = await Promise.all([imp(vechi, 'scan.mjs'), imp(RAD, 'scan.mjs'), imp(vechi, 'lexer.mjs'), imp(RAD, 'lexer.mjs'), imp(RAD, 'pem.mjs')]);
if (SV.VERSIUNE !== '0.1.0') { console.log(`NEMASURAT: revizia ${REV} are versiunea ${SV.VERSIUNE}, nu 0.1.0`); process.exit(2); }
// 1. arbori reali
const norm = (g) => JSON.stringify({ ...g, reason: String(g.reason).replace(/, which (cannot be resolved statically|this tool does not resolve statically).*$/, ', which [motiv]') });
for (const d of DOSARE) {
const a = SV.scaneaza(d), b = SN.scaneaza(d);
const ka = a.findings.map(norm), kb = b.findings.map(norm);
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 ok = !doarV.length && !doarN.length && ka.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries);
comparate += ka.length;
if (!ok) dif++;
console.log(`${ok ? 'IDENTIC ' : 'DIFERIT '} ${d}: ${a.stats.filesSeen} fisiere, gasiri ${ka.length}/${kb.length}, biblioteci ${a.libraries.length}/${b.libraries.length}`);
for (const x of [...doarV.slice(0, 3).map((x) => 'numai 0.1.0: ' + x), ...doarN.slice(0, 3).map((x) => 'numai 0.2.0: ' + x)]) console.log(' ' + x.slice(0, 260));
}
// 2. texte generate: lexerul pe patru limbaje si cautarea PEM (fata de regexul din 0.1.0)
const RE_010 = /-----BEGIN ((?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)-----([A-Za-z0-9+/=\s:,\-\r\n]*?)-----END \1-----/g;
let seed = 12345;
const rnd = (n) => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed % n; };
const gen = (jet, max) => { let s = ''; const L = rnd(max); for (let i = 0; i < L; i++) s += jet[rnd(jet.length)]; return s; };
const JET = {
python: ['"', "'", '"""', "'''", '\\', '\n', ' ', '\t', 'r', 'b', 'f', 'u', 'x', '#', '=', '(', ')', 'rb', '\r\n', 'a1'],
javascript: ['=/[', '=/', ' x/', '"', "'", '`', '${', '}', '{', '/', '//', '/*', '*/', '[', ']', '\\', '\n', ' ', '=', '(', ')', 'return', 'x', '1', '.', ',', 'typeof'],
java: ['"', "'", '"""', '\\', '\n', ' ', '/', '//', '/*', '*/', 'x', '=', '(', ')'],
go: ['"', "'", '`', '\\', '\n', ' ', '/', '//', '/*', '*/', 'x', '=', '(', ')'],
};
const JET_PEM = ['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----', '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----',
'-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----', '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----',
'BEGIN CERTIFICATE-----', 'BEGIN PRIVATE KEY-----', 'AAAA', 'QUJD', '\n', ' ', '.', '-', '--', '-----', '"', 'BEGIN', '=', ':', ' ', '\r\n'];
const cat = {};
for (let k = 0; k < ITER; k++) {
for (const [limbaj, jet] of Object.entries(JET)) {
const t = gen(jet, 40);
const a = LV.curata(t, limbaj), b = LN.curata(t, limbaj);
comparate++;
if (a.code !== b.code || JSON.stringify(a.siruri) !== JSON.stringify(b.siruri)) { cat[limbaj] = (cat[limbaj] || 0) + 1; if (dif++ < 5) console.log(`DIFERIT lexer ${limbaj}: ${JSON.stringify(t)}`); }
}
const t = gen(JET_PEM, 30);
const a = [...t.matchAll(RE_010)].map((m) => [m.index, m[1], m[2]]);
const b = [...PN.blocuriPem(t)].map((x) => [x.index, x.tip, x.corp]);
comparate++;
if (JSON.stringify(a) !== JSON.stringify(b)) { cat.pem = (cat.pem || 0) + 1; if (dif++ < 5) console.log(`DIFERIT pem: ${JSON.stringify(t)}`); }
}
console.log(`texte generate: ${ITER} runde (lexer x4 + PEM), diferente ${JSON.stringify(cat)}`);
} finally { rmSync(vechi, { recursive: true, force: true }); }
console.log(`\n${dif ? 'DIFERIT' : 'IDENTIC'}: ${dif} diferente in ${comparate} comparatii (0.1.0 din ${REV} fata de 0.2.0)`);
process.exitCode = dif ? 1 : comparate ? 0 : 2;

Binary file not shown.

View File

@ -0,0 +1,30 @@
// Fixtura: Go. In Go un import nefolosit nu compileaza, deci importul e dovada de folosire.
package fixturi
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/hmac"
"crypto/md5"
"crypto/mlkem"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256" // EXPECT: SHA-256 | quantum-safe
"crypto/tls"
_ "crypto/sha512" // EXPECT: SHA-512 | quantum-safe
)
func Exemple(data []byte) {
k, _ := rsa.GenerateKey(rand.Reader, 3072) // EXPECT: RSA-3072 | quantum-vulnerable
e, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // EXPECT: ECDSA-secp256r1 | quantum-vulnerable
s := md5.Sum(data) // EXPECT: MD5 | weak-now
m := hmac.New(sha1.New, data) // EXPECT: HMAC-SHA-1 | weak-now
dk, _ := mlkem.GenerateKey768() // EXPECT: ML-KEM-768 | quantum-safe
cfg := &tls.Config{
MinVersion: tls.VersionTLS12, // EXPECT: TLSv1.2 | quantum-vulnerable
CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.CurveP256}, // EXPECT: X25519MLKEM768 | quantum-safe ; ECDH-secp256r1 | quantum-vulnerable
}
f := sha256.New
_, _, _, _, _, _, _ = k, e, s, m, dk, cfg, f
}

View File

@ -0,0 +1,36 @@
// Fixtura: JCA, JSSE si Bouncy Castle. Fisierul nu se compileaza; e doar text pentru scaner.
package fixturi;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.net.ssl.SSLContext;
import org.bouncycastle.crypto.engines.RC4Engine; // EXPECT: RC4 | weak-now
import org.bouncycastle.pqc.crypto.mlkem.MLKEMParameters;
public class Crypto {
private static final String DIGEST = "SHA-256";
public void exemple(String algDinParametru) throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); // EXPECT: RSA-4096 | quantum-vulnerable
kpg.initialize(4096);
KeyPairGenerator ec = KeyPairGenerator.getInstance("EC"); // EXPECT: EC-secp256r1 | quantum-vulnerable
ec.initialize(new ECGenParameterSpec("secp256r1"));
Signature s1 = Signature.getInstance("SHA256withECDSA"); // EXPECT: ECDSA | quantum-vulnerable
Signature s2 = Signature.getInstance("SHA1withRSA"); // EXPECT: RSA | weak-now
Signature s3 = Signature.getInstance("ML-DSA-65"); // EXPECT: ML-DSA-65 | quantum-safe
Cipher c1 = Cipher.getInstance("AES"); // EXPECT: AES-ECB | weak-now
Cipher c2 = Cipher.getInstance("AES/GCM/NoPadding"); // EXPECT: AES-GCM | quantum-safe
Cipher c3 = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); // EXPECT: RSA | quantum-vulnerable
Cipher c4 = Cipher.getInstance("DESede/CBC/PKCS5Padding"); // EXPECT: 3DES-CBC | weak-now
KeyAgreement ka = KeyAgreement.getInstance("X25519"); // EXPECT: X25519 | quantum-vulnerable
MessageDigest md = MessageDigest.getInstance("MD5"); // EXPECT: MD5 | weak-now
MessageDigest md2 = MessageDigest.getInstance(DIGEST); // EXPECT: SHA-256 | quantum-safe
MessageDigest md3 = MessageDigest.getInstance(algDinParametru); // EXPECT: MessageDigest.getInstance | unknown
SSLContext tls = SSLContext.getInstance("TLSv1.1"); // EXPECT: TLSv1.1 | weak-now
Object p = MLKEMParameters.ml_kem_768; // EXPECT: ML-KEM-768 | quantum-safe
}
}

View File

@ -0,0 +1,19 @@
// Fixtura: biblioteci recunoscute din importuri (TypeScript).
import { Wallet, verifyMessage } from 'ethers';
import jwt from 'jsonwebtoken';
import { secp256k1 } from '@noble/curves/secp256k1'; // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; // EXPECT: ML-DSA-65 | quantum-safe
import { ml_kem768 } from '@noble/post-quantum/ml-kem'; // EXPECT: ML-KEM-768 | quantum-safe
import elliptic from 'elliptic';
import { sha1 } from '../vendor/node_modules/@noble/hashes/legacy.js'; // EXPECT: SHA-1 | weak-now
const ec = new elliptic.ec('p256'); // EXPECT: EC-secp256r1 | quantum-vulnerable
export async function semneaza(privateKey: string, payload: object) {
const w = new Wallet(privateKey); // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
const sig = await w.signMessage('hello'); // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
const ok = verifyMessage('hello', sig); // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
const t1 = jwt.sign(payload, privateKey, { algorithm: 'RS256' }); // EXPECT: JWS RS256 | quantum-vulnerable
const t2 = jwt.verify(t1, privateKey, { algorithms: ['ES256', 'HS256'] }); // EXPECT: JWS ES256 | quantum-vulnerable ; JWS HS256 | quantum-safe
return { ec, ok, t2, secp256k1, ml_dsa65, ml_kem768, sha1 };
}

View File

@ -0,0 +1,31 @@
// Fixtura: node:crypto. Fiecare rand cu o folosire poarta marcajul EXPECT al gasirii asteptate.
// Fisierul nu se executa niciodata; e doar text pentru scaner.
import crypto from 'node:crypto';
import { createHash, generateKeyPairSync } from 'crypto';
const HASH_ALG = 'sha256';
export function exemple(data, key, iv, algDinParametru, pemDinConfig) {
const a = createHash('md5').update(data).digest('hex'); // EXPECT: MD5 | weak-now
const b = crypto.createHash('sha1'); // EXPECT: SHA-1 | weak-now
const c = crypto.createHash(HASH_ALG); // EXPECT: SHA-256 | quantum-safe
const d = crypto.createHash(algDinParametru); // EXPECT: createHash | unknown
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); // EXPECT: RSA-2048 | quantum-vulnerable
const k2 = crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' }); // EXPECT: EC-secp256k1 | quantum-vulnerable
const k3 = crypto.generateKeyPairSync('ed25519'); // EXPECT: Ed25519 | quantum-vulnerable
const k4 = crypto.generateKeyPairSync('ml-dsa-65'); // EXPECT: ML-DSA-65 | quantum-safe
const k5 = crypto.generateKeyPairSync('ml-kem-768'); // EXPECT: ML-KEM-768 | quantum-safe
const s = crypto.createSign('RSA-SHA256'); // EXPECT: RSA | quantum-vulnerable
const v = crypto.createVerify('SHA1'); // EXPECT: signature-with-SHA-1 | weak-now
const e = crypto.createECDH('prime256v1'); // EXPECT: ECDH-secp256r1 | quantum-vulnerable
const c1 = crypto.createCipheriv('aes-256-gcm', key, iv); // EXPECT: AES-256-GCM | quantum-safe
const c2 = crypto.createCipheriv('des-ede3-cbc', key, iv); // EXPECT: 3DES-CBC | weak-now
const c3 = crypto.createCipheriv('aes-128-ecb', key, null); // EXPECT: AES-128-ECB | weak-now
const c4 = crypto.createDecipheriv('aes-128-cbc', key, iv); // EXPECT: AES-128-CBC | quantum-safe
const enc = crypto.publicEncrypt(publicKey, data); // EXPECT: RSA | quantum-vulnerable
const h = crypto.createHmac('sha256', key); // EXPECT: HMAC-SHA-256 | quantum-safe
const dh = crypto.getDiffieHellman('modp2'); // EXPECT: DH-1024 | weak-now
const sig = crypto.sign(null, data, key); // EXPECT: crypto.sign | unknown
const pk = crypto.createPrivateKey(pemDinConfig); // EXPECT: createPrivateKey | unknown
return [a, b, c, d, k2, k3, k4, k5, s, v, e, c1, c2, c3, c4, enc, h, dh, sig, pk];
}

View File

@ -0,0 +1,18 @@
// Fixtura: optiuni TLS in Node.
const tls = require('node:tls');
const https = require('https');
const server = tls.createServer({
minVersion: 'TLSv1', // EXPECT: TLSv1.0 | weak-now
ecdhCurve: 'X25519MLKEM768:X25519:prime256v1', // EXPECT: X25519MLKEM768 | quantum-safe ; X25519 | quantum-vulnerable ; ECDH-secp256r1 | quantum-vulnerable
});
const agent = new https.Agent({ minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }); // EXPECT: TLSv1.2 | quantum-vulnerable ; TLSv1.3 | unknown
const PQ = 'X25519MLKEM768';
const GRUPURI = { hibrid: 'X25519MLKEM768' };
const a2 = tls.connect({ ecdhCurve: PQ }); // EXPECT: X25519MLKEM768 | quantum-safe
const a3 = tls.connect({ ecdhCurve: `${PQ}:X25519` }); // EXPECT: X25519MLKEM768 | quantum-safe ; X25519 | quantum-vulnerable
const a4 = tls.connect({ ecdhCurve: GRUPURI[process.env.MOD] }); // EXPECT: tls ecdhCurve | unknown
module.exports = { server, agent, a2, a3, a4 };

View File

@ -0,0 +1,15 @@
// Fixtura: WebCrypto (browser sau globalThis.crypto in Node).
const subtle = globalThis.crypto.subtle;
export async function webcrypto(data) {
const rsa = await subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 3072, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, // EXPECT: RSA-3072 | quantum-vulnerable
true, ['encrypt', 'decrypt']);
const ec = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign', 'verify']); // EXPECT: ECDSA-secp384r1 | quantum-vulnerable
const ed = await subtle.generateKey('Ed25519', true, ['sign', 'verify']); // EXPECT: Ed25519 | quantum-vulnerable
const x = await subtle.generateKey({ name: 'X25519' }, true, ['deriveBits']); // EXPECT: X25519 | quantum-vulnerable
const aes = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt']); // EXPECT: AES-256-GCM | quantum-safe
const d1 = await subtle.digest('SHA-1', data); // EXPECT: SHA-1 | weak-now
const d2 = await subtle.digest({ name: 'SHA-384' }, data); // EXPECT: SHA-384 | quantum-safe
return [rsa, ec, ed, x, aes, d1, d2];
}

View File

@ -0,0 +1,22 @@
import crypto from 'node:crypto';
export const h = crypto.createHash('md5');
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime
// umplutura pentru limita de marime

View File

@ -0,0 +1,2 @@
import crypto from 'node:crypto';
export const h = crypto.createHash('sha256');

View File

@ -0,0 +1,9 @@
module example.com/fixtura
go 1.24
require (
github.com/cloudflare/circl v1.6.1
golang.org/x/crypto v0.36.0
golang.org/x/text v0.23.0
)

View File

@ -0,0 +1,12 @@
{
"name": "fixtura-manifest",
"version": "1.0.0",
"dependencies": {
"ethers": "^6.13.0",
"lodash": "^4.17.21",
"jsonwebtoken": "9.0.2"
},
"devDependencies": {
"@noble/post-quantum": "0.4.1"
}
}

View File

@ -0,0 +1,14 @@
<project>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,3 @@
requests==2.32.3
cryptography==43.0.1
PyJWT[crypto]>=2.8

View File

@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBmzCCAUGgAwIBAgIUSB9o0oiJeX65E+oyUE6ukzrvT2MwCgYIKoZIzj0EAwIw
IzEhMB8GA1UEAwwYY3J5cHRvLWludmVudG9yeS1maXh0dXJlMB4XDTI2MDkyNTA4
NTAwM1oXDTM2MDkyMjA4NTAwM1owIzEhMB8GA1UEAwwYY3J5cHRvLWludmVudG9y
eS1maXh0dXJlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPwYXPWSiw0/Y4M2Q
zkQHHXLNkRJv0W0Ml7C3vN9HyKl1C9LrNB5evAruKJVTTh+J5KJaxfjDLFldkczR
2IKBEaNTMFEwHQYDVR0OBBYEFPE/ChbrkN4O/z7KgfHWZR/+XBBmMB8GA1UdIwQY
MBaAFPE/ChbrkN4O/z7KgfHWZR/+XBBmMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZI
zj0EAwIDSAAwRQIhAMAK5Uxt090TnPPp5xn3cG1vD1BSNEc+ZJBI0JPPeCl1AiAM
egBkNslxucBd5XhFrWbvxCIRBedHr0dZGJYrHIZqrg==
-----END CERTIFICATE-----

View File

@ -0,0 +1,10 @@
Test-only public key generated for the crypto-inventory fixtures.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvhFbgeI5qpI+99IZBwOt
nuwndPRr+gpT5oLNlXVqjOhWSJZWPGgt4cNGw7XgeSujl91Wl8TGCoRAgq4xOxmw
d+WVycVlCMKK7Peaepe/wL+DMEABVDuqlkiFEe90/6Kk6sIge555qBGBa/tsvkxX
trU1khKI9A7g1YAkSkmUiCAPTktvZ/ED8r6P61j5u8lXnbGeNU7a7vivgWF4CaF4
a4m4oAVHUxWLKmeSLN8CeIn6AZSmqc5e/0DMEMGuzqC/XBKWz55bhkzq/CAMdPcI
1zbFcXkUM7tWJzLbgAW4R3Bpe1GUD8+h0fG9v8xnJOf1vQ1M4veJ1OQrEhFwuI4+
TQIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,14 @@
// MessageDigest.getInstance("MD5") intr-un comentariu nu e o folosire.
package fixturi;
public class Comentarii {
/* Cipher.getInstance("DES") intr-un comentariu bloc */
/**
* Signature.getInstance("SHA1withRSA") in Javadoc.
*/
private final String doc = "KeyPairGenerator.getInstance(\"RSA\") in a string";
public String doc() {
return doc;
}
}

View File

@ -0,0 +1,11 @@
// Fixtura negativa Go: niciun import criptografic.
package fixturi
/*
import "crypto/md5"
*/
// md5.Sum(data) intr-un comentariu nu e o folosire.
func Nimic() string {
return "crypto/rsa rsa.GenerateKey(rand.Reader, 1024)"
}

View File

@ -0,0 +1,14 @@
// Fixtura negativa: fisierul importa node:crypto doar pentru UUID-uri. Comentariile pomenesc
// API-uri reale; niciunul nu e o folosire si scanerul trebuie sa dea ZERO gasiri.
import crypto from 'node:crypto';
// crypto.createHash('md5') intr-un comentariu nu e o folosire.
// const k = crypto.generateKeyPairSync('rsa', { modulusLength: 1024 });
/* crypto.createCipheriv('des-ede3-cbc', key, iv) intr-un comentariu bloc */
/**
* Exemplu din documentatie: crypto.createSign('RSA-SHA1').
* RSA, ECDSA, secp256k1 si X25519 sunt aici doar cuvinte.
*/
export function id() {
return crypto.randomUUID(); // crypto.createHmac('sha1', k) tot comentariu
}

View File

@ -0,0 +1,13 @@
"""Modul fara criptografie.
Documentatia pomeneste rsa.generate_private_key(public_exponent=65537, key_size=1024)
si hashlib.md5(data), dar nu le cheama.
"""
import hashlib
def f(x):
"""Returneaza x. Nu foloseste hashlib.sha1(x)."""
# hashlib.md5(x) intr-un comentariu nu e o folosire
text = "hashlib.md5 apare aici doar ca text"
return x, text, hashlib

View File

@ -0,0 +1,9 @@
export function sign(x) {
return Math.sign(x);
}
export function createHash(n) {
return { n, hash: n * 31 };
}
export const rezultat = [sign(-3), createHash(7)].map((v) => JSON.stringify(v));

View File

@ -0,0 +1,9 @@
import crypto from 'node:crypto';
export const doc = "Call crypto.createHash('md5') to hash (documentation string).";
export const tpl = `Use crypto.createCipheriv('des-ede3-cbc', key, iv) at ${Date.now()}`;
export const eticheta = 'crypto.generateKeyPairSync("rsa", { modulusLength: 1024 })';
export const re = /createHash\('sha1'\)/;
export function id() {
return crypto.randomUUID();
}

View File

@ -0,0 +1,12 @@
// Fixtura: algoritmi dati prin variabile care NU se pot rezolva static. Trebuie "unknown", nu ghicit.
import crypto from 'node:crypto';
let alg = 'md5';
alg = 'sha256';
export function h(data, algDinApel, opts) {
const a = crypto.createHash(alg); // EXPECT: createHash | unknown
const b = crypto.createHash(algDinApel); // EXPECT: createHash | unknown
const c = crypto.createCipheriv(opts.cipher, opts.key, opts.iv); // EXPECT: createCipheriv | unknown
return [a, b, c, data];
}

View File

@ -0,0 +1,32 @@
"""Fixtura: criptografie in Python. Fisierul nu se executa; e doar text pentru scaner."""
import hashlib
import hmac
import ssl
from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519, padding
from cryptography.hazmat.primitives import hashes
from Crypto.Cipher import AES, DES3
from ecdsa import SigningKey, SECP256k1
import jwt
import oqs
DIGEST = "sha256"
def exemple(data, key, alg_din_parametru):
k1 = rsa.generate_private_key(public_exponent=65537, key_size=1024) # EXPECT: RSA-1024 | weak-now
k2 = ec.generate_private_key(ec.SECP384R1()) # EXPECT: EC-secp384r1 | quantum-vulnerable
k3 = ed25519.Ed25519PrivateKey.generate() # EXPECT: Ed25519 | quantum-vulnerable
sig = k2.sign(data, ec.ECDSA(hashes.SHA256())) # EXPECT: ECDSA | quantum-vulnerable ; SHA-256 | quantum-safe
ct = k1.public_key().encrypt(data, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) # EXPECT: RSA | quantum-vulnerable ; SHA-256 | quantum-safe
h1 = hashlib.md5(data).hexdigest() # EXPECT: MD5 | weak-now
h2 = hashlib.new(DIGEST) # EXPECT: SHA-256 | quantum-safe
h3 = hashlib.new(alg_din_parametru) # EXPECT: hashlib.new | unknown
mac = hmac.new(key, data, digestmod="sha1") # EXPECT: HMAC-SHA-1 | weak-now
c1 = AES.new(key, AES.MODE_ECB) # EXPECT: AES-ECB | weak-now
c2 = DES3.new(key, DES3.MODE_CBC) # EXPECT: 3DES-CBC | weak-now
sk = SigningKey.generate(curve=SECP256k1) # EXPECT: ECDSA-secp256k1 | quantum-vulnerable
tok = jwt.encode({"a": 1}, key, algorithm="ES256K") # EXPECT: JWS ES256K | quantum-vulnerable
kem = oqs.KeyEncapsulation("ML-KEM-768") # EXPECT: ML-KEM-768 | quantum-safe
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # EXPECT: TLS (negotiated) | unknown
ctx.minimum_version = ssl.TLSVersion.TLSv1_2 # EXPECT: TLSv1.2 | quantum-vulnerable
return [k3, sig, ct, h1, h2, h3, mac, c1, c2, sk, tok, kem]

View File

@ -0,0 +1,72 @@
// Proba de cost: inventarul trebuie sa ramana LINIAR pe fisiere facute anume (2026-09-29, revizuirea adversariala). Formele vechi
// ale cautarii PEM, ale literalului regex JS, ale docstring-ului Python, ale rezolvarii variabilelor si ale variabilei atribuite
// in Java costau patratic: un singur fisier de 1 MB (marimea maxima implicita) tinea scanarea minute intregi.
// Fiecare caz se scaneaza la doua marimi, a doua de 4 ori prima (64 si 256 KB; PEM 256 KB si 1 MB), fiecare intr-un proces
// separat cu termen; liniar inseamna ~4x, patratic ~16x. Verdict pe caz: VERDE daca t2 <= 6 * t1 + 250 ms si t2 < 10 s; ROSU
// altfel (si la depasirea termenului).
// node test/proba-timp.mjs [--doar caz1,caz2] -> 0 toate verzi, 1 unul rosu, 2 STRICAT (o masurare nu a pornit)
// Modulul masurat se schimba cu CRYPTO_INVENTORY_MODULE=<cale>/inventar.mjs (asa il foloseste controlul negativ).
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
const AICI = dirname(fileURLToPath(import.meta.url));
const CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs');
const TERMEN_MS = 20000;
// unitatea se repeta pana la marime; '@' se inlocuieste cu un contor (nume distincte)
export const CAZURI = {
// pem: cautarea sfarsitului e nativa (indexOf), deci forma patratica se vede abia spre marimea maxima implicita
'pem.txt': ['-----BEGIN CERTIFICATE-----AAAA\n', 256, 1024],
'py-triplu.py': 'x="""a"""+',
'py-linie.py': 'hashlib.new(x)+',
'js-regex.js': '=/[',
'js-linie.js': 'crypto.createHash(x)+',
'js-func.js': 'crypto.createHash(q)\nfunction (',
'java-linie.java': 'Cipher.getInstance(x)+',
'java-kpg.java': 'k = KeyPairGenerator.getInstance("EC")+',
};
const marimi = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume].slice(1) : [64, 256]);
const unitate = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume][0] : CAZURI[nume]);
const genereaza = (unit, kb) => unit.repeat(Math.max(1, Math.floor((kb * 1024) / unit.length)));
const arg = (f) => { const i = process.argv.indexOf(f); return i >= 0 ? process.argv[i + 1] : null; };
if (arg('--caz')) {
// copilul: scaneaza un singur fisier si tipareste milisecundele
const nume = arg('--caz'); const kb = Number(arg('--kb'));
const inv = await import(pathToFileURL(CALE_MODUL).href);
const d = mkdtempSync(join(tmpdir(), 'ci-timp-'));
try {
writeFileSync(join(d, nume), genereaza(unitate(nume), kb));
const t0 = process.hrtime.bigint();
const rez = inv.scan(d);
const ms = Number(process.hrtime.bigint() - t0) / 1e6;
if (rez.stats.filesSeen !== 1 || rez.stats.codeFilesTotal + rez.stats.textFilesPemOnly !== 1) throw new Error('fisierul nu a fost citit');
console.log(JSON.stringify({ ms }));
} finally { rmSync(d, { recursive: true, force: true }); }
} else {
const doar = arg('--doar') ? arg('--doar').split(',') : Object.keys(CAZURI);
const masoara = (nume, kb) => {
const r = spawnSync(process.execPath, [fileURLToPath(import.meta.url), '--caz', nume, '--kb', String(kb)], { encoding: 'utf8', timeout: TERMEN_MS, env: process.env });
if (r.error && r.error.code === 'ETIMEDOUT') return { termen: true };
try { return { ms: JSON.parse((r.stdout || '').trim().split('\n').pop()).ms }; } catch { return { stricat: `cod ${r.status}: ${(r.stderr || '').trim().split('\n').slice(-1)[0]}` }; }
};
let rosii = 0, stricate = 0;
for (const nume of doar) {
if (!CAZURI[nume]) { stricate++; console.log(`STRICAT ${nume}: caz necunoscut`); continue; }
const [k1, k2] = marimi(nume);
const a = masoara(nume, k1);
const b = a.ms !== undefined ? masoara(nume, k2) : a.termen ? {} : { stricat: `nemasurat (${k1} KB a cazut)` };
if (a.stricat || b.stricat) { stricate++; console.log(`STRICAT ${nume}: ${a.stricat || b.stricat}`); continue; }
if (a.termen || b.termen) { rosii++; console.log(`ROSU ${nume}: peste ${TERMEN_MS / 1000} s la ${a.termen ? k1 : k2} KB`); continue; }
const ok = b.ms <= 6 * a.ms + 250 && b.ms < 10000;
if (!ok) rosii++;
console.log(`${ok ? 'VERDE ' : 'ROSU '} ${nume.padEnd(16)} ${String(k1).padStart(4)} KB ${a.ms.toFixed(0).padStart(6)} ms | ${String(k2).padStart(4)} KB ${b.ms.toFixed(0).padStart(6)} ms x${(b.ms / Math.max(a.ms, 1)).toFixed(1)}`);
}
console.log(`\ncostul inventarului: ${doar.length - rosii - stricate}/${doar.length} liniare, ${rosii} rosii, ${stricate} stricate (modul: ${CALE_MODUL})`);
process.exitCode = stricate ? 2 : rosii ? 1 : 0;
}

View File

@ -0,0 +1,486 @@
// Suita de probe a inventarului criptografic.
// Folosire: node test/proba.mjs [--json]
// Modulul masurat se poate schimba cu CRYPTO_INVENTORY_MODULE=<cale>/inventar.mjs (asa il foloseste
// control-negativ.mjs pe o copie stricata). Fixturile raman mereu cele din test/fixturi.
// Fiecare proba ruleaza izolat: o proba care arunca iese ROSU cu mesajul ei, restul ruleaza mai departe.
import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, writeFileSync, rmSync, copyFileSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { generateKeyPairSync } from 'node:crypto';
const AICI = dirname(fileURLToPath(import.meta.url));
const FIX = join(AICI, 'fixturi');
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 CA_JSON = process.argv.includes('--json');
// numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient
const MARCAJE_ASTEPTATE = 96;
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 probe = [];
const proba = (nume, fn) => probe.push({ nume, fn });
function asigura(cond, mesaj) { if (!cond) throw new Error(mesaj); }
const memo = new Map();
function scan(dir, optiuni = {}) {
const k = dir + JSON.stringify(optiuni);
if (!memo.has(k)) {
try { memo.set(k, { rez: inv.scan(dir, optiuni) }); } catch (err) { memo.set(k, { err }); }
}
const r = memo.get(k);
if (r.err) throw r.err;
return r.rez;
}
function marcaje(rel) {
const r = [];
readFileSync(join(FIX, rel), 'utf8').split('\n').forEach((l, i) => {
const m = /EXPECT:\s*(.+?)\s*$/.exec(l);
if (!m) return;
for (const p of m[1].split(' ; ')) {
const [n, c] = p.split(' | ').map((s) => s.trim());
r.push(`${i + 1}|${n}|${c}`);
}
});
return r.sort();
}
function gasiriDin(rez, rel) {
return rez.findings.filter((g) => g.file === rel);
}
function comparaExact(rel) {
const rez = scan(FIX);
const ast = marcaje(rel);
asigura(ast.length > 0, `${rel}: fixtura nu are marcaje`);
const real = gasiriDin(rez, rel).map((g) => `${g.line}|${g.name}|${g.classification}`).sort();
const lipsa = ast.filter((x) => !real.includes(x));
const extra = real.filter((x) => !ast.includes(x));
asigura(!lipsa.length && !extra.length && ast.length === real.length,
`${rel}: lipsa [${lipsa.join(', ')}] extra [${extra.join(', ')}] (asteptate ${ast.length}, gasite ${real.length})`);
}
// --- pozitive: fiecare gasire asteptata, cu linia si clasa ei, si nimic in plus ---
for (const rel of FISIERE_CU_MARCAJE.filter((f) => !f.startsWith('negative/'))) {
proba(`pozitiv: ${rel}`, () => comparaExact(rel));
}
proba('pozitiv: numarul total de marcaje EXPECT este cel scris', () => {
const n = FISIERE_CU_MARCAJE.reduce((s, f) => s + marcaje(f).length, 0);
asigura(n === MARCAJE_ASTEPTATE, `marcaje citite ${n}, scrise ${MARCAJE_ASTEPTATE}`);
const rez = scan(FIX);
const inFisiere = rez.findings.filter((g) => FISIERE_CU_MARCAJE.includes(g.file)).length;
asigura(inFisiere === MARCAJE_ASTEPTATE, `gasiri in fisierele cu marcaje: ${inFisiere}, marcaje ${MARCAJE_ASTEPTATE}`);
});
proba('pozitiv: variabila rezolvata la un literal unic poarta resolvedFrom', () => {
const g = gasiriDin(scan(FIX), 'js/node-crypto.mjs').find((x) => x.name === 'SHA-256');
asigura(g && /HASH_ALG \(line 6\)/.test(g.resolvedFrom || ''), `resolvedFrom: ${g && g.resolvedFrom}`);
const j = gasiriDin(scan(FIX), 'java/Crypto.java').find((x) => x.name === 'SHA-256');
asigura(j && /DIGEST/.test(j.resolvedFrom || ''), `java resolvedFrom: ${j && j.resolvedFrom}`);
});
proba('pozitiv: manifestele dau biblioteci declarate, nu gasiri', () => {
const rez = scan(join(FIX, 'manifeste'));
asigura(rez.findings.length === 0, `gasiri din manifeste: ${rez.findings.length}`);
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',
'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on',
'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt'].sort();
asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`);
const bom = inv.buildCbom(rez, { deterministic: true });
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');
});
proba('pozitiv: material PEM (certificat si cheie publica)', () => {
const rez = scan(FIX);
const c = gasiriDin(rez, 'material/certificat-proba.pem');
asigura(c.length === 1 && c[0].assetType === 'certificate' && c[0].name === 'EC-secp256r1' && c[0].classification === 'quantum-vulnerable' && c[0].line === 1, `certificat: ${JSON.stringify(c.map((x) => [x.assetType, x.name, x.line]))}`);
asigura(/crypto-inventory-fixture/.test(c[0].certificate.subjectName), 'subiectul certificatului lipseste');
const p = gasiriDin(rez, 'material/cheie-publica-proba.pem');
asigura(p.length === 1 && p[0].assetType === 'related-crypto-material' && p[0].material.type === 'public-key' && p[0].name === 'RSA-2048' && p[0].line === 2, `cheie publica: ${JSON.stringify(p.map((x) => [x.assetType, x.name, x.line]))}`);
});
proba('pozitiv: cheie privata generata la rulare e raportata, iar valoarea ei nu apare in iesire', () => {
const d = mkdtempSync(join(tmpdir(), 'ci-cheie-'));
try {
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
const pem = privateKey.export({ type: 'pkcs8', format: 'pem' });
writeFileSync(join(d, 'config.txt'), `setari\n${pem}`);
const rez = inv.scan(d);
asigura(rez.findings.length === 1, `gasiri: ${rez.findings.length}`);
const g = rez.findings[0];
asigura(g.assetType === 'related-crypto-material' && g.material.type === 'private-key' && g.name === 'EC-secp256r1' && g.line === 2, `gasirea: ${JSON.stringify([g.assetType, g.material, g.name, g.line])}`);
const corp = pem.split('\n').filter((l) => l && !l.startsWith('-----')).join('');
const iesire = JSON.stringify(inv.buildCbom(rez)) + JSON.stringify(rez.findings) + inv.renderSummary(rez);
for (let i = 0; i + 24 <= corp.length; i += 8) asigura(!iesire.includes(corp.slice(i, i + 24)), `o bucata din cheia privata apare in iesire (offset ${i})`);
} finally {
rmSync(d, { recursive: true, force: true });
}
});
// --- clasificare ---
proba('clasificare: secp256k1 este quantum-vulnerable, cu recomandare de cont post-cuantic', () => {
const a = inv.classify({ grup: 'SECP256K1' });
asigura(a.classification === 'quantum-vulnerable' && a.quantumVulnerable === true, `clasa ${a.classification}`);
asigura(/post-quantum key/.test(a.recommendation) && /not a library swap/.test(a.recommendation), 'recomandarea nu vorbeste de contul post-cuantic');
const f = gasiriDin(scan(FIX), 'js/node-crypto.mjs').find((x) => x.curve === 'secp256k1');
asigura(f && f.classification === 'quantum-vulnerable', 'gasirea secp256k1 din fixtura nu e quantum-vulnerable');
});
proba('clasificare: MD5 si SHA-1 sunt weak-now, SHA-256 quantum-safe', () => {
const c = (h) => inv.classify({ grup: 'HASH', hash: h }).classification;
asigura(c('MD5') === 'weak-now' && c('SHA1') === 'weak-now' && c('SHA256') === 'quantum-safe', `MD5 ${c('MD5')} SHA1 ${c('SHA1')} SHA256 ${c('SHA256')}`);
});
proba('clasificare: AES-128 quantum-safe cu nota Grover, ECB weak-now, DES weak-now', () => {
const a = inv.classify({ grup: 'CIPHER', nume: 'AES', param: '128', mod: 'gcm' });
asigura(a.classification === 'quantum-safe' && /Grover/.test(a.reason + a.recommendation) && a.nistQuantumSecurityLevel === 1, `AES-128: ${a.classification} ${a.nistQuantumSecurityLevel}`);
asigura(inv.classify({ grup: 'CIPHER', nume: 'AES', param: '256', mod: 'ecb' }).classification === 'weak-now', 'ECB nu e weak-now');
asigura(inv.classify({ grup: 'CIPHER', nume: 'DES' }).classification === 'weak-now', 'DES nu e weak-now');
asigura(inv.classify({ grup: 'TLS', param: '1.1', rol: 'min' }).classification === 'weak-now', 'TLS 1.1 nu e weak-now');
});
proba('clasificare: fiecare gasire vulnerabila sau slaba are motiv si recomandare', () => {
const rez = scan(FIX);
for (const g of rez.findings) {
asigura(g.reason && g.reason.length > 20, `${g.file}:${g.line} ${g.name} fara motiv`);
if (g.classification === 'quantum-vulnerable' || g.classification === 'weak-now' || g.classification === 'unknown') {
asigura(g.recommendation && g.recommendation.length > 20, `${g.file}:${g.line} ${g.name} (${g.classification}) fara recomandare`);
}
}
});
proba('clasificare: JWT asimetric spune ca standardul post-cuantic nu e final', () => {
const g = gasiriDin(scan(FIX), 'js/biblioteci.ts').find((x) => x.name === 'JWS RS256');
asigura(g && /no final standard/.test(g.recommendation) && /draft/.test(g.recommendation), 'recomandarea JWT nu spune ca nu exista standard final');
});
// --- negative ---
function controlPozitivMd5() {
const g = gasiriDin(scan(FIX), 'js/node-crypto.mjs').find((x) => x.name === 'MD5');
asigura(g, 'controlul pozitiv al metodei: MD5 din js/node-crypto.mjs lipseste, deci un zero nu inseamna nimic');
}
proba('negativ: comentariile JS nu produc gasiri', () => {
controlPozitivMd5();
const g = gasiriDin(scan(FIX), 'negative/comentarii.js');
asigura(g.length === 0, `comentariile au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
});
proba('negativ: apelul scris intr-un sir JS nu e folosire', () => {
controlPozitivMd5();
const g = gasiriDin(scan(FIX), 'negative/siruri.js');
asigura(g.length === 0, `sirurile au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
});
proba('negativ: comentariile si docstring-urile Python nu produc gasiri', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'python/crypto_py.py').some((x) => x.name === 'MD5'), 'controlul pozitiv: hashlib.md5 din fixtura Python lipseste');
const g = gasiriDin(rez, 'negative/docstring.py');
asigura(g.length === 0, `docstring/comentariu au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
});
proba('negativ: comentariile si sirurile Java si Go nu produc gasiri', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'java/Crypto.java').some((x) => x.name === 'MD5') && gasiriDin(rez, 'go/crypto.go').some((x) => x.name === 'MD5'), 'controlul pozitiv: MD5 din fixturile Java/Go lipseste');
const g = [...gasiriDin(rez, 'negative/Comentarii.java'), ...gasiriDin(rez, 'negative/comentarii.go')];
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`);
});
proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => {
const rez = scan(join(FIX, 'negative'));
asigura(rez.stats.filesSeen === 7 && rez.stats.codeFilesTotal === 7, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`);
const g = gasiriDin(rez, 'fara-cripto.js');
asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`);
});
proba('negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit', () => {
comparaExact('negative/variabile.js');
const g = gasiriDin(scan(FIX), 'negative/variabile.js');
asigura(g.every((x) => x.classification === 'unknown' && !x.resolvedFrom), 'o variabila nerezolvabila a primit o valoare');
});
proba('negativ: fisierul binar e sarit si numarat', () => {
const rez = scan(join(FIX, 'binar'));
asigura(rez.stats.filesSeen === 1 && rez.stats.notRead.binary === 1 && rez.findings.length === 0, `vazute ${rez.stats.filesSeen}, binare ${rez.stats.notRead.binary}, gasiri ${rez.findings.length}`);
asigura(rez.stats.notReadExamples.binary.includes('modul.js'), 'exemplul binar lipseste din statistica');
// control pozitiv: aceiasi octeti fara NUL dau gasirea, deci zeroul de mai sus vine din saritura
const d = mkdtempSync(join(tmpdir(), 'ci-binar-'));
try {
const b = readFileSync(join(FIX, 'binar', 'modul.js'));
writeFileSync(join(d, 'modul.js'), Buffer.from([...b].filter((x) => x !== 0)));
const r2 = inv.scan(d);
asigura(r2.findings.some((x) => x.name === 'MD5'), 'controlul pozitiv: fara NUL, fisierul nu produce MD5');
} finally { rmSync(d, { recursive: true, force: true }); }
});
proba('negativ: fisier prea mare sarit si numarat', () => {
const rez = scan(join(FIX, 'limite'), { maxFileBytes: 400 });
asigura(rez.stats.filesSeen === 2 && rez.stats.notRead.tooLarge === 1, `vazute ${rez.stats.filesSeen}, prea mari ${rez.stats.notRead.tooLarge}`);
asigura(/^mare\.js \(\d+ bytes\)$/.test(rez.stats.notReadExamples.tooLarge[0] || ''), `exemplu: ${rez.stats.notReadExamples.tooLarge[0]}`);
asigura(!rez.findings.some((x) => x.file === 'mare.js') && rez.findings.some((x) => x.file === 'mic.js'), 'gasirile nu corespund limitei');
const tot = scan(join(FIX, 'limite'));
asigura(tot.findings.some((x) => x.file === 'mare.js' && x.name === 'MD5'), 'controlul pozitiv: fara limita, mare.js nu da MD5');
});
proba('negativ: limita de fisiere numara restul', () => {
const rez = scan(join(FIX, 'limite'), { maxFiles: 1 });
asigura(rez.stats.filesSeen === 2 && rez.stats.notRead.fileLimit === 1 && rez.stats.codeFilesTotal === 1, `vazute ${rez.stats.filesSeen}, peste limita ${rez.stats.notRead.fileLimit}`);
});
// --- cazurile-limita ale formelor liniare (2026-09-29, revizuirea adversariala: cautarea PEM, literalul regex JS, docstring-ul
// Python si rezolvarea variabilelor aveau cost patratic; formele noi trebuie sa dea EXACT ce dadeau cele vechi, si aici stau
// cazurile pe care o forma liniara scrisa gresit le-ar schimba). Modulele se iau de langa modulul masurat (copia controlului).
const LIB = join(dirname(CALE_MODUL), 'lib');
const lexer = await import(pathToFileURL(join(LIB, 'lexer.mjs')).href);
const pem = await import(pathToFileURL(join(LIB, 'pem.mjs')).href);
const contextMod = await import(pathToFileURL(join(LIB, 'context.mjs')).href);
proba('limite: un bloc PEM care incepe in liniutele unui antet fara sfarsit e gasit', () => {
const corp = 'A'.repeat(64);
const t = `-----BEGIN PRIVATE KEY-----BEGIN CERTIFICATE-----${corp}-----END CERTIFICATE-----`;
const b = [...pem.blocuriPem(t)].map((x) => `${x.index}|${x.tip}|${x.corp}`);
asigura(JSON.stringify(b) === JSON.stringify([`22|CERTIFICATE|${corp}`]), `blocuri: ${JSON.stringify(b)}`);
const fara = [...pem.blocuriPem(`-----BEGIN CERTIFICATE-----${corp}."-----END CERTIFICATE-----`)];
asigura(fara.length === 0, 'un caracter din afara alfabetului corpului inainte de sfarsit trebuie sa anuleze blocul');
});
proba('limite: literalul regex JS se cauta din starea lui, nu din ce a esuat alt literal pe acelasi rand', () => {
// primul `/` deschide o clasa care nu se inchide (esec pana la capatul randului, trecand prin 8 IN clasa); al doilea porneste
// din 8 IN AFARA clasei si se inchide la 10
const r = lexer.curata('x =/[ =/ x/;', 'javascript');
asigura(JSON.stringify(r.siruri) === JSON.stringify([[7, 11]]), `siruri: ${JSON.stringify(r.siruri)}`);
});
proba('limite: docstring Python pe randul lui (si dupa un sir neterminat), dar nu dupa cod pe acelasi rand', () => {
// docstring-ul de dupa sirul neterminat e INDENTAT: spatiile de inainte de el sunt pe randul nou, nu in coada sirului
const t = "a = 'abc\n '''doc'''\ny = '''s'''\n r'''d2'''\n";
const r = lexer.curata(t, 'python');
asigura(!r.code.includes('doc') && !r.code.includes('d2'), `docstring-urile trebuiau golite: ${JSON.stringify(r.code)}`);
asigura(r.code.includes("'''s'''"), `sirul triplu de dupa cod trebuia pastrat: ${JSON.stringify(r.code)}`);
const s = t.indexOf("'''s'''");
asigura(r.siruri.some(([a, b]) => a === s && b === s + 7), `intervalul sirului s lipseste: ${JSON.stringify(r.siruri)}`);
});
proba('limite: peste plafonul de variabile pe fisier numele ies unknown si fisierul se numara', () => {
const d = mkdtempSync(join(tmpdir(), 'ci-plafon-'));
try {
const n = contextMod.REZOLVARI_PE_FISIER;
const linii = ["import { createHash } from 'node:crypto';"];
for (let i = 0; i <= n; i++) linii.push(`const h${i} = 'sha256';`, `createHash(h${i});`);
writeFileSync(join(d, 'multe.mjs'), linii.join('\n') + '\n');
const rez = inv.scan(d);
const rezolvate = rez.findings.filter((g) => g.name === 'SHA-256' && g.resolvedFrom).length;
const necunoscute = rez.findings.filter((g) => g.classification === 'unknown').length;
asigura(rezolvate === n && necunoscute === 1, `rezolvate ${rezolvate} (cerut ${n}), unknown ${necunoscute} (cerut 1)`);
asigura(rez.stats.resolutionLimitFiles === 1 && rez.stats.resolutionLimitExamples[0] === 'multe.mjs', `statistica: ${rez.stats.resolutionLimitFiles}`);
} finally { rmSync(d, { recursive: true, force: true }); }
});
// --- CBOM ---
const E = {
bom: ['$schema', 'bomFormat', 'specVersion', 'serialNumber', 'version', 'metadata', 'components', 'services', 'externalReferences', 'dependencies', 'compositions', 'properties', 'vulnerabilities', 'annotations', 'formulation', 'declarations', 'definitions', 'signature'],
component: ['type', 'mime-type', 'bom-ref', 'supplier', 'manufacturer', 'authors', 'author', 'publisher', 'group', 'name', 'version', 'description', 'scope', 'hashes', 'licenses', 'copyright', 'cpe', 'purl', 'omniborId', 'swhid', 'swid', 'modified', 'pedigree', 'externalReferences', 'properties', 'components', 'evidence', 'releaseNotes', 'modelCard', 'data', 'cryptoProperties', 'signature', 'tags'],
componentType: ['application', 'framework', 'library', 'container', 'platform', 'operating-system', 'device', 'device-driver', 'firmware', 'file', 'machine-learning-model', 'data', 'cryptographic-asset'],
cryptoProperties: ['assetType', 'algorithmProperties', 'certificateProperties', 'relatedCryptoMaterialProperties', 'protocolProperties', 'oid'],
assetType: ['algorithm', 'certificate', 'protocol', 'related-crypto-material'],
algorithmProperties: ['primitive', 'parameterSetIdentifier', 'curve', 'executionEnvironment', 'implementationPlatform', 'certificationLevel', 'mode', 'padding', 'cryptoFunctions', 'classicalSecurityLevel', 'nistQuantumSecurityLevel'],
primitive: ['drbg', 'mac', 'block-cipher', 'stream-cipher', 'signature', 'hash', 'pke', 'xof', 'kdf', 'key-agree', 'kem', 'ae', 'combiner', 'other', 'unknown'],
mode: ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr', 'other', 'unknown'],
padding: ['pkcs5', 'pkcs7', 'pkcs1v15', 'oaep', 'raw', 'other', 'unknown'],
cryptoFunctions: ['generate', 'keygen', 'encrypt', 'decrypt', 'digest', 'tag', 'keyderive', 'sign', 'verify', 'encapsulate', 'decapsulate', 'other', 'unknown'],
protocolProperties: ['type', 'version', 'cipherSuites', 'ikev2TransformTypes', 'cryptoRefArray'],
protocolType: ['tls', 'ssh', 'ipsec', 'ike', 'sstp', 'wpa', 'other', 'unknown'],
certificateProperties: ['subjectName', 'issuerName', 'notValidBefore', 'notValidAfter', 'signatureAlgorithmRef', 'subjectPublicKeyRef', 'certificateFormat', 'certificateExtension'],
relatedCryptoMaterialProperties: ['type', 'id', 'state', 'algorithmRef', 'creationDate', 'activationDate', 'updateDate', 'expirationDate', 'value', 'size', 'format', 'securedBy'],
materialType: ['private-key', 'public-key', 'secret-key', 'key', 'ciphertext', 'signature', 'digest', 'initialization-vector', 'nonce', 'seed', 'salt', 'shared-secret', 'tag', 'additional-data', 'password', 'credential', 'token', 'other', 'unknown'],
evidence: ['identity', 'occurrences', 'callstack', 'licenses', 'copyright'],
occurrence: ['bom-ref', 'location', 'line', 'offset', 'symbol', 'additionalContext'],
};
export function valideazaCbom(bom) {
const err = [];
const e = (m) => err.push(m);
const chei = (o, perm, unde) => { for (const k of Object.keys(o)) if (!perm.includes(k)) e(`${unde}: cheie nepermisa "${k}"`); };
const sir = (v, unde) => { if (typeof v !== 'string' || !v.length) e(`${unde}: trebuie sir nevid`); };
const dataIso = (v, unde) => { if (typeof v !== 'string' || Number.isNaN(Date.parse(v)) || !/^\d{4}-\d{2}-\d{2}T/.test(v)) e(`${unde}: nu e date-time`); };
chei(bom, E.bom, 'bom');
if (bom.bomFormat !== 'CycloneDX') e('bomFormat');
if (bom.specVersion !== '1.6') e('specVersion');
if (!/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(bom.serialNumber || '')) e(`serialNumber ${bom.serialNumber}`);
if (!Number.isInteger(bom.version) || bom.version < 1) e('version');
const md = bom.metadata || {};
if (md.timestamp !== undefined) dataIso(md.timestamp, 'metadata.timestamp');
if (!md.tools || !Array.isArray(md.tools.components) || !md.tools.components.length) e('metadata.tools.components');
else for (const t of md.tools.components) { sir(t.name, 'tool.name'); sir(t.version, 'tool.version'); if (!E.componentType.includes(t.type)) e('tool.type'); }
if (!Array.isArray(bom.components)) { e('components nu e lista'); return err; }
const refs = new Set();
for (const [i, c] of bom.components.entries()) {
const u = `components[${i}]`;
chei(c, E.component, u);
if (!E.componentType.includes(c.type)) e(`${u}.type ${c.type}`);
sir(c.name, `${u}.name`);
sir(c['bom-ref'], `${u}.bom-ref`);
if (refs.has(c['bom-ref'])) e(`${u}: bom-ref dublat ${c['bom-ref']}`);
refs.add(c['bom-ref']);
if (c.properties) for (const p of c.properties) { sir(p.name, `${u}.properties.name`); if (typeof p.value !== 'string') e(`${u}.properties.value`); }
if (c.type === 'cryptographic-asset') {
const cp = c.cryptoProperties;
if (!cp) { e(`${u}: cryptoProperties lipsa`); continue; }
chei(cp, E.cryptoProperties, `${u}.cryptoProperties`);
if (!E.assetType.includes(cp.assetType)) e(`${u}.assetType ${cp.assetType}`);
if (cp.oid !== undefined && !/^[0-2](\.\d+)+$/.test(cp.oid)) e(`${u}.oid ${cp.oid}`);
if (cp.assetType === 'algorithm') {
const ap = cp.algorithmProperties;
if (!ap) { e(`${u}: algorithmProperties lipsa`); continue; }
chei(ap, E.algorithmProperties, `${u}.algorithmProperties`);
if (!E.primitive.includes(ap.primitive)) e(`${u}.primitive ${ap.primitive}`);
if (ap.parameterSetIdentifier !== undefined) sir(ap.parameterSetIdentifier, `${u}.parameterSetIdentifier`);
if (ap.curve !== undefined) sir(ap.curve, `${u}.curve`);
if (ap.mode !== undefined && !E.mode.includes(ap.mode)) e(`${u}.mode ${ap.mode}`);
if (ap.padding !== undefined && !E.padding.includes(ap.padding)) e(`${u}.padding ${ap.padding}`);
if (ap.cryptoFunctions !== undefined && (!Array.isArray(ap.cryptoFunctions) || ap.cryptoFunctions.some((f) => !E.cryptoFunctions.includes(f)))) e(`${u}.cryptoFunctions`);
if (ap.classicalSecurityLevel !== undefined && (!Number.isInteger(ap.classicalSecurityLevel) || ap.classicalSecurityLevel < 0)) e(`${u}.classicalSecurityLevel`);
if (ap.nistQuantumSecurityLevel !== undefined && (!Number.isInteger(ap.nistQuantumSecurityLevel) || ap.nistQuantumSecurityLevel < 0 || ap.nistQuantumSecurityLevel > 6)) e(`${u}.nistQuantumSecurityLevel`);
} else if (cp.assetType === 'protocol') {
const pp = cp.protocolProperties || {};
chei(pp, E.protocolProperties, `${u}.protocolProperties`);
if (!E.protocolType.includes(pp.type)) e(`${u}.protocolProperties.type`);
if (pp.version !== undefined) sir(pp.version, `${u}.protocolProperties.version`);
} else if (cp.assetType === 'certificate') {
const cc = cp.certificateProperties || {};
chei(cc, E.certificateProperties, `${u}.certificateProperties`);
for (const k of ['notValidBefore', 'notValidAfter']) if (cc[k] !== undefined) dataIso(cc[k], `${u}.${k}`);
} else if (cp.assetType === 'related-crypto-material') {
const rm = cp.relatedCryptoMaterialProperties || {};
chei(rm, E.relatedCryptoMaterialProperties, `${u}.relatedCryptoMaterialProperties`);
if (!E.materialType.includes(rm.type)) e(`${u}.relatedCryptoMaterialProperties.type`);
if (rm.size !== undefined && !Number.isInteger(rm.size)) e(`${u}.size`);
if (rm.value !== undefined) e(`${u}: valoarea materialului nu are voie sa apara`);
}
}
if (!c.evidence || !Array.isArray(c.evidence.occurrences) || !c.evidence.occurrences.length) { e(`${u}: evidence.occurrences lipsa`); continue; }
chei(c.evidence, E.evidence, `${u}.evidence`);
for (const o of c.evidence.occurrences) {
chei(o, E.occurrence, `${u}.occurrence`);
sir(o.location, `${u}.occurrence.location`);
if (o.line !== undefined && (!Number.isInteger(o.line) || o.line < 1)) e(`${u}.occurrence.line`);
}
}
return err;
}
proba('cbom: structura CycloneDX 1.6 (campuri obligatorii, tipuri, enumerari)', () => {
const rez = scan(FIX);
for (const det of [false, true]) {
const bom = inv.buildCbom(rez, { deterministic: det });
const err = valideazaCbom(bom);
asigura(err.length === 0, `erori (${det ? 'determinist' : 'normal'}): ${err.slice(0, 5).join('; ')}`);
asigura(det ? bom.metadata.timestamp === undefined : typeof bom.metadata.timestamp === 'string', 'timestamp');
}
const bom = inv.buildCbom(rez);
const tipuri = new Set(bom.components.map((c) => (c.cryptoProperties ? c.cryptoProperties.assetType : c.type)));
for (const t of ['algorithm', 'protocol', 'certificate', 'related-crypto-material', 'library']) asigura(tipuri.has(t), `lipseste tipul ${t}`);
// controlul negativ al validatorului: o forma stricata trebuie sa fie refuzata
const rau = JSON.parse(JSON.stringify(bom));
rau.components[0].cryptoProperties = { assetType: 'algorithm', algorithmProperties: { primitive: 'magic' } };
rau.components[1]['bom-ref'] = rau.components[2]['bom-ref'];
rau.specVersion = '1.5';
asigura(valideazaCbom(rau).length >= 3, 'validatorul nu refuza un CBOM stricat');
});
proba('cbom: bom-ref unice si stabile intre doua rulari', () => {
const a = inv.buildCbom(inv.scan(FIX), { deterministic: true });
const b = inv.buildCbom(inv.scan(FIX), { deterministic: true });
const ra = a.components.map((c) => c['bom-ref']);
asigura(new Set(ra).size === ra.length, `bom-ref dublate: ${ra.length - new Set(ra).size}`);
asigura(JSON.stringify(a) === JSON.stringify(b), 'doua rulari deterministe difera');
const gasiri = inv.scan(FIX).findings.length;
const occ = a.components.filter((c) => c.type === 'cryptographic-asset').reduce((s, c) => s + c.evidence.occurrences.length, 0);
asigura(occ === gasiri, `occurrences ${occ} != gasiri ${gasiri}`);
});
proba('cbom: fiecare occurrence arata un fisier care exista, cu o linie din fisier', () => {
const bom = inv.buildCbom(scan(FIX));
let n = 0;
for (const c of bom.components) {
for (const o of c.evidence.occurrences) {
const f = join(FIX, o.location);
asigura(existsSync(f) && statSync(f).isFile(), `nu exista: ${o.location}`);
const linii = readFileSync(f, 'utf8').split('\n').length;
asigura(o.line >= 1 && o.line <= linii, `${o.location}: linia ${o.line} din ${linii}`);
n++;
}
}
asigura(n > 50, `prea putine occurrences: ${n}`);
});
// --- CLI ---
function cli(args) {
return spawnSync(process.execPath, [CALE_MODUL, ...args], { encoding: 'utf8', timeout: 60000 });
}
proba('cli: --fail-on vulnerable iese 1 pe cod vulnerabil si 0 pe cod fara criptografie', () => {
const r = cli(['scan', join(FIX, 'js'), '--fail-on', 'vulnerable', '--summary']);
asigura(r.status === 1 && /quantum-vulnerable finding/.test(r.stderr), `status ${r.status}, stderr ${r.stderr.slice(0, 200)}`);
const d = mkdtempSync(join(tmpdir(), 'ci-cli-'));
try {
copyFileSync(join(FIX, 'negative', 'fara-cripto.js'), join(d, 'fara-cripto.js'));
const r0 = cli(['scan', d, '--fail-on', 'vulnerable', '--summary']);
asigura(r0.status === 0, `fara criptografie: status ${r0.status}`);
} finally { rmSync(d, { recursive: true, force: true }); }
const r2 = cli(['scan', join(FIX, 'js'), '--fail-on', 'nimic']);
asigura(r2.status === 2, `valoare --fail-on invalida: status ${r2.status}`);
const r3 = cli(['scan', join(FIX, 'js'), '--fail-on', 'weak', '--summary']);
asigura(r3.status === 1 && /weak-now finding/.test(r3.stderr), `--fail-on weak: status ${r3.status}`);
});
proba('cli: --summary spune cate fisiere nu au fost citite si de ce', () => {
const r = cli(['scan', FIX, '--summary', '--max-file-bytes', '400']);
asigura(r.status === 0, `status ${r.status} ${r.stderr}`);
const m = /Not read: (\d+) binary, (\d+) too large \(> 400 bytes\), (\d+) over the file limit/.exec(r.stdout);
asigura(m && m[1] === '1' && Number(m[2]) >= 1 && m[3] === '0', `randul "Not read": ${m ? m[0] : 'lipsa'}`);
asigura(/e\.g\. binary: binar\/modul\.js/.test(r.stdout), 'exemplul binar lipseste din rezumat');
const rj = cli(['scan', join(FIX, 'go'), '--deterministic']);
const bom = JSON.parse(rj.stdout);
asigura(bom.specVersion === '1.6' && valideazaCbom(bom).length === 0, 'CBOM-ul de pe stdout nu e valid');
});
proba('rezumat: caracterele de control din numele de fisiere nu ajung in terminal', () => {
const rez = scan(FIX);
const i = rez.findings.findIndex((g) => g.classification === 'quantum-vulnerable');
asigura(i >= 0, 'controlul pozitiv: fixturile trebuie sa aiba o gasire vulnerabila');
const rau = 'x\u001b]0;titlu\u0007\u001b[31mrosu.js';
const t = inv.renderSummary({ ...rez, findings: rez.findings.map((g, k) => (k === i ? { ...g, file: rau } : g)) });
asigura(t.includes('x?]0;titlu??[31mrosu.js'), 'numele fisierului trebuia sa apara, cu caracterele de control inlocuite');
asigura(!/[\u0000-\u0009\u000b-\u001f\u007f]/.test(t), 'un caracter de control a ajuns in rezumat');
});
proba('rezumat: numaratoarea pe clase egala cu gasirile', () => {
const rez = scan(FIX);
const c = inv.countByClass(rez.findings);
const s = Object.values(c).reduce((a, b) => a + b, 0);
asigura(s === rez.findings.length, `suma claselor ${s}, gasiri ${rez.findings.length}`);
const t = inv.renderSummary(rez);
for (const [k, v] of Object.entries(c)) asigura(new RegExp(`${k}\\s+${v}\\b`).test(t), `rezumatul nu arata ${k} ${v}`);
});
// --- rularea ---
const rezultate = {};
const erori = {};
for (const p of probe) {
try { await p.fn(); rezultate[p.nume] = 'VERDE'; } catch (err) { rezultate[p.nume] = 'ROSU'; erori[p.nume] = String(err && err.message || err); }
}
const verzi = Object.values(rezultate).filter((x) => x === 'VERDE').length;
if (CA_JSON) {
process.stdout.write(JSON.stringify({ suita: 'crypto-inventory', modul: CALE_MODUL, rulate: probe.length, verzi, rosii: probe.length - verzi, rezultate, erori }) + '\n');
} else {
for (const p of probe) process.stdout.write(`${rezultate[p.nume].padEnd(6)} ${p.nume}${erori[p.nume] ? `\n ${erori[p.nume]}` : ''}\n`);
process.stdout.write(`\n${verzi} din ${probe.length} probe verzi (modul: ${CALE_MODUL})\n`);
}
process.exitCode = verzi === probe.length ? 0 : 1;

View File

@ -45,6 +45,10 @@ the root. Set exactly one of `AERE_KMS_ROOT_KEY` / `AERE_KMS_ROOT_HSM`; both is
AERE_HSM_PIN=... node hsm-radacina.mjs sigileaza --modul /usr/lib/softhsm/libsofthsm2.so --token aere-kms --id 0a --iesire root-hsm.json --nou
AERE_KMS_ROOT_HSM=root-hsm.json AERE_HSM_MODULE=/usr/lib/softhsm/libsofthsm2.so AERE_HSM_PIN=... AERE_KMS_TOKEN=... node server.mjs
The tool's command words are Romanian: `sigileaza` seals (`--modul` the PKCS#11 library, `--iesire` the output file, never
overwritten, `--nou` a new random root instead of `AERE_KMS_ROOT_KEY`), and `verifica <file>` checks that a sealed root opens
with this HSM and PIN (it also needs `AERE_HSM_MODULE`). It prints `ok:` or `refused: <CODE>: <reason>`.
The sealed file does not choose what the process loads or sends: the PKCS#11 library comes from the process
configuration (`AERE_HSM_MODULE`, an absolute path) and must be exactly the one recorded in the file, checked
before any HSM tool runs (`HSM_MODULE_NOT_ALLOWED` otherwise); the PIN is always read from `AERE_HSM_PIN`, and a

View File

@ -135,8 +135,8 @@ async function cli(argv) {
console.log(`sealed root written to ${iesire} (${FORMAT_HSM}, token ${sig.token}, key id ${sig.idCheie})`);
} else if (cmd === 'verifica') {
const r = deschideRadacina(JSON.parse(fs.readFileSync(rest[0], 'utf8'))); r.fill(0);
console.log('DA: the sealed root opens with this HSM and PIN');
console.log('ok: the sealed root opens with this HSM and PIN');
} else { console.error('usage: node hsm-radacina.mjs sigileaza --modul <lib> --token <label> --id <hex> --iesire <file> [--nou] | verifica <file> (PIN in AERE_HSM_PIN; verifica also needs AERE_HSM_MODULE)'); process.exitCode = 2; }
} catch (e) { console.error(`NU: ${e.code || 'ERROR'}: ${e.message}`); process.exitCode = 1; }
} catch (e) { console.error(`refused: ${e.code || 'ERROR'}: ${e.message}`); process.exitCode = 1; }
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) cli(process.argv.slice(2));

View File

@ -34,7 +34,7 @@ export function int(v) {
if (Buffer.isBuffer(v)) b = v;
else {
let x = BigInt(v);
if (x < 0n) throw new Error('DER: numai intregi nenegativi');
if (x < 0n) throw new Error('DER: only non-negative integers');
const a = [];
do { a.unshift(Number(x & 0xffn)); x >>= 8n; } while (x > 0n);
b = Buffer.from(a);
@ -54,9 +54,9 @@ function base128(x) {
}
export function oid(dotted) {
const p = String(dotted).split('.').map((x) => { if (!/^\d+$/.test(x)) throw new Error('DER: OID cu componenta nevalida: ' + dotted); return BigInt(x); });
if (p.length < 2) throw new Error('DER: OID prea scurt');
if (p[0] > 2n || (p[0] < 2n && p[1] > 39n)) throw new Error('DER: OID cu arc nevalid: ' + dotted);
const p = String(dotted).split('.').map((x) => { if (!/^\d+$/.test(x)) throw new Error('DER: OID with an invalid component: ' + dotted); return BigInt(x); });
if (p.length < 2) throw new Error('DER: OID too short');
if (p[0] > 2n || (p[0] < 2n && p[1] > 39n)) throw new Error('DER: OID with an invalid arc: ' + dotted);
// [A10] primul subidentificator poarta impreuna arcul si al doilea numar (40*arc + al doilea) si, ca oricare altul, se scrie in baza 128
// pe mai multi octeti cand trece de 127: 2.999 e 88 37, nu un singur octet trunchiat. Pe arcul 2 al doilea numar nu are limita.
const out = base128(p[0] * 40n + p[1]);
@ -77,23 +77,23 @@ export function time(d) {
/** Un element: { tag, start, hdr, len, end, raw (tot TLV-ul), content } peste acelasi Buffer. */
export function read(buf, off = 0) {
if (off + 2 > buf.length) throw new Error('DER: trunchiat la antet');
if (off + 2 > buf.length) throw new Error('DER: truncated in the header');
const tag = buf[off];
if ((tag & 0x1f) === 0x1f) throw new Error('DER: eticheta cu forma lunga nesuportata');
if ((tag & 0x1f) === 0x1f) throw new Error('DER: long-form tag not supported');
let l = buf[off + 1], hdr = 2;
if (l === 0x80) throw new Error('DER: lungime nedefinita (BER), refuzata');
if (l === 0x80) throw new Error('DER: indefinite length (BER) refused');
if (l & 0x80) {
const n = l & 0x7f;
if (n === 0 || n > 4) throw new Error('DER: lungime pe ' + n + ' octeti');
if (off + 2 + n > buf.length) throw new Error('DER: trunchiat la lungime');
if (buf[off + 2] === 0) throw new Error('DER: lungime cu zero initial (necanonica)');
if (n === 0 || n > 4) throw new Error('DER: length in ' + n + ' bytes');
if (off + 2 + n > buf.length) throw new Error('DER: truncated in the length');
if (buf[off + 2] === 0) throw new Error('DER: length with a leading zero (non-canonical)');
l = 0;
for (let i = 0; i < n; i++) l = l * 256 + buf[off + 2 + i];
if (l < 0x80) throw new Error('DER: lungime scurta scrisa in forma lunga (necanonica)');
if (l < 0x80) throw new Error('DER: short length written in long form (non-canonical)');
hdr = 2 + n;
}
const end = off + hdr + l;
if (end > buf.length) throw new Error('DER: continut trunchiat');
if (end > buf.length) throw new Error('DER: truncated content');
return { tag, start: off, hdr, len: l, end, raw: buf.subarray(off, end), content: buf.subarray(off + hdr, end) };
}
@ -108,26 +108,26 @@ export function children(el) {
/** Decodeaza un document DER intreg si refuza octetii in plus. */
export function parse(buf) {
const el = read(buf, 0);
if (el.end !== buf.length) throw new Error('DER: ' + (buf.length - el.end) + ' octeti dupa structura');
if (el.end !== buf.length) throw new Error('DER: ' + (buf.length - el.end) + ' bytes after the structure');
return el;
}
export function expect(el, tag, what) {
if (el.tag !== tag) throw new Error(`DER: ${what}: eticheta 0x${el.tag.toString(16)}, asteptat 0x${tag.toString(16)}`);
if (el.tag !== tag) throw new Error(`DER: ${what}: tag 0x${el.tag.toString(16)}, expected 0x${tag.toString(16)}`);
return el;
}
export function oidToString(el) {
expect(el, TAG.OID, 'OID');
const b = el.content;
if (b.length === 0) throw new Error('DER: OID gol');
if (b[b.length - 1] & 0x80) throw new Error('DER: OID trunchiat');
if (b.length === 0) throw new Error('DER: empty OID');
if (b[b.length - 1] & 0x80) throw new Error('DER: truncated OID');
// [A10] intai se citesc TOTI subidentificatorii in baza 128 (si primul poate avea mai multi octeti), abia apoi primul se desparte in
// arc si al doilea numar: sub 80 arcul e catul impartirii la 40, de la 80 in sus arcul e 2 (X.690 8.19.4). Un octet 0x80 la inceputul
// unui subidentificator e o codificare necanonica si se refuza.
const subs = []; let v = 0n, inceput = true;
for (let i = 0; i < b.length; i++) {
if (inceput && b[i] === 0x80) throw new Error('DER: OID necanonic');
if (inceput && b[i] === 0x80) throw new Error('DER: non-canonical OID');
v = (v << 7n) | BigInt(b[i] & 0x7f); inceput = false;
if ((b[i] & 0x80) === 0) { subs.push(v); v = 0n; inceput = true; }
}
@ -139,9 +139,9 @@ export function oidToString(el) {
export function intToBigInt(el) {
expect(el, TAG.INTEGER, 'INTEGER');
const b = el.content;
if (b.length === 0) throw new Error('DER: INTEGER gol');
if (b.length > 1 && ((b[0] === 0 && (b[1] & 0x80) === 0) || (b[0] === 0xff && (b[1] & 0x80)))) throw new Error('DER: INTEGER necanonic');
if (b[0] & 0x80) throw new Error('DER: INTEGER negativ');
if (b.length === 0) throw new Error('DER: empty INTEGER');
if (b.length > 1 && ((b[0] === 0 && (b[1] & 0x80) === 0) || (b[0] === 0xff && (b[1] & 0x80)))) throw new Error('DER: non-canonical INTEGER');
if (b[0] & 0x80) throw new Error('DER: negative INTEGER');
return BigInt('0x' + (b.toString('hex') || '0'));
}
@ -155,11 +155,11 @@ export function timeToDate(el) {
if (el.tag === TAG.GEN_TIME && (m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(s))) {
return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]));
}
throw new Error('DER: timp in forma nepermisa de RFC 5280: ' + s);
throw new Error('DER: time in a form RFC 5280 does not allow: ' + s);
}
export function bitString(el) {
expect(el, TAG.BIT_STRING, 'BIT STRING');
if (el.content.length === 0) throw new Error('DER: BIT STRING gol');
if (el.content.length === 0) throw new Error('DER: empty BIT STRING');
return { unused: el.content[0], bytes: el.content.subarray(1) };
}

View File

@ -243,7 +243,7 @@ function parseExtensions(seqEl, what) {
export function parseCert(der) {
const top = D.parse(der);
D.expect(top, D.TAG.SEQUENCE, 'certificat');
D.expect(top, D.TAG.SEQUENCE, 'certificate');
const [tbsEl, algEl, sigEl, ...rest] = D.children(top);
if (rest.length || !sigEl) throw new PkiError('DER', 'a certificate has exactly three parts');
const t = D.children(D.expect(tbsEl, D.TAG.SEQUENCE, 'tbsCertificate'));

View File

@ -32,7 +32,7 @@ const PLANTARI = [
['extensia critica necunoscuta ignorata', 'pki.mjs', ' for (const [id, x] of exts) if (x.critical && !EXT_STIUTE.has(id))', " for (const [id, x] of exts) if (x.critical && !EXT_STIUTE.has(id) && Boolean(Number('0')))", 'extensie critica necunoscuta'],
['scopul (EKU) pe frunza ignorat', 'pki.mjs', ' if (L.eku && !L.eku.has(OID[purpose]))', " if (L.eku && !L.eku.has(OID[purpose]) && Boolean(Number('0')))", 'scopul'],
['cheia privata scrisa in clar (PKCS#8)', 'pki.mjs', ' return pem(KEY_LABEL, D.seq(header, D.octets(tag), D.octets(ct)));', " return key.export({ type: 'pkcs8', format: 'pem' });", 'linia de comanda'],
['DER necanonic acceptat', 'der.mjs', " if (l < 0x80) throw new Error('DER: lungime scurta scrisa in forma lunga (necanonica)');", " if (l < 0x80 && Boolean(Number('0'))) throw new Error('DER: lungime scurta scrisa in forma lunga (necanonica)');", 'forma'],
['DER necanonic acceptat', 'der.mjs', " if (l < 0x80) throw new Error('DER: short length written in long form (non-canonical)');", " if (l < 0x80 && Boolean(Number('0'))) throw new Error('DER: short length written in long form (non-canonical)');", 'forma'],
// revizuirea din 2026-09-25
['[A1] scrypt coborat la N=2^10', 'pki.mjs', 'N: 2 ** 17,', 'N: 2 ** 10,', 'sigilarea cheii'],
['[A1] politica de parola redusa la 12 caractere (accepta aaaaaaaaaaaa)', 'pki.mjs', 'export function checkPassphrase(p) {', "export function checkPassphrase(p) { if (typeof p === 'string' && p.length >= 12 && Boolean(Number('1'))) return p;", 'politica de parola'],

View File

@ -265,8 +265,8 @@ await test('forma: algoritm exterior diferit de cel interior (ALG_MISMATCH), par
const n = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), leaf.subarray(4, j), withNull, leaf.subarray(j + src.length)]);
n.writeUInt16BE(lung, 2);
e = null; try { P.parseCert(n); } catch (x) { e = x; } eq(e && e.code, 'ALG_PARAMS', 'NULL pe ML-DSA');
e = null; try { P.parseCert(Buffer.concat([leaf, Buffer.from([0])])); } catch (x) { e = x; } has(e && e.message, 'octeti dupa structura', 'coada');
const nc = Buffer.from([0x30, 0x81, 0x03, 0x02, 0x01, 0x05]); e = null; try { P.parseCert(nc); } catch (x) { e = x; } has(e && e.message, 'necanonica', 'lungime lunga pentru una scurta');
e = null; try { P.parseCert(Buffer.concat([leaf, Buffer.from([0])])); } catch (x) { e = x; } has(e && e.message, 'bytes after the structure', 'coada');
const nc = Buffer.from([0x30, 0x81, 0x03, 0x02, 0x01, 0x05]); e = null; try { P.parseCert(nc); } catch (x) { e = x; } has(e && e.message, 'non-canonical', 'lungime lunga pentru una scurta');
});
// [A5] SPKI
const cuSpkiRau = (der) => { const c = P.parseCert(der); const [algEl] = Dm.children(Dm.parse(c.spki)); const s = Dm.seq(algEl.raw, Dm.bits(Buffer.alloc(10, 7))); return { c, tbs: Dm.seq(...Dm.children(Dm.parse(c.tbs)).map((k) => (k.raw.equals(c.spki) ? s : k.raw))) }; };
@ -306,8 +306,8 @@ await test('OID [A10]: 2.999 se codifica 06 02 88 37 si se decodifica inapoi; 2.
const r = ossl(['asn1parse', '-genstr', 'OID:' + o, '-noout', '-out', f('oid.der')]); eq(r.code, 0, 'openssl genstr ' + o + ' ' + r.out);
eq(fs.readFileSync(f('oid.der')).toString('hex'), noi.toString('hex'), 'octetii OpenSSL pentru ' + o);
}
let e = null; try { Dm.oidToString(Dm.parse(Buffer.from('06028001', 'hex'))); } catch (x) { e = x; } has(e && e.message, 'necanonic', '0x80 initial');
e = null; try { Dm.oid('1.40'); } catch (x) { e = x; } has(e && e.message, 'arc nevalid', '1.40');
let e = null; try { Dm.oidToString(Dm.parse(Buffer.from('06028001', 'hex'))); } catch (x) { e = x; } has(e && e.message, 'non-canonical', '0x80 initial');
e = null; try { Dm.oid('1.40'); } catch (x) { e = x; } has(e && e.message, 'invalid arc', '1.40');
const o = ossl(['req', '-x509', '-newkey', 'mldsa44', '-keyout', f('c9.key'), '-out', f('c9.pem'), '-days', '2', '-nodes', '-subj', '/CN=crit999',
'-addext', 'basicConstraints=critical,CA:TRUE', '-addext', 'keyUsage=critical,keyCertSign,cRLSign,digitalSignature', '-addext', '2.999.1=critical,ASN1:UTF8String:x']);
eq(o.code, 0, 'req ' + o.out);