Local Storage Encryption Methods for Financial Data

Whistl stores sensitive financial data, transaction history, and behavioural patterns on your device. This comprehensive guide explains AES-256 encryption, SQLCipher database protection, secure enclave key storage, and how Whistl ensures your data remains private even if your device is compromised.

Why Local Encryption Matters

Financial apps store highly sensitive data locally:

  • Bank account details: Account numbers, balances
  • Transaction history: Spending patterns, merchant data
  • Behavioural data: Impulse patterns, risk scores
  • Authentication tokens: Plaid access tokens, session keys

Without encryption, anyone with physical access could extract this data.

Encryption Architecture Overview

Whistl uses multiple layers of encryption:

Defense in Depth

┌─────────────────────────────────────────────────┐
│              Application Layer                   │
│  - Field-level encryption for sensitive values   │
├─────────────────────────────────────────────────┤
│              Database Layer                      │
│  - SQLCipher full-database encryption (AES-256) │
├─────────────────────────────────────────────────┤
│              File System Layer                   │
│  - iOS Data Protection / Android File Encryption │
├─────────────────────────────────────────────────┤
│              Hardware Layer                      │
│  - Secure Enclave / Trusted Execution Environment│
└─────────────────────────────────────────────────┘

AES-256 Encryption

Advanced Encryption Standard with 256-bit keys is the gold standard:

Why AES-256?

  • Security: 2^256 possible keys (more than atoms in universe)
  • Performance: Hardware acceleration on modern CPUs
  • Standard: Used by governments, banks, military
  • Mode: GCM (Galois/Counter Mode) for authenticated encryption

iOS Implementation (CommonCrypto)

import CommonCrypto

class AES256Encryptor {
    func encrypt(plaintext: Data, key: Data) throws -> EncryptedData {
        // Generate random IV (16 bytes for AES)
        var iv = Data(count: kCCBlockSizeAES128)
        let ivResult = iv.withUnsafeMutableBytes { ivBytes in
            SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes.baseAddress!)
        }
        
        // Calculate ciphertext size
        let bufferSize = plaintext.count + kCCBlockSizeAES128
        var ciphertext = Data(count: bufferSize)
        var numBytesEncrypted: size_t = 0
        
        // Encrypt
        let status = ciphertext.withUnsafeMutableBytes { ciphertextBytes in
            plaintext.withUnsafeBytes { plaintextBytes in
                key.withUnsafeBytes { keyBytes in
                    iv.withUnsafeBytes { ivBytes in
                        CCCrypt(
                            CCOperation(kCCEncrypt),
                            CCAlgorithm(kCCAlgorithmAES),
                            CCOptions(kCCOptionGCMMode),
                            keyBytes.baseAddress, kCCKeySizeAES256,
                            ivBytes.baseAddress,
                            plaintextBytes.baseAddress, plaintext.count,
                            nil,  // AAD (none)
                            0,
                            ciphertextBytes.baseAddress!, bufferSize,
                            &numBytesEncrypted
                        )
                    }
                }
            }
        }
        
        guard status == kCCSuccess else {
            throw EncryptionError.failed(status)
        }
        
        ciphertext.count = numBytesEncrypted
        
        return EncryptedData(ciphertext: ciphertext, iv: iv)
    }
}

Android Implementation (Cipher)

import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

class AES256Encryptor {
    private val algorithm = "AES/GCM/NoPadding"
    private val gcmTagLength = 128  // bits
    private val ivSize = 12  // bytes for GCM
    
    fun encrypt(plaintext: ByteArray, key: ByteArray): EncryptedData {
        val cipher = Cipher.getInstance(algorithm)
        
        // Generate random IV
        val iv = ByteArray(ivSize)
        SecureRandom().nextBytes(iv)
        
        // Initialize cipher
        val keySpec = SecretKeySpec(key, "AES")
        val gcmSpec = GCMParameterSpec(gcmTagLength, iv)
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec)
        
        // Encrypt
        val ciphertext = cipher.doFinal(plaintext)
        val authTag = ciphertext.takeLast(gcmTagLength / 8).toByteArray()
        
        return EncryptedData(
            ciphertext = ciphertext.dropLast(gcmTagLength / 8).toByteArray(),
            iv = iv,
            authTag = authTag
        )
    }
}

SQLCipher Database Encryption

Whistl uses SQLCipher to encrypt the entire SQLite database:

What Is SQLCipher?

  • Extension: Open-source SQLite extension
  • Encryption: AES-256 in CBC mode
  • Key derivation: PBKDF2 with 256,000 iterations
  • Page-level: Each database page encrypted separately

