Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/satsigner/satsigner/llms.txt

Use this file to discover all available pages before exploring further.

Overview

SatSigner implements multiple layers of cryptographic protection to secure your Bitcoin wallet data. This document provides technical details of the encryption systems protecting your funds.

Encryption Architecture

Multi-Layer Security Model

SatSigner employs a defense-in-depth strategy: Layer 1: Platform Security
  • iOS Keychain (hardware-backed when available)
  • Android KeyStore (hardware-backed on supported devices)
  • Secure Enclave / TEE integration
Layer 2: Application Encryption
  • AES-256-CBC for sensitive data
  • PBKDF2 for key derivation
  • Per-account initialization vectors
Layer 3: Data Segregation
  • Sensitive data in secure storage
  • Non-sensitive data in MMKV
  • Clear separation of plaintext and ciphertext

Symmetric Encryption (AES-256-CBC)

Algorithm Details

Advanced Encryption Standard (AES):
  • Key Size: 256 bits
  • Block Size: 128 bits (16 bytes)
  • Mode: CBC (Cipher Block Chaining)
  • Padding: PKCS#7
Implementation (apps/mobile/utils/crypto.ts:30-36):
function aesEncrypt(text: string, key: string, iv: string) {
  return aesCrypto.encrypt(text, key, iv, 'aes-256-cbc')
}

function aesDecrypt(cipher: string, key: string, iv: string) {
  return aesCrypto.decrypt(cipher, key, iv, 'aes-256-cbc')
}
Library: react-native-aes-crypto
  • Native implementation (not JavaScript)
  • Platform-optimized cryptographic operations
  • Hardware acceleration where available

Why AES-256-CBC?

Advantages:
  • ✓ Industry standard, widely audited
  • ✓ NIST approved for TOP SECRET data
  • ✓ Hardware acceleration available
  • ✓ Well-understood security properties
  • ✓ Compatible with most platforms
CBC Mode Characteristics:
  • Each block depends on previous block
  • Requires initialization vector (IV)
  • Parallel decryption possible
  • No built-in authentication (relies on key verification)
Alternative Modes Considered:
  • GCM: Authenticated encryption, but more complex implementation
  • CTR: Streaming mode, but requires careful nonce handling
  • ECB: Never considered (vulnerable to pattern analysis)

Initialization Vectors (IV)

Purpose:
  • Ensures same plaintext produces different ciphertext each time
  • Prevents pattern recognition in encrypted data
  • Must be unique per encryption operation
Generation (apps/mobile/utils/crypto.ts:17-19):
function randomIv() {
  return uuid.v4().replace(/-/g, '') // 32-character hex string
}
Properties:
  • 32-character hexadecimal string (128 bits)
  • Cryptographically random
  • Unique per account
  • Stored alongside encrypted data (not secret)
Storage (apps/mobile/types/models/Account.ts:64):
type Key = {
  secret: Secret | string  // Encrypted data
  iv: string               // Initialization vector
  // ...
}

Key Derivation (PBKDF2)

Algorithm Details

Password-Based Key Derivation Function 2 (PBKDF2): Implementation (apps/mobile/utils/crypto.ts:39-41):
function pbkdf2Encrypt(pin: string, salt: string) {
  return aesCrypto.pbkdf2(pin, salt, 10_000, 256, 'sha256')
}
Parameters:
  • Input: User’s 4-digit PIN
  • Salt: 16-byte random value
  • Iterations: 10,000
  • Output Length: 256 bits
  • Hash Function: SHA-256

Security Properties

Iteration Count (10,000): Purpose:
  • Slow down brute-force attacks
  • Each PIN attempt takes measurable time
  • Negligible delay for legitimate user
  • Significant delay for attacker trying many PINs
Attack Resistance:
  • 10,000 PINs possible (0000-9999)
  • At ~1ms per iteration on mobile device
  • Brute force all PINs: ~100 seconds
  • With rate limiting: effectively impossible
