mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 21:40:41 -04:00
Add more scaffolding (#771)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
48
mobile-app/ios/autofill/CredentialIdentityStore.swift
Normal file
48
mobile-app/ios/autofill/CredentialIdentityStore.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user