crypto-inventory 0.4.0: Swift and Kotlin detectors (CryptoKit, CommonCrypto, Security.framework, Android Keystore, Tink, OkHttp, the libsignal Swift and Android APIs), Apple and Android manifests; remediation recipes for Apache and HAProxy measured on real servers, HAProxy precondition fixed

This commit is contained in:
Aere Network 2026-10-18 08:53:13 +03:00
parent e453c0c42d
commit b6ed4d2b71
37 changed files with 1623 additions and 82 deletions

View File

@ -12,13 +12,14 @@ wrote it.
Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a Paths that DO resolve are written relative to a side-by-side checkout of the Aere repositories, so they begin with a
repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`. repository name, for example `aere-contracts/contracts/pqc/AereCryptoRegistry.sol`.
Unresolvable distinct paths in this repository: **11**. Unresolvable distinct paths in this repository: **12**.
- `addresses.txt` cited in: aere-quantum/exposure-report/README.md - `addresses.txt` cited in: aere-quantum/exposure-report/README.md
- `antete.mjs` cited in: aere-quantum/exposure-report/README.md - `antete.mjs` cited in: aere-quantum/exposure-report/README.md
- `btc/antete.mjs` cited in: aere-quantum/exposure-report/README.md - `btc/antete.mjs` cited in: aere-quantum/exposure-report/README.md
- `data/audit.log` cited in: aere-quantum/pq-kms/README.md - `data/audit.log` cited in: aere-quantum/pq-kms/README.md
- `data/root-check.json` cited in: aere-quantum/pq-kms/README.md - `data/root-check.json` cited in: aere-quantum/pq-kms/README.md
- `libs.versions.toml` cited in: aere-quantum/crypto-inventory/README.md
- `plic-aip23.json` cited in: aere-quantum/exposure-report/README.md - `plic-aip23.json` cited in: aere-quantum/exposure-report/README.md
- `proba-client-real.mjs` cited in: aere-quantum/readiness/README.md - `proba-client-real.mjs` cited in: aere-quantum/readiness/README.md
- `raport.json` cited in: aere-quantum/exposure-report/README.md - `raport.json` cited in: aere-quantum/exposure-report/README.md

View File

@ -10,7 +10,7 @@ notarization command of the verification layer and for the agents' x402 wallet (
| [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates | | [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates |
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 | | [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL | | [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, Rust, PEM blocks, dependency manifests including Cargo.toml): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow | | [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Kotlin, Swift, Go, Rust, PEM blocks, dependency manifests including Cargo.toml, Package.swift, Podfile and Gradle): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
| [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log | | [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log |
| [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content | | [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content |
| [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass |
@ -39,11 +39,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 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) | | pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) | | pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
| crypto-inventory | 52/52 (`node test/proba.mjs`, measured 2026-10-18 with the Rust detector, glob imports, libcrux, BoringSSL and the libsignal Java/Node API included); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 25/25 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) | | crypto-inventory | 68/68 (`node test/proba.mjs`, measured 2026-10-18 with the Rust, Swift and Kotlin detectors, glob imports, libcrux, BoringSSL, the libsignal Java/Node/Swift/Android APIs, CryptoKit, CommonCrypto, Security.framework, Android Keystore, Tink and OkHttp included); cost on hostile input 13/13 linear (`node test/proba-timp.mjs`) | 35/35 (`node test/control-negativ.mjs`); cost 5/5 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
| verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) | | verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) |
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | | proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) | | readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
| control-plane | planner 44/44 (2026-10-18: a key agreement or a client-side TLS configured by the application itself is not routed to the TLS gateway; a self-signed trust anchor is blocked, not "automatic through PKI"; test code is LOW with its own scope), command line 10/10, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 30/30 (with the AIP-23 verifier; test-code findings are counted apart and are not "exposed today"), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) | | control-plane | planner 44/44 (2026-10-18: a key agreement or a client-side TLS configured by the application itself is not routed to the TLS gateway; a self-signed trust anchor is blocked, not "automatic through PKI"; test code is LOW with its own scope), command line 10/10, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers (all five server profiles measured end to end on 2026-10-18: nginx, Apache, HAProxy, Node, Go), compliance report 30/30 (with the AIP-23 verifier; test-code findings are counted apart and are not "exposed today"), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
| identity | 46/46 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-10-05 | 53/53 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 29/29 (`node control-negativ-sdjwt.mjs`) | | identity | 46/46 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-10-05 | 53/53 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 29/29 (`node control-negativ-sdjwt.mjs`) |
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 54/54 on 2026-10-05, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository | | agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 54/54 on 2026-10-05, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
| proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) | | proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) |
@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
## Licence ## Licence
MIT, see [LICENSE](LICENSE). Files: 722 (pq-gateway 9, pq-kms 10, pq-pki 6, crypto-inventory 55, verify-layer 12, proof-kinds 3, control-plane 25, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 479, device-identity 11, exposure-report 18, readiness 4). MIT, see [LICENSE](LICENSE). Files: 742 (pq-gateway 9, pq-kms 10, pq-pki 6, crypto-inventory 73, verify-layer 12, proof-kinds 3, control-plane 27, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 479, device-identity 11, exposure-report 18, readiness 4).

View File

@ -74,10 +74,10 @@ problems get operational steps. The **proof**: after you apply it, a new scan ju
required property is measured), `UNRESOLVED`, or `UNMEASURED` (the scan failed, is of another host, or is not from after the time you required property is measured), `UNRESOLVED`, or `UNMEASURED` (the scan failed, is of another host, or is not from after the time you
applied the change: `--applied-at`, otherwise the plan's `generatedAt`; with neither, everything is `UNMEASURED`). A domain in the applied the change: `--applied-at`, otherwise the plan's `generatedAt`; with neither, everything is `UNMEASURED`). A domain in the
plan that is not a host name gets no recipe, so no command to copy is built with it. What is measured by us is written on every plan that is not a host name gets no recipe, so no command to copy is built with it. What is measured by us is written on every
recipe (`measured`): the `node` profile end to end; for nginx, Apache and HAProxy only that OpenSSL 3.5 accepts the group list (the recipe (`measured`): since 2026-10-18 all five profiles end to end, on real servers started from exactly the recipe's fragments
directive comes from their documentation); nginx and Go measured end to end on 2026-10-18 on real servers started from exactly the recipe's (nginx 1.31.7 with OpenSSL 3.5.8/3.5.9, Apache 2.4.69 with OpenSSL 3.5.9 and HAProxy 3.4.7 with OpenSSL 3.5.9, each in a container;
fragments (nginx 1.31.7 with OpenSSL 3.5.8/3.5.9 in a container, Go 1.26.5): hybrid-only client accepted, TLS 1.2 refused, HSTS sent Go 1.26.5; Node earlier): a client offering only the hybrid group is accepted, a TLS 1.2 client is refused, HSTS is sent
(`dovezi/retete-2026-10-18/`); Apache and HAProxy still from their documentation. (`dovezi/retete-2026-10-18/`); not measured: a production server with its full configuration, real browsers as clients.
## 4. Compliance report: `raport-conformitate.mjs` ## 4. Compliance report: `raport-conformitate.mjs`

View File

@ -17,6 +17,24 @@ o strangere de mana reala cu grupurile oferite si, optional, cu versiunea maxima
| scanerul AERE pe server (`scaneaza-local.mjs`, `scaneazaAdresa` pe 127.0.0.1) | `pqKeyExchange X25519MLKEM768, prefersPqWhenOffered true, tls12Accepted false, hsts true`; scor 70 numai din certificatul de proba (autosemnat, 2 zile: `cert-expiring`, `chain-untrusted`) | NEMASURAT cu scanerul (modulul nu e instalat pe rpc2 in forma importabila); sondele si curl-ul acopera aceleasi fapte | | scanerul AERE pe server (`scaneaza-local.mjs`, `scaneazaAdresa` pe 127.0.0.1) | `pqKeyExchange X25519MLKEM768, prefersPqWhenOffered true, tls12Accepted false, hsts true`; scor 70 numai din certificatul de proba (autosemnat, 2 zile: `cert-expiring`, `chain-untrusted`) | NEMASURAT cu scanerul (modulul nu e instalat pe rpc2 in forma importabila); sondele si curl-ul acopera aceleasi fapte |
| curatenie | serverul oprit, binarul sters | container si imagine sterse; RPC-ul local 11 -> 13 ms | | curatenie | serverul oprit, binarul sters | container si imagine sterse; RPC-ul local 11 -> 13 ms |
Ce NU s-a masurat: Apache si HAProxy (nimic pe nicio gazda a noastra; raman "din documentatie"), un nginx de productie cu configuratia Ce NU s-a masurat la 04:31Z: Apache si HAProxy (masurate apoi la 05:18Z-05:25Z, mai jos), un nginx de productie cu configuratia
lui intreaga (aici un singur `server {}` de proba), Go sub Windows numai (Linux NEMASURAT; Go-ul din WSL e 1.22, sub 1.24), clienti lui intreaga (aici un singur `server {}` de proba), Go sub Windows numai (Linux NEMASURAT; Go-ul din WSL e 1.22, sub 1.24), clienti
reali (browsere) spre aceste servere. Jurnalele: `masoara-go.log`, `masoara-nginx.log`. reali (browsere) spre aceste servere. Jurnalele: `masoara-go.log`, `masoara-nginx.log`.
## Apache si HAProxy, masurate 2026-10-18 05:18Z-05:25Z (`masoara-apache-haproxy-rpc2.sh`, `masoara-haproxy2-rpc2.sh`)
Aceleasi conditii: containere pe rpc2 (`httpd:alpine`, `haproxy:alpine`, OpenSSL-ul lui Alpine), publicate NUMAI pe 127.0.0.1, EXACT
fragmentele retetei, aceeasi sonda, curatenie la sfarsit (containere si imagini sterse, RPC-ul local 13 -> 10 ms).
| | Apache (`httpd:alpine`) | HAProxy (`haproxy:alpine`) |
|---|---|---|
| serverul | **Apache/2.4.69 (Unix) OpenSSL/3.5.9** (randul de pornire AH00489 din jurnal, chiar preconditia retetei); `httpd -t`: Syntax OK; `SSLOpenSSLConfCmd Groups X25519MLKEM768:X25519`, `SSLProtocol -all +TLSv1.3`, `Header always set Strict-Transport-Security "max-age=31536000"` intr-un `<VirtualHost *:8447>` | **HAProxy 3.4.7** (2026-10-14), "Built with / Running on SSL library version : OpenSSL 3.5.9"; `haproxy -c -f` cod 0; `ssl-default-bind-curves X25519MLKEM768:X25519` si `ssl-default-bind-options ssl-min-ver TLSv1.3` in `global`, `http-response set-header Strict-Transport-Security "max-age=31536000"` in frontend |
| hibrid singur / clasic singur / hibrid + clasic | reusit / reusit / reusit (TLSv1.3) | reusit / reusit / reusit (TLSv1.3) |
| client TLS 1.2 | **refuzat** `ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION` | **refuzat** `ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION` |
| HSTS | `Strict-Transport-Security: max-age=31536000` | `strict-transport-security: max-age=31536000` prin HAProxy, pe un raspuns venit de la un backend httpd FARA antet (control: curl direct pe backend, fara HSTS) |
Doua lucruri gasite pe drum, despre PROBA si despre RETETA, nu despre servere: (1) prima masurare HAProxy folosea `http-request return` si nu
vedea HSTS, fiindca regulile `http-response` nu se aplica unui raspuns generat intern de HAProxy (numai `http-after-response`); a doua
masurare trece raspunsul unui backend real prin frontend, si antetul apare; (2) comanda de preconditie a retetei `haproxy -vv | grep -i
"running on openssl"` NU prinde nimic pe HAProxy 3.4: randul e "Running on SSL library version : OpenSSL 3.5.9"; reteta spune acum
`grep -i "SSL library version"`. Ramane NEMASURAT: un Apache/HAProxy de productie cu configuratia lui intreaga, clienti reali (browsere).

View File

@ -0,0 +1,92 @@
#!/bin/bash
# masoara-apache-haproxy-rpc2.sh (PE rpc2, copiat cu scp si chemat pe nume): retetele `apache` si `haproxy` ale remedierii
# (cloud-gateway/control-plane/remediere.mjs, GENERATOARE.apache / GENERATOARE.haproxy) masurate pe servere REALE, in containere
# (httpd:alpine, haproxy:alpine, fiecare cu OpenSSL-ul lui Alpine), publicate NUMAI pe 127.0.0.1 (8447 Apache, 8448 HAProxy; nimic pe
# porturile gazdei), cu EXACT fragmentele retetei pentru hndl-exposed + tls12-accepted + hsts-missing:
# apache: SSLOpenSSLConfCmd Groups X25519MLKEM768:X25519 / SSLProtocol -all +TLSv1.3 / Header always set Strict-Transport-Security "max-age=31536000"
# haproxy: ssl-default-bind-curves X25519MLKEM768:X25519 / ssl-default-bind-options ssl-min-ver TLSv1.3 / http-response set-header Strict-Transport-Security "max-age=31536000"
# Preconditiile retetei se citesc cum le cere ea (randul de pornire al lui Apache cu OpenSSL/; `haproxy -vv` "Running on OpenSSL"), si
# comenzile ei de verificare ruleaza in container (`httpd -t` = apachectl configtest; `haproxy -c -f`). Sonde: node 24 de pe rpc2
# (/root/reteta-sonda.mjs: hibrid singur, clasic singur, hibrid+clasic, TLS 1.2 refuzat), HSTS prin curl -k. Containere si imagini sterse
# la sfarsit; RPC-ul local cronometrat inainte si dupa. Iesire 0 = 8 din 8 sonde cum trebuie, 1 altfel.
set -u
D=/root/reteta-ah; rm -rf "$D"; mkdir -p "$D/certs" "$D/apache" "$D/haproxy"; cd "$D" || exit 2
N=/opt/node24/bin/node; S=/root/reteta-sonda.mjs
[ -x "$N" ] || { echo "NEMASURAT: node 24 lipseste la $N"; exit 2; }
[ -f "$S" ] || { echo "NEMASURAT: sonda lipseste la $S"; exit 2; }
rpc() { local t0=$(date +%s%N); curl -s -m 5 -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:8545 | grep -o '"result":"0x[0-9a-f]*"' | head -1; echo " ($(( ($(date +%s%N) - t0) / 1000000 )) ms)"; }
echo "== RPC inainte: $(rpc)"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout certs/privkey.pem -out certs/fullchain.pem -days 2 -subj '/CN=proba.local' > /dev/null 2>&1 && cat certs/privkey.pem certs/fullchain.pem > certs/proba.pem && chmod 644 certs/*.pem && echo "cert ok (autosemnat, 2 zile)"
rele=0
cere() { local nume="$1" ast="$2"; shift 2; out=$("$@" 2>&1); c=$?; if [ "$c" = "$ast" ]; then echo " OK $nume -> cod $c: $(printf '%s' "$out" | cut -c1-170)"; else echo " RAU $nume -> cod $c (asteptat $ast): $(printf '%s' "$out" | cut -c1-170)"; rele=$((rele + 1)); fi; }
sonde() { local port="$1" nume="$2"
cere "$nume: hibrid singur (X25519MLKEM768)" 0 $N $S 127.0.0.1 $port X25519MLKEM768
cere "$nume: clasic singur (X25519, rezerva din reteta)" 0 $N $S 127.0.0.1 $port X25519
cere "$nume: hibrid + clasic (preferinta)" 0 $N $S 127.0.0.1 $port X25519MLKEM768:X25519
cere "$nume: client TLS 1.2 (refuz)" 1 $N $S 127.0.0.1 $port X25519 TLSv1.2
echo " -- $nume: HSTS (curl -k)"; curl -sk -m 10 -D - -o /dev/null https://127.0.0.1:$port/ | grep -iE '^(strict-transport-security|HTTP)' | tr -d '\r' | sed 's/^/ /'; }
# ------------------------------------------------------------------ Apache httpd
echo "== APACHE: imaginea httpd:alpine"; docker pull -q httpd:alpine > /dev/null 2>&1; docker image inspect httpd:alpine --format 'imagine: {{.Id}} {{.Size}} octeti' | cut -c1-90
docker run --rm httpd:alpine httpd -v 2>&1 | head -2 | sed 's/^/ /'
# httpd.conf al imaginii + modulele cerute de reteta (ssl, socache, headers) + gazda virtuala cu EXACT fragmentul retetei
docker run --rm httpd:alpine cat /usr/local/apache2/conf/httpd.conf > apache/httpd.conf 2>/dev/null
sed -i -e 's/^#LoadModule ssl_module/LoadModule ssl_module/' -e 's/^#LoadModule socache_shmcb_module/LoadModule socache_shmcb_module/' -e 's/^#LoadModule headers_module/LoadModule headers_module/' apache/httpd.conf
cat >> apache/httpd.conf <<'EOF'
# ---- proba retetei AERE (remediere.mjs, profilul apache) ----
Listen 8447
<VirtualHost *:8447>
ServerName proba.local
DocumentRoot "/usr/local/apache2/htdocs"
SSLEngine on
SSLCertificateFile /usr/local/apache2/conf/certs/fullchain.pem
SSLCertificateKeyFile /usr/local/apache2/conf/certs/privkey.pem
# EXACT fragmentul retetei:
SSLOpenSSLConfCmd Groups X25519MLKEM768:X25519
SSLProtocol -all +TLSv1.3
Header always set Strict-Transport-Security "max-age=31536000"
</VirtualHost>
EOF
grep -c 'LoadModule ssl_module' apache/httpd.conf | sed 's/^/ LoadModule ssl_module randuri: /'
docker rm -f apache-reteta > /dev/null 2>&1
docker run -d --name apache-reteta -p 127.0.0.1:8447:8447 -v "$D/apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro" -v "$D/certs:/usr/local/apache2/conf/certs:ro" httpd:alpine > /dev/null; sleep 3
docker ps --filter name=apache-reteta --format ' container: {{.Status}}'
echo " -- verificarea retetei: httpd -t (apachectl configtest) in container"; docker exec apache-reteta httpd -t 2>&1 | tail -1 | sed 's/^/ /'
echo " -- preconditia retetei: randul de pornire cu OpenSSL/ (jurnalul serverului)"; docker logs apache-reteta 2>&1 | grep -i 'openssl/' | tail -1 | cut -c1-160 | sed 's/^/ /'
sonde 8447 apache
docker logs apache-reteta 2>&1 | grep -iE 'error|emerg' | grep -v 'AH00558' | head -3 | sed 's/^/ jurnal: /'
# ------------------------------------------------------------------ HAProxy
echo "== HAPROXY: imaginea haproxy:alpine"; docker pull -q haproxy:alpine > /dev/null 2>&1; docker image inspect haproxy:alpine --format 'imagine: {{.Id}} {{.Size}} octeti' | cut -c1-90
echo " -- preconditia retetei: haproxy -vv | grep -i 'running on openssl'"; docker run --rm haproxy:alpine haproxy -vv 2>&1 | grep -iE 'HAProxy version|running on openssl|built with openssl' | cut -c1-120 | sed 's/^/ /'
cat > haproxy/haproxy.cfg <<'EOF'
global
# EXACT fragmentul retetei (sectiunea global):
ssl-default-bind-curves X25519MLKEM768:X25519
ssl-default-bind-options ssl-min-ver TLSv1.3
defaults
mode http
timeout connect 5s
timeout client 10s
timeout server 10s
frontend proba
bind :8448 ssl crt /usr/local/etc/haproxy/certs/proba.pem
# EXACT fragmentul retetei (frontend-ul HTTPS):
http-response set-header Strict-Transport-Security "max-age=31536000"
http-request return status 200 content-type text/plain string "ok\n"
EOF
docker rm -f haproxy-reteta > /dev/null 2>&1
echo " -- verificarea retetei: haproxy -c -f"; docker run --rm -v "$D/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" -v "$D/certs:/usr/local/etc/haproxy/certs:ro" haproxy:alpine haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg 2>&1 | tail -2 | sed 's/^/ /'
docker run -d --name haproxy-reteta -p 127.0.0.1:8448:8448 -v "$D/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" -v "$D/certs:/usr/local/etc/haproxy/certs:ro" haproxy:alpine > /dev/null; sleep 3
docker ps --filter name=haproxy-reteta --format ' container: {{.Status}}'
sonde 8448 haproxy
docker logs haproxy-reteta 2>&1 | grep -iE 'alert|error|emerg' | head -3 | sed 's/^/ jurnal: /'
echo "== curatenie"; docker rm -f apache-reteta haproxy-reteta > /dev/null 2>&1; docker rmi httpd:alpine haproxy:alpine > /dev/null 2>&1 && echo " imaginile sterse"
cd /root && rm -rf "$D"; docker ps -a --format '{{.Names}}' | grep -cE 'apache-reteta|haproxy-reteta' | sed 's/^/ containere ramase: /'
echo "== RPC dupa: $(rpc)"
echo "retetele apache + haproxy: $((8 - rele)) din 8 sonde cum trebuie"
[ "$rele" = 0 ] && exit 0 || exit 1

View File

@ -0,0 +1,59 @@
#!/bin/bash
# masoara-haproxy2-rpc2.sh (PE rpc2): a doua masurare a retetei `haproxy`, dupa ce prima (masoara-apache-haproxy-rpc2.sh) a aratat doua
# lucruri despre PROBA, nu despre reteta: (1) `http-request return` genereaza raspunsul in HAProxy si regulile `http-response` NU se
# aplica unui raspuns generat intern (numai `http-after-response`), deci HSTS-ul retetei nu avea cum sa apara; (2) `haproxy -c -f`
# tace la succes si grep-ul pe `-vv` nu a prins randurile bibliotecii SSL. Aici: un backend HTTP simplu FARA HSTS (httpd:alpine pe
# 127.0.0.1:8449, control: curl direct pe el nu are antetul), HAProxy cu reteaua gazdei legat NUMAI pe 127.0.0.1:8448 (ssl) si cu
# backend-ul acela, deci raspunsul trece prin calea de raspuns si `http-response set-header` al retetei se vede sau nu; `haproxy -c -f`
# cu codul tiparit; `haproxy -vv` cu toate randurile despre SSL/OpenSSL si grupurile ML-KEM. Fragmentele retetei NESCHIMBATE.
set -u
D=/root/reteta-hp; rm -rf "$D"; mkdir -p "$D/certs" "$D/haproxy"; cd "$D" || exit 2
N=/opt/node24/bin/node; S=/root/reteta-sonda.mjs
[ -x "$N" ] || { echo "NEMASURAT: node 24 lipseste"; exit 2; }; [ -f "$S" ] || { echo "NEMASURAT: sonda lipseste"; exit 2; }
rpc() { local t0=$(date +%s%N); curl -s -m 5 -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:8545 | grep -o '"result":"0x[0-9a-f]*"' | head -1; echo " ($(( ($(date +%s%N) - t0) / 1000000 )) ms)"; }
echo "== RPC inainte: $(rpc)"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout certs/privkey.pem -out certs/fullchain.pem -days 2 -subj '/CN=proba.local' > /dev/null 2>&1 && cat certs/privkey.pem certs/fullchain.pem > certs/proba.pem && chmod 644 certs/*.pem && echo "cert ok"
rele=0
cere() { local nume="$1" ast="$2"; shift 2; out=$("$@" 2>&1); c=$?; if [ "$c" = "$ast" ]; then echo " OK $nume -> cod $c: $(printf '%s' "$out" | cut -c1-170)"; else echo " RAU $nume -> cod $c (asteptat $ast): $(printf '%s' "$out" | cut -c1-170)"; rele=$((rele + 1)); fi; }
docker pull -q httpd:alpine > /dev/null 2>&1; docker pull -q haproxy:alpine > /dev/null 2>&1
echo "== haproxy -vv: versiunea, biblioteca SSL, grupurile"; docker run --rm haproxy:alpine haproxy -vv 2>&1 | grep -iE 'HAProxy version|SSL|TLS|QUIC' | cut -c1-140 | sed 's/^/ /'
echo "== backend-ul simplu (httpd:alpine implicit, fara HSTS) pe 127.0.0.1:8449"
docker rm -f backend-reteta haproxy-reteta > /dev/null 2>&1
docker run -d --name backend-reteta -p 127.0.0.1:8449:80 httpd:alpine > /dev/null; sleep 2
echo " control: curl direct pe backend (fara HAProxy) -> antetele:"; curl -s -m 10 -D - -o /dev/null http://127.0.0.1:8449/ | grep -iE '^(strict-transport-security|HTTP)' | tr -d '\r' | sed 's/^/ /'
cat > haproxy/haproxy.cfg <<'EOF'
global
# EXACT fragmentul retetei (sectiunea global):
ssl-default-bind-curves X25519MLKEM768:X25519
ssl-default-bind-options ssl-min-ver TLSv1.3
defaults
mode http
timeout connect 5s
timeout client 10s
timeout server 10s
frontend proba
bind 127.0.0.1:8448 ssl crt /usr/local/etc/haproxy/certs/proba.pem
# EXACT fragmentul retetei (frontend-ul HTTPS):
http-response set-header Strict-Transport-Security "max-age=31536000"
default_backend simplu
backend simplu
server s1 127.0.0.1:8449
EOF
echo "== verificarea retetei: haproxy -c -f"; docker run --rm --network host -v "$D/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" -v "$D/certs:/usr/local/etc/haproxy/certs:ro" haproxy:alpine haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg > "$D/hc.txt" 2>&1; hc=$?; echo " cod $hc: $(tr '\n' ' ' < "$D/hc.txt" | cut -c1-160)"
docker run -d --name haproxy-reteta --network host -v "$D/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" -v "$D/certs:/usr/local/etc/haproxy/certs:ro" haproxy:alpine > /dev/null; sleep 3
docker ps --filter name=haproxy-reteta --format ' container: {{.Status}}'
cere "haproxy: hibrid singur (X25519MLKEM768)" 0 $N $S 127.0.0.1 8448 X25519MLKEM768
cere "haproxy: clasic singur (X25519, rezerva din reteta)" 0 $N $S 127.0.0.1 8448 X25519
cere "haproxy: hibrid + clasic (preferinta)" 0 $N $S 127.0.0.1 8448 X25519MLKEM768:X25519
cere "haproxy: client TLS 1.2 (refuz)" 1 $N $S 127.0.0.1 8448 X25519 TLSv1.2
echo " -- HSTS prin HAProxy (raspuns venit de la backend-ul fara HSTS, curl -k):"; curl -sk -m 10 -D - -o /dev/null https://127.0.0.1:8448/ > "$D/h.txt"; grep -iE '^(strict-transport-security|HTTP)' "$D/h.txt" | tr -d '\r' | sed 's/^/ /'
if grep -qi '^strict-transport-security: max-age=31536000' "$D/h.txt"; then echo " OK HSTS pus de HAProxy (backend-ul nu il are)"; else echo " RAU HSTS lipseste prin HAProxy"; rele=$((rele + 1)); fi
docker logs haproxy-reteta 2>&1 | grep -iE 'alert|error|emerg' | head -3 | sed 's/^/ jurnal: /'
echo "== curatenie"; docker rm -f backend-reteta haproxy-reteta > /dev/null 2>&1; docker rmi httpd:alpine haproxy:alpine > /dev/null 2>&1 && echo " imaginile sterse"
cd /root && rm -rf "$D"; docker ps -a --format '{{.Names}}' | grep -cE 'backend-reteta|haproxy-reteta' | sed 's/^/ containere ramase: /'
echo "== RPC dupa: $(rpc)"
echo "reteta haproxy (a doua masurare): $((5 - rele)) din 5 cum trebuie (4 sonde + HSTS)"
[ "$rele" = 0 ] && [ "$hc" = 0 ] && exit 0 || exit 1

View File

@ -29,8 +29,8 @@ const HSTS = 'max-age=31536000';
export const RETETE_MASURATE = { export const RETETE_MASURATE = {
node: 'measured end to end on 2026-09-28: a Node 24.14.1 / OpenSSL 3.5.5 server with the options of this recipe, scanned by the AERE scanner (proba-remediere.mjs)', node: 'measured end to end on 2026-09-28: a Node 24.14.1 / OpenSSL 3.5.5 server with the options of this recipe, scanned by the AERE scanner (proba-remediere.mjs)',
nginx: 'measured end to end on 2026-10-18: nginx 1.31.7 (nginx:alpine, OpenSSL 3.5.8/3.5.9) with exactly these directives, in a container on 127.0.0.1: a client offering only X25519MLKEM768 completes TLS 1.3, X25519 alone still works as the fallback, a TLS 1.2 client is refused, HSTS is sent (dovezi/retete-2026-10-18/)', nginx: 'measured end to end on 2026-10-18: nginx 1.31.7 (nginx:alpine, OpenSSL 3.5.8/3.5.9) with exactly these directives, in a container on 127.0.0.1: a client offering only X25519MLKEM768 completes TLS 1.3, X25519 alone still works as the fallback, a TLS 1.2 client is refused, HSTS is sent (dovezi/retete-2026-10-18/)',
apache: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the mod_ssl documentation, not measured by us on a real Apache', apache: 'measured end to end on 2026-10-18: Apache 2.4.69 with OpenSSL 3.5.9 (httpd:alpine, mod_ssl + mod_headers) with exactly these directives in a <VirtualHost>, in a container on 127.0.0.1: a client offering only X25519MLKEM768 completes TLS 1.3, X25519 alone still works as the fallback, a TLS 1.2 client is refused, HSTS is sent, httpd -t says Syntax OK and the startup line names OpenSSL/3.5.9 as the precondition command expects (dovezi/retete-2026-10-18/)',
haproxy: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the HAProxy documentation, not measured by us on a real HAProxy', haproxy: 'measured end to end on 2026-10-18: HAProxy 3.4.7 built and running on OpenSSL 3.5.9 (haproxy:alpine) with exactly these lines, in a container on 127.0.0.1 in front of a plain HTTP backend without HSTS: a client offering only X25519MLKEM768 completes TLS 1.3, X25519 alone works as the fallback, a TLS 1.2 client is refused, haproxy -c accepts the file, and the HSTS header appears on the proxied response (it does not on a response HAProxy generates itself with http-request return: there only http-after-response rules apply) (dovezi/retete-2026-10-18/)',
go: 'measured end to end on 2026-10-18: a Go 1.26.5 (windows/amd64) net/http server with exactly this tls.Config and header, on 127.0.0.1: X25519MLKEM768 alone completes TLS 1.3, X25519 alone works as the fallback, a TLS 1.2 client is refused, HSTS is sent, and the AERE scanner reports pqKeyExchange X25519MLKEM768 / tls12Accepted false / hsts true (dovezi/retete-2026-10-18/); Linux not measured (the WSL Go is 1.22)', go: 'measured end to end on 2026-10-18: a Go 1.26.5 (windows/amd64) net/http server with exactly this tls.Config and header, on 127.0.0.1: X25519MLKEM768 alone completes TLS 1.3, X25519 alone works as the fallback, a TLS 1.2 client is refused, HSTS is sent, and the AERE scanner reports pqKeyExchange X25519MLKEM768 / tls12Accepted false / hsts true (dovezi/retete-2026-10-18/); Linux not measured (the WSL Go is 1.22)',
}; };
@ -104,7 +104,7 @@ const GENERATOARE = {
s.tlsMin && `ssl-default-bind-options ssl-min-ver TLSv${s.tlsMin}`, s.tlsMin && `ssl-default-bind-options ssl-min-ver TLSv${s.tlsMin}`,
s.hsts && `http-response set-header Strict-Transport-Security "${s.hsts}"`, s.hsts && `http-response set-header Strict-Transport-Security "${s.hsts}"`,
].filter(Boolean), ].filter(Boolean),
precondition: s.groups ? { what: 'HAProxy running on OpenSSL 3.5 or later', command: 'haproxy -vv | grep -i "running on openssl"' } : null, precondition: s.groups ? { what: 'HAProxy running on OpenSSL 3.5 or later (haproxy -vv prints "Running on SSL library version : OpenSSL 3.5.x"; measured on 3.4.7)', command: 'haproxy -vv | grep -i "SSL library version"' } : null,
verify: 'haproxy -c -f /etc/haproxy/haproxy.cfg', reload: 'systemctl reload haproxy', verify: 'haproxy -c -f /etc/haproxy/haproxy.cfg', reload: 'systemctl reload haproxy',
}), }),
node: (s) => { node: (s) => {

View File

@ -24,8 +24,10 @@ An API name that appears only inside a comment or inside a string literal is not
| 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`, ...); the libsignal Java/Android API (`org.signal.libsignal`, anchored on its import or package: `ECKeyPair.generate`, `IdentityKeyPair.generate`, `KEMKeyPair.generate(KEMKeyType.KYBER_1024)`, `HKDF.deriveSecrets`, `new Aes256GcmSiv` / `Aes256GcmEncryption` / `Aes256Ctr32`, `new PreKeyBundle`) | | 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`, ...); the libsignal Java/Android API (`org.signal.libsignal`, anchored on its import or package: `ECKeyPair.generate`, `IdentityKeyPair.generate`, `KEMKeyPair.generate(KEMKeyType.KYBER_1024)`, `HKDF.deriveSecrets`, `new Aes256GcmSiv` / `Aes256GcmEncryption` / `Aes256Ctr32`, `new PreKeyBundle`) |
| Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) | | Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) |
| Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` and the BoringSSL bindings `boring` / `boring-signal` with the same API (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `SslConnector::builder`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, the pre-standard `kyber768` / `kyber1024` modules, `libcrux_ml_dsa::ml_dsa_65::sign`); `x25519_dalek::x25519(...)`; `HmacReset`; a mode type brought in by name (`use cbc::Decryptor; Decryptor::<Aes256>::new`); the hybrid Noise pattern `Noise_NKhfs_25519+Kyber1024_...` as one hybrid key exchange; a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order | | Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` and the BoringSSL bindings `boring` / `boring-signal` with the same API (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `SslConnector::builder`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, the pre-standard `kyber768` / `kyber1024` modules, `libcrux_ml_dsa::ml_dsa_65::sign`); `x25519_dalek::x25519(...)`; `HmacReset`; a mode type brought in by name (`use cbc::Decryptor; Decryptor::<Aes256>::new`); the hybrid Noise pattern `Noise_NKhfs_25519+Kyber1024_...` as one hybrid key exchange; a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order |
| Swift | names are resolved through `import`: CryptoKit / swift-crypto (`SHA256.hash`, `SHA384`, `SHA512`, `Insecure.MD5`, `Insecure.SHA1`, `HMAC<SHA256>`, `AES.GCM.seal/open` (key size from a `SymmetricKey(size:)` on the same line), `AES.KeyWrap`, `ChaChaPoly.seal`, `Curve25519.KeyAgreement.PrivateKey()`, `Curve25519.Signing.PrivateKey()`, `P256/P384/P521.Signing` and `.KeyAgreement`, `HKDF<SHA256>.deriveKey`, `SymmetricKey(size:)`, `_RSA` from `_CryptoExtras`); CommonCrypto (`CC_SHA256`, `CC_MD5`, `CCCrypt` / `CCCryptorCreate` with `kCCAlgorithm*`, `kCCOptionECBMode`, `kCCKeySizeAES*`, `CCHmac` with `kCCHmacAlg*`, `CCKeyDerivationPBKDF` with `kCCPRFHmacAlg*`); Security.framework (`SecKeyCreateRandomKey` with `kSecAttrKeyTypeRSA` / `kSecAttrKeyTypeECSECPrimeRandom` and `kSecAttrKeySizeInBits`, read from the call or from an attribute dictionary defined in the same file; `SecKeyCreateSignature` / `SecKeyVerifySignature` / `SecKeyCreateEncryptedData` / `SecKeyCreateDecryptedData` with `.rsaSignatureMessagePKCS1v15SHA256`, `.rsaSignatureMessagePSSSHA256`, `.ecdsaSignatureMessageX962SHA256`, `.rsaEncryptionOAEPSHA256`, ...; `SecTrustEvaluateWithError` / `SecPolicyCreateSSL` as certificate-chain validation); the client's TLS (`tlsMinimumSupportedProtocolVersion` / `tlsMaximumSupportedProtocolVersion`, `sec_protocol_options_set_min/max_tls_protocol_version`, `NWProtocolTLS.Options()`); CryptoSwift (`AES(key:blockMode:)`, `SHA2(variant:)`, `.md5()`, `HMAC(key:variant:)`, `PBKDF2`, `ChaCha20`, `Poly1305`, `RSA(keySize:)`, `Blowfish`); swift-sodium / Clibsodium (`sodium.box`, `.sign`, `.keyExchange`, `.secretBox`, `.aead.xchacha20poly1305ietf`, `.pwHash`, `.genericHash`, `crypto_box_*`, `crypto_sign_*`, `crypto_aead_*`, ...); the libsignal Swift API (`import LibSignalClient`, or `import SignalFfi` and the `Sources/LibSignalClient/` target directory for the library's own sources: `IdentityKeyPair.generate()`, `PrivateKey.generate()`, `KEMKeyPair.generate()` (Kyber-1024: the Swift API takes no key type, the bridge generates `kem::KeyType::Kyber1024`), `KEMPublicKey(...)`, `Aes256GcmSiv(key:)`, `Aes256GcmEncryptedData`, `Aes256GcmEncryption` / `Aes256GcmDecryption`, `Aes256Ctr32`, `hkdf(...)`, `PreKeyBundle(...)` / `processPreKeyBundle(...)`) |
| Kotlin | the Java rules with Kotlin syntax (no `new`, no `;`, `val`/`var`, `arrayOf(...)`, `ECGenParameterSpec("...")`): JCA `getInstance("...")`, `SSLContext`, `setEnabledProtocols`, Bouncy Castle imports, the libsignal Java/Android API without `new` (`ECPrivateKey.generate()`, `ECKeyPair.generate()`, `KEMKeyPair.generate(KEMKeyType.KYBER_1024)`, `KEMPublicKey(bytes)`, `HKDF.deriveSecrets`, `Aes256GcmSiv(key)`, `PreKeyBundle(...)`, `SealedSessionCipher(...)`) and the library's own native bridge inside `org.signal.libsignal` (`Native.ECPrivateKey_Generate()`, `Native.ECPrivateKey_Agree` / `_Sign` / `_HpkeOpen`, `Native.KyberKeyPair_Generate()`, `Native.HKDF_DeriveSecrets`, `Native.Aes256GcmSiv_New`); plus rules added for the JVM in 0.4.0 that apply to Java too: Android Keystore (`KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")` with the size, curve, block mode and padding read from the `KeyGenParameterSpec.Builder` passed to `initialize` / `init`, inline or through a variable), Tink (`KeyTemplates.get("AES256_GCM")`, `AeadKeyTemplates.X`, `PredefinedAeadParameters.X`; `*Config.register()` and the import count as import-level evidence), OkHttp (`ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)`, `.tlsVersions(TlsVersion.TLS_1_2)`, `.cipherSuites(CipherSuite.TLS_...)`, `CertificatePinner.Builder()`), and certificate-chain validation (`CertPathValidator.getInstance("PKIX")`, `TrustManagerFactory.getInstance`, `X509TrustManager`, `checkServerTrusted`, `KeyStore.getInstance("AndroidCAStore")`) |
| Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys | | Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys |
| Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`, `Cargo.toml`: cryptographic libraries that are declared | | Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `libs.versions.toml`, `go.mod`, `Cargo.toml`, `Package.swift`, `Podfile`, `*.podspec`: cryptographic libraries that are declared (`pkg:swift/github.com/apple/swift-crypto@3.0.0`, `pkg:cocoapods/CryptoSwift@1.8`, `pkg:maven/org.signal/libsignal-android@0.105.1`) |
In Go an unused import does not compile, so an import of a crypto package is evidence of use; the In Go an unused import does not compile, so an import of a crypto package is evidence of use; the
import-level finding is dropped when the same file has a call-level finding that covers it. A blank import-level finding is dropped when the same file has a call-level finding that covers it. A blank
@ -40,6 +42,24 @@ guessed. An unused import is only a warning in Rust, so an imported crypto crate
(`.sign()`, `.encrypt()`) without their constructor in the same file, macros, and a project's own wrapper (`.sign()`, `.encrypt()`) without their constructor in the same file, macros, and a project's own wrapper
types (the file that calls the crate directly is the one reported). types (the file that calls the crate directly is the one reported).
In Swift and Kotlin (added in 0.4.0, 2026-10-18, after the run on libsignal showed 321 files and 20.5% of the
source lines not analyzed at all) the `import` lines decide what a name means, as the `use` lines do in Rust:
`SHA256.hash` is CryptoKit only in a file that imports CryptoKit or Crypto, `IdentityKeyPair.generate()` is
libsignal only with `import LibSignalClient` (or, inside the library, `import SignalFfi` or the
`Sources/LibSignalClient/` target directory, because Swift does not declare the module in the file). A
third-party library imported without a recognized call (LibSignalClient, CryptoSwift, Sodium) is reported once
as import-level evidence; the system frameworks (CryptoKit, CommonCrypto, Security) are not, since Security
is imported for the keychain in almost every app. Certificate-chain validation (`SecTrustEvaluate*`, JCA
`CertPathValidator("PKIX")`, `X509TrustManager`, OkHttp pinning) is classified as a classical-signature
exposure: the chain is signed by the CA with RSA or ECDSA, which a quantum computer could forge; it is an
authentication exposure, not a key exchange, and nothing in that code alone can change it. Swift and Kotlin
block comments nest; Swift string interpolation `\( )` and Kotlin templates `${ }` may contain strings and
brackets, so the lexer scans them with an explicit stack (iteratively: a file made of `"\(` repeated would be
tens of thousands of levels deep). The content of a string, interpolation included, is never a use. Not
recognized: methods on values without their constructor in the same file (`generateSignature`,
`signalEncrypt`), a project's own wrapper types, a module imported through an Objective-C bridging header,
`#if DEBUG` as a scope, Kotlin extension functions over JCA.
When the algorithm is given by a variable, the tool resolves it only when the file assigns that name When the algorithm is given by a variable, the tool resolves it only when the file assigns that name
exactly once, to a plain string literal, and the name is not a function parameter or loop variable exactly once, to a plain string literal, and the name is not a function parameter or loop variable
(template literals whose `${...}` parts all resolve this way are also resolved). The finding then (template literals whose `${...}` parts all resolve this way are also resolved). The finding then
@ -186,30 +206,40 @@ console.log(renderSummary(result));
configuration files at run time. Those appear as `unknown` at best, or not at all. configuration files at run time. Those appear as `unknown` at best, or not at all.
- It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key - It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key
is reported like MD5 used for signatures; the reason text tells you what to check. is reported like MD5 used for signatures; the reason text tells you what to check.
- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java, Go and Rust. - It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java, Go, Rust, Swift
Kotlin, Scala, C#, C and C++ files are only checked for PEM blocks. Files that are not UTF-8 and Kotlin. Scala, C#, C, C++ and Objective-C files are only checked for PEM blocks. Files that are not
text (including UTF-16) are treated as binary and counted as not read. 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. - It does not execute anything from the scanned tree, and it makes no network requests.
## Tests ## Tests
``` ```
node test/proba.mjs # 52 probes: fixtures per language with the exact expected findings node test/proba.mjs # 68 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 (25 mutations), and node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (35 mutations), and
# requires the matching probe to fail while the rest of the suite still runs # 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/proba-timp.mjs # 13 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/control-negativ-timp.mjs # puts each quadratic form back (and one Swift comment rescan) 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); detectors node test/echivalenta-0.1.0.mjs # same findings as 0.1.0 (needs the git history; skipped without it); detectors
# added after 0.1.0 (Rust, the libsignal Java/Node API) are counted apart, and # added after 0.1.0 (Rust, Swift, Kotlin, the libsignal Java/Node API, the JVM rules of 0.4.0)
# a finding that 0.1.0 had and the current version lacks is always a difference # are counted apart, and a finding that 0.1.0 had and the current version lacks is always a difference
``` ```
Measured on 2026-10-18 on the libsignal tree (1,788 files; 596 Rust, 343 Java, 173 TypeScript, 210 Swift, 111 Kotlin): before the Measured on 2026-10-18 on the libsignal tree (1,788 files; 596 Rust, 343 Java, 173 TypeScript, 210 Swift, 111 Kotlin): before the
additions of that day the inventory returned 450 findings, all in Rust and Python, and zero for Java and TypeScript, because additions of that day the inventory returned 450 findings, all in Rust and Python, and zero for Java and TypeScript, because
libsignal does not call JCA or `node:crypto` but its own API over the native bridge; Swift and Kotlin (20.5% of the source lines) libsignal does not call JCA or `node:crypto` but its own API over the native bridge; Swift and Kotlin (20.5% of the source lines)
are not analyzed at all. After: 759 findings (Java 262). The Node side of libsignal itself still returns zero, because its own were not analyzed at all. After the 0.3.0 additions: 759 findings (Java 262). The Node side of libsignal itself still returns zero, because its own
tree imports by relative path, not by the package name the detector anchors on. tree imports by relative path, not by the package name the detector anchors on.
Measured later on 2026-10-18, same tree, with the Swift and Kotlin detectors of 0.4.0: all 1,440 source files are analyzed (no
source line left out by construction); 980 findings; Swift 0 to 134 in 65 files (84 calls, 50 import-level; 128 in test paths,
6 in production: X25519 49, Kyber-1024 8, AES-256-GCM 9, AES-256-CTR 4, HKDF 4, AES-256-GCM-SIV 1, session setup 9); Kotlin 0 to 67
in 52 files (22 calls, 2 configurations, 43 import-level; 20 in test paths, 47 in production: X.509 chain validation 7, Kyber-1024 6,
X25519 5, XEdDSA 2, HKDF 2, MD5 1); Java 262 to 282 through the JVM rules shared with Kotlin. Eleven files were read by hand against
the inventory (six Swift, five Kotlin): eight correct at the first run, three with a false negative each (the static form
`Aes256Ctr32.process`, a library source without any import, the Curve25519 bridge calls for signing, agreement and HPKE), all three
fixed the same hour with their form added to the fixtures. Of 38 Swift/Kotlin files whose code mentions a cryptographic API name, 8
have no finding, each for a stated reason (function definitions, handle wrappers, sender keys, randomness).
Fixtures mark each expected finding on its own line (`EXPECT: <name> | <class>`), and a probe fails on 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, 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 docstrings, strings that mention crypto APIs, a file without cryptography, algorithms from

