Backup Service
The Backup Service is a Nethermind-hosted REST service. Tools for Humanity has no production access; the service holds ciphertext and metadata only. Its open source repository isbackup-service.
Role and guarantees
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).
- Maps factor public identifiers back to
backup_idso a recovering client can locate its backup from a single Main Factor.
- 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
FactorLookuptable mapping{scope}#{type}#{factor_value}(e.g.MAIN#PASSKEY#<credential_id>) back tobackup_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_idlocks during create/sync, and post-recoverysync_factor_tokens used to register a new without re-prompting a Main Factor.
ChallengeContext enum live in backup-service/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 |
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).unauthorized_factor— factor identifier is inFactorLookupbut no longer in the backup metadata; the device is no longer authorized (see 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—/createcollision; 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.
backup-service/src/types/error.rs.
Turnkey
Turnkey 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.-
Ephemeral. A
root_user_genesisuser 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
0key (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_mainis set up.
-
auth_user_main— represents the user’s primary authentication and holds their credentials (passkeys and OIDC providers).- Permission policy: explicit
ALLOWfor all activities (condition = true).
- Permission policy: explicit
-
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.The policy filters on
activity.actionandactivity.resourceinstead ofactivity.typebecause the latter is version-specific. The current expansion coversACTIVITY_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).
auth_user_main and sync_factor_user as members. This ensures:
sync_factor_usercan updateauth_user_mainas granted by its policy (delete OAuth providers, delete authenticators).sync_factor_usercannot take any action not explicitly granted by the policy, because it cannot reach the Root Quorum on its own — Root Quorum still requiresauth_user_main’s approval.- A user can always update
sync_factor_userfromauth_user_mainafter 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) |