Arquitectura
Cómo se compone una wallet Accesly. Tres capas coordinadas para que el usuario firme con biométrico sin que ninguna entidad (ni Accesly, ni AWS, ni el integrador) pueda reconstruir su llave privada.
Vista de alto nivel
La wallet vive en tres lugares al mismo tiempo. Ninguno tiene la llave completa.
┌─────────────────────────────────────────────────────────────┐
│ Dispositivo del usuario (browser) │
│ │
│ IndexedDB Secure Enclave / TPM / StrongBox │
│ ┌──────────┐ ┌─────────────────────┐ │
│ │ F1 enc │ │ Passkey (privkey) │ │
│ │ F2 enc │ │ PRF(salt) => output │ │
│ │ F3 enc │ └─────────────────────┘ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
│ (SDK)
▼
┌─────────────────────────────────────────────────────────────┐
│ Backend Accesly (AWS) │
│ │
│ DynamoDB KMS │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ F2 wrapped │◀────▶│ fragmentsKey (HMAC) │ │
│ │ F3 wrapped │ │ sessionKey (X25519) │ │
│ │ appConfig │ │ relayerFundKey (KMS)│ │
│ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ (Stellar SDK)
▼
┌─────────────────────────────────────────────────────────────┐
│ Stellar Soroban │
│ │
│ Smart Account v3 Verifier contracts │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Owner ed25519 │ │ ed25519 verifier │ │
│ │ Passkey secp256r1│ │ secp256r1 verifier │ │
│ │ Context rules │ │ spending limit │ │
│ └──────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────┘ MPC con Shamir 2 de 3
Cuando el usuario crea su wallet, el SDK genera una llave privada ed25519 dentro del dispositivo. Esa llave nunca sale como está. Se parte en 3 fragmentos usando Shamir Secret Sharing con umbral 2 de 3.
Cada fragmento se cifra con AES 256 GCM y se guarda en un lugar distinto:
| Fragmento | Dónde vive | Llave de cifrado |
|---|---|---|
F1 | IndexedDB del navegador del usuario | HKDF(PRF output, salt, "f1") |
F2 | DynamoDB (envuelto en KMS) | HKDF(PRF output, salt, "f2") |
F3 | DynamoDB (email fragments) | PBKDF2(passphrase de recovery, salt, 600k) |
Para firmar una transacción se necesitan 2 fragmentos. En operación normal se usan F1 (del device) y F2 (del backend). Para recuperación en otro dispositivo se usan F2 y F3 (protegido por la passphrase que el usuario recuerda).
Si fueran solo F1 + F2, un usuario que pierde el device pierde los fondos para siempre. F3 protegido por passphrase da un camino de rescate. Y no puede ser 2 de 2 con F2 y F3 solamente porque el backend tendría acceso pleno si se compromete.
WebAuthn PRF: la llave que nunca se guarda
La parte crítica es qué cifra F1 y F2. La respuesta es: una llave derivada del biométrico del usuario, usando la extensión PRF de WebAuthn.
WebAuthn PRF funciona así:
- El usuario registra un passkey en el sitio. El sistema operativo guarda la llave privada en el chip seguro del dispositivo (Secure Enclave, TPM, StrongBox). Esa llave nunca sale.
- Cuando el sitio necesita una llave derivada, le manda al chip un salt aleatorio.
- El chip pide biométrico al usuario. Si aprueba, el chip calcula HMAC(privkey, salt) y devuelve el output al sitio.
- El sitio hace HKDF(output, encryption_salt, "accesly-f1-encryption") y obtiene la llave AES que cifra F1.
Ese HMAC(privkey, salt) es determinístico. Si el mismo dispositivo con el mismo passkey recibe el mismo salt, siempre devuelve el mismo output. Eso hace posible que el SDK pueda re derivar la llave de cifrado cada vez que necesita descifrar F1.
Un atacante que exfiltra todo el IndexedDB del navegador se lleva ciphertext inútil. Sin el biométrico del usuario en su dispositivo original, el HMAC nunca se puede reproducir. La llave privada del passkey nunca deja el chip.
Compatibilidad de PRF
- Android Chrome / Edge: soporte pleno.
- macOS Safari + iCloud Keychain: soporte pleno desde macOS 14.
- iOS Safari: soporte pleno desde iOS 17.4.
- Windows Chrome / Edge: soporte pleno con TPM 2.0.
- YubiKey y hardware keys físicos: soporte pleno.
Smart Account Soroban v3
La wallet on chain es un contrato Soroban llamado Smart Account v3. Cada usuario tiene el suyo, con su propia address única (C...).
El contrato guarda tres pubkeys al momento del deploy:
- Owner ed25519: la pubkey derivada del seed que Shamir reconstruye. Firma admin ops (rotate signer, upgrade).
- Passkey secp256r1: la pubkey del passkey del usuario. Firma transacciones normales (send, swap).
- Email commitment: hash del email verificado, usado por el flow de recovery.
Context rules
El contrato usa un patrón de context rules que define quién puede firmar qué. Cada operación pasa por una rule antes de ejecutarse.
| Rule | Firma requerida | Uso |
|---|---|---|
biometric-tx | Passkey secp256r1 + spending limit check | Send, swap, receive, transfers día a día |
admin-cfg | Owner ed25519 | Rotate signer, upgrade contract, cambiar rules |
upgrade-rule | Owner ed25519 + timelock 48h | Cambiar el WASM del contrato |
Esta separación hace que operaciones de riesgo alto (rotate signer, upgrade) requieran la seed reconstruida por Shamir, mientras operaciones normales solo requieren el passkey del dispositivo actual.
v3.2.0 rotate particionado
Smart Account v3.2.0 agrega dos funciones nuevas al contrato para wallets con muchas rules activas (4 o más, típicamente users que activaron XLM, USDC y otras policies): rotate_signer_partial(rule_ids, new_owner, new_secp) rota solo el subset de rules pasado en rule_ids, y finalize_rotation(new_owner, new_secp, new_email_commitment) cierra el flow multi tx persistiendo el email commitment y emitiendo SignerRotated. Esta separación existe porque el rotate_signer atómico en wallets grandes escribía más de 132KB en el footprint Soroban y excedía el cap de writeBytes de protocol 27.
El SDK detecta automáticamente cuándo usar cada path: si el rotate atómico rebota con RotateWriteCapExceededError, el hook cae al flow multi tx transparentemente. Wallets con 3 rules o menos siguen atómicas. Ambos paths cierran con la misma semántica: rotar owner + passkey + email commitment.
El flujo end to end
Signup y creación de wallet
- Usuario da signup con email o Google. Cognito emite JWT.
- SDK pide al chip que registre un passkey nuevo. El chip guarda la privkey internamente, devuelve la pubkey secp256r1 al SDK.
- Al registrar el passkey el SDK genera un salt de PRF y lo asocia al credential.
- SDK pide al chip el PRF output usando ese salt. Se deriva la llave AES para F1 y F2.
- SDK genera una seed ed25519 nueva, la parte con Shamir, cifra los 3 fragmentos.
- SDK guarda F1 encrypted en IndexedDB, manda F2 y F3 encrypted al backend.
- Backend arma la tx de deploy del Smart Account con el owner pubkey, passkey pubkey y email commitment. Firma con KMS relayer fund, submitea a Soroban.
- Contract se deploya, el usuario tiene su wallet lista.
Firmar una transacción
- Usuario hace click en "Enviar 10 USDC a Luis".
- Backend arma la tx unsigned. SDK la recibe.
- SDK pide al chip el PRF output. Chip pide biométrico. Usuario aprueba.
- SDK descifra F1 con la llave derivada.
- SDK pide F2 al backend (envuelto en session key X25519 efímera para transporte seguro).
- SDK descifra F2 con la llave derivada.
- SDK reconstruye la seed con Shamir(F1, F2), firma la tx, borra la seed de memoria.
- SDK manda la firma al backend, backend agrega fee bump con KMS, submitea a Soroban.
Toda la reconstrucción de la seed pasa en el device, en JavaScript, dentro de la sesión del usuario. Nunca sale del navegador. Cuando termina la firma, se zeroean los buffers de memoria.
Separación backend
El backend Accesly no puede firmar por el usuario. Lo que sí hace:
- Guarda F2 y F3 cifrados. Son ciphertext opacos sin las llaves del device.
- Envuelve F2 en KMS para reforzar el cifrado at rest (defense in depth).
- Emite JWT de sesión Cognito para autorizar reads de F2.
- Paga el gas de todas las transacciones desde una cuenta relayer administrada (KMS ed25519).
- Indexa historia de transacciones desde Horizon y Mercury Data para queries rápidas.
- Coordina el flujo de recovery emitiendo OTPs por email.
Lo que no hace: reconstruir la seed, firmar en nombre del usuario, o acceder a fondos sin la aprobación biométrica del usuario en su device.
Siguientes pasos
- Modelo de amenazas: qué ataques específicos mitiga cada capa.
- Flujo de recuperación: cómo Shamir 2 de 3 permite recuperación con solo F2 + F3.
- Redes: cómo el mismo backend sirve testnet y mainnet.