Add iOS suggestion credential insert scaffolding (#771)

This commit is contained in:
Leendert de Borst
2025-04-28 13:34:32 +02:00
parent 01f1cc8bc3
commit 16a858ee08
2 changed files with 63 additions and 12 deletions

View File

@@ -14,21 +14,41 @@ class CredentialIdentityStore {
private init() {}
func saveCredentialIdentities(_ credentials: [Credential]) async throws {
let identities = credentials.map { credential in
let serviceIdentifier = ASCredentialServiceIdentifier(
identifier: credential.service.name ?? "",
type: .domain
)
let identities: [ASPasswordCredentialIdentity] = credentials.compactMap { credential in
guard let urlString = credential.service.url,
let url = URL(string: urlString),
let host = url.host else {
return nil
}
guard let username = credential.username, !username.isEmpty else {
return nil
}
let effectiveDomain = Self.effectiveDomain(from: host)
return ASPasswordCredentialIdentity(
serviceIdentifier: serviceIdentifier,
user: credential.username ?? "",
// TODO: Use the actual record identifier when implementing the actual vault
recordIdentifier: UUID().uuidString
serviceIdentifier: ASCredentialServiceIdentifier(identifier: effectiveDomain, type: .domain),
user: username,
recordIdentifier: credential.id.uuidString
)
}
try await store.saveCredentialIdentities(identities)
guard !identities.isEmpty else {
print("No valid identities to save.")
return
}
let state = await storeState()
guard state.isEnabled else {
print("Credential identity store is not enabled.")
return
}
do {
try await store.saveCredentialIdentities(identities)
} catch {
print("Failed to save credential identities to native iOS storage: \(error)")
}
}
func removeAllCredentialIdentities() async throws {
@@ -52,4 +72,18 @@ class CredentialIdentityStore {
try await store.removeCredentialIdentities(identities)
}
private func storeState() async -> ASCredentialIdentityStoreState {
await withCheckedContinuation { continuation in
store.getState { state in
continuation.resume(returning: state)
}
}
}
private static func effectiveDomain(from host: String) -> String {
let parts = host.split(separator: ".")
guard parts.count >= 2 else { return host }
return parts.suffix(2).joined(separator: ".")
}
}

View File

@@ -29,7 +29,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
let viewModel = CredentialProviderViewModel(
loader: {
try VaultStore.shared.initializeDatabase()
return try VaultStore.shared.getAllCredentials()
let credentials = try VaultStore.shared.getAllCredentials()
await self.registerCredentialIdentities(credentials: credentials)
return credentials
},
selectionHandler: { [weak self] credential in
guard let self = self else { return }
@@ -85,7 +87,10 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
do {
let credentials = try VaultStore.shared.getAllCredentials()
if let matchingCredential = credentials.first(where: { $0.service.name ?? "" == credentialIdentity.serviceIdentifier.identifier }) {
if let matchingCredential = credentials.first(where: { credential in
return credential.id.uuidString == credentialIdentity.recordIdentifier
}) {
let passwordCredential = ASPasswordCredential(
user: matchingCredential.username ?? "",
password: matchingCredential.password?.value ?? ""
@@ -109,4 +114,16 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
)
}
}
/**
* This registers all known AliasVault credentials into iOS native credential storage, which iOS can then use to suggest autofill credentials when a user
* focuses an input field on a login form. These suggestions will then be shown above the iOS keyboard, which saves the user one step.
*/
private func registerCredentialIdentities(credentials: [Credential]) async {
do {
try await CredentialIdentityStore.shared.saveCredentialIdentities(credentials)
} catch {
print("Failed to save credential identities: \(error)")
}
}
}