From 55648e95b457640487682be93b95ac2650d1102a Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 11 Apr 2025 18:09:55 +0200 Subject: [PATCH] Add native swift SQLite implementation (#771) --- .../ios/Autofill/CredentialProviderView.swift | 4 +- .../CredentialProviderViewController.swift | 2 +- .../ios/CredentialManager/CredentialManager.m | 19 ++ .../CredentialManager/CredentialManager.swift | 177 +++++++--- .../SharedCredentialStore.swift | 312 ++++++++++++------ mobile-app/ios/Podfile | 2 + mobile-app/ios/Podfile.lock | 8 +- mobile-app/utils/SqliteClient.tsx | 141 ++------ 8 files changed, 379 insertions(+), 286 deletions(-) diff --git a/mobile-app/ios/Autofill/CredentialProviderView.swift b/mobile-app/ios/Autofill/CredentialProviderView.swift index 3bd6252e3..6ce84a90b 100644 --- a/mobile-app/ios/Autofill/CredentialProviderView.swift +++ b/mobile-app/ios/Autofill/CredentialProviderView.swift @@ -140,7 +140,7 @@ class CredentialProviderViewModel: ObservableObject { isLoading = true do { - credentials = try SharedCredentialStore.shared.getAllCredentials(createKeyIfNeeded: false) + credentials = try SharedCredentialStore.shared.getAllCredentials() Task { do { @@ -170,7 +170,7 @@ class CredentialProviderViewModel: ObservableObject { service: newService) do { - try SharedCredentialStore.shared.addCredential(credential, createKeyIfNeeded: false) + try SharedCredentialStore.shared.addCredential(credential) Task { try await CredentialIdentityStore.shared.saveCredentialIdentities([credential]) } diff --git a/mobile-app/ios/Autofill/CredentialProviderViewController.swift b/mobile-app/ios/Autofill/CredentialProviderViewController.swift index e1ca1ffac..80031b04d 100644 --- a/mobile-app/ios/Autofill/CredentialProviderViewController.swift +++ b/mobile-app/ios/Autofill/CredentialProviderViewController.swift @@ -42,7 +42,7 @@ class CredentialProviderViewController: ASCredentialProviderViewController { override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { // Get credentials and return the first one that matches the identity do { - let credentials = try SharedCredentialStore.shared.getAllCredentials(createKeyIfNeeded: false) + let credentials = try SharedCredentialStore.shared.getAllCredentials() if let matchingCredential = credentials.first(where: { $0.service == credentialIdentity.serviceIdentifier.identifier }) { let passwordCredential = ASPasswordCredential( user: matchingCredential.username, diff --git a/mobile-app/ios/CredentialManager/CredentialManager.m b/mobile-app/ios/CredentialManager/CredentialManager.m index 38ce9a15b..e9dabf865 100644 --- a/mobile-app/ios/CredentialManager/CredentialManager.m +++ b/mobile-app/ios/CredentialManager/CredentialManager.m @@ -7,4 +7,23 @@ RCT_EXTERN_METHOD(clearCredentials) RCT_EXTERN_METHOD(getCredentials) RCT_EXTERN_METHOD(requiresMainQueueSetup) +// New methods for SQLite database operations +RCT_EXTERN_METHOD(storeDatabase:(NSString *)base64EncryptedDb + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(storeEncryptionKey:(NSString *)base64EncryptionKey + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(executeQuery:(NSString *)query + params:(NSArray *)params + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(executeUpdate:(NSString *)query + params:(NSArray *)params + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + @end \ No newline at end of file diff --git a/mobile-app/ios/CredentialManager/CredentialManager.swift b/mobile-app/ios/CredentialManager/CredentialManager.swift index 96fb0a636..61a5b1740 100644 --- a/mobile-app/ios/CredentialManager/CredentialManager.swift +++ b/mobile-app/ios/CredentialManager/CredentialManager.swift @@ -1,58 +1,133 @@ import Foundation -import React -import CryptoKit +import SQLite +import LocalAuthentication @objc(CredentialManager) class CredentialManager: NSObject { - @objc - func addCredential(_ username: String, password: String, service: String) { - do { - let credential = Credential(username: username, password: password, service: service) - try SharedCredentialStore.shared.addCredential(credential) - } catch let error as CryptoKitError { - print("Encryption error: \(error)") - // Handle encryption errors - } catch { - print("Failed to add credential: \(error)") - // Handle other errors + private let credentialStore = SharedCredentialStore.shared + + override init() { + super.init() } - } - - @objc - func clearCredentials() { - SharedCredentialStore.shared.clearAllCredentials() - } - - @objc - func getCredentials() -> [[String: String]] { - do { - let credentials = try SharedCredentialStore.shared.getAllCredentials() - let credentialDicts = credentials.map { credential in - return [ - "username": credential.username, - "password": credential.password, - "service": credential.service - ] - } - return credentialDicts - } catch let error as CryptoKitError { - print("Decryption error: \(error)") - // Handle decryption errors - return [] - } catch { - print("Failed to get credentials: \(error)") - // Handle other errors - return [] + + @objc + func requiresMainQueueSetup() -> Bool { + return false + } + + @objc + func storeDatabase(_ base64EncryptedDb: String, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + do { + try credentialStore.storeEncryptedDatabase(base64EncryptedDb) + resolve(nil) + } catch { + reject("DB_ERROR", "Failed to store database: \(error.localizedDescription)", error) + } + } + + @objc + func storeEncryptionKey(_ base64EncryptionKey: String, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + do { + // Store the encryption key in the keychain with biometric protection + let context = LAContext() + var error: NSError? + + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + reject("BIOMETRICS_UNAVAILABLE", "Biometrics not available: \(error?.localizedDescription ?? "Unknown error")", error) + return + } + + // Store the key in the keychain with biometric protection + try credentialStore.storeEncryptionKey(base64EncryptionKey) + resolve(nil) + } catch { + reject("KEYCHAIN_ERROR", "Failed to store encryption key: \(error.localizedDescription)", error) + } + } + + @objc + func executeQuery(_ query: String, + params: [Any], + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + do { + // Ensure database is initialized + try credentialStore.initializeDatabase() + + // Convert all params to strings + let bindingParams = params.map { param -> Binding? in + return String(describing: param) + } + + // Execute the query through the credential store + let results = try credentialStore.executeQuery(query, params: bindingParams) + resolve(results) + } catch { + reject("QUERY_ERROR", "Failed to execute query: \(error.localizedDescription)", error) + } + } + + @objc + func executeUpdate(_ query: String, + params: [Any], + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + do { + // Ensure database is initialized + try credentialStore.initializeDatabase() + + // Convert all params to strings + let bindingParams = params.map { param -> Binding? in + return String(describing: param) + } + + // Execute the update through the credential store + let changes = try credentialStore.executeUpdate(query, params: bindingParams) + resolve(changes) + } catch { + reject("UPDATE_ERROR", "Failed to execute update: \(error.localizedDescription)", error) + } + } + + @objc + func addCredential(_ username: String, password: String, service: String) { + do { + let credential = Credential(username: username, password: password, service: service) + try credentialStore.addCredential(credential) + } catch { + print("Failed to add credential: \(error)") + } + } + + @objc + func clearCredentials() { + credentialStore.clearAllCredentials() + } + + @objc + func getCredentials() -> [String: Any] { + do { + let credentials = try credentialStore.getAllCredentials() + let credentialDicts = credentials.map { credential in + return [ + "username": credential.username, + "password": credential.password, + "service": credential.service + ] + } + return ["credentials": credentialDicts] + } catch { + print("Failed to get credentials: \(error)") + return [:] + } + } + + @objc + static func moduleName() -> String! { + return "CredentialManager" } - } - - @objc - static func requiresMainQueueSetup() -> Bool { - return false - } - - @objc - static func moduleName() -> String! { - return "CredentialManager" - } } diff --git a/mobile-app/ios/CredentialManager/SharedCredentialStore.swift b/mobile-app/ios/CredentialManager/SharedCredentialStore.swift index 781cd5935..8cedc4ee2 100644 --- a/mobile-app/ios/CredentialManager/SharedCredentialStore.swift +++ b/mobile-app/ios/CredentialManager/SharedCredentialStore.swift @@ -1,133 +1,225 @@ import Foundation import KeychainAccess +import SQLite +import LocalAuthentication import CryptoKit +import CommonCrypto -public class SharedCredentialStore { - public static let shared = SharedCredentialStore() - private let userDefaults: UserDefaults - private let encryptionKeyKey = "encryptionKey" - private let credentialsKey = "storedCredentials" - private var cachedEncryptionKey: SymmetricKey? +class SharedCredentialStore { + static let shared = SharedCredentialStore() + private let keychain = Keychain(service: "net.aliasvault.autofill", accessGroup: "group.net.aliasvault.autofill") + .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny]) + private let encryptionKeyKey = "aliasvault_encryption_key" + private let encryptedDbFileName = "encrypted_db.sqlite" + private var db: Connection? - private init() { - // Use the app group identifier - userDefaults = UserDefaults(suiteName: "group.net.aliasvault.autofill")! - } + private init() {} - private func getEncryptionKey(createKeyIfNeeded: Bool = true) throws -> SymmetricKey { - print("Getting encryption key") - - if let cached = cachedEncryptionKey { - // Verify the cached key is valid by checking its bit length - if cached.bitCount == 256 { - print("Using cached encryption key") - return cached - } else { - print("Cached key is invalid, clearing cache") - cachedEncryptionKey = nil - } + // MARK: - Encryption Key Management + + func storeEncryptionKey(_ base64Key: String) throws { + print("Storing encryption key") + guard let keyData = Data(base64Encoded: base64Key) else { + throw NSError(domain: "SharedCredentialStore", code: 6, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 key"]) } - print("No cached key, accessing keychain") - // Create a new keychain instance with authentication required for this specific access - let authKeychain = Keychain(service: "net.aliasvault.autofill", accessGroup: "group.net.aliasvault.autofill") - .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny]) - - if let keyData = try? authKeychain - .authenticationPrompt("Authenticate to unlock your vault") - .getData(encryptionKeyKey) { - - print("Found existing key in keychain") - let key = SymmetricKey(data: keyData) - cachedEncryptionKey = key - print("Returning existing key") - return key - } - - if createKeyIfNeeded { - print("Creating new encryption key") - // Create new key if none exists - let key = SymmetricKey(size: .bits256) - do { - try authKeychain - .authenticationPrompt("Authenticate to unlock your vault") - .set(key.withUnsafeBytes { Data($0) }, key: encryptionKeyKey) - print("New key saved in keychain") - } catch { - print("Failed to save key to keychain: \(error)") - } - - cachedEncryptionKey = key - return key - } else { - print("No encryption key found in keychain") - throw NSError(domain: "SharedCredentialStore", code: -1, userInfo: [NSLocalizedDescriptionKey: "No encryption key found in keychain"]) - } - } - - private func encrypt(_ data: Data, createKeyIfNeeded: Bool = true) throws -> Data { - print("Encrypting data") - let key = try getEncryptionKey(createKeyIfNeeded: createKeyIfNeeded) - print("Using key with bit length: \(key.bitCount)") - let sealedBox = try AES.GCM.seal(data, using: key) - guard let combined = sealedBox.combined else { - print("Failed to get combined data from sealed box") - throw NSError(domain: "SharedCredentialStore", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get combined data from sealed box"]) - } - print("Successfully encrypted data, combined length: \(combined.count)") - return combined - } - - private func decrypt(_ data: Data, createKeyIfNeeded: Bool = true) throws -> Data { - print("Decrypting data with length: \(data.count)") - let key = try getEncryptionKey(createKeyIfNeeded: createKeyIfNeeded) - print("Using key with bit length: \(key.bitCount)") do { - let sealedBox = try AES.GCM.SealedBox(combined: data) - print("Successfully created sealed box") - let decryptedData = try AES.GCM.open(sealedBox, using: key) - print("Successfully decrypted data, length: \(decryptedData.count)") - return decryptedData - } catch let error as CryptoKitError { - print("CryptoKit error during decryption: \(error)") - throw error + try keychain + .authenticationPrompt("Authenticate to unlock your vault") + .set(keyData, key: encryptionKeyKey) + print("Key saved in keychain") } catch { - print("Unexpected error during decryption: \(error)") + print("Failed to save key to keychain: \(error)") throw error } } - public func getAllCredentials(createKeyIfNeeded: Bool = true) throws -> [Credential] { - print("Getting all credentials") - guard let encryptedData = userDefaults.data(forKey: credentialsKey) else { - print("No encrypted data found in UserDefaults") - return [] + // MARK: - Database Management + + private func getEncryptedDbPath() -> URL { + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + return documentsDirectory.appendingPathComponent(encryptedDbFileName) + } + + func storeEncryptedDatabase(_ base64EncryptedDb: String) throws { + guard let encryptedData = Data(base64Encoded: base64EncryptedDb) else { + throw NSError(domain: "SharedCredentialStore", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 data"]) } - let decryptedData = try decrypt(encryptedData, createKeyIfNeeded: createKeyIfNeeded) - return try JSONDecoder().decode([Credential].self, from: decryptedData) + + // Store the encrypted database in the app's documents directory + try encryptedData.write(to: getEncryptedDbPath()) } - public func addCredential(_ credential: Credential, createKeyIfNeeded: Bool = true) throws { - print("Adding new credential") - var credentials = try getAllCredentials(createKeyIfNeeded: createKeyIfNeeded) - credentials.append(credential) - let data = try JSONEncoder().encode(credentials) - let encryptedData = try encrypt(data, createKeyIfNeeded: createKeyIfNeeded) - userDefaults.set(encryptedData, forKey: credentialsKey) + func getEncryptedDatabase() -> String? { + do { + let encryptedData = try Data(contentsOf: getEncryptedDbPath()) + return encryptedData.base64EncodedString() + } catch { + return nil + } } - public func clearAllCredentials() { - print("Clearing all credentials") - userDefaults.removeObject(forKey: credentialsKey) - - let authKeychain = Keychain(service: "net.aliasvault.autofill", accessGroup: "group.net.aliasvault.autofill") - .accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny]) - - try? authKeychain.authenticationPrompt("Authenticate to unlock your vault").removeAll() + func initializeDatabase() throws { + // Get the encrypted database + guard let base64EncryptedDb = getEncryptedDatabase() else { + throw NSError(domain: "SharedCredentialStore", code: 1, userInfo: [NSLocalizedDescriptionKey: "No encrypted database found"]) + } + + // Get the encryption key + guard let keyData = try? keychain + .authenticationPrompt("Authenticate to unlock your vault") + .getData(encryptionKeyKey) else { + throw NSError(domain: "SharedCredentialStore", code: 2, userInfo: [NSLocalizedDescriptionKey: "No encryption key found"]) + } + + // Convert base64 strings to data + guard let encryptedData = Data(base64Encoded: base64EncryptedDb) else { + throw NSError(domain: "SharedCredentialStore", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid base64 data"]) + } + + // Decrypt the database + let decryptedData = try decrypt(data: encryptedData, key: keyData) + + // Create an in-memory database + db = try Connection(":memory:") + + // Import the decrypted database into memory + try db?.execute("ATTACH DATABASE '\(decryptedData)' AS source") + try db?.execute("BEGIN TRANSACTION") + + // Copy all tables from source to memory + let tables = try db?.prepare("SELECT name FROM source.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'") + for table in tables! { + let tableName = table[0] as! String + try db?.execute("CREATE TABLE \(tableName) AS SELECT * FROM source.\(tableName)") + } + + try db?.execute("COMMIT") + try db?.execute("DETACH DATABASE source") + + // Setup database pragmas + try db?.execute("PRAGMA journal_mode = WAL") + try db?.execute("PRAGMA synchronous = NORMAL") + try db?.execute("PRAGMA foreign_keys = ON") } - public func clearCache() { - print("Clearing encryption key cache") - cachedEncryptionKey = nil + // MARK: - Encryption/Decryption + + private func encrypt(data: Data, key: Data) throws -> Data { + let key = SymmetricKey(data: key) + let sealedBox = try AES.GCM.seal(data, using: key) + return sealedBox.combined! + } + + private func decrypt(data: Data, key: Data) throws -> Data { + let key = SymmetricKey(data: key) + let sealedBox = try AES.GCM.SealedBox(combined: data) + return try AES.GCM.open(sealedBox, using: key) + } + + // MARK: - Credential Operations + + func addCredential(_ credential: Credential) throws { + guard let db = db else { + throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]) + } + + let credentials = Table("credentials") + let id = Expression("id") + let username = Expression("username") + let password = Expression("password") + let service = Expression("service") + let createdAt = Expression("created_at") + let updatedAt = Expression("updated_at") + + try db.run(credentials.insert( + id <- UUID().uuidString, + username <- credential.username, + password <- credential.password, + service <- credential.service, + createdAt <- Date(), + updatedAt <- Date() + )) + } + + func getAllCredentials() throws -> [Credential] { + guard let db = db else { + throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]) + } + + let credentials = Table("credentials") + let username = Expression("username") + let password = Expression("password") + let service = Expression("service") + + var result: [Credential] = [] + for row in try db.prepare(credentials) { + result.append(Credential( + username: row[username], + password: row[password], + service: row[service] + )) + } + return result + } + + func clearAllCredentials() { + guard let db = db else { return } + + let credentials = Table("credentials") + try? db.run(credentials.delete()) + } + + // MARK: - Query Execution + + func executeQuery(_ query: String, params: [Binding?]) throws -> [[String: Any]] { + guard let db = db else { + throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]) + } + + let statement = try db.prepare(query) + var results: [[String: Any]] = [] + + for row in statement { + var rowDict: [String: Any] = [:] + for (index, column) in statement.columnNames.enumerated() { + rowDict[column] = row[index] + } + results.append(rowDict) + } + + return results + } + + func executeUpdate(_ query: String, params: [Binding?]) throws -> Int { + guard let db = db else { + throw NSError(domain: "SharedCredentialStore", code: 4, userInfo: [NSLocalizedDescriptionKey: "Database not initialized"]) + } + + let statement = try db.prepare(query) + try statement.run(params) + return db.changes + } + + // MARK: - Biometric Authentication + + func authenticateWithBiometrics() async throws -> Bool { + let context = LAContext() + var error: NSError? + + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { + throw error ?? NSError(domain: "SharedCredentialStore", code: 5, userInfo: [NSLocalizedDescriptionKey: "Biometrics not available"]) + } + + return try await withCheckedThrowingContinuation { continuation in + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, + localizedReason: "Authenticate to access your credentials") { success, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: success) + } + } + } } } diff --git a/mobile-app/ios/Podfile b/mobile-app/ios/Podfile index ce131f4de..06ba3e0dc 100644 --- a/mobile-app/ios/Podfile +++ b/mobile-app/ios/Podfile @@ -16,6 +16,7 @@ prepare_react_native_project! target 'AliasVault' do use_expo_modules! pod 'KeychainAccess', '~> 4.2.2' + pod 'SQLite.swift', '~> 0.14.0' if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; @@ -68,4 +69,5 @@ end target 'Autofill' do pod 'KeychainAccess', '~> 4.2.2' + pod 'SQLite.swift', '~> 0.14.0' end diff --git a/mobile-app/ios/Podfile.lock b/mobile-app/ios/Podfile.lock index b694e16ca..709bf396b 100644 --- a/mobile-app/ios/Podfile.lock +++ b/mobile-app/ios/Podfile.lock @@ -2094,6 +2094,9 @@ PODS: - ReactCommon/turbomodule/core - Yoga - SocketRocket (0.7.1) + - SQLite.swift (0.14.1): + - SQLite.swift/standard (= 0.14.1) + - SQLite.swift/standard (0.14.1) - Yoga (0.0.0) DEPENDENCIES: @@ -2195,6 +2198,7 @@ DEPENDENCIES: - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) + - SQLite.swift (~> 0.14.0) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -2202,6 +2206,7 @@ SPEC REPOS: - CatCrypto - KeychainAccess - SocketRocket + - SQLite.swift EXTERNAL SOURCES: boost: @@ -2497,8 +2502,9 @@ SPEC CHECKSUMS: RNReanimated: 2e5069649cbab2c946652d3b97589b2ae0526220 RNScreens: 362f4c861dd155f898908d5035d97b07a3f1a9d1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 + SQLite.swift: 2992550ebf3c5b268bf4352603e3df87d2a4ed72 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 8289d60f8e8c7cee8b314289b8ec81b6b239f13f +PODFILE CHECKSUM: 71411f71c96e4b6a3db40174f3834328e5a380b9 COCOAPODS: 1.16.2 diff --git a/mobile-app/utils/SqliteClient.tsx b/mobile-app/utils/SqliteClient.tsx index dc667228f..f7206e9ed 100644 --- a/mobile-app/utils/SqliteClient.tsx +++ b/mobile-app/utils/SqliteClient.tsx @@ -1,5 +1,4 @@ -import * as SQLite from 'expo-sqlite'; -import * as FileSystem from 'expo-file-system'; +import { NativeModules } from 'react-native'; import { Credential } from './types/Credential'; import { EncryptionKey } from './types/EncryptionKey'; import { TotpCode } from './types/TotpCode'; @@ -12,83 +11,18 @@ const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp type SQLiteBindValue = string | number | null | Uint8Array; -interface SQLiteResult { - rows: { - length: number; - item: (index: number) => any; - }; - rowsAffected: number; -} - /** - * Client for interacting with the SQLite database. + * Client for interacting with the SQLite database through native code. */ class SqliteClient { - private db: SQLite.SQLiteDatabase | null = null; + private credentialManager = NativeModules.CredentialManager; /** * Initialize the SQLite database from a base64 string */ public async initializeFromBase64(base64String: string): Promise { try { - // Ensure SQLite directory exists - const sqliteDir = `${FileSystem.documentDirectory}SQLite`; - const dirInfo = await FileSystem.getInfoAsync(sqliteDir); - if (!dirInfo.exists) { - await FileSystem.makeDirectoryAsync(sqliteDir, { intermediates: true }); - } - // For in-memory database, we need to create a temporary file first - const tempFileUri = `${sqliteDir}/temp.db`; - console.log('Writing database to temporary file'); - - // Delete existing file if it exists - const fileInfo = await FileSystem.getInfoAsync(tempFileUri); - if (fileInfo.exists) { - await FileSystem.deleteAsync(tempFileUri); - } - - await FileSystem.writeAsStringAsync(tempFileUri, base64String, { - encoding: FileSystem.EncodingType.Base64, - }); - - console.log('Database written to temporary file'); - console.log('tempFileUri', tempFileUri); - - // Open the database in memory - console.log('Opening database from file'); - this.db = SQLite.openDatabaseSync('temp.db'); - - console.log('Database opened from file'); - - // TODO: Finish implementation of in-memory database as we don't want to persist the database to the file system. - - // Attach in-memory db - /*await this.executeUpdate(`ATTACH DATABASE ':memory:' AS target`); - await this.executeUpdate('BEGIN TRANSACTION'); - - console.log('Executing query to get tables'); - - // Copy all tables from source to memory - const tables = await this.executeQuery<{ name: string }>( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" - ); - - console.log('Tables copied'); - - for (const table of tables) { - await this.executeUpdate(`CREATE TABLE ${table.name} AS SELECT * FROM target.${table.name}`); - } - - await this.executeUpdate('COMMIT'); - await this.executeUpdate('DETACH DATABASE source'); - - // Clean up the temporary file - await FileSystem.deleteAsync(tempFileUri);*/ - - // Setup database pragmas to configure the database. - await this.executeUpdate('PRAGMA journal_mode = WAL'); - await this.executeUpdate('PRAGMA synchronous = NORMAL'); - await this.executeUpdate('PRAGMA foreign_keys = ON'); + await this.credentialManager.storeDatabase(base64String); } catch (error) { console.error('Error initializing SQLite database:', error); throw error; @@ -96,21 +30,13 @@ class SqliteClient { } /** - * Export the SQLite database to a base64 string - * @returns Base64 encoded string of the database + * Store the encryption key in the native keychain */ - public async exportToBase64(): Promise { - if (!this.db) { - throw new Error('Database not initialized'); - } - + public async storeEncryptionKey(base64EncryptionKey: string): Promise { try { - // TODO: Implement database export to base64 - // This would require reading the database file and converting it to base64 - console.warn('Database export to base64 not yet implemented'); - return ''; + await this.credentialManager.storeEncryptionKey(base64EncryptionKey); } catch (error) { - console.error('Error exporting SQLite database:', error); + console.error('Error storing encryption key:', error); throw error; } } @@ -119,25 +45,8 @@ class SqliteClient { * Execute a SELECT query */ public async executeQuery(query: string, params: SQLiteBindValue[] = []): Promise { - if (!this.db) { - throw new Error('Database not initialized'); - } - try { - console.log('Executing query:', query); - - // First do query to get all tables - const tables = await this.db.getAllAsync( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'" - ); - - console.log('Tables:', tables); - - const results = await this.db.getAllAsync( - query, - ...params - ); - console.log('Results:', results); + const results = await this.credentialManager.executeQuery(query, params); return results as T[]; } catch (error) { console.error('Error executing query:', error); @@ -149,16 +58,9 @@ class SqliteClient { * Execute an INSERT, UPDATE, or DELETE query */ public async executeUpdate(query: string, params: SQLiteBindValue[] = []): Promise { - if (!this.db) { - throw new Error('Database not initialized'); - } - try { - const result = await this.db.runAsync( - query, - ...params - ); - return result.changes; + const result = await this.credentialManager.executeUpdate(query, params); + return result as number; } catch (error) { console.error('Error executing update:', error); throw error; @@ -169,10 +71,7 @@ class SqliteClient { * Close the database connection and free resources. */ public close(): void { - if (this.db) { - this.db.closeAsync(); - this.db = null; - } + // No-op since the native code handles connection lifecycle } /** @@ -369,8 +268,8 @@ class SqliteClient { * @returns The number of rows modified */ public async createCredential(credential: Credential): Promise { - if (!this.db) { - throw new Error('Database not initialized'); + if (!this.credentialManager) { + throw new Error('CredentialManager not initialized'); } try { @@ -474,8 +373,8 @@ class SqliteClient { * Returns null if no migrations are found. */ public async getDatabaseVersion(): Promise { - if (!this.db) { - throw new Error('Database not initialized'); + if (!this.credentialManager) { + throw new Error('CredentialManager not initialized'); } try { @@ -512,8 +411,8 @@ class SqliteClient { * @returns Array of TotpCode objects */ public async getTotpCodesForCredential(credentialId: string): Promise { - if (!this.db) { - throw new Error('Database not initialized'); + if (!this.credentialManager) { + throw new Error('CredentialManager not initialized'); } try { @@ -643,8 +542,8 @@ class SqliteClient { * @returns True if the table exists, false otherwise */ private async tableExists(tableName: string): Promise { - if (!this.db) { - throw new Error('Database not initialized'); + if (!this.credentialManager) { + throw new Error('CredentialManager not initialized'); } try {