From b6ed4d2b71c7e494320963c18e02025bd1cc67e0 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Sun, 18 Oct 2026 08:53:13 +0300 Subject: [PATCH] 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 --- CITATIONS-UNRESOLVED.md | 3 +- README.md | 8 +- control-plane/README.md | 8 +- .../dovezi/retete-2026-10-18/CITESTE-MA.md | 20 +- .../masoara-apache-haproxy-rpc2.sh | 92 +++++++ .../masoara-haproxy2-rpc2.sh | 59 +++++ control-plane/remediere.mjs | 6 +- crypto-inventory/README.md | 52 +++- crypto-inventory/lib/catalog.mjs | 10 + crypto-inventory/lib/context.mjs | 7 + crypto-inventory/lib/detect-java.mjs | 207 +++++++++++++-- crypto-inventory/lib/detect-kotlin.mjs | 12 + crypto-inventory/lib/detect-swift.mjs | 239 ++++++++++++++++++ crypto-inventory/lib/lexer.mjs | 197 ++++++++++++++- crypto-inventory/lib/manifeste.mjs | 63 ++++- crypto-inventory/lib/scan.mjs | 9 +- .../test/control-negativ-timp.mjs | 2 + crypto-inventory/test/control-negativ.mjs | 85 ++++++- crypto-inventory/test/echivalenta-0.1.0.mjs | 22 +- .../test/fixturi/kotlin/Android.kt | 102 ++++++++ .../test/fixturi/kotlin/Libsignal.kt | 40 +++ .../test/fixturi/manifeste/Exemplu.podspec | 6 + .../test/fixturi/manifeste/Package.swift | 18 ++ .../test/fixturi/manifeste/Podfile | 9 + .../test/fixturi/manifeste/build.gradle.kts | 8 + .../test/fixturi/negative/Comentarii.kt | 23 ++ .../test/fixturi/negative/FaraLibsignal.kt | 16 ++ .../fixturi/negative/ZgomotSecureRandom.kt | 17 ++ .../test/fixturi/negative/comentarii.swift | 25 ++ .../fixturi/negative/zgomot-securitate.swift | 22 ++ .../test/fixturi/swift/biblioteci.swift | 35 +++ .../test/fixturi/swift/cryptokit.swift | 28 ++ .../test/fixturi/swift/libsignal-import.swift | 8 + .../test/fixturi/swift/libsignal.swift | 52 ++++ .../test/fixturi/swift/securitate.swift | 40 +++ crypto-inventory/test/proba-timp.mjs | 18 +- crypto-inventory/test/proba.mjs | 137 +++++++++- 37 files changed, 1623 insertions(+), 82 deletions(-) create mode 100644 control-plane/dovezi/retete-2026-10-18/masoara-apache-haproxy-rpc2.sh create mode 100644 control-plane/dovezi/retete-2026-10-18/masoara-haproxy2-rpc2.sh create mode 100644 crypto-inventory/lib/detect-kotlin.mjs create mode 100644 crypto-inventory/lib/detect-swift.mjs create mode 100644 crypto-inventory/test/fixturi/kotlin/Android.kt create mode 100644 crypto-inventory/test/fixturi/kotlin/Libsignal.kt create mode 100644 crypto-inventory/test/fixturi/manifeste/Exemplu.podspec create mode 100644 crypto-inventory/test/fixturi/manifeste/Package.swift create mode 100644 crypto-inventory/test/fixturi/manifeste/Podfile create mode 100644 crypto-inventory/test/fixturi/manifeste/build.gradle.kts create mode 100644 crypto-inventory/test/fixturi/negative/Comentarii.kt create mode 100644 crypto-inventory/test/fixturi/negative/FaraLibsignal.kt create mode 100644 crypto-inventory/test/fixturi/negative/ZgomotSecureRandom.kt create mode 100644 crypto-inventory/test/fixturi/negative/comentarii.swift create mode 100644 crypto-inventory/test/fixturi/negative/zgomot-securitate.swift create mode 100644 crypto-inventory/test/fixturi/swift/biblioteci.swift create mode 100644 crypto-inventory/test/fixturi/swift/cryptokit.swift create mode 100644 crypto-inventory/test/fixturi/swift/libsignal-import.swift create mode 100644 crypto-inventory/test/fixturi/swift/libsignal.swift create mode 100644 crypto-inventory/test/fixturi/swift/securitate.swift diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md index cb85927..044caa1 100644 --- a/CITATIONS-UNRESOLVED.md +++ b/CITATIONS-UNRESOLVED.md @@ -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 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 - `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/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 - `proba-client-real.mjs` cited in: aere-quantum/readiness/README.md - `raport.json` cited in: aere-quantum/exposure-report/README.md diff --git a/README.md b/README.md index ff9548d..cd7bbb8 100644 --- a/README.md +++ b/README.md @@ -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-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 | | [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL | -| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, 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 | | [`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 | @@ -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-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`) | -| 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= 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 | | 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`) | | 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 `) | 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) | @@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## 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). diff --git a/control-plane/README.md b/control-plane/README.md index 92f6314..28a1e0a 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -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 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 -recipe (`measured`): the `node` profile end to end; for nginx, Apache and HAProxy only that OpenSSL 3.5 accepts the group list (the -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 -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 -(`dovezi/retete-2026-10-18/`); Apache and HAProxy still from their documentation. +recipe (`measured`): since 2026-10-18 all five profiles end to end, on real servers started from exactly the recipe's fragments +(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; +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/`); not measured: a production server with its full configuration, real browsers as clients. ## 4. Compliance report: `raport-conformitate.mjs` diff --git a/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md b/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md index f662ed5..7f689d9 100644 --- a/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md +++ b/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md @@ -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 | | 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 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 `` | **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). diff --git a/control-plane/dovezi/retete-2026-10-18/masoara-apache-haproxy-rpc2.sh b/control-plane/dovezi/retete-2026-10-18/masoara-apache-haproxy-rpc2.sh new file mode 100644 index 0000000..c4da831 --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/masoara-apache-haproxy-rpc2.sh @@ -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 + + 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" + +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 diff --git a/control-plane/dovezi/retete-2026-10-18/masoara-haproxy2-rpc2.sh b/control-plane/dovezi/retete-2026-10-18/masoara-haproxy2-rpc2.sh new file mode 100644 index 0000000..9a61696 --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/masoara-haproxy2-rpc2.sh @@ -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 diff --git a/control-plane/remediere.mjs b/control-plane/remediere.mjs index 4e05426..0cea899 100644 --- a/control-plane/remediere.mjs +++ b/control-plane/remediere.mjs @@ -29,8 +29,8 @@ const HSTS = 'max-age=31536000'; 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)', 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', - 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', + 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 , 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: '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)', }; @@ -104,7 +104,7 @@ const GENERATOARE = { s.tlsMin && `ssl-default-bind-options ssl-min-ver TLSv${s.tlsMin}`, s.hsts && `http-response set-header Strict-Transport-Security "${s.hsts}"`, ].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', }), node: (s) => { diff --git a/crypto-inventory/README.md b/crypto-inventory/README.md index 2eea80e..8ab1391 100644 --- a/crypto-inventory/README.md +++ b/crypto-inventory/README.md @@ -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`) | | 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::`, `Hkdf::`, `type HmacSha256 = Hmac`, `SigningKey::`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::`, `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`, `cbc::Decryptor`); `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::::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`, `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.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 | -| 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 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 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 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 @@ -186,30 +206,40 @@ console.log(renderSummary(result)); configuration files at run time. Those appear as `unknown` at best, or not at all. - It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key is reported like MD5 used for signatures; the reason text tells you what to check. -- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java, Go and Rust. - Kotlin, Scala, C#, C and C++ files are only checked for PEM blocks. Files that are not UTF-8 - text (including UTF-16) are treated as binary and counted as not read. +- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java, Go, Rust, Swift + and Kotlin. Scala, C#, C, C++ and Objective-C files are only checked for PEM blocks. Files that are not + UTF-8 text (including UTF-16) are treated as binary and counted as not read. - It does not execute anything from the scanned tree, and it makes no network requests. ## Tests ``` -node test/proba.mjs # 52 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/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 (35 mutations), and # requires the matching probe to fail while the rest of the suite still runs -node test/proba-timp.mjs # 8 hostile inputs, each at two sizes: the cost must grow linearly -node test/control-negativ-timp.mjs # puts each quadratic form back and requires the cost probe to turn red +node test/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 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 - # added after 0.1.0 (Rust, the libsignal Java/Node API) are counted apart, and - # a finding that 0.1.0 had and the current version lacks is always a difference + # added after 0.1.0 (Rust, Swift, Kotlin, the libsignal Java/Node API, the JVM rules of 0.4.0) + # 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 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) -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. +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: | `), and a probe fails on a missing finding, an extra finding, a wrong line or a wrong class. Negative fixtures cover comments, docstrings, strings that mention crypto APIs, a file without cryptography, algorithms from diff --git a/crypto-inventory/lib/catalog.mjs b/crypto-inventory/lib/catalog.mjs index 997f805..2a9fe5c 100644 --- a/crypto-inventory/lib/catalog.mjs +++ b/crypto-inventory/lib/catalog.mjs @@ -227,6 +227,8 @@ export function evalueaza(a) { } 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 }) }); const h = HASH[a.hash]; 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 }) }); } 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') { 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.` }) }); diff --git a/crypto-inventory/lib/context.mjs b/crypto-inventory/lib/context.mjs index 9c6a98b..e6b1c1b 100644 --- a/crypto-inventory/lib/context.mjs +++ b/crypto-inventory/lib/context.mjs @@ -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)); 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}(? 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(`(? { 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(`(?= 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(/(? 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]}"` }); } } + // 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(/(? 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(/(? (kotlin ? nume : `new ${nume}`); + for (const m of ctx.potriviri(/(? /^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() }); diff --git a/crypto-inventory/lib/detect-kotlin.mjs b/crypto-inventory/lib/detect-kotlin.mjs new file mode 100644 index 0000000..c4d41ab --- /dev/null +++ b/crypto-inventory/lib/detect-kotlin.mjs @@ -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); +} diff --git a/crypto-inventory/lib/detect-swift.mjs b/crypto-inventory/lib/detect-swift.mjs new file mode 100644 index 0000000..cb3d615 --- /dev/null +++ b/crypto-inventory/lib/detect-swift.mjs @@ -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, AES.GCM.seal/open, ChaChaPoly, +// Curve25519.KeyAgreement / .Signing, P256/P384/P521.Signing / .KeyAgreement, HKDF.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(/(?/g)) adauga(null, m.index, `HMAC<${m[1]}>`, 'call', { grup: 'MAC', hash: m[1], functii: ['tag'] }, m[0]); + for (const m of ctx.potriviri(/(?\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(/(? ({ 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'] })], + [/(? X('libsodium crypto_box / crypto_kx / crypto_scalarmult: X25519.')], + [/(? ({ grup: 'EDDSA', nume: 'Ed25519', functii: [/keypair/.test(m[0]) ? 'keygen' : /verify|open/.test(m[0]) ? 'verify' : 'sign'] })], + [/(? ({ 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]] })], + [/(? ({ grup: 'CIPHER', nume: 'XSalsa20-Poly1305', functii: ['encrypt'] })], + [/(? ({ grup: 'KDF', nume: 'Argon2id', functii: ['keyderive'] })], + [/(? ({ grup: 'HASH', hash: 'BLAKE2B', functii: ['digest'] })], + [/(? ({ grup: 'HASH', hash: m[1].toUpperCase(), functii: ['digest'] })], + [/(? ({ 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(/(? 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) { return (a, b) => { 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; } if (c === '/' && d === '*') { - let j = i + 2; - let ad = 1; - while (j < n && ad > 0) { - if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; } - if (text[j] === '*' && text[j + 1] === '/') { ad--; j += 2; continue; } - j++; - } + const j = sfarsitComentariuImbricat(text, i, n); // (0.4.0: aceeasi functie pentru Rust, Swift si Kotlin) goleste(i, j); i = j; continue; } const dupaCuvant = i > 0 && /\w/.test(text[i - 1]); diff --git a/crypto-inventory/lib/manifeste.mjs b/crypto-inventory/lib/manifeste.mjs index d0aac3b..f9a3907 100644 --- a/crypto-inventory/lib/manifeste.mjs +++ b/crypto-inventory/lib/manifeste.mjs @@ -32,7 +32,30 @@ const MAVEN_GROUP = { 'org.bouncycastle': 'Bouncy Castle: classical and post-quantum algorithms', 'io.jsonwebtoken': 'JJWT: JWS HS*/RS*/ES*/PS*/EdDSA', 'com.nimbusds': 'Nimbus JOSE+JWT', 'com.auth0': 'java-jwt (if artifact is java-jwt)', 'org.web3j': 'secp256k1 ECDSA accounts (web3j)', 'com.google.crypto.tink': 'Tink: AEAD, signatures, hybrid encryption', 'org.conscrypt': 'Conscrypt TLS provider', + // 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 = [ [/^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'], @@ -61,18 +84,24 @@ const CARGO = [ [/^(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) { const { line } = randul(inceputuri, pos); - const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` - : ecosistem === 'pypi' ? `pkg:pypi/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` - : ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` - : ecosistem === 'cargo' ? `pkg:cargo/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` - : `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`; + const v = versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''; + const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${v}` + : ecosistem === 'pypi' ? `pkg:pypi/${nume}${v}` + : ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${v}` + : 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 }; } +// 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) { const inceputuri = inceputuriDeRand(text); const r = []; @@ -128,11 +157,31 @@ export function citesteManifest(text, numeFisier, rel) { if (!ofera) continue; r.push(lib('maven', `${g[1]}:${a[1]}`, v && !/\$\{/.test(v[1]) ? v[1] : undefined, ofera, m.index + m[0].indexOf(a[0]), inceputuri, rel)); } - } else if (/^build\.gradle/.test(numeFisier)) { + } 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)) { const ofera = MAVEN_GROUP[m[1]]; if (ofera) r.push(lib('maven', `${m[1]}:${m[2]}`, m[3], ofera, m.index + 1, inceputuri, rel)); } + } else if (numeFisier === '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') { let pos = 0; let inRequire = false; diff --git a/crypto-inventory/lib/scan.mjs b/crypto-inventory/lib/scan.mjs index da7197e..fa1863f 100644 --- a/crypto-inventory/lib/scan.mjs +++ b/crypto-inventory/lib/scan.mjs @@ -8,10 +8,12 @@ import { detecteazaPy } from './detect-py.mjs'; import { detecteazaJava } from './detect-java.mjs'; import { detecteazaGo } from './detect-go.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 { 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']; const LIMBAJ = { @@ -21,8 +23,11 @@ const LIMBAJ = { '.java': 'java', '.go': 'go', '.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; export function limbajul(cale) { diff --git a/crypto-inventory/test/control-negativ-timp.mjs b/crypto-inventory/test/control-negativ-timp.mjs index 5bac15d..82e0a49 100644 --- a/crypto-inventory/test/control-negativ-timp.mjs +++ b/crypto-inventory/test/control-negativ-timp.mjs @@ -6,6 +6,7 @@ // P1 rezolvarea variabilelor fara memorie (fiecare apel reciteste fisierul) -> js-linie rosu // P2 esecurile literalului regex JS netinute minte -> js-regex rosu // P3 cautarea PEM fara pozitiile tinute minte (sfarsitul cautat de la fiecare antet) -> pem rosu +// 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 import { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, mkdirSync } from 'node:fs'; 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']], ['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']], + ['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) { total++; diff --git a/crypto-inventory/test/control-negativ.mjs b/crypto-inventory/test/control-negativ.mjs index e7d5a9f..3a92127 100644 --- a/crypto-inventory/test/control-negativ.mjs +++ b/crypto-inventory/test/control-negativ.mjs @@ -67,11 +67,92 @@ const MUTATII = [ { nume: 'Cargo.toml scos din manifeste', fisier: 'lib/manifeste.mjs', - ancora: '|go\\.mod|Cargo\\.toml)$/;', - cu: '|go\\.mod)$/;', + ancora: '|Cargo\\.toml|Package\\.swift|Podfile|', + cu: '|Package\\.swift|Podfile|', tinta: 'pozitiv: manifestele dau biblioteci declarate, nu gasiri', 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', fisier: 'lib/catalog.mjs', diff --git a/crypto-inventory/test/echivalenta-0.1.0.mjs b/crypto-inventory/test/echivalenta-0.1.0.mjs index 62c1da2..229e41c 100644 --- a/crypto-inventory/test/echivalenta-0.1.0.mjs +++ b/crypto-inventory/test/echivalenta-0.1.0.mjs @@ -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]') }); for (const d of DOSARE) { const a = SV.scaneaza(d), b = SN.scaneaza(d); - // 0.3.0 (2026-10-18): Rust si Cargo.toml nu existau in 0.1.0, deci gasirile lor nu intra in echivalenta; se numara si se spun - const rustN = b.findings.filter((g) => g.language === 'rust').length; - const cargoN = b.libraries.filter((x) => x.ecosystem === 'cargo').length; - b.findings = b.findings.filter((g) => g.language !== 'rust'); - b.libraries = b.libraries.filter((x) => x.ecosystem !== 'cargo'); - if (rustN || cargoN) console.log(` (in afara echivalentei: ${rustN} gasiri Rust, ${cargoN} biblioteci Cargo; 0.1.0 nu le stia)`); + // 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.4.0 (2026-10-18): la fel Swift si Kotlin, manifestele Swift Package Manager / CocoaPods / libs.versions.toml, si grupurile Maven + // adaugate atunci (org.signal, org.whispersystems, org.mindrot, de.mkammerer, com.lambdaworks, org.bitcoinj). + const LIMBAJE_NOI = new Set(['rust', 'swift', 'kotlin']); + 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); + 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 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, // 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. - 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 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); comparate += ka.length; if (!ok) dif++; diff --git a/crypto-inventory/test/fixturi/kotlin/Android.kt b/crypto-inventory/test/fixturi/kotlin/Android.kt new file mode 100644 index 0000000..16d5f22 --- /dev/null +++ b/crypto-inventory/test/fixturi/kotlin/Android.kt @@ -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() + } +} diff --git a/crypto-inventory/test/fixturi/kotlin/Libsignal.kt b/crypto-inventory/test/fixturi/kotlin/Libsignal.kt new file mode 100644 index 0000000..4eb3558 --- /dev/null +++ b/crypto-inventory/test/fixturi/kotlin/Libsignal.kt @@ -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)) + } +} diff --git a/crypto-inventory/test/fixturi/manifeste/Exemplu.podspec b/crypto-inventory/test/fixturi/manifeste/Exemplu.podspec new file mode 100644 index 0000000..fede078 --- /dev/null +++ b/crypto-inventory/test/fixturi/manifeste/Exemplu.podspec @@ -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 diff --git a/crypto-inventory/test/fixturi/manifeste/Package.swift b/crypto-inventory/test/fixturi/manifeste/Package.swift new file mode 100644 index 0000000..99c69ab --- /dev/null +++ b/crypto-inventory/test/fixturi/manifeste/Package.swift @@ -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"]), + ] +) diff --git a/crypto-inventory/test/fixturi/manifeste/Podfile b/crypto-inventory/test/fixturi/manifeste/Podfile new file mode 100644 index 0000000..262b3eb --- /dev/null +++ b/crypto-inventory/test/fixturi/manifeste/Podfile @@ -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 diff --git a/crypto-inventory/test/fixturi/manifeste/build.gradle.kts b/crypto-inventory/test/fixturi/manifeste/build.gradle.kts new file mode 100644 index 0000000..ea04787 --- /dev/null +++ b/crypto-inventory/test/fixturi/manifeste/build.gradle.kts @@ -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") +} diff --git a/crypto-inventory/test/fixturi/negative/Comentarii.kt b/crypto-inventory/test/fixturi/negative/Comentarii.kt new file mode 100644 index 0000000..9bedc94 --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/Comentarii.kt @@ -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 + } +} diff --git a/crypto-inventory/test/fixturi/negative/FaraLibsignal.kt b/crypto-inventory/test/fixturi/negative/FaraLibsignal.kt new file mode 100644 index 0000000..0a5bf9c --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/FaraLibsignal.kt @@ -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) + } +} diff --git a/crypto-inventory/test/fixturi/negative/ZgomotSecureRandom.kt b/crypto-inventory/test/fixturi/negative/ZgomotSecureRandom.kt new file mode 100644 index 0000000..12ea616 --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/ZgomotSecureRandom.kt @@ -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 +} diff --git a/crypto-inventory/test/fixturi/negative/comentarii.swift b/crypto-inventory/test/fixturi/negative/comentarii.swift new file mode 100644 index 0000000..10d6452 --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/comentarii.swift @@ -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.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 +} diff --git a/crypto-inventory/test/fixturi/negative/zgomot-securitate.swift b/crypto-inventory/test/fixturi/negative/zgomot-securitate.swift new file mode 100644 index 0000000..650ecdf --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/zgomot-securitate.swift @@ -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) +} diff --git a/crypto-inventory/test/fixturi/swift/biblioteci.swift b/crypto-inventory/test/fixturi/swift/biblioteci.swift new file mode 100644 index 0000000..d7406c2 --- /dev/null +++ b/crypto-inventory/test/fixturi/swift/biblioteci.swift @@ -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) +} diff --git a/crypto-inventory/test/fixturi/swift/cryptokit.swift b/crypto-inventory/test/fixturi/swift/cryptokit.swift new file mode 100644 index 0000000..7e8e24b --- /dev/null +++ b/crypto-inventory/test/fixturi/swift/cryptokit.swift @@ -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.authenticationCode(for:using:), +// AES.GCM.seal(_:using:), ChaChaPoly.seal, Curve25519.KeyAgreement.PrivateKey(), P256.Signing.PrivateKey(), +// HKDF.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.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.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) +} diff --git a/crypto-inventory/test/fixturi/swift/libsignal-import.swift b/crypto-inventory/test/fixturi/swift/libsignal-import.swift new file mode 100644 index 0000000..f040c50 --- /dev/null +++ b/crypto-inventory/test/fixturi/swift/libsignal-import.swift @@ -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() +} diff --git a/crypto-inventory/test/fixturi/swift/libsignal.swift b/crypto-inventory/test/fixturi/swift/libsignal.swift new file mode 100644 index 0000000..966d4eb --- /dev/null +++ b/crypto-inventory/test/fixturi/swift/libsignal.swift @@ -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) +} diff --git a/crypto-inventory/test/fixturi/swift/securitate.swift b/crypto-inventory/test/fixturi/swift/securitate.swift new file mode 100644 index 0000000..970a69f --- /dev/null +++ b/crypto-inventory/test/fixturi/swift/securitate.swift @@ -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? + 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) +} diff --git a/crypto-inventory/test/proba-timp.mjs b/crypto-inventory/test/proba-timp.mjs index c040d2f..bb8c128 100644 --- a/crypto-inventory/test/proba-timp.mjs +++ b/crypto-inventory/test/proba-timp.mjs @@ -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 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 = { // pem: cautarea sfarsitului e nativa (indexOf), deci forma patratica se vede abia spre marimea maxima implicita 'pem.txt': ['-----BEGIN CERTIFICATE-----AAAA\n', 256, 1024], @@ -27,11 +27,21 @@ export const CAZURI = { 'js-func.js': 'crypto.createHash(q)\nfunction (', 'java-linie.java': 'Cipher.getInstance(x)+', '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 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; }; @@ -41,7 +51,7 @@ if (arg('--caz')) { const inv = await import(pathToFileURL(CALE_MODUL).href); const d = mkdtempSync(join(tmpdir(), 'ci-timp-')); 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 rez = inv.scan(d); const ms = Number(process.hrtime.bigint() - t0) / 1e6; diff --git a/crypto-inventory/test/proba.mjs b/crypto-inventory/test/proba.mjs index 80b231a..bb8c6a2 100644 --- a/crypto-inventory/test/proba.mjs +++ b/crypto-inventory/test/proba.mjs @@ -3,7 +3,7 @@ // Modulul masurat se poate schimba cu CRYPTO_INVENTORY_MODULE=/inventar.mjs (asa il foloseste // control-negativ.mjs pe o copie stricata). Fixturile raman mereu cele din test/fixturi. // Fiecare proba ruleaza izolat: o proba care arunca iese ROSU cu mesajul ei, restul ruleaza mai departe. -import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, writeFileSync, rmSync, copyFileSync } from 'node:fs'; +import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, copyFileSync } from 'node:fs'; import { join, resolve, dirname } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { spawnSync } from 'node:child_process'; @@ -18,8 +18,12 @@ const lx = await import(pathToFileURL(join(dirname(CALE_MODUL), 'lib', 'lexer.mj const CA_JSON = process.argv.includes('--json'); // numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient -const MARCAJE_ASTEPTATE = 158; -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']; +// (0.4.0, 2026-10-18: 158 + 111 din fixturile Swift si Kotlin, numarate de compara-fixturi inainte de a fi scrise aici) +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 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', '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', - '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(' | ')}`); const bom = inv.buildCbom(rez, { deterministic: true }); 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)', () => { @@ -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', () => { 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'); 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) proba('negativ: numele API-ului libsignal fara importul lui nu produc gasiri (Java si TypeScript), iar cu import se recunosc', () => { const rez = scan(FIX);