iOS Implementation

import SQLCipher

class EncryptedDatabase {
    private var db: OpaquePointer?
    
    func open(path: String, password: String) throws {
        // Open database
        var status = sqlite3_open(path, &db)
        guard status == SQLITE_OK else {
            throw DatabaseError.openFailed(status)
        }
        
        // Set encryption key
        let keyQuery = "PRAGMA key = '\(password)';"
        status = sqlite3_exec(db, keyQuery, nil, nil, nil)
        guard status == SQLITE_OK else {
            throw DatabaseError.keyFailed(status)
        }
        
        // Verify encryption
        let verifyQuery = "SELECT count(*) FROM sqlite_master;"
        status = sqlite3_exec(db, verifyQuery, nil, nil, nil)
        guard status == SQLITE_OK else {
            throw DatabaseError.verificationFailed
        }
        
        // Configure SQLCipher settings
        sqlite3_exec(db, "PRAGMA cipher_page_size = 4096;", nil, nil, nil)
        sqlite3_exec(db, "PRAGMA kdf_iter = 256000;", nil, nil, nil)
        sqlite3_exec(db, "PRAGMA cipher_memory_security = ON;", nil, nil, nil)
    }
    
    func storeTransaction(_ transaction: Transaction) throws {
        // All data automatically encrypted by SQLCipher
        let sql = """
            INSERT INTO transactions (id, amount, merchant, mcc, category, date)
            VALUES (?, ?, ?, ?, ?, ?)
        """
        
        var statement: OpaquePointer?
        guard sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK else {
            throw DatabaseError.prepareFailed
        }
        
        sqlite3_bind_text(statement, 1, transaction.id, -1, nil)
        sqlite3_bind_double(statement, 2, transaction.amount)
        sqlite3_bind_text(statement, 3, transaction.merchant, -1, nil)
        sqlite3_bind_text(statement, 4, transaction.mcc, -1, nil)
        sqlite3_bind_text(statement, 5, transaction.category, -1, nil)
        sqlite3_bind_double(statement, 6, transaction.date.timeIntervalSince1970)
        
        guard sqlite3_step(statement) == SQLITE_DONE else {
            throw DatabaseError.insertFailed
        }
    }
}

Android Implementation

import net.sqlcipher.database.*

class EncryptedDatabase(context: Context) {
    private val db: SQLiteDatabase
    
    init {
        // Initialize SQLCipher
        SQLiteDatabase.loadLibs(context)
        
        // Open encrypted database
        val password = getEncryptionKey()
        db = SQLiteDatabase.openOrCreateDatabase(
            context.getDatabasePath("whistl.db"),
            password
        )
        
        // Configure settings
        db.execSQL("PRAGMA cipher_page_size = 4096")
        db.execSQL("PRAGMA kdf_iter = 256000")
        db.execSQL("PRAGMA cipher_memory_security = ON")
    }
    
    fun storeTransaction(transaction: Transaction) {
        val sql = """
            INSERT INTO transactions (id, amount, merchant, mcc, category, date)
            VALUES (?, ?, ?, ?, ?, ?)
        """
        
        val statement = db.compileStatement(sql)
        statement.bindString(1, transaction.id)
        statement.bindDouble(2, transaction.amount)
        statement.bindString(3, transaction.merchant)
        statement.bindString(4, transaction.mcc)
        statement.bindString(5, transaction.category)
        statement.bindDouble(6, transaction.date.time)
        
        statement.executeInsert()
    }
}

Secure Enclave Key Storage

Encryption keys are stored in hardware security modules:

iOS Secure Enclave

import Security

class SecureKeyStore {
    func generateKey(tag: String) throws -> Data {
        let attributes: [String: Any] = [
            kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeySizeInBits as String: 256,
            kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
            kSecPrivateKeyAttrs as String: [
                kSecAttrIsPermanent as String: true,
                kSecAttrApplicationTag as String: tag,
                kSecAttrAccessControl as String: createAccessControl()
            ]
        ]
        
        var error: Unmanaged?
        guard let privateKey = SecKeyCreateRandomKey(
            attributes as CFDictionary,
            &error
        ) else {
            throw KeyStoreError.generationFailed(error?.takeRetainedValue())
        }
        
        return privateKey.dataRepresentation
    }
    
    private func createAccessControl() -> SecAccessControl {
        return SecAccessControlCreateWithFlags(
            nil,
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
            [.privateKeyUsage, .biometryCurrentSet],
            nil
        )!
    }
    