Why 10,000 Iterations?:
  • Balance between security and UX
  • Mobile devices have limited power
  • Higher iterations would cause noticeable delay
  • Supplemented by attempt limiting (5 max attempts)

Salt Generation

Purpose:
  • Prevents pre-computed rainbow table attacks
  • Ensures identical PINs produce different keys
  • Unique per installation
Generation (apps/mobile/utils/crypto.ts:43-45):
function generateSalt() {
  return aesCrypto.randomKey(16) // 16 bytes = 128 bits
}
Properties:
  • 128-bit random value
  • Generated at PIN setup
  • Stored in secure storage
  • Never changes unless PIN reset
Storage:
// Secure storage keys (apps/mobile/config/auth.ts)
SALT_KEY: 'satsigner_salt'
PIN_KEY: 'satsigner_pin'         // PBKDF2 output
DURESS_PIN_KEY: 'satsigner_duress_pin'

Secure Storage Implementation

Expo Secure Store

Implementation (apps/mobile/storage/encrypted.ts):
import * as SecureStore from 'expo-secure-store'

async function setItem(key: string, value: string): Promise<void> {
  const vKey = `${VERSION}_${key}`
  await SecureStore.setItemAsync(vKey, value)
}

async function getItem(key: string): Promise<string | null> {
  const vKey = `${VERSION}_${key}`
  return SecureStore.getItemAsync(vKey)
}
Features:
  • Automatic versioning (supports migration)
  • Platform-specific secure storage
  • Encrypted at rest
  • Protected by device unlock

Platform-Specific Storage

iOS: Keychain

Security Features:
  • Hardware-backed encryption (Secure Enclave)
  • Tied to device unlock state
  • iCloud Keychain sync (disabled by default)
  • Access control lists
Protection Classes:
  • Default: kSecAttrAccessibleWhenUnlocked
  • Data accessible only when device unlocked
  • Encrypted using key protected by device passcode
Secure Enclave:
  • AES-256 encryption in hardware
  • Key material never leaves Secure Enclave
  • Protection against physical attacks
  • Available on iPhone 5s and newer

Android: SharedPreferences + EncryptedSharedPreferences

Security Features:
  • Master key in Android KeyStore
  • Hardware-backed when available (TEE/Strongbox)
  • AES-256-GCM encryption
  • Key attestation support
KeyStore Protection:
  • Keys generated in hardware (supported devices)
  • Protected by device lock screen
  • Cannot be extracted from device
  • Deleted on factory reset
Hardware Backing:
  • Strongbox: Dedicated security chip (Pixel 3+)
  • TEE: Trusted Execution Environment
  • Software: Fallback for older devices

MMKV Storage

Implementation (apps/mobile/storage/mmkv.ts):
import { MMKV } from 'react-native-mmkv'

const storage = new MMKV({ id: 'mmkv.satsigner' })
Purpose:
  • High-performance key-value storage
  • Application state and preferences
  • Non-sensitive data only
What is Stored:
  • Account metadata (names, IDs)
  • Public keys and addresses
  • Transaction history (no signing keys)
  • Application settings
  • UI preferences
Security Note:
  • NOT encrypted by default
  • Relies on device-level encryption
  • Protected by OS sandboxing
  • Never stores sensitive key material

Data Classification

Highly Sensitive (Secure Storage + AES)

Encrypted with PIN-derived key:
type Secret = {
  mnemonic?: string              // BIP39 seed phrase
  passphrase?: string            // BIP39 passphrase
  // Stored encrypted in Key.secret field
}
Protection:
  • AES-256-CBC encryption
  • Key derived from PIN via PBKDF2
  • Unique IV per account
  • Stored in Keychain/KeyStore

Sensitive (Secure Storage Only)

