Skip to content

feat(wia): WebView reads existing identity/documents via host secureStorage handler (SELF-3584) - #2230

Open
seshanthS wants to merge 5 commits into
devfrom
feat/wia-b2-securestorage-translator
Open

feat(wia): WebView reads existing identity/documents via host secureStorage handler (SELF-3584)#2230
seshanthS wants to merge 5 commits into
devfrom
feat/wia-b2-securestorage-translator

Conversation

@seshanthS

@seshanthS seshanthS commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Implements B2 (SELF-3584) — existing users' identity + documents are visible to the WebView path, with no migration/copy and without weakening or touching the legacy keychain.

Approach (per review discussion): host-provided translating secureStorage handler

Crypto is already compatible (same mnemonic → same key at m/44'/60'/0'/0/0); only storage namespace differs. Rather than copy legacy data into the SDK's self_sdk_* store (which uses react-native-keychain defaults — weaker, non-biometric), the app services the WebView's secureStorage in place from its existing biometric-gated keychain.

  • rn-sdk: secureStorage handler is now host-injectable — new SecureStorageStore + optional secureStorage prop on <SelfVerification>, mirroring the existing documents injection. Default stays KeychainHandler. Additive/opt-in: no bridge-protocol change; the KMP path (useKmpBridge intercepts secureStorage before the router) is untouched.
  • app: WebViewHostScreen provides webViewSecureStorageAdapter mapping the WebView's self_* keys → legacy secret / documentCatalog / document-{contentHash} for get and set. Content-hash doc ids kept (WebView treats doc.id as opaque). Reuses authProvider (new getStoredMnemonicPhrase/restoreMnemonicPhrase) + passportDataProvider primitives. requireBiometric is honored, so the secret keeps strong protection.
  • Identity write-guard: set('self_mnemonic'/'self_private_key') never overwrites an existing secret (blocks a biometric-cancel-then-mint from clobbering the identity). Removes of identity/catalog are no-ops.

Result: no migration, no copy, no drift, legacy stays the single biometric-gated source of truth, cloud backup untouched, rollback trivial (flip IS_WIA_ENABLED off).

Follow-up (deferred, Path A)

When secureStorage migrates to the KMP transport, re-inject the same translation as a host SecureStorageProvider via SdkProviderRegistry (the ==null hook in SelfBridgeModule.kt). The translation logic is written to be reusable there.

Validation

  • rn-sdk: 180 tests + types (5 new injected-store cases).
  • app: webViewSecureStorageAdapter 17 tests, WebViewHostScreen 8, full app tsc clean.
  • webview-app: derivePrivateKey BIP44 parity guard (golden Hardhat key == ethers) → proves the translated key == the existing identity.
  • On-device pending (SELF-3584 AC / ties to SELF-3582): registered user with IS_WIA_ENABLED on shows same identity + docs, no re-registration, recovery works, legacy never overwritten.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added host-injected secure storage for WebView identity verification, including mnemonic/private-key/document catalog translation with safe write/remove behavior.
    • Added helpers to retrieve a stored mnemonic and restore a mnemonic phrase, with optional biometric-gated retrieval.
  • Tests
    • Added coverage for the secure storage adapter, injected secure-storage delegation, and BIP44/private-key parity validation.
  • Documentation
    • Updated the WebView in-app cutover plan to reflect the implemented secure-storage approach.

seshanthS and others added 4 commits July 23, 2026 17:44
Adds an optional SecureStorageStore + a secureStorage prop on
SelfVerification (mirroring the existing documents injection). When a host
store is provided, KeychainHandler delegates get/set/remove to it and
forwards requireBiometric; otherwise the default self_sdk_ react-native-
keychain behavior is unchanged. Additive/opt-in: no bridge-protocol change,
KMP path (useKmpBridge intercepts secureStorage before the router) untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cureStorage (SELF-3584)

WebViewHostScreen provides a secureStorage store that maps the WebView's
self_* keys to the app's EXISTING legacy keychain (secret / documentCatalog /
document-{contentHash}) for reads and writes — no migration, no copy, legacy
stays the single biometric-gated source of truth, content-hash doc ids kept.
Identity write-guard: set(self_mnemonic/self_private_key) never overwrites an
existing secret. Reuses authProvider (new getStoredMnemonicPhrase /
restoreMnemonicPhrase exports) + passportDataProvider primitives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…parity guard)

