Four phases. Each task carries acceptance criteria so it can be lifted verbatim into a GitHub issue. Order within a phase is roughly by dependency, not strict — anything in P0 should land before anything in P2.
Status legend: [ ] open, [~] in progress, [x] done.
The cheapest, broadest wins. Everything here is engineering hygiene that makes later phases possible. None of it requires design or product input.
- Done in PR #12 (
no-floating-promises,security,no-secrets,no-console: error).
-
noUncheckedIndexedAccess,exactOptionalPropertyTypes, andnoImplicitOverrideenabled intsconfig.json. - All 11 resulting errors fixed without loosening the flags or
introducing
as any. Patterns used: explicitoverridekeyword where required, conditional spread for optional props that may legitimately be omitted, and non-null assertion!with an inline justification comment where the array bound is statically known. - Acceptance:
npm run typecheckclean with the new flags; PR diff shows the rule changes plus the fixes, noas anyintroduced.
- Add Jest coverage thresholds (
global: 60%,src/services/**: 70%,src/features/hardware-wallet/**: 90%). - Wire
npm test -- --coverageinto CI as its own job. - Acceptance: CI fails if a PR drops coverage below the gate; baseline documented in this file.
- Every
uses:in.github/workflows/pinned to a commit SHA, with a trailing# vX.Y.Zcomment for human reading. -
permissions: contents: readat workflow root in every workflow; per-job escalation only where required (CodeQL needssecurity-events: write, auto-release-pr needspull-requests: write). -
checksplit into paralleltypecheck,lint,format,testjobs;auditretained. - Acceptance: workflows pass;
permissionsblock present in every workflow file; no@v4/@mainreferences.
- First cleanup PR merged (#20:
uuid+@xmldom/xmldomoverrides to clear initial Dependabot alerts). -
npm audit --audit-level=highas a non-blocking CI job (separateauditjob withcontinue-on-error: trueso it surfaces as a yellow status without breaking the gate). - Dependabot enabled for
npm+github-actions, weekly Monday, targetdevelop(.github/dependabot.yml). - GitHub CodeQL workflow for
javascript-typescriptwith thesecurity-and-qualityquery suite, weekly schedule + on push/PR tomain/develop(.github/workflows/codeql.yml). - Next: flip
auditto blocking once the existing alerts are cleared and we have a few quiet weeks of Dependabot output. - Acceptance: scheduled scans visible in the Security tab; first cleanup PR merged.
Configured as repository rulesets (the modern UI), not classic branch protection — the older API endpoint will report both branches as "unprotected" but the rulesets are active.
The asymmetry between develop and main is deliberate: we want low
friction merging into develop so PRs do not pile up and spawn merge
conflicts with each other; the integration testing happens on develop
itself. The approval gate sits in front of the develop → main release
PR, where it actually matters.
-
developruleset (refs/heads/develop):pull_requestwithrequired_approving_review_count: 0— deliberately no approval needed, optimised for "merge fast, test on develop". Squash-merge only.required_status_checks:typecheck,lint,format,test,Analyze (javascript-typescript).auditdeliberately not required (it'scontinue-on-error).non_fast_forward(no force push).deletionblocked.current_user_can_bypass: never(admins can't bypass).
-
mainruleset (refs/heads/main):pull_requestwithrequired_approving_review_count: 2— this is where the human review gate sits, before anything reaches release.- Plain merge commits (no squash) — preserves develop's commit history on release.
- Same required status checks as develop.
non_fast_forward,deletionblocked, no admin bypass.
- Signed commits — roadmap says "encouraged", left as opt-in for now. Flip to required once everyone has GPG / SSH signing configured.
- Acceptance: settings recorded here; tested by attempting a direct
push to
develop(rejected with "protected branch").
- Done in PR #22 —
SECURITY.mdat repo root with disclosure policy, response timeline, scope, safe-harbor; linked fromREADME.md.
Concrete defenses for the device + UX layer. Most are bounded changes that ship per-screen or per-flow.
- Use
expo-screen-capture(AndroidFLAG_SECURE, iOSUIScreen.isCapturedlistener) on: seed reveal, seed verify, PIN entry, PIN setup, balance card. - Acceptance: manual verification on device + emulator; Android screenshots return blank, iOS screen-recording obscures the view.
- Render an opaque overlay when
AppStatetransitions toinactive/background, on at least the same screens as P1.1 plus the authenticated tabs. - Acceptance: switching apps shows the overlay before the OS snapshot is taken; no leakage in iOS task switcher.
- Track failed attempts in
expo-secure-store. Apply exponential backoff. After N (e.g. 10) failures: wipeexpo-secure-store,IS_ONBOARDED, encrypted seed, and reset to onboarding. - UI shows remaining attempts and lockout time.
- Acceptance: dedicated test in
src/services/pin.tscovers backoff schedule and wipe trigger; manual test confirms recovery flow.
- Require
expo-local-authenticationsuccess immediately before any call into the WDK signing path or the BitBox signing path. Cache result for ≤30s of foreground time. - Acceptance: signing without recent biometric prompts; cancellation aborts cleanly and surfaces no exception.
- On copy of address: announce "copied", auto-clear after 60s if clipboard still holds the same value.
- On copy of mnemonic: confirmation modal explaining the risk; same 60s auto-clear.
- Acceptance: integration test on
Clipboard.setStringAsynccalls; manual verification on iOS + Android.
- Strict parser for
dfx://and Universal Links. Reject unknown hosts/paths. Never auto-confirm a sensitive action (send, swap, KYC callback) from a deep-link payload — always require the user to confirm pre-filled data. - Universal Links signed via Apple AASA / Android assetlinks.
- Acceptance: fuzz-test the parser; manual test that a hostile deep link cannot trigger a transaction without confirmation.
- Audit every
react-native-webviewusage:originWhitelistminimal,allowFileAccess: false,javaScriptEnabledonly when required, noinjectedJavaScriptcarrying user data. - Acceptance: review document listing each WebView and its config.
- HTTPS only. iOS
NSAppTransportSecuritystrict, AndroidcleartextTrafficPermitted=false. - Certificate pinning for
api.dfx.swissandapp.dfx.swissviareact-native-ssl-pinningor platform config. - Pin rotation playbook documented.
- Acceptance: MITM proxy fails to intercept; pin-rotation steps recorded here.
- On app launch, check for jailbreak/root indicators. If detected, show a non-blocking warning with risk explanation. Do not block usage.
- Acceptance: warning visible on a jailbroken simulator profile; unaffected on stock devices.
- Wire Sentry (or equivalent) with
beforeSendfilter that strips: addresses, amounts, mnemonic-shaped strings, JWTs, KYC field values. - No automatic breadcrumbs from sensitive screens.
- Acceptance: deliberate crash from a test screen produces a Sentry event with no PII; review checklist in this doc.
- On every API session start, fetch a min-supported-version from the backend. If app version is below it, show a blocking update screen (only Settings → Reset is reachable).
- Acceptance: dev-server returning a higher min-version triggers the block; works offline-first (graceful degradation if endpoint unreachable).
Tightening the parts an external auditor will look at hardest. None of this is shippable as a single feature — it is the work that makes the audit cheap.
- Add property-based tests (
fast-check) and BIP-vector tests for every supported chain: BTC (BIP44/49/84/86), EVM, Solana, TON, TRON, Spark/Lightning. - Acceptance: per-chain test files under
test/derivation/; CI runs them; failing a vector breaks CI.
- Single source of truth per scheme. Property-based tests for malformed input. Fuzz round-tripping.
- Acceptance: parsers under
src/services/payment-uri/; tests cover amount precision, decimals across chains, malicious unicode.
- Lint rule (
no-restricted-syntax) banningNumberarithmetic on monetary values. Centralise in aMoneyhelper backed bydecimal.js. - Acceptance: rule active in
eslint.config.js;Moneyhelper has 100% coverage; noparseFloat(amount) + ...patterns left.
- Persist BitBox static pubkey on first successful pairing. On subsequent connects, refuse to proceed if it changes; require explicit re-pair confirmation.
- Acceptance: pairing flow re-tested; tampered pubkey produces a hard stop with a clear error message.
- Document and test that for every signing operation (BTC PSBT, ETH EIP-1559, EIP-712, ERC-20), the values shown on-device on the BitBox match what the app displayed — including units and decimals.
- Acceptance: parity matrix in this directory; manual verification recorded per supported transaction type before each release.
- Verify the bundled worklet (
wdk-secret-manager-worklet.bundle.js) is reproducible from source. Pin thebare-packversion, lock the Tether SDK versions exactly (no^). - Acceptance: build from clean checkout produces byte-identical worklet bundle; comparison automated in CI.
- Audit every pricing read path. On error, refuse to proceed (no stale-fallback rendering of an amount).
- Add an explicit "could not load price" UI state.
- Acceptance: simulating an API error produces an error state, not a silently-stale price; documented in threat-model section 4.
- EAS Build with managed credentials. Keystore + provisioning profiles in EAS Secrets, not in repo.
- CycloneDX SBOM produced per release as a build artifact.
- Acceptance: release page lists
.sbom.json; signing keys never appear in the repo.
- Document the exact toolchain (Node version, npm version, Xcode
version, Android SDK). Pin in
package.jsonenginesand EAS config. - CI re-builds the artifact and compares hashes against the release.
- Acceptance: two clean builds from the same git ref produce byte-equal binaries (best effort — note any unavoidable non-determinism).
- Internal walkthrough against the threat model. Ensure every
mitigation marked
✓or~is actually true. Fix gaps before paying for an external review. - Acceptance: review notes filed under
docs/security/; threat model refreshed.
- Candidates worth a quote (engineering opinion only, no commitment):
- Cure53 — strong on mobile + WebView surfaces.
- Trail of Bits — strong on cryptography + native crypto code.
- Least Authority — wallet-specific track record.
- Scope a fixed-price audit: in scope = onboarding, signing, BitBox flow, DFX API integration; out of scope = DFX backend, third-party KYC iframe.
- Acceptance: signed SoW; kickoff date set.
- All Critical / High findings fixed before public release.
- Medium findings either fixed or have a tracked workaround + follow-up issue.
- Acceptance: audit report lives under
docs/security/audits/; remediation PRs cross-referenced.
- After remediation: launch a private bounty (HackerOne or Immunefi). Public after one quiet month.
- Scope, rewards, safe-harbor language drafted.
- Acceptance: bounty page live; first triage rotation scheduled.
Vulnerabilities reported through SECURITY.md (P0.7) get acknowledged
within 72 hours, triaged within one week. Public disclosure happens after
a fix ships, or 90 days after report — whichever comes first.
A real or near-miss security incident produces (1) a chronological
post-mortem in docs/security/incidents/YYYY-MM-DD-<slug>.md, (2) an
update to threat-model.md, (3) a roadmap entry under the appropriate
phase if a structural change is needed.
Any PR touching src/features/hardware-wallet/, src/services/wallet/,
src/features/dfx-backend/, src/services/pin.ts, src/features/biometric/, or
secure storage requires a security-conscious review (one of the
maintainers tagged for security). Document this requirement in
CODEOWNERS once the file exists.