Stored in platform secure storage without additional encryption:
// Authentication data
PIN_KEY: Hashed PIN (PBKDF2 output)
DURESS_PIN_KEY: Hashed duress PIN
SALT_KEY: PBKDF2 salt
Protection:
  • Platform secure storage
  • Hardware-backed encryption
  • Device unlock required

Non-Sensitive (MMKV)

Plaintext storage (relies on device encryption):
  • Public keys (xpub, ypub, zpub)
  • Addresses
  • Transaction history
  • UTXOs (no spending ability)
  • Account names and labels
  • Application settings
Rationale:
  • Public data by design
  • No risk if exposed
  • Performance critical
  • Large data volumes

Encryption Operations

Account Creation Flow

  1. Generate Seed
    mnemonic = generateMnemonic(wordCount)
    
  2. Create Secret Object
    const secret: Secret = {
      mnemonic: mnemonic,
      passphrase: passphrase || undefined,
      fingerprint: fingerprint
    }
    
  3. Serialize Secret
    const secretJson = JSON.stringify(secret)
    
  4. Generate IV
    const iv = randomIv()
    
  5. Encrypt with PIN
    const pin = await getItem(PIN_KEY)
    const encrypted = await aesEncrypt(secretJson, pin, iv)
    
  6. Store Encrypted Data
    key.secret = encrypted  // Ciphertext
    key.iv = iv             // Initialization vector
    

Transaction Signing Flow

  1. Retrieve Encrypted Secret
    const encrypted = key.secret as string
    const iv = key.iv
    
  2. Get PIN for Decryption
    const pin = await getItem(PIN_KEY)
    
  3. Decrypt Secret
    const secretJson = await aesDecrypt(encrypted, pin, iv)
    const secret = JSON.parse(secretJson) as Secret
    
  4. Use Mnemonic
    const seed = mnemonicToSeed(secret.mnemonic, secret.passphrase)
    const privateKey = derivePrivateKey(seed, path)
    
  5. Sign Transaction
    const signature = sign(transactionHash, privateKey)
    
  6. Zero Sensitive Data
    // secret, seed, privateKey cleared from memory
    // (JavaScript limitations on secure memory clearing)
    

PIN Change Flow

Re-encryption Required (apps/mobile/hooks/useReEncryptAccounts.ts):
  1. Validate Old PIN
    const isValid = await validatePin(oldPin)
    
  2. Decrypt All Secrets with Old PIN
    for (const key of account.keys) {
      const secretJson = await aesDecrypt(key.secret, oldPin, key.iv)
      secrets.push(JSON.parse(secretJson))
    }
    
  3. Set New PIN
    const newSalt = await generateSalt()
    const newPinHash = await pbkdf2Encrypt(newPin, newSalt)
    await setItem(SALT_KEY, newSalt)
    await setItem(PIN_KEY, newPinHash)
    
  4. Re-encrypt All Secrets with New PIN
    for (let i = 0; i < secrets.length; i++) {
      const secretJson = JSON.stringify(secrets[i])
      const newIv = randomIv()
      const encrypted = await aesEncrypt(secretJson, newPinHash, newIv)
      account.keys[i].secret = encrypted
      account.keys[i].iv = newIv
    }
    
  5. Update Account Storage
    updateAccount(account)
    
Security Notes:
  • Brief moment where both old and new keys exist
  • Operation is atomic (all or nothing)
  • No intermediate state persisted
  • Failure requires retry with old PIN

Duress PIN Implementation

Architecture

Dual PIN System:
// Normal PIN unlocks wallet
PIN_KEY: 'satsigner_pin'

// Duress PIN triggers data deletion
DURESS_PIN_KEY: 'satsigner_duress_pin'
Both PINs:
  • Use same PBKDF2 derivation
  • Share same salt
  • Produce different hashes
  • Stored separately in secure storage

Duress PIN Behavior

Unlock Flow (apps/mobile/app/unlock.tsx:63-78):
const encryptedPin = await pbkdf2Encrypt(pin, salt)
const storedEncryptedPin = await getItem(PIN_KEY)
const storedEncryptedDuressPin = await getItem(DURESS_PIN_KEY)

