Skip to content

AUD-02: key material & keychain audit + remediation (F-02/F-06/F-07/F-08) - #2185

Open
seshanthS wants to merge 2 commits into
devfrom
fix/aud-02-key-material
Open

AUD-02: key material & keychain audit + remediation (F-02/F-06/F-07/F-08)#2185
seshanthS wants to merge 2 commits into
devfrom
fix/aud-02-key-material

Conversation

@seshanthS

@seshanthS seshanthS commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Linear: SELF-3180

Pilot of the Codebase Audits workstream. This PR contains the AUD-02 findings report and the remediation for the owner-accepted findings (two commits: audit doc + tests, then fixes).

Audit

Read-only audit of mnemonic/keychain generation, storage, migration, biometric gating, cloud backup/restore, and the vendored keychain patch. Full report: docs/reviews/2026-06-17-key-material-keychain-audit.md. Every Critical/Major was independently refutation-verified.

Fixes in this PR

  • F-02 (Critical)loadOrCreateMnemonic no longer overwrites a present-but-unparseable secret. It fails closed (fires keychainCryptoFailureCallback('crypto_failed'), returns false), so the create/overwrite path only runs when no secret existed. Corrupted/legacy entries route to recovery instead of being destroyed.
  • F-07 (Major)hasSecretStored returns a tri-state (present | absent | unknown) instead of collapsing read errors to false. SplashScreen treats only an explicit absent as a fresh install, so a transient keychain error no longer misroutes an existing user into onboarding/recovery.
  • F-08 (Major) — cloud backup keeps a single canonical Drive file. Android upload updates in place via setIdOfFileToUpdate (new settingStore.backupFileId), adopting/deduping pre-existing duplicates; download/delete operate by id with a list+dedupe self-heal fallback. iOS already overwrites a fixed path.
  • F-06 — corrected the useStrongBox JSDoc (effective default is false; StrongBox off by default due to Samsung-device issues).

Owner dispositions (recorded in the report)

  • Accepted, documented: F-03 (non-atomic iOS migration — native lib, crash-window only), F-05 (no-passcode no-auth — intentional), F-09 (dev key-reveal screens in release — intentional).
  • Deferred: F-01 (plaintext cloud backup — app-scoped storage accepted for now), F-04 (restore confirmation).
  • Dropped: F-10–F-14 lows (F-06 JSDoc folded in).

Validation

Run in-worktree after building @selfxyz/common, mobile-sdk-alpha, webview-bridge:

  • jest (authProvider + cloud-backup): 33 passed
  • tsc --noEmit: clean
  • check-test-requires.cjs: clean

The F-02 characterization test is committed pinning current behavior (audit commit) then flipped to assert the fix (remediation commit).

Reviewer note

Both commits used git commit --no-verify: the pre-commit hook runs yarn gitleaks, but dev is on pnpm so the hook errors before scanning. gitleaks protect --staged --config=gitleaks-override.toml was run directly on each commit — no leaks. The stale yarn-based hook is worth a separate fix.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Cloud backup now caches backup file identifiers to eliminate repeated scanning.
    • Backup uploads now update existing files in-place instead of creating duplicates.
  • Bug Fixes

    • Enhanced handling for corrupted stored secrets with failure callbacks.
    • Improved startup routing during transient keychain access errors.
    • Backup cleanup now properly removes cached file references.
  • Tests

    • Expanded test coverage for secret storage states and cloud backup operations.

seshanthS and others added 2 commits June 18, 2026 16:19
Read-only pilot audit (SELF-3180). Adds the findings report, current-behavior
characterization tests, owner dispositions, and the SPEC backlog/status-log update.

Confirmed: F-01 plaintext mnemonic to iCloud/Drive (Critical), F-02 legacy-format
parse failure overwrites the secret (Critical), plus F-03..F-09 and F-10..F-14 lows.
Every Critical/Major independently refutation-verified (Stage 4).

Dispositions: fix F-02/F-06/F-07/F-08 (remediation PR fix/aud-02-key-material);
accept+document F-03/F-05/F-09; defer F-01/F-04; drop lows. AUD-02 row -> In Review.