View File

@ -227,6 +227,8 @@ export function evalueaza(a) {
} }
if (g === 'MAC') { if (g === 'MAC') {
// 0.4.0 (2026-10-18): autentificatoare care nu sunt HMAC (Poly1305 din CryptoSwift, AES-CMAC din Tink): numite, fara hash
if (!a.hash && a.nume) return f({ nume: a.nume, primitiv: 'mac', ...rez({ c: CLS.S, motiv: `${a.nume}: symmetric authenticator; a quantum attacker gains at most a quadratic speed-up (Grover) on key search.` }) });
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 }) }); 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 h = HASH[a.hash];
const nume = `HMAC-${h ? h.n : String(a.hash || 'unknown')}`; const nume = `HMAC-${h ? h.n : String(a.hash || 'unknown')}`;
@ -295,6 +297,14 @@ export function evalueaza(a) {
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.${nota}`, q: pq }) }); 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.${nota}`, q: pq }) });
} }
if (g === 'TLS') return tls(a, f); if (g === 'TLS') return tls(a, f);
// 0.4.0 (2026-10-18, dupa fisierul 8 din raportul libsignal: rustls folosit NUMAI ca verificator de certificate iesea "X25519, schimba
// furnizorul"): verificarea unui lant de certificate (PKIX in JCA, X509TrustManager, SecTrustEvaluate, pinning) e o expunere de
// AUTENTIFICARE (lantul e semnat clasic de CA), nu un schimb de cheie; nimic de recoltat acum, nimic de schimbat in codul acesta singur.
if (g === 'PKI-VERIFY') {
const nume = a.nume || 'X.509 chain validation (PKIX)';
const nota = a.nota ? ` ${a.nota}` : '';
return f({ nume, primitiv: 'signature', ...rez({ c: CLS.V, qv: true, motiv: `${nume}: the certificate chain is authenticated by the issuing CA's classical signatures (RSA or ECDSA), which a cryptographically relevant quantum computer could forge; this is an authentication exposure (impersonation once such a computer exists), not a harvest-now-decrypt-later exposure, since no key is exchanged here.${nota}`, rec: 'Nothing to change in this code alone: certificate chains move to post-quantum or composite signatures only when the issuing CA offers them (no public CA did when this rule was written). Until then keep the trust store small, prefer short-lived certificates, and plan the switch of the trust anchors when post-quantum roots are published.', q: 0 }) });
}
if (g === 'JWT-HMAC') { if (g === 'JWT-HMAC') {
const h = HASH[a.hash]; 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.` }) }); 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.` }) });

View File

@ -172,6 +172,13 @@ function rezolvaOData(ctx, nume) {
const m = new RegExp(`^\\s*(?:(?:const|var)\\s+)?${e}\\s*(?:string\\s*)?(?::=|=)\\s*"([^"\\n\\\\]*)"\\s*$`).exec(c.slice(ls, le)); 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 !parametru && m ? { valoare: m[1], line: ctx.poz(k).line } : null;
} }
// 0.4.0 (2026-10-18): Kotlin `val X = "..."` / `const val X = "..."`, Swift `let X = "..."`; un parametru `X: String` in paranteze
// nu se rezolva; un sir cu $ (sablon) sau \( (interpolare) nu e un literal simplu
if (ctx.limbaj === 'kotlin' || ctx.limbaj === 'swift') {
parametru = new RegExp(`\\(([^()]{0,2000}(?<![\\w.])${e}\\s*:\\s*String\\b[^()]{0,2000})\\)`).test(c);
const m = new RegExp(`\\b(?:val|var|let)\\s+${e}\\s*(?::\\s*String\\s*)?=\\s*"([^"\\n\\\\$]*)"\\s*;?\\s*$`).exec(linie);
return !parametru && m ? { valoare: m[1], line: ctx.poz(k).line } : null;
}
return null; return null;
} }

View File

