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 queries/api/mpc-keys/useBackupWallet.ts:
// 1. Request Google Drive access token
const accessToken = await requestGoogleDriveAccessToken();
const gdrive = new GoogleDrive(accessToken);
// 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 FaceTec Enroll3D step runs on the /backup/facelock screen before this flow — it registers the user's face template on FaceTec servers so it can be matched during recovery.
Recovery
Recovery runs on a new device (or after reinstalling the app) and requires the same Google account used during backup.
How it works
Code walkthrough
The recovery logic lives in queries/api/mpc-keys/useRecoverWallet.ts:
// encryptionKey comes from Google Drive, face_session_token from FaceTec Match3D
const { encryptionKey, ...rest } = variables;
// 1. Fetch encrypted keyshares from auth-svc (face token authorizes the request)
const recoveryResponse = await APIClient.recoverKeyshares(rest);
const { keyshares } = recoveryResponse;
// 2. 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 };
}),
);
// 3. 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();
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.