Technical Specification

Technical & Security Architecture

An audit-level technical specification of SatoLink's local vault encryption, key derivation, and trustless routing protocol.

1. Client-Side Seed Generation

The creation of the user's wallet is processed entirely on the client side using the browser's cryptographic sandbox. SatoLink does not generate, witness, or store seed phrases.

  • Entropy Source: Standard Web Crypto API crypto.getRandomValues, generating 128 bits of high-entropy cryptographic bytes.
  • Checksum calculation: SHA-256 is executed over the 128-bit entropy. The first 4 bits of the hash are appended as a checksum.
  • Dictionary Mapping: The resulting 132 bits are split into 12 segments of 11 bits each, which map directly to the official **BIP-39 English wordlist**.
  • Library: @scure/bip39 (developed by Paul Miller/Noble Cryptography), an audited dependency-free package ensuring mathematical correctness.

2. Local Cryptographic Vault (Cifrado & Storage)

Once the 12-word mnemonic is generated or imported, it is protected with symmetric authenticated encryption before being saved on physical storage.

Vault Derivation and Cifrado Details:

1. Key Derivation (PBKDF2)

To protect the local wallet from offline brute-force attacks if a device is compromised, we stretch the user-defined password:

$$\text{Derived Key} = \text{PBKDF2-SHA256}(\text{Password}, \text{Salt}, 100000)$$
  • Iterations: 100,000 rounds.
  • Salt: 16-byte cryptographically-random salt unique to each wallet.
  • Derived Key Size: 256 bits (suitable for AES-256).
2. Authenticated Encryption (AES-GCM 256)

The mnemonic string is encrypted using the derived key via AES-GCM-256 (Galois/Counter Mode).

  • Initialization Vector (IV): A unique, random 12-byte IV is generated for every encryption run.
  • Integrity Tag: GCM automatically appends a 128-bit authentication tag, guaranteeing that any tempering with the local store will prevent decryption.
3. Persistent Storage (IndexedDB)

The encrypted package is persisted in the client's browser database using IndexedDB under the Same-Origin Policy (SOP).

{
  "link": "username",
  "walletData": {
    "encryptedMnemonic": "Base64(Ciphertext)",
    "salt": "Base64(Salt)",
    "iv": "Base64(IV)"
  }
}

3. Breez Liquid SDK Node Instance

SatoLink compiles Rust-native nodes into **WebAssembly (WASM)** modules via @breeztech/breez-sdk-liquid, permitting the browser to act as a light node on the Liquid sidechain.

Liquid Mainnet

Connects to the Liquid mainnet. The SDK handles a secure, sandboxed client-side database at ./breez-liquid-mainnet-data to store wallet states, UTXOs, and swap logs.

Liquid Regtest (SatoLink Testing Environment)

For zero-risk development and testing, SatoLink hosts a private testing network utilizing dedicated nodes and indexes:

  • Liquid Indexer (Esplora): https://app.satolink.com/waterfalls/
  • Bitcoin Indexer (Esplora): https://app.satolink.com/esplora/
  • Submarine Swap Gateway (Boltz): https://app.satolink.com/swapproxy/

4. Submarine Swaps & LNURL-pay (Trustless Routing)

Since the user's keys are only on their client device, they cannot sign transactions when offline. SatoLink overcomes this limitation through **Submarine Swaps** (atomic swaps) coordinated over LNURL-pay.

Submarine Swap Payment Routing Flow
Lightning PayerSatoLink API GatewayBoltz (Swap Proxy)Liquid Client Node1. Pays username@satolink.com2. Request Swap (Uses Pool Address)3. Return Invoice + Swap HTLC4. Return Invoice (LNURL)5. Pay Lightning Invoice6. Settle Liquid BTC/USDT

When a payer resolves user@satolink.com:

  1. The server queries the DB for the next unused Liquid address in the recipient's pre-generated pool.
  2. The server requests a Submarine Swap from the liquidity provider (Boltz). The swap links the recipient's Liquid address to a Lightning Invoice.
  3. The server serves the Bolt11 invoice to the payer. Once paid, the swap provider automatically locks the funds into a Hash Time-Locked Contract (HTLC) on-chain on the Liquid sidechain.
  4. When the receiver opens their wallet client, the Breez SDK scans the Liquid blockchain, discovers the pending HTLC, reveals the local preimage, and claims the Liquid BTC/USDT directly to their local custody.

5. Challenge-Response API Specification

The authentication API endpoints expect structured parameters to verify the user's signature without central storage of passwords.

EndpointMethodRequest PayloadResponse / Verification Rule
/api/v1/user/wallet/challengePOST{ "walletID": "string" }Returns a hex-encoded challenge. Expire in Redis after 300 seconds.
/api/v1/user/wallet/accessPOST{ "walletID": "string", "authhash": "string", "challenge": "string", "signature": "string", "publicKey": "string" }Verifies that:
1. Challenge exists and hasn't expired.
2. ECDSA signature is valid for publicKey.
3. authhash matches the user record.

6. Axios JWT Refresh Interceptor

To prevent session expirations from interrupting user operations, the client HTTP client (Axios) incorporates a queue-based request interceptor.

// Simplified Axios Interceptor Flow
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      // Pause incoming requests and queue them
      if (!isRefreshing) {
        isRefreshing = true;
        try {
          const { data } = await axios.post('/api/v1/user/refresh');
          const newAccessToken = data.accessToken;
          
          store.setAccessToken(newAccessToken);
          isRefreshing = false;
          
          // Replay all waiting requests with the updated token
          onRefreshed(newAccessToken);
          return axios(originalRequest);
        } catch (refreshError) {
          store.logout();
          return Promise.reject(refreshError);
        }
      }
      
      return new Promise((resolve) => {
        addSubscriber((token) => {
          originalRequest.headers['Authorization'] = 'Bearer ' + token;
          resolve(axios(originalRequest));
        });
      });
    }
    return Promise.reject(error);
  }
);

7. WebSocket Security & Sockets.io Core

WebSocket channels must be protected with short-lived tokens. The connection setup requires an authentication handshake before any events are processed.

  • Transport Limit: Configured to use only transports: ["websocket"].
  • Connection Handshake: On initial connection, the client must trigger the register event with a valid JWT token.
  • Security Disconnect: If the server detects that the JWT has expired, it emits an auth_error code TOKEN_EXPIRED and forcefully closes the socket.
  • Automatic Recovery: Upon receiving auth_error, the client handles the exception by requesting a refreshed JWT via the API, re-establishes the socket connection, and registers again.

8. Cryptographic Summary Table

A high-level reference of cryptographic algorithms and parameters implemented across the SatoLink system, in alignment with NIST standards.

ComponentAlgorithmKey / Entropy SizePurpose
Seed GenerationBIP-39128 bitsDerives a deterministic wallet mnemomic.
Local Key StretchingPBKDF2-SHA256256 bits output, 100,000 iterationsStretches passwords to defend against local dictionary extraction.
Local Wallet CifradoAES-GCM256 bits key, 12-byte random IVSecures the seed phrase in IndexedDB with authenticated integrity.
Challenge SigningECDSA (secp256k1)256 bitsSigns the challenge to authenticate wallet identity trustlessly.
Authentication HashSHA-256256 bits outputHashes password and wallet ID to authenticate on-chain logins.
HTTP Transit SecurityTLS 1.3Varies (ECC based)Prevents eavesdropping and man-in-the-middle attacks.