mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-19 13:57:18 -04:00
138 lines
5.3 KiB
Swift
138 lines
5.3 KiB
Swift
import AuthenticationServices
|
|
import VaultModels
|
|
import VaultUtils
|
|
|
|
/**
|
|
* Native iOS implementation of the CredentialIdentityStore protocol.
|
|
*
|
|
* This class is used to save and remove credential identities from the iOS password
|
|
* and passkey autosuggest store, which is shown inline in the keyboard amongst other places.
|
|
*/
|
|
public class CredentialIdentityStore {
|
|
/// Get global credential store instance
|
|
public static let shared = CredentialIdentityStore()
|
|
private let store = ASCredentialIdentityStore.shared
|
|
|
|
private init() {}
|
|
|
|
/// Save credentials into the native iOS credential store.
|
|
/// - Parameters:
|
|
/// - credentials: The credentials to register
|
|
public func saveCredentialIdentities(_ credentials: [AutofillCredential]) async throws {
|
|
var allIdentities: [ASCredentialIdentity] = []
|
|
|
|
let state = await storeState()
|
|
guard state.isEnabled else {
|
|
return
|
|
}
|
|
|
|
// Save passwords into autofill store
|
|
let passwordIdentities = createPasswordIdentities(from: credentials)
|
|
allIdentities.append(contentsOf: passwordIdentities)
|
|
|
|
// Save passkeys into autofill store
|
|
let passkeyIdentities = createPasskeyIdentities(from: credentials)
|
|
allIdentities.append(contentsOf: passkeyIdentities)
|
|
|
|
guard !allIdentities.isEmpty else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
// First, remove all existing credential identities to ensure a clean replacement
|
|
try await store.removeAllCredentialIdentities()
|
|
|
|
// Then save the new credential identities
|
|
try await store.saveCredentialIdentities(allIdentities)
|
|
} catch {
|
|
// Re-throw the error so the caller knows the operation failed
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/// Remove all credentials from iOS credential store.
|
|
public func removeAllCredentialIdentities() async throws {
|
|
try await store.removeAllCredentialIdentities()
|
|
}
|
|
|
|
private func storeState() async -> ASCredentialIdentityStoreState {
|
|
await withCheckedContinuation { continuation in
|
|
store.getState { state in
|
|
continuation.resume(returning: state)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if the credential identity store supports incremental updates and has credentials
|
|
/// Returns true if the store is empty or doesn't support incremental updates
|
|
public func isStoreEmpty() async -> Bool {
|
|
let state = await storeState()
|
|
return !state.supportsIncrementalUpdates
|
|
}
|
|
|
|
/// Create password credential identities from credentials
|
|
/// Creates one identity per URL for multi-URL support (iOS matches by domain itself)
|
|
private func createPasswordIdentities(from credentials: [AutofillCredential]) -> [ASPasswordCredentialIdentity] {
|
|
return credentials.flatMap { credential -> [ASPasswordCredentialIdentity] in
|
|
guard !credential.hasPasskey else {
|
|
// Skip if this record has a passkey as they will be saved in createPasskeyIdentities
|
|
return []
|
|
}
|
|
|
|
guard credential.hasPassword else {
|
|
// Skip credentials with no password
|
|
return []
|
|
}
|
|
|
|
let identifier = credential.identifier
|
|
guard !identifier.isEmpty else {
|
|
return [] // Skip credentials with no identifier
|
|
}
|
|
|
|
// Create one identity per URL for multi-URL support
|
|
let identities = credential.serviceUrls.compactMap { urlString -> ASPasswordCredentialIdentity? in
|
|
guard let url = URL(string: urlString),
|
|
let host = url.host else {
|
|
return nil
|
|
}
|
|
|
|
let identity = ASPasswordCredentialIdentity(
|
|
serviceIdentifier: ASCredentialServiceIdentifier(identifier: urlString, type: .URL),
|
|
user: identifier,
|
|
recordIdentifier: credential.id.uuidString
|
|
)
|
|
|
|
return identity
|
|
}
|
|
|
|
return identities
|
|
}
|
|
}
|
|
|
|
/// Create passkey credential identities from credentials
|
|
private func createPasskeyIdentities(from credentials: [AutofillCredential]) -> [ASPasskeyCredentialIdentity] {
|
|
return credentials.compactMap { credential -> ASPasskeyCredentialIdentity? in
|
|
guard let passkey = credential.passkey, !passkey.isDeleted else {
|
|
return nil
|
|
}
|
|
|
|
// Get the userName for display in iOS AutoFill UI
|
|
// Passkeys may store userName, otherwise use the credential's identifier
|
|
let userName = passkey.userName ?? credential.identifier
|
|
|
|
// Convert passkey.Id to bytes for credentialID
|
|
let credentialId = try? PasskeyHelper.guidToBytes(passkey.id.uuidString)
|
|
|
|
// For passkeys, we use the rpId from the passkey itself, not the service URL
|
|
// This is because passkeys are tied to the RP ID, which may differ from the service URL
|
|
return ASPasskeyCredentialIdentity(
|
|
relyingPartyIdentifier: passkey.rpId,
|
|
userName: userName,
|
|
credentialID: credentialId ?? Data(), // WebAuthn credential ID (16-byte GUID)
|
|
userHandle: passkey.userHandle ?? Data(),
|
|
recordIdentifier: passkey.id.uuidString
|
|
)
|
|
}
|
|
}
|
|
}
|