blob: 23e575d4e32e40795fbd89c0706cb4e30f970d3b [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import AuthenticationServices
import Foundation
/// Delegate for CredentialImportManager.
@objc public protocol CredentialImportManagerDelegate {
/// Called when parsing the credentials from ASExportedCredentialData is finished.
@objc func onCredentialsParsed(
passwords: [CredentialExchangePassword], passkeys: [CredentialExchangePasskey])
}
/// Handles importing user credentials through ASCredentialImportManager.
@MainActor
@objc public class CredentialImportManager: NSObject {
/// The activity type used in user activity objects sent to importing apps.
@available(iOS 26, *)
@objc public static var credentialExchangeActivity: NSString {
return ASCredentialExchangeActivity as NSString
}
/// The key for the token in the user info dictionary of the user activity sent to importing apps.
@available(iOS 26, *)
@objc public static var credentialImportToken: NSString {
return ASCredentialImportToken as NSString
}
/// Delegate for this class.
@objc weak public var delegate: CredentialImportManagerDelegate?
/// Begins the credential import process by providing a UUID token received by the OS during the
/// app launch.
@available(iOS 26, *)
@objc public func startImport(_ uuid: NSUUID) {
let importManager = ASCredentialImportManager()
Task {
do {
let credentialData = try await importManager.importCredentials(token: uuid as UUID)
let translatedData = translateCredentialData(credentialData)
delegate?.onCredentialsParsed(
passwords: translatedData.passwords, passkeys: translatedData.passkeys)
} catch {
// TODO(crbug.com/445889307): Handle errors.
}
}
}
/// Translates ASExportedCredentialData into lists of NSObjects used in CredentialImporter.
@available(iOS 26, *)
private func translateCredentialData(
_ credentialData: ASExportedCredentialData
) -> (passwords: [CredentialExchangePassword], passkeys: [CredentialExchangePasskey]) {
var passwords: [CredentialExchangePassword] = []
var passkeys: [CredentialExchangePasskey] = []
for account in credentialData.accounts {
for item in account.items {
let optionalUrl = item.scope?.urls.first
let credentials = item.credentials
for i in 0..<credentials.count {
switch credentials[i] {
case .basicAuthentication(let basicAuth):
// Password without a url cannot be imported, skip it.
// TODO(crbug.com/445889719): Handle Android app scope as well.
// TODO(crbug.com/445889706): Either initialize empty url object and let credential
// importer handle it, or add metric logging.
guard let url = optionalUrl else { continue }
// If the next credential is of type note, treat it as note for password.
var note = ""
let nextIndex = i + 1
if nextIndex < credentials.count {
if case .note(let noteData) = credentials[nextIndex] {
note = noteData.content.value
}
}
passwords.append(
CredentialExchangePassword(
url: url,
username: basicAuth.userName?.value ?? "",
password: basicAuth.password?.value ?? "",
note: note
))
case .passkey(let passkey):
passkeys.append(
CredentialExchangePasskey(
credentialId: passkey.credentialID,
rpId: passkey.relyingPartyIdentifier,
userName: passkey.userName,
userDisplayName: passkey.userDisplayName,
userId: passkey.userHandle,
privateKey: passkey.key))
default:
// TODO(crbug.com/445889706): Add logging to assess dropped types.
continue
}
}
}
}
return (passwords, passkeys)
}
}