Cloud Sync with End-to-End Encryption
Whistl offers optional cloud sync so your data is backed up and available across devices. This comprehensive guide explains end-to-end encryption architecture, key derivation, conflict resolution, and how your data remains private even from Whistl's servers.
Why End-to-End Encryption?
Cloud sync introduces security risks that E2E encryption solves:
- Server breach protection: Encrypted data is useless to attackers
- Privacy from provider: Whistl can't read your data
- Regulatory compliance: Meets GDPR, HIPAA requirements
- Trust minimization: Security doesn't depend on trusting Whistl
With E2E encryption, only you hold the keys to decrypt your data.
E2E Encryption Architecture
Whistl's E2E encryption ensures data is encrypted before leaving your device:
Data Flow
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ Your │ │ Encrypt │ │ Whistl │ │ Your │
│ Device │───▶│ On-Device │───▶│ Servers │───▶│ Other │
│ │ │ │ │ (Encrypted) │ │ Device │
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘
│ │ │ │
│ Master Key │ Ciphertext │ Ciphertext │ Master Key
│ (Never leaves) │ Only │ Only │ (Never leaves)
│ │ │ │
▼ ▼ ▼ ▼
Key Derivation AES-256-GCM Secure Storage Decrypt
(PBKDF2 + Salt) (Client-side) (AWS S3) (Client-side)
Key Components
- Master Password: User's password (never stored)
- Salt: Random value per user (stored on server)
- Master Key: Derived from password + salt (never leaves device)
- Encryption Key: Derived from master key for data encryption
- Ciphertext: Encrypted data stored on server
Key Derivation
Keys are derived from user passwords using PBKDF2:
Key Derivation Function
import CommonCrypto
class KeyDerivation {
static let saltBytes = 32
static let keyBytes = 32 // 256 bits
static let iterations = 256000
// Generate random salt for new user
static func generateSalt() -> Data {
var salt = Data(count: saltBytes)
salt.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, saltBytes, bytes.baseAddress!)
}
return salt
}
// Derive master key from password
static func deriveMasterKey(password: String, salt: Data) -> Data {
var key = Data(count: keyBytes)
let result = key.withUnsafeMutableBytes { keyBytes 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),
keyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
keyBytes.count
)
}
}
}
guard result == 0 else {
fatalError("Key derivation failed")
}
return key
}
// Derive encryption key from master key
static func deriveEncryptionKey(masterKey: Data, purpose: String) -> Data {
// HKDF for key derivation
let info = purpose.data(using: .utf8)!
var output = Data(count: keyBytes)
output.withUnsafeMutableBytes { outputBytes in
masterKey.withUnsafeBytes { masterKeyBytes in
info.withUnsafeBytes { infoBytes in
CCHKDF(
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
masterKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
masterKey.count,
infoBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
info.count,
nil, // Salt (none)
0,
outputBytes.baseAddress!.assumingMemoryBound(to: UInt8.self),
output.count
)
}
}
}
return output
}
}
Encryption Process
Data is encrypted client-side before upload:
Client-Side Encryption
class CloudEncryptor {
private let masterKey: Data
func encryptForUpload(_ data: SyncData) throws -> EncryptedPayload {
// Derive encryption key for this data type
let encryptionKey = KeyDerivation.deriveEncryptionKey(
masterKey: masterKey,
purpose: "sync_\(data.type)"
)
// Generate random IV
var iv = Data(count: 12) // 96 bits for GCM
iv.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 12, bytes.baseAddress!)
}
// Encrypt with AES-256-GCM
let ciphertext = try AES256.encrypt(
plaintext: data.json,
key: encryptionKey,
iv: iv
)
// Create payload
return EncryptedPayload(
ciphertext: ciphertext,
iv: iv,
authTag: ciphertext.authTag,
version: 1,
timestamp: Date(),
type: data.type
)
}
}
Encrypted Payload Structure
{
"user_id": "usr-abc123",
"payload": {
"ciphertext": "base64_encoded_encrypted_data...",
"iv": "base64_encoded_initialization_vector",
"auth_tag": "base64_encoded_authentication_tag",
"version": 1,
"timestamp": "2026-03-05T12:34:56Z",
"type": "transactions" // or "settings", "goals", "patterns"
},
"signature": "base64_encoded_hmac_signature"
}
Decryption Process
Data is decrypted client-side after download:
Client-Side Decryption
class CloudDecryptor {
private let masterKey: Data
func decryptFromDownload(_ payload: EncryptedPayload) throws -> SyncData {
// Derive encryption key
let encryptionKey = KeyDerivation.deriveEncryptionKey(
masterKey: masterKey,
purpose: "sync_\(payload.type)"
)
// Decrypt
let plaintext = try AES256.decrypt(
ciphertext: payload.ciphertext,
key: encryptionKey,
iv: payload.iv,
authTag: payload.authTag
)
// Parse JSON
let data = try JSONDecoder().decode(SyncData.self, from: plaintext)
return data
}
}
Sync Data Types
Whistl syncs multiple data types with different priorities:
Sync Categories
| Data Type | Sync Priority | Size (typical) | Frequency |
|---|---|---|---|
| User Settings | High | 2 KB | On change |
| Dream Board Goals | High | 50 KB | On change |
| Accountability Partner | High | 5 KB | On change |
| Behavioural Patterns | Medium | 100 KB | Daily |
| Transaction History | Low | 5 MB | Weekly |
| ML Model Weights | Low | 450 KB | Monthly |
Conflict Resolution
When multiple devices modify data, conflicts are resolved:
Last-Write-Wins with Merge
class ConflictResolver {
enum ResolutionStrategy {
case lastWriteWins
case merge
case manual
}
func resolve(
local: SyncData,
remote: SyncData,
type: DataType
) -> SyncData {
let strategy = getStrategy(for: type)
switch strategy {
case .lastWriteWins:
return local.timestamp > remote.timestamp ? local : remote
case .merge:
return mergeData(local: local, remote: remote)
case .manual:
// Queue for user resolution
ConflictQueue.shared.add(local, remote)
return local // Keep local until resolved
}
}
private func getStrategy(for type: DataType) -> ResolutionStrategy {
switch type {
case .settings: return .lastWriteWins
case .goals: return .merge
case .transactions: return .merge
case .patterns: return .lastWriteWins
}
}
}
Merge Strategy for Goals
func mergeGoals(local: [Goal], remote: [Goal]) -> [Goal] {
var merged: [String: Goal] = [:]
// Add all local goals
for goal in local {
merged[goal.id] = goal
}
// Merge remote goals
for goal in remote {
if let existing = merged[goal.id] {
// Keep version with more progress
if goal.progress > existing.progress {
merged[goal.id] = goal
}
} else {
// New goal - add it
merged[goal.id] = goal
}
}
return Array(merged.values)
}
Sync Triggers
Data syncs automatically based on triggers:
Sync Conditions
- App foreground: Sync on app launch
- Data change: Sync within 30 seconds of modification
- Background fetch: Periodic sync (iOS background app refresh)
- Push notification: Server-initiated sync for urgent updates
- Network change: Sync when switching to WiFi
Sync Manager
class SyncManager {
func scheduleSync(for data: SyncData) {
// Immediate sync for high priority
if data.priority == .high {
Task {
try? await performSync(data)
}
return
}
// Debounced sync for lower priority
debounceTimer?.invalidate()
debounceTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: false) { _ in
Task {
try? await self.performSync(data)
}
}
}
func handleBackgroundFetch() async -> UIBackgroundFetchResult {
let startTime = Date()
do {
try await performFullSync()
let duration = Date().timeIntervalSince(startTime)
return duration < 25 ? .newData : .noData
} catch {
return .failed
}
}
}
Server Architecture
Whistl's servers store only encrypted data:
Server-Side Storage
- Storage: AWS S3 with server-side encryption
- Database: PostgreSQL for metadata (user IDs, timestamps)
- CDN: CloudFront for fast global delivery
- Backups: Encrypted backups in multiple regions
What Servers Know
| Information | Stored | Encrypted |
|---|---|---|
| User ID | Yes | No |
| Yes | Yes (at rest) | |
| Password | No | — |
| Salt | Yes | No |
| Master Key | No | — |
| Data Payload | Yes | Yes (E2E) |
| Timestamps | Yes | No |
| Data Type | Yes | No |
Recovery Mechanisms
Users can recover data if they lose access:
Recovery Options
- Recovery key: Downloadable backup key (store safely)
- Trusted contact: Partner can help recover
- Email recovery: Encrypted recovery link
Recovery Key Generation
func generateRecoveryKey() -> String {
// Generate 256-bit random key
var keyBytes = Data(count: 32)
keyBytes.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 32, bytes.baseAddress!)
}
// Encode as readable phrase (like Bitcoin BIP-39)
let words = encodeAsWords(keyBytes)
return words.joined(separator: " ")
// Example output: "correct horse battery staple..."
}
Privacy Guarantees
Whistl's E2E encryption provides strong privacy guarantees:
What Whistl Cannot Access
- Transaction data: Encrypted before upload
- Spending patterns: Only you can decrypt
- Goal details: Dream board images encrypted
- Partner communications: E2E encrypted messages
- Behavioural insights: Personal patterns encrypted
Performance Metrics
Cloud sync performance:
| Metric | Target | Actual |
|---|---|---|
| Initial Sync Time | <30s | 18s average |
| Incremental Sync | <5s | 2.3s average |
| Encryption Overhead | <100ms | 45ms average |
| Sync Success Rate | >99% | 99.6% |
| Conflict Rate | <1% | 0.4% |
Conclusion
End-to-end encrypted cloud sync ensures your financial data is backed up and available across devices while remaining completely private. Even Whistl's servers cannot read your data—only you hold the decryption keys.
Optional cloud sync means you control your data: keep it local-only or enable encrypted backup for peace of mind.
Get Encrypted Cloud Backup
Whistl's optional cloud sync uses end-to-end encryption to protect your data. Download free and enable secure backup in settings.
Download Whistl FreeRelated: Local Storage Encryption | Plaid Bank Integration Security | Biometric Authentication