Backup & Recovery
MPC keyshares only exist on the device. If the app is uninstalled or the device is lost, the wallet is gone — unless it was backed up. The backup system uses a combination of FaceTec biometrics, AES encryption, Google Drive, and auth-svc to make recovery possible on a new device.
Where each piece is stored
| Data | Stored at |
|---|---|
| AES encryption key (random password) | User's Google Drive |
| Encrypted MPC keyshares | auth-svc backend |
| Face biometric template | FaceTec servers |
Backup
How it works
Code walkthrough
The backup logic lives in src/queries/api/mpc-keys/useBackupWallet.ts:
// 1. Request Google Drive access token
const accessToken = await requestGoogleDriveAccessToken();
// The scope makes the backup filename specific to this deployment
const gdrive = new GoogleDrive(accessToken, { backendBaseUrl, cloudNodeType, cloudNodeUrl });
// 2. Generate a random password and AES-encrypt every enabled keyshare
const randomPassword = await encryptionService.generateRandomPassword();
const backups = await Promise.all(
supportedProtocols.map(async (protocol) => {
const keyId = wallet[protocol].mpcShareId;
const share = await storageProvider.getKeyshare(keyId);
const shareData = formatBackup({ value: share, keyType: protocol });
const encrypted = await encryptionService.encryptData(shareData, randomPassword);
return { keyId, encrypted };
}),
);
// 3. Save the password to Google Drive and verify the round-trip
await gdrive.createBackup(gdrive.getFileName(userId), randomPassword);
const backupPassword = await gdrive.retrieveBackup(gdrive.getFileName(userId));
// throws if verification fails
// 4. Upload encrypted keyshares to auth-svc
await APIClient.saveBackup({
keyshares: backups.map(({ keyId, encrypted }) => ({ key_id: keyId, encrypted_keyshare: encrypted })),
});
The face enrollment step runs on the src/app/(root)/backup/facelock.tsx screen before this flow — it registers the user's face so it can be matched during recovery.
gdrive.getFileName(userId) hashes the user ID together with the active backend URL, cloud node type, and cloud node URL. Each deployment therefore gets its own backup file, so switching between the Duo and Trio presets never overwrites the other's backup.
Recovery
Recovery runs on a new device (or after reinstalling the app) and requires the same Google account used during backup.
How it works
Registering the new device and recovering the keyshares used to be two separate calls, each needing its own face scan. They are now a single call, so the user scans their face once.
Code walkthrough
The recovery logic lives in src/queries/api/mpc-keys/useRecoverWallet.ts:
// encryptionKey comes from Google Drive, the face session id from the face match
const { encryptionKey, ...rest } = variables;
// 1. Prove this device owns its secure-hardware key
const secureKey = getSecureKey();
const { challenge } = await APIClient.getDeviceChallenge(secureKey.publicKeyHex);
const signature = secureKey.sign(Buffer.from(JSON.stringify({ challenge }), 'utf-8').toString('base64'));
// 2. Register the device and fetch the encrypted keyshares in one call
const recoveryResponse = await APIClient.registerAndRecover({
...rest,
device_id: secureKey.publicKeyHex,
signature,
});
const { keyshares } = recoveryResponse;
// 3. Decrypt and restore each keyshare with the key from Google Drive
const restored = await Promise.all(
keyshares.map(async (k) => {
const decrypted = await encryptionService.decryptData(k.encrypted_keyshare, encryptionKey);
const data = restoreBackup(decrypted);
const [type, key] = await createKeyshareObject(data);
await storageProvider.setKeyshare(key.keyIdHex, data.value);
return { type, key };
}),
);
// 4. Update wallet store with recovered share IDs
for (const protocol of supportedProtocols) {
const entry = restored.find((r) => r.type === protocol);
switch (protocol) {
case Algorithms.ecdsa:
setEcdsaMpcShare(entry.key);
break;
case Algorithms.eddsa:
setEddsaMpcShare(entry.key);
break;
case Algorithms.mldsa:
setMldsaMpcShare(entry.key);
break;
}
}
finishSetup();
If auth-svc reports the device key already belongs to another user, the hook generates a fresh device key, reloads the MPC sessions, and tries again — at most a few attempts, then it gives up with an error.
Recovery requires the same Google account that was used when the backup was created. The encryption key file in Google Drive is tied to the user's account — signing in with a different account will not find it.