From b8e767af85a20f76a294eb714ca66198f8ccd7ba Mon Sep 17 00:00:00 2001 From: pd Date: Sat, 8 Nov 2025 13:30:15 -0800 Subject: [PATCH 01/14] wip --- fonts/excalifont.woff2 | Bin 0 -> 52296 bytes style.css | 7 +++++++ world-app/backup/advanced.mdx | 8 +------- world-app/backup/components.mdx | 12 ++++++++++++ world-app/backup/index.mdx | 2 +- world-app/backup/structure-and-sync.mdx | 3 +-- world-app/cryptography.mdx | 2 +- 7 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 fonts/excalifont.woff2 create mode 100644 style.css create mode 100644 world-app/backup/components.mdx diff --git a/fonts/excalifont.woff2 b/fonts/excalifont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..24ce44aa15c0f18163c5bf9583aac124f8a27aec GIT binary patch literal 52296 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 is a plain-text file that is stored in the backup service. It generally contains the following information (see [source code](https://github.com/worldcoin/backup-service/blob/main/src/types/backup_metadata.rs) for most up-to-date information): - The ID of the backup -- The list of . +- The list of s. - The list of s. - The list of s. \ No newline at end of file diff --git a/world-app/backup/structure-and-sync.mdx b/world-app/backup/structure-and-sync.mdx index ed2d014..2a9eb21 100644 --- a/world-app/backup/structure-and-sync.mdx +++ b/world-app/backup/structure-and-sync.mdx @@ -83,5 +83,4 @@ On app startup, the client fetches the remote state checksum. If it differs from ### Additional Safety Measures -- **Backend short-lived locks** - Prevents narrow race windows during updates, only one update at a time. Authoritative enforcement remains via manifest hash validation. -- **Hard links (Android Only)** - Hard links are created under `/backup/files/{checksum}` to guard against accidental deletion of source files. Links are removed on file removal. \ No newline at end of file +- **Backend short-lived locks** - Prevents narrow race windows during updates, only one update at a time. Authoritative enforcement remains via manifest hash validation. \ No newline at end of file diff --git a/world-app/cryptography.mdx b/world-app/cryptography.mdx index e1667bb..1cc00b4 100644 --- a/world-app/cryptography.mdx +++ b/world-app/cryptography.mdx @@ -5,4 +5,4 @@ description: "Cryptography reference for World App." ## CSPRNG and Randomness Source -Various operations in World App require a [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) (Cryptographically Secure Pseudo-Random Number Generator). For all such operations, we use the standard OS-provided CSPRNG. Most cryptographically sensitive operations in World App are handled by [Bedrock](/world-app/bedrock) foundational library. Bedrock accomplishes this by using the `OsRng` from the [rand](https://docs.rs/rand/latest/rand/rngs/struct.OsRng.html) crate. Under the hood, `OsRng` uses `getrandom` which uses the OS-provided CSPRNG (e.g. `getrandom(2)` syscall on Android (Linux), `SecRandomCopyBytes` on iOS). \ No newline at end of file +Various operations in World App require a [CSPRNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) (Cryptographically Secure Pseudo-Random Number Generator). For all such operations, we use the standard OS-provided CSPRNG. Most cryptographically sensitive operations in World App are handled by [Bedrock](/world-app/bedrock). Bedrock accomplishes this by using the `OsRng` from the [rand](https://docs.rs/rand/latest/rand/rngs/struct.OsRng.html) crate. Under the hood, `OsRng` uses `getrandom` which uses the OS-provided CSPRNG (e.g. `getrandom(2)` syscall on Android (Linux), `SecRandomCopyBytes` on iOS). \ No newline at end of file From dee8ccd722d55d564489e2165919bef48a418798 Mon Sep 17 00:00:00 2001 From: pd Date: Mon, 1 Dec 2025 10:27:47 -0600 Subject: [PATCH 02/14] wip2 --- world-app/backup/advanced.mdx | 27 ++++++++++++++++++++++++- world-app/backup/flows.mdx | 7 +++++-- world-app/backup/structure-and-sync.mdx | 2 +- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/world-app/backup/advanced.mdx b/world-app/backup/advanced.mdx index de24e28..3f19c60 100644 --- a/world-app/backup/advanced.mdx +++ b/world-app/backup/advanced.mdx @@ -4,6 +4,31 @@ sidebarTitle: "Advanced Topics" description: "Internal advanced technical reference for the backup system." --- +import { SyncFactor, MainFactor } from "/snippets/backup/terms.mdx"; + ## Encryption -All encryption operations use libsodium's asymmetric [sealed_box](https://doc.libsodium.org/public-key_cryptography/sealed_boxes) primitive through the [crypto_box](https://crates.io/crates/crypto_box) crate. A randomness source is required for sealed boxes to generate ephemeral key pairs for sealing. See [Cryptography](/world-app/cryptography) for more details on the randomness source. \ No newline at end of file +All encryption operations use libsodium's asymmetric [sealed_box](https://doc.libsodium.org/public-key_cryptography/sealed_boxes) primitive through the [crypto_box](https://crates.io/crates/crypto_box) crate. A randomness source is required for sealed boxes to generate ephemeral key pairs for sealing. See [Cryptography](/world-app/cryptography) for more details on the randomness source. + + +## Advanced Edge Cases + +### 1. Multiple Device Sync + +A user may be logged in multiple devices at the same time. When one of the devices syncs a new file, this file needs to be retrieved in the other devices. This cannot be done in the background because for security reasons, the is not authorized to retrieve the user's backup, nor has any secret material for decryption. The client detects it needs to update its backup using the manifest hashes (see [Backup Sync & Internal Structure](/world-app/backup/structure-and-sync)). When an update is required, the client prompts the user to authenticate with a and download the updated remote backup. + +Importantly, whenever a client tries to sync, the backup service will help the client ensure it can only perform updates to the backup if it is operating on the latest state. This helps prevent accidental data loss. + +### 2. Unauthorized Device + +At a point in time, it is possible a device becomes unauthorized to manage a user's backup. This means that the device's is no longer authorized with either or both Components. With Turnkey, this can be seen when receiving a `PUBLIC_KEY_NOT_FOUND` error and with the backup service when receiving an `unauthorized_factor` error (if the right `backup_id` is provided in the request). This can occur in some edge case scenarios: +- The user is authenticated in multiple devices and deletes and recreates the entire backup. Other authenticated devices become unauthorized. +- The user loses their . +- (Future). The user explicitly unauthorizes a different device. + + +The remediation path for this case is to use a to register a new . In particular, there is explicit handling in these scenarios: +1. When creating a backup if the `backup_account_id_already_exists` error is received, the client prompts the user to authenticate with a and register a new (see **[BF-1](/world-app/backup/flows#bf-1-backup-creation)**). +2. When fetching metadata if the `unauthorized_factor` error is received, the client prompts the user to authenticate with a and register a new . If on the other hand, a `backup_does_not_exist` error is received, the client knows the backup has been deleted by the user and will update its local state accordingly. +3. When deleting a factor if the `PUBLIC_KEY_NOT_FOUND` error is received from Turnkey, the client prompts the user to authenticate with a to register a new and retry their operation (see **[BF-7](/world-app/backup/flows#bf-7-factor-deletion)**). + It's not necessary to handle error cases on factor addition/deletion with the backup service because a `/retrieve-metadata` call always preceeds this. diff --git a/world-app/backup/flows.mdx b/world-app/backup/flows.mdx index e78d41e..c172c6e 100644 --- a/world-app/backup/flows.mdx +++ b/world-app/backup/flows.mdx @@ -6,7 +6,10 @@ description: "Detailed documentation on the key flows related to the World App b import { MainFactor } from "/snippets/backup/terms.mdx"; -## BF-7. Factor Deletion +## 1. Backup Creation + + +## 7. Factor Deletion - This flow is triggered when the user deletes a from the UI. - Sync Factors are not manually removed by the user. This is usually done through other flows (e.g. when logging out). - Currently, Passkeys are enrolled into Turnkey to provide a fallback mechanism in case a Sync Factor is no longer available. When the requested operation cannot be completed with the Sync Factor (see `PUBLIC_KEY_NOT_FOUND` error), the client will use the Passkey to execute the operation instead. @@ -51,7 +54,7 @@ flowchart TD D -. fail .-> EndError ``` -## BF-8. Backup Deletion +## 8. Backup Deletion - Backup Deletion is triggered from either: - The user removing the last (see BF-7). - The user requesting full account deletion of World App. diff --git a/world-app/backup/structure-and-sync.mdx b/world-app/backup/structure-and-sync.mdx index 2a9eb21..bfd6c52 100644 --- a/world-app/backup/structure-and-sync.mdx +++ b/world-app/backup/structure-and-sync.mdx @@ -17,7 +17,7 @@ The contains the encoded for World App plus a set ## Backup State Management -The backup system uses a **manifest-based approach** to prevent accidental data loss during syncs. Each backup contains a manifest that describes its contents and enforces proper versioning. +The backup system uses a **manifest-based approach** to prevent accidental data loss during syncs (e.g. Device A adds a file Y, when Device B tries to add file Z it doesn't know about file Y and the updated backup would not contain file Y, accidentally removing it). Each backup contains a manifest that describes its contents and enforces proper versioning. ### The Manifest From 54c0039b7dc2b5e24c79562db75f1bc908e4ab43 Mon Sep 17 00:00:00 2001 From: pd Date: Mon, 1 Dec 2025 10:55:22 -0600 Subject: [PATCH 03/14] user setup --- world-app/backup/components.mdx | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/world-app/backup/components.mdx b/world-app/backup/components.mdx index 870279d..c9a1197 100644 --- a/world-app/backup/components.mdx +++ b/world-app/backup/components.mdx @@ -4,9 +4,52 @@ sidebarTitle: "Components & Parties" description: "The parties and their respective components and responsibilities within the backup system." --- +import { SyncFactor, MainFactor } from "/snippets/backup/terms.mdx"; + # Backup Service Details coming soon. # Turnkey +FIXME: Add intro + + +All short-lived keys used with Turnkey are set for a duration of **5 minutes** unless otherwise specified. + + +## Turnkey User Setup + +The initial release of this feature relied on a slightly different user setup. Some early World App Users will have a slightly different setup (with the Root Quorum being `auth_user_main` solely) before a migration is introduced. + +1. **Ephemeral**. A `root_user_genesis` user is created by World App's backend. + - This user is a root user. + - This user is created with a short-lived keypair provided by the client. + - The user is also registered with a fallback `0` key (`Curve25519`) as Turnkey requires all users to have an always valid authenticator or keypair. + - This user is immediately deleted by the client upon setup of `auth_user_main` +2. An `auth_user_main` is created by the client. This user represents how the World App User primarily authenticates, and it's authenticated with the s. + - This user has a permission policy with explicit `ALLOW` for all activities (`*`). + - The user's passkey and OIDC factors are added to this user for authentication. + + ```json + { + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '')", + "condition": "*" + } + ``` +3. A `sync_factor_user` is created by the client. This user represents user interactions operated with their s. + - A long lived keypair is added per- to this user. A user will have a unique per device. + - This user has permission to perform certain deletion operations. This enables the user to perform management operations without requiring additional authentication, as well as providing a recovery path for when access to s is lost. For example, it's used to remove an OIDC factor without additional user interaction, or to delete the user's Turnkey account altogether. + + ```json + { + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '')", + "condition": "activity.type == 'ACTIVITY_TYPE_DELETE_AUTHENTICATORS' || activity.type == 'ACTIVITY_TYPE_DELETE_PRIVATE_KEYS' || activity.type == 'ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS' || activity.type == 'ACTIVITY_TYPE_DELETE_API_KEYS' || activity.type == 'ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION'" + } + ``` +4. In addition to the user setup, Turnkey has the concept of a [Root Quorum](https://docs.turnkey.com/concepts/users/root-quorum). When the Root Quorum is met, any action does not go through the Policy Engine and is directly approved. For World App, the Root Quorum is generally not used. To prevent misuse, the Root Quorum is set to 2-of-2 with both the `auth_user_main` and `sync_factor_user` being part of the quorum. This ensures that: + - `sync_factor_user` can update `auth_user_main` as granted by its policy (i.e. delete OAuth providers or authenticators). + - `sync_factor_user` cannot take any action not explicitly granted by the policy, as `sync_factor_user` cannot reach Root Quorum on its own. Or in other words, Root Quorum still requires approval by `auth_user_main`. + - A World App User can always update their `sync_factor_user` using `auth_user_main` in case of loss of all devices. From 8e0d4ecc900c9a92b513d0a647c06b4b546d380f Mon Sep 17 00:00:00 2001 From: pd Date: Wed, 21 Jan 2026 15:13:16 +0100 Subject: [PATCH 04/14] small improvements --- world-app/backup/components.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/world-app/backup/components.mdx b/world-app/backup/components.mdx index c9a1197..2fcfa68 100644 --- a/world-app/backup/components.mdx +++ b/world-app/backup/components.mdx @@ -39,8 +39,8 @@ FIXME: Add intro } ``` 3. A `sync_factor_user` is created by the client. This user represents user interactions operated with their s. - - A long lived keypair is added per- to this user. A user will have a unique per device. - - This user has permission to perform certain deletion operations. This enables the user to perform management operations without requiring additional authentication, as well as providing a recovery path for when access to s is lost. For example, it's used to remove an OIDC factor without additional user interaction, or to delete the user's Turnkey account altogether. + - A long lived keypair is added per- to this user. A user will have a unique per device. Each registers a different keypair with the same `sync_factor_user` in Turnkey. + - This user has permission to perform certain deletion operations. This enables the user to perform management operations without requiring additional authentication. For example, it's used to remove an OIDC factor without additional user interaction, or to delete the user's Turnkey account altogether. ```json { From 31f42786ed027bcbfe8b51d139cb60049f82101e Mon Sep 17 00:00:00 2001 From: pd Date: Wed, 21 Jan 2026 15:15:53 +0100 Subject: [PATCH 05/14] typos --- world-app/backup/advanced.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/world-app/backup/advanced.mdx b/world-app/backup/advanced.mdx index 3f19c60..3213683 100644 --- a/world-app/backup/advanced.mdx +++ b/world-app/backup/advanced.mdx @@ -24,11 +24,11 @@ Importantly, whenever a client tries to sync, the backup service will help the c At a point in time, it is possible a device becomes unauthorized to manage a user's backup. This means that the device's is no longer authorized with either or both Components. With Turnkey, this can be seen when receiving a `PUBLIC_KEY_NOT_FOUND` error and with the backup service when receiving an `unauthorized_factor` error (if the right `backup_id` is provided in the request). This can occur in some edge case scenarios: - The user is authenticated in multiple devices and deletes and recreates the entire backup. Other authenticated devices become unauthorized. - The user loses their . -- (Future). The user explicitly unauthorizes a different device. +- (Future). The user explicitly removes authorization for a specific device. The remediation path for this case is to use a to register a new . In particular, there is explicit handling in these scenarios: 1. When creating a backup if the `backup_account_id_already_exists` error is received, the client prompts the user to authenticate with a and register a new (see **[BF-1](/world-app/backup/flows#bf-1-backup-creation)**). 2. When fetching metadata if the `unauthorized_factor` error is received, the client prompts the user to authenticate with a and register a new . If on the other hand, a `backup_does_not_exist` error is received, the client knows the backup has been deleted by the user and will update its local state accordingly. 3. When deleting a factor if the `PUBLIC_KEY_NOT_FOUND` error is received from Turnkey, the client prompts the user to authenticate with a to register a new and retry their operation (see **[BF-7](/world-app/backup/flows#bf-7-factor-deletion)**). - It's not necessary to handle error cases on factor addition/deletion with the backup service because a `/retrieve-metadata` call always preceeds this. + It's not necessary to handle error cases on factor addition/deletion with the backup service because a `/retrieve-metadata` call always precedes this. From 539a32397e409ec92a6b989d152b7eabe36d708e Mon Sep 17 00:00:00 2001 From: pd Date: Mon, 26 Jan 2026 08:23:07 +0100 Subject: [PATCH 06/14] update from feedback --- world-app/backup/components.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/world-app/backup/components.mdx b/world-app/backup/components.mdx index 2fcfa68..f3a6513 100644 --- a/world-app/backup/components.mdx +++ b/world-app/backup/components.mdx @@ -28,14 +28,14 @@ FIXME: Add intro - The user is also registered with a fallback `0` key (`Curve25519`) as Turnkey requires all users to have an always valid authenticator or keypair. - This user is immediately deleted by the client upon setup of `auth_user_main` 2. An `auth_user_main` is created by the client. This user represents how the World App User primarily authenticates, and it's authenticated with the s. - - This user has a permission policy with explicit `ALLOW` for all activities (`*`). + - This user has a permission policy with explicit `ALLOW` for all activities (`condition = true`). - The user's passkey and OIDC factors are added to this user for authentication. ```json { "effect": "EFFECT_ALLOW", "consensus": "approvers.any(user, user.id == '')", - "condition": "*" + "condition": "true" } ``` 3. A `sync_factor_user` is created by the client. This user represents user interactions operated with their s. @@ -46,9 +46,12 @@ FIXME: Add intro { "effect": "EFFECT_ALLOW", "consensus": "approvers.any(user, user.id == '')", - "condition": "activity.type == 'ACTIVITY_TYPE_DELETE_AUTHENTICATORS' || activity.type == 'ACTIVITY_TYPE_DELETE_PRIVATE_KEYS' || activity.type == 'ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS' || activity.type == 'ACTIVITY_TYPE_DELETE_API_KEYS' || activity.type == 'ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION'" + "condition": "activity.action == 'DELETE' && (activity.resource == 'CREDENTIAL' || activity.resource == 'PRIVATE_KEY' || activity.resource == 'ORGANIZATION')" } ``` + + > The policy filters on `activity.action` and `activity.resource` instead of `activity.type` as the latter is version specific. At the current time, the policy translates into these activity types: `ACTIVITY_TYPE_DELETE_API_KEYS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_AUTHENTICATORS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION` (`ORGANIZATION`), `ACTIVITY_TYPE_DELETE_PRIVATE_KEYS` (`PRIVATE_KEY`), `ACTIVITY_TYPE_DELETE_PRIVATE_KEY_TAGS` (`PRIVATE_KEY`), `ACTIVITY_TYPE_DISABLE_PRIVATE_KEY` (`PRIVATE_KEY`) + 4. In addition to the user setup, Turnkey has the concept of a [Root Quorum](https://docs.turnkey.com/concepts/users/root-quorum). When the Root Quorum is met, any action does not go through the Policy Engine and is directly approved. For World App, the Root Quorum is generally not used. To prevent misuse, the Root Quorum is set to 2-of-2 with both the `auth_user_main` and `sync_factor_user` being part of the quorum. This ensures that: - `sync_factor_user` can update `auth_user_main` as granted by its policy (i.e. delete OAuth providers or authenticators). - `sync_factor_user` cannot take any action not explicitly granted by the policy, as `sync_factor_user` cannot reach Root Quorum on its own. Or in other words, Root Quorum still requires approval by `auth_user_main`. From 7bce195361c2ffd8556ff90b1b806ea1a511fdc8 Mon Sep 17 00:00:00 2001 From: pd Date: Wed, 6 May 2026 16:28:40 -0700 Subject: [PATCH 07/14] wip --- cspell.json | 18 ++++++++++- world-app/backup/index.mdx | 61 ++++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/cspell.json b/cspell.json index 481f799..27f1888 100644 --- a/cspell.json +++ b/cspell.json @@ -12,7 +12,23 @@ "cspell", "spellcheck", "pimlico", - "bdrck" + "bdrck", + "androidx", + "Synchronizable", + "secp", + "Nethermind", + "Turnkey", + "Keystore", + "Keychain", + "WebAuthn", + "passkey", + "passkeys", + "OIDC", + "Curve", + "libsodium", + "Salsa", + "Poly", + "Bedrock" ], "ignorePaths": [ "node_modules", diff --git a/world-app/backup/index.mdx b/world-app/backup/index.mdx index f372f44..b56e6f4 100644 --- a/world-app/backup/index.mdx +++ b/world-app/backup/index.mdx @@ -1,48 +1,59 @@ --- title: "Introduction to Backup" sidebarTitle: "Introduction" -description: "The backup system enables logging in to World App." +description: "How World App backs up and recovers a user's self-custodial data." --- -import { UnsealedBackup, BackupEncryptionKeypair, EncryptedBackupEncryptionKeypair, BackupMetadata, MainFactor, SealedBackup, SyncFactor } from "/snippets/backup/terms.mdx"; - -[turnkey]: https://turnkey.com/ +import { UnsealedBackup, BackupEncryptionKeypair, EncryptedBackupEncryptionKeypair, BackupMetadata, MainFactor, SealedBackup, SyncFactor, FactorSecret } from "/snippets/backup/terms.mdx"; +import { RootKey, PersonalCustodyPackage } from "/snippets/terms.mdx"; # Introduction -Introduction and high-level context coming soon. +World App is self-custodial: the user's [Root Key](/world-app/cryptography), wallet, World ID, etc. all live on their device. Backup & Recovery is what lets the user re-derive that state on a new device when the old one is lost/replaced without trusting any single party with the plaintext data. This system is particularly designed with distributed trust where no single party can compromise a user's backup, in essence the _lock_ and _key_ are handled by independent parties and hardened independently. + +The backup is end-to-end encrypted on the client, stored as ciphertext by the **Backup Service**, and decryptable only with one of the user's enrolled s (Passkey, OIDC account, iCloud Keychain keypair). +# System Overview -# Technical Reference +There are four main components involved in the system. -## High-Level Overview -Coming soon. +- **Client (iOS, Android)**: User-facing flows. Implements platform-specific factor logic and calls into Bedrock for shared crypto and manifest logic. +- **[Bedrock](/world-app/bedrock)**: Cross-platform library shared by both clients. Owns the backup file format, manifest computation, sealing/unsealing, and the Turnkey stamping primitives. See [`bedrock`](https://github.com/worldcoin/bedrock/tree/main/bedrock/src/backup). +- **Backup Service**: A secure backend storage layer which holds the and , validates state transitions on sync, and exposes endpoints for factor management. It never sees the decrypted plain text, nor has it any way of accessing the ciphertext. The keys with which the ciphertext is encrypted are never seen by this service. See [`backup-service`](https://github.com/worldcoin/backup-service). Currently this service is operated independently by [Nethermind](https://www.nethermind.io/). +- **[Turnkey][turnkey]** — Holds s for OIDC factors inside its Trusted Execution Environment (TEE on AWS Nitro) and provably authenticates the user (via OIDC). Internally, TFH is the parent organization; each user is a sub-organization. Neither TFH or Turnkey can access the user's secrets, this is enforced by the TEE. -## Backup Composition +The Client orchestrates everything; the Backup Service is intentionally narrow. + +# Layered Encryption Model + +The backup uses two layers of asymmetric encryption rather than a single password-derived key. This decouples factor lifecycle from backup content. ![Backup Composition](./images/backup-composition.svg) +- The is the plaintext: a collection of files (the , PCPs, the credential vault) plus a manifest. Its structure is owned by Bedrock's `BackupFormat`. See [Backup Structure & Sync](/world-app/backup/structure-and-sync). +- The client encrypts the with a randomly generated at backup creation. The result is the . +- The client then encrypts the once per with that factor's , producing $n$ s. The keypair's secret key is then **discarded** — only the public key is kept locally so the client can re-encrypt new versions of the backup without authenticating a Main Factor. +- The s live in the alongside the list of factors. -- The is a collection of raw files stored locally on the device only. The structure of the is described in the [Backup Composition & Sync](/world-app/backup/structure-and-sync) section. -- The user encrypts the with a that's randomly generated by World App. -- The user then encrypts the with each _Factor Secret_, creating $n$ . After the is encrypted with each _Factor Secret_, the user discards the secret key of the , and stores the public key (which is used to encrypt new versions of the to **Sync** the backup). -- All the s are stored in the . +The reason for the keypair indirection is that adding a new only requires re-encrypting the keypair (small) under the new , not the entire backup. Without it, $n$ copies of the would have to be maintained. -The reason for the layer is that when adding a new , the factor can be added without requiring to create a new . If _Factor Secrets_ were used directly, $n$ versions of the would need to be stored. +For the encryption primitives themselves, see [Advanced Topics — Encryption](/world-app/backup/advanced#encryption). +# Threat Model -### Factor Secrets -Main Factors are able to derive or store _Factor Secrets_. A _Factor Secret_ is a key that is used to encrypt the . When decrypting the , the is decrypted first with the _Factor Secret_ and then the Sealed Backup to retrieve the raw files. Each derives its _Factor Secret_ this way: +The system is designed to defend against: -- For Passkeys, the _Factor Secret_ is a [PRF extension key](https://www.w3.org/TR/webauthn-3/#prf-extension) which allows deriving a deterministic symmetric key. -- For OIDC, the _Factor Secret_ is a random 32 byte sequence that is generated in the user's World App and then stored inside [Turnkey][turnkey]'s Secure Enclaves as a _Private Key_. On recovery, the Secure Enclave provably authenticates the user before providing the secret. -- For iCloud Keychain, a P-256 keypair is generated by the `Security` framework on iOS and stored in the keychain. The private key is used as a secret to encrypt the . +- **Backup Service compromise.** A breach of the Backup Service exposes ciphertext and metadata only. s never reach the service; the private key is destroyed after sealing. +- **TFH backend compromise.** TFH's app-backend stamps Turnkey sub-organization creation but does not custody factor secrets or backup contents. An attacker with TFH backend access cannot decrypt a backup or even retrieve the ciphertext. +- **Loss of a single device.** Recovery only requires one enrolled . +- **Loss of a single factor.** Multiple Main Factors can be enrolled. Sync Factors are scoped narrowly (see [Authentication Factors](/world-app/backup/factors)). +- **Single party coercion.** If a single party is coerced or malicious, they cannot compromise a user's backup. +Out of scope: -### Backup Metadata +- Compromise of the user's passkey provider or OIDC identity provider. +- Malware on the user's device with access to plaintext after recovery. +- Coercion-resistant recovery (no duress codes). +- Rotation of long-lived keys (factor secrets, the backup keypair) is not currently supported. -The is a plain-text file that is stored in the backup service. It generally contains the following information (see [source code](https://github.com/worldcoin/backup-service/blob/main/src/types/backup_metadata.rs) for most up-to-date information): -- The ID of the backup -- The list of s. -- The list of s. -- The list of s. \ No newline at end of file +[turnkey]: https://turnkey.com/ From ea83ce170dc35e45fff3a87f705bac10fa767910 Mon Sep 17 00:00:00 2001 From: pd Date: Wed, 6 May 2026 16:28:47 -0700 Subject: [PATCH 08/14] Update terms.mdx --- snippets/backup/terms.mdx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/snippets/backup/terms.mdx b/snippets/backup/terms.mdx index 7cdb804..c75d8c3 100644 --- a/snippets/backup/terms.mdx +++ b/snippets/backup/terms.mdx @@ -40,4 +40,16 @@ export const EncryptedBackupEncryptionKeypair = () => { return ( Encrypted Backup Encryption Keypair ) +} + +export const FactorSecret = () => { + return ( + Factor Secret + ) +} + +export const BackupAccountId = () => { + return ( + Backup Account ID + ) } \ No newline at end of file From 784f199cf3412fcb4369c74c0422a5394b897ff1 Mon Sep 17 00:00:00 2001 From: pd Date: Wed, 6 May 2026 16:36:38 -0700 Subject: [PATCH 09/14] factors --- docs.json | 1 + snippets/backup/terms.mdx | 4 +- world-app/backup/factors.mdx | 87 ++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 world-app/backup/factors.mdx diff --git a/docs.json b/docs.json index ddfe1ff..36c009a 100644 --- a/docs.json +++ b/docs.json @@ -50,6 +50,7 @@ "group": "Backup & Recovery", "pages": [ "world-app/backup/index", + "world-app/backup/factors", "world-app/backup/structure-and-sync", "world-app/backup/flows", "world-app/backup/advanced" diff --git a/snippets/backup/terms.mdx b/snippets/backup/terms.mdx index c75d8c3..876a45d 100644 --- a/snippets/backup/terms.mdx +++ b/snippets/backup/terms.mdx @@ -50,6 +50,6 @@ export const FactorSecret = () => { export const BackupAccountId = () => { return ( - Backup Account ID + Backup Account ID ) -} \ No newline at end of file +} diff --git a/world-app/backup/factors.mdx b/world-app/backup/factors.mdx new file mode 100644 index 0000000..11bd3c2 --- /dev/null +++ b/world-app/backup/factors.mdx @@ -0,0 +1,87 @@ +--- +title: "Authentication Factors" +sidebarTitle: "Factors" +description: "Main Factors, Sync Factors, and how each Factor Secret is derived." +--- + +import { MainFactor, SyncFactor, FactorSecret, BackupEncryptionKeypair, BackupAccountId, BackupMetadata } from "/snippets/backup/terms.mdx"; +import { RootKey } from "/snippets/terms.mdx"; + +# Main Factors vs Sync Factors + +A backup distinguishes two factor categories. They differ in *what they can do*, not just *how strongly they authenticate*. + +| | | | +| --- | --- | --- | +| Purpose | Authenticate the user; unlock the backup; manage other factors | Update existing backup contents; delete the backup; delete factors | +| Requires user interaction | Yes (passkey tap, OIDC sign-in, biometric on Keychain) | No (silent — background sync) | +| Can decrypt the backup? | Yes (holds a ) | **No** | +| Can add new factors? | Yes | **No** | +| Per-device | No (Passkey/OIDC roam) | Yes — one Sync Factor per device | +| Allowed types | Passkey, OIDC + Turnkey, iCloud Keychain (iOS) | EC keypair only | + +The split exists for two reasons. First, sync needs to happen silently in the background; requiring a passkey tap on every change would be untenable. Second, the Sync Factor is long-lived on the device. If it was granted the power to *enroll* new recovery methods would mean a stolen device could silently add factors and gain persistent access. The Sync Factor can *delete* but not *add*; this is enforced by both Backup Service authorization and Turnkey policy (see [Components & Parties](/world-app/backup/components#turnkey)). + +# Factor Secrets + +A is a 32-byte secret used to encrypt the . Each either *derives* its secret deterministically (in a cryptographically-secure form) or *holds* it in a system made for cryptographic storage; the client never persists secrets in plaintext. + +## Passkey (PRF) + +The factor secret is the output of the [WebAuthn PRF extension](https://www.w3.org/TR/webauthn-3/#prf-extension) evaluated over a fixed salt (`"world-app-backup"`). PRF is deterministic per credential, so the same passke always produces the same secret. + +Platform notes: + +- **iOS** — Requires iOS 18+. Implemented with `AuthenticationServices` (`ASAuthorizationPublicKeyCredentialPRFRegistrationInput` / assertion input). The credential may be stored in iCloud Keychain by the system (out of scope of mobile apps). +- **Android** — Implemented with the [Credential Manager API](https://developer.android.com/identity/sign-in/credential-manager) (`androidx.credentials`). PRF support depends on the active credential provider — typically Google Password Manager (requires Google Play Services 23.33+); Samsung Pass and others are not yet supported. Minimum API level 29; device screen lock must be set up or registration fails. + +## OIDC + Turnkey + +The factor secret is a random 32-byte sequence generated by the client at enrollment, then *imported* into Turnkey's TEE-backed enclave under a sub-organization owned by the user. From that point on, the secret only leaves the enclave when the user authenticates the OIDC factor. Turnkey's enclave performs OIDC token verification itself before releasing the secret — the user proves possession of their Google or Apple identity, not just to the Backup Service but to the enclave too. + +Supported providers: + +- **Google** (iOS, Android) +- **Apple** (iOS, Android) + +The client uses Bedrock's `Turnkey` helper (P-256 ECDSA stamping over Turnkey API requests) to interact with the enclave. See [Components & Parties — Turnkey](/world-app/backup/components#turnkey) for the sub-organization structure (root user, auth user, sync factor user, root quorum) and policy details. + +## iCloud Keychain (iOS only) + +A P-256 keypair is generated by the iOS [`Security`](https://developer.apple.com/documentation/security) framework via `SecKeyCreateRandomKey` (key type `kSecAttrKeyTypeECSECPrimeRandom`). The private key scalar (32 bytes) is the factor secret. + +iCloud Keychain is intentionally **not** synced (`kSecAttrSynchronizable: false`) — it is an iOS-only, device-local factor. Its purpose is to provide an additional recovery path on iOS without requiring the user to set up a passkey or sign in with OIDC. Android has no equivalent. + +# Sync Factor + +The Sync Factor is a P-256 ECDSA keypair generated client-side at backup creation (and again on every recovery onto a new device). The public key is registered with the Backup Service; the private key is stored locally in platform-secure storage: + +- **iOS** — Keychain (non-synced). +- **Android** — Android Keystore (hardware-backed when available); falls back to `EncryptedSharedPreferences` if Keystore is unavailable. The fallback is a lower security tier and accepted as a known limitation today. + +When the client signs a Backup Service challenge with the Sync Factor, the server verifies the P-256 ECDSA signature (DER-encoded) against the public key in the . Each Sync Factor authorizes exactly one device; recovery onto a new device produces a new Sync Factor and registers it via `/add-sync-factor`. + +For OIDC users, the Sync Factor is *also* registered with Turnkey as a sub-organization user (`sync_factor_user`) with a policy permitting deletion-only actions. This is what makes background factor cleanup (e.g., removing an OIDC provider after the user removes the factor in Settings) possible without an additional passkey tap. + +# Backup Account ID + +Independent of the factor system, every backup has a deterministic derived from the user's via a BLAKE2b-based KDF (subkey ID `0x101`, context `OXIDEKEY`). The keypair is **secp256k1**, distinct from the P-256 keypairs used elsewhere in the system. Format: + +``` +backup_account_<33-byte_compressed_pubkey_hex> +``` + +This keypair is **not** a factor — it cannot decrypt the backup. Its sole purpose is the disaster-recovery `/reset` endpoint: when a user has lost every Main and Sync Factor but still controls the original device (and therefore the Root Key), the Backup Account ID lets them prove ownership and wipe the existing backup so they can re-enroll fresh factors. See [Disaster Recovery via /reset](/world-app/backup/advanced#disaster-recovery-via-reset). + +# Platform Differences + +The clients diverge in places where the underlying platform requires it. The list below is the *intentional* divergence, anything else is a bug. + +| | iOS | Android | +| --- | --- | --- | +| Passkey API | `AuthenticationServices` (PRF on iOS 18+) | Credential Manager (`androidx.credentials`); PRF requires GMS 23.33+ and a PRF-supporting provider | +| iCloud Keychain factor | Supported (Main Factor) | **Not available** — no platform equivalent | +| OIDC providers | Google, Apple | Google, Apple | +| Sync Factor secure storage | Keychain (non-synced) | Android Keystore; falls back to `EncryptedSharedPreferences` on devices without Keystore | +| Passkey rename | iOS 26.2+ only | Provider-dependent | +| OIDC sign-in | `GoogleSignInSDK` / `AppleSignInSDK` | `GetSignInWithGoogleOption` (Credential Manager) | From 4c1463607b9715015c1dce906fc6c0a2d4101fec Mon Sep 17 00:00:00 2001 From: pd Date: Thu, 7 May 2026 15:40:36 -0700 Subject: [PATCH 10/14] structure --- snippets/terms.mdx | 4 +- world-app/backup/structure-and-sync.mdx | 94 +++++++++++-------------- 2 files changed, 43 insertions(+), 55 deletions(-) diff --git a/snippets/terms.mdx b/snippets/terms.mdx index 0bf866d..3ee0027 100644 --- a/snippets/terms.mdx +++ b/snippets/terms.mdx @@ -7,6 +7,6 @@ export const RootKey = () => { export const PersonalCustodyPackage = () => { return ( - Personal Custody Package + Personal Custody Package ) -} \ No newline at end of file +} diff --git a/world-app/backup/structure-and-sync.mdx b/world-app/backup/structure-and-sync.mdx index bfd6c52..c86f858 100644 --- a/world-app/backup/structure-and-sync.mdx +++ b/world-app/backup/structure-and-sync.mdx @@ -1,86 +1,74 @@ --- -title: "Backup Sync & Internal Structure" +title: "Backup Structure & Sync" sidebarTitle: "Structure & Sync" -description: "Describes how the backup is synced between devices and used for authenticating new devices." +description: "What's inside the backup, how the manifest enforces consistent updates, and how sync works." --- -import { UnsealedBackup, SealedBackup } from "/snippets/backup/terms.mdx"; +import { UnsealedBackup, SealedBackup, MainFactor, SyncFactor, BackupEncryptionKeypair } from "/snippets/backup/terms.mdx"; import { RootKey, PersonalCustodyPackage } from "/snippets/terms.mdx"; +# What's in the Backup -Syncing is the operation by which a user **updates** their backup from an authenticated device. A backup sync (`/sync`) is unidirectional, with a mobile client providing an update when new files are included in the backup. The client only fetches remote updates in certain scenarios (see below). +The contains the user's plus a set of files needed to fully restore World App on a new device. Today this includes the user's s (Orb, Document, Face) and the credential vault. Each file is tagged with a *designator* identifying its kind. -The contains the encoded for World App plus a set of raw files. The source of truth is the `BackupFormat` structure in [Bedrock](https://github.com/worldcoin/bedrock). The files back up all required self-custodial information that a user holds in their World App. In particular, today this is the user's World ID s (or PCPs). +The exact list of designators is owned by [Bedrock](/world-app/bedrock) and evolves as new credentials are added. The authoritative enumeration lives in [`BackupFileDesignator`](https://github.com/worldcoin/bedrock/blob/main/bedrock/src/backup/mod.rs); refer to it directly rather than duplicating the list here. -One of the challenges the system faces is that because the backup is end-to-end encrypted, the **Backup Service** cannot enforce any type of rules that prevent accidental data loss. For example, consider Client M in which the User gets their passport verified and hence obtains a PCP. If the user is also authenticated on Client N, and a backup sync is triggered from Client N, the PCP would be removed from the backup because Client N does not have it. +The backup format itself is versioned ([`BackupFormat`](https://github.com/worldcoin/bedrock/blob/main/bedrock/src/backup/backup_format)). The current version is V0: a tar+gzip archive of CBOR-encoded files with per-file BLAKE3 checksums. +# The Manifest -## Backup State Management +Because the backup is end-to-end encrypted, the Backup Service cannot enforce content-level rules. If Client M observes that the user just verified their passport (and now holds a new PCP), the Backup Service has no way to require that a sync from Client N — which doesn't have that file — must include it. The manifest mechanism described below is what prevents accidental data loss in that case. -The backup system uses a **manifest-based approach** to prevent accidental data loss during syncs (e.g. Device A adds a file Y, when Device B tries to add file Z it doesn't know about file Y and the updated backup would not contain file Y, accidentally removing it). Each backup contains a manifest that describes its contents and enforces proper versioning. +Every backup ships with a **manifest**: a versioned, hashed ledger of every file in the . The manifest entries record each file's path, designator, and content checksum. The hash of the manifest is the backup's *state*. -### The Manifest +Both the client and the Backup Service track this hash. The Backup Service uses it to enforce that every update transitions from the *current* state — preventing an out-of-date client from silently overwriting a newer backup with a stale one. -Each backup ships with a manifest that describes the backup's contents. The manifest serves as a versioned ledger, ensuring clients always begin from the current state ($m_t$) and apply incremental updates to produce the next state ($m_{t+1}$). +The canonical encoding and BLAKE3 hashing are computed by Bedrock so they're identical across iOS and Android. See [`BackupManifest`](https://github.com/worldcoin/bedrock/blob/main/bedrock/src/backup/manifest.rs) for the implementation. -```json -{ - "version": 0, - "previous_manifest_hash": "0x1234567890000000000000000000000", - "files": [ - { - "checksum": "0x1234567890000000000000000000000", - "file_path": "/personal_custody/unpack/me/here", - "designator": "orb_pcp" - } - ] -} -``` +# State Transitions -Each state is defined as the `blake3` hash of the manifest file (raw bytes). +A successful update transitions the backup from state $m_t$ (the current manifest hash) to $m_{t+1}$ (the updated manifest hash). The rules: -### Responsibilities +- **Client.** Bears responsibility for performing updates against the current state. Verifies it holds the latest manifest before making changes. +- **Backup Service.** Maintains the current state hash. Validates each update is a transition from the recorded state. **Cannot** inspect encrypted contents. -- **Client**: Bears sole responsibility for performing backup updates based on the current state. Must verify it's operating on the latest manifest before making changes. -- **Server**: Maintains a pointer to the current state hash. Validates state transitions during updates but cannot inspect encrypted contents. +If the client tries to sync from a stale state, the service rejects with `manifest_hash_mismatch` (HTTP 412). The client must re-authenticate a , download the latest , apply local changes on top, and retry. See [Multiple Device Sync](/world-app/backup/advanced#multiple-device-sync) for why this happens in practice. -### ManifestManager API (Client-Side) +# ManifestManager API -The `ManifestManager` provides controlled access to backup modifications. Before any operation, it fetches the remote state hash to ensure consistency: +Clients interact with the manifest through Bedrock's `ManifestManager`. It gates every operation on a fresh remote-hash check before mutating local state, so the client cannot accidentally produce an update from a stale base: -- If the local manifest is missing → prompts user to authenticate and download the remote state -- If `remote_manifest_hash == local_manifest_hash` → proceeds with updates -- If hashes differ → prompts user to authenticate and download the remote state +- `store_file(designator, path)` — Add a file. Computes its checksum, appends to the manifest, packs and uploads. +- `list_files(designator)` — List files for a designator. +- `replace_all_files_for_designator(designator, new_path)` — Atomically replace every file under a designator with a new one. +- `remove_file(path)` — Remove a file entry. -#### Available Methods +If the local manifest is missing or its hash differs from the remote, every method short-circuits with `RemoteAheadStaleError`. The client surfaces this as a prompt to re-authenticate and download the latest backup before retrying the change. -- **`store_file(designator, filePath)`** - Adds a new file to the backup. Verifies path uniqueness, computes checksum, triggers pack & upload. -- **`list_files(designator)`** - Returns all entries for the given designator. -- **`replace_all_files_for_designator(designator, newFilePath)`** - Replaces all existing entries for a designator with a new file. -- **`remove_file(filePath)`** - Removes a file entry from the manifest. +See [`ManifestManager`](https://github.com/worldcoin/bedrock/blob/main/bedrock/src/backup/manifest.rs) for signatures and behavior. -### Pack & Upload Process +# Pack & Upload -When building and uploading a new backup version: +When building a new backup version (see [BF-4: Sync](/world-app/backup/flows#bf-4-backup-sync)): -1. **Validate local manifest** - Error if missing; prompt to download remote. -2. **Build unsealed backup** - Collect all files from the manifest. Verify checksums match for every file. -3. **Submit sealed backup** - Include updated manifest inside. Report current and new manifest hashes to backend. -4. **Backend validation** - Verifies current hash matches recorded state before accepting update. +1. Validate the local manifest exists and matches the remote state. +2. Build the from the files referenced in the manifest. Verify each file's checksum. +3. Encrypt with the 's keypair public key. +4. POST `/sync` with the new , the current manifest hash, and the new manifest hash. +5. Backup Service checks the current hash matches its recorded state, then atomically swaps the blob and the recorded hash. -### Download & Unpack Process +# Download & Unpack -When downloading and unpacking a remote backup: +When restoring or catching up (see [BF-2](/world-app/backup/flows#bf-2-backup-recovery) and [BF-5](/world-app/backup/flows#bf-5-remote-update-detection)): -1. **Decrypt sealed backup** - Extract and replace local manifest unconditionally. -2. **Unpack files** - Write each file to its declared `filePath`. Replace existing files (log errors for checksum mismatches). +1. Decrypt the with the -unlocked . +2. Replace the local manifest unconditionally with the manifest from the unpacked backup. +3. Write each file to its declared path. On checksum mismatch the file is overwritten with the version from the backup and the mismatch is logged — the backup is the source of truth. -This approach is safe because clients enforce explicit state transitions based on the current manifest. +# Startup Verification -### Startup Verification +On every app launch the client fetches the remote manifest hash. If it differs from the local hash, the user is prompted to re-authenticate a and download the remote backup. This handles the multi-device case where the user updated their backup from Device A and then opens World App on Device B. -On app startup, the client fetches the remote state checksum. If it differs from the local manifest, the user is prompted to authenticate and download the remote backup. +# Backend Safety Measures -### Additional Safety Measures - -- **Backend short-lived locks** - Prevents narrow race windows during updates, only one update at a time. Authoritative enforcement remains via manifest hash validation. \ No newline at end of file +- **Short-lived locks.** The Backup Service holds a short-lived per-`backup_id` lock during create and sync to close the narrow race window between the hash check and the write. The authoritative consistency check remains the manifest hash comparison. From 4572360ebb31b69896c27c44731de8679d42354b Mon Sep 17 00:00:00 2001 From: pd Date: Thu, 7 May 2026 15:45:11 -0700 Subject: [PATCH 11/14] flows --- world-app/backup/flows.mdx | 164 +++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 70 deletions(-) diff --git a/world-app/backup/flows.mdx b/world-app/backup/flows.mdx index c172c6e..a048f56 100644 --- a/world-app/backup/flows.mdx +++ b/world-app/backup/flows.mdx @@ -1,85 +1,109 @@ --- title: "Backup Flows" sidebarTitle: "Flows" -description: "Detailed documentation on the key flows related to the World App backup system." +description: "The key user-facing flows. Focused on non-obvious behavior and decisions rather than step-by-step UI scripts." --- -import { MainFactor } from "/snippets/backup/terms.mdx"; +import { MainFactor, SyncFactor, BackupEncryptionKeypair, FactorSecret, SealedBackup, BackupMetadata, EncryptedBackupEncryptionKeypair } from "/snippets/backup/terms.mdx"; -## 1. Backup Creation +# Conventions +The flows are numbered (BF-1, BF-2, …) and referenced from code and other docs. Each section below covers **purpose, what's non-obvious, and where the iOS and Android implementations diverge** — not a step-by-step script. The authoritative sequencing lives in the client code; this page exists to explain *why* a flow looks the way it does. -## 7. Factor Deletion -- This flow is triggered when the user deletes a from the UI. -- Sync Factors are not manually removed by the user. This is usually done through other flows (e.g. when logging out). -- Currently, Passkeys are enrolled into Turnkey to provide a fallback mechanism in case a Sync Factor is no longer available. When the requested operation cannot be completed with the Sync Factor (see `PUBLIC_KEY_NOT_FOUND` error), the client will use the Passkey to execute the operation instead. -- **Transitory**: Currently, removing a Passkey factor while there is still an OIDC factor is not supported. This is because a Passkey factor is required to manage other factors (restriction on `backup-service` when adding other factors) and the Passkey is used to recover access to Turnkey should the Sync Factor fail. -- **Future Note**: The current flow assumes that Turnkey is only used for OIDC Factors. This may change in the future (e.g. to support non-PRF Passkeys). +# BF-1: Backup Creation + +**When.** A user finishes onboarding and creates their first backup, picking one or two recovery methods. + +**Non-obvious.** The client generates a *fresh* at this moment, encrypts the , encrypts the keypair with the of each enrolled , and then **discards the keypair's secret**. Only the public key is retained locally so subsequent sync operations can re-encrypt new s without authenticating a Main Factor. This is what makes background sync (BF-4) possible. + +A is also generated at creation and registered with the Backup Service in the same `/create` call, so the device is immediately authorized to push updates. + +**OIDC + Passkey one-tap enrollment.** When the user enrolls both a Passkey and an OIDC factor (Google/Apple) at creation, the client takes advantage of a *merged Turnkey passkey challenge*: the Passkey signs both a Backup Service challenge and a Turnkey activity stamp in a single user gesture. This is also the pattern used in BF-3 when adding an OIDC factor to an existing Passkey-only account. + +**Cross-platform.** iOS and Android diverge only in the platform-specific factor enrollment (Passkey ceremony, OIDC sign-in SDK). The Bedrock-side sealing and backup-service interaction are identical. ```mermaid -flowchart TD - A{"Removing the last Main factor?"} -- Yes --> EndDelete(["Delete the backup: BF-8"]) - A -- No --> C["backup-service: /delete-factor/challenge/keypair"] - C --> D["backup-service: /delete-factor with factorId with Sync Factor"] - D --> E{"Deleting an OIDC factor?"} - E -- Yes --> F{"Deleting the last OIDC factor?"} - F -- Yes --> G["Delete Turnkey Account (ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION) with Sync Factor"] - F -- No --> H["Delete OIDC account from Turnkey (ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS) with Sync Factor"] - G --> EndSuccess - H --> EndSuccess - - E -- No -->I{"Deleting iCloud Keychain?"} - I -- No --> EndSuccess - I -- Yes --> J["Delete keypair from Keychain"] - J --> EndSuccess(["End: Show Success"]) - - G .- fail .-> K{Failed with PUBLIC_KEY_NOT_FOUND?} - K -- No --> Fail["Retry 2 times with back-off. Log errors. After max retries, continue."] - K -- Yes --> L{"User has Passkey?"} - L -- Yes --> M["Delete Turnkey Account (ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION) with Main Factor (Passkey)"] - L -- No --> EndSuccess - M --> EndSuccess - - H -. fail .-> N{Failed with PUBLIC_KEY_NOT_FOUND?} - N -- Yes --> Q{"User has Passkey?"} - Q -- No --> EndSuccess - Q -- Yes --> O["Create new Sync Factor in Turnkey Account using Passkey"] - O --> P["Delete OIDC account from Turnkey (ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS) with NEW Sync Factor"] - P --> EndSuccess - - O -. fail .-> Fail - P -. fail .-> Fail - - C -. fail .-> EndError(["End: Show Failure to User"]) - D -. fail .-> EndError +flowchart LR + User -->|enroll factor| Client + Client -->|generate BackupEncryptionKeypair| Client + Client -->|seal backup| Client + Client -->|encrypt keypair with FactorSecret| Client + Client -->|POST /create| BackupService + Client -->|register Sync Factor| BackupService + Client -.->|if OIDC| Turnkey ``` -## 8. Backup Deletion -- Backup Deletion is triggered from either: - - The user removing the last (see BF-7). - - The user requesting full account deletion of World App. -- Backup Deletion will clear all data related to the backup. +# BF-2: Backup Retrieval + +**When.** A user signs into World App on a new device. + +**Non-obvious.** Retrieval (or previously "Recovery") is the most architecturally distinct flow because the user has no local state. The client has to bootstrap from a single Main Factor. + +The client identifies the backup by presenting the Main Factor's *public identifier* (passkey credential ID, OIDC `sub`, or Keychain public key) — the Backup Service has a reverse lookup table that maps these to a `backup_id`. Once the backup is located, the user authenticates the factor and the Backup Service returns the together with the metadata. + +After the backup is decrypted, the client generates a fresh for the new device and registers it via `/add-sync-factor`. This call uses a short-lived `sync_factor_token` issued by `/retrieve/from-challenge`, which lets the client add the new device authorization without re-prompting for a Main Factor. +**OIDC recovery quirk.** For OIDC factors, decrypting the requires the held by Turnkey. The user authenticates to Turnkey via OIDC; Turnkey's enclave verifies the OIDC token *and the user's identity* before exporting the secret to the client. This is why OIDC recovery requires a network round-trip beyond the one to the Backup Service. ```mermaid -flowchart TD - A["backup-service: /delete-backup/challenge/keypair with Sync Factor"] --> B["backup-service: /delete-backup"] - B --> C{"User has Turnkey account?"} - C -- Yes --> D["Delete Turnkey Account (ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION) with Sync Factor"] - C -- No --> E["Clear Bedrock Data"] - D --> E - E --> F["Clear local keypair for Sync Factor"] - F --> G{"User on iOS and has iCloud Keychain Factor"} - G -- Yes --> H["Delete iCloud Keychain Factor (Main)"] - G -- No --> EndSuccess(["End: Show Success"]) - H --> EndSuccess - - - D -. fail .-> Fail["Retry 2 times with back-off. Log errors. After max retries, continue."] - F -. fail .-> Fail - E -. fail .-> Fail - H -. fail .-> Fail - - A -. fail .-> EndError(["End: Show Failure to User"]) - B -. fail .-> EndError -``` \ No newline at end of file +sequenceDiagram + participant Client + participant BackupService + participant Turnkey + Client->>BackupService: identify backup by factor public ID + Client->>BackupService: /retrieve/challenge/* + auth Main Factor + BackupService->>Client: Sealed Backup + Metadata + sync_factor_token + opt OIDC factor + Client->>Turnkey: authenticate (OIDC) + Turnkey->>Client: factor secret + end + Client->>Client: decrypt EncryptedBackupKeypair → decrypt SealedBackup + Client->>BackupService: /add-sync-factor (with token) + Client->>Turnkey: Add sync factor +``` + +# BF-3: Adding a Main Factor + +**When.** A user adds a second (or third) recovery method from Settings. + +**Non-obvious.** Adding a factor *does not* require re-encrypting the entire backup. The client only needs to encrypt the existing with the new and append the resulting to the metadata. This is the entire point of the keypair indirection layer (see [Layered Encryption Model](/world-app/backup/index#layered-encryption-model)). + +To add an OIDC factor with a Passkey already enrolled, the merged Turnkey passkey challenge from BF-1 is reused — one passkey tap authorizes both `/add-factor` on the Backup Service and the Turnkey activity that creates the OIDC provider. + +**Currently disabled.** Adding a Passkey when an OIDC factor exists is *not* supported today. Removing a Passkey while an OIDC factor exists is also not supported, because the Passkey is the fallback path for Turnkey when the Sync Factor's Turnkey credentials are no longer valid (see [Unauthorized Device](/world-app/backup/advanced#unauthorized-device)). + +# BF-4: Backup Sync + +**When.** The user takes an action that produces a new file (e.g., orb verification yields a new PCP) or modifies the credential vault. + +**Non-obvious.** Sync runs silently — it only uses the , not a . The client retains the public half of the from creation/recovery, which is enough to re-encrypt the new without unlocking any factor secret. + +The flow is gated by the manifest hash. If the local manifest is stale (the user updated their backup on another device), the sync attempt fails with `manifest_hash_mismatch` and the UI escalates to BF-5. + +# BF-5: Remote-Update Detection + +**When.** App launch, and on demand from sync failures. + +**Non-obvious.** This is the only flow that prompts re-authentication of a Main Factor *outside* of explicit user intent. Because background sync cannot fetch encrypted backup contents (the can't decrypt), reconciling a remote update requires a Main Factor. + +The client simply fetches the remote manifest hash; if it doesn't match the local hash, the UI presents a recovery-style prompt and the user re-authenticates a Main Factor to download the updated backup. After download, local sync resumes normally. + +# BF-6: Multi-Device Sync + +This is not a separate flow but a consequence of BF-4 and BF-5 interacting. See [Advanced Topics — Multiple Device Sync](/world-app/backup/advanced#multiple-device-sync). + +# BF-7: Factor Deletion + +**When.** The user removes a recovery method from Settings. + +**Non-obvious.** Two systems need updating: the Backup Service (remove from ) and Turnkey (remove the corresponding OIDC provider, or delete the sub-organization if it's the last OIDC factor). Both deletions happen using the , not a Main Factor — Turnkey policy explicitly authorizes the Sync Factor for these specific deletion activities (see [Components & Parties — Turnkey](/world-app/backup/components#turnkey)). This avoids a passkey prompt on every Settings interaction. + +**Failure handling.** If the Sync Factor is no longer valid in Turnkey (`PUBLIC_KEY_NOT_FOUND` — see [Unauthorized Device](/world-app/backup/advanced#unauthorized-device)), the client falls back to the user's Passkey to perform the Turnkey operation. This is one of the reasons a Passkey is currently required to manage other factors. + +**Last factor.** Deleting the last cascades into BF-8. + +# BF-8: Backup Deletion + +**When.** The user removes their last , or initiates full account deletion in World App. + +**Non-obvious.** Deletion clears state across three places — the Backup Service (metadata + sealed blob + factor lookup), Turnkey (entire sub-organization, if one exists), and local device (Sync Factor keypair, plus the iCloud Keychain factor on iOS). All three deletions are issued from the client with retries; failures are logged but do not block the user-visible "deleted" outcome, because the Backup Service's deletion is authoritative. From bee9b4a0c6c7e13e759714569fc0ed752a4c95c5 Mon Sep 17 00:00:00 2001 From: pd Date: Thu, 7 May 2026 15:48:17 -0700 Subject: [PATCH 12/14] components --- docs.json | 1 + world-app/backup/components.mdx | 141 +++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 40 deletions(-) diff --git a/docs.json b/docs.json index 36c009a..8cde8a5 100644 --- a/docs.json +++ b/docs.json @@ -53,6 +53,7 @@ "world-app/backup/factors", "world-app/backup/structure-and-sync", "world-app/backup/flows", + "world-app/backup/components", "world-app/backup/advanced" ] }, diff --git a/world-app/backup/components.mdx b/world-app/backup/components.mdx index f3a6513..89a14a9 100644 --- a/world-app/backup/components.mdx +++ b/world-app/backup/components.mdx @@ -1,58 +1,119 @@ --- title: "Components & Parties" sidebarTitle: "Components & Parties" -description: "The parties and their respective components and responsibilities within the backup system." +description: "What the Backup Service and Turnkey actually do, what they store, and where their authority ends." --- -import { SyncFactor, MainFactor } from "/snippets/backup/terms.mdx"; +import { SyncFactor, MainFactor, BackupMetadata, SealedBackup, FactorSecret, BackupEncryptionKeypair, BackupAccountId } from "/snippets/backup/terms.mdx"; # Backup Service -Details coming soon. +The Backup Service is a [Nethermind](https://www.nethermind.io/)-hosted REST service. Tools for Humanity has no production access; the service holds ciphertext and metadata only. Its open source repository is [`backup-service`](https://github.com/worldcoin/backup-service). -# Turnkey +## Role and guarantees -FIXME: Add intro +The service: +- Stores the and . +- Authenticates clients via per-operation challenges signed by the relevant factor. +- Enforces state transitions on sync via the manifest hash (see [Structure & Sync](/world-app/backup/structure-and-sync#state-transitions)). +- Maps factor public identifiers back to `backup_id` so a recovering client can locate its backup from a single Main Factor. -All short-lived keys used with Turnkey are set for a duration of **5 minutes** unless otherwise specified. +It does **not**: + +- Hold the 's decryption key, any , or any factor private key. +- Inspect the contents of the . +- Authenticate users beyond verifying signatures and tokens — there is no concept of a Backup Service user account. + +## Storage layout + +- **S3** — blobs and JSON, keyed by `backup_id`. +- **DynamoDB** — A `FactorLookup` table mapping `{scope}#{type}#{factor_value}` (e.g. `MAIN#PASSKEY#`) back to `backup_id`. This is a lookup utility — the source of truth for *which factors authorize a backup* is the metadata in S3. +- **Redis** — Ephemeral state: JWE-encrypted challenge tokens (single-use, ~5 min TTL), short-lived per-`backup_id` locks during create/sync, and post-recovery `sync_factor_token`s used to register a new without re-prompting a Main Factor. + +The list of endpoints, request shapes, and the `ChallengeContext` enum live in [`backup-service/src/routes`](https://github.com/worldcoin/backup-service/tree/main/src/routes); the OpenAPI spec is served at `/docs` on each environment. + +## Authorization + +Three factor types are recognized: +| Factor type | Verification | Allowed scope | +| --- | --- | --- | +| Passkey | WebAuthn assertion (COSE `ES256`) verified via `webauthn-rs` | Main only | +| OIDC account | OIDC token signature + claim verification (nonce = SHA256(ephemeral session pubkey)) | Main only | +| EC keypair (P-256) | ECDSA signature, DER-encoded, verified with `p256` | Main or Sync | + +The challenge token is bound to the operation. A `Sync` challenge cannot be replayed against `/delete-factor` because the embedded `ChallengeContext` differs. Challenges are single-use (Redis enforces). + +## What the service rejects + +A handful of named errors are surfaced in client code paths and worth knowing by name: + +- `manifest_hash_mismatch` — client tried to sync from a stale state (see [BF-5](/world-app/backup/flows#bf-5-remote-update-detection)). +- `unauthorized_factor` — factor identifier is in `FactorLookup` but no longer in the backup metadata; the device is no longer authorized (see [Unauthorized Device](/world-app/backup/advanced#unauthorized-device)). +- `backup_does_not_exist` — the metadata is gone (the user deleted their backup); local state should be cleared. +- `backup_account_id_already_exists` — `/create` collision; some prior creation attempt registered this backup ID. The client retries with a fresh sync factor authorization on the existing record. +- `factor_already_exists` — attempted to add a factor whose public identifier is already enrolled. +- `invalid_challenge_context` — challenge token didn't match the operation it was submitted for. + +The full enumeration is in [`backup-service/src/types/error.rs`](https://github.com/worldcoin/backup-service/blob/main/src/types/error.rs). + +# Turnkey + +[Turnkey](https://turnkey.com/) is the custodian of s for OIDC factors. It runs key material inside TEE-backed enclaves and exposes a REST API; we picked it because no other provider met the combined requirements of OIDC-aware authentication, non-custodial key handling, and an API-first integration model. + +All short-lived keys used with Turnkey are set for a duration of **5 minutes** unless otherwise specified. ## Turnkey User Setup The initial release of this feature relied on a slightly different user setup. Some early World App Users will have a slightly different setup (with the Root Quorum being `auth_user_main` solely) before a migration is introduced. -1. **Ephemeral**. A `root_user_genesis` user is created by World App's backend. - - This user is a root user. - - This user is created with a short-lived keypair provided by the client. - - The user is also registered with a fallback `0` key (`Curve25519`) as Turnkey requires all users to have an always valid authenticator or keypair. - - This user is immediately deleted by the client upon setup of `auth_user_main` -2. An `auth_user_main` is created by the client. This user represents how the World App User primarily authenticates, and it's authenticated with the s. - - This user has a permission policy with explicit `ALLOW` for all activities (`condition = true`). - - The user's passkey and OIDC factors are added to this user for authentication. - - ```json - { - "effect": "EFFECT_ALLOW", - "consensus": "approvers.any(user, user.id == '')", - "condition": "true" - } - ``` -3. A `sync_factor_user` is created by the client. This user represents user interactions operated with their s. - - A long lived keypair is added per- to this user. A user will have a unique per device. Each registers a different keypair with the same `sync_factor_user` in Turnkey. - - This user has permission to perform certain deletion operations. This enables the user to perform management operations without requiring additional authentication. For example, it's used to remove an OIDC factor without additional user interaction, or to delete the user's Turnkey account altogether. - - ```json - { - "effect": "EFFECT_ALLOW", - "consensus": "approvers.any(user, user.id == '')", - "condition": "activity.action == 'DELETE' && (activity.resource == 'CREDENTIAL' || activity.resource == 'PRIVATE_KEY' || activity.resource == 'ORGANIZATION')" - } - ``` - - > The policy filters on `activity.action` and `activity.resource` instead of `activity.type` as the latter is version specific. At the current time, the policy translates into these activity types: `ACTIVITY_TYPE_DELETE_API_KEYS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_AUTHENTICATORS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION` (`ORGANIZATION`), `ACTIVITY_TYPE_DELETE_PRIVATE_KEYS` (`PRIVATE_KEY`), `ACTIVITY_TYPE_DELETE_PRIVATE_KEY_TAGS` (`PRIVATE_KEY`), `ACTIVITY_TYPE_DISABLE_PRIVATE_KEY` (`PRIVATE_KEY`) - -4. In addition to the user setup, Turnkey has the concept of a [Root Quorum](https://docs.turnkey.com/concepts/users/root-quorum). When the Root Quorum is met, any action does not go through the Policy Engine and is directly approved. For World App, the Root Quorum is generally not used. To prevent misuse, the Root Quorum is set to 2-of-2 with both the `auth_user_main` and `sync_factor_user` being part of the quorum. This ensures that: - - `sync_factor_user` can update `auth_user_main` as granted by its policy (i.e. delete OAuth providers or authenticators). - - `sync_factor_user` cannot take any action not explicitly granted by the policy, as `sync_factor_user` cannot reach Root Quorum on its own. Or in other words, Root Quorum still requires approval by `auth_user_main`. - - A World App User can always update their `sync_factor_user` using `auth_user_main` in case of loss of all devices. +Each World App user is a Turnkey *sub-organization*. The sub-organization has three users with distinct roles: + +1. **Ephemeral.** A `root_user_genesis` user is created by World App's backend to bootstrap the sub-organization. + - It is a root user. + - It is created with a short-lived keypair provided by the client. + - It is also registered with a fallback `0` key (`Curve25519`) because Turnkey requires every user to have at least one valid authenticator. After the genesis user is no longer needed, the keypair is discarded — the fallback key remains but no one holds its private half. + - The client deletes this user immediately after `auth_user_main` is set up. +2. **`auth_user_main`** — represents the user's primary authentication and holds their credentials (passkeys and OIDC providers). + - Permission policy: explicit `ALLOW` for all activities (`condition = true`). + + ```json + { + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '')", + "condition": "true" + } + ``` +3. **`sync_factor_user`** — represents operations. One long-lived API keypair is registered per device (each device holds its own private key locally; all are attached to the same user). The user's policy permits *deletion-only* activities, so a Sync Factor cannot grant itself recovery powers. + + ```json + { + "effect": "EFFECT_ALLOW", + "consensus": "approvers.any(user, user.id == '')", + "condition": "activity.action == 'DELETE' && (activity.resource == 'CREDENTIAL' || activity.resource == 'PRIVATE_KEY' || activity.resource == 'ORGANIZATION')" + } + ``` + + > The policy filters on `activity.action` and `activity.resource` instead of `activity.type` because the latter is version-specific. The current expansion covers `ACTIVITY_TYPE_DELETE_API_KEYS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_AUTHENTICATORS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_OAUTH_PROVIDERS` (`CREDENTIAL`), `ACTIVITY_TYPE_DELETE_SUB_ORGANIZATION` (`ORGANIZATION`), `ACTIVITY_TYPE_DELETE_PRIVATE_KEYS` (`PRIVATE_KEY`), `ACTIVITY_TYPE_DELETE_PRIVATE_KEY_TAGS` (`PRIVATE_KEY`), `ACTIVITY_TYPE_DISABLE_PRIVATE_KEY` (`PRIVATE_KEY`). + +In addition, Turnkey has the concept of a [Root Quorum](https://docs.turnkey.com/concepts/users/root-quorum). When the Root Quorum is met, any action bypasses the Policy Engine. We set the Root Quorum to 2-of-2 with both `auth_user_main` and `sync_factor_user` as members. This ensures: + +- `sync_factor_user` can update `auth_user_main` *as granted by its policy* (delete OAuth providers, delete authenticators). +- `sync_factor_user` cannot take any action *not* explicitly granted by the policy, because it cannot reach the Root Quorum on its own — Root Quorum still requires `auth_user_main`'s approval. +- A user can always update `sync_factor_user` from `auth_user_main` after losing all devices. + +## What Turnkey holds vs what the Backup Service holds + +| Held by Turnkey | Held by Backup Service | +| --- | --- | +| OIDC factor secrets (inside enclave) | blob (encrypted) | +| OIDC provider configurations | (encrypted keypair copies, factor list) | +| Sub-organization membership (auth/sync users) | `FactorLookup` reverse index | +| P-256 API keypairs (per-device sync factor) | Challenge state (Redis) | + +Neither holds the 's secret key — that is destroyed at backup creation. + +## Direct vs proxied activities + +Most Turnkey activities (importing the OIDC factor secret at enrollment, exporting it at recovery) are sent **directly** from the client to Turnkey using a stamp computed in Bedrock. The exception is *creating a new sub-organization*, which requires a stamp from Tools for Humanity's parent-organization API key — that single activity goes through TFH's app-backend. From d555157c5bd2afec81f79ba35e1280c3f403277f Mon Sep 17 00:00:00 2001 From: pd Date: Thu, 7 May 2026 15:49:01 -0700 Subject: [PATCH 13/14] advanced --- world-app/backup/advanced.mdx | 72 ++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/world-app/backup/advanced.mdx b/world-app/backup/advanced.mdx index 3213683..ecc70ad 100644 --- a/world-app/backup/advanced.mdx +++ b/world-app/backup/advanced.mdx @@ -1,34 +1,68 @@ --- -title: "Advanced Topics on Backup" +title: "Advanced Topics" sidebarTitle: "Advanced Topics" -description: "Internal advanced technical reference for the backup system." +description: "Encryption details, edge cases, and disaster recovery." --- -import { SyncFactor, MainFactor } from "/snippets/backup/terms.mdx"; +import { SyncFactor, MainFactor, BackupEncryptionKeypair, EncryptedBackupEncryptionKeypair, FactorSecret, SealedBackup, UnsealedBackup, BackupAccountId, BackupMetadata } from "/snippets/backup/terms.mdx"; +import { RootKey } from "/snippets/terms.mdx"; -## Encryption +# Encryption -All encryption operations use libsodium's asymmetric [sealed_box](https://doc.libsodium.org/public-key_cryptography/sealed_boxes) primitive through the [crypto_box](https://crates.io/crates/crypto_box) crate. A randomness source is required for sealed boxes to generate ephemeral key pairs for sealing. See [Cryptography](/world-app/cryptography) for more details on the randomness source. +All asymmetric encryption in the backup uses libsodium's [sealed box](https://doc.libsodium.org/public-key_cryptography/sealed_boxes) primitive (X25519 key agreement + XSalsa20-Poly1305 AEAD), via the [`crypto_box`](https://crates.io/crates/crypto_box) crate. A sealed box generates an ephemeral sender keypair per encryption, so each ciphertext is independently sealed. +Two distinct seals exist in the system: -## Advanced Edge Cases +- **Sealing the .** Done with the public key of the freshly-generated (X25519). The result is the . The keypair's secret key is destroyed after this seal, so even the original device cannot decrypt the without going through a Main Factor. +- **Sealing the .** Done once per Main Factor, with a public key derived from that factor's (the secret is hashed into a Curve25519 secret key). The result is an stored in the . -### 1. Multiple Device Sync +Randomness for ephemeral keys comes from the OS CSPRNG via Bedrock's `OsRng`. See [Cryptography](/world-app/cryptography). -A user may be logged in multiple devices at the same time. When one of the devices syncs a new file, this file needs to be retrieved in the other devices. This cannot be done in the background because for security reasons, the is not authorized to retrieve the user's backup, nor has any secret material for decryption. The client detects it needs to update its backup using the manifest hashes (see [Backup Sync & Internal Structure](/world-app/backup/structure-and-sync)). When an update is required, the client prompts the user to authenticate with a and download the updated remote backup. +# Edge Cases -Importantly, whenever a client tries to sync, the backup service will help the client ensure it can only perform updates to the backup if it is operating on the latest state. This helps prevent accidental data loss. +## Multiple Device Sync -### 2. Unauthorized Device +A user authenticated on multiple devices will produce divergent local backup state if both devices make changes. The system handles this conservatively: a is *not* authorized to retrieve the encrypted backup, so a device cannot silently catch up to remote updates. Instead, the manifest hash mismatch detected at sync (or startup, see [BF-5](/world-app/backup/flows#bf-5-remote-update-detection)) escalates to a re-authentication and a fresh download. -At a point in time, it is possible a device becomes unauthorized to manage a user's backup. This means that the device's is no longer authorized with either or both Components. With Turnkey, this can be seen when receiving a `PUBLIC_KEY_NOT_FOUND` error and with the backup service when receiving an `unauthorized_factor` error (if the right `backup_id` is provided in the request). This can occur in some edge case scenarios: -- The user is authenticated in multiple devices and deletes and recreates the entire backup. Other authenticated devices become unauthorized. -- The user loses their . -- (Future). The user explicitly removes authorization for a specific device. +The Backup Service enforces that every sync transitions from the *current* manifest hash, so two concurrent devices cannot accidentally clobber each other — the second one to arrive will be rejected and forced to re-fetch. +## Unauthorized Device -The remediation path for this case is to use a to register a new . In particular, there is explicit handling in these scenarios: -1. When creating a backup if the `backup_account_id_already_exists` error is received, the client prompts the user to authenticate with a and register a new (see **[BF-1](/world-app/backup/flows#bf-1-backup-creation)**). -2. When fetching metadata if the `unauthorized_factor` error is received, the client prompts the user to authenticate with a and register a new . If on the other hand, a `backup_does_not_exist` error is received, the client knows the backup has been deleted by the user and will update its local state accordingly. -3. When deleting a factor if the `PUBLIC_KEY_NOT_FOUND` error is received from Turnkey, the client prompts the user to authenticate with a to register a new and retry their operation (see **[BF-7](/world-app/backup/flows#bf-7-factor-deletion)**). - It's not necessary to handle error cases on factor addition/deletion with the backup service because a `/retrieve-metadata` call always precedes this. +A device's can become unauthorized in either or both of the systems it's registered with (Backup Service, Turnkey). Symptoms: + +- Backup Service returns `unauthorized_factor` (when the right `backup_id` was provided) or `backup_does_not_exist` (factor lookup found nothing). +- Turnkey returns `PUBLIC_KEY_NOT_FOUND`. + +Triggers: + +- The user deleted and re-created the entire backup from another device. Sibling devices become unauthorized. +- The user lost their (e.g., the device was reset). +- (Future) The user explicitly revoked authorization for a specific device. + +The remediation path is always the same: re-authenticate a and register a fresh via `/add-sync-factor`. The clients have explicit handling at three call sites: + +1. On `/create` returning `backup_account_id_already_exists`, prompt for a Main Factor and run the recovery-style sync-factor refresh — this turns a duplicate creation attempt into an idempotent re-authorization. +2. On `/retrieve-metadata` returning `unauthorized_factor`, prompt for a Main Factor and refresh the Sync Factor. If `backup_does_not_exist` is returned instead, the backup has been deleted and the local state is wiped. +3. On `/delete-factor` returning `PUBLIC_KEY_NOT_FOUND` from Turnkey, fall back to the user's Passkey to perform the Turnkey operation, then continue with the Backup Service deletion. + +Error handling on `/add-factor` and `/delete-factor` against the Backup Service does not need its own unauthorized-factor branch because every such call is preceded by `/retrieve-metadata`, which surfaces the condition first. + +## Stale Local Backup + +If the local manifest hash differs from the remote on app startup or before a sync, Bedrock returns `RemoteAheadStaleError`. The UI escalates the user to a recovery-style prompt and runs [BF-5](/world-app/backup/flows#bf-5-remote-update-detection). The local state is overwritten unconditionally with the freshly-downloaded backup — the manifest itself is the arbiter. + +## Last Main Factor Restriction + +Removing the last deletes the entire backup (the data is no longer recoverable). The Backup Service enforces this server-side; clients confirm with the user before issuing the request. See [BF-7](/world-app/backup/flows#bf-7-factor-deletion) and [BF-8](/world-app/backup/flows#bf-8-backup-deletion). + +# Disaster Recovery via /reset + +The keypair (secp256k1, derived deterministically from the ) is a last-resort recovery mechanism for the case where: + +- The user has lost every (no enrolled passkey, OIDC, or iCloud Keychain factor remains accessible). +- The user has *also* lost every (no authorized device). +- But the user **still has the original device** (or the original Root Key in some other form) — and therefore can reproduce the deterministic backup-account secp256k1 keypair. + +In that situation, the user can sign a `/reset` challenge with the backup-account secret key. The Backup Service verifies the signature against the public key encoded in the `backup_account_id`, then deletes the backup metadata, the sealed blob, and all factor lookup entries. The user can then run [BF-1](/world-app/backup/flows#bf-1-backup-creation) again from scratch. + +This is **not** a recovery of the existing backup contents — it's a clean wipe that lets the user re-enroll without an account-already-exists collision. The Root Key (and therefore the wallet and on-chain identity) is preserved because the user already has it locally; what they're rebuilding is the *backup* of their PCPs and credential vault. From 875017ad6a8d44b3df85a1dc9902219d3e49d295 Mon Sep 17 00:00:00 2001 From: pd Date: Thu, 7 May 2026 15:49:36 -0700 Subject: [PATCH 14/14] typo --- world-app/backup/factors.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/world-app/backup/factors.mdx b/world-app/backup/factors.mdx index 11bd3c2..0d8f5be 100644 --- a/world-app/backup/factors.mdx +++ b/world-app/backup/factors.mdx @@ -28,7 +28,7 @@ A is a 32-byte secret used to encrypt the