Tests written but not validated in-session (worktree has no node_modules; the stale
main checkout's jest is broken). Pre-commit hook bypassed: it runs `yarn gitleaks`
but origin/dev is pnpm; gitleaks itself was run directly
(protect --staged --config=gitleaks-override.toml) and reported no leaks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remediation for the AUD-02 key-material audit (SELF-3180). Stacked on the
read-only audit commit; targets the owner-accepted fixes.

F-02 (Critical): loadOrCreateMnemonic no longer overwrites a present-but-
unparseable secret. It fails closed — fires keychainCryptoFailureCallback
('crypto_failed') and returns false — so the create/overwrite block only runs
when no secret existed. Corrupted/legacy entries route to recovery instead of
being destroyed.

F-07 (Major): hasSecretStored returns a tri-state (present | absent | unknown)
instead of collapsing read errors to false. SplashScreen treats only an
explicit 'absent' as a fresh install, so a transient keychain error no longer
misroutes an existing user into onboarding/recovery.

F-08 (Major): cloud backup keeps a single canonical Drive file. Android upload
updates in place via setIdOfFileToUpdate (new settingStore.backupFileId),
adopting/deduping any pre-existing duplicates; download/delete operate by id
with a list+dedupe self-heal fallback. iOS already overwrites a fixed path.

F-06: corrected the useStrongBox JSDoc (effective default is false; StrongBox
off by default due to Samsung-device issues).

Tests: flipped the F-02 characterization test to assert no-overwrite + failure
callback; added F-07 tri-state/routing tests; added F-08 update-in-place,
dedupe, download-by-id, and clear-on-disable tests. Validated in-worktree:
jest (33 passed), tsc --noEmit (clean), check-test-requires (clean).

Pre-commit hook bypassed (runs `yarn gitleaks`; repo is pnpm). gitleaks ran
directly (protect --staged --config=gitleaks-override.toml): no leaks.

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

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
self-webview-app Ignored Ignored Jun 18, 2026 11:20am

Request Review

@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: 207023a62f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
const [canonicalId, ...duplicates] = ids;
if (duplicates.length > 0) {
await Promise.all(duplicates.map(id => gdrive.files.delete(id)));

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 Validate Drive backup before deleting duplicates

On Android accounts that already have duplicate encrypted-private-key files from the old upload path, this deletes every id after whichever files.list happens to return first before the selected file is parsed during restore. Since the list request does not request an order or compare modifiedTime, the first id can be stale or corrupt, so a cloud restore can delete the remaining valid/newer copies and then fail or restore the wrong mnemonic. Choose and validate the canonical backup first, and delete duplicates only after a successful parse/update.

Useful? React with 👍 / 👎.

Comment on lines +98 to +103
if (!fileId) {
fileId = await resolveCanonicalDriveFileId(gdrive);
if (fileId) {
setBackupFileId(fileId);
}
}

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 Retry lookup when cached Drive id is stale

Once backupFileId is persisted, Android restore skips the name-based lookup and goes straight to getText for that cached id. If the Drive file was deleted/recreated or the user signs into a different Google account while local settings still hold the old id, a current same-name backup can exist but restore still fails on the stale id; the previous code listed by name every time. Clear the cached id and retry resolveCanonicalDriveFileId on not-found/access failures before surfacing the error.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR delivers two AUD-02 security remediations: authProvider now fails closed when a stored secret is present but unparseable (returning false without overwriting), and hasSecretStored() returns a tri-state SecretPresence value propagated into SplashScreen routing. Separately, cloud-backup caches the canonical Google Drive backup file id in settingStore with self-healing deduplication. The AUD-02 audit report and plan completion notes are also added.

Changes

Fail-closed keychain secret handling and startup routing

Layer / File(s) Summary
SecretPresence type and fail-closed mnemonic loading
app/src/providers/authProvider.tsx, app/src/integrations/keychain/index.ts
Exports SecretPresence = 'present' | 'absent' | 'unknown', updates hasSecretStored() return type, and adds fail-closed logic in loadOrCreateMnemonic that invokes the crypto failure callback with crypto_failed and returns false when a stored secret cannot be parsed.
SplashScreen startup routing guard
app/src/screens/app/SplashScreen.tsx
Replaces the boolean hasStoredSecret with secretPresence from Promise.all and passes secretPresence !== 'absent' to getStartupNavigationTarget, treating unknown as present to prevent fresh-install misrouting on transient keychain errors.
AUD-02 F-02 and F-07 characterization tests
app/tests/src/providers/authProvider.aud02.test.tsx
New test file asserting no-overwrite on unparseable secret, crypto_failed callback invocation, correct key return for valid JSON, and all three hasSecretStored() return values including the routing guard for unknown.

Drive backup canonical file id caching and self-healing

Layer / File(s) Summary
backupFileId field in settingStore
app/src/stores/settingStore.ts
Adds persisted backupFileId: string | null and setBackupFileId action to useSettingStore, initialized to null.
resolveCanonicalDriveFileId and updated upload/download/disable
app/src/services/cloud-backup/index.ts
Adds resolveCanonicalDriveFileId (lists Drive files by name, returns first as canonical, deletes duplicates). download prefers cached id, resolves and stores if missing, throws if none found. upload attempts in-place update of cached id with fallback to create-and-store. disableBackup clears the cached id after deletion.
Cloud backup tests
app/tests/src/services/cloud-backup.test.ts
Extends tests to reset backupFileId per test, assert plaintext mnemonic payload on both platforms, cover create/update/dedupe upload modes, download-by-id shortcut, and disableBackup clearing backupFileId.

AUD-02 audit report, plan completion, and spec status update

Layer / File(s) Summary
AUD-02 report, plan, and spec
docs/reviews/2026-06-17-key-material-keychain-audit.md, specs/projects/sdk/workstreams/audits/plans/AUD-02-key-material-keychain-lifecycle.md, specs/projects/sdk/workstreams/audits/SPEC.md
Adds full AUD-02 audit report (F-01–F-14, follow-up buckets, verification log, coverage map, owner decisions). Plan status log updated with completion state and five pilot lessons. SPEC.md backlog row advanced from Planned to In Review.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically summarizes the main changes: completing audit AUD-02 with remediation for four identified findings (F-02, F-06, F-07, F-08) related to key material and keychain lifecycle.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/aud-02-key-material

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR delivers the AUD-02 security audit report plus remediations for four accepted findings: F-02 (fail-closed on unparseable keychain secret), F-07 (tri-state hasSecretStored return to prevent misrouting), F-08 (single-canonical-file Drive backup with in-place update and self-healing dedupe), and F-06 (JSDoc correction for useStrongBox).

  • F-02: loadOrCreateMnemonic now returns false and fires keychainCryptoFailureCallback('crypto_failed') instead of falling through to the create/overwrite block when a stored secret is unparseable — eliminating silent, unrecoverable key destruction for legacy-format entries.
  • F-07: hasSecretStored returns 'present' | 'absent' | 'unknown' (was boolean); SplashScreen maps only an explicit 'absent' to a fresh-install route, so a transient keychain read error no longer misroutes an existing user into onboarding/recovery.
  • F-08: Android upload updates the existing Drive file in place via setIdOfFileToUpdate (persisting settingStore.backupFileId); a new resolveCanonicalDriveFileId helper lists, dedupes, and returns the canonical file ID for the no-cached-ID and fallback paths.

Confidence Score: 4/5

The F-02 and F-07 fixes are correct and well-tested; the cloud-backup changes are mostly solid but have one correctness gap in the self-healing path.

The F-02 and F-07 remediations are clean and regression-tested. The F-08 Drive deduplication logic works for the happy path, but resolveCanonicalDriveFileId propagates deletion failures to callers rather than treating cleanup as best-effort — meaning users with accumulated duplicate files could have their first backup or restore aborted by a transient network error during the cleanup phase. The download function also loses the old list-based resilience when the stored backupFileId becomes stale.

app/src/services/cloud-backup/index.ts — specifically the duplicate-deletion error handling in resolveCanonicalDriveFileId and the absence of a stale-ID fallback in download.

Important Files Changed

Filename Overview
app/src/services/cloud-backup/index.ts Introduces resolveCanonicalDriveFileId for F-08 dedupe; upload now updates in-place by stored ID with fallback to create; download skips listing when backupFileId is cached. One correctness issue: duplicate-deletion failures in resolveCanonicalDriveFileId propagate to both upload and download, aborting the primary operation for users with accumulated duplicates who encounter a transient delete error. Additionally, download has no fallback from getText failure to re-list when the stored ID becomes stale.
app/src/providers/authProvider.tsx F-02 fix: inner catch on parse failure now fires keychainCryptoFailureCallback and returns false, preventing fall-through to the create/overwrite block. F-07 fix: hasSecretStored returns SecretPresence tri-state instead of boolean. Both changes are clean and well-tested.
app/src/stores/settingStore.ts Adds backupFileId: string
app/src/screens/app/SplashScreen.tsx Updated to consume the SecretPresence tri-state from hasSecretStored; only maps 'absent' to fresh-install route. Change is minimal and correct.
app/tests/src/providers/authProvider.aud02.test.tsx New regression test file covering F-02 (no overwrite on unparseable secret) and F-07 (unknown vs absent distinction); tests assert fixed behavior after remediation commit. Coverage is thorough for the changed paths.
app/tests/src/services/cloud-backup.test.ts Extended with F-08 upload/download scenarios (create-new, update-in-place, dedupe, download-by-cached-id) and F-01 plaintext-characterization tests. Store state reset between tests prevents ID leakage. The stale-ID fallback in download is not tested because download lacks that code path.
app/src/integrations/keychain/index.ts F-06 JSDoc updated: useStrongBox now documents the effective default (false, store-driven) vs the library-level default (true), resolving the documented drift.
docs/reviews/2026-06-17-key-material-keychain-audit.md New audit report covering F-01 through F-14 with adversarial refutation log, owner dispositions, coverage map, and follow-up issue buckets. Well-structured and thorough.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SplashScreen startup] --> B[hasSecretStored]
    B --> |'present'| C[secretPresence !== 'absent' = true]
    B --> |'absent'| D[secretPresence !== 'absent' = false]
    B --> |'unknown' - transient error| C

    C --> E[getStartupNavigationTarget hasSecretStored: true]
    D --> F[getStartupNavigationTarget hasSecretStored: false]
    E --> G[Route: Home / normal flow]
    F --> H[Route: Onboarding / Recovery]

    subgraph F02 [F-02 Fix]
    I[loadOrCreateMnemonic] --> J{storedMnemonic exists?}
    J --> |yes| K{JSON.parse success?}
    K --> |yes| L[return mnemonic string]
    K --> |no - parse error| M[keychainCryptoFailureCallback crypto_failed return false]
    J --> |no - no entry| N[Create new mnemonic setGenericPassword return data]
    end

    subgraph F08 [F-08 Fix - Android upload]
    O[upload called] --> P{backupFileId or resolveCanonical?}
    P --> |existingId found| Q[setIdOfFileToUpdate update in-place]
    Q --> |success| R[setBackupFileId return]
    Q --> |failure| S[setBackupFileId null fall through to create]
    P --> |no existing file| T[create new file]
    S --> T
    T --> U[setBackupFileId created.id]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[SplashScreen startup] --> B[hasSecretStored]
    B --> |'present'| C[secretPresence !== 'absent' = true]
    B --> |'absent'| D[secretPresence !== 'absent' = false]
    B --> |'unknown' - transient error| C

    C --> E[getStartupNavigationTarget hasSecretStored: true]
    D --> F[getStartupNavigationTarget hasSecretStored: false]
    E --> G[Route: Home / normal flow]
    F --> H[Route: Onboarding / Recovery]

    subgraph F02 [F-02 Fix]
    I[loadOrCreateMnemonic] --> J{storedMnemonic exists?}
    J --> |yes| K{JSON.parse success?}
    K --> |yes| L[return mnemonic string]
    K --> |no - parse error| M[keychainCryptoFailureCallback crypto_failed return false]
    J --> |no - no entry| N[Create new mnemonic setGenericPassword return data]
    end

    subgraph F08 [F-08 Fix - Android upload]
    O[upload called] --> P{backupFileId or resolveCanonical?}
    P --> |existingId found| Q[setIdOfFileToUpdate update in-place]
    Q --> |success| R[setBackupFileId return]
    Q --> |failure| S[setBackupFileId null fall through to create]
    P --> |no existing file| T[create new file]
    S --> T
    T --> U[setBackupFileId created.id]
    end
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(auth,backup): AUD-02 remediation — F..." | Re-trigger Greptile

Comment on lines +52 to +54
if (duplicates.length > 0) {
await Promise.all(duplicates.map(id => gdrive.files.delete(id)));
}

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.

P1 Duplicate-deletion failure aborts upload and download

Promise.all will reject if any single delete call fails (transient network blip, Drive quota, etc.), and resolveCanonicalDriveFileId has no try/catch around it, so the rejection propagates unhandled to both callers (upload line 144 and download line 99). For a user who accumulated duplicates under the old code, the very first upload or download after this PR lands would attempt the self-healing cleanup, and any deletion hiccup aborts the primary operation entirely — even though the canonical ID was already resolved and the actual file is reachable. Wrapping the deletion in a try/catch (or using Promise.allSettled) and continuing to return canonicalId regardless would make the cleanup genuinely best-effort.

Fix in Claude Code

Comment on lines +96 to 113
const { backupFileId, setBackupFileId } = useSettingStore.getState();
let fileId = backupFileId;
if (!fileId) {
fileId = await resolveCanonicalDriveFileId(gdrive);
if (fileId) {
setBackupFileId(fileId);
}
}

if (!isDriveFile(firstFile)) {
if (!fileId) {
throw new Error(
'Couldnt find the encrypted backup, did you back it up previously?',
);
}
const resolvedFileId = fileId;
const mnemonicString = await withRetries(() =>
gdrive.files.getText(firstFile.id),
gdrive.files.getText(resolvedFileId),
);

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.

P2 download has no stale-ID fallback, unlike upload

upload handles a stale backupFileId in its catch block by clearing the stored ID and falling through to a resolveCanonicalDriveFileId-backed create path. download does not: if backupFileId is set but the Drive file was deleted externally (e.g., user cleared app data from Google account settings), getText(resolvedFileId) fails and the function propagates the Drive API error with no opportunity to re-list and find the file under a new ID. Before this PR, download always listed first and was therefore resilient to moved/re-created files. Adding the same catch-and-re-list recovery that upload uses would close the gap.

Fix in Claude Code

@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: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ed2fd10-5266-452c-b8f7-c43955ef7d34

📥 Commits

Reviewing files that changed from the base of the PR and between e0f2aa0 and 207023a.

📒 Files selected for processing (10)
  • app/src/integrations/keychain/index.ts
  • app/src/providers/authProvider.tsx
  • app/src/screens/app/SplashScreen.tsx
  • app/src/services/cloud-backup/index.ts
  • app/src/stores/settingStore.ts
  • app/tests/src/providers/authProvider.aud02.test.tsx
  • app/tests/src/services/cloud-backup.test.ts
  • docs/reviews/2026-06-17-key-material-keychain-audit.md
  • specs/projects/sdk/workstreams/audits/SPEC.md
  • specs/projects/sdk/workstreams/audits/plans/AUD-02-key-material-keychain-lifecycle.md

Comment on lines +43 to +55
const { files } = await gdrive.files.list({
spaces: APP_DATA_FOLDER_ID,
q: `name = '${FILE_NAME}'`,
});
const ids = (files as unknown[]).filter(isDriveFile).map(file => file.id);
if (ids.length === 0) {
return null;
}
const [canonicalId, ...duplicates] = ids;
if (duplicates.length > 0) {
await Promise.all(duplicates.map(id => gdrive.files.delete(id)));
}
return canonicalId;

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect current Drive list usage and dependency version so the fix can target the wrapper's supported list options.
rg -n -C 4 'files\.list\(|orderBy|modifiedTime|createdTime' --glob 'app/**/*.{ts,tsx,js,jsx}'
rg -n -C 2 '`@robinbobin/react-native-google-drive-api-wrapper`' package.json pnpm-lock.yaml

Repository: selfxyz/self

Length of output: 3171


🏁 Script executed:

# Find the wrapper source or check what parameters are documented
fd -i 'google-drive' app/ --type f | head -20

Repository: selfxyz/self

Length of output: 38


🌐 Web query:

@robinbobin/react-native-google-drive-api-wrapper files.list orderBy parameter support

💡 Result:

The @robinbobin/react-native-google-drive-api-wrapper library does not explicitly document or expose an orderBy parameter in its IFilesListQueryParameters interface for the files.list() method [1][2]. While the underlying Google Drive API files.list method natively supports an orderBy parameter to sort results by fields like modifiedTime, name, or createdTime [3][4], this specific wrapper library provides a simplified interface [1][2]. The library primarily facilitates file listing via a q parameter (which supports a ListQueryBuilder helper class) and handles specific query parameter transformations, such as converting arrays to comma-separated strings for properties like spaces [1][2]. Because the library's IFilesListQueryParameters interface is not fully exhaustive of all native Google Drive API v3 parameters, you may find that passing an orderBy property directly into the queryParameters object of the list() method is not supported by the library's type definitions or internal processing [1][2]. If you need to use orderBy, you may need to check the library's source code for the IFilesListQueryParameters type definition or consider if the wrapper allows passing arbitrary parameters that are then passed through to the underlying fetch request [1][2].

Citations:


🏁 Script executed:

# Check the full resolveCanonicalDriveFileId and surrounding context to understand the data flow
cd app && sed -n '35,85p' src/services/cloud-backup/index.ts

Repository: selfxyz/self

Length of output: 1556


The Google Drive wrapper v2.2.6 does not expose orderBy, making the current canonical file selection non-deterministic and risking data loss.

gdrive.files.list() without explicit orderBy has undefined ordering from the Google Drive API. The code selects the first returned file as canonical and deletes the rest, but without deterministic ordering, a stale duplicate could be returned first, leaving you restoring outdated mnemonic data.

The underlying Google Drive API supports orderBy for sorting by modifiedTime, but the wrapper's IFilesListQueryParameters interface does not expose it. You need one of:

  1. Upgrade or fork the wrapper to expose orderBy parameter and sort by modifiedTime descending
  2. Store backup creation/modification metadata outside of the filename and use it to select the newest copy before deletion
  3. Explicitly document and test that duplicates are acceptable (i.e., any copy is safe to restore)

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect current Drive list usage and dependency version so the fix can target the wrapper's supported list options.
rg -n -C 4 'files\.list\(|orderBy|modifiedTime|createdTime' --glob 'app/**/*.{ts,tsx,js,jsx}'
rg -n -C 2 '`@robinbobin/react-native-google-drive-api-wrapper`' package.json pnpm-lock.yaml

Repository: selfxyz/self

Length of output: 3171


🏁 Script executed:

# Read the complete file to examine the flagged issues
cat -n app/src/services/cloud-backup/index.ts | head -200

Repository: selfxyz/self

Length of output: 6794


🏁 Script executed:

# Check wrapper API and documentation if available locally
rg -n 'orderBy|pageSize|spaces|maxResults' --glob 'node_modules/**/*robinbobin*' -A 3 | head -100

Repository: selfxyz/self

Length of output: 205


🏁 Script executed:

# Specifically inspect lines around line 160 for error logging
sed -n '150,170p' app/src/services/cloud-backup/index.ts

Repository: selfxyz/self

Length of output: 756


🏁 Script executed:

# Search for wrapper API documentation or type definitions in node_modules
find node_modules -path '*robinbobin*' -name '*.ts' -o -name '*.d.ts' -o -name 'README.md' 2>/dev/null | head -20

Repository: selfxyz/self

Length of output: 38


🏁 Script executed:

# Check the wrapper package for list API signature
find node_modules/@robinbobin -type f \( -name '*.d.ts' -o -name 'index.js' \) 2>/dev/null | xargs grep -l 'files\.list\|function list' 2>/dev/null | head -5

Repository: selfxyz/self

Length of output: 38


🌐 Web query:

Google Drive API v3 files.list orderBy parameter support query language modifiedTime

💡 Result:

The Google Drive API v3 files.list method explicitly supports modifiedTime as a valid key for the orderBy parameter [1][2]. Key details regarding this parameter include: Sorting Behavior: You can use modifiedTime to sort files by the last time they were modified by anyone [1]. By default, this sorts in ascending order, but you can append desc to the key to sort in descending order (e.g., ?orderBy=modifiedTime desc) [1][2]. Optimization: Official documentation explicitly recommends using modifiedTime for time-related sorting on large item collections because it is optimized to handle these queries efficiently, whereas other keys like createdTime may result in timeouts [1][3]. Separation of Concerns: It is important to distinguish between the orderBy parameter and the q (query) parameter [3][4]: - The orderBy parameter determines the sort order of the results returned [1]. - The q parameter is used to filter results (e.g., q=modifiedTime > '2026-06-01T00:00:00Z') using the Drive search query language [4]. The modifiedTime field is a valid attribute to filter by within these search queries as well [4]. In summary, modifiedTime is fully supported for both sorting (orderBy) and filtering (q) in the Google Drive API v3 [1][3][4].

Citations:


Resolve canonical backup deterministically and add stale-cache fallback to prevent data loss and restore failures.

The resolveCanonicalDriveFileId function (lines 43–55) selects the first file returned by files.list() without specifying orderBy, meaning if Drive returns an older duplicate first, the newer backup is deleted and the stale data restored. Use orderBy: 'modifiedTime desc' to keep the most recent backup and delete older duplicates only after confirming the chosen file is readable.

The download() function (lines 87–120) caches the backup file ID but never falls back to re-listing and resolving duplicates if the cached ID becomes stale (e.g., file deleted server-side). If getText() fails on a stale cached ID, the restore fails even though a valid backup exists on Drive. Add a fallback: if getText() fails on the cached ID, clear it and call resolveCanonicalDriveFileId() again before throwing.

Line 160: Remove the raw error object from the log. Google Drive API errors can expose credentials, tokens, and session identifiers. Log only the error message: console.warn('Cloud backup update failed; creating a new file', (e as Error).message).

Comment on lines +96 to +112
const { backupFileId, setBackupFileId } = useSettingStore.getState();
let fileId = backupFileId;
if (!fileId) {
fileId = await resolveCanonicalDriveFileId(gdrive);
if (fileId) {
setBackupFileId(fileId);
}
}

if (!isDriveFile(firstFile)) {
if (!fileId) {
throw new Error(
'Couldnt find the encrypted backup, did you back it up previously?',
);
}
const resolvedFileId = fileId;
const mnemonicString = await withRetries(() =>
gdrive.files.getText(firstFile.id),
gdrive.files.getText(resolvedFileId),

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fall back when the cached Drive file id is stale.

When backupFileId is present, download() calls getText() directly; if that id was deleted or belongs to a stale Drive state, recovery fails without trying the name-based self-heal path. Catch the cached-id read failure, clear it, resolve the canonical id, and retry once before surfacing the restore error.

🐛 Proposed fallback shape
   const { backupFileId, setBackupFileId } = useSettingStore.getState();
   let fileId = backupFileId;
   if (!fileId) {
     fileId = await resolveCanonicalDriveFileId(gdrive);
     if (fileId) {
       setBackupFileId(fileId);
     }
   }
 
   if (!fileId) {
     throw new Error(
       'Couldnt find the encrypted backup, did you back it up previously?',
     );
   }
-  const resolvedFileId = fileId;
-  const mnemonicString = await withRetries(() =>
-    gdrive.files.getText(resolvedFileId),
-  );
+  let mnemonicString: string;
+  try {
+    mnemonicString = await withRetries(() => gdrive.files.getText(fileId));
+  } catch (error) {
+    if (!backupFileId) {
+      throw error;
+    }
+    setBackupFileId(null);
+    const resolvedFileId = await resolveCanonicalDriveFileId(gdrive);
+    if (!resolvedFileId) {
+      throw error;
+    }
+    setBackupFileId(resolvedFileId);
+    mnemonicString = await withRetries(() =>
+      gdrive.files.getText(resolvedFileId),
+    );
+  }

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the relevant section
cat app/src/services/cloud-backup/index.ts | head -120 | tail -40

Repository: selfxyz/self

Length of output: 1034


🏁 Script executed:

# Get the full context of the download function to understand error handling
rg -A 30 "const.*download.*=.*async" app/src/services/cloud-backup/index.ts

Repository: selfxyz/self

Length of output: 38


🏁 Script executed:

# Search for tests related to cloud-backup to check if stale ID scenario is covered
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "cloud-backup\|backupFileId" 2>/dev/null | head -5

Repository: selfxyz/self

Length of output: 102


🏁 Script executed:

# Check how getText() is defined and what errors it may throw
rg -B 5 -A 10 "getText\(" app/src/services/cloud-backup/index.ts | head -40

Repository: selfxyz/self

Length of output: 507


🏁 Script executed:

# Read the test file to see if stale ID scenario is tested
cat app/tests/src/services/cloud-backup.test.ts

Repository: selfxyz/self

Length of output: 22203


🏁 Script executed:

# Search for the gdrive implementation to understand getText() error behavior
rg -B 5 -A 15 "files.*getText" --type ts --type tsx | head -60

Repository: selfxyz/self

Length of output: 84


🏁 Script executed:

# Check if there's any error handling in withRetries wrapper
rg -B 5 -A 15 "withRetries" app/src/services/cloud-backup/index.ts

Repository: selfxyz/self

Length of output: 2534


🏁 Script executed:

# Check the upload function to see if it has similar error handling for stale IDs
rg -B 10 -A 20 "Cloud backup update failed" app/src/services/cloud-backup/index.ts

Repository: selfxyz/self

Length of output: 918


🏁 Script executed:

# Check withRetries implementation to understand what errors it might catch/rethrow
fd -e ts -e tsx | xargs rg -l "withRetries" | head -3

Repository: selfxyz/self

Length of output: 155


🏁 Script executed:

# Find the withRetries utility
rg -B 3 -A 12 "export.*withRetries" app/src/utils/retry.ts

Repository: selfxyz/self

Length of output: 547


Add stale cache detection and recovery to download() to match error handling in upload().

When backupFileId is cached, download() calls getText() directly without error handling. If that file ID was deleted or belongs to stale Drive state, the function fails hard. The upload() function already implements the correct pattern: catch the error, clear the stale ID from cache, resolve the canonical file ID via name-based lookup, and retry. Apply the same approach to download() to recover from stale cached IDs before surfacing the restore error.

The test suite covers the happy path but does not exercise the getText failure scenario, leaving this recovery path untested.

Proposed fix shape
   const { backupFileId, setBackupFileId } = useSettingStore.getState();
   let fileId = backupFileId;
   if (!fileId) {
     fileId = await resolveCanonicalDriveFileId(gdrive);
     if (fileId) {
       setBackupFileId(fileId);
     }
   }
 
   if (!fileId) {
     throw new Error(
       'Couldnt find the encrypted backup, did you back it up previously?',
     );
   }
-  const resolvedFileId = fileId;
-  const mnemonicString = await withRetries(() =>
-    gdrive.files.getText(resolvedFileId),
-  );
+  let mnemonicString: string;
+  try {
+    mnemonicString = await withRetries(() => gdrive.files.getText(fileId));
+  } catch (error) {
+    if (!backupFileId) {
+      throw error;
+    }
+    setBackupFileId(null);
+    const resolvedFileId = await resolveCanonicalDriveFileId(gdrive);
+    if (!resolvedFileId) {
+      throw error;
+    }
+    setBackupFileId(resolvedFileId);
+    mnemonicString = await withRetries(() =>
+      gdrive.files.getText(resolvedFileId),
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { backupFileId, setBackupFileId } = useSettingStore.getState();
let fileId = backupFileId;
if (!fileId) {
fileId = await resolveCanonicalDriveFileId(gdrive);
if (fileId) {
setBackupFileId(fileId);
}
}
if (!isDriveFile(firstFile)) {
if (!fileId) {
throw new Error(
'Couldnt find the encrypted backup, did you back it up previously?',
);
}
const resolvedFileId = fileId;
const mnemonicString = await withRetries(() =>
gdrive.files.getText(firstFile.id),
gdrive.files.getText(resolvedFileId),
const { backupFileId, setBackupFileId } = useSettingStore.getState();
let fileId = backupFileId;
if (!fileId) {
fileId = await resolveCanonicalDriveFileId(gdrive);
if (fileId) {
setBackupFileId(fileId);
}
}
if (!fileId) {
throw new Error(
'Couldnt find the encrypted backup, did you back it up previously?',
);
}
let mnemonicString: string;
try {
mnemonicString = await withRetries(() => gdrive.files.getText(fileId));
} catch (error) {
if (!backupFileId) {
throw error;
}
setBackupFileId(null);
const resolvedFileId = await resolveCanonicalDriveFileId(gdrive);
if (!resolvedFileId) {
throw error;
}
setBackupFileId(resolvedFileId);
mnemonicString = await withRetries(() =>
gdrive.files.getText(resolvedFileId),
);
}

Comment on lines +158 to 162
} catch (e) {
// Stored id may be stale (file deleted server-side); fall back to create.
console.warn('Cloud backup update failed; creating a new file', e);
setBackupFileId(null);
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t log raw Drive errors.

The raw error object from the Drive update path can contain request/response metadata; avoid emitting headers, tokens, or session identifiers into app logs. Log a fixed sanitized message instead. As per coding guidelines, “NEVER log sensitive data including PII, credentials, tokens, API keys, private keys, or session identifiers.”

🔒 Proposed log sanitization
     } catch (e) {
       // Stored id may be stale (file deleted server-side); fall back to create.
-      console.warn('Cloud backup update failed; creating a new file', e);
+      console.warn('Cloud backup update failed; creating a new file');
       setBackupFileId(null);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e) {
// Stored id may be stale (file deleted server-side); fall back to create.
console.warn('Cloud backup update failed; creating a new file', e);
setBackupFileId(null);
}
} catch (e) {
// Stored id may be stale (file deleted server-side); fall back to create.
console.warn('Cloud backup update failed; creating a new file');
setBackupFileId(null);
}

Source: Coding guidelines


⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t log raw Drive errors.

The raw error object from the Drive update path can contain request/response metadata; avoid emitting headers, tokens, or session identifiers into app logs. Log a sanitized status/message instead. As per coding guidelines, “NEVER log sensitive data including PII, credentials, tokens, API keys, private keys, or session identifiers.”

🔒 Proposed log sanitization
     } catch (e) {
       // Stored id may be stale (file deleted server-side); fall back to create.
-      console.warn('Cloud backup update failed; creating a new file', e);
+      console.warn(
+        'Cloud backup update failed; creating a new file',
+        e instanceof Error ? e.message : 'Unknown error',
+      );
       setBackupFileId(null);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e) {
// Stored id may be stale (file deleted server-side); fall back to create.
console.warn('Cloud backup update failed; creating a new file', e);
setBackupFileId(null);
}
} catch (e) {
// Stored id may be stale (file deleted server-side); fall back to create.
console.warn(
'Cloud backup update failed; creating a new file',
e instanceof Error ? e.message : 'Unknown error',
);
setBackupFileId(null);
}

Source: Coding guidelines

Comment on lines +158 to +172
} catch (e) {
// Stored id may be stale (file deleted server-side); fall back to create.
console.warn('Cloud backup update failed; creating a new file', e);
setBackupFileId(null);
}
await withRetries(() =>
gdrive.files
.newMultipartUploader()
.setData(JSON.stringify(mnemonic))
.setDataMimeType(MIME_TYPES.application.json)
.setRequestBody({ name: FILE_NAME, parents: [APP_DATA_FOLDER_ID] })
.execute(),
);
}

const created = await withRetries(() =>
gdrive.files
.newMultipartUploader()
.setData(data)
.setDataMimeType(MIME_TYPES.application.json)
.setRequestBody({ name: FILE_NAME, parents: [APP_DATA_FOLDER_ID] })
.execute(),
);

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the cloud-backup file structure and content
cd app/src/services/cloud-backup && find . -type f && wc -l index.ts

Repository: selfxyz/self

Length of output: 114


🏁 Script executed:

# Get the full context of the backup file, focusing on the relevant function
cat -n app/src/services/cloud-backup/index.ts | head -200

Repository: selfxyz/self

Length of output: 6794


🏁 Script executed:

# Look for setBackupFileId usage and backupFileId references
rg -n "backupFileId" app/src/services/cloud-backup/

Repository: selfxyz/self

Length of output: 464


🏁 Script executed:

# Check if there's a name-based lookup function for existing backups
rg -n "FILE_NAME\|gdrive\.files\." app/src/services/cloud-backup/ -A 3 -B 3

Repository: selfxyz/self

Length of output: 38


🏁 Script executed:

# Check the withRetries implementation to understand what errors it retries
rg -n "withRetries" app/src/utils/retry/ -A 10

Repository: selfxyz/self

Length of output: 117


🏁 Script executed:

# Check if withRetries is exported from utils/retry
cat -n app/src/utils/retry.ts

Repository: selfxyz/self

Length of output: 1509


Re-resolve by name before creating after an update failure.

If the update fails (after 10 retries) but the file still exists server-side, the catch handler clears the cache and immediately creates a new Drive file, introducing duplicates instead of adopting the existing canonical file. Call resolveCanonicalDriveFileId() again after the update fails; if a file is found, attempt the update again or proceed to creation only if resolution returns null.

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