Add more scaffolding (#771)

This commit is contained in:
Leendert de Borst
2025-04-09 15:49:37 +02:00
parent de6a46fb8e
commit e6149a8936
4 changed files with 233 additions and 60 deletions

View File

@@ -1,12 +1,21 @@
import Foundation
import React
import CryptoKit
@objc(CredentialManager)
class CredentialManager: NSObject {
@objc
func addCredential(_ username: String, password: String, service: String) {
let credential = Credential(username: username, password: password, service: service)
SharedCredentialStore.shared.addCredential(credential)
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
}
}
@objc
@@ -16,15 +25,25 @@ class CredentialManager: NSObject {
@objc
func getCredentials() -> [[String: String]] {
let credentials = SharedCredentialStore.shared.getAllCredentials()
let credentialDicts = credentials.map { credential in
return [
"username": credential.username,
"password": credential.password,
"service": credential.service
]
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 []
}
return credentialDicts
}
@objc

View File

@@ -16,15 +16,17 @@ public struct Credential: Codable {
public class SharedCredentialStore {
public static let shared = SharedCredentialStore()
private let userDefaults: UserDefaults
private let encryptionKeyKey = "encryptionKey"
private let credentialsKey = "storedCredentials"
private var cachedEncryptionKey: SymmetricKey?
private init() {
// Use the app group identifier
userDefaults = UserDefaults(suiteName: "group.net.aliasvault.autofill")!
}
private func getEncryptionKey() throws -> SymmetricKey {
private func getEncryptionKey(createKeyIfNeeded: Bool = true) throws -> SymmetricKey {
print("Getting encryption key")
if let cached = cachedEncryptionKey {
@@ -41,76 +43,99 @@ public class SharedCredentialStore {
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])
.accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny])
if let keyData = try? authKeychain
.authenticationPrompt("Authenticate to unlock your vault")
// Try to get existing key from keychain
if let keyData = try? authKeychain.getData(encryptionKeyKey) {
.getData(encryptionKeyKey) {
print("Found existing key in keychain")
let key = SymmetricKey(data: keyData)
cachedEncryptionKey = key
print("Returning existing key")
return key
}
print("Creating new encryption key")
// Create new key if none exists
let key = SymmetricKey(size: .bits256)
try authKeychain.set(key.withUnsafeBytes { Data($0) }, key: encryptionKeyKey)
cachedEncryptionKey = 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) throws -> Data {
private func encrypt(_ data: Data, createKeyIfNeeded: Bool = true) throws -> Data {
print("Encrypting data")
let key = try getEncryptionKey()
let key = try getEncryptionKey(createKeyIfNeeded: createKeyIfNeeded)
print("Using key with bit length: \(key.bitCount)")
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined ?? Data()
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) throws -> Data {
print("Decrypting data")
let key = try getEncryptionKey()
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
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
} catch {
print("Unexpected error during decryption: \(error)")
throw error
}
}
public func getAllCredentials() -> [Credential] {
public func getAllCredentials(createKeyIfNeeded: Bool = true) throws -> [Credential] {
print("Getting all credentials")
guard let encryptedData = UserDefaults.standard.data(forKey: credentialsKey) else {
guard let encryptedData = userDefaults.data(forKey: credentialsKey) else {
print("No encrypted data found in UserDefaults")
return []
}
do {
let decryptedData = try decrypt(encryptedData)
return try JSONDecoder().decode([Credential].self, from: decryptedData)
} catch {
print("Failed to decrypt credentials: \(error)")
return []
}
let decryptedData = try decrypt(encryptedData, createKeyIfNeeded: createKeyIfNeeded)
return try JSONDecoder().decode([Credential].self, from: decryptedData)
}
public func addCredential(_ credential: Credential) {
public func addCredential(_ credential: Credential, createKeyIfNeeded: Bool = true) throws {
print("Adding new credential")
var credentials = getAllCredentials()
var credentials = try getAllCredentials(createKeyIfNeeded: createKeyIfNeeded)
credentials.append(credential)
do {
let data = try JSONEncoder().encode(credentials)
let encryptedData = try encrypt(data)
UserDefaults.standard.set(encryptedData, forKey: credentialsKey)
} catch {
print("Failed to save credentials: \(error)")
}
let data = try JSONEncoder().encode(credentials)
let encryptedData = try encrypt(data, createKeyIfNeeded: createKeyIfNeeded)
userDefaults.set(encryptedData, forKey: credentialsKey)
}
public func clearAllCredentials() {
print("Clearing all credentials")
UserDefaults.standard.removeObject(forKey: credentialsKey)
userDefaults.removeObject(forKey: credentialsKey)
let authKeychain = Keychain(service: "net.aliasvault.autofill", accessGroup: "group.net.aliasvault.autofill")
.accessibility(.whenPasscodeSetThisDeviceOnly, authenticationPolicy: [.biometryAny])
.authenticationPrompt("Authenticate to unlock your vault")
try? authKeychain.removeAll()
try? authKeychain.authenticationPrompt("Authenticate to unlock your vault").removeAll()
}
public func clearCache() {

View File

@@ -0,0 +1,48 @@
import AuthenticationServices
class CredentialIdentityStore {
static let shared = CredentialIdentityStore()
private let store = ASCredentialIdentityStore.shared
private init() {}
func saveCredentialIdentities(_ credentials: [Credential]) async throws {
let identities = credentials.map { credential in
let serviceIdentifier = ASCredentialServiceIdentifier(
identifier: credential.service,
type: .domain
)
return ASPasswordCredentialIdentity(
serviceIdentifier: serviceIdentifier,
user: credential.username,
// TODO: Use the actual record identifier when implementing the actual vault
recordIdentifier: UUID().uuidString
)
}
try await store.saveCredentialIdentities(identities)
}
func removeAllCredentialIdentities() async throws {
try await store.removeAllCredentialIdentities()
}
func removeCredentialIdentities(_ credentials: [Credential]) async throws {
let identities = credentials.map { credential in
let serviceIdentifier = ASCredentialServiceIdentifier(
identifier: credential.service,
type: .domain
)
return ASPasswordCredentialIdentity(
serviceIdentifier: serviceIdentifier,
user: credential.username,
// TODO: Use the actual record identifier when implementing the actual vault
recordIdentifier: UUID().uuidString
)
}
try await store.removeCredentialIdentities(identities)
}
}

View File

@@ -12,16 +12,34 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
private var credentials: [Credential] = []
private let tableView = UITableView()
private let addButton = UIButton(type: .system)
private let loadButton = UIButton(type: .system)
private let loadingIndicator = UIActivityIndicatorView(style: .large)
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadCredentials()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
}
private func setupUI() {
view.backgroundColor = .systemBackground
// Setup Loading Indicator
loadingIndicator.hidesWhenStopped = false
loadingIndicator.color = .systemBlue
view.addSubview(loadingIndicator)
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
loadingIndicator.startAnimating()
// Setup TableView
tableView.delegate = self
tableView.dataSource = self
@@ -35,12 +53,26 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
view.addSubview(addButton)
addButton.translatesAutoresizingMaskIntoConstraints = false
// Setup Load Button
loadButton.setTitle("Load Credentials", for: .normal)
loadButton.addTarget(self, action: #selector(loadCredentials), for: .touchUpInside)
view.addSubview(loadButton)
loadButton.translatesAutoresizingMaskIntoConstraints = false
// Setup Constraints
NSLayoutConstraint.activate([
loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: addButton.topAnchor),
tableView.bottomAnchor.constraint(equalTo: loadButton.topAnchor),
loadButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
loadButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
loadButton.bottomAnchor.constraint(equalTo: addButton.topAnchor, constant: -8),
loadButton.heightAnchor.constraint(equalToConstant: 44),
addButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
addButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
@@ -49,9 +81,30 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
])
}
private func loadCredentials() {
credentials = SharedCredentialStore.shared.getAllCredentials()
tableView.reloadData()
@objc private func loadCredentials() {
do {
credentials = try SharedCredentialStore.shared.getAllCredentials(createKeyIfNeeded: false)
// Update credential identities in the system
Task {
try await CredentialIdentityStore.shared.saveCredentialIdentities(credentials)
DispatchQueue.main.async { [weak self] in
self?.loadingIndicator.stopAnimating()
self?.tableView.isHidden = false
self?.tableView.reloadData()
}
}
} catch let error as NSError {
loadingIndicator.stopAnimating()
let errorAlert = UIAlertController(
title: "Error Loading Credentials",
message: error.localizedDescription,
preferredStyle: .alert
)
errorAlert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
self?.extensionContext.cancelRequest(withError: error)
})
present(errorAlert, animated: true)
}
}
@objc private func addCredentialTapped() {
@@ -76,24 +129,52 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
!username.isEmpty, !password.isEmpty, !service.isEmpty else { return }
let credential = Credential(username: username, password: password, service: service)
SharedCredentialStore.shared.addCredential(credential)
do {
try SharedCredentialStore.shared.addCredential(credential, createKeyIfNeeded: false)
// Update credential identities in the system
Task {
try await CredentialIdentityStore.shared.saveCredentialIdentities([credential])
}
} catch let error as NSError {
let errorAlert = UIAlertController(
title: "Error Adding Credential",
message: error.localizedDescription,
preferredStyle: .alert
)
errorAlert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(errorAlert, animated: true)
return
}
self?.loadCredentials()
})
present(alert, animated: true)
}
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
loadCredentials()
@objc private func cancelTapped() {
extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
}
@IBAction func cancel(_ sender: AnyObject?) {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
// Loading it from here doesn't play well with the
//loadCredentials()
}
override func prepareInterfaceForUserChoosingTextToInsert() {
loadCredentials()
}
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
loadCredentials()
// For testing purposes: we just return the first credential.
if let firstCredential = credentials.first {
let passwordCredential = ASPasswordCredential(user: firstCredential.username, password: firstCredential.password)
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
} else {
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.credentialIdentityNotFound.rawValue))
}
}
}
extension CredentialProviderViewController: UITableViewDelegate, UITableViewDataSource {