Golden-value assertion (Hardhat account-0 key) locks @Scure derivation to
the same key ethers produces at m/44'/60'/0'/0/0, guaranteeing the WebView
derives the identical key as the app from the same phrase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
self-webview-app Ready Ready Preview, Comment Jul 23, 2026 12:36pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ee9f88bd-d12d-4905-a9fe-79ab4f7dc044

📥 Commits

Reviewing files that changed from the base of the PR and between feaf9ec and 4bdf692.

📒 Files selected for processing (1)
  • packages/webview-app/tests/utils/secretManager.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/webview-app/tests/utils/secretManager.test.ts

📝 Walkthrough

Walkthrough

The RN SDK now accepts injected secure storage, and the app supplies an adapter that maps WebView self_* keys to legacy Keychain and document storage. Mnemonic handling, private-key derivation, host wiring, tests, and cutover documentation were added.

Changes

WebView secure storage

Layer / File(s) Summary
Injected SDK storage backend
packages/rn-sdk/src/handlers/..., packages/rn-sdk/src/index.ts, packages/rn-sdk/src/__tests__/KeychainHandler.test.ts
KeychainHandler supports an injected SecureStorageStore, and the handler configuration and package exports expose it.
Legacy storage translation and validation
app/src/providers/authProvider.tsx, app/src/providers/webViewSecureStorageAdapter.ts, app/tests/src/providers/webViewSecureStorageAdapter.test.ts, packages/webview-app/tests/utils/secretManager.test.ts, specs/projects/.../WIA-APP-CUTOVER.md
The adapter translates identity, document, and passthrough keys to legacy storage with guarded writes and deletion rules; adapter behavior and BIP44 derivation are tested and documented.
Verification host wiring
packages/rn-sdk/src/SelfVerification.tsx, app/src/screens/dev/WebViewHostScreen.tsx
SelfVerification accepts secure storage, and WebViewHostScreen creates and passes the adapter to it.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: WebView now reads existing identity and documents through a host secureStorage handler.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wia-b2-securestorage-translator

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: feaf9ec4d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +141 to +144
if (await deps.hasSecretStored()) {
return;
}
await deps.writeNewMnemonic(value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Distinguish keychain errors from a missing identity

When an existing protected secret cannot be read—for example after biometric cancellation or a transient keychain error—hasSecretStored() catches the error and returns false, so this branch treats the user as new and calls writeNewMnemonic() for the same service. That can replace the existing identity with the mnemonic generated by ensureSecret() despite this guard being intended to prevent exactly that data loss; only a definitive “item absent” result should permit the write, while read errors must reject the bridge request.

AGENTS.md reference: AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

Comment on lines +141 to +142
if (await deps.hasSecretStored()) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Permit validated recovery to replace the stored mnemonic

For an existing user, the WebView recovery flow validates the submitted phrase against the selected document and then calls restoreSecretFromMnemonic(), but this unconditional no-op reports success without changing the legacy secret. The flow subsequently finalizes registration and displays recovery success while later proving still uses the old key, so the adapter needs an explicit authenticated recovery operation rather than silently discarding every mnemonic replacement.

AGENTS.md reference: AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

*/
export async function restoreMnemonicPhrase(phrase: string): Promise<void> {
const options = await createKeychainOptions({ requireAuth: true });
await restoreFromMnemonic(phrase, options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject the bridge write when mnemonic persistence fails

When Keychain.setGenericPassword() fails, restoreFromMnemonic() catches the error and resolves to false, but this wrapper discards that result and resolves successfully. Consequently a first-time secureStorage.set('self_mnemonic', ...) receives a successful bridge response even though no identity was persisted, allowing ensureSecret() and onboarding to continue until a later key read or proof fails; convert the false result into a rejection.

AGENTS.md reference: AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e8939d34-e93d-4a1b-8d88-1a9e5d072fb6

📥 Commits

Reviewing files that changed from the base of the PR and between 0d85a91 and feaf9ec.

📒 Files selected for processing (11)
  • app/src/providers/authProvider.tsx
  • app/src/providers/webViewSecureStorageAdapter.ts
  • app/src/screens/dev/WebViewHostScreen.tsx
  • app/tests/src/providers/webViewSecureStorageAdapter.test.ts
  • packages/rn-sdk/src/SelfVerification.tsx
  • packages/rn-sdk/src/__tests__/KeychainHandler.test.ts
  • packages/rn-sdk/src/handlers/KeychainHandler.ts
  • packages/rn-sdk/src/handlers/index.ts
  • packages/rn-sdk/src/index.ts
  • packages/webview-app/tests/utils/secretManager.test.ts
  • specs/projects/sdk/workstreams/webview-in-app/plans/WIA-APP-CUTOVER.md

Comment on lines +137 to +146
async set(key, value) {
if (key === MNEMONIC_KEY) {
// Identity write-guard: never overwrite an existing identity. Only a
// genuine new user (no legacy secret) may write a fresh mnemonic.
if (await deps.hasSecretStored()) {
return;
}
await deps.writeNewMnemonic(value);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does react-native-keychain getGenericPassword throw when the user cancels the biometric prompt, even for a presence-only check?

💡 Result:

Yes, react-native-keychain's getGenericPassword method will throw an error when a user cancels the biometric prompt, regardless of whether it is a full authentication check or a presence-only check [1][2]. When biometric authentication is requested via getGenericPassword—often triggered by options requiring authentication—the native biometric prompt is presented to the user [1][3]. If the user explicitly cancels this prompt (e.g., by tapping "Cancel," pressing the back button, or clicking outside the prompt), the library rejects the promise, causing getGenericPassword to throw an error [1][2][4]. Key details regarding this behavior include: Error Handling: Because the cancellation is treated as a rejection, developers must wrap the getGenericPassword call in a try/catch block to handle the user's action gracefully [5][1]. Platform Inconsistency: Historically, error messages and codes for cancellation have varied significantly between iOS and Android, requiring developers to match on specific error messages or codes [1][2][6]. Newer versions of the library (such as via PR #762) have introduced standardized error codes (e.g., Keychain.ERROR_CODE.AUTH_CANCELED) to improve cross-platform consistency [5][7]. No Distinction for Presence-Only: The library does not inherently treat "presence-only" checks differently regarding the promise resolution. If the prompt is displayed to the user and they decline to complete the biometric interaction, the operation is treated as an authentication failure due to user cancellation [2][6]. For robust implementation, developers should check for user cancellation errors within the catch block using the standardized error codes if available, or by inspecting the error object for known cancellation patterns if using older versions of the library [5][1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'webViewSecureStorageAdapter\.ts|authProvider\.ts|authProvider\.tsx|passportDataProvider\.tsx|passportDataProvider\.ts|webViewSecureStorageAdapter' || true

echo "== outlines =="
for f in $(git ls-files | rg 'webViewSecureStorageAdapter\.ts|authProvider\.ts|authProvider\.tsx|passportDataProvider\.tsx|passportDataProvider\.ts' || true); do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  ast-grep outline "$f" --view compact || true
done

echo "== relevant snippets =="
for f in app/src/providers/webViewSecureStorageAdapter.ts app/src/providers/authProvider.tsx app/src/providers/passportDataProvider.tsx app/src/providers/authProvider.ts 2>/dev/null; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n "hasSecretStored|writeNewMnemonic|restoreFromMnemonic|getGenericPassword|setGenericPassword|MNEMONIC_KEY|self_mnemonic|requireAuth|Keychain" "$f" -C 4 || true
  fi
done

echo "== package keychain versions =="
rg -n '"react-native-keychain"|keychain' package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || true

Repository: selfxyz/self

Length of output: 1083


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant snippets =="
sed -n '1,220p' app/src/providers/webViewSecureStorageAdapter.ts | cat -n
echo
sed -n '1,260p' app/src/providers/authProvider.tsx | cat -n
echo
sed -n '1,240p' app/src/providers/passportDataProvider.tsx | cat -n

echo "== keychain usages/options =="
rg -n "getGenericPassword|setGenericPassword|deleteGenericPassword|requireAuth|SERVICE_NAME|MNEMONIC_KEY|storeMnemonic|restoreFromMnemonic|writeNewMnemonic|hasSecretStored" app/src/providers/app/src/providers app/src/providers -C 5 || true

echo "== package keychain versions =="
rg -n '"react-native-keychain"|keychain' package.json pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null || true

Repository: selfxyz/self

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== authProvider hasSecretStored section =="
sed -n '503,638p' app/src/providers/authProvider.tsx | cat -n

echo "== keychain option creators =="
fd -a 'keychain.*' app/src | sed 's#^\./##' | head -20
for f in app/src/integrations/keychain.ts app/src/integrations/keychain.tsx app/src/utils/keychainErrors.ts app/src/integrations/keychain.ts 2>/dev/null; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,260p'
  fi
done

echo "== migration/restore callers for createKeychainOptions(requireAuth true) =="
rg -n "createKeychainOptions\\(\\{ requireAuth: true \\}\\)|restoreMnemonicPhrase\\(|restoreAccountFromMnemonic|resetGenericPassword\\(\\{ service: 'secret' \\}\\)|resetGenericPassword\\(\\{ service: \"secret\" \\}\\)|storeMnemonic|hasSecretStored" app/src/providers app/src -C 4 -g '*.ts' -g '*.tsx' || true

Repository: selfxyz/self

Length of output: 5172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== keychain integrations files =="
git ls-files | rg '^app/src/(integrations|types|utils)/' | rg 'keychain|react-native-keychain|keychainErrors'

echo "== keychain option creators =="
for f in app/src/integrations/keychain.ts app/src/integrations/keychain/index.ts app/src/integrations/keychain/index.tsx app/src/types/react-native-keychain.d.ts app/src/utils/keychainErrors.ts 2>/dev/null; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,260p'
  fi
done

echo "== identity store writes and guard callers =="
rg -n "createKeychainOptions\\(|requireAuth: true|SERVICE_NAME|resetGenericPassword|hasSecretStored\\(|restoreMnemonicPhrase\\(|restoreFromMnemonic|storeMnemonicPhrase|save\\(.*self_mnemonic|safeLoadDocumentCatalog" app/src/providers app/src -g '*.ts' -g '*.tsx' -C 3 2>/dev/null || true

Repository: selfxyz/self

Length of output: 380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== keychain integrations files =="
git ls-files | rg '^app/src/(integrations|types|utils)/' | rg 'keychain|react-native-keychain|keychainErrors'

for f in app/src/integrations/keychain/index.ts app/src/types/react-native-keychain.d.ts app/src/utils/keychainErrors.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,260p'
  fi
done

echo "== identity store writes and guard callers =="
rg -n "createKeychainOptions\\(|requireAuth: true|SERVICE_NAME|resetGenericPassword|hasSecretStored\\(|restoreMnemonicPhrase\\(|restoreFromMnemonic|storeMnemonicPhrase|save\\(.*self_mnemonic|safeLoadDocumentCatalog" app/src/providers app/src -g '*.ts' -g '*.tsx' -C 3 2>/dev/null || true

echo "== static diff/changes around files =="
git diff -- app/src/providers/webViewSecureStorageAdapter.ts app/src/providers/authProvider.tsx app/src/providers/passportDataProvider.tsx | sed -n '1,260p' || true

Repository: selfxyz/self

Length of output: 50368


🌐 Web query:

react-native-keychain getGenericPassword accessControl user cancel biometric prompt returns false null or throws

💡 Result:

When a user cancels a biometric prompt during Keychain.getGenericPassword(), the library does not return false or null; instead, it throws an error [1]. Historically, handling these cancellations required parsing platform-specific error messages or codes [2][3]. For instance, on Android, cancellations often manifest as errors containing codes like 10 or 13 [2][4][3], while iOS typically returns a message such as "User canceled the operation" [2]. Recent updates have introduced standardized error handling to resolve these inconsistencies [5][6]. You should wrap your getGenericPassword call in a try...catch block and check for specific error codes provided by the library, such as Keychain.ERROR_CODE.AUTH_CANCELED [5]. Example of recommended handling: try { await Keychain.getGenericPassword; } catch (error) { switch (error.code) { case Keychain.ERROR_CODE.AUTH_CANCELED: console.log('Authentication was canceled'); break; case Keychain.ERROR_CODE.BIOMETRIC_LOCKOUT: console.log('Biometric authentication locked'); break; default: console.log(Keychain error: ${error.code} - ${error.message}); } } If you encounter false or null results in other scenarios, they typically indicate that no credentials were found for the specified service or storage, rather than an authentication cancellation [7][8]. Always ensure that you are using consistent service names and configurations when setting and getting credentials to avoid unexpected "not found" results [9][8].

Citations:


Handle biometric/auth errors in the identity presence check.

hasSecretStored() does a plain Keychain.getGenericPassword({ service: 'secret' }) inside a catch-all that returns false. If a biometric/credential prompt is cancelled or fails for this Keychain entry, the write-guard treats the real identity as missing and allows restoreMnemonicPhrase() to overwrite it. Preserve an existing identity on any keychain/auth failure (including user cancellation) before minting a new mnemonic.

Fixes WebView App CI format/lint (long golden hex on one line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant