549 lines
17 KiB
Markdown
549 lines
17 KiB
Markdown
---
|
|
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/securevault-guidelines.mdc
|
|
confidence: 0.9
|
|
namespace: work
|
|
last_synced: 2026-04-28
|
|
alwaysApply: false
|
|
---
|
|
|
|
# SecureVault Implementation Guidelines
|
|
|
|
## Introduction
|
|
|
|
These guidelines encapsulate vital information for using SecureVault in our application. SecureVault is our secure storage system built on GRDB with SQLCipher encryption, providing a layered security approach for sensitive data.
|
|
|
|
For general GRDB knowledge, refer to [GRDB documentation](https://github.com/groue/GRDB.swift). This guide covers DuckDuckGo-specific patterns and requirements.
|
|
|
|
## Essential Knowledge
|
|
|
|
### SecureVault Architecture Overview
|
|
|
|
SecureVault is a protocol-based system that provides:
|
|
|
|
- **L0**: Not encrypted (currently unused)
|
|
- **L1**: Secret key encrypted (usernames, domains, duck addresses)
|
|
- **L2**: User password encrypted with time-based access (user passwords)
|
|
- **L3**: User password required at request time (future: credit cards, sensitive data)
|
|
|
|
```swift
|
|
// Core SecureVault protocol pattern
|
|
public protocol SecureVault {
|
|
associatedtype DatabaseProvider: SecureStorageDatabaseProvider
|
|
init(providers: SecureStorageProviders<DatabaseProvider>)
|
|
}
|
|
|
|
// Example: AutofillSecureVault implementation
|
|
public protocol AutofillSecureVault: SecureVault {
|
|
func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64
|
|
func websiteCredentialsFor(domain: String) throws -> [SecureVaultModels.WebsiteCredentials]
|
|
// ... other autofill-specific methods
|
|
}
|
|
```
|
|
|
|
## Database Location and Setup
|
|
|
|
### Choosing the Database Location
|
|
|
|
**Important Note**: If you need to share the Database through AppGroup, please refer to [Sharing Database through AppGroup](#sharing-database-through-appgroup) section below.
|
|
|
|
For typical single-app usage:
|
|
- Create a dedicated folder for the database file
|
|
- This helps separate it from other files and makes file coordination easier
|
|
- Makes future migration simpler if needed
|
|
|
|
```swift
|
|
// Example database location setup
|
|
let databaseDirectory = FileManager.default
|
|
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
.appendingPathComponent("SecureVault")
|
|
|
|
try FileManager.default.createDirectory(at: databaseDirectory,
|
|
withIntermediateDirectories: true)
|
|
|
|
let databaseURL = databaseDirectory.appendingPathComponent("vault.sqlite")
|
|
```
|
|
|
|
### Database Configuration
|
|
|
|
The simplest and best (future-proof) way to setup database is to use the following configuration:
|
|
|
|
```swift
|
|
var config = Configuration()
|
|
config.prepareDatabase { database in
|
|
try database.usePassphrase(key)
|
|
try database.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
|
|
}
|
|
```
|
|
|
|
**Why the PRAGMA statement?**
|
|
- This PRAGMA enables sharing databases across App Groups
|
|
- It has no negative impact when used for single-app scenarios
|
|
- Setting it upfront makes future AppGroup sharing easier
|
|
- For details, see [iOS] Requirements to store Vault in AppGroup Container
|
|
|
|
### Choosing DatabaseWriter
|
|
|
|
GRDB operates with two possible writers:
|
|
|
|
#### DatabaseQueue
|
|
- **Single writer, single reader**
|
|
- Simpler configuration
|
|
- Suitable for single-threaded access
|
|
|
|
#### DatabasePool
|
|
- **Single writer, multiple readers**
|
|
- Enables WAL mode on SQLite file
|
|
- Better performance for multi-threaded access
|
|
|
|
```swift
|
|
// DatabaseQueue example
|
|
let queue = try DatabaseQueue(path: databaseURL.path, configuration: config)
|
|
|
|
// DatabasePool example
|
|
let pool = try DatabasePool(path: databaseURL.path, configuration: config)
|
|
```
|
|
|
|
**Decision criteria:**
|
|
- Single thread access (e.g., UserScripts only): Either option works
|
|
- Multi-threaded access: Use `DatabasePool` for better performance
|
|
|
|
## SecureVault Instance Management
|
|
|
|
### ✅ CORRECT: Single Instance Pattern
|
|
|
|
> GRDB documentation is extremely clear on the topic:
|
|
> "Open one single DatabaseQueue or DatabasePool per database file, for the whole duration of your use of the database. Not for the duration of each database access, but really for the duration of all database accesses to this file."
|
|
|
|
**Follow DuckDuckGo patterns**: Combine this with [✓ Approved] Avoiding static vars and singletons:
|
|
|
|
```swift
|
|
// ✅ CORRECT: Factory pattern with dependency injection
|
|
public let AutofillSecureVaultFactory: AutofillVaultFactory = SecureVaultFactory<DefaultAutofillSecureVault>(
|
|
makeCryptoProvider: {
|
|
return AutofillCryptoProvider()
|
|
},
|
|
makeKeyStoreProvider: { reporter in
|
|
return AutofillKeyStoreProvider(reporter: reporter)
|
|
},
|
|
makeDatabaseProvider: { key, _ in
|
|
return try DefaultAutofillDatabaseProvider(key: key)
|
|
}
|
|
)
|
|
|
|
// Usage in ViewModels via dependency injection
|
|
final class MyFeatureViewModel: ObservableObject {
|
|
private let vault: any AutofillSecureVault
|
|
|
|
init(dependencies: DependencyProvider = AppDependencyProvider.shared) {
|
|
self.vault = dependencies.autofillSecureVault
|
|
}
|
|
}
|
|
```
|
|
|
|
### ❌ AVOID: Multiple Instances
|
|
|
|
```swift
|
|
// ❌ DON'T DO THIS - creates multiple instances
|
|
func someMethod() {
|
|
let vault1 = try AutofillSecureVaultFactory.makeVault(reporter: nil)
|
|
// ... later in another method
|
|
let vault2 = try AutofillSecureVaultFactory.makeVault(reporter: nil)
|
|
// This can cause database corruption and performance issues
|
|
}
|
|
```
|
|
|
|
## Sharing Database through AppGroup
|
|
|
|
Since we've started sharing SecureVault (GRDB) in AppGroup with the intention of using system-wide extensions to access it, there are important considerations and requirements.
|
|
|
|
### Side Effects and Challenges
|
|
|
|
When sharing across App Groups, you must handle:
|
|
|
|
1. **File-access constraints**: Multiple processes accessing simultaneously
|
|
2. **System file lock constraints**: OS monitors SQLite access locks
|
|
3. **WAL mode performance**: Proper setup for read/write performance
|
|
4. **Persistent WAL**: Correct read-only access between processes
|
|
|
|
### Critical Setup Requirements
|
|
|
|
#### 1. Database Configuration for AppGroup
|
|
|
|
```swift
|
|
var config = Configuration()
|
|
config.prepareDatabase { database in
|
|
try database.usePassphrase(key)
|
|
// MANDATORY for AppGroup sharing
|
|
try database.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
|
|
// Additional AppGroup optimizations
|
|
try database.execute(sql: "PRAGMA wal_checkpoint = TRUNCATE")
|
|
}
|
|
|
|
// Use DatabasePool for AppGroup scenarios
|
|
let pool = try DatabasePool(path: sharedDatabaseURL.path, configuration: config)
|
|
```
|
|
|
|
#### 2. Background Task Management
|
|
|
|
**Critical**: Extend host app lifecycle to prevent 0xdead10cc crashes:
|
|
|
|
```swift
|
|
// ✅ REQUIRED: Use BackgroundTask during database operations
|
|
func performDatabaseOperation() async {
|
|
let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "SecureVault Operation") {
|
|
// Cleanup if needed
|
|
}
|
|
|
|
defer {
|
|
if backgroundTask != .invalid {
|
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
}
|
|
}
|
|
|
|
// Perform your database operations here
|
|
try await vault.performOperation()
|
|
}
|
|
```
|
|
|
|
#### 3. Using ExpiringActivity (Alternative)
|
|
|
|
For more sophisticated background management:
|
|
|
|
```swift
|
|
import ActivityKit
|
|
|
|
func performDatabaseOperationWithActivity() async {
|
|
let activity = try? Activity<DatabaseActivityAttributes>.request(
|
|
attributes: DatabaseActivityAttributes(),
|
|
content: .init(state: .active),
|
|
pushToken: nil
|
|
)
|
|
|
|
defer {
|
|
Task {
|
|
await activity?.end()
|
|
}
|
|
}
|
|
|
|
// Database operations
|
|
try await vault.performOperation()
|
|
}
|
|
```
|
|
|
|
### Crash Prevention: 0xdead10cc
|
|
|
|
**Problem**: App crashes with `Termination Reason: RUNNINGBOARD 0xdead10cc` when:
|
|
- App transitions from Background to Suspended state
|
|
- Database locks are still held
|
|
- Extensions need database access
|
|
|
|
**Solution**: Always use background tasks or expiring activities during database operations in AppGroup scenarios.
|
|
|
|
## Performance Optimization
|
|
|
|
### WAL Mode Optimization
|
|
|
|
```swift
|
|
// Optimize WAL checkpoints for AppGroup usage
|
|
config.prepareDatabase { database in
|
|
try database.usePassphrase(key)
|
|
try database.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
|
|
|
|
// Performance optimizations
|
|
try database.execute(sql: "PRAGMA journal_mode = WAL")
|
|
try database.execute(sql: "PRAGMA synchronous = NORMAL")
|
|
try database.execute(sql: "PRAGMA cache_size = -16384") // 16MB cache
|
|
try database.execute(sql: "PRAGMA temp_store = MEMORY")
|
|
}
|
|
```
|
|
|
|
### Efficient Database Operations
|
|
|
|
```swift
|
|
// ✅ GOOD: Batch operations
|
|
try vault.inDatabaseTransaction { database in
|
|
for credential in credentials {
|
|
try vault.storeWebsiteCredentials(credential, in: database)
|
|
}
|
|
}
|
|
|
|
// ❌ AVOID: Individual transactions
|
|
for credential in credentials {
|
|
try vault.storeWebsiteCredentials(credential) // Creates separate transaction each time
|
|
}
|
|
```
|
|
|
|
## Encryption and Security
|
|
|
|
### Multi-Layer Encryption Implementation
|
|
|
|
```swift
|
|
public class DefaultAutofillSecureVault<T: AutofillDatabaseProvider>: AutofillSecureVault {
|
|
|
|
// L1: Secret key encryption (stored in Keychain)
|
|
private func l1Encrypt(data: Data) throws -> Data {
|
|
let l1Key = try providers.keystore.l1Key()
|
|
return try providers.crypto.encrypt(data, withKey: l1Key)
|
|
}
|
|
|
|
// L2: User password encryption (with expiring access)
|
|
private func l2Encrypt(data: Data, using l2Key: Data? = nil) throws -> Data {
|
|
let key: Data = try {
|
|
if let l2Key {
|
|
return l2Key
|
|
}
|
|
let password = try passwordInUse()
|
|
return try l2KeyFrom(password: password)
|
|
}()
|
|
return try providers.crypto.encrypt(data, withKey: key)
|
|
}
|
|
|
|
// Password-based access with expiration
|
|
public func authWith(password: Data) throws -> any AutofillSecureVault {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
do {
|
|
_ = try self.l2KeyFrom(password: password) // Validates password
|
|
self.expiringPassword.value = password
|
|
return self
|
|
} catch {
|
|
let error = error as? SecureStorageError ?? .authError(cause: error)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Password Management
|
|
|
|
```swift
|
|
// Secure password reset
|
|
public func resetL2Password(oldPassword: Data?, newPassword: Data) throws {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
// Force re-auth on future calls
|
|
self.expiringPassword.value = nil
|
|
|
|
do {
|
|
// Use provided old password or stored generated password
|
|
let generatedPassword = try self.providers.keystore.generatedPassword()
|
|
guard let oldPassword = oldPassword ?? generatedPassword else {
|
|
throw SecureStorageError.invalidPassword
|
|
}
|
|
|
|
// Get decrypted L2 key using old password
|
|
let l2Key = try self.l2KeyFrom(password: oldPassword)
|
|
|
|
// Derive new encryption key
|
|
let newEncryptionKey = try self.providers.crypto.deriveKeyFromPassword(newPassword)
|
|
|
|
// Encrypt L2 key with new encryption key
|
|
let encryptedKey = try self.providers.crypto.encrypt(l2Key, withKey: newEncryptionKey)
|
|
|
|
// Store encrypted L2 key
|
|
try self.providers.keystore.storeEncryptedL2Key(encryptedKey)
|
|
|
|
// Clear generated password
|
|
try self.providers.keystore.clearGeneratedPassword()
|
|
} catch {
|
|
if let error = error as? SecureStorageError {
|
|
throw error
|
|
} else {
|
|
throw SecureStorageError.databaseError(cause: error)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### Mock SecureVault Implementation
|
|
|
|
```swift
|
|
// ✅ FOLLOW: Use existing mock patterns
|
|
typealias MockVaultFactory = SecureVaultFactory<MockSecureVault<MockDatabaseProvider>>
|
|
|
|
let MockSecureVaultFactory = SecureVaultFactory<MockSecureVault>(
|
|
makeCryptoProvider: {
|
|
let provider = MockCryptoProvider()
|
|
provider._derivedKey = "derived".data(using: .utf8)
|
|
return provider
|
|
},
|
|
makeKeyStoreProvider: { _ in
|
|
let provider = MockKeyStoreProvider()
|
|
provider._l1Key = "key".data(using: .utf8)
|
|
return provider
|
|
},
|
|
makeDatabaseProvider: { key, _ in
|
|
return try MockDatabaseProvider(key: key)
|
|
}
|
|
)
|
|
|
|
// Usage in tests
|
|
final class MockSecureVault<T: AutofillDatabaseProvider>: AutofillSecureVault {
|
|
var storedAccounts: [SecureVaultModels.WebsiteAccount] = []
|
|
var storedCredentials: [Int64: SecureVaultModels.WebsiteCredentials] = [:]
|
|
|
|
// Simplified implementations for testing
|
|
func encrypt(_ data: Data, using key: Data) throws -> Data { data }
|
|
func decrypt(_ data: Data, using key: Data) throws -> Data { data }
|
|
|
|
// Test-specific storage
|
|
func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64 {
|
|
let id = Int64(storedCredentials.count + 1)
|
|
storedCredentials[id] = credentials
|
|
if let account = credentials.account {
|
|
storedAccounts.append(account)
|
|
}
|
|
return id
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Database Operations
|
|
|
|
```swift
|
|
func testSecureVaultStorage() async throws {
|
|
let vault = try MockSecureVaultFactory.makeVault(reporter: nil)
|
|
|
|
let account = SecureVaultModels.WebsiteAccount(
|
|
title: "Test Site",
|
|
username: "testuser",
|
|
domain: "example.com"
|
|
)
|
|
|
|
let credentials = SecureVaultModels.WebsiteCredentials(
|
|
account: account,
|
|
password: "testpassword".data(using: .utf8)
|
|
)
|
|
|
|
// Test storage
|
|
let storedId = try vault.storeWebsiteCredentials(credentials)
|
|
XCTAssertGreaterThan(storedId, 0)
|
|
|
|
// Test retrieval
|
|
let retrievedCredentials = try vault.websiteCredentialsFor(domain: "example.com")
|
|
XCTAssertEqual(retrievedCredentials.count, 1)
|
|
XCTAssertEqual(retrievedCredentials.first?.account?.username, "testuser")
|
|
}
|
|
```
|
|
|
|
## Updating GRDB
|
|
|
|
Instead of using GRDB directly as source code, we package it into an XCFramework to speed up compilation time.
|
|
|
|
### Update Process
|
|
|
|
1. **Check the fork**: Go to our [GRDB fork](https://github.com/duckduckgo/GRDB.swift)
|
|
2. **Follow instructions**: Run the provided script to create a new release
|
|
3. **If script fails**:
|
|
- The script patches GRDB code that may change between releases
|
|
- Create a task in **Apple Developer Infrastructure (CI, Releases, DevEx)**
|
|
- Request script fixes and new release creation
|
|
|
|
### GRDB Version Considerations
|
|
|
|
```swift
|
|
// Check current GRDB version compatibility
|
|
import GRDB
|
|
|
|
// Ensure new features/APIs are available
|
|
#if compiler(>=5.9) && canImport(GRDB, _version: 6.0)
|
|
// Use newer GRDB features
|
|
#else
|
|
// Fallback to older patterns
|
|
#endif
|
|
```
|
|
|
|
## Error Handling Patterns
|
|
|
|
### SecureVault-Specific Errors
|
|
|
|
```swift
|
|
// Handle authentication errors gracefully
|
|
func handleSecureVaultOperation() async {
|
|
do {
|
|
let data = try await vault.getSensitiveData()
|
|
processData(data)
|
|
} catch SecureStorageError.authRequired {
|
|
// Prompt user for password
|
|
await requestUserAuthentication()
|
|
} catch SecureStorageError.invalidPassword {
|
|
// Show password error message
|
|
showPasswordError()
|
|
} catch SecureStorageError.databaseError(let cause) {
|
|
// Handle database-specific errors
|
|
Logger.secureStorage.error("Database error: \(cause)")
|
|
showGenericError()
|
|
} catch {
|
|
// Handle other errors
|
|
Logger.secureStorage.error("Unexpected error: \(error)")
|
|
showGenericError()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Background Access Error Handling
|
|
|
|
```swift
|
|
// Handle AppGroup background access gracefully
|
|
func performBackgroundVaultOperation() async {
|
|
let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Vault Access")
|
|
|
|
defer {
|
|
if backgroundTask != .invalid {
|
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
}
|
|
}
|
|
|
|
do {
|
|
try await vault.performOperation()
|
|
} catch {
|
|
// If we're in background and get access errors, defer operation
|
|
if UIApplication.shared.applicationState == .background {
|
|
queueOperationForForeground()
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices Summary
|
|
|
|
### ✅ DO
|
|
|
|
- Use the factory pattern with dependency injection
|
|
- Set up database configuration with AppGroup PRAGMA upfront
|
|
- Use background tasks for AppGroup database operations
|
|
- Implement proper error handling for authentication states
|
|
- Follow the single instance pattern for database writers
|
|
- Use DatabasePool for multi-threaded access
|
|
- Batch database operations when possible
|
|
- Use existing mock patterns for testing
|
|
|
|
### ❌ DON'T
|
|
|
|
- Create multiple SecureVault instances for the same database
|
|
- Perform database operations without background tasks in AppGroup scenarios
|
|
- Ignore authentication required errors
|
|
- Use force unwrapping with SecureVault operations
|
|
- Mix DatabaseQueue and DatabasePool for the same database file
|
|
- Perform individual transactions for batch operations
|
|
- Skip error handling for background access scenarios
|
|
|
|
### 🔒 Security Reminders
|
|
|
|
- Never log decrypted password data
|
|
- Always validate user passwords before storing
|
|
- Use appropriate encryption layers (L1/L2/L3) for data sensitivity
|
|
- Implement proper cleanup for background tasks
|
|
- Handle device lock scenarios gracefully
|
|
- Clear sensitive data from memory when appropriate
|
|
|
|
---
|
|
|
|
This documentation ensures secure, performant, and maintainable SecureVault implementation across the DuckDuckGo browser ecosystem.
|