    func retrieveKey(tag: String) throws -> Data {
        let query: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: tag,
            kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
            kSecReturnRef as String: true
        ]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        guard status == errSecSuccess, let key = item as? SecKey else {
            throw KeyStoreError.notFound
        }
        
        return key.dataRepresentation
    }
}

Android Keystore

import android.security.keystore.KeyGenParameterSpec
import java.security.KeyStore

class SecureKeyStore {
    private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
        load(null)
    }
    
    fun generateKey(alias: String) {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            "AndroidKeyStore"
        )
        
        val spec = KeyGenParameterSpec.Builder(
            alias,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .setUserAuthenticationRequired(true)  // Require biometric
            .setInvalidatedByBiometricEnrollment(true)
            .build()
        
        keyGenerator.init(spec)
        keyGenerator.generateKey()
    }
    
    fun getKey(alias: String): SecretKey {
        val entry = keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry
        return entry.secretKey
    }
}

Key Derivation

User passwords are converted to encryption keys using PBKDF2:

PBKDF2 Configuration

import CommonCrypto

class KeyDerivation {
    static let saltLength = 16
    static let keyLength = 32  // 256 bits
    static let iterations = 256000
    
    static func deriveKey(password: String, salt: Data) -> Data {
        var derivedKey = Data(count: keyLength)
        
        let result = derivedKey.withUnsafeMutableBytes { derivedKeyBytes in
            password.withCString { passwordPtr in
                salt.withUnsafeBytes { saltBytes in
                    CCKeyDerivationPBKDF(
                        CCPBKDFAlgorithm(kCCPBKDF2),
                        passwordPtr,
                        password.count + 1,
                        saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
                        salt.count,
                        CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
                        UInt32(iterations),
                        derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
                        keyLength
                    )
                }
            }
        }
        
        guard result == 0 else {
            fatalError("Key derivation failed")
        }
        
        return derivedKey
    }
}

Data Types Encrypted

Whistl encrypts all sensitive data at rest:

Encryption Coverage

Data TypeEncryption MethodKey Storage
Plaid access tokensAES-256-GCMSecure Enclave
Transaction dataSQLCipher (AES-256-CBC)Derived from password
Account balancesSQLCipher (AES-256-CBC)Derived from password
Behavioural patternsSQLCipher (AES-256-CBC)Derived from password
ML model weightsAES-256-GCMSecure Enclave
User preferencesNSDataProtectionSystem managed

File-Level Protection (iOS)

iOS provides file-level data protection:

Data Protection Classes

ClassAvailabilityUse Case
Complete (NSFileProtectionComplete)After first unlockMost sensitive data
Unless Open (NSFileProtectionCompleteUnlessOpen)While file openFiles in use
Until First Auth (NSFileProtectionCompleteUntilFirstUserAuthentication)After boot + unlockCached data
No Protection (NSFileProtectionNone)AlwaysPublic data only

Implementation

func saveEncryptedData(_ data: Data, to url: URL) throws {
    // Write data
    try data.write(to: url)
    
    // Set protection class
    try url.setResourceValue(
        FileProtectionType.complete,
        forKey: .protectionKey
    )
}

Security Best Practices

Whistl follows encryption best practices:

Key Management

  • Never hardcode keys: All keys generated or derived
  • Rotate keys: Periodic key rotation for long-term data
  • Separate keys: Different keys for different data types
  • Zero on deallocation: Keys cleared from memory after use

IV/Nonce Management

  • Never reuse: Fresh random IV for each encryption
  • Store with ciphertext: IV prepended to encrypted data
  • 12 bytes for GCM: Recommended size for AES-GCM

Performance Impact

Encryption has minimal performance impact:

OperationWithout EncryptionWith EncryptionOverhead
Database read (100 rows)2.3ms2.8ms+22%
Database write (1 row)0.8ms1.1ms+38%
Token storage0.1ms0.3ms+200%
App launch1.2s1.3s+8%

Conclusion

Whistl's multi-layer encryption architecture ensures financial data remains private even if devices are lost, stolen, or compromised. Through AES-256 encryption, SQLCipher database protection, and secure enclave key storage, sensitive data is protected at every level.

Your financial information stays encrypted on your device—accessible only to you.

Get Bank-Level Security

Whistl protects your financial data with AES-256 encryption and secure enclave storage. Download free and experience privacy-first protection.

Download Whistl Free

Related: Plaid Bank Integration Security | Biometric Authentication | Cloud Sync with E2E Encryption