@ -1,5 +1,9 @@
// Detectorul Java: JCA getInstance("..."), SSLContext, protocoale activate, Bouncy Castle. // Detectorul Java: JCA getInstance("..."), SSLContext, protocoale activate, Bouncy Castle.
import { argumente, imparteArgumente, valoareArgument, necunoscut } from './context.mjs'; // 0.4.0 (2026-10-18): acelasi cod serveste si Kotlin (`detecteazaJvm(ctx, true)`, chemat din detect-kotlin.mjs): JCA e acelasi API,
// fara `new`, fara `;`, cu `val`/`var` si `arrayOf(...)`; si regulile noi comune JVM-ului: Android Keystore (KeyGenParameterSpec),
// Tink (sabloanele de chei), OkHttp (ConnectionSpec, TlsVersion, CipherSuite, CertificatePinner) si verificarea lanturilor de
// certificate (CertPathValidator "PKIX", TrustManagerFactory, X509TrustManager, checkServerTrusted), raportata ca autentificare.
import { argumente, imparteArgumente, valoareArgument, necunoscut, escapeRe } from './context.mjs';
import { hashCanonic, grupTls, curbaCanonica } from './catalog.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' }; 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' };
@ -40,7 +44,7 @@ export function activJca(clasa, val, dupa = '', variabila = null) {
if (u === 'DSA') return { grup: 'DSA', param: bits, 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 === 'DH' || u === 'DIFFIEHELLMAN') return { grup: 'DH', param: bits, functii: f };
if (u === 'EC' || u === 'ECDSA' || u === 'ECDH') { if (u === 'EC' || u === 'ECDSA' || u === 'ECDH') {
const spec = peVar('initialize', 'new\\s+ECGenParameterSpec\\s*\\(\\s*"([^"]+)"'); const spec = peVar('initialize', '(?:new\\s+)?ECGenParameterSpec\\s*\\(\\s*"([^"]+)"'); // Kotlin: fara new
return { grup: u === 'ECDH' ? 'ECDH' : 'EC', curba: spec ? spec[1] : undefined, functii: f }; 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 === 'ED25519' || u === 'ED448' || u === 'EDDSA') return { grup: 'EDDSA', nume: u === 'ED448' ? 'Ed448' : 'Ed25519', functii: f };
@ -169,14 +173,94 @@ export function variabilaAtribuita(c, pos) {
return st < sf ? c.slice(st, sf) : null; return st < sf ? c.slice(st, sf) : null;
} }
export function detecteazaJava(ctx) { // KeyProperties.KEY_ALGORITHM_* (Android Keystore) -> numele JCA pe care il primeste getInstance
const KEY_ALGORITHM = { EC: 'EC', RSA: 'RSA', AES: 'AES', '3DES': 'DESede', HMAC_SHA1: 'HmacSHA1', HMAC_SHA224: 'HmacSHA224', HMAC_SHA256: 'HmacSHA256', HMAC_SHA384: 'HmacSHA384', HMAC_SHA512: 'HmacSHA512', XDH: 'XDH' };
const NUME_PKIX = 'X.509 chain validation (PKIX)';
// Lantul unui KeyGenParameterSpec.Builder(...)....build(): marimea cheii, curba, hash-urile, modurile, umpluturile
function citesteSpecAndroid(lant) {
const t = lant.slice(0, 3000);
const r = {};
let m;
if ((m = /setKeySize\s*\(\s*(\d+)\s*\)/.exec(t))) r.bits = m[1];
if ((m = /(?:new\s+)?ECGenParameterSpec\s*\(\s*"([^"]+)"/.exec(t))) { r.curba = m[1]; r.pozCurba = m.index; } // pozitia, ca sa nu se numere si separat
if ((m = /BLOCK_MODE_(GCM|CBC|ECB|CTR)\b/.exec(t))) r.mod = m[1].toLowerCase();
if ((m = /DIGEST_(SHA1|SHA224|SHA256|SHA384|SHA512|MD5)\b/.exec(t))) r.hash = hashCanonic(m[1]) || m[1];
if ((m = /(ENCRYPTION_PADDING_RSA_OAEP|ENCRYPTION_PADDING_RSA_PKCS1|SIGNATURE_PADDING_RSA_PSS|SIGNATURE_PADDING_RSA_PKCS1)\b/.exec(t))) r.padding = /OAEP/.test(m[1]) ? 'oaep' : /PSS/.test(m[1]) ? 'other' : 'pkcs1v15';
return r;
}
// Specificatia Android Keystore data generatorului `variabila` prin .initialize(spec) / .init(spec): inline in apel sau printr-o variabila
// definita in fisier cu KeyGenParameterSpec.Builder; intoarce { spec, pozitiiBuilder } (pozitiile builderelor consumate)
function specAndroidPentru(ctx, dupaStart, dupa, variabila) {
if (!variabila) return null;
const s = new RegExp(`(?<![\\w.])${escapeRe(variabila)}\\s*\\.\\s*(?:initialize|init)\\s*\\(`).exec(dupa);
if (!s) return null;
const a = argumente(ctx, dupaStart + s.index + s[0].length);
const RE_B = /(?<![\w.])KeyGenParameterSpec\s*\.\s*Builder\s*\(/g;
const cu = (baza, text) => { const spec = citesteSpecAndroid(text); const pozitii = [baza]; if (spec.pozCurba !== undefined) pozitii.push(baza + spec.pozCurba); return { spec, pozitii }; };
let b = RE_B.exec(a.text);
if (b) return cu(a.start + b.index, a.text.slice(b.index));
const id = /^\s*([A-Za-z_]\w*)\s*$/.exec(a.text);
if (!id) return null;
const def = new RegExp(`(?<![\\w.])${escapeRe(id[1])}\\s*(?::\\s*KeyGenParameterSpec\\s*)?=\\s*KeyGenParameterSpec\\s*\\.\\s*Builder\\s*\\(`).exec(ctx.code);
if (!def) return null;
const inceput = def.index + def[0].lastIndexOf('KeyGenParameterSpec');
return cu(inceput, ctx.code.slice(inceput));
}
// Sabloanele de chei Tink (KeyTemplates.get("..."), AeadKeyTemplates.X, PredefinedAeadParameters.X): numele spune algoritmul
export function sablonTink(nume) {
const t = String(nume).trim().replace(/_RAW$/, '');
let m;
const pq = pqNume(t.replace(/_/g, '-'));
if (pq) return pq;
if ((m = /^AES(128|256)_GCM(_SIV)?$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: m[2] ? 'gcm-siv' : 'gcm', functii: ['encrypt', 'decrypt'] };
if ((m = /^AES(128|256)_EAX$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'eax', functii: ['encrypt', 'decrypt'] };
if ((m = /^AES(128|256)_CTR_HMAC_SHA256$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'ctr', functii: ['encrypt', 'decrypt'], nota: 'Tink AES-CTR with HMAC-SHA-256 (encrypt-then-MAC).' };
if ((m = /^AES(128|256)_GCM_HKDF_(\d+KB|\d+MB)$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'gcm', functii: ['encrypt', 'decrypt'], nota: 'Tink streaming AEAD (segments keyed with HKDF).' };
if (t === 'AES256_SIV') return { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'siv', functii: ['encrypt', 'decrypt'] };
if (t === 'XCHACHA20_POLY1305') return { grup: 'CIPHER', nume: 'XChaCha20-Poly1305', functii: ['encrypt', 'decrypt'] };
if (t === 'CHACHA20_POLY1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt', 'decrypt'] };
if (t === 'AES_CMAC') return { grup: 'MAC', nume: 'AES-CMAC', functii: ['tag'] };
if ((m = /^HMAC_SHA(256|512)_(\d+)BITTAG$/.exec(t))) return { grup: 'MAC', hash: `SHA${m[1]}`, functii: ['tag'] };
if ((m = /^HMAC_SHA(256|512)_PRF$/.exec(t)) || (m = /^HKDF_SHA(256)$/.exec(t))) return { grup: 'KDF', nume: /^HKDF/.test(t) ? 'HKDF' : 'HMAC-PRF', hash: `SHA${m[1]}`, functii: ['keyderive'] };
if ((m = /^ECDSA_P(256|384|521)(?:_IEEE_P1363|_RAW)?$/.exec(t))) return { grup: 'ECDSA', curba: `secp${m[1]}r1`, functii: ['sign', 'verify'] };
if (t === 'ED25519') return { grup: 'EDDSA', nume: 'Ed25519', functii: ['sign', 'verify'] };
if ((m = /^RSA_SSA_(PKCS1|PSS)_(\d{4})_SHA(\d{3})(?:_SHA\d{3}_\d+)?_F4$/.exec(t))) return { grup: 'RSA', param: m[2], primitiv: 'signature', padding: m[1] === 'PSS' ? 'other' : 'pkcs1v15', hash: `SHA${m[3]}`, functii: ['sign', 'verify'] };
if ((m = /^ECIES_P(256|384|521)(?:_COMPRESSED)?_HKDF_HMAC_SHA\d+_AES\d+_(?:GCM|CTR_HMAC_SHA\d+)$/.exec(t))) return { grup: 'ECDH', curba: `secp${m[1]}r1`, primitiv: 'pke', functii: ['encrypt', 'decrypt'], nota: `Tink ECIES (${t}).` };
if ((m = /^DHKEM_(X25519|P256|P384|P521)_HKDF_SHA\d+_HKDF_SHA\d+_(AES_128_GCM|AES_256_GCM|CHACHA20_POLY1305)$/.exec(t))) {
const nota = `Tink HPKE (RFC 9180) with DHKEM ${m[1]}: the key encapsulation is classical.`;
return m[1] === 'X25519' ? { grup: 'XDH', nume: 'X25519', functii: ['encapsulate', 'decapsulate'], nota } : { grup: 'ECDH', curba: `secp${m[1].slice(1)}r1`, primitiv: 'pke', functii: ['encapsulate', 'decapsulate'], nota };
}
return { grup: 'UNKNOWN', nume: `Tink template ${nume}`, motiv: `Tink key template "${nume}" is not in this tool's catalog.` };
}
// Suita de cifruri TLS (OkHttp CipherSuite.TLS_...): schimbul de cheie din nume, sau cifrul pentru suitele TLS 1.3
function suitaTls(nume) {
let m;
if ((m = /^TLS_(ECDHE|ECDH)_/.exec(nume))) return { grup: 'ECDH', nume, functii: ['keygen'], nota: 'TLS cipher suite with (EC)DHE key exchange; the curve is negotiated separately.' };
if ((m = /^TLS_(DHE|DH)_/.exec(nume))) return { grup: 'DH', nume, functii: ['keygen'] };
if (/^TLS_RSA_/.test(nume)) return { grup: 'RSA', nume, primitiv: 'pke', padding: 'pkcs1v15', functii: ['encrypt'], nota: 'TLS cipher suite with RSA key transport (no forward secrecy).' };
if ((m = /^TLS_AES_(128|256)_GCM_SHA\d+$/.exec(nume))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'gcm', functii: ['encrypt'], nota: 'TLS 1.3 cipher suite: the key exchange group is negotiated separately.' };
if (/^TLS_CHACHA20_POLY1305_SHA256$/.test(nume)) return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'], nota: 'TLS 1.3 cipher suite: the key exchange group is negotiated separately.' };
if ((m = /^TLS_AES_(128|256)_CCM/.exec(nume))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'ccm', functii: ['encrypt'] };
return { grup: 'UNKNOWN', nume, motiv: `TLS cipher suite "${nume}" is not in this tool's catalog.` };
}
export function detecteazaJava(ctx) { detecteazaJvm(ctx, false); }
export function detecteazaJvm(ctx, kotlin) {
const c = ctx.code; const c = ctx.code;
const consumate = new Set(); 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)) { 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 a = argumente(ctx, m.index + m[0].length);
const p = imparteArgumente(ctx, a.start, a.end); const p = imparteArgumente(ctx, a.start, a.end);
const api = `${m[1]}.getInstance`; const api = `${m[1]}.getInstance`;
const v = valoareArgument(ctx, p[0]); let v = valoareArgument(ctx, p[0]);
// Android Keystore: KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
const kp = v.fel === 'necunoscut' && /^KeyProperties\s*\.\s*KEY_ALGORITHM_(\w+)$/.exec(v.expresie);
if (kp && KEY_ALGORITHM[kp[1]]) v = { fel: 'rezolvat', valoare: KEY_ALGORITHM[kp[1]], din: `KeyProperties.KEY_ALGORITHM_${kp[1]}` };
if (v.fel === 'necunoscut') { ctx.adauga(m.index, api, 'call', necunoscut(v.expresie, api), { bucata: m[0] + v.expresie }); continue; } 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; if (v.fel !== 'literal' && v.fel !== 'rezolvat') continue;
const ex = v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {}; const ex = v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {};
@ -188,13 +272,39 @@ export function detecteazaJava(ctx) {
const dupa = c.slice(a.end, a.end + 2000); const dupa = c.slice(a.end, a.end + 2000);
const variabila = variabilaAtribuita(c, m.index); const variabila = variabilaAtribuita(c, m.index);
if (variabila && m[1] === 'KeyPairGenerator' && /^(EC|ECDSA|ECDH)$/i.test(v.valoare)) { 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); 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])); 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}"` }); const act = activJca(m[1], v.valoare, dupa, variabila);
// Android Keystore: marimea, curba, modul si umplutura vin din KeyGenParameterSpec.Builder, nu din initialize(bits)
if (/^(KeyPairGenerator|KeyGenerator)$/.test(m[1]) && act.grup !== 'UNKNOWN') {
const sp = specAndroidPentru(ctx, a.end, dupa, variabila);
if (sp) {
for (const pz of sp.pozitii) consumate.add(pz);
if (sp.spec.bits && !act.param) act.param = sp.spec.bits;
if (sp.spec.curba && !act.curba && /^(EC|ECDH)$/.test(act.grup)) act.curba = sp.spec.curba;
if (sp.spec.mod && act.grup === 'CIPHER') act.mod = sp.spec.mod;
if (sp.spec.padding && act.grup === 'RSA') { act.padding = sp.spec.padding; act.primitiv = sp.spec.padding === 'oaep' ? 'pke' : 'signature'; }
if (sp.spec.hash && act.grup === 'RSA' && !act.hash) act.hash = sp.spec.hash;
}
}
ctx.adauga(m.index, api, 'call', act, { ...ex, bucata: `${m[0]}"${v.valoare}"` });
} }
// curbe numite in afara unui KeyPairGenerator (ECGenParameterSpec, ECNamedCurveTable) // Android Keystore: un KeyGenParameterSpec.Builder neconsumat de un getInstance din acelasi fisier (generatorul e in alta parte);
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)) { // inaintea curbelor numite, fiindca ECGenParameterSpec-ul din lantul lui e al lui, nu o a doua curba
for (const m of ctx.potriviri(/(?<![\w.])KeyGenParameterSpec\s*\.\s*Builder\s*\(/g)) {
if (consumate.has(m.index)) continue;
const sp = citesteSpecAndroid(c.slice(m.index));
if (sp.pozCurba !== undefined) consumate.add(m.index + sp.pozCurba);
const act = sp.curba ? { grup: 'EC', curba: sp.curba, functii: ['keygen'] }
: sp.mod ? { grup: 'CIPHER', nume: sp.bits === '168' ? '3DES' : 'AES', param: sp.bits, mod: sp.mod, functii: ['keygen'] }
: sp.padding || (sp.bits && Number(sp.bits) >= 1024) ? { grup: 'RSA', param: sp.bits, padding: sp.padding, primitiv: sp.padding === 'oaep' ? 'pke' : (sp.padding ? 'signature' : undefined), hash: sp.hash, functii: ['keygen'] }
: sp.bits && /^(224|256|384|521)$/.test(sp.bits) ? { grup: 'EC', curba: `secp${sp.bits}r1`, functii: ['keygen'] }
: { grup: 'UNKNOWN', nume: 'KeyGenParameterSpec', motiv: 'Android Keystore key specification: the algorithm is given to the generator (KeyProperties.KEY_ALGORITHM_*), which is not in this file; the spec alone does not say it.' };
ctx.adauga(m.index, 'KeyGenParameterSpec.Builder', 'config', act, { bucata: m[0] });
}
// curbe numite in afara unui KeyPairGenerator (ECGenParameterSpec, ECNamedCurveTable); Kotlin: fara new
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; if (consumate.has(m.index)) continue;
ctx.adauga(m.index, m[0].replace(/\s*\(.*$/, ''), 'call', { grup: 'EC', curba: m[1] }, { bucata: m[0] }); ctx.adauga(m.index, m[0].replace(/\s*\(.*$/, ''), 'call', { grup: 'EC', curba: m[1] }, { bucata: m[0] });
} }
@ -203,8 +313,8 @@ export function detecteazaJava(ctx) {
const act = pqNume(t) || (/^X/.test(t) ? { grup: 'XDH', nume: t } : { grup: 'EDDSA', nume: t === 'ED448' ? 'Ed448' : 'Ed25519' }); 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] }); ctx.adauga(m.index, `NamedParameterSpec.${m[1]}`, 'call', act, { bucata: m[0] });
} }
// protocoale TLS activate explicit // protocoale TLS activate explicit: Java new String[]{...}, Kotlin arrayOf(...) / listOf(...)
for (const m of ctx.potriviri(/(?<![\w.])set(?:Enabled)?Protocols\s*\(\s*new\s+String\s*\[\s*\]\s*\{/g)) { for (const m of ctx.potriviri(/(?<![\w.])set(?:Enabled)?Protocols\s*\(\s*(?:new\s+String\s*\[\s*\]\s*\{|arrayOf\s*\(|listOf\s*\()/g)) {
const a = argumente(ctx, m.index + m[0].length); const a = argumente(ctx, m.index + m[0].length);
const vers = [...a.text.matchAll(/"([^"]+)"/g)]; const vers = [...a.text.matchAll(/"([^"]+)"/g)];
const lista = vers.map((x) => TLSV[x[1]]).filter(Boolean); const lista = vers.map((x) => TLSV[x[1]]).filter(Boolean);
@ -215,6 +325,39 @@ export function detecteazaJava(ctx) {
ctx.adauga(a.start + x.index, 'setEnabledProtocols', 'config', { grup: 'TLS', param: ver, rol }, { bucata: `"${x[1]}"` }); ctx.adauga(a.start + x.index, 'setEnabledProtocols', 'config', { grup: 'TLS', param: ver, rol }, { bucata: `"${x[1]}"` });
} }
} }
// OkHttp (ancorat pe importul okhttp3): ConnectionSpec, versiunile si suitele cerute, pinning
if (/^[ \t]*import\s+okhttp3\./m.test(c)) {
for (const m of ctx.potriviri(/(?<![\w.])ConnectionSpec\s*\.\s*Builder\s*\(\s*ConnectionSpec\s*\.\s*(MODERN_TLS|COMPATIBLE_TLS|RESTRICTED_TLS|CLEARTEXT)\s*\)/g)) {
if (m[1] === 'CLEARTEXT') { ctx.adauga(m.index, 'ConnectionSpec.CLEARTEXT', 'config', { grup: 'UNKNOWN', nume: 'OkHttp CLEARTEXT', motiv: 'OkHttp ConnectionSpec.CLEARTEXT: plain HTTP, no TLS at all on this connection spec.' }, { bucata: m[0] }); continue; }
ctx.adauga(m.index, `ConnectionSpec.${m[1]}`, 'config', { grup: 'TLS', param: 'negotiated', nume: `TLS (negotiated, OkHttp ${m[1]})` }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/\.\s*tlsVersions\s*\(/g)) {
const a = argumente(ctx, m.index + m[0].length);
const vers = [...a.text.matchAll(/TlsVersion\s*\.\s*(TLS_1_0|TLS_1_1|TLS_1_2|TLS_1_3|SSL_3_0)\b/g)];
const map = { TLS_1_0: '1.0', TLS_1_1: '1.1', TLS_1_2: '1.2', TLS_1_3: '1.3', SSL_3_0: 'ssl3' };
const lista = vers.map((x) => map[x[1]]);
for (const x of vers) ctx.adauga(a.start + x.index, 'ConnectionSpec.tlsVersions', 'config', { grup: 'TLS', param: map[x[1]], rol: map[x[1]] === '1.2' && lista.includes('1.3') ? 'min' : 'only' }, { bucata: x[0] });
}
for (const m of ctx.potriviri(/\.\s*cipherSuites\s*\(/g)) {
const a = argumente(ctx, m.index + m[0].length);
for (const x of a.text.matchAll(/CipherSuite\s*\.\s*(TLS_[A-Z0-9_]+)\b/g)) ctx.adauga(a.start + x.index, 'ConnectionSpec.cipherSuites', 'config', suitaTls(x[1]), { bucata: x[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])CertificatePinner\s*\.\s*Builder\s*\(/g)) ctx.adauga(m.index, 'CertificatePinner.Builder', 'config', { grup: 'PKI-VERIFY', nume: 'certificate pinning (SPKI)', nota: 'OkHttp CertificatePinner: the pinned SPKI hashes name classical (RSA or EC) keys.' }, { bucata: m[0] });
}
// verificarea lanturilor de certificate (lantul de incredere TLS al clientului): autentificare, nu schimb de cheie
for (const m of ctx.potriviri(/(?<![\w.])(CertPathValidator|TrustManagerFactory)\s*\.\s*getInstance\s*\(/g)) ctx.adauga(m.index, `${m[1]}.getInstance`, 'call', { grup: 'PKI-VERIFY', nume: NUME_PKIX, functii: ['verify'] }, { bucata: m[0] });
for (const m of ctx.potriviri(/(?<![\w.])KeyStore\s*\.\s*getInstance\s*\(\s*"AndroidCAStore"\s*\)/g)) ctx.adauga(m.index, 'KeyStore.getInstance("AndroidCAStore")', 'call', { grup: 'PKI-VERIFY', nume: NUME_PKIX, nota: 'The Android system CA store (the client\'s TLS trust anchors).' }, { bucata: m[0] });
for (const m of ctx.potriviri(/(?<![\w.])X509TrustManager\b(?!\s*\.)/g)) ctx.adauga(m.index, 'X509TrustManager', 'config', { grup: 'PKI-VERIFY', nume: NUME_PKIX, functii: ['verify'] }, { bucata: c.slice(Math.max(0, m.index - 12), m.index + 16) });
for (const m of ctx.potriviri(/\.\s*checkServerTrusted\s*\(/g)) ctx.adauga(m.index, 'checkServerTrusted', 'call', { grup: 'PKI-VERIFY', nume: NUME_PKIX, functii: ['verify'] }, { bucata: m[0].trim() });
// Tink (ancorat pe importul com.google.crypto.tink): sablonul de cheie spune algoritmul; inregistrarea primitivelor e dovada slaba
const tink = ctx.potriviri(/^[ \t]*import\s+com\.google\.crypto\.tink\.[\w.*]+/gm);
if (tink.length) {
const TINK_SUPRIMA = ['CIPHER', 'MAC', 'KDF', 'ECDSA', 'EDDSA', 'RSA', 'XDH', 'ECDH', 'MLDSA', 'MLKEM', 'SLHDSA', 'UNKNOWN'];
for (const m of ctx.potriviri(/(?<![\w.])KeyTemplates\s*\.\s*get\s*\(\s*"([^"]+)"\s*\)/g)) ctx.adauga(m.index, 'KeyTemplates.get', 'call', sablonTink(m[1]), { bucata: m[0] });
for (const m of ctx.potriviri(/(?<![\w.])(?:(?:Aead|Mac|Signature|Hybrid|Daead|StreamingAead|Prf)KeyTemplates|Predefined(?:Aead|Mac|Signature|Hybrid|Daead|StreamingAead|Prf)Parameters)\s*\.\s*([A-Z][A-Z0-9_]+)\b/g)) ctx.adauga(m.index, m[0].replace(/\s+/g, ''), 'call', sablonTink(m[1]), { bucata: m[0] });
for (const m of ctx.potriviri(/(?<![\w.])(Aead|Mac|Signature|Hybrid|Daead|StreamingAead|Prf|Tink)Config\s*\.\s*register\s*\(/g)) ctx.adauga(m.index, `${m[1]}Config.register`, 'import', { grup: 'LIB-MULTI', nume: `Tink ${m[1]} primitives`, motiv: `Tink ${m[1]}Config.register(): registers the primitive family; the algorithm is chosen by the key template, not visible in this file.` }, { suprimaDe: TINK_SUPRIMA, bucata: m[0] });
ctx.adauga(tink[0].index + tink[0][0].indexOf('import'), 'import com.google.crypto.tink', 'import', { grup: 'LIB-MULTI', nume: 'Tink (com.google.crypto.tink)', motiv: 'Imports Tink (AEAD, MAC, signatures, hybrid encryption, HPKE); no key template recognized in this file.' }, { suprimaDe: [...TINK_SUPRIMA, 'LIB-MULTI'], bucata: tink[0][0].trim() });
}
// proprietati de sistem ale JSSE // 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)) { 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; const off = m[0].lastIndexOf('"' + m[2] + '"') + 1;
@ -228,8 +371,8 @@ export function detecteazaJava(ctx) {
else if (TLSV[t]) ctx.adauga(pos, m[1], 'config', { grup: 'TLS', param: TLSV[t], rol: 'only' }, { 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 // Bouncy Castle: importurile (Java cu `;`, Kotlin fara) si parametrii post-cuantici
for (const m of ctx.potriviri(/^[ \t]*import\s+(?:static\s+)?(org\.bouncycastle\.[\w.]+)\s*;/gm)) { for (const m of ctx.potriviri(/^[ \t]*import\s+(?:static\s+)?(org\.bouncycastle\.[\w.]+)\s*(?:;|$)/gm)) {
const act = bcActiv(m[1]); 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]}` }); 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]}` });
} }
@ -237,7 +380,7 @@ export function detecteazaJava(ctx) {
const act = bcParam(m[1], m[2]); const act = bcParam(m[1], m[2]);
if (act) ctx.adauga(m.index, `${m[1]}Parameters.${m[2]}`, 'call', act, { bucata: m[0] }); if (act) ctx.adauga(m.index, `${m[1]}Parameters.${m[2]}`, 'call', act, { bucata: m[0] });
} }
detecteazaLibsignalJava(ctx); detecteazaLibsignalJvm(ctx, kotlin);
void curbaCanonica; void curbaCanonica;
} }
@ -253,23 +396,43 @@ export function kemLibsignal(tip) {
if ((k = /^ML_?KEM_?(768|1024)$/i.exec(tip || ''))) return { grup: 'MLKEM', param: k[1], functii: ['keygen'] }; if ((k = /^ML_?KEM_?(768|1024)$/i.exec(tip || ''))) return { grup: 'MLKEM', param: k[1], functii: ['keygen'] };
return { grup: 'UNKNOWN', nume: 'libsignal KEM key pair', motiv: 'libsignal KEMKeyPair: the key type (Kyber-1024 or ML-KEM) is the KEMKeyType argument, not visible on this line.' }; return { grup: 'UNKNOWN', nume: 'libsignal KEM key pair', motiv: 'libsignal KEMKeyPair: the key type (Kyber-1024 or ML-KEM) is the KEMKeyType argument, not visible on this line.' };
} }
function detecteazaLibsignalJava(ctx) { export const LIBSIGNAL_NOTA_KYBER_PK = 'libsignal KEMPublicKey: a Kyber-1024 public key (a PQXDH prekey), built from bytes or from a native handle.';
export const LIBSIGNAL_NOTA_SEALED = 'libsignal sealed sender: the envelope key is derived with X25519 (an ephemeral key against the recipient\'s identity key) and HKDF-SHA-256, then AES-256-CTR with HMAC-SHA-256 (v1) or AES-256-GCM-SIV (v2); the session inside is X3DH/PQXDH.';
// 0.4.0 (2026-10-18): Kotlin are aceleasi clase fara `new` (Aes256GcmSiv(key), PreKeyBundle(...)); forma fara `new` nu se admite in Java,
// unde ar prinde declaratii. In Kotlin, `class X(` / `fun X(` / `constructor(` sunt declaratii, nu apeluri. Si puntea nativa a bibliotecii
// insesi (Native.ECPrivateKey_Generate(), Native.KyberKeyPair_Generate(), Native.HKDF_DeriveSecrets(...)): asa isi fac cheile sursele
// Kotlin si Java din org.signal.libsignal (ECKeyPair.kt, KEMKeyPair.kt, HKDF.kt), unde ancora e pachetul.
function detecteazaLibsignalJvm(ctx, kotlin) {
const ancore = ctx.potriviri(/(?<![\w.])(?:import\s+(?:static\s+)?org\.signal\.libsignal\.(?:protocol|crypto|metadata|zkgroup|svr|keytrans)\b|package\s+org\.signal\.libsignal\b)/g); const ancore = ctx.potriviri(/(?<![\w.])(?:import\s+(?:static\s+)?org\.signal\.libsignal\.(?:protocol|crypto|metadata|zkgroup|svr|keytrans)\b|package\s+org\.signal\.libsignal\b)/g);
if (!ancore.length) return; if (!ancore.length) return;
for (const m of ctx.potriviri(/(?<![\w.])(?:(ECKeyPair|IdentityKeyPair|ECPrivateKey)\s*\.\s*generate|(Curve)\s*\.\s*generateKeyPair)\s*\(/g)) { const ctor = kotlin ? '(?<![\\w.])(?<!class\\s)(?<!fun\\s)(?<!constructor\\s)(?<!object\\s)' : '(?<![\\w.])new\\s+';
ctx.adauga(m.index, `${m[1] || m[2]}.${m[1] ? 'generate' : 'generateKeyPair'}`, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: LIBSIGNAL_NOTA_EC }, { bucata: m[0] }); const api = (nume) => (kotlin ? nume : `new ${nume}`);
for (const m of ctx.potriviri(/(?<![\w.])(?:(ECKeyPair|IdentityKeyPair|ECPrivateKey)\s*\.\s*generate|(Curve)\s*\.\s*generateKeyPair|Native\s*\.\s*(ECPrivateKey_Generate|IdentityKeyPair_Generate))\s*\(/g)) {
const nume = m[3] ? `Native.${m[3]}` : `${m[1] || m[2]}.${m[1] ? 'generate' : 'generateKeyPair'}`;
ctx.adauga(m.index, nume, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: LIBSIGNAL_NOTA_EC }, { bucata: m[0] });
} }
for (const m of ctx.potriviri(/(?<![\w.])KEMKeyPair\s*\.\s*generate\s*\(\s*(?:KEMKeyType\s*\.\s*([A-Za-z_0-9]+))?/g)) { for (const m of ctx.potriviri(/(?<![\w.])KEMKeyPair\s*\.\s*generate\s*\(\s*(?:KEMKeyType\s*\.\s*([A-Za-z_0-9]+))?/g)) {
ctx.adauga(m.index, 'KEMKeyPair.generate', 'call', kemLibsignal(m[1]), { bucata: m[0].trim() }); ctx.adauga(m.index, 'KEMKeyPair.generate', 'call', kemLibsignal(m[1]), { bucata: m[0].trim() });
} }
for (const m of ctx.potriviri(/(?<![\w.])HKDF\s*\.\s*deriveSecrets\s*\(/g)) { for (const m of ctx.potriviri(/(?<![\w.])Native\s*\.\s*KyberKeyPair_Generate\s*\(/g)) ctx.adauga(m.index, 'Native.KyberKeyPair_Generate', 'call', kemLibsignal('KYBER_1024'), { bucata: m[0] });
ctx.adauga(m.index, 'HKDF.deriveSecrets', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, { bucata: m[0] }); // puntea cheii Curve25519 (ECPrivateKey.kt / ECPrivateKey.java): acordul X25519, semnatura XEdDSA pe aceeasi cheie, HPKE (RFC 9180, DHKEM X25519)
for (const m of ctx.potriviri(/(?<![\w.])Native\s*\.\s*(ECPrivateKey_Agree|ECPrivateKey_Sign|ECPublicKey_Verify|ECPrivateKey_HpkeOpen|ECPublicKey_HpkeSeal)\s*\(/g)) {
const act = m[1] === 'ECPrivateKey_Agree' ? { grup: 'XDH', nume: 'X25519', functii: ['other'], nota: 'libsignal Diffie-Hellman agreement over Curve25519.' }
: /Hpke/.test(m[1]) ? { grup: 'XDH', nume: 'X25519', primitiv: 'pke', functii: [m[1] === 'ECPublicKey_HpkeSeal' ? 'encrypt' : 'decrypt'], nota: 'libsignal HPKE (RFC 9180) with DHKEM X25519: the key encapsulation is classical.' }
: { grup: 'EDDSA', nume: 'XEdDSA (Curve25519)', functii: [m[1] === 'ECPublicKey_Verify' ? 'verify' : 'sign'], nota: 'libsignal XEdDSA: an Ed25519-style signature made with the Curve25519 (X25519) identity key.' };
ctx.adauga(m.index, `Native.${m[1]}`, 'call', act, { bucata: m[0] });
} }
for (const m of ctx.potriviri(/(?<![\w.])new\s+(Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32)\s*\(/g)) { for (const m of ctx.potriviri(new RegExp(`${ctor}KEMPublicKey\\s*\\(`, 'g'))) ctx.adauga(m.index, api('KEMPublicKey'), 'call', { grup: 'PREPQ', nume: 'Kyber-1024', primitiv: 'kem', functii: ['other'], nota: LIBSIGNAL_NOTA_KYBER_PK }, { bucata: m[0] });
const mod = m[1] === 'Aes256GcmSiv' ? 'gcm-siv' : m[1] === 'Aes256Ctr32' ? 'ctr' : 'gcm'; for (const m of ctx.potriviri(/(?<![\w.])(?:HKDF\s*\.\s*deriveSecrets|Native\s*\.\s*HKDF_DeriveSecrets)\s*\(/g)) {
ctx.adauga(m.index, `new ${m[1]}`, 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod, functii: [m[1] === 'Aes256GcmDecryption' ? 'decrypt' : 'encrypt'] }, { bucata: m[0] }); ctx.adauga(m.index, /Native/.test(m[0]) ? 'Native.HKDF_DeriveSecrets' : 'HKDF.deriveSecrets', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, { bucata: m[0] });
} }
for (const m of ctx.potriviri(/(?<![\w.])new\s+PreKeyBundle\s*\(/g)) ctx.adauga(m.index, 'new PreKeyBundle', 'call', LIBSIGNAL_BUNDLE, { bucata: m[0] }); for (const m of ctx.potriviri(new RegExp(`${ctor}(Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32)\\s*\\(|(?<![\\w.])Native\\s*\\.\\s*(Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32)_New\\s*\\(`, 'g'))) {
const tip = m[1] || m[2];
const mod = tip === 'Aes256GcmSiv' ? 'gcm-siv' : tip === 'Aes256Ctr32' ? 'ctr' : 'gcm';
ctx.adauga(m.index, m[2] ? `Native.${m[2]}_New` : api(tip), 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod, functii: [tip === 'Aes256GcmDecryption' ? 'decrypt' : 'encrypt'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(new RegExp(`${ctor}PreKeyBundle\\s*\\(|(?<![\\w.])Native\\s*\\.\\s*(?:PreKeyBundle_New|SessionBuilder_ProcessPreKeyBundle)\\s*\\(`, 'g'))) ctx.adauga(m.index, /Native/.test(m[0]) ? m[0].replace(/\s+|\($/g, '') : api('PreKeyBundle'), 'call', LIBSIGNAL_BUNDLE, { bucata: m[0] });
for (const m of ctx.potriviri(new RegExp(`${ctor}SealedSessionCipher\\s*\\(`, 'g'))) ctx.adauga(m.index, api('SealedSessionCipher'), 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: LIBSIGNAL_NOTA_SEALED }, { bucata: m[0] });
// importul: dovada slaba, o singura data pe fisier, suprimata de orice apel recunoscut de mai sus // importul: dovada slaba, o singura data pe fisier, suprimata de orice apel recunoscut de mai sus
const imp = ancore.find((a) => /^import/.test(a[0])); const imp = ancore.find((a) => /^import/.test(a[0]));
if (imp) ctx.adauga(imp.index, 'import org.signal.libsignal', 'import', { grup: 'LIB-MULTI', nume: 'libsignal (org.signal.libsignal)', motiv: 'Imports the Signal protocol library (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender); no specific call recognized in this file.' }, { suprimaDe: ['XDH', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'LIB-MULTI'], bucata: imp[0].trim() }); if (imp) ctx.adauga(imp.index, 'import org.signal.libsignal', 'import', { grup: 'LIB-MULTI', nume: 'libsignal (org.signal.libsignal)', motiv: 'Imports the Signal protocol library (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender); no specific call recognized in this file.' }, { suprimaDe: ['XDH', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'LIB-MULTI'], bucata: imp[0].trim() });

View File

@ -0,0 +1,12 @@
// Detectorul Kotlin (0.4.0, 2026-10-18; scris dupa rularea pe libsignal: 111 fisiere Kotlin, 24.117 randuri, neanalizate prin
// constructie). E un adaptor subtire peste detectorul Java: JCA/JSSE, Android Keystore, Tink, OkHttp, verificarea PKIX si API-ul
// libsignal sunt aceleasi clase si aceleasi metode; ce difera e sintaxa (fara `new`, fara `;`, `val`/`var`, `arrayOf(...)`,
// `ECGenParameterSpec("...")` fara `new`, `Aes256GcmSiv(key)` fara `new`), si lexerul (comentarii imbricate, sabloane `${ }`,
// siruri `"""`, literale char), care sta in lexer.mjs. Rezolvarea variabilelor (`val X = "..."`) sta in context.mjs.
// Ce NU se recunoaste: extensiile Kotlin peste JCA (`"text".toByteArray().sha256()` dintr-o biblioteca proprie), DSL-urile care
// construiesc numele algoritmului din bucati, si `libs.versions.toml` altfel decat ca manifest.
import { detecteazaJvm } from './detect-java.mjs';
export function detecteazaKotlin(ctx) {
detecteazaJvm(ctx, true);
}

View File

@ -0,0 +1,239 @@
// Detectorul Swift (0.4.0, 2026-10-18; scris dupa rularea pe libsignal: 210 fisiere Swift, 44.923 randuri, neanalizate prin
// constructie). In Swift un modul se vede in fisier numai dupa `import Modul`, deci importurile spun ce poate insemna un nume:
// - CryptoKit / Crypto (swift-crypto, acelasi API): SHA256.hash, Insecure.MD5/SHA1, HMAC<SHA256>, AES.GCM.seal/open, ChaChaPoly,
// Curve25519.KeyAgreement / .Signing, P256/P384/P521.Signing / .KeyAgreement, HKDF<SHA256>.deriveKey, SymmetricKey(size:),
// _RSA (din _CryptoExtras);
// - CommonCrypto: CC_SHA256 & co, CCCrypt / CCCryptorCreate cu kCCAlgorithm* si kCCOption*, CCHmac cu kCCHmacAlg*, CCKeyDerivationPBKDF;
// - Security: SecKeyCreateRandomKey (tipul si marimea din dictionarul de atribute, inline sau dintr-o variabila a fisierului),
// SecKeyCreateSignature / SecKeyVerifySignature / SecKeyCreateEncryptedData / SecKeyCreateDecryptedData cu algoritmul .rsa*/.ecdsa*/.ecies*,
// SecTrustEvaluate* si SecPolicyCreateSSL (verificare de certificate: autentificare, nu schimb de cheie);
// - TLS-ul clientului: tlsMinimumSupportedProtocolVersion / tlsMaximumSupportedProtocolVersion (URLSessionConfiguration),
// sec_protocol_options_set_min/max_tls_protocol_version, NWProtocolTLS.Options();
// - CryptoSwift: AES(key:blockMode:), SHA2(variant:), .md5()/.sha256(), HMAC(key:variant:), PBKDF2, ChaCha20, Poly1305, RSA(keySize:), Blowfish;
// - Sodium / Clibsodium: sodium.box / .sign / .keyExchange / .secretBox / .aead.xchacha20poly1305ietf / .pwHash / .genericHash si functiile C crypto_*;
// - LibSignalClient (si SignalFfi, modulul FFI pe care il importa chiar sursele bibliotecii): IdentityKeyPair.generate(), PrivateKey.generate(),
// KEMKeyPair.generate() (Kyber-1024: API-ul Swift nu are parametru de tip, puntea genereaza kem::KeyType::Kyber1024), KEMPublicKey(...),
// Aes256GcmSiv(key:), Aes256GcmEncryptedData, Aes256GcmEncryption/Decryption, Aes256Ctr32, hkdf(...), PreKeyBundle(...), processPreKeyBundle(...).
// Importul unei biblioteci terte cu multe primitive (LibSignalClient, CryptoSwift, Sodium) fara niciun apel recunoscut e dovada slaba
// (unknown, o data pe fisier); cadrele de sistem (CryptoKit, CommonCrypto, Security) nu se raporteaza la import: Security se importa
// pentru keychain si SecRandomCopyBytes in aproape orice aplicatie.
// Ce NU se recunoaste: un modul adus printr-un bridging header Objective-C (fara `import` in fisierul Swift), tipurile proprii care
// invelesc un cadru, metodele pe valori fara constructorul lor in acelasi fisier, `#if DEBUG` (scopul se deriva din cale, nu din modul).
import { argumente, escapeRe } from './context.mjs';
import { hashCanonic } from './catalog.mjs';
const NUME_PKIX = 'X.509 chain validation (PKIX)';
const NOTA_EC_LIBSIGNAL = 'libsignal Curve25519 key pair: used for Diffie-Hellman (X3DH/PQXDH, Double Ratchet) and for XEdDSA signatures over the same key.';
export const NOTA_KEM_SWIFT = 'libsignal Swift KEMKeyPair.generate() takes no key type: the bridge generates kem::KeyType::Kyber1024 (rust/bridge/shared/src/protocol.rs, KYBER_KEY_TYPE), the PQXDH prekey; CRYSTALS-Kyber is the pre-standard form of ML-KEM.';
const BUNDLE = { grup: 'LIB-MULTI', nume: 'libsignal session setup (X3DH/PQXDH)', motiv: 'libsignal prekey bundle: Curve25519 identity and signed prekey; a Kyber prekey in the bundle makes the session PQXDH (hybrid). Which prekeys the bundle carries is decided at run time.' };
const TLSV = { TLSv10: '1.0', TLSv11: '1.1', TLSv12: '1.2', TLSv13: '1.3', tlsProtocol1: '1.0', tlsProtocol11: '1.1', tlsProtocol12: '1.2', tlsProtocol13: '1.3', sslProtocol3: 'ssl3' };
const CURBA_P = { P256: 'secp256r1', P384: 'secp384r1', P521: 'secp521r1' };
export function importuriSwift(ctx) {
const r = new Map(); // modul -> pozitia primului import
for (const m of ctx.potriviri(/^[ \t]*(?:@\w+(?:\([^)]*\))?[ \t]+)*import[ \t]+(?:(?:class|struct|enum|protocol|typealias|func|var|let)[ \t]+)?([A-Za-z_]\w*)/gm)) {
if (!r.has(m[1])) r.set(m[1], m.index + m[0].indexOf('import'));
}
return r;
}
// algoritmul SecKeyAlgorithm (.rsaSignatureMessagePKCS1v15SHA256, .ecdsaSignatureMessageX962SHA256, .rsaEncryptionOAEPSHA256, ...)
export function algoritmSecKey(nume, functie) {
let m;
const hash = (s) => { const h = /SHA(1|224|256|384|512)$/.exec(s); return h ? hashCanonic('SHA' + h[1]) : undefined; };
if ((m = /^rsaSignature(?:Message|Digest)(PKCS1v15|PSS)(?:SHA\d+|Raw)?$/.exec(nume))) return { grup: 'RSA', primitiv: 'signature', padding: m[1] === 'PSS' ? 'other' : 'pkcs1v15', hash: hash(nume), functii: [functie] };
if ((m = /^rsaSignatureRaw$/.exec(nume))) return { grup: 'RSA', primitiv: 'signature', padding: 'raw', functii: [functie] };
if ((m = /^rsaEncryption(OAEP|PKCS1|Raw)/.exec(nume))) return { grup: 'RSA', primitiv: 'pke', padding: m[1] === 'OAEP' ? 'oaep' : m[1] === 'PKCS1' ? 'pkcs1v15' : 'raw', hash: hash(nume.replace(/AESGCM$/, '')), functii: [functie] };
if ((m = /^ecdsaSignature(?:Message|Digest|Rfc4754)(?:X962|RFC4754)?(?:SHA\d+)?$/.exec(nume))) return { grup: 'ECDSA', hash: hash(nume), functii: [functie] };
if (/^eciesEncryption/.test(nume)) return { grup: 'ECDH', nume: `ECIES (${nume})`, primitiv: 'pke', functii: [functie] };
if (/^ecdhKeyExchange/.test(nume)) return { grup: 'ECDH', functii: ['keygen'] };
return { grup: 'UNKNOWN', nume: `SecKeyAlgorithm.${nume}`, motiv: `SecKeyAlgorithm .${nume} is not in this tool's catalog.` };
}
// tipul si marimea cheii dintr-un dictionar de atribute SecKey (text): kSecAttrKeyTypeRSA / kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeySizeInBits
function cheieDinAtribute(text) {
const tip = /kSecAttrKeyType(RSA|ECSECPrimeRandom|ECDSA|EC)\b/.exec(text);
const bits = /kSecAttrKeySizeInBits(?:\s+as\s+String)?\s*:\s*(\d+)/.exec(text);
if (!tip) return null;
if (tip[1] === 'RSA') return { grup: 'RSA', param: bits ? bits[1] : undefined, functii: ['keygen'] };
const curba = bits ? ({ 256: 'secp256r1', 384: 'secp384r1', 521: 'secp521r1', 224: 'secp224r1', 192: 'secp192r1' })[bits[1]] : undefined;
return { grup: 'EC', curba, functii: ['keygen'] };
}
export function detecteazaSwift(ctx) {
const c = ctx.code;
const imp = importuriSwift(ctx);
const are = (...module) => module.some((m) => imp.has(m));
const folosite = new Set(); // bibliotecile terte cu cel putin un apel recunoscut (nu primesc dovada slaba de import)
const adauga = (lib, pos, api, fel, activ, bucata) => { if (lib) folosite.add(lib); ctx.adauga(pos, api, fel, activ, { bucata }); };
const randul = (pos) => { const a = c.lastIndexOf('\n', pos) + 1; const b = c.indexOf('\n', pos); return c.slice(a, b < 0 ? c.length : b); };
const argsDupa = (m) => argumente(ctx, m.index + m[0].length);
// --- CryptoKit / swift-crypto ---------------------------------------------------------------------------------------------------
if (are('CryptoKit', 'Crypto', '_CryptoExtras')) {
const bitsPeRand = (pos) => { const b = /SymmetricKey\s*\(\s*size\s*:\s*\.bits(128|192|256)\s*\)/.exec(randul(pos)); return b ? b[1] : undefined; };
for (const m of ctx.potriviri(/(?<![\w.])(SHA256|SHA384|SHA512)\s*(?:\.\s*hash\s*\(|\(\s*\))/g)) adauga(null, m.index, `${m[1]}${/hash/.test(m[0]) ? '.hash' : '()'}`, 'call', { grup: 'HASH', hash: m[1], functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])Insecure\s*\.\s*(MD5|SHA1)\s*(?:\.\s*hash\s*\(|\(\s*\))/g)) adauga(null, m.index, `Insecure.${m[1]}`, 'call', { grup: 'HASH', hash: m[1], functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])HMAC\s*<\s*(?:Insecure\s*\.\s*)?(MD5|SHA1|SHA256|SHA384|SHA512)\s*>/g)) adauga(null, m.index, `HMAC<${m[1]}>`, 'call', { grup: 'MAC', hash: m[1], functii: ['tag'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])HKDF\s*<\s*(?:Insecure\s*\.\s*)?(MD5|SHA1|SHA256|SHA384|SHA512)\s*>\s*\.\s*(deriveKey|extract|expand)/g)) adauga(null, m.index, `HKDF<${m[1]}>.${m[2]}`, 'call', { grup: 'KDF', nume: 'HKDF', hash: m[1], functii: ['keyderive'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])AES\s*\.\s*GCM\s*\.\s*(seal|open|SealedBox|Nonce)\b/g)) adauga(null, m.index, `AES.GCM.${m[1]}`, 'call', { grup: 'CIPHER', nume: 'AES', param: bitsPeRand(m.index), mod: 'gcm', functii: [m[1] === 'open' ? 'decrypt' : 'encrypt'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])AES\s*\.\s*KeyWrap\s*\.\s*(wrap|unwrap)\b/g)) adauga(null, m.index, `AES.KeyWrap.${m[1]}`, 'call', { grup: 'CIPHER', nume: 'AES', param: bitsPeRand(m.index), mod: 'kw', functii: ['encrypt'], nota: 'AES key wrap (RFC 3394).' }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])ChaChaPoly\s*\.\s*(seal|open|SealedBox|Nonce)\b/g)) adauga(null, m.index, `ChaChaPoly.${m[1]}`, 'call', { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: [m[1] === 'open' ? 'decrypt' : 'encrypt'] }, m[0]);
// constructorii (PrivateKey(), PublicKey(rawRepresentation:)), nu tipul scris intr-o semnatura (`peer: Curve25519.KeyAgreement.PublicKey`)
for (const m of ctx.potriviri(/(?<![\w.])Curve25519\s*\.\s*(KeyAgreement|Signing)\s*\.\s*(PrivateKey|PublicKey)\s*\(/g)) {
adauga(null, m.index, `Curve25519.${m[1]}.${m[2]}`, 'call', m[1] === 'KeyAgreement' ? { grup: 'XDH', nume: 'X25519', functii: [m[2] === 'PrivateKey' ? 'keygen' : 'other'] } : { grup: 'EDDSA', nume: 'Ed25519', functii: [m[2] === 'PrivateKey' ? 'keygen' : 'verify'] }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])(P256|P384|P521)\s*\.\s*(KeyAgreement|Signing)\s*\.\s*(PrivateKey|PublicKey|ECDSASignature)\s*\(/g)) {
const curba = CURBA_P[m[1]];
adauga(null, m.index, `${m[1]}.${m[2]}.${m[3]}`, 'call', m[2] === 'KeyAgreement' ? { grup: 'ECDH', curba, functii: [m[3] === 'PrivateKey' ? 'keygen' : 'other'] } : { grup: 'ECDSA', curba, functii: [m[3] === 'PrivateKey' ? 'keygen' : 'verify'] }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])_RSA\s*\.\s*(Signing|Encryption)\s*\.\s*(PrivateKey|PublicKey)\s*(\(\s*keySize\s*:\s*\.bits(\d+)|\()/g)) {
adauga(null, m.index, `_RSA.${m[1]}.${m[2]}`, 'call', { grup: 'RSA', param: m[4], primitiv: m[1] === 'Signing' ? 'signature' : 'pke', functii: [m[2] === 'PrivateKey' ? 'keygen' : 'other'] }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])SymmetricKey\s*\(\s*size\s*:\s*\.bits(128|192|256)\s*\)/g)) {
if (/AES\s*\.\s*(GCM|KeyWrap)|ChaChaPoly|HMAC\s*<|HKDF\s*</.test(randul(m.index))) continue; // marimea e deja in cifrul de pe acelasi rand
adauga(null, m.index, 'SymmetricKey(size:)', 'call', { grup: 'UNKNOWN', nume: `SymmetricKey (${m[1]}-bit)`, motiv: `CryptoKit SymmetricKey of ${m[1]} bits: the algorithm that uses it (AES-GCM, ChaChaPoly, HMAC, HKDF) is chosen where the key is used, not here.` }, m[0]);
}
}
// --- CommonCrypto ------------------------------------------------------------------------------------------------------------------
if (are('CommonCrypto')) {
for (const m of ctx.potriviri(/(?<![\w.])CC_(MD2|MD4|MD5|SHA1|SHA224|SHA256|SHA384|SHA512)(?:_Init|_Update|_Final)?\s*\(/g)) adauga(null, m.index, m[0].replace(/\s*\($/, ''), 'call', { grup: 'HASH', hash: m[1], functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])(CCCrypt|CCCryptorCreate(?:WithMode)?)\s*\(/g)) {
const t = argsDupa(m).text;
const alg = /kCCAlgorithm(AES128|AES|DES|3DES|CAST|RC4|RC2|Blowfish)\b/.exec(t);
if (!alg) { adauga(null, m.index, m[1], 'call', { grup: 'UNKNOWN', nume: m[1], motiv: `${m[1]}: the algorithm constant (kCCAlgorithm*) is not among the arguments of this call.` }, m[0]); continue; }
const bits = /kCCKeySizeAES(128|192|256)\b/.exec(t);
const mod = /kCCOptionECBMode\b/.test(t) ? 'ecb' : /kCCModeCTR\b/.test(t) ? 'ctr' : /kCCModeGCM\b/.test(t) ? 'gcm' : /kCCModeCFB\b/.test(t) ? 'cfb' : /kCCModeOFB\b/.test(t) ? 'ofb' : 'cbc';
const nume = { AES128: 'AES', AES: 'AES', DES: 'DES', '3DES': '3DES', CAST: 'CAST5', RC4: 'RC4', RC2: 'RC2', Blowfish: 'Blowfish' }[alg[1]];
adauga(null, m.index, `${m[1]}(kCCAlgorithm${alg[1]})`, 'call', { grup: 'CIPHER', nume, param: nume === 'AES' ? (bits ? bits[1] : undefined) : undefined, mod: /^(RC4)$/.test(nume) ? undefined : mod, functii: [/kCCDecrypt\b/.test(t) ? 'decrypt' : 'encrypt'], nota: nume === 'AES' && mod === 'cbc' && !/kCCMode/.test(t) ? 'CommonCrypto block ciphers default to CBC unless kCCOptionECBMode is set.' : undefined }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])CCHmac(?:Init)?\s*\(/g)) {
const h = /kCCHmacAlg(MD5|SHA1|SHA224|SHA256|SHA384|SHA512)\b/.exec(argsDupa(m).text);
adauga(null, m.index, 'CCHmac', 'call', h ? { grup: 'MAC', hash: h[1], functii: ['tag'] } : { grup: 'MAC', motiv: 'CCHmac: the hash constant (kCCHmacAlg*) is not among the arguments of this call.' }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])CCKeyDerivationPBKDF\s*\(/g)) {
const h = /kCCPRFHmacAlg(SHA1|SHA224|SHA256|SHA384|SHA512)\b/.exec(argsDupa(m).text);
adauga(null, m.index, 'CCKeyDerivationPBKDF', 'call', { grup: 'KDF', nume: h ? `PBKDF2-HMAC-${h[1]}` : 'PBKDF2', hash: h ? h[1] : undefined, functii: ['keyderive'] }, m[0]);
}
}
// --- Security.framework --------------------------------------------------------------------------------------------------------------
if (are('Security')) {
for (const m of ctx.potriviri(/(?<![\w.])SecKeyCreateRandomKey\s*\(/g)) {
const a = argsDupa(m);
let act = cheieDinAtribute(a.text);
if (!act) {
const id = /^\s*([A-Za-z_]\w*)\b/.exec(a.text);
const def = id && new RegExp(`(?<![\\w.])(?:let|var)\\s+${escapeRe(id[1])}\\b[^=\\n]*=\\s*\\[`).exec(c);
if (def) act = cheieDinAtribute(c.slice(def.index, argumente(ctx, def.index + def[0].length).end + 1));
}
adauga(null, m.index, 'SecKeyCreateRandomKey', 'call', act || { grup: 'UNKNOWN', nume: 'SecKeyCreateRandomKey', motiv: 'SecKeyCreateRandomKey: the key type (kSecAttrKeyType) is not in the attributes of this call nor in a dictionary defined in this file.' }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])SecKey(CreateSignature|VerifySignature|CreateEncryptedData|CreateDecryptedData)\s*\(/g)) {
const a = argsDupa(m);
const fn = { CreateSignature: 'sign', VerifySignature: 'verify', CreateEncryptedData: 'encrypt', CreateDecryptedData: 'decrypt' }[m[1]];
let alg = /(?:SecKeyAlgorithm\s*)?\.\s*((?:rsa|ecdsa|ecies|ecdh)\w+)/.exec(a.text);
if (!alg) {
const parti = a.text.split(',');
const id = parti[1] && /^\s*([A-Za-z_]\w*)\s*$/.exec(parti[1]);
if (id) alg = new RegExp(`(?<![\\w.])(?:let|var)\\s+${escapeRe(id[1])}\\s*(?::\\s*SecKeyAlgorithm\\s*)?=\\s*(?:SecKeyAlgorithm\\s*)?\\.\\s*((?:rsa|ecdsa|ecies|ecdh)\\w+)`).exec(c);
}
adauga(null, m.index, `SecKey${m[1]}`, 'call', alg ? algoritmSecKey(alg[1], fn) : { grup: 'UNKNOWN', nume: `SecKey${m[1]}`, motiv: `SecKey${m[1]}: the SecKeyAlgorithm is not a literal in this call nor a variable set once in this file.` }, m[0] + (alg ? `.${alg[1]}` : ''));
}
for (const m of ctx.potriviri(/(?<![\w.])(SecTrustEvaluate(?:WithError|AsyncWithError|Async)?|SecPolicyCreateSSL)\s*\(/g)) adauga(null, m.index, m[1], 'call', { grup: 'PKI-VERIFY', nume: NUME_PKIX, functii: ['verify'] }, m[0]);
}
// --- TLS-ul clientului (URLSession, Network) ---------------------------------------------------------------------------------------
for (const m of ctx.potriviri(/\.\s*tls(Minimum|Maximum)SupportedProtocolVersion\s*=\s*\.(TLSv10|TLSv11|TLSv12|TLSv13)\b/g)) adauga(null, m.index, `tls${m[1]}SupportedProtocolVersion`, 'config', { grup: 'TLS', param: TLSV[m[2]], rol: m[1] === 'Minimum' ? 'min' : 'max' }, m[0].trim());
for (const m of ctx.potriviri(/\.\s*TLS(Minimum|Maximum)SupportedProtocol\s*=\s*\.(tlsProtocol1|tlsProtocol11|tlsProtocol12|tlsProtocol13|sslProtocol3)\b/g)) adauga(null, m.index, `TLS${m[1]}SupportedProtocol`, 'config', { grup: 'TLS', param: TLSV[m[2]], rol: m[1] === 'Minimum' ? 'min' : 'max' }, m[0].trim());
for (const m of ctx.potriviri(/(?<![\w.])sec_protocol_options_set_(min|max)_tls_protocol_version\s*\(\s*[^,]+,\s*\.(TLSv10|TLSv11|TLSv12|TLSv13)\b/g)) adauga(null, m.index, `sec_protocol_options_set_${m[1]}_tls_protocol_version`, 'config', { grup: 'TLS', param: TLSV[m[2]], rol: m[1] }, m[0]);
if (are('Network')) for (const m of ctx.potriviri(/(?<![\w.])NWProtocolTLS\s*\.\s*Options\s*\(/g)) adauga(null, m.index, 'NWProtocolTLS.Options', 'config', { grup: 'TLS', param: 'negotiated' }, m[0]);
// --- CryptoSwift -----------------------------------------------------------------------------------------------------------------------
if (are('CryptoSwift')) {
const L = 'CryptoSwift';
for (const m of ctx.potriviri(/(?<![\w.])(AES|Blowfish)\s*\(\s*key\s*:/g)) {
const mod = /blockMode\s*:\s*(GCM|CBC|ECB|CTR|CCM|OFB|CFB|PCBC)\b/.exec(argsDupa(m).text);
adauga(L, m.index, `${m[1]}(key:blockMode:)`, 'call', { grup: 'CIPHER', nume: m[1], mod: mod ? mod[1].toLowerCase() : undefined, functii: ['encrypt', 'decrypt'] }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])SHA2\s*\(\s*variant\s*:\s*\.(sha224|sha256|sha384|sha512)\b/g)) adauga(L, m.index, 'SHA2(variant:)', 'call', { grup: 'HASH', hash: m[1].toUpperCase(), functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])SHA3\s*\(\s*variant\s*:\s*\.(sha224|sha256|sha384|sha512|keccak224|keccak256|keccak384|keccak512)\b/g)) adauga(L, m.index, 'SHA3(variant:)', 'call', { grup: 'HASH', hash: /^keccak/.test(m[1]) ? 'KECCAK256' : 'SHA3-' + m[1].slice(3), functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])(?:MD5|SHA1)\s*\(\s*\)/g)) adauga(L, m.index, m[0].replace(/\s+/g, ''), 'call', { grup: 'HASH', hash: m[0].startsWith('MD5') ? 'MD5' : 'SHA1', functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])Digest\s*\.\s*(md5|sha1|sha224|sha256|sha384|sha512)\s*\(/g)) adauga(L, m.index, `Digest.${m[1]}`, 'call', { grup: 'HASH', hash: m[1].toUpperCase(), functii: ['digest'] }, m[0]);
for (const m of ctx.potriviri(/\.\s*(md5|sha1|sha224|sha256|sha384|sha512)\s*\(\s*\)/g)) adauga(L, m.index, `.${m[1]}()`, 'call', { grup: 'HASH', hash: m[1].toUpperCase(), functii: ['digest'] }, m[0].trim());
for (const m of ctx.potriviri(/(?<![\w.])HMAC\s*\(\s*key\s*:/g)) {
const v = /variant\s*:\s*\.(md5|sha1|sha224|sha256|sha384|sha512)\b/.exec(argsDupa(m).text);
adauga(L, m.index, 'HMAC(key:variant:)', 'call', v ? { grup: 'MAC', hash: v[1].toUpperCase(), functii: ['tag'] } : { grup: 'MAC', hash: 'MD5', functii: ['tag'], nota: 'CryptoSwift HMAC without variant: defaults to MD5.' }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])(?:PKCS5\s*\.\s*)?PBKDF2\s*\(/g)) {
const v = /variant\s*:\s*\.(md5|sha1|sha224|sha256|sha384|sha512)\b/.exec(argsDupa(m).text);
adauga(L, m.index, 'PBKDF2', 'call', { grup: 'KDF', nume: v ? `PBKDF2-HMAC-${v[1].toUpperCase()}` : 'PBKDF2', hash: v ? v[1].toUpperCase() : undefined, functii: ['keyderive'] }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])Scrypt\s*\(/g)) adauga(L, m.index, 'Scrypt', 'call', { grup: 'KDF', nume: 'scrypt', functii: ['keyderive'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])ChaCha20\s*\(\s*key\s*:/g)) adauga(L, m.index, 'ChaCha20(key:iv:)', 'call', { grup: 'CIPHER', nume: 'ChaCha20', functii: ['encrypt', 'decrypt'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])Poly1305\s*\(\s*key\s*:/g)) adauga(L, m.index, 'Poly1305(key:)', 'call', { grup: 'MAC', nume: 'Poly1305', functii: ['tag'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])Rabbit\s*\(\s*key\s*:/g)) adauga(L, m.index, 'Rabbit(key:)', 'call', { grup: 'CIPHER', nume: 'Rabbit', functii: ['encrypt', 'decrypt'] }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])RSA\s*\(\s*(?:keySize\s*:\s*(\d+)|[nedp]\s*:|rawRepresentation\s*:|publicDER|privateDER)/g)) adauga(L, m.index, 'RSA(...)', 'call', { grup: 'RSA', param: m[1], functii: [m[1] ? 'keygen' : 'other'] }, m[0]);
}
// --- Sodium (swift-sodium) si Clibsodium ------------------------------------------------------------------------------------------------
if (are('Sodium', 'Clibsodium')) {
const L = imp.has('Sodium') ? 'Sodium' : 'Clibsodium';
const X = (nota) => ({ grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota });
const FORME = [
[/\.\s*box\s*\.\s*(keyPair|seal|open|beforenm)\s*\(/g, 'sodium.box', () => X('libsodium crypto_box: X25519 key agreement with XSalsa20-Poly1305.')],
[/\.\s*sealedBox\s*\.\s*(seal|open)\s*\(/g, 'sodium.sealedBox', () => X('libsodium sealed box: an ephemeral X25519 key against the recipient key, XSalsa20-Poly1305.')],
[/\.\s*keyExchange\s*\.\s*(keyPair|sessionKeyPair)\s*\(/g, 'sodium.keyExchange', () => X('libsodium crypto_kx: X25519 with BLAKE2b.')],
[/\.\s*sign\s*\.\s*(keyPair|signature|sign|verify|open)\s*\(/g, 'sodium.sign', (m) => ({ grup: 'EDDSA', nume: 'Ed25519', functii: [m[1] === 'keyPair' ? 'keygen' : /verify|open/.test(m[1]) ? 'verify' : 'sign'] })],
[/\.\s*secretBox\s*\.\s*(seal|open)\s*\(/g, 'sodium.secretBox', (m) => ({ grup: 'CIPHER', nume: 'XSalsa20-Poly1305', functii: [m[1] === 'open' ? 'decrypt' : 'encrypt'] })],
[/\.\s*aead\s*\.\s*xchacha20poly1305ietf\s*\.\s*(encrypt|decrypt)\s*\(/g, 'sodium.aead.xchacha20poly1305ietf', (m) => ({ grup: 'CIPHER', nume: 'XChaCha20-Poly1305', functii: [m[1]] })],
[/\.\s*pwHash\s*\.\s*(hash|str|strVerify)\s*\(/g, 'sodium.pwHash', () => ({ grup: 'KDF', nume: 'Argon2id', functii: ['keyderive'] })],
[/\.\s*genericHash\s*\.\s*(hash|initStream)\s*\(/g, 'sodium.genericHash', () => ({ grup: 'HASH', hash: 'BLAKE2B', functii: ['digest'] })],
[/(?<![\w.])crypto_(?:box|scalarmult|kx)(?:_\w+)?\s*\(/g, 'crypto_box', () => X('libsodium crypto_box / crypto_kx / crypto_scalarmult: X25519.')],
[/(?<![\w.])crypto_sign(?:_\w+)?\s*\(/g, 'crypto_sign', (m) => ({ grup: 'EDDSA', nume: 'Ed25519', functii: [/keypair/.test(m[0]) ? 'keygen' : /verify|open/.test(m[0]) ? 'verify' : 'sign'] })],
[/(?<![\w.])crypto_aead_(xchacha20poly1305|chacha20poly1305|aes256gcm|aegis256|aegis128l)(?:_ietf)?_(encrypt|decrypt)(?:_detached)?\s*\(/g, 'crypto_aead', (m) => ({ grup: 'CIPHER', nume: { xchacha20poly1305: 'XChaCha20-Poly1305', chacha20poly1305: 'ChaCha20-Poly1305', aes256gcm: 'AES', aegis256: 'AEGIS-256', aegis128l: 'AEGIS-128L' }[m[1]], param: m[1] === 'aes256gcm' ? '256' : undefined, mod: m[1] === 'aes256gcm' ? 'gcm' : undefined, functii: [m[2]] })],
[/(?<![\w.])crypto_secretbox(?:_\w+)?\s*\(/g, 'crypto_secretbox', () => ({ grup: 'CIPHER', nume: 'XSalsa20-Poly1305', functii: ['encrypt'] })],
[/(?<![\w.])crypto_pwhash(?:_\w+)?\s*\(/g, 'crypto_pwhash', () => ({ grup: 'KDF', nume: 'Argon2id', functii: ['keyderive'] })],
[/(?<![\w.])crypto_generichash(?:_\w+)?\s*\(/g, 'crypto_generichash', () => ({ grup: 'HASH', hash: 'BLAKE2B', functii: ['digest'] })],
[/(?<![\w.])crypto_hash_(sha256|sha512)(?:_\w+)?\s*\(/g, 'crypto_hash', (m) => ({ grup: 'HASH', hash: m[1].toUpperCase(), functii: ['digest'] })],
[/(?<![\w.])crypto_auth(?:_\w+)?\s*\(/g, 'crypto_auth', () => ({ grup: 'MAC', hash: 'SHA512/256', functii: ['tag'] })],
];
for (const [re, api, act] of FORME) for (const m of ctx.potriviri(re)) adauga(L, m.index, api, 'call', act(m), m[0].trim());
}
// --- libsignal (LibSignalClient; in sursele bibliotecii, SignalFfi) ----------------------------------------------------------------
// Swift nu declara modulul in fisier: un fisier din tinta SwiftPM `LibSignalClient` (dosarul Sources/LibSignalClient/) vede numele
// bibliotecii fara niciun import (masurat pe libsignal: DataStoreInMemory.swift, `IdentityKeyPair.generate()`, importa doar
// Foundation). A treia ancora e deci dosarul cu numele modulului, scrisa pe cale.
const inModulLibsignal = /(^|\/)LibSignalClient(Tests)?\//.test(String(ctx.rel || '').replace(/\\/g, '/'));
const libsignal = are('LibSignalClient', 'SignalFfi') || inModulLibsignal;
if (libsignal) {
const L = 'LibSignalClient';
for (const m of ctx.potriviri(/(?<![\w.])(IdentityKeyPair|PrivateKey)\s*\.\s*generate\s*\(\s*\)/g)) adauga(L, m.index, `${m[1]}.generate`, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: NOTA_EC_LIBSIGNAL }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])KEMKeyPair\s*\.\s*generate\s*\(\s*\)/g)) adauga(L, m.index, 'KEMKeyPair.generate', 'call', { grup: 'PREPQ', nume: 'Kyber-1024', primitiv: 'kem', functii: ['keygen'], nota: NOTA_KEM_SWIFT }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])(?<!class\s)(?<!func\s)KEMPublicKey\s*\(/g)) adauga(L, m.index, 'KEMPublicKey(...)', 'call', { grup: 'PREPQ', nume: 'Kyber-1024', primitiv: 'kem', functii: ['other'], nota: 'libsignal KEMPublicKey: a Kyber-1024 public key (a PQXDH prekey), built from bytes or from a native handle.' }, m[0]);
// constructorii (Aes256GcmSiv(key:), Aes256Ctr32(key:nonce:)) si formele statice (Aes256Ctr32.process(_:key:nonce:), CryptoTests.swift:169;
// Aes256GcmEncryptedData.encrypt(_:key:)); numele tipului intr-o declaratie (class/struct) sau intr-o constanta (Aes256Ctr32.nonceLength) nu e un apel
for (const m of ctx.potriviri(/(?<![\w.])(?<!class\s)(?<!struct\s)(Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32|Aes256GcmEncryptedData)\s*(?:\.\s*(encrypt|decrypt|process)\s*)?\(/g)) {
const tip = m[1];
const mod = tip === 'Aes256GcmSiv' ? 'gcm-siv' : tip === 'Aes256Ctr32' ? 'ctr' : 'gcm';
const fn = m[2] === 'decrypt' || tip === 'Aes256GcmDecryption' || (tip === 'Aes256GcmEncryptedData' && !m[2]) ? 'decrypt' : 'encrypt';
adauga(L, m.index, m[0].replace(/\s+/g, '').replace(/\($/, ''), 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod, functii: [fn] }, m[0]);
}
for (const m of ctx.potriviri(/(?<![\w.])(?<!func\s)hkdf\s*\(/g)) adauga(L, m.index, 'hkdf', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, m[0]);
for (const m of ctx.potriviri(/(?<![\w.])(?<!class\s)PreKeyBundle\s*\(|(?<![\w.])(?<!func\s)processPreKeyBundle\s*\(/g)) adauga(L, m.index, m[0].replace(/\s*\($/, ''), 'call', BUNDLE, m[0]);
}
// --- importurile bibliotecilor terte fara niciun apel recunoscut: dovada slaba, o data pe fisier ----------------------------------------
const TERTE = {
LibSignalClient: 'libsignal for Swift (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender)',
CryptoSwift: 'CryptoSwift (AES, ChaCha20, Blowfish, RSA, SHA-2/3, MD5, SHA-1, HMAC, PBKDF2)',
Sodium: 'swift-sodium (libsodium: X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2, BLAKE2b)',
Clibsodium: 'libsodium (X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2, BLAKE2b)',
};
for (const [modul, descriere] of Object.entries(TERTE)) {
if (!imp.has(modul) || folosite.has(modul) || (modul === 'Clibsodium' && folosite.has('Sodium'))) continue;
ctx.adauga(imp.get(modul), `import ${modul}`, 'import', { grup: 'LIB-MULTI', nume: modul === 'LibSignalClient' ? 'libsignal (LibSignalClient)' : modul, motiv: `Imports ${descriere}; no specific call recognized in this file.` }, { suprimaDe: ['XDH', 'EDDSA', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'HASH', 'MAC', 'RSA', 'LIB-MULTI'], bucata: `import ${modul}` });
}
}

View File

@ -16,10 +16,199 @@ export function curata(text, limbaj) {
case 'go': return curataC(text, { raw: true }); case 'go': return curataC(text, { raw: true });
case 'python': return curataPython(text); case 'python': return curataPython(text);
case 'rust': return curataRust(text); case 'rust': return curataRust(text);
case 'swift': return curataSwift(text);
case 'kotlin': return curataKotlin(text);
default: return { code: text, siruri: [] }; default: return { code: text, siruri: [] };
} }
} }
// Sfarsitul unui comentariu bloc IMBRICAT (/* a /* b */ c */) care incepe la i; n daca nu se inchide. Comun pentru Rust, Swift si
// Kotlin: toate trei imbrica; un lexer C ar fi inchis comentariul la primul */ si ar fi lasat restul drept cod.
function sfarsitComentariuImbricat(text, i, n) {
let j = i + 2;
let ad = 1;
while (j < n && ad > 0) {
if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; }
if (text[j] === '*' && text[j + 1] === '/') { ad--; j += 2; continue; }
j++;
}
return j;
}
// Swift (0.4.0, 2026-10-18): comentariile bloc se imbrica; sirurile sunt "..." (un singur rand), """...""" (multi-rand), si formele
// brute #"..."# / #"""..."""# cu oricate diezuri; INTERPOLAREA \( expr ) (in sirurile brute \#( expr )) poate contine la randul ei
// siruri si paranteze, deci sfarsitul sirului se cauta cu o stiva de cadre, nu cu "pana la urmatorul ghilimea". Nu exista literale
// char. Scanarea e ITERATIVA (stiva explicita): un fisier facut anume din `"\(` repetat ar fi 85.000 de niveluri de recursie.
// Continutul sirului (interpolare inclusa) ramane un singur interval: un nume de API scris in interpolare nu e o folosire masurata.
function sfarsitSirSwift(text, start, n) {
// cadre: { fel: 'sir', diezi, multi } sau { fel: 'interp', ad } (ad = paranteze deschise in interpolare)
const stiva = [];
const deschide = (j) => {
let d = 0;
while (text[j] === '#') { d++; j++; }
if (text[j] !== '"') return -1;
const multi = text.startsWith('"""', j);
stiva.push({ fel: 'sir', diezi: d, multi });
return j + (multi ? 3 : 1);
};
let j = deschide(start);
if (j < 0) return -1;
while (j < n && stiva.length) {
const cadru = stiva[stiva.length - 1];
const c = text[j];
if (cadru.fel === 'sir') {
if (c === '\\') {
// escape-ul unui sir brut e \ urmat de exact `diezi` diezi; un \ singur in sirul brut e un caracter obisnuit
let k = j + 1;
let d = 0;
while (d < cadru.diezi && text[k] === '#') { d++; k++; }
if (d < cadru.diezi) { j++; continue; }
if (text[k] === '(') { stiva.push({ fel: 'interp', ad: 1 }); j = k + 1; continue; }
j = k + 1; continue;
}
if (c === '"') {
const inchidere = (cadru.multi ? '"""' : '"') + '#'.repeat(cadru.diezi);
if (text.startsWith(inchidere, j)) { stiva.pop(); j += inchidere.length; continue; }
j++; continue;
}
if (c === '\n' && !cadru.multi) { stiva.pop(); j++; continue; } // sir neterminat pe rand: se inchide la capatul randului
j++; continue;
}
// in interpolare: paranteze echilibrate, siruri imbricate
if (c === '(') { cadru.ad++; j++; continue; }
if (c === ')') { if (--cadru.ad === 0) stiva.pop(); j++; continue; }
if (c === '"' || (c === '#' && /^#+"/.test(text.slice(j, j + 8)))) {
const k = deschide(j);
if (k >= 0) { j = k; continue; }
}
j++;
}
return Math.min(j, n);
}
function curataSwift(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (c === '/' && d === c) { // (scris altfel decat in lexerul C si in cel Rust: controlul negativ isi ancoreaza cioturile pe acelea)
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
const jSw = sfarsitComentariuImbricat(text, i, n);
goleste(i, jSw); i = jSw; continue;
}
if (c === '"' || (c === '#' && (d === '#' || d === '"'))) {
const sf = sfarsitSirSwift(text, i, n);
if (sf > i) { siruri.push([i, sf]); i = sf; continue; }
}
if (/[A-Za-z_]/.test(c)) {
let j = i + 1;
while (j < n && /\w/.test(text[j])) j++;
i = j; continue;
}
i++;
}
return { code: out.join(''), siruri };
}
// Kotlin (0.4.0, 2026-10-18): comentariile bloc se imbrica; sirurile sunt "..." (un singur rand, cu escape-uri) si """...""" (brute,
// fara escape-uri); amandoua au sabloane $nume si ${ expr }, iar expr poate contine siruri si acolade, deci sfarsitul se cauta cu o
// stiva de cadre, iterativ. 'c' e un literal char ('a', '\n', 'A', '\''), nu deschide un sir. Continutul sirului ramane un singur
// interval: un nume de API scris intr-un sablon nu e o folosire masurata.
function sfarsitSirKotlin(text, start, n) {
const stiva = []; // { fel: 'sir', brut } | { fel: 'sablon', ad }
const deschide = (j) => {
if (text[j] !== '"') return -1;
const brut = text.startsWith('"""', j);
stiva.push({ fel: 'sir', brut });
return j + (brut ? 3 : 1);
};
let j = deschide(start);
if (j < 0) return -1;
while (j < n && stiva.length) {
const cadru = stiva[stiva.length - 1];
const c = text[j];
if (cadru.fel === 'sir') {
if (c === '\\' && !cadru.brut) { j += 2; continue; }
if (c === '$' && text[j + 1] === '{') { stiva.push({ fel: 'sablon', ad: 1 }); j += 2; continue; }
if (c === '"') {
if (cadru.brut) {
if (text.startsWith('"""', j)) { j += 3; while (text[j] === '"') j++; stiva.pop(); continue; } // """"" inchide si pastreaza ghilimelele in plus
j++; continue;
}
stiva.pop(); j++; continue;
}
if (c === '\n' && !cadru.brut) { stiva.pop(); j++; continue; }
j++; continue;
}
if (c === '{') { cadru.ad++; j++; continue; }
if (c === '}') { if (--cadru.ad === 0) stiva.pop(); j++; continue; }
if (c === '"') { const k = deschide(j); if (k >= 0) { j = k; continue; } }
if (c === "'") { const k = sfarsitCharKotlin(text, j, n); if (k > 0) { j = k; continue; } }
j++;
}
return Math.min(j, n);
}
// literal char Kotlin la i: 'x', '\n', '\'', 'A', un caracter astral; -1 daca la i nu incepe un literal char
function sfarsitCharKotlin(text, i, n) {
const d = text[i + 1];
if (d === undefined || d === '\n' || d === "'") return -1;
if (d === '\\') {
let j = i + 2;
while (j < n && text[j] !== "'" && text[j] !== '\n' && j - i < 8) j++;
return text[j] === "'" ? j + 1 : -1;
}
if (text[i + 2] === "'") return i + 3;
const cod = d.charCodeAt(0);
if (cod >= 0xd800 && cod <= 0xdbff && text[i + 3] === "'") return i + 4;
return -1;
}
function curataKotlin(text) {
const n = text.length;
const out = text.split('');
const goleste = golitor(out);
const siruri = [];
let i = 0;
while (i < n) {
const c = text[i];
const d = text[i + 1];
if (c === d && d === '/') { // (scris altfel decat in lexerele C, Rust si Swift: fiecare ciot al controlului negativ are ancora lui)
let j = i;
while (j < n && text[j] !== '\n') j++;
goleste(i, j); i = j; continue;
}
if (c === '/' && d === '*') {
const jKt = sfarsitComentariuImbricat(text, i, n);
goleste(i, jKt); i = jKt; continue;
}
if (c === '"') {
const sf = sfarsitSirKotlin(text, i, n);
if (sf > i) { siruri.push([i, sf]); i = sf; continue; }
}
if (c === "'") {
const sf = sfarsitCharKotlin(text, i, n);
if (sf > 0) { siruri.push([i, sf]); i = sf; continue; }
i++; continue;
}
if (/[A-Za-z_]/.test(c)) {
let j = i + 1;
while (j < n && /\w/.test(text[j])) j++;
i = j; continue;
}
i++;
}
return { code: out.join(''), siruri };
}
function golitor(out) { function golitor(out) {
return (a, b) => { return (a, b) => {
for (let k = a; k < b; k++) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' '; for (let k = a; k < b; k++) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' ';
@ -218,13 +407,7 @@ function curataRust(text) {
goleste(i, j); i = j; continue; goleste(i, j); i = j; continue;
} }
if (c === '/' && d === '*') { if (c === '/' && d === '*') {
let j = i + 2; const j = sfarsitComentariuImbricat(text, i, n); // (0.4.0: aceeasi functie pentru Rust, Swift si Kotlin)
let ad = 1;
while (j < n && ad > 0) {
if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; }
if (text[j] === '*' && text[j + 1] === '/') { ad--; j += 2; continue; }
j++;
}
goleste(i, j); i = j; continue; goleste(i, j); i = j; continue;
} }
const dupaCuvant = i > 0 && /\w/.test(text[i - 1]); const dupaCuvant = i > 0 && /\w/.test(text[i - 1]);

View File

@ -32,7 +32,30 @@ const MAVEN_GROUP = {
'org.bouncycastle': 'Bouncy Castle: classical and post-quantum algorithms', '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)', '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', 'org.web3j': 'secp256k1 ECDSA accounts (web3j)', 'com.google.crypto.tink': 'Tink: AEAD, signatures, hybrid encryption', 'org.conscrypt': 'Conscrypt TLS provider',
// 0.4.0 (2026-10-18, libsignal pentru Android/JVM si bibliotecile obisnuite ale unei aplicatii Android)
'org.signal': 'libsignal: Curve25519 identity and prekeys (X3DH/PQXDH), Kyber prekeys, Double Ratchet, SPQR, sealed sender',
'org.whispersystems': 'legacy Signal protocol libraries (Curve25519, Double Ratchet)',
'org.mindrot': 'bcrypt password hashing (jbcrypt)', 'de.mkammerer': 'Argon2 password hashing (argon2-jvm)', 'com.lambdaworks': 'scrypt',
'org.bitcoinj': 'secp256k1 ECDSA (bitcoinj)',
}; };
// Swift Package Manager (Package.swift) si CocoaPods (Podfile, *.podspec), 0.4.0: pachetele criptografice cunoscute, dupa calea depozitului
// sau numele pachetului / pod-ului. Versiunea e CERINTA declarata (from:, exact:, '~> 1.8'), nu cea rezolvata din Package.resolved / Podfile.lock.
const SWIFT_PKG = [
[/(^|\/)apple\/swift-crypto$|^swift-crypto$|^Crypto$/i, 'swift-crypto: the CryptoKit API on every platform (AES-GCM, ChaChaPoly, SHA-2, HMAC, HKDF, P-256/384/521, Curve25519; RSA in _CryptoExtras)'],
[/(^|\/)krzyzanowskim\/CryptoSwift$|^CryptoSwift$/i, 'CryptoSwift: AES, ChaCha20, Blowfish, RSA, SHA-2/3, MD5, SHA-1, HMAC, PBKDF2 (pure Swift)'],
[/(^|\/)jedisct1\/swift-sodium$|^swift-sodium$|^Sodium$/i, 'swift-sodium (libsodium): X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2, BLAKE2b'],
[/(^|\/)signalapp\/libsignal(-client)?$|^LibSignalClient$/i, 'libsignal: Curve25519 identity and prekeys (X3DH/PQXDH), Kyber prekeys, Double Ratchet, SPQR, sealed sender'],
[/(^|\/)apple\/swift-certificates$|^swift-certificates$|^X509$/i, 'X.509 certificates (swift-certificates)'],
[/(^|\/)apple\/swift-nio-ssl$|^swift-nio-ssl$/i, 'TLS for SwiftNIO (BoringSSL)'],
[/(^|\/)(GigaBitcoin\/secp256k1\.swift|21-DOT-DEV\/swift-secp256k1)$|^secp256k1(\.swift)?$/i, 'secp256k1 ECDSA/Schnorr'],
[/(^|\/)(skywinder|web3swift-team)\/web3swift$|^web3swift$/i, 'secp256k1 ECDSA accounts and signatures (web3swift)'],
[/(^|\/)airsidemobile\/JOSESwift$|^JOSESwift$/i, 'JWS/JWE (classical algorithms)'],
[/(^|\/)TakeScoop\/SwiftyRSA$|^SwiftyRSA$/i, 'RSA'],
[/(^|\/)datatheorem\/TrustKit$|^TrustKit$/i, 'certificate pinning (SPKI)'],
[/(^|\/)krzyzanowskim\/OpenSSL$|^OpenSSL(-Universal)?$/i, 'OpenSSL'],
[/(^|\/)tmthecoder\/Argon2Swift$|^Argon2Swift$/i, 'Argon2 password hashing'],
];
const swiftPachet = (cale, nume) => SWIFT_PKG.find(([re]) => (cale && re.test(cale)) || (nume && re.test(nume)));
const GO = [ const GO = [
[/^golang\.org\/x\/crypto$/, 'extended crypto: ssh, chacha20poly1305, curve25519, bcrypt, argon2'], [/^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\/cloudflare\/circl$/, 'CIRCL: ML-KEM, ML-DSA, SLH-DSA, hybrid KEMs, classical curves'],
@ -61,18 +84,24 @@ const CARGO = [
[/^(sodiumoxide|libsodium-sys|dryoc)$/, 'libsodium (NaCl): X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2'], [/^(sodiumoxide|libsodium-sys|dryoc)$/, 'libsodium (NaCl): X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2'],
]; ];
export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod|Cargo\.toml)$/; export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|libs\.versions\.toml|go\.mod|Cargo\.toml|Package\.swift|Podfile|[\w.+-]+\.podspec)$/;
function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) { function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) {
const { line } = randul(inceputuri, pos); const { line } = randul(inceputuri, pos);
const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` const v = versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : '';
: ecosistem === 'pypi' ? `pkg:pypi/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${v}`
: ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` : ecosistem === 'pypi' ? `pkg:pypi/${nume}${v}`
: ecosistem === 'cargo' ? `pkg:cargo/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` : ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${v}`
: `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`; : ecosistem === 'cargo' ? `pkg:cargo/${nume}${v}`
: ecosistem === 'swift' ? `pkg:swift/${nume}${v}`
: ecosistem === 'cocoapods' ? `pkg:cocoapods/${nume}${v}`
: `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`;
return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl }; return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl };
} }
// versiunea dintr-o cerinta CocoaPods ('~> 1.8', '>= 2.0', '1.8.0')
const versiunePod = (s) => (s ? s.replace(/^[~>=<\s]+/, '').trim() || undefined : undefined);
export function citesteManifest(text, numeFisier, rel) { export function citesteManifest(text, numeFisier, rel) {
const inceputuri = inceputuriDeRand(text); const inceputuri = inceputuriDeRand(text);
const r = []; const r = [];
@ -128,11 +157,31 @@ export function citesteManifest(text, numeFisier, rel) {
if (!ofera) continue; 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)); 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)) { } else if (/^build\.gradle/.test(numeFisier) || numeFisier === 'libs.versions.toml') {
// Groovy si Kotlin DSL ("group:artifact:version"), si catalogul de versiuni Gradle (module = "group:artifact", versiunea in alt camp)
for (const m of text.matchAll(/["']([\w.-]+):([\w.-]+)(?::([\w.-]+))?["']/g)) { for (const m of text.matchAll(/["']([\w.-]+):([\w.-]+)(?::([\w.-]+))?["']/g)) {
const ofera = MAVEN_GROUP[m[1]]; const ofera = MAVEN_GROUP[m[1]];
if (ofera) r.push(lib('maven', `${m[1]}:${m[2]}`, m[3], ofera, m.index + 1, inceputuri, rel)); if (ofera) r.push(lib('maven', `${m[1]}:${m[2]}`, m[3], ofera, m.index + 1, inceputuri, rel));
} }
} else if (numeFisier === 'Package.swift') {
// .package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"), .package(url: ..., exact: / .upToNextMajor(from: / "1.0.0"..<),
// .package(name: "LibSignalClient", path: "../..")
for (const m of text.matchAll(/\.package\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g)) {
const a = m[1];
const url = /\burl\s*:\s*"([^"]+)"/.exec(a);
const nume = /\bname\s*:\s*"([^"]+)"/.exec(a);
const cale = url ? url[1].replace(/^[a-z]+:\/\//i, '').replace(/^git@([^:]+):/, '$1/').replace(/\.git$/, '').replace(/\/+$/, '') : null;
const intrare = swiftPachet(cale, nume ? nume[1] : (cale ? cale.split('/').pop() : null));
if (!intrare) continue;
const v = /\b(?:from|exact)\s*:\s*"([^"]+)"/.exec(a) || /\.upToNext(?:Major|Minor)\s*\(\s*from\s*:\s*"([^"]+)"/.exec(a) || /"([^"]+)"\s*\.\.[.<]/.exec(a);
r.push(lib('swift', cale || nume[1], v ? v[1] : undefined, intrare[1], m.index + m[0].indexOf(url ? url[0] : nume[0]), inceputuri, rel));
}
} else if (numeFisier === 'Podfile' || /\.podspec$/.test(numeFisier)) {
// pod 'CryptoSwift', '~> 1.8' / s.dependency 'CryptoSwift', '~> 1.8'
for (const m of text.matchAll(/(?:^|\s)(?:pod|\w+\.dependency)\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/g)) {
const intrare = swiftPachet(null, m[1].split('/')[0]);
if (intrare) r.push(lib('cocoapods', m[1].split('/')[0], versiunePod(m[2]), intrare[1], m.index + m[0].indexOf(m[1]), inceputuri, rel));
}
} else if (numeFisier === 'go.mod') { } else if (numeFisier === 'go.mod') {
let pos = 0; let pos = 0;
let inRequire = false; let inRequire = false;

View File

@ -8,10 +8,12 @@ import { detecteazaPy } from './detect-py.mjs';
import { detecteazaJava } from './detect-java.mjs'; import { detecteazaJava } from './detect-java.mjs';
import { detecteazaGo } from './detect-go.mjs'; import { detecteazaGo } from './detect-go.mjs';
import { detecteazaRust } from './detect-rust.mjs'; import { detecteazaRust } from './detect-rust.mjs';
import { detecteazaSwift } from './detect-swift.mjs';
import { detecteazaKotlin } from './detect-kotlin.mjs';
import { detecteazaPem } from './pem.mjs'; import { detecteazaPem } from './pem.mjs';
import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs'; import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs';
export const VERSIUNE = '0.3.0'; export const VERSIUNE = '0.4.0';
export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache', '.claude']; export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache', '.claude'];
const LIMBAJ = { const LIMBAJ = {
@ -21,8 +23,11 @@ const LIMBAJ = {
'.java': 'java', '.java': 'java',
'.go': 'go', '.go': 'go',
'.rs': 'rust', '.rs': 'rust',
// 0.4.0 (2026-10-18): Swift si Kotlin, dupa rularea pe libsignal (20,5% din randuri neanalizate); .kts e si manifest (build.gradle.kts)
'.swift': 'swift',
'.kt': 'kotlin', '.kts': 'kotlin',
}; };
const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo, rust: detecteazaRust }; const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo, rust: detecteazaRust, swift: detecteazaSwift, kotlin: detecteazaKotlin };
const EXEMPLE = 5; const EXEMPLE = 5;
export function limbajul(cale) { export function limbajul(cale) {

View File

@ -6,6 +6,7 @@
// P1 rezolvarea variabilelor fara memorie (fiecare apel reciteste fisierul) -> js-linie rosu // P1 rezolvarea variabilelor fara memorie (fiecare apel reciteste fisierul) -> js-linie rosu
// P2 esecurile literalului regex JS netinute minte -> js-regex 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 // P3 cautarea PEM fara pozitiile tinute minte (sfarsitul cautat de la fiecare antet) -> pem rosu
// P4 (0.4.0) comentariul bloc Swift neinchis reluat de la urmatorul caracter (fiecare `/*` rescaneaza pana la capat) -> swift-coment rosu
// node test/control-negativ-timp.mjs -> 0 toate prinse, 1 unul scapat, 2 STRICAT // 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 { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
@ -62,6 +63,7 @@ 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']], ['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']], ['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']], ['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']],
['P4', 'lib/lexer.mjs', 'goleste(i, jSw); i = jSw; continue;', 'goleste(i, jSw); i = jSw < n ? jSw : i + 2; continue;', ['swift-coment.swift']],
]; ];
for (const [id, fisier, ancora, cu, tinte] of P) { for (const [id, fisier, ancora, cu, tinte] of P) {
total++; total++;

View File

@ -67,11 +67,92 @@ const MUTATII = [
{ {
nume: 'Cargo.toml scos din manifeste', nume: 'Cargo.toml scos din manifeste',
fisier: 'lib/manifeste.mjs', fisier: 'lib/manifeste.mjs',
ancora: '|go\\.mod|Cargo\\.toml)$/;', ancora: '|Cargo\\.toml|Package\\.swift|Podfile|',
cu: '|go\\.mod)$/;', cu: '|Package\\.swift|Podfile|',
tinta: 'pozitiv: manifestele dau biblioteci declarate, nu gasiri', tinta: 'pozitiv: manifestele dau biblioteci declarate, nu gasiri',
martor: B, martor: B,
}, },
// 0.4.0 (2026-10-18): Swift si Kotlin
{
nume: 'Package.swift, Podfile si podspec scoase din manifeste',
fisier: 'lib/manifeste.mjs',
ancora: '|Package\\.swift|Podfile|[\\w.+-]+\\.podspec)$/;',
cu: ')$/;',
tinta: 'manifeste: Package.swift, Podfile, podspec si build.gradle.kts dau versiunea ceruta si purl pkg:swift / pkg:cocoapods / pkg:maven',
martor: B,
},
{
nume: 'comentariile bloc Swift nu se mai imbrica (lexerul)',
fisier: 'lib/lexer.mjs',
ancora: 'const jSw = sfarsitComentariuImbricat(text, i, n);',
cu: "const jSw = (() => { const k = text.indexOf('*/', i + 2); return k < 0 ? n : k + 2; })();",
tinta: 'negativ: comentariile imbricate, sirurile multi-rand, brute si interpolate Swift nu produc gasiri',
martor: B,
},
{
nume: 'API-ul libsignal Swift recunoscut fara ancora de import (orice IdentityKeyPair.generate devine libsignal)',
fisier: 'lib/detect-swift.mjs',
ancora: "const libsignal = are('LibSignalClient', 'SignalFfi') || inModulLibsignal;",
cu: 'const libsignal = true;',
tinta: 'negativ: numele API-ului libsignal si CryptoKit fara importurile lor nu produc gasiri (Swift), iar SecRandomCopyBytes nu e criptografie',
martor: B,
},
{
nume: 'importul CryptoKit ignorat (SHA256.hash recunoscut in orice fisier Swift)',
fisier: 'lib/detect-swift.mjs',
ancora: "if (are('CryptoKit', 'Crypto', '_CryptoExtras')) {",
cu: 'if (true) {',
tinta: 'negativ: numele API-ului libsignal si CryptoKit fara importurile lor nu produc gasiri (Swift), iar SecRandomCopyBytes nu e criptografie',
martor: B,
},
{
nume: 'KEMKeyPair.generate() din Swift citit ca cheie clasica (X25519), nu Kyber-1024',
fisier: 'lib/detect-swift.mjs',
ancora: "{ grup: 'PREPQ', nume: 'Kyber-1024', primitiv: 'kem', functii: ['keygen'], nota: NOTA_KEM_SWIFT }",
cu: "{ grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: NOTA_KEM_SWIFT }",
tinta: 'pozitiv: swift/libsignal.swift',
martor: B,
},
{
nume: 'constructorul Kotlin fara `new` ignorat (Aes256GcmSiv(key), PreKeyBundle(...), SealedSessionCipher(...) cer forma Java)',
fisier: 'lib/detect-java.mjs',
ancora: 'const ctor = kotlin ? ',
cu: 'const ctor = false ? ',
tinta: 'pozitiv: kotlin/Libsignal.kt',
martor: B,
},
{
nume: 'comentariile bloc Kotlin nu se mai imbrica (lexerul)',
fisier: 'lib/lexer.mjs',
ancora: 'const jKt = sfarsitComentariuImbricat(text, i, n);',
cu: "const jKt = (() => { const k = text.indexOf('*/', i + 2); return k < 0 ? n : k + 2; })();",
tinta: 'negativ: comentariile imbricate, sirurile brute, sabloanele si literalele char Kotlin nu produc gasiri',
martor: B,
},
{
nume: 'acolada care inchide sablonul ${ } Kotlin ignorata (sirul inghite tot ce urmeaza)',
fisier: 'lib/lexer.mjs',
ancora: "if (c === '}') { if (--cadru.ad === 0) stiva.pop(); j++; continue; }",
cu: "if (c === '}') { j++; continue; }",
tinta: 'pozitiv: kotlin/Android.kt',
martor: B,
},
{
nume: 'KeyProperties.KEY_ALGORITHM_* netradus (Android Keystore iese unknown)',
fisier: 'lib/detect-java.mjs',
ancora: "const kp = v.fel === 'necunoscut' && /^KeyProperties",
cu: "const kp = false && /^KeyProperties",
tinta: 'kotlin: variabila rezolvata (val X = "...") poarta resolvedFrom, iar KeyProperties.KEY_ALGORITHM_* se traduce in numele JCA',
martor: B,
},
{
nume: 'verificarea lanturilor de certificate fara regula de clasificare (PKI-VERIFY cade pe "unknown")',
fisier: 'lib/catalog.mjs',
ancora: "if (g === 'PKI-VERIFY') {",
cu: 'if (false) {',
tinta: 'jvm si swift: verificarea lanturilor de certificate iese autentificare (signature, quantum-vulnerable), nu schimb de cheie',
martor: B,
},
{ {
nume: 'secp256k1 clasificat quantum-safe', nume: 'secp256k1 clasificat quantum-safe',
fisier: 'lib/catalog.mjs', fisier: 'lib/catalog.mjs',

View File

@ -45,21 +45,27 @@ try {
const norm = (g) => JSON.stringify({ ...g, reason: String(g.reason).replace(/, which (cannot be resolved statically|this tool does not resolve statically).*$/, ', which [motiv]') }); const norm = (g) => JSON.stringify({ ...g, reason: String(g.reason).replace(/, which (cannot be resolved statically|this tool does not resolve statically).*$/, ', which [motiv]') });
for (const d of DOSARE) { for (const d of DOSARE) {
const a = SV.scaneaza(d), b = SN.scaneaza(d); const a = SV.scaneaza(d), b = SN.scaneaza(d);
// 0.3.0 (2026-10-18): Rust si Cargo.toml nu existau in 0.1.0, deci gasirile lor nu intra in echivalenta; se numara si se spun // 0.3.0 (2026-10-18): Rust si Cargo.toml nu existau in 0.1.0, deci gasirile lor nu intra in echivalenta; se numara si se spun.
const rustN = b.findings.filter((g) => g.language === 'rust').length; // 0.4.0 (2026-10-18): la fel Swift si Kotlin, manifestele Swift Package Manager / CocoaPods / libs.versions.toml, si grupurile Maven
const cargoN = b.libraries.filter((x) => x.ecosystem === 'cargo').length; // adaugate atunci (org.signal, org.whispersystems, org.mindrot, de.mkammerer, com.lambdaworks, org.bitcoinj).
b.findings = b.findings.filter((g) => g.language !== 'rust'); const LIMBAJE_NOI = new Set(['rust', 'swift', 'kotlin']);
b.libraries = b.libraries.filter((x) => x.ecosystem !== 'cargo'); const libNoua = (x) => x.ecosystem === 'cargo' || x.ecosystem === 'swift' || x.ecosystem === 'cocoapods' || /(^|\/)libs\.versions\.toml$/.test(x.file) || /^(org\.signal|org\.whispersystems|org\.mindrot|de\.mkammerer|com\.lambdaworks|org\.bitcoinj):/.test(x.name);
if (rustN || cargoN) console.log(` (in afara echivalentei: ${rustN} gasiri Rust, ${cargoN} biblioteci Cargo; 0.1.0 nu le stia)`); const peLimbaj = {};
for (const g of b.findings) if (LIMBAJE_NOI.has(g.language)) peLimbaj[g.language] = (peLimbaj[g.language] || 0) + 1;
const libN = b.libraries.filter(libNoua).length;
b.findings = b.findings.filter((g) => !LIMBAJE_NOI.has(g.language));
b.libraries = b.libraries.filter((x) => !libNoua(x));
if (Object.keys(peLimbaj).length || libN) console.log(` (in afara echivalentei: gasiri ${JSON.stringify(peLimbaj)}, ${libN} biblioteci Cargo/Swift/CocoaPods/grupuri Maven noi; 0.1.0 nu le stia)`);
const ka = a.findings.map(norm), kb = b.findings.map(norm); const ka = a.findings.map(norm), kb = b.findings.map(norm);
const sa = new Set(ka), sb = new Set(kb); const sa = new Set(ka), sb = new Set(kb);
// 0.3.0 (2026-10-18, libsignal): detectoarele noi pe limbaje pe care 0.1.0 le STIA (API-ul libsignal in Java si Node) sunt adaugiri, // 0.3.0 (2026-10-18, libsignal): detectoarele noi pe limbaje pe care 0.1.0 le STIA (API-ul libsignal in Java si Node) sunt adaugiri,
// nu rescrieri: o gasire numai in versiunea noua, cu un API din lista de mai jos, se numara si se spune, nu e diferenta. O gasire // nu rescrieri: o gasire numai in versiunea noua, cu un API din lista de mai jos, se numara si se spune, nu e diferenta. O gasire
// numai in 0.1.0 ramane intotdeauna diferenta: echivalenta pazeste ca rescrierea nu a PIERDUT nimic. // numai in 0.1.0 ramane intotdeauna diferenta: echivalenta pazeste ca rescrierea nu a PIERDUT nimic.
const ADAUGIRI = /"api":"(?:(?:ECKeyPair|IdentityKeyPair|ECPrivateKey|PrivateKey|KEMKeyPair)\.generate|Curve\.generateKeyPair|HKDF\.deriveSecrets|new (?:Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32|PreKeyBundle)|import org\.signal\.libsignal|hkdf|Aes256GcmSiv\.new|PreKeyBundle\.new|processPreKeyBundle|import @signalapp\/libsignal-client(?:\/[^"]*)?)"/; // 0.4.0: si regulile JVM adaugate pentru Kotlin, care se aplica si in Java (Android Keystore, Tink, OkHttp, PKIX, puntea libsignal).
const ADAUGIRI = /"api":"(?:(?:ECKeyPair|IdentityKeyPair|ECPrivateKey|PrivateKey|KEMKeyPair)\.generate|Curve\.generateKeyPair|HKDF\.deriveSecrets|new (?:Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32|PreKeyBundle|KEMPublicKey|SealedSessionCipher)|import org\.signal\.libsignal|hkdf|Aes256GcmSiv\.new|PreKeyBundle\.new|processPreKeyBundle|import @signalapp\/libsignal-client(?:\/[^"]*)?|Native\.\w+|KeyGenParameterSpec\.Builder|KeyTemplates\.get|\w+KeyTemplates\.\w+|Predefined\w+Parameters\.\w+|\w+Config\.register|import com\.google\.crypto\.tink|ConnectionSpec\.\w+|CertificatePinner\.Builder|(?:CertPathValidator|TrustManagerFactory)\.getInstance|KeyStore\.getInstance\("AndroidCAStore"\)|X509TrustManager|checkServerTrusted)"/;
const doarV = ka.filter((x) => !sb.has(x)), doarN0 = kb.filter((x) => !sa.has(x)); const doarV = ka.filter((x) => !sb.has(x)), doarN0 = kb.filter((x) => !sa.has(x));
const adaugate = doarN0.filter((x) => ADAUGIRI.test(x)), doarN = doarN0.filter((x) => !ADAUGIRI.test(x)); const adaugate = doarN0.filter((x) => ADAUGIRI.test(x)), doarN = doarN0.filter((x) => !ADAUGIRI.test(x));
if (adaugate.length) console.log(` (in afara echivalentei: ${adaugate.length} gasiri ale detectoarelor adaugate dupa 0.1.0, API-ul libsignal Java/Node)`); if (adaugate.length) console.log(` (in afara echivalentei: ${adaugate.length} gasiri ale detectoarelor adaugate dupa 0.1.0, API-ul libsignal Java/Node si regulile JVM din 0.4.0)`);
const ok = !doarV.length && !doarN.length && ka.length + adaugate.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries); const ok = !doarV.length && !doarN.length && ka.length + adaugate.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries);
comparate += ka.length; comparate += ka.length;
if (!ok) dif++; if (!ok) dif++;

View File

@ -0,0 +1,102 @@
// Fixtura: JCA/JSSE din Kotlin (fara `new`, cu `val`), verificatorul PKIX al lui rustls-platform-verifier asa cum sta in clona libsignal
// (java/android/src/main/java/org/rustls/platformverifier/CertificateVerifier.kt, randurile in dreptul formelor), Android Keystore
// (developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder), Tink (developers.google.com/tink, sabloanele
// de chei) si OkHttp (square.github.io/okhttp/features/https: ConnectionSpec, TlsVersion, CipherSuite, CertificatePinner). Nu se compileaza.
package fixturi
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.MessageDigest
import java.security.cert.CertPathValidator
import java.security.cert.CertificateFactory
import java.security.spec.ECGenParameterSpec
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.KeysetHandle
import com.google.crypto.tink.aead.AeadConfig
import okhttp3.CertificatePinner
import okhttp3.CipherSuite
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import org.bouncycastle.crypto.engines.RC4Engine // EXPECT: RC4 | weak-now
class Android {
private val DIGEST = "SHA-256"
fun jca(algDinParametru: String, keystore: KeyStore) {
val alias = "root_${keystore.size()}" // CertificateVerifier.kt:149: un sablon ${ } inaintea apelurilor (lexerul trebuie sa il inchida)
val kpg = KeyPairGenerator.getInstance("RSA") // EXPECT: RSA-3072 | quantum-vulnerable
kpg.initialize(3072)
val ec = KeyPairGenerator.getInstance("EC") // EXPECT: EC-secp256r1 | quantum-vulnerable
ec.initialize(ECGenParameterSpec("secp256r1"))
val c1 = Cipher.getInstance("AES/GCM/NoPadding") // EXPECT: AES-GCM | quantum-safe
val mac = Mac.getInstance("HmacSHA256") // EXPECT: HMAC-SHA-256 | quantum-safe
val md = MessageDigest.getInstance(DIGEST) // EXPECT: SHA-256 | quantum-safe
val md2 = MessageDigest.getInstance(algDinParametru) // EXPECT: MessageDigest.getInstance | unknown
val tls = SSLContext.getInstance("TLSv1.2") // EXPECT: TLSv1.2 | quantum-vulnerable
println(listOf(alias, c1, mac, md, md2, tls))
}
// rustls-platform-verifier, CertificateVerifier.kt: lantul de incredere TLS al clientului (verificare de certificate, nu schimb de cheie)
fun pkix(keystore: KeyStore?, principal: ByteArray) {
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) // :101; EXPECT: X.509 chain validation (PKIX) | quantum-vulnerable
factory.init(keystore)
for (manager in factory.trustManagers) {
if (manager is X509TrustManager) { // :113; EXPECT: X.509 chain validation (PKIX) | quantum-vulnerable
manager.checkServerTrusted(arrayOf(), "RSA") // :291; EXPECT: X.509 chain validation (PKIX) | quantum-vulnerable
}
}
val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509") // :198: parsare de certificate, nu verificare: nimic
val systemKeystore = KeyStore.getInstance("AndroidCAStore") // :209; EXPECT: X.509 chain validation (PKIX) | quantum-vulnerable
val validator = CertPathValidator.getInstance("PKIX") // :367; EXPECT: X.509 chain validation (PKIX) | quantum-vulnerable
val digest = MessageDigest.getInstance("MD5").digest(principal) // :435: aliasul de sistem al unei radacini, nu securitate; unealta nu judeca intentia; EXPECT: MD5 | weak-now
println(listOf(certFactory, systemKeystore, validator, digest))
}
fun keystore() {
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") // curba din Builder-ul de mai jos; EXPECT: EC-secp256r1 | quantum-vulnerable
kpg.initialize(
KeyGenParameterSpec.Builder("alias", KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.build()
)
val rsa = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore") // marimea din variabila spec; EXPECT: RSA-2048 | quantum-vulnerable
val spec = KeyGenParameterSpec.Builder("rsa", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setKeySize(2048)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.build()
rsa.initialize(spec)
val kg = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") // EXPECT: AES-256-GCM | quantum-safe
kg.init(KeyGenParameterSpec.Builder("aes", KeyProperties.PURPOSE_ENCRYPT).setBlockModes(KeyProperties.BLOCK_MODE_GCM).setKeySize(256).build())
val singur = KeyGenParameterSpec.Builder("hmac", KeyProperties.PURPOSE_SIGN).setKeySize(521).build() // fara generator in fisier: din marime; EXPECT: EC-secp521r1 | quantum-vulnerable
println(singur)
}
fun tink() {
AeadConfig.register() // dovada slaba, suprimata de sabloanele de mai jos: nimic
val aead = KeysetHandle.generateNew(KeyTemplates.get("AES256_GCM")) // EXPECT: AES-256-GCM | quantum-safe
val sig = KeysetHandle.generateNew(KeyTemplates.get("ECDSA_P256")) // EXPECT: ECDSA-secp256r1 | quantum-vulnerable
val hybrid = KeysetHandle.generateNew(KeyTemplates.get("DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM")) // HPKE; EXPECT: X25519 | quantum-vulnerable
val mac = KeysetHandle.generateNew(KeyTemplates.get("HMAC_SHA256_256BITTAG")) // EXPECT: HMAC-SHA-256 | quantum-safe
val rsa = KeysetHandle.generateNew(KeyTemplates.get("RSA_SSA_PSS_3072_SHA256_F4")) // EXPECT: RSA-3072 | quantum-vulnerable
println(listOf(aead, sig, hybrid, mac, rsa))
}
fun okhttp(): OkHttpClient {
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) // EXPECT: TLS (negotiated, OkHttp MODERN_TLS) | unknown
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) // EXPECT: TLSv1.2 | quantum-vulnerable ; TLSv1.3 | unknown
.cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384) // EXPECT: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 | quantum-vulnerable ; AES-256-GCM | quantum-safe
.build()
val pinner = CertificatePinner.Builder().add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=").build() // EXPECT: certificate pinning (SPKI) | quantum-vulnerable
return OkHttpClient.Builder().connectionSpecs(listOf(spec)).certificatePinner(pinner).build()
}
}

View File

@ -0,0 +1,40 @@
// Fixtura: API-ul libsignal din Kotlin (org.signal.libsignal), 2026-10-18. Formele vin din clona signalapp/libsignal 1738ead8
// (fisier:rand in dreptul lor); unde Kotlin-ul clonei are numai DEFINITIA, forma de apel e semnatura ei plus apelul Java corespunzator
// din aceeasi clona (scris in dreptul randului). Fisierul nu se compileaza; e doar text pentru scaner. Ancora e importul
// org.signal.libsignal.* (sau pachetul); fara ea, aceleasi nume nu inseamna libsignal (vezi negative/FaraLibsignal.kt).
package fixturi
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.ecc.ECPrivateKey
import org.signal.libsignal.protocol.kem.KEMKeyPair
import org.signal.libsignal.protocol.kem.KEMKeyType
import org.signal.libsignal.protocol.kem.KEMPublicKey
import org.signal.libsignal.protocol.kdf.HKDF
import org.signal.libsignal.protocol.state.PreKeyBundle
import org.signal.libsignal.metadata.SealedSessionCipher
import org.signal.libsignal.crypto.Aes256GcmSiv
import org.signal.libsignal.internal.Native
class Libsignal {
fun exemple(key: ByteArray, ikm: ByteArray, info: ByteArray, bytes: ByteArray, tip: KEMKeyType, store: Any, aci: Any, e164: String) {
var privateKey = ECPrivateKey.generate() // java/shared/.../protocol/ecc/ECKeyPair.kt:15; EXPECT: X25519 | quantum-vulnerable
val pair = ECKeyPair(privateKey.getPublicKey(), privateKey) // ECKeyPair.kt:16: constructor din parti, NU o generare: nimic
val pair2 = ECKeyPair.generate() // semnatura ECKeyPair.kt:14 + apelul Java `ECKeyPair.generate()` (x66 in clona); EXPECT: X25519 | quantum-vulnerable
val id = IdentityKeyPair.generate() // apelul Java `IdentityKeyPair.generate()` (x9 in clona); EXPECT: X25519 | quantum-vulnerable
val kem = KEMKeyPair.generate(KEMKeyType.KYBER_1024) // semnatura KEMKeyPair.kt:18 + apelul Java (x5); EXPECT: Kyber-1024 | quantum-safe
val kem2 = KEMKeyPair.generate(tip) // tipul nu e pe rand: nu se ghiceste; EXPECT: libsignal KEM key pair | unknown
val kemDinPunte = KEMKeyPair(Native.KyberKeyPair_Generate()) // KEMKeyPair.kt:20; EXPECT: Kyber-1024 | quantum-safe
val remote = KEMPublicKey(bytes) // java/client/src/test/.../net/AuthKeysServiceTest.kt:66; EXPECT: Kyber-1024 | quantum-safe
val okm = HKDF.deriveSecrets(ikm, info, 32) // semnatura HKDF.kt:10 + apelul Java (x7); EXPECT: HKDF | quantum-safe
val okm2 = Native.HKDF_DeriveSecrets(32, ikm, info, null) // HKDF.kt:15; EXPECT: HKDF | quantum-safe
val siv = Aes256GcmSiv(key) // Java `new Aes256GcmSiv(key)` (x3), in Kotlin fara new; EXPECT: AES-256-GCM-SIV | quantum-safe
val bundle = PreKeyBundle(1, 1, 2, pair.publicKey, 3, pair.publicKey, ByteArray(64), id.publicKey, 4, kem.publicKey, ByteArray(64)) // constructorul PreKeyBundle.kt:26; EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
val sealed = SealedSessionCipher(store, aci, e164, 1) // Java `new SealedSessionCipher(store, aci, e164, deviceId)`; plicul e X25519 + HKDF; EXPECT: X25519 | quantum-vulnerable
val deserializat = ECPrivateKey(bytes) // UnauthBackupsServiceTest.kt:61: deserializare, nu generare: nimic
val semnatura = Native.ECPrivateKey_Sign(1L, info) // ECPrivateKey.kt:30 (calculateSignature): XEdDSA pe cheia Curve25519; EXPECT: XEdDSA (Curve25519) | quantum-vulnerable
val acord = Native.ECPrivateKey_Agree(1L, 2L) // ECPrivateKey.kt:35 (calculateAgreement); EXPECT: X25519 | quantum-vulnerable
val deschis = Native.ECPrivateKey_HpkeOpen(1L, bytes, info, info) // ECPrivateKey.kt:55 (open, HPKE RFC 9180 cu DHKEM X25519); EXPECT: X25519 | quantum-vulnerable
println(listOf(pair, pair2, kem2, kemDinPunte, remote, okm, okm2, siv, bundle, sealed, deserializat, semnatura, acord, deschis))
}
}

View File

@ -0,0 +1,6 @@
Pod::Spec.new do |s|
s.name = 'Exemplu'
s.version = '1.0.0'
s.dependency 'Sodium', '~> 0.9'
s.dependency 'SwiftyJSON'
end

View File

@ -0,0 +1,18 @@
// swift-tools-version:5.9
// Fixtura: manifest Swift Package Manager. Formele sunt cele din clona libsignal (Package.swift si swift/Benchmarks/Package.swift:
// .package(url:from:), .package(name:path:)) si din documentatia SwiftPM (.upToNextMajor(from:), exact:).
import PackageDescription
let package = Package(
name: "Exemplu",
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.0")),
.package(url: "https://github.com/jedisct1/swift-sodium.git", exact: "0.9.1"),
.package(name: "LibSignalClient", path: "../libsignal"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
],
targets: [
.target(name: "Exemplu", dependencies: ["CryptoSwift"]),
]
)

View File

@ -0,0 +1,9 @@
platform :ios, '15'
use_frameworks!
target 'Exemplu' do
pod 'CryptoSwift', '~> 1.8'
pod 'LibSignalClient', path: '../libsignal'
pod 'Alamofire', '~> 5.8'
pod 'TrustKit'
end

View File

@ -0,0 +1,8 @@
plugins { id("com.android.application") }
dependencies {
implementation("org.signal:libsignal-android:0.105.1")
implementation("com.google.crypto.tink:tink-android:1.15.0")
implementation("org.bouncycastle:bcprov-jdk18on:1.78.1")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
testImplementation("junit:junit:4.13.2")
}

View File

@ -0,0 +1,23 @@
// Fixtura negativa Kotlin: comentarii IMBRICATE, siruri obisnuite cu escape-uri, siruri brute """...""", sabloane ${ } cu apeluri si
// siruri inauntru, literale char (inclusiv '"' si '\''): niciunul nu e o folosire si niciunul nu are voie sa deschida un sir peste
// codul de dupa el. Numele JCA nu au nevoie de ancora, deci daca lexerul greseste, gasirea apare.
package fixturi
/* import javax.crypto.Cipher
/* val c = Cipher.getInstance("AES/ECB/PKCS5Padding") */
val md = MessageDigest.getInstance("MD5")
*/
/** KeyPairGenerator.getInstance("RSA") intr-un comentariu KDoc nu e o folosire. */
class Comentarii {
fun nimic(x: Int): String {
val ghilimea = '"'
val apostrof = '\''
val unicode = 'A'
val brut = """MessageDigest.getInstance("MD5") si Cipher.getInstance("DES") "ghilimele" """
val sablon = "k=${listOf("MessageDigest.getInstance(\"SHA-1\")", "${x}").size} $x MessageDigest.getInstance(\"MD5\")"
val text = "KeyPairGenerator.getInstance(\"RSA\") \\ CertPathValidator.getInstance(\"PKIX\")"
val tm = "javax.net.ssl.X509TrustManager si TrustManagerFactory.getInstance(\"PKIX\")"
return brut + sablon + text + tm + ghilimea + apostrof + unicode
}
}

View File

@ -0,0 +1,16 @@
// Fixtura negativa Kotlin: aceleasi nume scurte ca in API-ul libsignal (ECKeyPair.generate(), HKDF.deriveSecrets, Aes256GcmSiv(key)),
// dar FARA importul org.signal.libsignal: nu sunt libsignal, deci nu produc nicio gasire. Nu se compileaza; e text pentru scaner.
package fixturi
import com.exemplu.chei.ECKeyPair
import com.exemplu.kdf.HKDF
import com.exemplu.cifru.Aes256GcmSiv
class FaraLibsignal {
fun exemple(key: ByteArray, ikm: ByteArray): Any {
val ec = ECKeyPair.generate()
val okm = HKDF.deriveSecrets(ikm, "info".toByteArray(), 32)
println(listOf(ec, okm))
return Aes256GcmSiv(key)
}
}

View File

@ -0,0 +1,17 @@
// Fixtura negativa Kotlin: SecureRandom singur (aleator, nu criptografie; forma din libsignal
// java/shared/java/org/signal/libsignal/zkgroup/donation/DonationPermit.kt:122) si un tip X509Certificate purtat ca parametru,
// fara nicio verificare de lant. Nicio gasire.
package fixturi
import java.security.SecureRandom
import java.security.cert.X509Certificate
class ZgomotSecureRandom {
fun aleator(rng: SecureRandom = SecureRandom()): ByteArray {
val b = ByteArray(32)
rng.nextBytes(b)
return b
}
fun subiect(cert: X509Certificate): String = cert.subjectX500Principal.name
}

View File

@ -0,0 +1,25 @@
// Fixtura negativa Swift: comentarii IMBRICATE, siruri "..." cu interpolare \( ) (care contine la randul ei siruri), siruri multi-rand
// """...""" si brute #"..."# / #"""..."""# care contin nume de API: niciunul nu e o folosire si niciunul nu are voie sa deschida un
// sir peste codul de dupa el. Importul CryptoKit e aici dinadins: cu el, numele AR insemna ceva daca ar fi in cod.
import Foundation
import CryptoKit
/* let d = SHA256.hash(data: x)
/* let k = Curve25519.KeyAgreement.PrivateKey() */
let m = Insecure.MD5.hash(data: x)
*/
/// `P256.Signing.PrivateKey()` intr-un comentariu de documentatie nu e o folosire.
func nimic(_ x: Data) -> String {
let interpolat = "digest \(x.count) SHA256.hash(data: x) \("Insecure.SHA1.hash(data: x)") \(x.isEmpty ? "a" : "b")"
let multiRand = """
AES.GCM.seal(data, using: key) "ghilimele simple inauntru"
Curve25519.KeyAgreement.PrivateKey()
"""
let brut = #"HMAC<SHA256>.authenticationCode(for: "x", using: key) \#(x.count) \(nu e interpolare in sir brut)"#
let brutMulti = #"""
ChaChaPoly.seal(data, using: key) "quoted" """ inca nu s-a inchis
"""#
let gol = ""
return interpolat + multiRand + brut + brutMulti + gol
}

View File

@ -0,0 +1,22 @@
// Fixtura negativa Swift: Security importat pentru SecRandomCopyBytes (aleator, nu criptografie; forma din libsignal
// swift/Sources/LibSignalClient/Utils.swift:258-267), si numele API-urilor libsignal si CryptoKit FARA importurile lor: un fisier
// care nu importa modulul nu poate chema acel API, deci nu e o folosire. Nicio gasire.
import Foundation
#if canImport(Security)
import Security
#endif
func fillRandom(_ buffer: inout [UInt8]) throws {
let result = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
guard result == errSecSuccess else {
throw NSError(domain: "SecRandomCopyBytes failed (error code \(result))", code: Int(result))
}
}
func faraImport() {
let id = IdentityKeyPair.generate()
let d = SHA256.hash(data: Data())
let k = KEMKeyPair.generate()
let s = try? Aes256GcmSiv(key: [UInt8]())
_ = (id, d, k, s)
}

View File

@ -0,0 +1,35 @@
// Fixtura: CryptoSwift (formele din README-ul github.com/krzyzanowskim/CryptoSwift: AES(key:blockMode:padding:), SHA2(variant:),
// bytes.md5(), HMAC(key:variant:), PKCS5.PBKDF2(...), ChaCha20(key:iv:), Poly1305(key:),
// RSA(keySize:), Blowfish(key:blockMode:padding:)) si swift-sodium (README-ul github.com/jedisct1/swift-sodium: sodium.box.keyPair(),
// sodium.sign.keyPair(), sodium.secretBox.seal(message:secretKey:), sodium.aead.xchacha20poly1305ietf.encrypt(message:secretKey:),
// sodium.pwHash.hash(...)) plus o functie C a libsodium (crypto_box_keypair, Clibsodium). Nu se compileaza; e text pentru scaner.
// (Parametrul parolei se numeste `parola`: poarta de secrete a casei citeste eticheta engleza a parolei urmata de un tip drept o parola
// atribuita in clar; eticheta ramane numai in apelul API-ului, unde valoarea e identificatorul `parola`.)
import Foundation
import CryptoSwift
import Sodium
import Clibsodium
func exemple(key: [UInt8], iv: [UInt8], input: [UInt8], parola: [UInt8], salt: [UInt8]) throws {
let aes = try AES(key: key, blockMode: GCM(iv: iv, mode: .combined), padding: .noPadding) // EXPECT: AES-GCM | quantum-safe
let cbc = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7) // EXPECT: AES-CBC | quantum-safe
let ecb = try AES(key: key, blockMode: ECB()) // EXPECT: AES-ECB | weak-now
let digest = SHA2(variant: .sha256).calculate(for: input) // EXPECT: SHA-256 | quantum-safe
let md = input.md5() // EXPECT: MD5 | weak-now
let mac = try HMAC(key: key, variant: .sha256).authenticate(input) // EXPECT: HMAC-SHA-256 | quantum-safe
let dk = try PKCS5.PBKDF2(password: parola, salt: salt, iterations: 4096, keyLength: 32, variant: .sha256).calculate() // EXPECT: PBKDF2-HMAC-SHA256 | quantum-safe
let chacha = try ChaCha20(key: key, iv: iv) // EXPECT: ChaCha20 | quantum-safe
let poly = try Poly1305(key: key) // EXPECT: Poly1305 | quantum-safe
let rsa = try RSA(keySize: 2048) // EXPECT: RSA-2048 | quantum-vulnerable
let blowfish = try Blowfish(key: key, blockMode: CBC(iv: iv), padding: .pkcs7) // EXPECT: Blowfish | weak-now
let sodium = Sodium()
let box = sodium.box.keyPair() // EXPECT: X25519 | quantum-vulnerable
let signer = sodium.sign.keyPair() // EXPECT: Ed25519 | quantum-vulnerable
let sealed = sodium.secretBox.seal(message: input, secretKey: key) // EXPECT: XSalsa20-Poly1305 | quantum-safe
let aead = sodium.aead.xchacha20poly1305ietf.encrypt(message: input, secretKey: key) // EXPECT: XChaCha20-Poly1305 | quantum-safe
let pw = sodium.pwHash.hash(outputLength: 32, passwd: parola, salt: salt, opsLimit: 1, memLimit: 1) // EXPECT: Argon2id | quantum-safe
var pk = [UInt8](repeating: 0, count: 32)
var sk = [UInt8](repeating: 0, count: 32)
let rc = crypto_box_keypair(&pk, &sk) // EXPECT: X25519 | quantum-vulnerable
_ = (aes, cbc, ecb, digest, md, mac, dk, chacha, poly, rsa, blowfish, box, signer, sealed, aead, pw, rc)
}

View File

@ -0,0 +1,28 @@
// Fixtura: CryptoKit (si swift-crypto, `import Crypto`, acelasi API). Formele sunt cele din documentatia Apple
// (developer.apple.com/documentation/cryptokit: SHA256.hash(data:), Insecure.MD5, HMAC<SHA256>.authenticationCode(for:using:),
// AES.GCM.seal(_:using:), ChaChaPoly.seal, Curve25519.KeyAgreement.PrivateKey(), P256.Signing.PrivateKey(),
// HKDF<SHA256>.deriveKey(inputKeyMaterial:salt:info:outputByteCount:), SymmetricKey(size: .bits256)). Nu se compileaza; e text pentru scaner.
import Foundation
import CryptoKit
func exemple(data: Data, key: SymmetricKey, peer: Curve25519.KeyAgreement.PublicKey) throws {
let digest = SHA256.hash(data: data) // EXPECT: SHA-256 | quantum-safe
let d384 = SHA384.hash(data: data) // EXPECT: SHA-384 | quantum-safe
var hasher = SHA512() // EXPECT: SHA-512 | quantum-safe
let md5 = Insecure.MD5.hash(data: data) // EXPECT: MD5 | weak-now
let sha1 = Insecure.SHA1.hash(data: data) // EXPECT: SHA-1 | weak-now
let tag = HMAC<SHA256>.authenticationCode(for: data, using: key) // EXPECT: HMAC-SHA-256 | quantum-safe
let sealed = try AES.GCM.seal(data, using: SymmetricKey(size: .bits256)) // marimea cheii de pe acelasi rand; EXPECT: AES-256-GCM | quantum-safe
let opened = try AES.GCM.open(sealed, using: key) // EXPECT: AES-GCM | quantum-safe
let cc = try ChaChaPoly.seal(data, using: key) // EXPECT: ChaCha20-Poly1305 | quantum-safe
let x = Curve25519.KeyAgreement.PrivateKey() // EXPECT: X25519 | quantum-vulnerable
let shared = try x.sharedSecretFromKeyAgreement(with: peer)
let ed = Curve25519.Signing.PrivateKey() // EXPECT: Ed25519 | quantum-vulnerable
let p256 = P256.Signing.PrivateKey() // EXPECT: ECDSA-secp256r1 | quantum-vulnerable
let p384 = P384.KeyAgreement.PrivateKey() // EXPECT: ECDH-secp384r1 | quantum-vulnerable
let p521 = P521.Signing.PrivateKey() // EXPECT: ECDSA-secp521r1 | quantum-vulnerable
let derived = HKDF<SHA256>.deriveKey(inputKeyMaterial: key, salt: data, info: data, outputByteCount: 32) // EXPECT: HKDF | quantum-safe
let k = SymmetricKey(size: .bits128) // singura pe rand: algoritmul se alege unde e folosita; EXPECT: SymmetricKey (128-bit) | unknown
hasher.update(data: data)
_ = (digest, d384, md5, sha1, tag, opened, cc, shared, ed, p256, p384, p521, derived, k)
}

View File

@ -0,0 +1,8 @@
// Fixtura: un fisier care importa LibSignalClient fara niciun apel recunoscut: importul e dovada slaba (unknown), o singura data.
// Cu un apel recunoscut in acelasi fisier, importul nu se mai raporteaza (vezi swift/libsignal.swift, care nu are marcaj pe import).
import Foundation
import LibSignalClient // EXPECT: libsignal (LibSignalClient) | unknown
func store() -> InMemorySignalProtocolStore {
return InMemorySignalProtocolStore()
}

View File

@ -0,0 +1,52 @@
// Fixtura: API-ul Swift al libsignal (LibSignalClient), 2026-10-18. FIECARE forma e luata din clona signalapp/libsignal 1738ead8
// (fisier:rand scris in dreptul ei; fisierul nu se compileaza, e doar text pentru scaner). Ancora e `import LibSignalClient`
// (sau `@testable import LibSignalClient`; in sursele bibliotecii insesi, `import SignalFfi`). Fara ancora, aceleasi nume nu
// inseamna nimic (vezi negative/zgomot-securitate.swift).
import Foundation
import LibSignalClient
func exemple(key: [UInt8], nonce: [UInt8], ad: [UInt8], ptext: [UInt8], accountEntropy: String, bytes: [UInt8]) throws {
let bob_pre_key = PrivateKey.generate() // swift/Tests/LibSignalClientTests/SessionTests.swift:145; EXPECT: X25519 | quantum-vulnerable
let bob_identity = IdentityKeyPair.generate() // swift/Sources/LibSignalClient/DataStoreInMemory.swift:33; EXPECT: X25519 | quantum-vulnerable
let bob_kyber_pre_key = KEMKeyPair.generate() // SessionTests.swift:147; puntea genereaza kem::KeyType::Kyber1024; EXPECT: Kyber-1024 | quantum-safe
let kyberPreKey = KEMKeyPair.generate().publicKey // PublicAPITests.swift:506; EXPECT: Kyber-1024 | quantum-safe
let remoteKem = try KEMPublicKey(bytes) // ChatServices/UnauthKeysServiceTests.swift:67; EXPECT: Kyber-1024 | quantum-safe
let gcm_siv = try Aes256GcmSiv(key: key) // CryptoTests.swift:62; EXPECT: AES-256-GCM-SIV | quantum-safe
let encryptedParts = try Aes256GcmEncryptedData.encrypt(Data(ptext), key: key) // CryptoTests.swift:79; EXPECT: AES-256-GCM | quantum-safe
let splitParts = try Aes256GcmEncryptedData(concatenated: encryptedParts.concatenate()) // CryptoTests.swift:86; EXPECT: AES-256-GCM | quantum-safe
let gcmEnc = try Aes256GcmEncryption(key: key, nonce: nonce, associatedData: ad) // CryptoTests.swift:131; EXPECT: AES-256-GCM | quantum-safe
let gcmDec = try Aes256GcmDecryption(key: key, nonce: nonce, associatedData: ad) // CryptoTests.swift:138; EXPECT: AES-256-GCM | quantum-safe
var message = Data(ptext)
try Aes256Ctr32(key: key, nonce: nonce).process(&message) // swift/Sources/LibSignalClient/Aes256Ctr.swift:87; EXPECT: AES-256-CTR | quantum-safe
try Aes256Ctr32.process(&message, key: key, nonce: nonce) // CryptoTests.swift:169 (forma statica); EXPECT: AES-256-CTR | quantum-safe
var nonceCtr = Array(repeating: UInt8(0), count: Aes256Ctr32.nonceLength) // CryptoTests.swift:163: o constanta a tipului, nu un apel: nimic
let backupKey = try hkdf( // MessageBackupTests.swift:225; EXPECT: HKDF | quantum-safe
outputLength: 32,
inputKeyMaterial: Array(accountEntropy.utf8),
salt: [],
info: Array("20240801_SIGNAL_BACKUP_KEY".utf8)
)
let bob_bundle = try PreKeyBundle( // SessionTests.swift:165; EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
registrationId: 1,
deviceId: 9,
prekeyId: 4570,
prekey: bob_pre_key.publicKey,
signedPrekeyId: 3006,
signedPrekey: bob_pre_key.publicKey,
signedPrekeySignature: Data(),
identity: bob_identity.identityKey,
kyberPrekeyId: 8888,
kyberPrekey: bob_kyber_pre_key.publicKey,
kyberPrekeySignature: Data()
)
try processPreKeyBundle( // SessionTests.swift:180; EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
bob_bundle,
for: bob_address,
ourAddress: alice_address,
sessionStore: alice_store,
identityStore: alice_store,
now: Date(),
context: NullContext()
)
_ = (kyberPreKey, remoteKem, gcm_siv, splitParts, gcmEnc, gcmDec, backupKey)
}

View File

@ -0,0 +1,40 @@
// Fixtura: CommonCrypto, Security.framework si TLS-ul clientului (URLSession, Network). Formele sunt cele din documentatia Apple:
// CC_SHA256 / CCCrypt / CCHmac / CCKeyDerivationPBKDF (CommonCrypto.h), SecKeyCreateRandomKey cu kSecAttrKeyType si kSecAttrKeySizeInBits
// (developer.apple.com/documentation/security/generating-new-cryptographic-keys), SecKeyCreateSignature / SecKeyVerifySignature /
// SecKeyCreateEncryptedData cu SecKeyAlgorithm, SecTrustEvaluateWithError, URLSessionConfiguration.tlsMinimumSupportedProtocolVersion,
// NWProtocolTLS.Options + sec_protocol_options_set_min_tls_protocol_version. Nu se compileaza; e text pentru scaner.
import Foundation
import CommonCrypto
import Security
import Network
func exemple(data: Data, keyData: Data, password: String, salt: Data, trust: SecTrust, privateKey: SecKey, publicKey: SecKey) throws {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) } // EXPECT: SHA-256 | quantum-safe
_ = CC_MD5(nil, 0, &hash) // EXPECT: MD5 | weak-now
_ = CC_SHA1(nil, 0, &hash) // EXPECT: SHA-1 | weak-now
let status = CCCrypt(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES), CCOptions(kCCOptionPKCS7Padding), keyBytes, kCCKeySizeAES256, iv, dataBytes, data.count, outBytes, outLength, &moved) // EXPECT: AES-256-CBC | quantum-safe
let s3des = CCCrypt(CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithm3DES), CCOptions(kCCOptionECBMode), keyBytes, kCCKeySize3DES, nil, dataBytes, data.count, outBytes, outLength, &moved) // EXPECT: 3DES-ECB | weak-now
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes, keyData.count, dataBytes, data.count, &hash) // EXPECT: HMAC-SHA-256 | quantum-safe
let r = CCKeyDerivationPBKDF(CCPBKDFAlgorithm(kCCPBKDF2), password, password.utf8.count, saltBytes, salt.count, CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA1), 10000, derived, 32) // EXPECT: PBKDF2-HMAC-SHA1 | weak-now
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
]
var error: Unmanaged<CFError>?
let ecKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) // dictionarul e variabila de mai sus; EXPECT: EC-secp256r1 | quantum-vulnerable
let rsaKey = SecKeyCreateRandomKey([kSecAttrKeyType as String: kSecAttrKeyTypeRSA, kSecAttrKeySizeInBits as String: 2048] as CFDictionary, &error) // EXPECT: RSA-2048 | quantum-vulnerable
let sig = SecKeyCreateSignature(privateKey, .ecdsaSignatureMessageX962SHA256, data as CFData, &error) // EXPECT: ECDSA | quantum-vulnerable
let sig2 = SecKeyCreateSignature(privateKey, .rsaSignatureMessagePSSSHA256, data as CFData, &error) // EXPECT: RSA | quantum-vulnerable
let ok = SecKeyVerifySignature(publicKey, .rsaSignatureMessagePKCS1v15SHA1, data as CFData, sig!, &error) // EXPECT: RSA | weak-now
let ct = SecKeyCreateEncryptedData(publicKey, .rsaEncryptionOAEPSHA256, data as CFData, &error) // EXPECT: RSA | quantum-vulnerable
let trusted = SecTrustEvaluateWithError(trust, &error) // lantul de incredere TLS: autentificare, nu schimb de cheie; EXPECT: X.509 chain validation (PKIX) | quantum-vulnerable
let config = URLSessionConfiguration.default
config.tlsMinimumSupportedProtocolVersion = .TLSv12 // EXPECT: TLSv1.2 | quantum-vulnerable
config.tlsMaximumSupportedProtocolVersion = .TLSv13 // EXPECT: TLSv1.3 | unknown
let tls = NWProtocolTLS.Options() // EXPECT: TLS (negotiated) | unknown
sec_protocol_options_set_min_tls_protocol_version(tls.securityProtocolOptions, .TLSv13) // EXPECT: TLSv1.3 | unknown
_ = (status, s3des, r, ecKey, rsaKey, sig2, ok, ct, trusted, config)
}

