Repository navigation
feat: Backup passkeys with the Block Store API - #311
Conversation
…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.
There was a problem hiding this comment.
🟡 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
accessTokenandrefreshToken, but this restoration only writes the access token. Every successful full backup therefore permanently leaves all refresh tokens asnull; restore the complete savedUser(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.keyand<id>-public.keyfor 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.
|
This PR/issue depends on: |
There was a problem hiding this comment.
🟡 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
falseresult 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
storeBytescall is converted tofalse; file reads, Block Store retrieval, and protobuf decoding can still throw here. That exception escapesrunBlockingand 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
|
There was a problem hiding this comment.
🟢 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



Depends on Infomaniak/android-core#855
Depends on Infomaniak/multiplatform-authenticator#32