mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 21:40:41 -04:00
Add native swift SQLite implementation (#771)
This commit is contained in:
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>("id")
|
||||
let username = Expression<String>("username")
|
||||
let password = Expression<String>("password")
|
||||
let service = Expression<String>("service")
|
||||
let createdAt = Expression<Date>("created_at")
|
||||
let updatedAt = Expression<Date>("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<String>("username")
|
||||
let password = Expression<String>("password")
|
||||
let service = Expression<String>("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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
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<string> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
public async storeEncryptionKey(base64EncryptionKey: string): Promise<void> {
|
||||
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<T>(query: string, params: SQLiteBindValue[] = []): Promise<T[]> {
|
||||
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<number> {
|
||||
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<number> {
|
||||
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<string | null> {
|
||||
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<TotpCode[]> {
|
||||
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<boolean> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
if (!this.credentialManager) {
|
||||
throw new Error('CredentialManager not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user