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:
parent
7d814d1bb8
commit
0f081f0847
@ -1,6 +1,6 @@
|
|||||||
# Aere Quantum
|
# 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.
|
Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry.
|
||||||
|
|
||||||
| component | what it does |
|
| 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-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates |
|
||||||
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
|
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
|
||||||
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
|
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
|
||||||
|
| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, PEM blocks, dependency manifests): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
|
||||||
|
|
||||||
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
|
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-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-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`) |
|
| 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
|
## 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
202
crypto-inventory/README.md
Normal 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.
|
||||||
98
crypto-inventory/inventar.mjs
Normal file
98
crypto-inventory/inventar.mjs
Normal 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));
|
||||||
|
}
|
||||||
418
crypto-inventory/lib/catalog.mjs
Normal file
418
crypto-inventory/lib/catalog.mjs
Normal 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)$/;
|
||||||
158
crypto-inventory/lib/cbom.mjs
Normal file
158
crypto-inventory/lib/cbom.mjs
Normal 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;
|
||||||
|
}
|
||||||
253
crypto-inventory/lib/context.mjs
Normal file
253
crypto-inventory/lib/context.mjs
Normal 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;
|
||||||
|
}
|
||||||
207
crypto-inventory/lib/detect-go.mjs
Normal file
207
crypto-inventory/lib/detect-go.mjs
Normal 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}"` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
251
crypto-inventory/lib/detect-java.mjs
Normal file
251
crypto-inventory/lib/detect-java.mjs
Normal 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;
|
||||||
|
}
|
||||||
510
crypto-inventory/lib/detect-js.mjs
Normal file
510
crypto-inventory/lib/detect-js.mjs
Normal 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;
|
||||||
|
}
|
||||||
208
crypto-inventory/lib/detect-py.mjs
Normal file
208
crypto-inventory/lib/detect-py.mjs
Normal 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.` };
|
||||||
|
}
|
||||||
230
crypto-inventory/lib/lexer.mjs
Normal file
230
crypto-inventory/lib/lexer.mjs
Normal 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 };
|
||||||
|
}
|
||||||
134
crypto-inventory/lib/manifeste.mjs
Normal file
134
crypto-inventory/lib/manifeste.mjs
Normal 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 };
|
||||||
|
}
|
||||||
103
crypto-inventory/lib/pem.mjs
Normal file
103
crypto-inventory/lib/pem.mjs
Normal 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}-----` });
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crypto-inventory/lib/rezumat.mjs
Normal file
76
crypto-inventory/lib/rezumat.mjs
Normal 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');
|
||||||
|
}
|
||||||
141
crypto-inventory/lib/scan.mjs
Normal file
141
crypto-inventory/lib/scan.mjs
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
80
crypto-inventory/test/control-negativ-timp.mjs
Normal file
80
crypto-inventory/test/control-negativ-timp.mjs
Normal 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;
|
||||||
242
crypto-inventory/test/control-negativ.mjs
Normal file
242
crypto-inventory/test/control-negativ.mjs
Normal 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;
|
||||||
|
}
|
||||||
87
crypto-inventory/test/echivalenta-0.1.0.mjs
Normal file
87
crypto-inventory/test/echivalenta-0.1.0.mjs
Normal 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;
|
||||||
BIN
crypto-inventory/test/fixturi/binar/modul.js
Normal file
BIN
crypto-inventory/test/fixturi/binar/modul.js
Normal file
Binary file not shown.
30
crypto-inventory/test/fixturi/go/crypto.go
Normal file
30
crypto-inventory/test/fixturi/go/crypto.go
Normal 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
|
||||||
|
}
|
||||||
36
crypto-inventory/test/fixturi/java/Crypto.java
Normal file
36
crypto-inventory/test/fixturi/java/Crypto.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crypto-inventory/test/fixturi/js/biblioteci.ts
Normal file
19
crypto-inventory/test/fixturi/js/biblioteci.ts
Normal 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 };
|
||||||
|
}
|
||||||
31
crypto-inventory/test/fixturi/js/node-crypto.mjs
Normal file
31
crypto-inventory/test/fixturi/js/node-crypto.mjs
Normal 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];
|
||||||
|
}
|
||||||
18
crypto-inventory/test/fixturi/js/tls.cjs
Normal file
18
crypto-inventory/test/fixturi/js/tls.cjs
Normal 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 };
|
||||||
15
crypto-inventory/test/fixturi/js/webcrypto.js
Normal file
15
crypto-inventory/test/fixturi/js/webcrypto.js
Normal 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];
|
||||||
|
}
|
||||||
22
crypto-inventory/test/fixturi/limite/mare.js
Normal file
22
crypto-inventory/test/fixturi/limite/mare.js
Normal 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
|
||||||
2
crypto-inventory/test/fixturi/limite/mic.js
Normal file
2
crypto-inventory/test/fixturi/limite/mic.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
export const h = crypto.createHash('sha256');
|
||||||
9
crypto-inventory/test/fixturi/manifeste/go.mod
Normal file
9
crypto-inventory/test/fixturi/manifeste/go.mod
Normal 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
|
||||||
|
)
|
||||||
12
crypto-inventory/test/fixturi/manifeste/package.json
Normal file
12
crypto-inventory/test/fixturi/manifeste/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
crypto-inventory/test/fixturi/manifeste/pom.xml
Normal file
14
crypto-inventory/test/fixturi/manifeste/pom.xml
Normal 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>
|
||||||
3
crypto-inventory/test/fixturi/manifeste/requirements.txt
Normal file
3
crypto-inventory/test/fixturi/manifeste/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
requests==2.32.3
|
||||||
|
cryptography==43.0.1
|
||||||
|
PyJWT[crypto]>=2.8
|
||||||
11
crypto-inventory/test/fixturi/material/certificat-proba.pem
Normal file
11
crypto-inventory/test/fixturi/material/certificat-proba.pem
Normal 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-----
|
||||||
@ -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-----
|
||||||
14
crypto-inventory/test/fixturi/negative/Comentarii.java
Normal file
14
crypto-inventory/test/fixturi/negative/Comentarii.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crypto-inventory/test/fixturi/negative/comentarii.go
Normal file
11
crypto-inventory/test/fixturi/negative/comentarii.go
Normal 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)"
|
||||||
|
}
|
||||||
14
crypto-inventory/test/fixturi/negative/comentarii.js
Normal file
14
crypto-inventory/test/fixturi/negative/comentarii.js
Normal 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
|
||||||
|
}
|
||||||
13
crypto-inventory/test/fixturi/negative/docstring.py
Normal file
13
crypto-inventory/test/fixturi/negative/docstring.py
Normal 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
|
||||||
9
crypto-inventory/test/fixturi/negative/fara-cripto.js
Normal file
9
crypto-inventory/test/fixturi/negative/fara-cripto.js
Normal 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));
|
||||||
9
crypto-inventory/test/fixturi/negative/siruri.js
Normal file
9
crypto-inventory/test/fixturi/negative/siruri.js
Normal 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();
|
||||||
|
}
|
||||||
12
crypto-inventory/test/fixturi/negative/variabile.js
Normal file
12
crypto-inventory/test/fixturi/negative/variabile.js
Normal 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];
|
||||||
|
}
|
||||||
32
crypto-inventory/test/fixturi/python/crypto_py.py
Normal file
32
crypto-inventory/test/fixturi/python/crypto_py.py
Normal 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]
|
||||||
72
crypto-inventory/test/proba-timp.mjs
Normal file
72
crypto-inventory/test/proba-timp.mjs
Normal 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;
|
||||||
|
}
|
||||||
486
crypto-inventory/test/proba.mjs
Normal file
486
crypto-inventory/test/proba.mjs
Normal 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;
|
||||||
@ -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_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
|
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
|
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
|
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
|
before any HSM tool runs (`HSM_MODULE_NOT_ALLOWED` otherwise); the PIN is always read from `AERE_HSM_PIN`, and a
|
||||||
|
|||||||
@ -135,8 +135,8 @@ async function cli(argv) {
|
|||||||
console.log(`sealed root written to ${iesire} (${FORMAT_HSM}, token ${sig.token}, key id ${sig.idCheie})`);
|
console.log(`sealed root written to ${iesire} (${FORMAT_HSM}, token ${sig.token}, key id ${sig.idCheie})`);
|
||||||
} else if (cmd === 'verifica') {
|
} else if (cmd === 'verifica') {
|
||||||
const r = deschideRadacina(JSON.parse(fs.readFileSync(rest[0], 'utf8'))); r.fill(0);
|
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; }
|
} 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));
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) cli(process.argv.slice(2));
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export function int(v) {
|
|||||||
if (Buffer.isBuffer(v)) b = v;
|
if (Buffer.isBuffer(v)) b = v;
|
||||||
else {
|
else {
|
||||||
let x = BigInt(v);
|
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 = [];
|
const a = [];
|
||||||
do { a.unshift(Number(x & 0xffn)); x >>= 8n; } while (x > 0n);
|
do { a.unshift(Number(x & 0xffn)); x >>= 8n; } while (x > 0n);
|
||||||
b = Buffer.from(a);
|
b = Buffer.from(a);
|
||||||
@ -54,9 +54,9 @@ function base128(x) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function oid(dotted) {
|
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); });
|
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 prea scurt');
|
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 cu arc nevalid: ' + dotted);
|
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
|
// [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.
|
// 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]);
|
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. */
|
/** Un element: { tag, start, hdr, len, end, raw (tot TLV-ul), content } peste acelasi Buffer. */
|
||||||
export function read(buf, off = 0) {
|
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];
|
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;
|
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) {
|
if (l & 0x80) {
|
||||||
const n = l & 0x7f;
|
const n = l & 0x7f;
|
||||||
if (n === 0 || n > 4) throw new Error('DER: lungime pe ' + n + ' octeti');
|
if (n === 0 || n > 4) throw new Error('DER: length in ' + n + ' bytes');
|
||||||
if (off + 2 + n > buf.length) throw new Error('DER: trunchiat la lungime');
|
if (off + 2 + n > buf.length) throw new Error('DER: truncated in the length');
|
||||||
if (buf[off + 2] === 0) throw new Error('DER: lungime cu zero initial (necanonica)');
|
if (buf[off + 2] === 0) throw new Error('DER: length with a leading zero (non-canonical)');
|
||||||
l = 0;
|
l = 0;
|
||||||
for (let i = 0; i < n; i++) l = l * 256 + buf[off + 2 + i];
|
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;
|
hdr = 2 + n;
|
||||||
}
|
}
|
||||||
const end = off + hdr + l;
|
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) };
|
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. */
|
/** Decodeaza un document DER intreg si refuza octetii in plus. */
|
||||||
export function parse(buf) {
|
export function parse(buf) {
|
||||||
const el = read(buf, 0);
|
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;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expect(el, tag, what) {
|
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;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function oidToString(el) {
|
export function oidToString(el) {
|
||||||
expect(el, TAG.OID, 'OID');
|
expect(el, TAG.OID, 'OID');
|
||||||
const b = el.content;
|
const b = el.content;
|
||||||
if (b.length === 0) throw new Error('DER: OID gol');
|
if (b.length === 0) throw new Error('DER: empty OID');
|
||||||
if (b[b.length - 1] & 0x80) throw new Error('DER: OID trunchiat');
|
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
|
// [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
|
// 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.
|
// unui subidentificator e o codificare necanonica si se refuza.
|
||||||
const subs = []; let v = 0n, inceput = true;
|
const subs = []; let v = 0n, inceput = true;
|
||||||
for (let i = 0; i < b.length; i++) {
|
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;
|
v = (v << 7n) | BigInt(b[i] & 0x7f); inceput = false;
|
||||||
if ((b[i] & 0x80) === 0) { subs.push(v); v = 0n; inceput = true; }
|
if ((b[i] & 0x80) === 0) { subs.push(v); v = 0n; inceput = true; }
|
||||||
}
|
}
|
||||||
@ -139,9 +139,9 @@ export function oidToString(el) {
|
|||||||
export function intToBigInt(el) {
|
export function intToBigInt(el) {
|
||||||
expect(el, TAG.INTEGER, 'INTEGER');
|
expect(el, TAG.INTEGER, 'INTEGER');
|
||||||
const b = el.content;
|
const b = el.content;
|
||||||
if (b.length === 0) throw new Error('DER: INTEGER gol');
|
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: INTEGER necanonic');
|
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: INTEGER negativ');
|
if (b[0] & 0x80) throw new Error('DER: negative INTEGER');
|
||||||
return BigInt('0x' + (b.toString('hex') || '0'));
|
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))) {
|
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]));
|
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) {
|
export function bitString(el) {
|
||||||
expect(el, TAG.BIT_STRING, 'BIT STRING');
|
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) };
|
return { unused: el.content[0], bytes: el.content.subarray(1) };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -243,7 +243,7 @@ function parseExtensions(seqEl, what) {
|
|||||||
|
|
||||||
export function parseCert(der) {
|
export function parseCert(der) {
|
||||||
const top = D.parse(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);
|
const [tbsEl, algEl, sigEl, ...rest] = D.children(top);
|
||||||
if (rest.length || !sigEl) throw new PkiError('DER', 'a certificate has exactly three parts');
|
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'));
|
const t = D.children(D.expect(tbsEl, D.TAG.SEQUENCE, 'tbsCertificate'));
|
||||||
|
|||||||
@ -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'],
|
['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'],
|
['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'],
|
['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
|
// revizuirea din 2026-09-25
|
||||||
['[A1] scrypt coborat la N=2^10', 'pki.mjs', 'N: 2 ** 17,', 'N: 2 ** 10,', 'sigilarea cheii'],
|
['[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'],
|
['[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'],
|
||||||
|
|||||||
@ -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)]);
|
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);
|
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(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');
|
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, 'necanonica', 'lungime lunga pentru una scurta');
|
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
|
// [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))) }; };
|
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);
|
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);
|
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');
|
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, 'arc nevalid', '1.40');
|
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',
|
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']);
|
'-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);
|
eq(o.code, 0, 'req ' + o.out);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user