Recuperación cross device
Cómo un usuario recupera su wallet en un dispositivo nuevo. El modelo Shamir 2 de 3 permite hacer esto sin comprometer la premisa no custodial.
Cuándo se necesita recovery
El flujo de recuperación se dispara en tres situaciones típicas:
- Cambio de dispositivo: el usuario compra un teléfono nuevo o formatea la laptop. El passkey viejo ya no está.
- Cambio de navegador: usuario que operaba en Chrome ahora quiere usar Safari en el mismo dispositivo. Los passkeys de un browser no cruzan a otro (a menos que usen iCloud Keychain o Google Password Manager que sincroniza).
- Borrado accidental de IndexedDB: el usuario limpió el cache del navegador y perdió el
CredentialRecordlocal.
En los tres casos la wallet on chain sigue existiendo intacta. Los fondos están seguros. El problema es que el dispositivo actual no tiene F1 ni el passkey necesarios para firmar.
Los tres fragmentos, revisitados
La wallet fue creada partiendo la seed con Shamir 2 de 3. Necesitás 2 fragmentos cualesquiera de estos tres:
- F1: guardado local en IndexedDB del navegador del user. Cifrado con llave derivada del biométrico del passkey del device.
- F2: guardado en el backend Accesly. Cifrado con llave derivada del biométrico. Requiere JWT válido para descargar.
- F3: guardado en el backend Accesly. Cifrado con llave derivada de una passphrase que el usuario eligió al crear la wallet (PBKDF2 600 mil iteraciones).
En operación normal se usan F1 + F2. En recuperación se usan F2 + F3, porque F1 no está disponible en el device nuevo. La passphrase del usuario descifra F3.
Dos caminos de recuperación
El flow de recovery del kit (<RecoveryFlow />) ofrece dos caminos según cómo se registró el usuario originalmente.
Camino 1: recovery por email
Para users que se registraron con email y password.
- Usuario ingresa su email en el flow.
- Backend envía un OTP de 6 dígitos al email via Amazon SES.
- Usuario ingresa el OTP y su passphrase de recovery en el flow.
- Backend verifica el OTP contra el hash bcrypt, emite un
recoveryJwtcon TTL 5 minutos. - SDK descarga F2 y F3 (envueltos en session key efímera).
- SDK deriva la
recoveryKeycon PBKDF2(passphrase, salt, 600k), descifra F3. - SDK reconstruye la seed usando Shamir(F2, F3).
- SDK genera un passkey nuevo en el device actual con la extensión PRF.
- SDK genera una seed nueva, la parte con Shamir, cifra los nuevos F1, F2, F3.
- SDK firma una transacción
rotate_signercon la seed vieja, la manda al backend, se ejecuta on chain. - Backend actualiza F2 y F3 en DynamoDB con los nuevos fragmentos. SDK guarda F1 nuevo en IndexedDB local.
Camino 2: recovery via Google
Para users que se registraron con Google.
- Usuario clickea "Continuar con Google" en el flow.
- Sesión Cognito federada se establece.
- Backend envía OTP al email del Google account.
- Usuario ingresa OTP y passphrase de recovery.
- El resto es idéntico al Camino 1 desde el paso 4.
Incluso en el flow Google, mandamos OTP al email como segundo factor. Esto protege contra el escenario donde alguien obtiene control del Google account del user (via compromiso de sesión OAuth) pero no del email inbox.
Qué pasa on chain
El paso crítico del recovery es la rotación del signer on chain. El Smart Account v3 tiene una función rotate_signer que reemplaza el owner ed25519 pubkey y el passkey secp256r1 pubkey.
La firma de esta transacción se hace con la seed vieja (recién reconstruida por Shamir F2 + F3). Después de la rotación:
- La seed vieja ya no autoriza operaciones en el Smart Account.
- El passkey viejo (del device perdido) ya no vale.
- El nuevo owner y el nuevo passkey son los únicos que firman ops futuras.
Como side effect, la G address bridge (dirección Stellar classic asociada) también se invalida porque su master key es la seed vieja. El SDK detecta esto y hace bootstrap de una G address nueva la próxima vez que se necesite (típicamente al hacer un swap SDEX).
Rotate multi tx en wallets con muchas rules
A partir de Smart Account v3.2.0 el SDK maneja automáticamente wallets con 4 o más context rules activas (típicamente cuando el usuario activó XLM, USDC y otras policies). En esas wallets el rotate_signer atómico escribe más de 132KB en el footprint Soroban y excede el cap de protocol 27.
Cuando el backend detecta esta situación, devuelve un error tipado RotateWriteCapExceededError con la lista de rotatableRuleIds. El SDK reacciona particionando la rotación en varias transacciones:
- El SDK agrupa las rules en batches de 2 y ejecuta una tx
rotate_signer_partial(rule_ids, new_owner, new_secp)por batch. Cada tx queda holgadamente bajo el cap. - Cuando todos los batches fueron aplicados, el SDK ejecuta una tx final
finalize_rotation(new_owner, new_secp, new_email_commitment)que persiste el nuevo email commitment y emite el eventoSignerRotated. - El backend recibe la firma del
finalize_rotationy en la misma llamada persiste todos los nuevos fragments F1, F2, F3 en DynamoDB.
El caller del hook recovery.completeRecovery(...) no ve la diferencia entre el path atómico y el multi tx. Recibe el mismo shape de respuesta con walletAddress, txHash del finalize_rotation y status. Wallets con 3 rules o menos siguen usando el path atómico legacy, sin overhead ni endpoints adicionales.
Cada tx del multi-tx cobra un fee que Accesly paga desde el relayer fund. En la práctica una wallet con 4 rules dispara 3 tx (2 batches + finalize) en lugar de 1, pero el usuario nunca paga XLM.
El rol de la passphrase
La passphrase de recovery es la única cosa que el usuario tiene que recordar. Es el gate de seguridad del F3.
Sin la passphrase, la recuperación es imposible. Ni Accesly, ni AWS, ni el integrador tiene forma de derivar la recoveryKey sin conocer la passphrase. Es literalmente PBKDF2 con 600 mil rondas de un secreto que solo el user sabe.
Al momento de creación de wallet, el usuario elige una passphrase. Si la olvida, y además pierde el device, los fondos son inaccesibles. No hay backdoor. Es la contra parte del modelo no custodial. Tu UI tiene que dejarlo brutalmente claro.
Anti abuse en recovery
El endpoint POST /recovery/otp/request tiene rate limiting:
- Máximo 1 OTP cada 60 segundos por email hash.
- Máximo 3 OTPs por hora por email hash.
- Anti enumeración: la respuesta es 200 OK incluso si el email no existe en la base. El atacante no puede usar este endpoint para descubrir cuentas.
Y en POST /recovery/otp/verify:
- Máximo 5 intentos por OTP. Después el row se invalida hasta que TTL lo borre.
- OTP TTL: 10 minutos. Después expira.
- OTP es one shot: al primer verify exitoso el row se elimina.
Diseño del RecoveryFlow del kit
El componente <RecoveryFlow /> del kit maneja los dos caminos con un state machine interno:
import { RecoveryFlow } from '@accesly/react/kit';
import { useNavigate } from 'react-router-dom';
export function RecoverPage() {
const navigate = useNavigate();
return (
<RecoveryFlow
onDone={() => navigate('/wallet')}
onCancel={() => navigate('/')}
/>
);
}
El componente renderiza 4 pantallas: selector de método (email vs Google), input de email, input de OTP + passphrase, y confirmación. Toda la lógica de descargar fragments, reconstruir seed, rotar signer y firmar la nueva config está encapsulada en el hook recovery.finalize.
Casos especiales
Wallet huérfana detectada al signup
Si un usuario intenta hacer signup con un email que ya tiene wallet on chain (típicamente porque su device tenía credential pero se lo borraron, y ahora está en device nuevo), el SDK detecta la situación y devuelve WalletAlreadyExistsError. El kit <CreateWalletFlow /> muestra pantalla "Ya tenés una wallet" con CTA a <RecoveryFlow />.
Recovery múltiple
Un usuario puede hacer recovery cuantas veces necesite. Cada recovery rota el signer on chain. Cada rotación tiene un fee on chain que Accesly cubre con el relayer fund.
Cognito dual users con mismo email
Escenario edge case: un usuario se registró primero con email y password, después olvidó y se registró con Google (mismo email). Cognito los trata como users distintos con sub distintos. El flow de recovery incluye el cognitoSub en el recoveryJwt para asegurar que la rotación afecta exactamente al usuario Cognito autenticado, no a un dual user del mismo email.
Siguientes pasos
- Modelo de amenazas: cómo se protege el flow de recovery contra ataques.
- SDK Reference: props y callbacks del
<RecoveryFlow />. - API Reference: endpoints
/recovery/otp/*,/recovery/finalize,/recovery/rotate-partialy/recovery/finalize-rotation.