if (encryptedPin === storedEncryptedDuressPin && duressPinEnabled) {
  // Duress PIN entered
  deleteAccounts()  // Delete all Bitcoin accounts
  deleteWallets()   // Delete all wallet data
  deleteTags()      // Delete metadata
  
  // Hide evidence of duress PIN
  setDuressPinEnabled(false)
  await deleteItem(DURESS_PIN_KEY)
  await setItem(PIN_KEY, storedEncryptedDuressPin)
  
  // App appears normal but empty
}
Design Goals:
  • Plausible deniability
  • No indication duress PIN exists
  • Irreversible data deletion
  • Appears as normal unlock
Irreversible: Duress PIN permanently deletes all wallet data. Only use in genuine emergency. Funds can only be recovered from seed phrase backup.

Cryptographic Primitives

Random Number Generation

Implementation (apps/mobile/utils/crypto.ts:21-24):
function randomNum() {
  return crypto.getRandomValues(new Uint32Array(1))[0] / MAX_UINT32
}
Source: react-native-get-random-values
  • Polyfill for Web Crypto API
  • Platform-specific CSPRNG:
    • iOS: SecRandomCopyBytes
    • Android: SecureRandom
Quality:
  • Cryptographically Secure Pseudo-Random Number Generator (CSPRNG)
  • Hardware-backed when available
  • Suitable for key generation
  • Passes statistical randomness tests

Hashing (SHA-256)

Implementation (apps/mobile/utils/crypto.ts:26-28):
function sha256(text: string) {
  return aesCrypto.sha256(text)
}
Uses:
  • Key fingerprint calculation
  • Cryptographic commitments
  • Data integrity verification
  • Bitcoin address generation
Properties:
  • 256-bit output
  • Collision resistant
  • Pre-image resistant
  • Avalanche effect

Security Considerations

Memory Protection

Limitations:
  • JavaScript doesn’t guarantee memory clearing
  • Sensitive data may persist in memory
  • Garbage collection timing unpredictable
  • No explicit memory zeroing
Mitigations:
  • Minimize time secrets in memory
  • Avoid string concatenation of secrets
  • Rely on device-level protection
  • Process isolation (OS sandboxing)

Side-Channel Attacks

Timing Attacks:
  • PBKDF2 uses constant iterations
  • AES implementation uses constant-time operations (native)
  • PIN comparison uses hashed values
Power Analysis:
  • Rely on hardware countermeasures
  • TEE/Secure Enclave protection
  • Application-level mitigation limited

Cryptanalysis Resistance

AES-256:
  • No practical attacks on full AES-256
  • Best known attack: Related-key attack (irrelevant to this use)
  • Quantum resistance: ~128-bit security (post-quantum)
PBKDF2:
  • Resistant to rainbow tables (salt)
  • Resistant to parallel attacks (independent iterations)
  • Vulnerable to ASICs/GPUs (not used here due to rate limiting)
CBC Mode:
  • Vulnerable to padding oracle attacks (mitigated by no padding verification)
  • Requires unique IV per encryption (enforced)
  • No authentication (acceptable for this use case)

Compliance & Standards

Standards Compliance

NIST:
  • AES-256: FIPS 197
  • SHA-256: FIPS 180-4
  • PBKDF2: NIST SP 800-132
RFC Compliance:
  • AES-CBC: RFC 3602
  • PBKDF2: RFC 8018
  • BIP39: Bitcoin Improvement Proposal 39
  • BIP32: Hierarchical Deterministic Wallets

Audit Recommendations

Third-Party Review:
  • Cryptographic implementation audit
  • Key management review
  • Secure storage verification
  • Side-channel testing
Open Source:
  • All crypto code auditable
  • Dependency on well-audited libraries
  • No proprietary cryptography