View File

@ -16,7 +16,7 @@ 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 CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs');
const TERMEN_MS = 20000; const TERMEN_MS = 20000;
// unitatea se repeta pana la marime; '@' se inlocuieste cu un contor (nume distincte) // unitatea se repeta pana la marime; forma lista e [unitate, kb1, kb2, prefix?] (prefixul se pune o singura data, inaintea repetarii)
export const CAZURI = { export const CAZURI = {
// pem: cautarea sfarsitului e nativa (indexOf), deci forma patratica se vede abia spre marimea maxima implicita // pem: cautarea sfarsitului e nativa (indexOf), deci forma patratica se vede abia spre marimea maxima implicita
'pem.txt': ['-----BEGIN CERTIFICATE-----AAAA\n', 256, 1024], 'pem.txt': ['-----BEGIN CERTIFICATE-----AAAA\n', 256, 1024],
@ -27,11 +27,21 @@ export const CAZURI = {
'js-func.js': 'crypto.createHash(q)\nfunction (', 'js-func.js': 'crypto.createHash(q)\nfunction (',
'java-linie.java': 'Cipher.getInstance(x)+', 'java-linie.java': 'Cipher.getInstance(x)+',
'java-kpg.java': 'k = KeyPairGenerator.getInstance("EC")+', 'java-kpg.java': 'k = KeyPairGenerator.getInstance("EC")+',
// 0.4.0 (2026-10-18), Swift si Kotlin: zeci de mii de comentarii imbricate deschise si neinchise; o interpolare / un sablon care se
// deschide la nesfarsit (fiecare `"\(` deschide un sir si o interpolare: 85.000 de niveluri la 256 KB, deci scanarea trebuie sa fie
// iterativa); un singur sir de un megaoctet neinchis (" si """)
'swift-coment.swift': '/* ',
'swift-interp.swift': '"\\(',
'swift-sir-lung.swift': ['a', 256, 1024, '"'],
'kt-sablon.kt': '"${',
'kt-brut-lung.kt': ['a', 256, 1024, '"""'],
}; };
const marimi = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume].slice(1) : [64, 256]); const marimi = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume].slice(1, 3) : [64, 256]);
const unitate = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume][0] : CAZURI[nume]); 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 prefixul = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume][3] || '' : '');
// prefixul intra in marime: la 1024 KB fisierul nu are voie sa treaca de maxFileBytes (1048576), altfel e sarit ca prea mare si proba e STRICATA
const genereaza = (unit, kb, prefix = '') => prefix + unit.repeat(Math.max(1, Math.floor((kb * 1024 - prefix.length) / unit.length)));
const arg = (f) => { const i = process.argv.indexOf(f); return i >= 0 ? process.argv[i + 1] : null; }; const arg = (f) => { const i = process.argv.indexOf(f); return i >= 0 ? process.argv[i + 1] : null; };
@ -41,7 +51,7 @@ if (arg('--caz')) {
const inv = await import(pathToFileURL(CALE_MODUL).href); const inv = await import(pathToFileURL(CALE_MODUL).href);
const d = mkdtempSync(join(tmpdir(), 'ci-timp-')); const d = mkdtempSync(join(tmpdir(), 'ci-timp-'));
try { try {
writeFileSync(join(d, nume), genereaza(unitate(nume), kb)); writeFileSync(join(d, nume), genereaza(unitate(nume), kb, prefixul(nume)));
const t0 = process.hrtime.bigint(); const t0 = process.hrtime.bigint();
const rez = inv.scan(d); const rez = inv.scan(d);
const ms = Number(process.hrtime.bigint() - t0) / 1e6; const ms = Number(process.hrtime.bigint() - t0) / 1e6;

View File

@ -3,7 +3,7 @@
// Modulul masurat se poate schimba cu CRYPTO_INVENTORY_MODULE=<cale>/inventar.mjs (asa il foloseste // 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. // 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. // 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 { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, copyFileSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path'; import { join, resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
@ -18,8 +18,12 @@ const lx = await import(pathToFileURL(join(dirname(CALE_MODUL), 'lib', 'lexer.mj
const CA_JSON = process.argv.includes('--json'); const CA_JSON = process.argv.includes('--json');
// numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient // numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient
const MARCAJE_ASTEPTATE = 158; // (0.4.0, 2026-10-18: 158 + 111 din fixturile Swift si Kotlin, numarate de compara-fixturi inainte de a fi scrise aici)
const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'js/libsignal-client.ts', 'js/libsignal-client-import.ts', 'python/crypto_py.py', 'java/Crypto.java', 'java/Libsignal.java', 'java/LibsignalImport.java', 'go/crypto.go', 'rust/cripto.rs', 'rust/ambiguu.rs', 'rust/glob.rs', 'rust/libsignal.rs', 'negative/variabile.js']; const MARCAJE_ASTEPTATE = 269;
const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'js/libsignal-client.ts', 'js/libsignal-client-import.ts', 'python/crypto_py.py', 'java/Crypto.java', 'java/Libsignal.java', 'java/LibsignalImport.java', 'go/crypto.go', 'rust/cripto.rs', 'rust/ambiguu.rs', 'rust/glob.rs', 'rust/libsignal.rs', 'negative/variabile.js',
'swift/libsignal.swift', 'swift/libsignal-import.swift', 'swift/cryptokit.swift', 'swift/securitate.swift', 'swift/biblioteci.swift', 'kotlin/Libsignal.kt', 'kotlin/Android.kt'];
// fixturile negative: fiecare fisier de aici e un fisier de cod citit si analizat, cu zero gasiri (in afara de variabile.js, care are marcaje)
const FISIERE_NEGATIVE = 15;
const probe = []; const probe = [];
const proba = (nume, fn) => probe.push({ nume, fn }); const proba = (nume, fn) => probe.push({ nume, fn });
@ -91,11 +95,28 @@ proba('pozitiv: manifestele dau biblioteci declarate, nu gasiri', () => {
const ast = ['go.mod:6 github.com/cloudflare/circl', 'go.mod:7 golang.org/x/crypto', 'package.json:10 @noble/post-quantum', const ast = ['go.mod:6 github.com/cloudflare/circl', 'go.mod:7 golang.org/x/crypto', 'package.json:10 @noble/post-quantum',
'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on', 'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on',
'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt', 'Cargo.toml:7 sha2', 'Cargo.toml:8 ed25519-dalek', 'Cargo.toml:9 rustls', 'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt', 'Cargo.toml:7 sha2', 'Cargo.toml:8 ed25519-dalek', 'Cargo.toml:9 rustls',
'Cargo.toml:11 aes-gcm', 'Cargo.toml:12 pqcrypto-mlkem', 'Cargo.toml:15 ml-dsa', 'Cargo.toml:21 argon2'].sort(); 'Cargo.toml:11 aes-gcm', 'Cargo.toml:12 pqcrypto-mlkem', 'Cargo.toml:15 ml-dsa', 'Cargo.toml:21 argon2',
// 0.4.0: Swift Package Manager, CocoaPods (Podfile si podspec), Gradle Kotlin DSL
'Package.swift:9 github.com/apple/swift-crypto', 'Package.swift:10 github.com/krzyzanowskim/CryptoSwift', 'Package.swift:11 github.com/jedisct1/swift-sodium',
'Package.swift:12 LibSignalClient', 'Podfile:5 CryptoSwift', 'Podfile:6 LibSignalClient', 'Podfile:8 TrustKit', 'Exemplu.podspec:4 Sodium',
'build.gradle.kts:3 org.signal:libsignal-android', 'build.gradle.kts:4 com.google.crypto.tink:tink-android', 'build.gradle.kts:5 org.bouncycastle:bcprov-jdk18on'].sort();
asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`); asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`);
const bom = inv.buildCbom(rez, { deterministic: true }); const bom = inv.buildCbom(rez, { deterministic: true });
const c = bom.components.filter((x) => x.type === 'library'); const c = bom.components.filter((x) => x.type === 'library');
asigura(c.length === 15 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), 'componentele library nu poarta declared-only'); // 26 de biblioteci declarate, 25 de componente: bcprov-jdk18on 1.78.1 e declarat si in pom.xml si in build.gradle.kts (acelasi purl)
asigura(c.length === 25 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), `componentele library: ${c.length}, sau nu poarta declared-only`);
});
proba('manifeste: Package.swift, Podfile, podspec si build.gradle.kts dau versiunea ceruta si purl pkg:swift / pkg:cocoapods / pkg:maven', () => {
const libs = scan(join(FIX, 'manifeste')).libraries;
const l = (f, n) => libs.find((b) => b.file === f && b.name === n);
asigura(l('Package.swift', 'github.com/apple/swift-crypto').purl === 'pkg:swift/github.com/apple/swift-crypto@3.0.0', `from: ${JSON.stringify(l('Package.swift', 'github.com/apple/swift-crypto'))}`);
asigura(l('Package.swift', 'github.com/krzyzanowskim/CryptoSwift').version === '1.8.0' && l('Package.swift', 'github.com/jedisct1/swift-sodium').version === '0.9.1', 'upToNextMajor / exact');
asigura(l('Package.swift', 'LibSignalClient').version === undefined && l('Package.swift', 'LibSignalClient').purl === 'pkg:swift/LibSignalClient', 'pachetul local, fara versiune');
asigura(!libs.some((b) => /argument-parser|Alamofire|SwiftyJSON|okhttp|junit/.test(b.name)), 'pachetele fara criptografie nu sunt biblioteci declarate');
asigura(l('Podfile', 'CryptoSwift').purl === 'pkg:cocoapods/CryptoSwift@1.8' && l('Podfile', 'TrustKit').version === undefined, `Podfile: ${JSON.stringify([l('Podfile', 'CryptoSwift'), l('Podfile', 'TrustKit')])}`);
asigura(l('Exemplu.podspec', 'Sodium').purl === 'pkg:cocoapods/Sodium@0.9', `podspec: ${JSON.stringify(l('Exemplu.podspec', 'Sodium'))}`);
asigura(l('build.gradle.kts', 'org.signal:libsignal-android').purl === 'pkg:maven/org.signal/libsignal-android@0.105.1', `gradle kts: ${JSON.stringify(l('build.gradle.kts', 'org.signal:libsignal-android'))}`);
}); });
proba('pozitiv: material PEM (certificat si cheie publica)', () => { proba('pozitiv: material PEM (certificat si cheie publica)', () => {
@ -254,11 +275,115 @@ proba('rust: Cargo.toml: forma tabel, workspace = true fara versiune, redenumire
proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => { proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => {
const rez = scan(join(FIX, 'negative')); const rez = scan(join(FIX, 'negative'));
asigura(rez.stats.filesSeen === 10 && rez.stats.codeFilesTotal === 10, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`); asigura(rez.stats.filesSeen === FISIERE_NEGATIVE && rez.stats.codeFilesTotal === FISIERE_NEGATIVE, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal} (asteptate ${FISIERE_NEGATIVE})`);
const g = gasiriDin(rez, 'fara-cripto.js'); const g = gasiriDin(rez, 'fara-cripto.js');
asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`); asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`);
}); });
// --- Swift si Kotlin (0.4.0, 2026-10-18, dupa rularea pe libsignal) ---
proba('negativ: comentariile imbricate, sirurile multi-rand, brute si interpolate Swift nu produc gasiri', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'swift/cryptokit.swift').some((x) => x.name === 'MD5'), 'controlul pozitiv: Insecure.MD5 din fixtura CryptoKit lipseste');
const g = gasiriDin(rez, 'negative/comentarii.swift');
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
});
proba('negativ: numele API-ului libsignal si CryptoKit fara importurile lor nu produc gasiri (Swift), iar SecRandomCopyBytes nu e criptografie', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'swift/libsignal.swift').some((x) => x.name === 'Kyber-1024') && gasiriDin(rez, 'swift/cryptokit.swift').some((x) => x.name === 'SHA-256'), 'controlul pozitiv: Kyber-1024 / SHA-256 din fixturile Swift lipsesc');
const g = gasiriDin(rez, 'negative/zgomot-securitate.swift');
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
// ancorele din sursele bibliotecii: `import SignalFfi` (fara import LibSignalClient) si dosarul tintei SwiftPM (Sources/LibSignalClient/,
// DataStoreInMemory.swift importa doar Foundation) fac numele libsignal, fara dovada slaba de import; acelasi fisier in alt dosar: nimic
const d = mkdtempSync(join(tmpdir(), 'ci-swift-ffi-'));
try {
// (variabilele se numesc `cheie` / `identitate`, nu privateKey: poarta de secrete a casei citeste `privateKey = ...` din acest text drept o cheie atribuita)
writeFileSync(join(d, 'IdentityKey.swift'), 'import Foundation\nimport SignalFfi\n\nextension IdentityKeyPair {\n public static func generate() -> IdentityKeyPair {\n let cheie = PrivateKey.generate()\n return IdentityKeyPair(publicKey: cheie.publicKey, privateKey: cheie)\n }\n}\n');
const store = 'import Foundation\n\npublic class InMemorySignalProtocolStore {\n private var identitate: IdentityKeyPair\n public init() {\n self.identitate = IdentityKeyPair.generate()\n }\n}\n';
mkdirSync(join(d, 'Sources', 'LibSignalClient'), { recursive: true });
writeFileSync(join(d, 'Sources', 'LibSignalClient', 'DataStoreInMemory.swift'), store);
writeFileSync(join(d, 'DataStoreInMemory.swift'), store);
const r = inv.scan(d);
const f = r.findings.map((x) => `${x.file}:${x.line} ${x.name}`).sort();
asigura(JSON.stringify(f) === JSON.stringify(['IdentityKey.swift:6 X25519', 'Sources/LibSignalClient/DataStoreInMemory.swift:6 X25519']), `ancorele SignalFfi / dosarul modulului: ${JSON.stringify(f)}`);
} finally { rmSync(d, { recursive: true, force: true }); }
});
proba('negativ: comentariile imbricate, sirurile brute, sabloanele si literalele char Kotlin nu produc gasiri', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'kotlin/Android.kt').some((x) => x.name === 'MD5'), 'controlul pozitiv: MD5 din fixtura Kotlin lipseste');
const g = gasiriDin(rez, 'negative/Comentarii.kt');
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
});
proba('negativ: numele API-ului libsignal fara importul lui nu produc gasiri (Kotlin), iar SecureRandom singur nu e criptografie', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'kotlin/Libsignal.kt').some((x) => x.name === 'Kyber-1024' && x.api === 'KEMKeyPair.generate'), 'controlul pozitiv: Kyber-1024 din fixtura Kotlin libsignal lipseste');
const g = [...gasiriDin(rez, 'negative/FaraLibsignal.kt'), ...gasiriDin(rez, 'negative/ZgomotSecureRandom.kt')];
asigura(g.length === 0, `au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`);
// ancora de pachet, ca in Java: un fisier din chiar arborele libsignal (package org.signal.libsignal...), fara import
const d = mkdtempSync(join(tmpdir(), 'ci-kotlin-libsignal-'));
try {
writeFileSync(join(d, 'ECKeyPair.kt'), 'package org.signal.libsignal.protocol.ecc\n\npublic data class ECKeyPair(val publicKey: ECPublicKey, val privateKey: ECPrivateKey) {\n public companion object {\n public fun generate(): ECKeyPair {\n var privateKey = ECPrivateKey.generate()\n return ECKeyPair(privateKey.getPublicKey(), privateKey)\n }\n }\n}\n');
const r = inv.scan(d);
asigura(r.findings.length === 1 && r.findings[0].name === 'X25519' && r.findings[0].line === 6, `ancora de pachet Kotlin: ${JSON.stringify(r.findings.map((x) => [x.line, x.name, x.api]))}`);
} finally { rmSync(d, { recursive: true, force: true }); }
});
proba('swift: lexerul: comentariile bloc se imbrica, interpolarea tine siruri imbricate, sirurile brute se inchid la diezi, multi-rand la """', () => {
const t1 = '/* a /* b */ SHA256.hash(data: x) */ Insecure.MD5.hash(data: x)';
const c1 = lx.curata(t1, 'swift');
asigura(!c1.code.includes('SHA256') && c1.code.includes('Insecure.MD5') && c1.code.length === t1.length, `t1: ${JSON.stringify(c1.code)}`);
const t2 = 'let s = "a \\(f("b \\(g("c"))")) d"; SHA256.hash(data: x)';
const c2 = lx.curata(t2, 'swift');
asigura(c2.siruri.length === 1 && c2.siruri[0][1] === t2.indexOf('; SHA256') && !lx.inSir(c2.siruri, t2.indexOf('SHA256')), `t2: ${JSON.stringify(c2.siruri)}`);
const t3 = 'let r = #"a "b" \\(nu) \\#(da) "# ; let m = ##"x"# "## ; Insecure.SHA1.hash(data: x)';
const c3 = lx.curata(t3, 'swift');
asigura(c3.siruri.length === 2 && c3.siruri[0][1] === t3.indexOf(' ; let m') && c3.siruri[1][1] === t3.indexOf(' ; Insecure') && !lx.inSir(c3.siruri, t3.indexOf('Insecure')), `t3: ${JSON.stringify(c3.siruri)}`);
const t4 = 'let m = """\n a "b" \\("c")\n """\nlet n = #"""\n d """ e\n """#\nSHA512()';
const c4 = lx.curata(t4, 'swift');
asigura(c4.siruri.length === 2 && !lx.inSir(c4.siruri, t4.indexOf('SHA512')) && lx.inSir(c4.siruri, t4.indexOf('d """ e')), `t4: ${JSON.stringify(c4.siruri)}`);
// sir neterminat pe un rand: se inchide la capatul randului, nu inghite randul urmator
const t5 = 'let x = "neterminat\nSHA256.hash(data: y)';
const c5 = lx.curata(t5, 'swift');
asigura(!lx.inSir(c5.siruri, t5.indexOf('SHA256')), `t5: ${JSON.stringify(c5.siruri)}`);
});
proba('kotlin: lexerul: sabloanele ${ } tin siruri imbricate, sirurile brute nu au escape-uri, literalele char nu deschid siruri', () => {
const t1 = 'val s = "k=${f("a", "${g("b")}")} ${x}"; MessageDigest.getInstance("MD5")';
const c1 = lx.curata(t1, 'kotlin');
asigura(c1.siruri.length === 2 && c1.siruri[0][1] === t1.indexOf('; MessageDigest') && !lx.inSir(c1.siruri, t1.indexOf('MessageDigest')), `t1: ${JSON.stringify(c1.siruri)}`);
const t2 = 'val r = """a \\ "b" ${"c"} """" ; Cipher.getInstance("DES")';
const c2 = lx.curata(t2, 'kotlin');
asigura(c2.siruri.length === 2 && c2.siruri[0][1] === t2.indexOf(' ; Cipher') && !lx.inSir(c2.siruri, t2.indexOf('Cipher')), `t2: ${JSON.stringify(c2.siruri)}`);
const t3 = "val a = '\"'; val b = '\\''; val c = '\\u0041'; val d = 'x'; Mac.getInstance(\"HmacSHA1\")";
const c3 = lx.curata(t3, 'kotlin');
asigura(c3.siruri.length === 5 && !lx.inSir(c3.siruri, t3.indexOf('Mac.')), `t3: ${JSON.stringify(c3.siruri)}`);
const t4 = '/* a /* b */ KeyGenerator.getInstance("AES") */ KeyAgreement.getInstance("X25519")';
const c4 = lx.curata(t4, 'kotlin');
asigura(!c4.code.includes('KeyGenerator') && c4.code.includes('KeyAgreement') && c4.code.length === t4.length, `t4: ${JSON.stringify(c4.code)}`);
});
proba('kotlin: variabila rezolvata (val X = "...") poarta resolvedFrom, iar KeyProperties.KEY_ALGORITHM_* se traduce in numele JCA', () => {
const g = gasiriDin(scan(FIX), 'kotlin/Android.kt');
const md = g.find((x) => x.name === 'SHA-256' && x.api === 'MessageDigest.getInstance');
asigura(md && /DIGEST \(line \d+\)/.test(md.resolvedFrom || ''), `resolvedFrom: ${md && md.resolvedFrom}`);
const ec = g.find((x) => /KEY_ALGORITHM_EC\b/.test(x.resolvedFrom || ''));
// curba vine din Builder-ul inline din initialize(...), si ECGenParameterSpec-ul de acolo NU se numara si separat (o singura gasire pe cele 7 randuri)
asigura(ec && ec.name === 'EC-secp256r1' && g.filter((x) => x.line >= ec.line && x.line <= ec.line + 6).length === 1, `Android Keystore EC: ${JSON.stringify(g.filter((x) => ec && x.line >= ec.line && x.line <= ec.line + 6).map((x) => [x.line, x.name, x.resolvedFrom]))}`);
const rsa = g.find((x) => /KEY_ALGORITHM_RSA\b/.test(x.resolvedFrom || ''));
asigura(rsa && rsa.name === 'RSA-2048' && rsa.padding === 'oaep' && rsa.primitive === 'pke', `Android Keystore RSA prin variabila spec: ${JSON.stringify(rsa && [rsa.name, rsa.padding, rsa.primitive])}`);
});
proba('jvm si swift: verificarea lanturilor de certificate iese autentificare (signature, quantum-vulnerable), nu schimb de cheie', () => {
const rez = scan(FIX);
const pk = [...gasiriDin(rez, 'kotlin/Android.kt'), ...gasiriDin(rez, 'swift/securitate.swift')].filter((x) => x.group === 'PKI-VERIFY');
asigura(pk.length === 7, `gasiri PKI-VERIFY: ${pk.length} (asteptate 7: 6 in Kotlin, 1 in Swift)`);
for (const x of pk) asigura(x.primitive === 'signature' && x.quantumVulnerable === true && x.classification === 'quantum-vulnerable' && /authentication exposure/.test(x.reason) && /not a harvest-now-decrypt-later/.test(x.reason), `${x.file}:${x.line} ${x.name}: ${x.primitive} ${x.classification}`);
const a = inv.classify({ grup: 'PKI-VERIFY' });
asigura(a.primitiv === 'signature' && /Nothing to change in this code alone/.test(a.recommendation), 'recomandarea PKI-VERIFY nu spune ca schimbarea e la CA');
});
// 2026-10-18 (libsignal): API-ul libsignal se recunoaste numai cu ancora lui (importul org.signal.libsignal / @signalapp/libsignal-client) // 2026-10-18 (libsignal): API-ul libsignal se recunoaste numai cu ancora lui (importul org.signal.libsignal / @signalapp/libsignal-client)
proba('negativ: numele API-ului libsignal fara importul lui nu produc gasiri (Java si TypeScript), iar cu import se recunosc', () => { proba('negativ: numele API-ului libsignal fara importul lui nu produc gasiri (Java si TypeScript), iar cu import se recunosc', () => {
const rez = scan(FIX); const rez = scan(FIX);