# Unforgettable Docs > User-friendly recovery solution based on ERC-7947 for secure, seedless smart account recovery with visual keys(face, objects), short password, and PoW challenge. This file contains all documentation content in a single document following the llmstxt.org standard. ## Architecture & Trust Model import IdealImage from '@site/src/components/IdealImage'; This page explains where Embedded Wallet components live, what each part can access, and the trust assumptions you should design around. ## High-level components - User device (client): runs factor capture + key derivation and signs user-authorized actions. - Smart account (on-chain): holds funds and enforces spending policies and recovery rules. - Passkey (optional, client-side): a WebAuthn/FIDO2 Passkey can be configured as a fast confirmation path so the user can approve actions without re-running the full Unforgettable Key derivation flow each time. - App backend (optional): coordination, notifications, policy evaluation, storage of recovery artifacts (never plaintext keys). - Policy Engine: the rules layer that defines allowed actions and recovery configuration. ## Data placement: what lives where | Data / Capability | On-device | Off-chain backend | On-chain | |---|:--:|:--:|:--:| | Raw biometrics / visual input | ✅ | ❌ | ❌ | | Derived controlling key (plaintext) | ✅ (ephemeral) | ❌ | ❌ | | Smart account code + state | ❌ | ❌ | ✅ | | Recovery configuration (timelocks, thresholds) | ✅ (as UI/config) | ✅ (as config) | ✅ (as enforced rules) | | Passkeys (if enabled) | ✅ (authenticator) | ❌ | ❌ | | Timelocked fallback tx (if enabled) | ❌ | ✅ (stored) | ✅ (executed) | | Guardian approvals (if enabled) | ❌ | optional | ✅ | ## Next - [Smart Account](/embedded-wallets/smart-account) - [Policy Engine](/embedded-wallets/policy-engine) - [Recovery](/embedded-wallets/recovery-overview) --- ## Embedded Wallets Unforgettable Embedded Wallets let your app provision a self-custodial user wallet with seamless onboarding and a familiar, app-native experience. They are built on Just-in-Time (JIT) key infrastructure: a user’s private key is deterministically reconstructed on the device from multiple noisy inputs (such as biometrics) only when a signature is required, used to authorize the action, and destroyed immediately afterward. The key benefits of Embedded Wallets are: - **True self-custody, zero key liability.** Users fully control their wallets; your backend cannot move funds. - **Streamlined onboarding.** No seed phrases, no extensions, just a clean, intuitive flow. - **Phishing-resistant key management.** Nothing to type or copy, and no keys stored at rest—removing most social-engineering attack vectors. ### How it works Let’s say a user wants to transfer stablecoins from their embedded wallet. The flow looks like this: 1. The user logs into your app. 2. Wallet setup or recovery begins. 3. Multi-step, on-device authentication runs using biometric or visual inputs, during which the key is reconstructed. 4. The stablecoin transaction is signed. 5. The key is discarded immediately after use. ## Policy controls Embedded Wallets use a [Policy Engine](/embedded-wallets/policy-engine/) to define and enforce what actions are allowed, when they can occur, and under which conditions. These rules are applied consistently and cannot be bypassed. Policy controls include transaction limits, geofencing, time-based restrictions, and recovery constraints. ## Extra conveniences with Passkeys Embedded Wallets support Passkeys as a convenience feature. Passkeys provide a fast, OS-native confirmation flow, allowing users to approve actions without re-running the full Unforgettable Key derivation each time. Passkeys do not replace the Unforgettable Key. Any action confirmed with a passkey can always be completed by deriving the Unforgettable Key instead. ## What happens when key derivation fails Key derivation mechanisms based on noisy inputs (biometrics, visual keys) are robust to *small* changes but can fail under *large* changes. Unforgettable provides several fallback options to recover access without compromising self-custody. See [Recovery](/embedded-wallets/recovery-overview) for trade-offs and flows. ## Next * [Architecture & Trust Model](/embedded-wallets/architecture) * [Smart Account](/embedded-wallets/smart-account) * [Policy Engine](/embedded-wallets/policy-engine) --- ## Policy Engine The Policy Engine is the rules layer for Embedded Wallets. It defines what actions are allowed, under which conditions, and at what time, so security and recovery logic lives in one place instead of being fragmented across UI code, backend services, and smart contracts. At a high level, the Policy Engine combines two complementary layers: * **Off-chain decisioning** tied to the user’s root account—used for limits, step-up requirements, cooldowns, and auditability. * **On-chain enforcement hooks** that make critical constraints non-bypassable—such as timelocks, approval thresholds, and hard denies. ## What the Policy Engine controls Policies apply to both day-to-day wallet actions and wallet configuration. ### Limits and risk controls * Per-transaction and daily spending limits * Chain-, token-, or contract-level allowlists * When a passkey is acceptable vs full key derivation ### Step-up requirements * When higher-assurance checks (for example, liveness) are enforced * Additional verification for sensitive recovery operations ### Recovery configuration * Timelocks before recovery can execute * Approval thresholds (for example, guardian-based recovery) * Cooldowns, cancellation windows, and validity periods ### Account-abstraction behavior * When gas sponsorship is permitted * Which actions may be batched together * Safety guardrails for relaying and transaction simulation ### Operational constraints * Audit logging and monitoring hooks * Expiration and renewal reminders * Safe-by-default behavior (deny unless explicitly allowed) ## Core policy primitives Policies are composed from a small, explicit set of building blocks: * Limits (amounts and time windows) * Step-up prompts (liveness) * Cooldowns for risky changes * Timelocks for delayed recovery * Thresholds for multi-party approval * Validity and expiration rules * Scope restrictions (chains, tokens, contracts) * Gas sponsorship rules This structure keeps policies readable, auditable, and easy to evolve as wallet requirements change. --- ## Recovery Recovery applies when the user can no longer reproduce the wallet's controlling secret, such as when biometric or visual inputs drift beyond the extractor's tolerance. Even if rare, embedded wallets need a recovery path that preserves self-custody and keeps assets in place. Recovery is implemented through rekeying: the wallet address and on-chain state remain unchanged, control rotates to a new key derived from the user's new embedded wallet, and assets never leave the account. This avoids asset transfers, reduces operational risk, and keeps recovery fully on-chain. ## Choosing a mechanism | Mechanism | What changes | Strengths | Trade-offs | |---|---|---|---| | Timelocked fallback transfer | Control transfer after delay | Works even with zero guardians | Introduces “assisted recovery” ops + compliance surface; funds temporarily leave wallet | | Social recovery (guardians) | Control key rotates | Funds stay in wallet; no single party controls recovery | Coordination risk; guardian compromise/collusion needs mitigation | In practice, many products support both, using guardians as the primary safety net and timelocked fallback as a last resort. ## Option 1: Timelocked fallback transfer (assisted recovery) ### Summary At wallet setup, the user signs a pre-signed recovery transaction that is not submitted immediately. The backend stores it and can only submit it after a timelock (e.g., 90 days) and after a manual identity verification process. A reference UX flow typically includes: - A clear explanation (“you sign a recovery transaction; it’s not submitted now”). - A validity window (e.g., “valid for 90 days”). - A “review & sign” step that highlights “all funds” and “not submitted now.” - A passkey-confirmed action (“Confirm using passkey”). ### Sequence ```mermaid sequenceDiagram participant U as User participant BO as Support / BO participant C as Smart Account participant NW as New Wallet U->>BO: Send pre-signed recovery tx Note over BO: Store tx, executable after timelock U->>BO: Initiate recovery BO->>U: Request identity verification U->>BO: Provide proof BO->>C: Submit pre-signed tx C->>NW: Transfer control Note over NW: Recovery completed ``` ### Key considerations - The platform does not need the user's private keys to execute this flow. - The platform cannot execute before the timelock. - The user should be able to regenerate/rotate the fallback transaction while they still have access (invalidating older artifacts). - This mechanism is subject to policy, legal, and compliance review. ## Option 2: Social recovery (guardians) ### Summary The smart account is configured with a set of guardians (trusted accounts). Recovery requires approvals from a configurable threshold (e.g., 2-of-3, 3-of-5). After threshold approval, the wallet rekeys to a new controlling key derived from the user's new embedded wallet. A reference UX flow typically includes: - Selecting guardians (trusted contacts). - Choosing a threshold (e.g., 2-of-3). - A recovery screen that shows approval status and allows canceling the request. ### Sequence ```mermaid sequenceDiagram participant U as User participant G as Guardians participant C as Smart Account U->>C: Initiate recovery request C-->>G: Notify/request approvals G-->>C: Submit approvals C->>C: Rotate controlling key Note over C: Funds stay in wallet ``` ### Key considerations - No single guardian (or platform party) can unilaterally take control when thresholds are used. - Funds do not leave the wallet during recovery; only the controlling key rotates. - Security increases with more diverse guardians, but coordination becomes harder. - Guardian compromise/collusion remains a residual risk; mitigate with thresholds, delays, and monitoring. --- ## Smart Account A Smart Account is the on-chain smart-contract wallet used by Unforgettable Embedded Wallets. It holds user assets and enforces programmable rules on-chain and is controlled by the Unforgettable Key. The Smart Account supports account abstraction patterns such as gasless (sponsored) transactions, batched execution, and custom validation logic via ERC-4337. ## Action authorization flow At a high level, an action is executed only when all required checks pass: 1. Unforgettable Key authorizes the action (user’s primary control). Authorization can happen by deriving the key on demand, or (if enabled) by using a passkey as a fast confirmation path without re-running the full derivation flow. 2. Policy Engine approves it (rules, limits, recovery constraints). ## Passkeys Smart Accounts support passkeys (WebAuthn/FIDO2) as a convenience for confirming actions. A passkey enables fast, OS-native approval on a trusted device, avoiding repeated Unforgettable Key derivation for common flows such as small transfers. Passkeys never replace the Unforgettable Key: they are only a shortcut for confirmation. Any action approved with a passkey can always be completed by deriving the Unforgettable Key instead, which remains the primary authority. If a passkey is unavailable, the user proceeds via Unforgettable Key derivation without loss of functionality. --- ## Benchmarks This page summarizes benchmark results for Unforgettable recovery factors across face, objects, location, and voice. ## Metrics - *FAR (False Acceptance Rate):* probability an incorrect user/sample is accepted. - *FRR (False Rejection Rate):* probability a correct user/sample is rejected during recovery. ## Evaluation | Category | Security bits | FAR | FRR | Train dataset | Test dataset | |---|---:|---:|---:|---|---| | Face recognition | ~25–30 | 2-25 to 2-30 | ~1% | MS1MV2 | MS1MV2_10k, LFW + custom | | Object recognition (Universal Vision) | ~20–40 | TBD | TBD | LVD-1689M | ImageNet + custom | | Location (Sphere Grid) | Up to 40 | 2-40 | Depends on device location module + GPS service | Not required | Not required | | Voice recognition | ~5–8 | ~1% | ~1% | EER/minDCF | Custom (WIP) | ## Factors ### 1. Face recognition The face extractor uses MagFace with a custom co-training upgrade by the Rarimo team. The probability of a false detection (a different person extracts the same key) is around *2-30* (negligible), while false rejection is around *1%* (≈ one login error per 100 attempts). Training used MS1MV2, with validation on LFW and additional custom data. To reduce false rejections, we apply preprocessing such as image quality checks and positioning guidance. A liveness step gates key extraction until the user proves they are live, mitigating attacks using manipulated biometric samples. ### 2. Object recognition The object extractor is based on Meta's DinoV3, trained on LVD-1689M and validated on ImageNet + a custom object dataset. Additional experiments are in progress; FAR/FRR numbers are currently marked as under tests. ### 3. Location The location factor uses Google's S2 Geometry to provide a stable location source. For the factor to be recoverable, the user's location accuracy needs to be within roughly a 10-meter range. ### 4. Voice recognition The voice extractor uses ECAPA-TDNN, with false-acceptance and false-rejection rates under 1%. It is trained on the EER/minDCF dataset and is being validated on additional datasets. ## Runtime considerations All components can run client-side, but all extractors except location may require significant storage and can take *dozens of seconds* in-browser. Running extractors inside a TEE can reduce execution time to about *0.1 seconds* per extractor. --- ## What is Unforgettable? import OutLink from "@site/src/components/OutLink"; import IdealImage from '@site/src/components/IdealImage'; # Fuzzy Extractors Unforgettable relies on a novel cryptographic primitive called *fuzzy extractors*, which convert “noisy” biometric/visual data into a stable, private key that can be reproduced from a close‑enough reading of the same object. Security is layered by combining factors (e.g., face + object) and strengthened further with a password or a proof-of-work challenge. ### Security assumptions To achieve a 112‑bit security baseline, Unforgettable fuses ≈46 bits of entropy extracted from a face‑plus‑object pair with an additional 66 bits contributed by either a short password alone or a shorter password reinforced by an optional 2¹⁶‑space proof‑of‑work challenge. | Factor | Entropy usable today | Notes | |-------------------|-----------------------|-------| | Face | ≈14-20 bits | FAR ≈ 2⁻²⁰–2⁻²¹ | | Generic object | ≈40 bits | Image distinguishing points | | Location | Up to 40 bits | 10-meter range accuracy required | | Face + object | ≈54 bits | Independent sources add up | | Password | ≈49 bits (8 chars) | ~6.12 bits per printable ASCII char | | PoW Challenge | ≈6 bits | 2⁶ space | | **Total** | ≈112 bits | ≥112-bit baseline satisfied | Further improvements are being researched: - **Model upgrades:** Moving to 512‑D face embeddings and “specific‑object” extractors is expected to yield 40-60 bits per object and ≥ 70 bits from a face + object pair. - **Shorter secrets:** With 70 visual bits, the password can drop to 7 random characters, or 5 with the same PoW range, while preserving 112‑bit security. - **New modalities:** Voice prints, 3-D scans and other high‑entropy sources are being researched to reduce user effort further. --- ## Architecture Overview # Architecture Overview Understand the high-level architecture and design principles of the Unforgettable SDK. ## System Architecture ```mermaid graph TB App[Your Application] SDK[Unforgettable SDK] UnfApp[Unforgettable.app] API[Unforgettable API] App -->|1. Initialize| SDK SDK -->|2. Generate Key Pair| SDK SDK -->|3. Create Recovery URL| App App -->|4. Display QR/Link| User User -->|5. Scan/Open| UnfApp UnfApp -->|6. Recover key| User UnfApp -->|7. Encrypt & Send| API SDK -->|8. Poll| API API -->|9. Return Encrypted Data| SDK SDK -->|10. Decrypt| SDK SDK -->|11. Return Key| App ``` ## Components ### 1. Your Application Your application integrates the SDK and is responsible for: - Initializing the SDK with appropriate parameters - Displaying the recovery URL (as QR code or link) - Handling the recovered private key - Creating/restoring user wallets or accounts ### 2. Unforgettable SDK The client-side SDK running in your application that: - **Generates Cryptographic Keys**: Creates an X25519 key pair for secure communication - **Builds Recovery URLs**: Composes URLs with embedded parameters - **Polls for Data**: Periodically checks the API for recovered keys - **Decrypts Data**: Decrypts the recovered key using the private key ### 3. Unforgettable.app The web application where users complete the recovery process: - Parses recovery URL parameters - Authenticates users with selected factors (Face, Image, Password) - Retrieves the stored recovery key from secure storage - Encrypts the key with the public key from the URL - Sends encrypted data to the API ### 4. Unforgettable API The backend service that: - Acts as a temporary encrypted data transfer mechanism - Stores encrypted recovery keys (cannot decrypt them) - Serves encrypted data to the SDK upon request - Implements rate limiting and security measures ## Security Model ### End-to-End Encryption ```mermaid sequenceDiagram participant SDK as SDK (Your App) participant API as Unforgettable API participant UnfApp as Unforgettable.app Note over SDK: Generate X25519 Key Pair SDK->>SDK: Private Key (stays local) SDK->>SDK: Public Key (embedded in URL) SDK->>UnfApp: Recovery URL with Public Key Note over UnfApp: User authenticates UnfApp->>UnfApp: Retrieve recovery key UnfApp->>UnfApp: Encrypt with Public Key UnfApp->>API: Send encrypted data Note over API: Store encrypted data(cannot decrypt) SDK->>API: Poll for data API->>SDK: Return encrypted data SDK->>SDK: Decrypt with Private Key Note over SDK: Recovery key available ``` **Key Points:** - The API never has access to decryption keys - Private keys never leave the originating device - Even if API is compromised, data remains encrypted ### Data Flow 1. **Setup Phase** - SDK generates ephemeral X25519 key pair - Public key embedded in recovery URL - Private key stored in memory (never persisted) 2. **Recovery Phase** - User authenticates on Unforgettable.app - App encrypts recovery key with SDK's public key - Encrypted data sent to API with unique transfer ID 3. **Retrieval Phase** - SDK polls API with transfer ID - Receives encrypted data - Decrypts using private key - Returns plaintext recovery key to application ## Design Principles ### 1. Zero-Trust Architecture - No component can access unencrypted recovery keys except the endpoints - API is an untrusted intermediary - Cryptographic guarantees over operational security ### 2. Separation of Concerns ``` ┌─────────────────────────────────────────────────────────────┐ │ Application Layer │ │ - Business logic │ │ - Wallet management │ │ - User interface │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ SDK Layer │ │ - Cryptography │ │ - URL generation │ │ - API communication │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Protocol Layer │ │ - HTTP/HTTPS transport │ │ - JSON serialization │ │ - Error handling │ └─────────────────────────────────────────────────────────────┘ ``` ### 3. Stateless SDK - Each SDK instance is independent - No persistent storage required - Fresh encryption key pair for each recovery session - Prevents key reuse vulnerabilities ### 4. Fail-Safe Defaults - Secure defaults for all configurations - Explicit opt-in for permissive settings - Timeout mechanisms prevent indefinite waiting - Clear error messages for debugging ## Recovery Factors The SDK supports multiple [recovery factors](../api/recovery-factors.md). Applications can specify which factors to support: ```typescript // Multiple factors factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password] // Single factor factors: [RecoveryFactor.Face] // No factors specified (user chooses from all available) factors: [] ``` ## Data Transfer Protocol ### Transfer Lifecycle ```mermaid stateDiagram-v2 [*] --> Created: SDK generates transfer ID Created --> Pending: URL generated Pending --> Completed: User authenticates Pending --> Expired: Timeout Completed --> Retrieved: SDK fetches data Retrieved --> [*] Expired --> [*] ``` ### Transfer States 1. **Created**: Transfer ID generated, waiting for URL to be shared 2. **Pending**: URL shared, waiting for user to complete factors 3. **Completed**: User completed factors, encrypted data available 4. **Retrieved**: SDK successfully fetched and decrypted data 5. **Expired**: Transfer exceeded time-to-live (TTL) ## Next Steps - [Encryption Details](/sdk/advanced/encryption) - Deep dive into cryptography - [Data Transfer](/sdk/advanced/data-transfer) - API communication details - [URL Generation](/sdk/advanced/url-generation) - URL parameter specifications --- ## Data Transfer # Data Transfer Learn how encrypted data is transferred between your application and Unforgettable.app. ## Overview The data transfer mechanism is a temporary, encrypted messaging system that enables secure key exchange between your application and Unforgettable.app without exposing sensitive data to the API. ## Transfer Lifecycle ```mermaid sequenceDiagram participant App as Your App (SDK) participant API as Unforgettable API participant UnfApp as Unforgettable.app Note over App: Generate Transfer ID (UUID) App->>API: Check if ID exists (optional) App->>UnfApp: Share Recovery URL(contains Transfer ID) UnfApp->>UnfApp: User completes factors UnfApp->>UnfApp: Encrypt recovery key UnfApp->>API: POST /data-transfers/{id}Encrypted payload API->>API: Store encrypted data loop Polling (every 3-5 seconds) App->>API: GET /data-transfers/{id} alt Data available API->>App: Return encrypted data App->>App: Decrypt with private key Note over App: Recovery complete else Data not ready API->>App: 404 Not Found end end ``` ## Transfer ID ### Generation The transfer ID is a UUID v4 generated by the SDK: ```typescript import { v4 as uuid } from 'uuid' const dataTransferId = uuid() // Example: "550e8400-e29b-41d4-a716-446655440000" ``` **Properties:** - **Format**: UUID v4 (RFC 4122) - **Length**: 36 characters (32 hex + 4 hyphens) - **Uniqueness**: ~5.3×10³⁶ possible values - **Collision Probability**: Negligible ### Purpose - Uniquely identifies each recovery session - Links SDK instance to Unforgettable.app session - Enables API to route encrypted data correctly ## API Endpoints ### Base URL ``` https://api.unforgettable.app ``` ### Endpoints #### GET /integrations/helper-keeper/v1/public/data-transfers/\{id\} Retrieve encrypted data for a transfer. **Request:** ```http GET /integrations/helper-keeper/v1/public/data-transfers/550e8400-e29b-41d4-a716-446655440000 Accept: application/json ``` **Response (Success):** ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "data": "base64url_encoded_encrypted_payload" } } ``` **Response (Not Found):** ```http HTTP/1.1 404 Not Found ``` #### POST /integrations/helper-keeper/v1/public/data-transfers/\{id\} Create a new data transfer (called by Unforgettable.app). **Request:** ```http POST /integrations/helper-keeper/v1/public/data-transfers/550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json { "data": "base64url_encoded_encrypted_payload" } ``` **Response:** ```json { "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "data": "base64url_encoded_encrypted_payload" } } ``` ## Polling Strategy ### Basic Polling ```typescript async function pollForRecoveryKey( sdk: UnforgettableSdk, interval: number = 3000, maxAttempts: number = 60 ): Promise { let attempts = 0 while (attempts new Promise(resolve => setTimeout(resolve, ms)) ``` ### Exponential Backoff (Advanced) For more efficient polling with reduced server load: ```typescript async function pollWithBackoff( sdk: UnforgettableSdk, initialInterval: number = 1000, maxInterval: number = 10000, maxAttempts: number = 60 ): Promise { let attempts = 0 let interval = initialInterval while (attempts { abortController.abort() }) while (!abortController.signal.aborted) { await poll() } ``` ## Monitoring and Debugging ### Logging Log polling attempts for debugging: ```typescript console.log(`[Poll #${attempts}] Checking for data...`) console.log(`Transfer ID: ${dataTransferId}`) console.log(`Elapsed: ${Date.now() - startTime}ms`) ``` ### Metrics Track key metrics: ```typescript const metrics = { totalAttempts: 0, successTime: 0, failureRate: 0, averageLatency: 0, } // Update on each poll metrics.totalAttempts++ metrics.averageLatency = (metrics.averageLatency + latency) / 2 ``` ## Next Steps - [URL Generation](/sdk/advanced/url-generation) - Recovery URL structure - [Architecture](/sdk/advanced/architecture) - System overview - [API Reference](/sdk/api/unforgettable-sdk) - SDK methods --- ## Encryption # Encryption Deep dive into the cryptographic primitives and protocols used by Unforgettable SDK. ## Overview The SDK uses industry-standard cryptographic algorithms to ensure end-to-end encryption of recovery keys: - **Key Exchange**: X25519 (Curve25519 ECDH) - **Encryption**: ChaCha20-Poly1305 AEAD - **Key Derivation**: HKDF with SHA-256 - **Encoding**: Base64 URL-safe encoding ## Cryptographic Primitives ### X25519 Key Exchange X25519 is a high-performance elliptic curve Diffie-Hellman (ECDH) function built on Curve25519. **Properties:** - **Key Size**: 32 bytes (256 bits) - **Security Level**: ~128-bit security - **Performance**: Fast on modern CPUs - **Side-Channel Resistance**: Designed to resist timing attacks **Key Generation:** ```typescript // Generate random private key (32 bytes) const privateKey = crypto.getRandomValues(new Uint8Array(32)) // Derive public key const publicKey = x25519.getPublicKey(privateKey) ``` **Shared Secret Computation:** ```typescript // Receiver's perspective const sharedSecret = x25519.getSharedSecret( receiverPrivateKey, senderPublicKey ) // Sender's perspective const sharedSecret = x25519.getSharedSecret( senderPrivateKey, receiverPublicKey ) ``` ### ChaCha20-Poly1305 AEAD ChaCha20-Poly1305 is an Authenticated Encryption with Associated Data (AEAD) cipher. **Components:** - **ChaCha20**: Stream cipher for encryption - **Poly1305**: MAC for authentication - **Combined**: Provides confidentiality and authenticity **Properties:** - **Nonce Size**: 12 bytes (96 bits) - **Key Size**: 32 bytes (256 bits) - **Tag Size**: 16 bytes (128 bits) - **Performance**: Excellent software performance **Encryption:** ```typescript const cipher = chacha20poly1305(encryptionKey, nonce) const ciphertext = cipher.encrypt(plaintext) // ciphertext includes 16-byte authentication tag ``` **Decryption:** ```typescript const cipher = chacha20poly1305(encryptionKey, nonce) const plaintext = cipher.decrypt(ciphertext) // Throws if authentication fails ``` ### HKDF Key Derivation HKDF (HMAC-based Key Derivation Function) derives encryption keys from shared secrets. **Properties:** - **Hash Function**: SHA-256 - **Output Size**: 32 bytes (256 bits) - **Info String**: "unforgettable-encryption" **Derivation:** ```typescript const encryptionKey = hkdf( sha256, // Hash function sharedSecret, // Input key material undefined, // Salt (none) infoBytes, // Context string 32 // Output length ) ``` ## Encryption Protocol ### Data Format Encrypted data is structured as: ``` [32 bytes ephemeral public key][12 bytes nonce][ciphertext + 16 bytes tag] ``` ### Encryption Process ```mermaid flowchart TD A[Plaintext Recovery Key] --> B[Generate Ephemeral Key Pair] B --> C[Compute Shared Secretvia X25519 ECDH] C --> D[Derive Encryption Keyvia HKDF-SHA256] D --> E[Generate Random Nonce12 bytes] E --> F[Encrypt with ChaCha20-Poly1305] F --> G[Combine: Ephemeral PK + Nonce + Ciphertext] G --> H[Base64 URL Encode] H --> I[Encrypted Data] ``` **Step-by-Step:** 1. **Generate Ephemeral Key Pair** ```typescript const ephemeralPrivateKey = randomBytes(32) const ephemeralPublicKey = x25519.getPublicKey(ephemeralPrivateKey) ``` 2. **Compute Shared Secret** ```typescript const sharedSecret = x25519.getSharedSecret( ephemeralPrivateKey, recipientPublicKey ) ``` 3. **Derive Encryption Key** ```typescript const encryptionKey = hkdf( sha256, sharedSecret, undefined, stringToBytes('unforgettable-encryption'), 32 ) ``` 4. **Generate Nonce** ```typescript const nonce = randomBytes(12) ``` 5. **Encrypt Data** ```typescript const cipher = chacha20poly1305(encryptionKey, nonce) const ciphertextWithTag = cipher.encrypt(plaintextBytes) ``` 6. **Combine Components** ```typescript const combined = new Uint8Array([ ...ephemeralPublicKey, // 32 bytes ...nonce, // 12 bytes ...ciphertextWithTag // variable + 16 bytes tag ]) ``` 7. **Encode** ```typescript const encoded = bytesToBase64Url(combined) ``` ### Decryption Process ```mermaid flowchart TD A[Encrypted Data] --> B[Base64 URL Decode] B --> C[Extract Ephemeral Public Keybytes 0-31] C --> D[Extract Noncebytes 32-43] D --> E[Extract Ciphertextbytes 44+] E --> F[Compute Shared Secretvia X25519 ECDH] F --> G[Derive Encryption Keyvia HKDF-SHA256] G --> H[Decrypt with ChaCha20-Poly1305] H --> I[Verify Authentication Tag] I --> J[Plaintext Recovery Key] ``` **Step-by-Step:** 1. **Decode** ```typescript const combined = base64UrlToBytes(encryptedData) ``` 2. **Extract Components** ```typescript const ephemeralPublicKey = combined.slice(0, 32) const nonce = combined.slice(32, 44) const ciphertextWithTag = combined.slice(44) ``` 3. **Compute Shared Secret** ```typescript const sharedSecret = x25519.getSharedSecret( receiverPrivateKey, ephemeralPublicKey ) ``` 4. **Derive Encryption Key** ```typescript const encryptionKey = hkdf( sha256, sharedSecret, undefined, stringToBytes('unforgettable-encryption'), 32 ) ``` 5. **Decrypt and Verify** ```typescript const cipher = chacha20poly1305(encryptionKey, nonce) const plaintext = cipher.decrypt(ciphertextWithTag) // Throws error if authentication tag is invalid ``` ## Security Properties ### Confidentiality - **Forward Secrecy**: Each transfer uses fresh ephemeral keys - **Quantum Resistance**: Limited (classical ECDH) - **Key Isolation**: Private keys never transmitted ### Authentication - **AEAD**: Poly1305 MAC prevents tampering - **Public Key**: Embedded in URL, trusted by recipient - **Integrity**: Any modification causes decryption failure ### Randomness - **Nonces**: Cryptographically random, never reused - **Keys**: Generated using platform crypto API - **Ephemeral**: New keys for each encryption ## Implementation Details ### Random Number Generation import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```typescript const randomBytes = (length: number): Uint8Array => { return crypto.getRandomValues(new Uint8Array(length)) } ``` ```typescript import { randomBytes } from 'crypto' const bytes = randomBytes(32) ``` ```kotlin import java.security.SecureRandom val random = SecureRandom() val bytes = ByteArray(32) random.nextBytes(bytes) ``` ```swift import Security var bytes = [UInt8](repeating: 0, count: 32) let status = SecRandomCopyBytes( kSecRandomDefault, bytes.count, &bytes ) ``` ### Base64 URL Encoding Standard Base64 with URL-safe modifications: ```typescript function bytesToBase64Url(bytes: Uint8Array): string { const b64 = btoa(String.fromCharCode(...bytes)) return b64 .replace(/\+/g, '-') // Replace + with - .replace(/\//g, '_') // Replace / with _ .replace(/=+$/, '') // Remove padding } function base64UrlToBytes(base64Url: string): Uint8Array { let b64 = base64Url .replace(/-/g, '+') .replace(/_/g, '/') // Add padding while (b64.length % 4) b64 += '=' const binary = atob(b64) return new Uint8Array( binary.split('').map(c => c.charCodeAt(0)) ) } ``` ## Security Considerations ### Key Management **DO:** - ✅ Generate fresh keys for each session - ✅ Use platform-provided crypto APIs - ✅ Clear keys from memory after use - ✅ Never log private keys **DON'T:** - ❌ Reuse ephemeral keys - ❌ Store private keys on disk - ❌ Implement custom crypto - ❌ Use weak random number generators ### Attack Resistance **Protected Against:** - ✅ Man-in-the-Middle (MITM) - ✅ Replay Attacks - ✅ Tampering - ✅ Known-plaintext attacks **Limitations:** - ⚠️ Requires secure URL transmission - ⚠️ Vulnerable if recipient device compromised - ⚠️ No post-quantum cryptography (yet) ## Performance ### Benchmarks Typical performance on modern hardware: | Operation | Time | Throughput | |-----------|------|------------| | Key Generation | ~0.1ms | 10,000 ops/s | | ECDH | ~0.1ms | 10,000 ops/s | | Encryption (1KB) | ~0.05ms | 20MB/s | | Decryption (1KB) | ~0.05ms | 20MB/s | ### Optimization Tips 1. **Batch Operations**: Generate multiple keys at once if needed 2. **Worker Threads**: Offload crypto to web workers/background threads 3. **Caching**: Cache derived keys (with caution) 4. **Platform APIs**: Use native implementations when available ## Standards Compliance - **RFC 7748**: Elliptic Curves for Security (X25519) - **RFC 7539**: ChaCha20 and Poly1305 - **RFC 5869**: HKDF - **RFC 4648**: Base64 URL encoding ## Next Steps - [Data Transfer](/sdk/advanced/data-transfer) - How encrypted data moves - [URL Generation](/sdk/advanced/url-generation) - Recovery URL structure - [API Reference](/sdk/api/unforgettable-sdk) - SDK methods --- ## URL Generation # URL Generation Learn how recovery URLs are constructed and what each parameter means. ## URL Structure Recovery URLs follow this structure: ``` https://unforgettable.app/sdk/{mode}#{parameters} ``` ### Components | Component | Description | Example | |-----------|-------------|---------| | Base URL | Unforgettable app URL | `https://unforgettable.app/sdk` | | Mode Path | `/c` (create) or `/r` (restore) | `/c` | | Hash Fragment | URL-encoded parameters | `#id=...&epk=...&f=...` | ### Complete Example **Create Mode:** ``` https://unforgettable.app/sdk/c#id=550e8400-e29b-41d4-a716-446655440000&epk=yL8bV...&f=1,2,3&g=my-app ``` **Restore Mode:** ``` https://unforgettable.app/sdk/r#id=550e8400-e29b-41d4-a716-446655440000&epk=yL8bV...&f=1&wa=0x1234...&g=my-app ``` ## URL Parameters Parameters are passed in the hash fragment as URL-encoded key-value pairs. ### Standard Parameters #### `id` (Required) **Description**: Unique transfer identifier (UUID v4) **Format**: UUID string ``` id=550e8400-e29b-41d4-a716-446655440000 ``` **Purpose**: - Links SDK instance to recovery session - Used to retrieve encrypted data from API - Must be globally unique #### `epk` (Required) **Description**: Encryption Public Key **Format**: Base64 URL-encoded X25519 public key (32 bytes) ``` epk=yL8bV5c4pEqFk2_GxPbN1mQvXhD4wZKj8RtY3nL9cWo ``` **Purpose**: - Enables Unforgettable.app to encrypt recovery key - Generated fresh for each session - 44 characters (base64url encoding of 32 bytes) #### `f` (Optional) **Description**: [Recovery Factors](../api/recovery-factors.md) **Format**: Comma-separated list of factor IDs ``` f=1,2,3 ``` **Purpose**: - Pre-selects available recovery factors - If omitted, user chooses from all factors - Multiple factors = user picks one **Examples**: ``` f=1 # Face only f=1,3 # Face and Password f=1,2,3 # All factors (omitted) # User chooses factors ``` #### `wa` (Conditional) **Description**: Wallet Address **Format**: Ethereum-style address (0x-prefixed hex) ``` wa=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb ``` **Purpose**: - Required for restore mode - Identifies which recovery key to retrieve - Not used in create mode **Validation**: - Must start with `0x` - 42 characters total (0x + 40 hex digits) - Case-insensitive #### `g` (Optional) **Description**: Group Identifier **Format**: URL-safe string ``` g=my-wallet-app ``` **Purpose**: - Organizes recovery keys by application - Helps users identify which app the recovery is for **Best Practices**: - Use app name or identifier - Keep it short and recognizable - URL-encode if contains special characters ### Custom Parameters Applications can add custom parameters for application-specific needs. **Format**: Any key not reserved (`id`, `epk`, `f`, `wa`, `g`) ``` theme=dark&lang=en&version=2.0 ``` **Examples**: ```typescript const sdk = new UnforgettableSdk({ mode: 'create', customParams: { theme: 'dark', lang: 'en', }, }) ``` **Resulting URL:** ``` https://unforgettable.app/sdk/c#id=...&epk=...&theme=dark&lang=en ``` ## Parameter Encoding ### URL Encoding Parameters are URL-encoded following RFC 3986. **Characters that need encoding:** ``` Space → %20 : → %3A / → %2F ? → %3F # → %23 & → %26 = → %3D ``` **Base64 URL-Safe Encoding:** The `epk` parameter uses base64url encoding (RFC 4648): - `+` → `-` - `/` → `_` - Remove padding `=` **Example:** ```typescript // Standard Base64: "abc+def/ghi=" // Base64 URL: "abc-def_ghi" ``` ## Generation Process ### Step-by-Step ```typescript import { UnforgettableSdk } from '@rarimo/unforgettable-sdk' // 1. Initialize SDK const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], group: 'my-app', customParams: { theme: 'dark' }, }) // 2. SDK generates internal components // - Transfer ID: uuid() // - Key pair: generateDataTransferKeyPair() // 3. SDK builds URL const recoveryUrl = await sdk.getRecoveryUrl() // Result: "https://unforgettable.app/sdk/c#id=...&epk=...&f=1,3&g=my-app&theme=dark" ``` ### Internal Implementation ```typescript function composeUnforgettableLocationHash(params: UnforgettablePathParams): string { const searchParams = new URLSearchParams() // Required parameters searchParams.set('id', params.dataTransferId) searchParams.set('epk', params.encryptionPublicKey) // Optional: Recovery factors if (params.factors.length > 0) { searchParams.set('f', params.factors.join(',')) } // Conditional: Wallet address (restore mode) if (params.walletAddress) { searchParams.set('wa', params.walletAddress) } // Optional: Group if (params.group) { searchParams.set('g', params.group) } // Custom parameters if (params.customParams) { for (const [key, value] of Object.entries(params.customParams)) { // Skip reserved keys if (!['id', 'epk', 'f', 'wa', 'g'].includes(key)) { searchParams.set(key, value) } } } return `#${searchParams.toString()}` } ``` ## URL Parsing Unforgettable.app parses the URL to extract parameters: ```typescript function parseUnforgettableLocationHash(hash: string): UnforgettablePathParams { // Remove leading '#' const rawParams = hash.startsWith('#') ? hash.slice(1) : hash const searchParams = new URLSearchParams(rawParams) // Extract standard parameters const dataTransferId = searchParams.get('id') || '' const encryptionPublicKey = searchParams.get('epk') || '' const factors = parseFactors(searchParams.get('f') || '') const walletAddress = searchParams.get('wa') ?? undefined const group = searchParams.get('g') ?? undefined // Extract custom parameters const customParams: Record = {} const reservedKeys = ['id', 'epk', 'f', 'wa', 'g'] for (const [key, value] of searchParams.entries()) { if (!reservedKeys.includes(key)) { customParams[key] = value } } // Validate required parameters if (!dataTransferId || !encryptionPublicKey) { throw new Error('Invalid recovery URL: missing required parameters') } return { dataTransferId, encryptionPublicKey, factors, walletAddress, group, ...(Object.keys(customParams).length > 0 && { customParams }), } } ``` ## Validation ### SDK Validation The SDK validates parameters before generating URLs: ```typescript // Validate mode if (!['create', 'restore'].includes(options.mode)) { throw new Error('Invalid mode: must be "create" or "restore"') } // Validate wallet address (restore mode) if (options.mode === 'restore' && !options.walletAddress) { throw new Error('Wallet address required for restore mode') } // Validate recovery factors const validFactors = [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password] for (const factor of options.factors) { if (!validFactors.includes(factor)) { throw new Error(`Invalid recovery factor: ${factor}`) } } ``` ### App Validation Unforgettable.app validates received parameters: ```typescript // Validate transfer ID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i if (!uuidRegex.test(dataTransferId)) { throw new Error('Invalid transfer ID format') } // Validate public key length const publicKeyBytes = base64UrlToBytes(encryptionPublicKey) if (publicKeyBytes.length !== 32) { throw new Error('Invalid public key length') } // Validate wallet address format (if present) if (walletAddress && !/^0x[0-9a-fA-F]{40}$/.test(walletAddress)) { throw new Error('Invalid wallet address format') } ``` ## Security Considerations ### Parameter Tampering **Risk**: Malicious actors modifying URL parameters **Mitigation**: - Encryption key embedded in URL - Even if transfer ID is stolen, cannot decrypt data - Wallet address validation on Unforgettable.app ### URL Exposure **Risk**: Recovery URLs contain sensitive transfer information **Best Practices**: - Display URLs as QR codes (not clickable links) - Use HTTPS for URL transmission - Implement URL expiration (24-hour TTL) - Don't log full URLs ### Custom Parameters **Risk**: Custom parameters might leak sensitive data **Best Practices**: ```typescript // Good: Non-sensitive data customParams: { theme: 'dark', lang: 'en' } // Bad: Sensitive data customParams: { apiKey: 'secret', userId: '12345' } ``` ## URL Length Considerations ### Maximum Lengths | Browser | Max URL Length | |---------|----------------| | Chrome | ~2MB | | Firefox | ~65,536 chars | | Safari | ~80,000 chars | | Edge | ~2MB | ### Typical URL Length ``` Base URL: 31 chars (https://unforgettable.app/sdk/c) Hash symbol: 1 char (#) id parameter: 42 chars (id=uuid) epk parameter: 48 chars (epk=base64url) f parameter: 10 chars (f=1,2,3) g parameter: 20 chars (g=my-app) Custom params: 50 chars (varies) --- Total: ~200 chars ``` ### QR Code Capacity QR codes have limited data capacity: | Version | Error Correction | Max Alphanumeric | |---------|------------------|------------------| | 10 | L (7%) | 395 chars | | 15 | L (7%) | 580 chars | | 20 | L (7%) | 858 chars | **Recommendations**: - Keep URLs under 300 characters for reliable QR codes - Use short group names - Minimize custom parameters ## Examples ### Minimal URL (Create Mode) ``` https://unforgettable.app/sdk/c#id=a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7&epk=yL8bV5c4pEqFk2_GxPbN1mQvXhD4wZKj8RtY3nL9cWo ``` ### Full Featured URL (Create Mode) ``` https://unforgettable.app/sdk/c#id=a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7&epk=yL8bV5c4pEqFk2_GxPbN1mQvXhD4wZKj8RtY3nL9cWo&f=1,2,3&g=my-wallet&theme=dark&lang=en ``` ### Restore Mode URL ``` https://unforgettable.app/sdk/r#id=a1b2c3d4-e5f6-4789-a0b1-c2d3e4f5a6b7&epk=yL8bV5c4pEqFk2_GxPbN1mQvXhD4wZKj8RtY3nL9cWo&f=1&wa=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb&g=my-wallet ``` ## Next Steps - [Architecture](/sdk/advanced/architecture) - System overview - [Encryption](/sdk/advanced/encryption) - Cryptography details - [API Reference](/sdk/api/unforgettable-sdk) - SDK methods --- ## Error Reference # Error Reference Complete reference for error types and error handling in Unforgettable SDK. ## Error Types ### NotFoundError Data transfer not found or not yet available. **When thrown:** - User hasn't completed recovery process - Transfer ID doesn't exist - Data expired (past TTL) import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```typescript import { NotFoundError } from '@rarimo/unforgettable-sdk' try { const key = await sdk.getRecoveredKey() } catch (error) { if (error instanceof NotFoundError) { console.log('Data not ready yet, continue polling') } } ``` ```kotlin try { val key = sdk.getRecoveredKey() } catch (e: UnforgettableSDKError.NotFound) { println("Data not ready yet") } ``` ```swift do { let key = try await sdk.getRecoveredKey() } catch let error as UnforgettableSDKError { if case .notFound = error { print("Data not ready yet") } } ``` **Recommended Action:** Continue polling --- ### NetworkError Network connectivity or HTTP request failed. **When thrown:** - No internet connection - API server unreachable - Request timeout - HTTP error codes (500, 503, etc.) ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { if (error.name === 'NetworkError') { console.error('Network issue:', error.message) // Show retry button to user } } ``` ```kotlin try { val key = sdk.getRecoveredKey() } catch (e: UnforgettableSDKError.NetworkError) { println("Network error: ${e.cause}") } ``` ```swift do { let key = try await sdk.getRecoveredKey() } catch let error as UnforgettableSDKError { if case .networkError(let underlying) = error { print("Network error: \(underlying)") } } ``` **Recommended Action:** Retry with exponential backoff --- ### CryptoError Cryptographic operation failed. **When thrown:** - Key generation failed - Encryption failed - Decryption failed - Invalid encryption key - Malformed encrypted data ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { if (error.name === 'CryptoError') { console.error('Decryption failed:', error.message) // Data may be corrupted, abort } } ``` ```kotlin try { val sdk = UnforgettableSDK(options) } catch (e: CryptoError.KeyGenerationFailed) { println("Failed to generate keys") } try { val data = sdk.getRecoveredData() } catch (e: UnforgettableSDKError.CryptoError) { println("Decryption failed: ${e.cause}") } ``` ```swift do { let sdk = try UnforgettableSDK(options: options) } catch let error as CryptoError { if case .keyGenerationFailed = error { print("Key generation failed") } } do { let data = try await sdk.getRecoveredData() } catch let error as UnforgettableSDKError { if case .cryptoError(let underlying) = error { print("Crypto error: \(underlying)") } } ``` **Recommended Action:** Abort and restart recovery process --- ### InvalidResponse API returned unexpected or malformed response. **When thrown:** - Response doesn't match expected schema - Missing required fields - Invalid JSON ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { if (error.name === 'InvalidResponse') { console.error('API returned invalid data') } } ``` ```kotlin try { val data = sdk.getRecoveredData() } catch (e: UnforgettableSDKError.InvalidResponse) { println("Invalid API response") } ``` ```swift do { let data = try await sdk.getRecoveredData() } catch let error as UnforgettableSDKError { if case .invalidResponse = error { print("Invalid API response") } } ``` **Recommended Action:** Retry or report error --- ### DecodingError Failed to decode/parse data. **When thrown:** - JSON parsing failed - Base64 decoding failed - Invalid data format ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { if (error.name === 'DecodingError') { console.error('Failed to parse response') } } ``` ```kotlin try { val data = sdk.getRecoveredData() } catch (e: UnforgettableSDKError.DecodingError) { println("Decoding failed: ${e.cause}") } ``` ```swift do { let data = try await sdk.getRecoveredData() } catch let error as UnforgettableSDKError { if case .decodingError(let underlying) = error { print("Decoding error: \(underlying)") } } ``` **Recommended Action:** Retry or abort ## Error Handling Patterns ### Basic Try-Catch ```typescript async function recoverKey(sdk: UnforgettableSdk): Promise { try { return await sdk.getRecoveredKey() } catch (error) { console.error('Recovery failed:', error) return null } } ``` ### Specific Error Handling ```typescript async function recoverKeyWithRetry(sdk: UnforgettableSdk): Promise { try { return await sdk.getRecoveredKey() } catch (error) { if (error instanceof NotFoundError) { // Not ready, caller should retry throw error } else if (error.name === 'NetworkError') { // Network issue, retry after delay await sleep(5000) return recoverKeyWithRetry(sdk) } else if (error.name === 'CryptoError') { // Crypto failure, abort throw new Error('Decryption failed, please restart recovery') } else { // Unknown error throw error } } } ``` ### Polling with Error Handling ```typescript async function pollForKey( sdk: UnforgettableSdk, maxAttempts: number = 60 ): Promise { let attempts = 0 let lastError: Error | null = null while (attempts { try { console.log('[Recovery] Starting...') const key = await sdk.getRecoveredKey() console.log('[Recovery] Success!') return key } catch (error) { console.error('[Recovery] Error:', { type: error.name, message: error.message, stack: error.stack, }) throw error } } ``` ### Production ```typescript async function recoverWithTracking(sdk: UnforgettableSdk): Promise { try { const key = await sdk.getRecoveredKey() analytics.track('recovery_success') return key } catch (error) { // Log error without exposing sensitive data errorTracking.captureException(error, { tags: { component: 'unforgettable-sdk', method: 'getRecoveredKey', }, extra: { errorType: error.name, // DO NOT log private keys or sensitive data }, }) throw error } } ``` ## Platform-Specific Errors ### Android (Kotlin) ```kotlin sealed class UnforgettableSDKError : Exception() { data class NetworkError(override val cause: Throwable) : UnforgettableSDKError() object InvalidResponse : UnforgettableSDKError() object NotFound : UnforgettableSDKError() data class DecodingError(override val cause: Throwable) : UnforgettableSDKError() data class CryptoError(override val cause: Throwable) : UnforgettableSDKError() } sealed class CryptoError : Exception() { object KeyGenerationFailed : CryptoError() object EncryptionFailed : CryptoError() object DecryptionFailed : CryptoError() object InvalidPublicKey : CryptoError() object EncodingFailed : CryptoError() object DecodingFailed : CryptoError() } ``` ### iOS (Swift) ```swift public enum UnforgettableSDKError: Error { case networkError(Error) case invalidResponse case notFound case decodingError(Error) case cryptoError(Error) } public enum CryptoError: Error { case keyGenerationFailed case encryptionFailed case decryptionFailed case invalidPublicKey case encodingFailed case decodingFailed } ``` ## Best Practices ### 1. Always Handle Errors ❌ **Bad:** ```typescript const key = await sdk.getRecoveredKey() // No error handling! ``` ✅ **Good:** ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { handleError(error) } ``` ### 2. Distinguish Between Error Types ❌ **Bad:** ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { console.error('Error!') // Generic handling } ``` ✅ **Good:** ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { if (error instanceof NotFoundError) { // Continue polling } else if (error.name === 'NetworkError') { // Retry } else { // Abort } } ``` ### 3. Provide User Feedback ❌ **Bad:** ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { console.error(error) // User sees nothing } ``` ✅ **Good:** ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { showErrorMessage(getErrorMessage(error)) enableRetryButton() } ``` ### 4. Don't Log Sensitive Data ❌ **Bad:** ```typescript try { const key = await sdk.getRecoveredKey() console.log('Recovered key:', key) // Private key exposed! } catch (error) { console.error(error) } ``` ✅ **Good:** ```typescript try { const key = await sdk.getRecoveredKey() console.log('Recovery successful') // Use key without logging it } catch (error) { console.error('Recovery failed:', error.name) } ``` ## Next Steps - [UnforgettableSDK API](/sdk/api/unforgettable-sdk) - Main SDK reference - [Recovery Factors](/sdk/api/recovery-factors) - Recovery factor details - [Advanced: Data Transfer](/sdk/advanced/data-transfer) - Polling strategies --- ## Recovery Factors # Recovery Factors Reference for available recovery factor types and their usage. ## Overview Recovery factors are authentication methods users can choose to protect their recovery keys. Each factor provides a different balance of security and user experience. ## Available Factors ### RecoveryFactor Enum import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```typescript export enum RecoveryFactor { Face = 1, Image = 2, Password = 3, } ``` ```kotlin enum class RecoveryFactor(val value: Int) { FACE(1), IMAGE(2), PASSWORD(3) } ``` ```swift public enum RecoveryFactor: Int { case face = 1 case image = 2 case password = 3 } ``` ## Factor Details ### Face (1) Biometric facial recognition. **Security Level:** High **User Experience:** - ✅ Fast and convenient - ✅ No memorization required - ❌ Requires camera access - ❌ May fail in low light **Best For:** - Mobile applications - Consumer wallets - Quick authentication **Implementation:** ```typescript factors: [RecoveryFactor.Face] ``` **Considerations:** - Uses device biometric sensors - Requires user permission for camera - Works best with front-facing camera - May require liveness detection --- ### Image (2) Physical object or image recognition. **Security Level:** Medium-High **User Experience:** - ✅ Unique and memorable - ✅ Offline-capable (with photo) - ❌ Requires keeping physical object safe - ❌ Can be lost or damaged **Best For:** - Backup authentication - Physical security tokens - Multi-factor setups **Implementation:** ```typescript factors: [RecoveryFactor.Image] ``` **Considerations:** - User must have access to physical object - Image should be unique and recognizable - Avoid common objects (keys, coins) - Consider digital alternatives --- ### Password (3) User-defined password or passphrase. **Security Level:** Medium **User Experience:** - ✅ Familiar to users - ✅ Works everywhere - ❌ Can be forgotten - ❌ Vulnerable to weak passwords **Best For:** - Enterprise applications - Users familiar with password managers - Fallback authentication **Implementation:** ```typescript factors: [RecoveryFactor.Password] ``` **Considerations:** - Encourage strong passwords - Provide password strength feedback - Consider password managers - Implement rate limiting ## Usage Patterns ### All Factors (Default) Don't specify factors to allow all options: ```typescript const sdk = new UnforgettableSdk({ mode: 'create', // factors omitted = all factors available }) ``` **Equivalent to:** ```typescript factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password] ``` --- ### Single Factor Specify one factor for a streamlined experience: ```typescript const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face], // Only face recognition }) ``` **Pros:** - Simple user experience - Fastest setup **Cons:** - No fallback if factor unavailable - Lower security for single factor --- ### Multiple Factors (User Choice) Allow users to choose their preferred factor: ```typescript const sdk = new UnforgettableSdk({ mode: 'create', factors: [ RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password, ], // User picks one }) ``` **Pros:** - Flexible user experience - Users choose what works best for them **Cons:** - Slightly more complex UX - Users may choose weak factors --- ## Factor Validation ```typescript import { RecoveryFactor, ALL_RECOVERY_FACTORS } from '@rarimo/unforgettable-sdk' function validateFactors(factors: RecoveryFactor[]): boolean { return factors.every(factor => ALL_RECOVERY_FACTORS.includes(factor)) } // Usage const userFactors = [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password] if (validateFactors(userFactors)) { console.log('Valid factors') } ``` ```kotlin fun validateFactors(factors: List): Boolean { val validFactors = RecoveryFactor.values().toList() return factors.all { it in validFactors } } ``` ```swift func validateFactors(_ factors: [RecoveryFactor]) -> Bool { let validFactors: Set = [.face, .image, .password] return factors.allSatisfy { validFactors.contains($0) } } ``` ## UI Recommendations ### Factor Icons Suggest using recognizable icons: - **Face:** 👤 or face scan icon - **Image:** 📷 or object icon - **Password:** 🔑 or lock icon ### Factor Labels Clear, user-friendly labels: - **Face:** "Face Recognition" or "Biometric" - **Image:** "Physical Object" or "Security Image" - **Password:** "Password" or "Passphrase" ### Factor Descriptions Help users understand each option: ```typescript const factorDescriptions = { [RecoveryFactor.Face]: "Use your face to recover your account", [RecoveryFactor.Image]: "Use a physical object or image", [RecoveryFactor.Password]: "Use a password you create", } ``` ## Future Factors Potential future additions: - **Geolocation:** Location-based verification - **Voice:** Voice recognition ## Examples ```tsx import { RecoveryFactor } from '@rarimo/unforgettable-sdk' function FactorSelector({ onSelect }: { onSelect: (factor: RecoveryFactor) => void }) { const factors = [ { value: RecoveryFactor.Face, label: 'Face', icon: '👤' }, { value: RecoveryFactor.Image, label: 'Image', icon: '📷' }, { value: RecoveryFactor.Password, label: 'Password', icon: '🔑' }, ] return ( Choose Recovery Method {factors.map(factor => ( onSelect(factor.value)} className="factor-option" > {factor.icon} {factor.label} ))} ) } ``` ```typescript async function getAvailableFactors(): Promise { const factors: RecoveryFactor[] = [] // Check if camera available (for Face and Image) if (await hasCameraAccess()) { factors.push(RecoveryFactor.Face) factors.push(RecoveryFactor.Image) } // Password always available factors.push(RecoveryFactor.Password) return factors } ``` ## Next Steps - [UnforgettableSDK](/sdk/api/unforgettable-sdk) - Main SDK API - [Errors](/sdk/api/errors) - Error handling - [Quick Start](/sdk/getting-started) - Get started quickly --- ## UnforgettableSDK Class import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # UnforgettableSDK Class Complete API reference for the `UnforgettableSDK` class. ## Constructor ### `new UnforgettableSDK(options)` Creates a new instance of the Unforgettable SDK. **Parameters:** | Name | Type | Required | Description | |------|------|----------|-------------| | `options` | `UnforgettableSdkOptions` | Yes | Configuration options | ```typescript import { UnforgettableSdk, RecoveryFactor } from '@rarimo/unforgettable-sdk' const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], group: 'my-app', }) ``` ```kotlin import app.unforgettable.sdk.* val sdk = UnforgettableSDK( UnforgettableSdkOptions( mode = UnforgettableMode.CREATE, factors = listOf(RecoveryFactor.FACE, RecoveryFactor.IMAGE, RecoveryFactor.PASSWORD), group = "my-app" ) ) ``` ```swift import UnforgettableSDK let sdk = try UnforgettableSDK(options: UnforgettableSdkOptions( mode: .create, factors: [.face, .image, .password], group: "my-app" )) ``` ## Methods ### `getRecoveryUrl()` Generates the recovery URL to share with the user. **Returns:** `Promise` (TypeScript) | `String` (Kotlin/Swift) ```typescript const recoveryUrl = await sdk.getRecoveryUrl() console.log(recoveryUrl) // "https://unforgettable.app/sdk/c#id=...&epk=...&f=1,3&g=my-app" ``` ```kotlin val recoveryUrl = sdk.getRecoveryUrl() println(recoveryUrl) ``` ```swift let recoveryUrl = sdk.getRecoveryUrl() print(recoveryUrl) ``` **Throws:** - `CryptoError` - If key generation fails ### `getRecoveredData()` Retrieves the complete recovered data including recovery key. **Returns:** `Promise` (TypeScript) | `RecoveredData` (Kotlin/Swift) ```typescript try { const data = await sdk.getRecoveredData() console.log('Recovery Key:', data.recoveryKey) } catch (error) { if (error instanceof NotFoundError) { console.log('Data not ready yet') } } ``` ```kotlin lifecycleScope.launch { try { val data = sdk.getRecoveredData() println("Recovery Key: ${data.recoveryKey}") } catch (e: UnforgettableSDKError.NotFound) { println("Data not ready yet") } } ``` ```swift Task { do { let data = try await sdk.getRecoveredData() print("Recovery Key: \(data.recoveryKey)") } catch let error as UnforgettableSDKError { if case .notFound = error { print("Data not ready yet") } } } ``` **Throws:** - `NotFoundError` - Data transfer not found (user hasn't completed recovery) - `NetworkError` - Network connectivity issues - `CryptoError` - Decryption failed - `InvalidResponse` - Server returned invalid data ### `getRecoveredKey()` Convenience method to retrieve only the recovery key. **Returns:** `Promise` (TypeScript) | `String` (Kotlin/Swift) ```typescript const recoveryKey = await sdk.getRecoveredKey() // Equivalent to: (await sdk.getRecoveredData()).recoveryKey ``` ```kotlin val recoveryKey = sdk.getRecoveredKey() ``` ```swift let recoveryKey = try await sdk.getRecoveredKey() ``` **Throws:** - Same as `getRecoveredData()` ## Types ### `UnforgettableSdkOptions` Configuration options for initializing the SDK. ```typescript interface UnforgettableSdkOptions { mode: 'create' | 'restore' appUrl?: string apiUrl?: string factors?: RecoveryFactor[] walletAddress?: string group?: string customParams?: Record } ``` ```kotlin data class UnforgettableSdkOptions( val mode: UnforgettableMode, val appUrl: String = UNFORGETTABLE_APP_URL, val apiUrl: String = UNFORGETTABLE_API_URL, val factors: List = emptyList(), val walletAddress: String? = null, val group: String? = null, val customParams: Map? = null ) ``` ```swift public struct UnforgettableSdkOptions { public let mode: UnforgettableMode public let appUrl: String public let apiUrl: String public let factors: [RecoveryFactor] public let walletAddress: String? public let group: String? public let customParams: [String: String]? } ``` #### Properties | Property | Type | Default | Description | |----------|------|---------|-------------| | `mode` | `'create' \| 'restore'` | - | **Required.** Operation mode | | `appUrl` | `string` | `'https://unforgettable.app/sdk'` | Unforgettable app URL | | `apiUrl` | `string` | `'https://api.unforgettable.app'` | API endpoint URL | | `factors` | `RecoveryFactor[]` | `[]` | Recovery factors to enable | | `walletAddress` | `string` | `undefined` | Wallet address (required for restore mode) | | `group` | `string` | `undefined` | Group identifier for organizing keys | | `customParams` | `Record` | `undefined` | Custom URL parameters | ### `RecoveredData` Data returned from recovery operations. ```typescript interface RecoveredData { recoveryKey: string } ``` ```kotlin data class RecoveredData( val recoveryKey: String, ) ``` ```swift public struct RecoveredData { public let recoveryKey: String } ``` #### Properties | Property | Type | Description | |----------|------|-------------| | `recoveryKey` | `string` | The decrypted recovery/private key | ### `UnforgettableMode` Recovery operation mode. ```typescript type UnforgettableMode = 'create' | 'restore' ``` ```kotlin enum class UnforgettableMode { CREATE, RESTORE } ``` ```swift public enum UnforgettableMode { case create case restore } ``` ## Properties ### `mode` The operation mode (create or restore). **Type:** `UnforgettableMode` **Read-only:** Yes ```typescript console.log(sdk.mode) // 'create' or 'restore' ``` ### `appUrl` The Unforgettable app URL. **Type:** `string` **Read-only:** Yes ```typescript console.log(sdk.appUrl) // 'https://unforgettable.app/sdk' ``` ### `factors` The [recovery factors](./recovery-factors.md) configured for this instance. **Type:** `RecoveryFactor[]` **Read-only:** Yes ```typescript console.log(sdk.factors) // [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password] ``` ### `walletAddress` The wallet address (if specified). **Type:** `string | undefined` **Read-only:** Yes ```typescript console.log(sdk.walletAddress) // '0x...' or undefined ``` ### `group` The group identifier (if specified). **Type:** `string | undefined` **Read-only:** Yes ```typescript console.log(sdk.group) // 'my-app' or undefined ``` ### `customParams` Custom URL parameters (if specified). **Type:** `Record | undefined` **Read-only:** Yes ```typescript console.log(sdk.customParams) // { theme: 'dark' } or undefined ``` ## Examples ### Create Recovery Flow ```typescript import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk' async function setupRecovery() { // Initialize SDK const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], group: 'my-wallet', }) // Get recovery URL const url = await sdk.getRecoveryUrl() displayQRCode(url) // Poll for recovery key const maxAttempts = 60 for (let i = 0; i < maxAttempts; i++) { try { const key = await sdk.getRecoveredKey() console.log('Recovery successful!') return key } catch (error) { if (error instanceof NotFoundError) { await sleep(3000) } else { throw error } } } throw new Error('Recovery timeout') } ``` ### Restore Account Flow ```typescript async function restoreAccount(walletAddress: string) { const sdk = new UnforgettableSdk({ mode: 'restore', walletAddress, factors: [RecoveryFactor.Face], }) const url = await sdk.getRecoveryUrl() displayQRCode(url) // Poll for restored key while (true) { try { const data = await sdk.getRecoveredData() console.log('Account restored!') console.log('Key:', data.recoveryKey) return data } catch (error) { if (error instanceof NotFoundError) { await sleep(3000) } else { throw error } } } } ``` ## Error Handling All SDK methods can throw errors. Always use try-catch: ```typescript try { const key = await sdk.getRecoveredKey() } catch (error) { if (error instanceof NotFoundError) { // Data not ready, retry } else if (error.name === 'NetworkError') { // Network issue } else if (error.name === 'CryptoError') { // Encryption/decryption failed } else { // Other error } } ``` See [Error Reference](/sdk/api/errors) for detailed error documentation. ## Next Steps - [Recovery Factors](/sdk/api/recovery-factors) - Available authentication methods - [Errors](/sdk/api/errors) - Error types and handling - [Advanced: Architecture](/sdk/advanced/architecture) - How it works --- ## Getting Started # Getting Started This guide will help you install and get started with Unforgettable SDK. ## Requirements ### Web / React / React Native - Node.js 18+ or modern browser with Web Crypto API support - TypeScript 5.0+ (optional but recommended) ### Android - Android SDK 21+ (Android 5.0 Lollipop) - Kotlin 1.9+ - Java 17+ ### iOS - iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ - Swift 5.9+ - Xcode 15.0+ ## Installation Choose your platform and install the SDK: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```bash npm install @rarimo/unforgettable-sdk ``` ```bash yarn add @rarimo/unforgettable-sdk ``` ```bash pnpm add @rarimo/unforgettable-sdk ``` ```bash npm install @rarimo/unforgettable-sdk-react ``` ```bash yarn add @rarimo/unforgettable-sdk-react ``` ```bash pnpm add @rarimo/unforgettable-sdk-react ``` ```bash npm install @rarimo/unforgettable-sdk ``` ```bash yarn add @rarimo/unforgettable-sdk ``` ```kotlin repositories { maven { url = uri("https://jitpack.io") } } dependencies { implementation("com.github.rarimo.unforgettable-sdk:android:1.0.0") } ``` ```groovy repositories { maven { url 'https://jitpack.io' } } dependencies { implementation 'com.github.rarimo.unforgettable-sdk:android:1.0.0' } ``` ```xml jitpack.io https://jitpack.io com.github.rarimo.unforgettable-sdk android 1.0.0 ``` **Permissions**: Add to `AndroidManifest.xml`: ```xml ``` **Swift Package Manager:** Add the package dependency to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/rarimo/unforgettable-sdk", from: "1.0.0") ] ``` Or add via Xcode: 1. File → Add Package Dependencies 2. Enter: `https://github.com/rarimo/unforgettable-sdk` 3. Select version 1.0.0+ ## Quick Start Let's create a simple recovery flow where users can set up a new private key. ### Step 1: Initialize the SDK ```typescript import { UnforgettableSdk, RecoveryFactor } from '@rarimo/unforgettable-sdk' const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], }) ``` ```tsx import UnforgettableQrCode from '@rarimo/unforgettable-sdk-react' import { RecoveryFactor } from '@rarimo/unforgettable-sdk' function App() { const handleSuccess = (privateKey: string) => { console.log('Recovery successful!', privateKey) } return ( ) } ``` ```kotlin import com.rarimo.unforgettable.UnforgettableSDK import com.rarimo.unforgettable.UnforgettableSdkOptions import com.rarimo.unforgettable.UnforgettableMode import com.rarimo.unforgettable.RecoveryFactor val sdk = UnforgettableSDK( UnforgettableSdkOptions( mode = UnforgettableMode.CREATE, factors = listOf(RecoveryFactor.FACE, RecoveryFactor.IMAGE, RecoveryFactor.PASSWORD) ) ) ``` ```swift import UnforgettableSDK let sdk = UnforgettableSDK( mode: .create, factors: [.face, .image, .password] ) ``` ### Step 2: Generate Recovery URL ```typescript const recoveryUrl = await sdk.getRecoveryUrl() console.log('Recovery URL:', recoveryUrl) // Display this as a QR code for users to scan ``` The `UnforgettableQrCode` component handles this automatically! It generates the URL and displays it as a QR code. ```kotlin val recoveryUrl = sdk.getRecoveryUrl() // Load this URL in a WebView webView.loadUrl(recoveryUrl) ``` ```swift let recoveryUrl = try await sdk.getRecoveryUrl() // Load this URL in a WKWebView if let url = URL(string: recoveryUrl) { webView.load(URLRequest(url: url)) } ``` ### Step 3: Poll for Recovered Key ```typescript import { NotFoundError } from '@rarimo/unforgettable-sdk' async function pollForKey() { const maxAttempts = 60 let attempts = 0 while (attempts setTimeout(resolve, 3000)) } else { throw error } } } throw new Error('Recovery timeout') } pollForKey() ``` The `UnforgettableQrCode` component handles polling automatically! Just provide an `onSuccess` callback. ```kotlin import kotlinx.coroutines.delay suspend fun pollForKey(): String { val maxAttempts = 60 var attempts = 0 while (attempts ```swift func pollForKey() async throws -> String { let maxAttempts = 60 var attempts = 0 while attempts ## Next Steps Explore platform-specific guides for detailed examples and advanced features: - [Web](/sdk/platforms/web) - QR code implementation for browsers - [React](/sdk/platforms/react) - Hooks and components - [React Native](/sdk/platforms/react-native) - WebView integration - [Android](/sdk/platforms/android) - Native Android implementation - [iOS](/sdk/platforms/ios) - Native iOS implementation ## Key Concepts Before diving deeper, understand these core concepts: - **Mode**: Either `create` (for setting up a new key) or `restore` (for recovering an existing key) - **Recovery Factors**: The methods users can choose to create/restore their key (Face, Image, Password) - **Recovery URL**: A secure link that users scan/open to complete the recovery process - **Data Transfer**: The encrypted communication between your app and Unforgettable.app - **QR Code** (Web): Web apps display QR codes for users to scan with mobile devices - **WebView** (Mobile): Mobile apps open the recovery URL in a WebView for in-app recovery --- ## Index import { Redirect } from '@docusaurus/router'; --- ## Unforgettable SDK # Unforgettable SDK Unforgettable SDK enables you to integrate seedless login and account recovery into your applications. It allows users to recover their private keys using various recovery factors such as: - 👤 **Face** - 🖼️ **Physical object/Image** - 🔑 **Password** ## Key Features - **🌐 Cross-Platform**: Available for Web, React, React Native, Android (Kotlin), and iOS (Swift) - **🚀 Easy Integration**: Simple APIs to generate recovery URLs and retrieve recovered keys - **🔒 End-to-End Encryption**: All data is encrypted using industry-standard X25519 key exchange and ChaCha20-Poly1305 cipher - **🔄 Flexible Recovery**: Support for multiple recovery factors - **🎨 Customizable**: Support for custom parameters ## How It Works The Unforgettable SDK follows a simple flow: 1. **Generate Recovery URL**: Create a secure link containing your app's public encryption key 2. **User Recovery Process**: - **Web**: User scans QR code to open the recovery page - **Mobile**: App opens the URL in a WebView for in-app recovery 3. **Retrieve Recovered Key**: Poll the API to receive the encrypted private key 4. **Decrypt & Use**: SDK automatically decrypts the key for use in your application ```mermaid sequenceDiagram participant App as Your App participant SDK as Unforgettable SDK participant User participant UnfApp as Unforgettable.app participant API as Unforgettable API App->>SDK: Initialize SDK SDK->>SDK: Generate key pair SDK->>App: Return recovery URL App->>User: Display QR code (Web) or WebView (Mobile) User->>UnfApp: Scan QR code or use WebView UnfApp->>User: Verify recovery factors User->>UnfApp: Complete verification UnfApp->>API: Send encrypted recovery key App->>SDK: Poll for recovered data SDK->>API: Fetch encrypted data API->>SDK: Return encrypted key SDK->>SDK: Decrypt with private key SDK->>App: Return decrypted key ``` ## Quick Start Choose your platform to get started: - [Web (JavaScript/TypeScript)](/sdk/platforms/web) - [React](/sdk/platforms/react) - [React Native](/sdk/platforms/react-native) - [Android (Kotlin)](/sdk/platforms/android) - [iOS (Swift)](/sdk/platforms/ios) Or dive deeper into how the SDK works: - [Architecture Overview](/sdk/advanced/architecture) - [Encryption Details](/sdk/advanced/encryption) - [API Reference](/sdk/api/unforgettable-sdk) ## Support - **GitHub**: [rarimo/unforgettable-sdk](https://github.com/rarimo/unforgettable-sdk) - **Issues**: [GitHub Issues](https://github.com/rarimo/unforgettable-sdk/issues) - **Community & Support**: [Telegram group](https://t.me/+pWugh5xgDiE3Y2Jk) --- ## Android # Android Learn how to integrate Unforgettable SDK into your Android application using Kotlin. ## Requirements - Android SDK 21+ (Android 5.0 Lollipop) - Kotlin 1.9+ - Java 17+ ## Installation Add JitPack repository and dependency to your build configuration: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```kotlin repositories { maven { url = uri("https://jitpack.io") } } dependencies { implementation("com.github.rarimo.unforgettable-sdk:android:1.0.0") } ``` ```groovy repositories { maven { url 'https://jitpack.io' } } dependencies { implementation 'com.github.rarimo.unforgettable-sdk:android:1.0.0' } ``` ```xml jitpack.io https://jitpack.io com.github.rarimo.unforgettable-sdk android 1.0.0 ``` ## Permissions Add to your `AndroidManifest.xml`: ```xml ``` ## Quick Start Here's a minimal example to get started: ```kotlin import android.webkit.WebView import androidx.lifecycle.lifecycleScope import com.rarimo.unforgettable.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch class RecoveryActivity : AppCompatActivity() { private lateinit var sdk: UnforgettableSDK private lateinit var webView: WebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) webView = findViewById(R.id.recoveryWebView) webView.settings.javaScriptEnabled = true lifecycleScope.launch { // 1. Initialize SDK sdk = UnforgettableSDK( UnforgettableSdkOptions( mode = UnforgettableMode.CREATE, factors = listOf(RecoveryFactor.FACE, RecoveryFactor.IMAGE) ) ) // 2. Load recovery URL in WebView val recoveryUrl = sdk.getRecoveryUrl() webView.loadUrl(recoveryUrl) // 3. Poll for recovered key var attempts = 0 while (attempts ``` ### WebView Setup ```kotlin import android.webkit.WebView import android.webkit.WebSettings import android.webkit.WebChromeClient import android.webkit.PermissionRequest fun setupWebView(webView: WebView) { webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true mediaPlaybackRequiresUserGesture = false databaseEnabled = true } // Enable camera access for face verification webView.webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { request.grant(request.resources) } } } ``` ## Complete Example ### Create Recovery Activity ```kotlin import android.os.Bundle import android.webkit.WebView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.rarimo.unforgettable.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch class CreateRecoveryActivity : AppCompatActivity() { private lateinit var sdk: UnforgettableSDK private lateinit var webView: WebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_create_recovery) webView = findViewById(R.id.recoveryWebView) setupWebView(webView) setupRecovery() } private fun setupRecovery() { lifecycleScope.launch { try { // Initialize SDK sdk = UnforgettableSDK( UnforgettableSdkOptions( mode = UnforgettableMode.CREATE, factors = listOf( RecoveryFactor.FACE, RecoveryFactor.IMAGE, RecoveryFactor.PASSWORD ), group = "android-wallet-app" ) ) // Get recovery URL and load it in WebView val recoveryUrl = sdk.getRecoveryUrl() webView.loadUrl(recoveryUrl) // Start polling for recovered key pollForRecoveryKey() } catch (e: Exception) { showError("Failed to initialize recovery: ${e.message}") } } } private suspend fun pollForRecoveryKey() { val maxAttempts = 60 var attempts = 0 while (attempts ``` ### Restore Account Activity ```kotlin class RestoreAccountActivity : AppCompatActivity() { private lateinit var walletAddressInput: EditText private lateinit var restoreButton: Button private lateinit var webView: WebView private lateinit var sdk: UnforgettableSDK override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_restore_account) walletAddressInput = findViewById(R.id.walletAddressInput) restoreButton = findViewById(R.id.restoreButton) webView = findViewById(R.id.recoveryWebView) setupWebView(webView) restoreButton.setOnClickListener { val walletAddress = walletAddressInput.text.toString() if (walletAddress.isNotBlank()) { startRestore(walletAddress) } else { Toast.makeText(this, "Please enter wallet address", Toast.LENGTH_SHORT).show() } } } private fun startRestore(walletAddress: String) { lifecycleScope.launch { try { // Initialize SDK in restore mode sdk = UnforgettableSDK( UnforgettableSdkOptions( mode = UnforgettableMode.RESTORE, walletAddress = walletAddress, factors = listOf(RecoveryFactor.FACE, RecoveryFactor.IMAGE, RecoveryFactor.PASSWORD) ) ) // Get recovery URL and load it in WebView val recoveryUrl = sdk.getRecoveryUrl() webView.loadUrl(recoveryUrl) webView.visibility = View.VISIBLE // Hide input form walletAddressInput.visibility = View.GONE restoreButton.visibility = View.GONE // Poll for restored key pollForRestoredKey() } catch (e: Exception) { Toast.makeText( this@RestoreAccountActivity, "Failed to start restore: ${e.message}", Toast.LENGTH_LONG ).show() } } } private suspend fun pollForRestoredKey() { val maxAttempts = 60 var attempts = 0 while (attempts (RecoveryState.Idle) val state: StateFlow = _state private var sdk: UnforgettableSDK? = null fun startRecovery(mode: UnforgettableMode, walletAddress: String? = null) { viewModelScope.launch { try { _state.value = RecoveryState.Loading sdk = UnforgettableSDK( UnforgettableSdkOptions( mode = mode, walletAddress = walletAddress, factors = listOf(RecoveryFactor.FACE, RecoveryFactor.IMAGE, RecoveryFactor.PASSWORD ) ) val recoveryUrl = sdk!!.getRecoveryUrl() _state.value = RecoveryState.WebViewReady(recoveryUrl) pollForKey() } catch (e: Exception) { _state.value = RecoveryState.Error(e.message ?: "Unknown error") } } } private suspend fun pollForKey() { val maxAttempts = 60 var attempts = 0 while (attempts { Button(onClick = { viewModel.startRecovery(UnforgettableMode.CREATE) }) { Text("Start Recovery Setup") } } is RecoveryState.Loading -> { CircularProgressIndicator() Spacer(modifier = Modifier.height(16.dp)) Text("Initializing recovery...") } is RecoveryState.WebViewReady -> { AndroidView( modifier = Modifier .fillMaxWidth() .weight(1f), factory = { context -> WebView(context).apply { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.mediaPlaybackRequiresUserGesture = false webChromeClient = object : android.webkit.WebChromeClient() { override fun onPermissionRequest(request: android.webkit.PermissionRequest) { request.grant(request.resources) } } loadUrl(currentState.url) } } ) Spacer(modifier = Modifier.height(16.dp)) Row( verticalAlignment = Alignment.CenterVertically ) { CircularProgressIndicator(modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.width(8.dp)) Text("Waiting for completion...") } } is RecoveryState.Success -> { Text("✅ Recovery Successful!") Spacer(modifier = Modifier.height(16.dp)) Text("Your account is now protected") } is RecoveryState.Error -> { Text("❌ Error: ${currentState.message}") } } } } ``` ## ProGuard Rules If using ProGuard, add to `proguard-rules.pro`: ```proguard # Keep SDK classes -keep class com.rarimo.unforgettable.** { *; } # Keep serialization classes -keepclassmembers class com.rarimo.unforgettable.** { @kotlinx.serialization.* ; } ``` ## Error Handling ```kotlin try { val recoveredData = sdk.getRecoveredData() println("Key: ${recoveredData.recoveryKey}") } catch (e: UnforgettableSDKError.NotFound) { println("Data not ready yet") } catch (e: UnforgettableSDKError.NetworkError) { println("Network error: ${e.cause}") } catch (e: UnforgettableSDKError.CryptoError) { println("Cryptography error: ${e.cause}") } catch (e: Exception) { println("Unexpected error: $e") } ``` ## Next Steps - [iOS Integration](/sdk/platforms/ios) - Native iOS SDK - [Advanced: Encryption](/sdk/advanced/encryption) - Cryptography details - [API Reference](/sdk/api/unforgettable-sdk) - Complete API documentation --- ## iOS # iOS Learn how to integrate Unforgettable SDK into your iOS application using Swift. ## Requirements - iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ - Swift 5.9+ - Xcode 15.0+ ## Installation ### Swift Package Manager Add the package dependency to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/rarimo/unforgettable-sdk", from: "1.0.0") ] ``` ### Xcode 1. File → Add Package Dependencies 2. Enter the repository URL: `https://github.com/rarimo/unforgettable-sdk` 3. Select the version you want to use (1.0.0+) ## Quick Start Here's a minimal example to get started: ```swift import UIKit import WebKit import UnforgettableSDK class RecoveryViewController: UIViewController { private var webView: WKWebView! private var sdk: UnforgettableSDK! override func viewDidLoad() { super.viewDidLoad() // Setup WebView let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true webView = WKWebView(frame: view.bounds, configuration: configuration) view.addSubview(webView) Task { // 1. Initialize SDK sdk = UnforgettableSDK( mode: .create, factors: [.face, .image, .password] ) // 2. Load recovery URL in WebView let recoveryUrl = try await sdk.getRecoveryUrl() if let url = URL(string: recoveryUrl) { webView.load(URLRequest(url: url)) } // 3. Poll for recovered key var attempts = 0 while attempts WKWebView { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] let webView = WKWebView(frame: .zero, configuration: configuration) webView.translatesAutoresizingMaskIntoConstraints = false return webView } ``` ### Camera Permissions Add to `Info.plist`: ```xml NSCameraUsageDescription Camera access is required for face verification during account recovery ``` ## Complete Example ### Create Recovery View Controller ```swift import UIKit import WebKit import UnforgettableSDK class CreateRecoveryViewController: UIViewController { private var sdk: UnforgettableSDK? private var webView: WKWebView! private let statusLabel = UILabel() private let activityIndicator = UIActivityIndicatorView(style: .large) override func viewDidLoad() { super.viewDidLoad() setupUI() setupRecovery() } private func setupUI() { view.backgroundColor = .systemBackground // Title let titleLabel = UILabel() titleLabel.text = "Set Up Account Recovery" titleLabel.font = .systemFont(ofSize: 24, weight: .bold) titleLabel.textAlignment = .center titleLabel.translatesAutoresizingMaskIntoConstraints = false // WebView webView = setupWebView() // Status statusLabel.text = "Waiting for recovery..." statusLabel.font = .systemFont(ofSize: 14) statusLabel.textAlignment = .center statusLabel.textColor = .secondaryLabel statusLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(titleLabel) view.addSubview(webView) view.addSubview(activityIndicator) view.addSubview(statusLabel) activityIndicator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), webView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20), webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), webView.bottomAnchor.constraint(equalTo: activityIndicator.topAnchor, constant: -20), activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), activityIndicator.bottomAnchor.constraint(equalTo: statusLabel.topAnchor, constant: -8), statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), statusLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20) ]) } private func setupWebView() -> WKWebView { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] let webView = WKWebView(frame: .zero, configuration: configuration) webView.translatesAutoresizingMaskIntoConstraints = false return webView } private func setupRecovery() { activityIndicator.startAnimating() Task { do { // Initialize SDK sdk = try UnforgettableSDK(options: UnforgettableSdkOptions( mode: .create, factors: [.face, .image, .password], group: "ios-wallet-app" )) // Get recovery URL and load it in WebView let recoveryUrl = sdk!.getRecoveryUrl() await MainActor.run { if let url = URL(string: recoveryUrl) { webView.load(URLRequest(url: url)) } activityIndicator.stopAnimating() } // Start polling try await pollForRecoveryKey() } catch { await showError("Failed to initialize recovery: \(error.localizedDescription)") } } } private func pollForRecoveryKey() async throws { let maxAttempts = 60 var attempts = 0 while attempts WKWebView { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] let webView = WKWebView(frame: .zero, configuration: configuration) webView.translatesAutoresizingMaskIntoConstraints = false return webView } @objc private func handleRestore() { guard let walletAddress = walletAddressTextField.text, !walletAddress.isEmpty else { showError("Please enter a wallet address") return } startRestore(walletAddress: walletAddress) } private func startRestore(walletAddress: String) { Task { do { // Initialize SDK in restore mode sdk = try UnforgettableSDK(options: UnforgettableSdkOptions( mode: .restore, walletAddress: walletAddress, factors: [.face, .image, .password] )) // Get recovery URL and load it in WebView let recoveryUrl = sdk!.getRecoveryUrl() await MainActor.run { if let url = URL(string: recoveryUrl) { webView.load(URLRequest(url: url)) } webView.isHidden = false walletAddressTextField.isHidden = true restoreButton.isHidden = true statusLabel.text = "Complete recovery in the browser" } // Poll for restored key try await pollForRestoredKey() } catch { await showError("Failed to start restore: \(error.localizedDescription)") } } } private func pollForRestoredKey() async throws { let maxAttempts = 60 var attempts = 0 while attempts WKWebView { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] return WKWebView(frame: .zero, configuration: configuration) } func updateUIView(_ webView: WKWebView, context: Context) { webView.load(URLRequest(url: url)) } } @MainActor class RecoveryViewModel: ObservableObject { enum State { case idle case loading case webViewReady(String) case success(String) case error(String) } @Published var state: State = .idle private var sdk: UnforgettableSDK? func startRecovery(mode: UnforgettableMode) { state = .loading Task { do { sdk = try UnforgettableSDK(options: UnforgettableSdkOptions( mode: mode, factors: [.face, .image, .password] )) let recoveryUrl = sdk!.getRecoveryUrl() state = .webViewReady(recoveryUrl) try await pollForKey() } catch { state = .error(error.localizedDescription) } } } private func pollForKey() async throws { let maxAttempts = 60 var attempts = 0 while attempts CFBundleURLTypes CFBundleURLSchemes yourapp ``` ### Handle Deep Links (UIKit) ```swift func application( _ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:] ) -> Bool { handleDeepLink(url) return true } private func handleDeepLink(_ url: URL) { print("Received deep link: \(url)") // Parse and handle the URL } ``` ### Handle Deep Links (SwiftUI) ```swift struct YourApp: App { var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in handleDeepLink(url) } } } private func handleDeepLink(_ url: URL) { print("Received deep link: \(url)") } } ``` ## Best Practices 1. **Async/Await**: Use Swift's modern concurrency features 2. **Error Handling**: Always handle all possible error cases 3. **UI Updates**: Ensure UI updates happen on the main thread 4. **Memory Management**: Use `weak` references to avoid retain cycles 5. **Security**: Never log or expose private keys in production ## Next Steps - [Advanced: Architecture](/sdk/advanced/architecture) - How it works under the hood - [Advanced: Encryption](/sdk/advanced/encryption) - Cryptography details - [API Reference](/sdk/api/unforgettable-sdk) - Complete API documentation --- ## React Native # React Native Learn how to integrate Unforgettable SDK into your React Native application. ## Installation import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```bash npm install @rarimo/unforgettable-sdk ``` ```bash yarn add @rarimo/unforgettable-sdk ``` ### Additional Dependencies For WebView integration, install: ```bash npm install react-native-webview ``` ```bash yarn add react-native-webview ``` For iOS, install pods: ```bash cd ios && pod install && cd .. ``` ## Quick Start Here's a minimal example to get started: ```tsx import React, { useEffect, useState } from 'react' import { View, ActivityIndicator } from 'react-native' import { WebView } from 'react-native-webview' import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk' export const RecoveryScreen = () => { const [recoveryUrl, setRecoveryUrl] = useState(null) const [sdk, setSdk] = useState(null) useEffect(() => { initRecovery() }, []) const initRecovery = async () => { // 1. Initialize SDK const newSdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image], }) // 2. Get recovery URL const url = await newSdk.getRecoveryUrl() setRecoveryUrl(url) setSdk(newSdk) // 3. Poll for key pollForKey(newSdk) } const pollForKey = async (sdkInstance: UnforgettableSdk) => { let attempts = 0 while (attempts setTimeout(resolve, 3000)) } } } } if (!recoveryUrl) return return ( ) } ``` ## Basic Setup ### Import the SDK ```typescript import { UnforgettableSdk, RecoveryFactor } from '@rarimo/unforgettable-sdk' ``` ### Initialize the SDK ```typescript const sdk = new UnforgettableSdk({ mode: 'create', // or 'restore' factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], walletAddress: '0x...', // Required for 'restore' mode group: 'my-mobile-app', }) ``` ## WebView Component Create a reusable WebView component for recovery: ```tsx import React, { useEffect, useState } from 'react' import { View, Text, StyleSheet, ActivityIndicator } from 'react-native' import { WebView } from 'react-native-webview' import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk' interface RecoveryWebViewProps { mode: 'create' | 'restore' walletAddress?: string onSuccess: (privateKey: string) => void onError: (error: Error) => void } export const RecoveryWebView: React.FC = ({ mode, walletAddress, onSuccess, onError, }) => { const [recoveryUrl, setRecoveryUrl] = useState(null) const [loading, setLoading] = useState(true) const [sdk, setSdk] = useState(null) useEffect(() => { initializeSDK() }, []) const initializeSDK = async () => { try { const newSdk = new UnforgettableSdk({ mode, factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], walletAddress, group: 'react-native-app', }) const url = await newSdk.getRecoveryUrl() setRecoveryUrl(url) setSdk(newSdk) setLoading(false) // Start polling startPolling(newSdk) } catch (error) { setLoading(false) onError(error as Error) } } const startPolling = async (sdkInstance: UnforgettableSdk) => { const maxAttempts = 60 let attempts = 0 const poll = async () => { try { const privateKey = await sdkInstance.getRecoveredKey() onSuccess(privateKey) } catch (error) { if (error instanceof NotFoundError && attempts Loading recovery... ) } if (!recoveryUrl) { return ( Failed to initialize recovery ) } return ( Waiting for completion... ) } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, webview: { flex: 1, width: '100%', }, statusContainer: { flexDirection: 'row', alignItems: 'center', padding: 16, }, statusText: { marginLeft: 8, fontSize: 14, color: '#666', }, loadingText: { marginTop: 16, fontSize: 16, color: '#666', }, errorText: { fontSize: 16, color: '#ff0000', }, }) ``` ## Complete Example ### Create Recovery Screen ```tsx import React, { useState } from 'react' import { View, Text, StyleSheet, TouchableOpacity, Alert, SafeAreaView, } from 'react-native' import { RecoveryWebView } from './components/RecoveryWebView' export const CreateRecoveryScreen = () => { const [showQR, setShowQR] = useState(false) const [privateKey, setPrivateKey] = useState(null) const handleStartRecovery = () => { setShowQR(true) } const handleSuccess = (key: string) => { setPrivateKey(key) Alert.alert('Success', 'Recovery set up successfully!') // Create wallet with the key createWallet(key) } const handleError = (error: Error) => { Alert.alert('Error', error.message) setShowQR(false) } const createWallet = (key: string) => { // Your wallet creation logic console.log('Creating wallet with key:', key) } return ( Set Up Account Recovery {!showQR && !privateKey && ( Protect your account with biometric recovery. You can restore access using your face or other factors. Start Setup )} {showQR && !privateKey && ( )} {privateKey && ( ✅ Recovery Set Up Successfully! Your account is now protected. )} ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, content: { flex: 1, padding: 20, justifyContent: 'center', }, title: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', marginBottom: 16, }, description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 32, lineHeight: 24, }, button: { backgroundColor: '#007AFF', padding: 16, borderRadius: 8, alignItems: 'center', }, buttonText: { color: '#fff', fontSize: 18, fontWeight: '600', }, successContainer: { alignItems: 'center', }, successIcon: { fontSize: 64, marginBottom: 16, }, successText: { fontSize: 20, fontWeight: 'bold', marginBottom: 8, }, successDescription: { fontSize: 16, color: '#666', }, }) ``` ### Restore Account Screen ```tsx import React, { useState } from 'react' import { View, Text, TextInput, StyleSheet, TouchableOpacity, Alert, SafeAreaView, KeyboardAvoidingView, Platform, } from 'react-native' import { RecoveryWebView } from './components/RecoveryWebView' export const RestoreAccountScreen = () => { const [walletAddress, setWalletAddress] = useState('') const [showQR, setShowQR] = useState(false) const [recovered, setRecovered] = useState(false) const handleRestore = () => { if (!walletAddress.trim()) { Alert.alert('Error', 'Please enter a wallet address') return } setShowQR(true) } const handleSuccess = (key: string) => { setRecovered(true) Alert.alert('Success', 'Account restored successfully!') // Restore wallet access restoreWallet(key) } const handleError = (error: Error) => { Alert.alert('Error', error.message) setShowQR(false) } const restoreWallet = (key: string) => { console.log('Restoring wallet with key:', key) // Import wallet, restore user access, etc. } return ( Restore Your Account {!showQR && !recovered && ( Enter your wallet address to restore access Restore Account )} {showQR && !recovered && ( setShowQR(false)} > Cancel )} {recovered && ( ✅ Account Restored! Welcome back! )} ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, keyboardView: { flex: 1, }, content: { flex: 1, padding: 20, justifyContent: 'center', }, title: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', marginBottom: 16, }, description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 24, }, input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 16, fontSize: 16, marginBottom: 16, }, button: { backgroundColor: '#007AFF', padding: 16, borderRadius: 8, alignItems: 'center', }, buttonText: { color: '#fff', fontSize: 18, fontWeight: '600', }, cancelButton: { marginTop: 16, padding: 16, alignItems: 'center', }, cancelButtonText: { color: '#007AFF', fontSize: 16, }, successContainer: { alignItems: 'center', }, successIcon: { fontSize: 64, marginBottom: 16, }, successText: { fontSize: 20, fontWeight: 'bold', marginBottom: 8, }, successDescription: { fontSize: 16, color: '#666', }, }) ``` ## Deep Linking To handle deep links from the Unforgettable app back to your app: ### iOS Configuration Add to `ios/YourApp/Info.plist`: ```xml CFBundleURLTypes CFBundleURLSchemes yourapp ``` ### Android Configuration Add to `android/app/src/main/AndroidManifest.xml`: ```xml ``` ### Handle Deep Links ```tsx import { useEffect } from 'react' import { Linking } from 'react-native' function App() { useEffect(() => { // Handle initial URL if app was opened from a deep link Linking.getInitialURL().then(url => { if (url) handleDeepLink(url) }) // Listen for deep links while app is running const subscription = Linking.addEventListener('url', ({ url }) => { handleDeepLink(url) }) return () => subscription.remove() }, []) const handleDeepLink = (url: string) => { // Parse and handle the deep link console.log('Deep link received:', url) } return } ``` ## Platform-Specific Considerations ### iOS For iOS, add camera permissions to `ios/YourApp/Info.plist`: ```xml NSCameraUsageDescription Camera access is required for face verification during account recovery ``` ### Android Add permissions to `AndroidManifest.xml`: ```xml ``` ## Error Handling ```tsx import { NotFoundError } from '@rarimo/unforgettable-sdk' try { const key = await sdk.getRecoveredKey() // Success } catch (error) { if (error instanceof NotFoundError) { console.log('Data not ready yet') } else { console.error('Unexpected error:', error) Alert.alert('Error', 'Something went wrong') } } ``` ## TypeScript Support Full TypeScript support included: ```tsx import type { UnforgettableSdkOptions, RecoveredData, RecoveryFactor, } from '@rarimo/unforgettable-sdk' const options: UnforgettableSdkOptions = { mode: 'create', factors: [RecoveryFactor.Face], } ``` ## Next Steps - [Android Integration](/sdk/platforms/android) - Native Android SDK - [iOS Integration](/sdk/platforms/ios) - Native iOS SDK - [Advanced: Data Transfer](/sdk/advanced/data-transfer) - How polling works --- ## React # React Learn how to integrate Unforgettable SDK into your React application using hooks and components. :::info Platform-Specific Approach React web applications display QR codes for users to scan with their mobile device. For mobile apps (Android, iOS, React Native), the recovery URL is opened in a WebView for in-app recovery. ::: ## Installation import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```bash npm install @rarimo/unforgettable-sdk-react ``` ```bash yarn add @rarimo/unforgettable-sdk-react ``` ```bash pnpm add @rarimo/unforgettable-sdk-react ``` ## Quick Start Get started in seconds with the `UnforgettableQrCode` component: ```tsx import UnforgettableQrCode from '@rarimo/unforgettable-sdk-react' import { RecoveryFactor } from '@rarimo/unforgettable-sdk' function App() { const handleSuccess = (privateKey: string) => { console.log('✅ Recovery successful!', privateKey) // Use the key for wallet creation, authentication, etc. } return ( Account Recovery console.error(error)} qrProps={{ size: 256 }} /> ) } ``` That's it! The component handles URL generation, QR code display, and polling automatically. ## Components ### UnforgettableQrCode A ready-to-use QR code component that handles the entire recovery flow. #### Basic Usage ```tsx import UnforgettableQrCode from '@rarimo/unforgettable-sdk-react' import { RecoveryFactor } from '@rarimo/unforgettable-sdk' function App() { const handleSuccess = (privateKey: string) => { console.log('Recovery successful!') console.log('Private Key:', privateKey) // Use the key for wallet creation, authentication, etc. } const handleError = (error: Error) => { console.error('Recovery failed:', error) } return ( Account Recovery ) } ``` #### Props | Prop | Type | Required | Description | |------|------|----------|-------------| | `mode` | `'create' \| 'restore'` | Yes | Operation mode | | `appUrl` | `string` | No | Custom Unforgettable app URL | | `apiUrl` | `string` | No | Custom API URL | | `factors` | `RecoveryFactor[]` | No | [Recovery factors](../api/recovery-factors.md) to use | | `walletAddress` | `string` | Conditional | Required for `restore` mode | | `group` | `string` | No | Group identifier | | `customParams` | `Record` | No | Custom URL parameters | | `pollingInterval` | `number` | No | Polling interval in ms (default: 5000) | | `pollingDisabled` | `boolean` | No | Disable automatic polling (default: false) | | `onSuccess` | `(key: string, url?: string) => void` | No | Success callback | | `onError` | `(error: Error) => void` | No | Error callback | | `qrProps` | `QRCodeSVGProps` | No | Props for QR code customization | | `loader` | `ReactNode` | No | Custom loading indicator | | `className` | `string` | No | CSS class for the link wrapper | | `style` | `CSSProperties` | No | Inline styles | #### Customization Examples **Custom QR Code Styling** ```tsx ``` **Custom Loading State** ```tsx Generating QR code... } /> ``` **Styling the Component** ```tsx ``` ## Hooks ### useUnforgettableLink A React hook that generates a recovery link and handles polling. #### Basic Usage ```tsx import { useUnforgettableLink } from '@rarimo/unforgettable-sdk-react' import { RecoveryFactor } from '@rarimo/unforgettable-sdk' import { QRCodeSVG } from 'qrcode.react' function RecoveryComponent() { const [recoveredKey, setRecoveredKey] = React.useState(null) const recoveryLink = useUnforgettableLink({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image], pollingInterval: 3000, onSuccess: (privateKey) => { setRecoveredKey(privateKey) alert('Recovery successful!') }, onError: (error) => { console.error('Recovery error:', error) }, }) return ( Scan to Recover {recoveryLink ? ( Open in browser ) : ( Generating link... )} {recoveredKey && ( Recovery Complete! Key: {recoveredKey.slice(0, 10)}... )} ) } ``` #### Parameters ```typescript interface UseUnforgettableLinkOptions { mode: 'create' | 'restore' appUrl?: string apiUrl?: string factors?: RecoveryFactor[] walletAddress?: string group?: string customParams?: Record pollingInterval?: number pollingDisabled?: boolean onSuccess?: (privateKey: string) => void onError?: (error: Error) => void } ``` #### Returns - `string` - The recovery URL (empty string while loading) ## Complete Examples ### Create Recovery Flow ```tsx import { useState } from 'react' import UnforgettableQrCode from '@rarimo/unforgettable-sdk-react' import { RecoveryFactor } from '@rarimo/unforgettable-sdk' function CreateRecovery() { const [privateKey, setPrivateKey] = useState(null) const [loading, setLoading] = useState(false) const handleSuccess = (key: string) => { setPrivateKey(key) setLoading(false) // Create wallet with the key createWallet(key) } const handleError = (error: Error) => { setLoading(false) alert(`Error: ${error.message}`) } const createWallet = (key: string) => { // Your wallet creation logic console.log('Creating wallet with key:', key) } return ( Set Up Account Recovery {!privateKey ? ( Scan this QR code with Unforgettable app to set up recovery Generating QR code...} /> ) : ( ✅ Recovery Set Up Successfully! Your account is now protected. )} ) } ``` ### Restore Account Flow ```tsx import { useState } from 'react' import UnforgettableQrCode from '@rarimo/unforgettable-sdk-react' import { RecoveryFactor } from '@rarimo/unforgettable-sdk' function RestoreAccount() { const [walletAddress, setWalletAddress] = useState('') const [showQR, setShowQR] = useState(false) const [recoveredKey, setRecoveredKey] = useState(null) const handleRestore = () => { if (!walletAddress) { alert('Please enter a wallet address') return } setShowQR(true) } const handleSuccess = (key: string) => { setRecoveredKey(key) // Restore wallet access restoreWalletAccess(key) } const restoreWalletAccess = (key: string) => { console.log('Restoring wallet with key:', key) // Import wallet, authenticate user, etc. } return ( Restore Your Account {!showQR ? ( setWalletAddress(e.target.value)} /> Restore Account ) : !recoveredKey ? ( Scan this QR code to restore your account alert(error.message)} qrProps={{ size: 300 }} /> setShowQR(false)}>Cancel ) : ( ✅ Account Restored! Welcome back! )} ) } ``` ### Advanced: Manual Polling Control ```tsx import { useUnforgettableLink } from '@rarimo/unforgettable-sdk-react' import { useState, useCallback } from 'react' function ManualPolling() { const [isPolling, setIsPolling] = useState(false) const recoveryLink = useUnforgettableLink({ mode: 'create', pollingDisabled: !isPolling, // Control polling manually onSuccess: (key) => { console.log('Got key:', key) setIsPolling(false) }, }) const startPolling = useCallback(() => { setIsPolling(true) }, []) return ( {!isPolling ? ( I've scanned the QR code ) : ( Waiting for recovery... )} ) } ``` ## Styling The component renders a standard `` tag wrapping an SVG QR code. You can style it with CSS: ```css .recovery-qr { display: block; max-width: 300px; margin: 2rem auto; padding: 1rem; border: 2px solid #e0e0e0; border-radius: 12px; background: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transition: transform 0.2s; } .recovery-qr:hover { transform: scale(1.02); } .recovery-qr svg { display: block; width: 100%; height: auto; } ``` ## TypeScript Support Full TypeScript support with type definitions: ```tsx import type { RecoveryFactor } from '@rarimo/unforgettable-sdk' import type { UnforgettableQrCodeProps } from '@rarimo/unforgettable-sdk-react' const props: UnforgettableQrCodeProps = { mode: 'create', factors: [RecoveryFactor.Face], onSuccess: (key: string) => console.log(key), } ``` ## Next Steps - [React Native Integration](/sdk/platforms/react-native) - Mobile apps - [Advanced: Architecture](/sdk/advanced/architecture) - How it works - [API Reference](/sdk/api/unforgettable-sdk) - Complete API docs --- ## Web # Web Learn how to integrate Unforgettable SDK into your vanilla JavaScript or TypeScript web application. :::info Platform-Specific Approach Web applications display QR codes for users to scan with their mobile device. For mobile apps (Android, iOS, React Native), the recovery URL is opened in a WebView for in-app recovery. ::: ## Installation import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```bash npm install @rarimo/unforgettable-sdk ``` ```bash yarn add @rarimo/unforgettable-sdk ``` ```bash pnpm add @rarimo/unforgettable-sdk ``` ## Quick Start Here's a minimal example to get you started: ```typescript import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk' import QRCode from 'qrcode' async function createRecovery() { // 1. Initialize SDK const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], }) // 2. Generate and display QR code const recoveryUrl = await sdk.getRecoveryUrl() const canvas = document.getElementById('qr-canvas') await QRCode.toCanvas(canvas, recoveryUrl, { width: 300 }) // 3. Poll for recovered key const maxAttempts = 60 let attempts = 0 while (attempts setTimeout(resolve, 3000)) } else { throw error } } } } createRecovery() ``` ## Basic Setup ### Import the SDK ```typescript import { UnforgettableSdk, RecoveryFactor } from '@rarimo/unforgettable-sdk' ``` ### Initialize the SDK ```typescript const sdk = new UnforgettableSdk({ mode: 'create', // or 'restore' factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], walletAddress: '0x...', // Required for 'restore' mode group: 'my-web-app', // Optional customParams: { theme: 'dark' }, // Optional }) ``` ## Creating a Recovery Flow ### Step 1: Generate QR Code First, get the recovery URL and display it as a QR code: ```typescript import QRCode from 'qrcode' // Install: npm install qrcode async function displayRecoveryQR() { const sdk = new UnforgettableSdk({ mode: 'create', factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], }) const recoveryUrl = await sdk.getRecoveryUrl() // Display QR code in a canvas element const canvas = document.getElementById('qr-canvas') await QRCode.toCanvas(canvas, recoveryUrl, { width: 300, margin: 2, }) // Or generate as data URL for an image const dataUrl = await QRCode.toDataURL(recoveryUrl) document.getElementById('qr-image').src = dataUrl } ``` ### Step 2: Poll for Recovery Key ```typescript async function pollForRecoveryKey(sdk: UnforgettableSdk) { const maxAttempts = 60 // 3 minutes let attempts = 0 while (attempts setTimeout(resolve, 3000)) } else { throw error } } } throw new Error('Recovery timeout') } ``` ## Complete Example Here's a complete HTML + TypeScript example: ### HTML ```html Unforgettable Recovery .container { max-width: 600px; margin: 50px auto; text-align: center; font-family: Arial, sans-serif; } .qr-container { margin: 20px 0; padding: 20px; border: 2px solid #ddd; border-radius: 8px; } .status { padding: 10px; margin: 10px 0; border-radius: 4px; } .loading { background: #fff3cd; } .success { background: #d4edda; } .error { background: #f8d7da; } button { padding: 10px 20px; font-size: 16px; cursor: pointer; border-radius: 4px; border: none; background: #007bff; color: white; } button:disabled { background: #ccc; cursor: not-allowed; } Account Recovery Create Recovery Restore Account Scan QR Code Or click the link below: Open Recovery Page ``` ### TypeScript (main.ts) ```typescript import { UnforgettableSdk, RecoveryFactor, NotFoundError } from '@rarimo/unforgettable-sdk' import QRCode from 'qrcode' class RecoveryApp { private sdk: UnforgettableSdk | null = null private polling = false constructor() { this.setupEventListeners() } private setupEventListeners() { document.getElementById('createBtn')?.addEventListener('click', () => { this.startRecovery('create') }) document.getElementById('restoreBtn')?.addEventListener('click', () => { const walletAddress = prompt('Enter wallet address to restore:') if (walletAddress) { this.startRecovery('restore', walletAddress) } }) } private async startRecovery(mode: 'create' | 'restore', walletAddress?: string) { try { this.updateStatus('Initializing...', 'loading') // Initialize SDK this.sdk = new UnforgettableSdk({ mode, factors: [RecoveryFactor.Face, RecoveryFactor.Image, RecoveryFactor.Password], walletAddress, group: 'web-demo', }) // Generate and display QR code const recoveryUrl = await this.sdk.getRecoveryUrl() await this.displayQRCode(recoveryUrl) this.updateStatus('Waiting for user to complete recovery...', 'loading') // Start polling const recoveryKey = await this.pollForKey() if (recoveryKey) { this.updateStatus(`✅ Recovery successful! Key: ${recoveryKey.slice(0, 10)}...`, 'success') // Use the recovery key for your application this.onRecoverySuccess(recoveryKey) } } catch (error) { this.updateStatus(`❌ Error: ${error.message}`, 'error') console.error(error) } } private async displayQRCode(url: string) { const container = document.getElementById('qrContainer') const canvas = document.getElementById('qrCanvas') as HTMLCanvasElement const link = document.getElementById('recoveryLink') as HTMLAnchorElement if (canvas && link && container) { await QRCode.toCanvas(canvas, url, { width: 300 }) link.href = url container.style.display = 'block' } } private async pollForKey(): Promise { if (!this.sdk) return null const maxAttempts = 60 let attempts = 0 while (attempts setTimeout(resolve, 3000)) } else { throw error } } } throw new Error('Recovery timeout - user did not complete the process') } private updateStatus(message: string, type: 'loading' | 'success' | 'error') { const statusEl = document.getElementById('status') if (statusEl) { statusEl.textContent = message statusEl.className = `status ${type}` } } private onRecoverySuccess(key: string) { // Implement your logic here // For example: create a wallet, restore user access, etc. console.log('Recovery key available for use:', key) } } // Initialize the app new RecoveryApp() ``` ## Advanced Features ### Custom API and App URLs ```typescript const sdk = new UnforgettableSdk({ mode: 'create', appUrl: 'https://custom.unforgettable.app', apiUrl: 'https://api.custom.unforgettable.app', }) ``` ### Error Handling ```typescript import { NotFoundError } from '@rarimo/unforgettable-sdk' try { const key = await sdk.getRecoveredKey() } catch (error) { if (error instanceof NotFoundError) { console.log('Data not ready yet') } else if (error.name === 'NetworkError') { console.log('Network issue') } else { console.log('Unknown error:', error) } } ``` ## TypeScript Support The SDK is written in TypeScript and includes full type definitions: ```typescript import type { UnforgettableSdkOptions, RecoveredData, UnforgettableMode, } from '@rarimo/unforgettable-sdk' const options: UnforgettableSdkOptions = { mode: 'create', factors: [RecoveryFactor.Face], } ``` ## Browser Compatibility The SDK requires browsers that support: - Web Crypto API - ES2020+ JavaScript features - Async/await Supported browsers: - Chrome 80+ - Firefox 78+ - Safari 14+ - Edge 80+ ## Next Steps - [React Integration](/sdk/platforms/react) - Use React components - [Advanced: Encryption](/sdk/advanced/encryption) - Understand the cryptography - [API Reference](/sdk/api/unforgettable-sdk) - Complete API documentation --- ## Smart contract reference import OutLink from '@site/src/components/OutLink' ## Overview Unforgettable Contracts is a set of Solidity smart contracts that power account and vault access recovery for the Unforgettable protocol. The system supports subscription-gated recovery methods and cross-chain synchronization of subscription state, enabling the same recovery guarantees to apply across multiple networks. Contracts are grouped by functional domains, such as accounts, recovery, subscriptions, cross-chain messaging, and vaults, to make it easy to navigate the architecture and identify relevant integration and extension points. ## Deployed addresses ### Ethereum Mainnet | Contract | Address | |---|---| | `VaultFactory` | `0x54C239E71af51Fc141A2BDf5469dc992b7256AD8` | | `VaultSubscriptionManager` | `0xdc8838478f49C212e68fD8b538B90c81D0f47621` | :::note `VaultFactory` is deployed deterministically (via CREATE3 / CreateX in this repo’s deployment approach), so **its address is intended to match across supported chains**. Other contracts may be chain-specific. ::: ### Deterministic deployment salt To lock deterministic deployment to a specific protocol deployer, the deployment uses: - **Salt:** `0x00D37f35Ec44ecC4e2F54de1FA3208F73d632E5900004e6f6e204f626c697461` - **Protocol deployer address:** `0x00D37f35Ec44ecC4e2F54de1FA3208F73d632E59` --- ## Contract map ### Accounts - `Account` — Minimal EIP-7702 smart account integrating recovery. - `AccountSubscriptionManager` — Subscription manager scoped to a smart account. - `Base7702AccountRecovery` — Base recovery logic for EIP-7702 accounts. --- ### Recovery core - `RecoveryManager` — Central recovery coordinator (strategy-based). - `ARecoveryStrategy` — Abstract strategy interface / base implementation. - `SignatureRecoveryStrategy` — EIP-712 signature-based recovery strategy. --- ### Subscription core - `BaseSubscriptionManager` — Subscription manager coordinating multiple modules. - `BaseSideChainSubscriptionManager` — Base subscription manager for side-chains. #### Subscription modules - `BaseSubscriptionModule` — Base extension hook for subscription logic. - `CrossChainModule` — Cross-chain subscription synchronization module. - `SBTDiscountModule` — Discounts based on supported SBT ownership. - `SBTPaymentModule` — Subscription payments using SBTs mapped to fixed durations. - `SignatureSubscriptionModule` — EIP-712 permit-style subscription extension. - `TokensPaymentModule` — Payments in ETH or ERC-20 tokens. ### Cross-chain subscription sync via Wormhole - `SubscriptionsSynchronizer` — Publishes subscription state updates across chains. - `SubscriptionsStateReceiver` — Receives and processes cross-chain subscription updates. - `SideChainSubscriptionManager` — Side-chain manager with cross-chain sync support. ### Helper data - `HelperDataFactory` — Registration + helper-data part submissions + metadata/subscription tracking. - `HelperDataRegistry` — Helper-data storage updated via EIP-712 signatures. ### Gnosis Safe integration - `UnforgettableRecoveryModule` — Safe module enabling recovery by swapping a specified owner. ### Tokens - `SignatureSBT` — SBT with signature-based minting. - `ReservedRMO` — ERC-20 granting each vault a one-time reserved token allocation. ### Vaults - `Vault` — Vault controlled with a master-key EIP-712 signature. - `VaultFactory` — Deterministic factory for vaults (incl. registry of unchangeable vault names). - `VaultSubscriptionManager` — Subscription manager scoped to a vault. ### Libraries - `EIP712SignatureChecker` — Wrapper around OpenZeppelin `SignatureChecker` for EIP-712 validation flows. - `TokensHelper` — ETH / ERC-20 transfer helpers. ## Practical integration entrypoints - **Vault lifecycle:** `VaultFactory` → `Vault` - **Vault subscriptions:** `VaultSubscriptionManager` (plus subscription modules where relevant) - **Recovery:** `RecoveryManager` (configured with one or more strategies) - **Smart accounts:** `Account` + `Base7702AccountRecovery` + `AccountSubscriptionManager` - **Cross-chain subscriptions:** `SubscriptionsSynchronizer` + `SubscriptionsStateReceiver` ## Extension points - **Add a new recovery method:** implement a new `ARecoveryStrategy` and wire it into `RecoveryManager`. - **Add a new subscription payment/extension path:** implement `BaseSubscriptionModule` and register it in your subscription manager. - **Add cross-chain behaviors:** extend `CrossChainModule` and/or the synchronizer/receiver logic.