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:
- 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.
When a payer resolves user@satolink.com:
- The server queries the DB for the next unused Liquid address in the recipient's pre-generated pool.
- The server requests a Submarine Swap from the liquidity provider (Boltz). The swap links the recipient's Liquid address to a Lightning Invoice.
- 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.
- 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.
| Endpoint | Method | Request Payload | Response / Verification Rule |
|---|---|---|---|
/api/v1/user/wallet/challenge | POST | { "walletID": "string" } | Returns a hex-encoded challenge. Expire in Redis after 300 seconds. |
/api/v1/user/wallet/access | POST | { "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
registerevent with a valid JWT token. - Security Disconnect: If the server detects that the JWT has expired, it emits an
auth_errorcodeTOKEN_EXPIREDand 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.
| Component | Algorithm | Key / Entropy Size | Purpose |
|---|---|---|---|
| Seed Generation | BIP-39 | 128 bits | Derives a deterministic wallet mnemomic. |
| Local Key Stretching | PBKDF2-SHA256 | 256 bits output, 100,000 iterations | Stretches passwords to defend against local dictionary extraction. |
| Local Wallet Cifrado | AES-GCM | 256 bits key, 12-byte random IV | Secures the seed phrase in IndexedDB with authenticated integrity. |
| Challenge Signing | ECDSA (secp256k1) | 256 bits | Signs the challenge to authenticate wallet identity trustlessly. |
| Authentication Hash | SHA-256 | 256 bits output | Hashes password and wallet ID to authenticate on-chain logins. |
| HTTP Transit Security | TLS 1.3 | Varies (ECC based) | Prevents eavesdropping and man-in-the-middle attacks. |