Skip to content

feat: Backup passkeys with the Block Store API - #311

Merged
LouisCAD merged 16 commits into
mainfrom
block-store
Sep 15, 2026
Merged

LouisCAD merged 16 commits into
mainfrom
block-store

Conversation

@LouisCAD

Copy link
Copy Markdown
Contributor

…h 2FA

In case a token expires or is revoked, the 1st API call for
two-factor auth challenges will now automatically trigger
a refresh with the passkey because the http client is
reused and shares the same (re)authentication mechanism.

Copilot AI 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.

🟡 Changes recommended

Token handling, F-Droid compilation, duplicate backup entries, and failure handling contain blocking issues.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds passkey backup and restoration through Google Block Store for the standard flavor.

Changes:

  • Serializes, stores, and restores passkey key pairs.
  • Adds a full-backup agent that temporarily removes user tokens.
  • Connects restoration and shared HTTP clients through the authenticator bridge.
File summaries
File Description
gradle/libs.versions.toml Declares the Block Store dependency.
app/build.gradle.kts Adds Block Store to the standard flavor.
PasskeysBackup.kt Defines the protobuf backup schema.
BlockStoreBackup.kt (standard) Implements passkey backup and restoration.
AuthenticatorFullBackupAgent.kt Integrates Block Store with Android full backup.
AndroidManifest.xml (standard) Registers the backup agent.
ApplicationModule.kt Connects restoration and shared HTTP clients.
BlockStoreBackup.kt (F-Droid) Adds the intended no-op implementation.
Review details

Suppressed comments (2)

app/src/standard/kotlin/com/infomaniak/auth/backup/AuthenticatorFullBackupAgent.kt:58

  • The temporary scrub clears both accessToken and refreshToken, but this restoration only writes the access token. Every successful full backup therefore permanently leaves all refresh tokens as null; restore the complete saved User (or both token fields) here.
                    userDao().updateUserToken(user.id, user.apiToken.accessToken)

app/src/standard/kotlin/com/infomaniak/auth/backup/BlockStoreBackup.kt:141

  • keyRefs() sees both <id>-private.key and <id>-public.key for every passkey and relies on .distinct(), but this regular class uses identity equality, so both references remain. Every key pair is serialized twice, unnecessarily consuming the strict Block Store size limit and causing backups to fail earlier. Give the reference structural equality.
    private class KeyPairReference(
        val userId: Long,
        val keyId: String,
    )
  • Files reviewed: 10/10 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread app/build.gradle.kts
@github-actions

Copy link
Copy Markdown
Contributor

Copilot AI 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.

🟡 Changes recommended

Credential exposure, restoration correctness, and stale submodule revisions currently block safe integration.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

app/src/standard/kotlin/com/infomaniak/auth/backup/AuthenticatorFullBackupAgent.kt:32

  • A false result includes oversized data and Block Store failures, but this branch invokes the full backup before sanitizing the database. Since the backup rules include the database, access and refresh tokens are copied precisely on this failure path; do not bypass token removal when Block Store backup fails.
        val blockStoreBackupSucceeded = runBlocking { BlockStoreBackup.backupPasskeys() }
        if (!blockStoreBackupSucceeded) return super.onFullBackup(data)

app/src/standard/kotlin/com/infomaniak/auth/backup/BlockStoreBackup.kt:55

  • retrieveBytes().await() and protobuf decoding can throw, and this callback runs before the eager account flow initializes with no upstream recovery. A transient Play Services error or corrupt backup can therefore prevent accounts from loading; log and ignore non-cancellation restore failures so normal account recovery can continue.
    suspend fun restorePasskeys() {
        val passKeysBackup = readPasskeysBackup() ?: return
        applyPasskeysBackup(passKeysBackup)
    }

app/src/standard/kotlin/com/infomaniak/auth/backup/BlockStoreBackup.kt:46

  • Only the final storeBytes call is converted to false; file reads, Block Store retrieval, and protobuf decoding can still throw here. That exception escapes runBlocking and aborts the Android full-backup callback entirely, so convert all non-cancellation preparation failures to the method's documented Boolean failure path.
        val backupContent = dumpPasskeys()
        val alreadyBackedUpContent = readPasskeysBackup()
  • Files reviewed: 10/11 changed files
  • Comments generated: 5
  • Review effort level: Balanced

Comment thread app/src/main/kotlin/com/infomaniak/auth/di/ApplicationModule.kt
Comment thread app/src/standard/kotlin/com/infomaniak/auth/backup/BlockStoreBackup.kt Outdated
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI 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.

🟢 Approval recommended

The implementation is coherent across both flavors, and the previously identified duplicate-entry issue has been addressed.

Review details
  • Files reviewed: 13/14 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@LouisCAD
LouisCAD enabled auto-merge September 15, 2026 13:07
@LouisCAD
LouisCAD merged commit dee9ad7 into main Sep 15, 2026
13 checks passed
@LouisCAD
LouisCAD deleted the block-store branch September 15, 2026 13:09
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.

3 participants