8000 [PM-8828] Fido2 autofill without user interaction by fedemkr · Pull Request #744 · bitwarden/ios · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[PM-8828] Fido2 autofill without user interaction #744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AuthenticationServices
import BitwardenSdk
import BitwardenShared
import OSLog

Expand Down Expand Up @@ -61,6 +62,22 @@
provideCredential(for: recordIdentifier)
}

@available(iOSApplicationExtension 17.0, *)
override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) {
switch credentialRequest {
case let passwordRequest as ASPasswordCredentialRequest:
provideCredentialWithoutUserInteraction(for: passwordRequest)
case let passkeyRequest as ASPasskeyCredentialRequest:
initializeApp(
with: DefaultCredentialProviderContext(.autofillFido2Credential(passkeyRequest)),
userInteraction: false
)
provideFido2Credential(for: passkeyRequest)
default:
break

Check warning on line 77 in BitwardenAutoFillExtension/CredentialProviderViewController.swift

View check run for this annotation

Codecov / codecov/patch

BitwardenAutoFillExtension/CredentialProviderViewController.swift#L66-L77

Added lines #L66 - L77 were not covered by tests
}
}

// MARK: Private

/// Cancels the extension request and dismisses the extension's view controller.
Expand Down Expand Up @@ -135,6 +152,28 @@
}
}
}

/// Provides a Fido2 credential for a passkey request.
/// - Parameter passkeyRequest: Request to get the credential.
@available(iOSApplicationExtension 17.0, *)
private func provideFido2Credential(for passkeyRequest: ASPasskeyCredentialRequest) {
guard let appProcessor else {
cancel(error: ASExtensionError(.failed))
10000 return

Check warning on line 162 in BitwardenAutoFillExtension/CredentialProviderViewController.swift

View check run for this annotation

Codecov / codecov/patch

BitwardenAutoFillExtension/CredentialProviderViewController.swift#L159-L162

Added lines #L159 - L162 were not covered by tests
}

Task {
do {
let credential = try await appProcessor.provideFido2Credential(
for: passkeyRequest
)
await extensionContext.completeAssertionRequest(using: credential)
} catch {
Logger.appExtension.error("Error providing credential without user interaction: \(error)")
cancel(error: error)

Check warning on line 173 in BitwardenAutoFillExtension/CredentialProviderViewController.swift

View check run for this annotation

Codecov / codecov/patch

BitwardenAutoFillExtension/CredentialProviderViewController.swift#L165-L173

Added lines #L165 - L173 were not covered by tests
}
}
}
}

// MARK: - AppExtensionDelegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import BitwardenSdk

class MockClientFido2Authenticator: ClientFido2AuthenticatorProtocol {
var credentialsForAutofillResult: Result<[Fido2CredentialAutofillView], Error> = .success([])
var getAssertionResult: Result<BitwardenSdk.GetAssertionResult, Error> = .success(
BitwardenSdk.GetAssertionResult.fixture()
)
var getAssertionMocker = InvocationMockerWithThrowingResult<GetAssertionRequest, GetAssertionResult>()
.withResult(.fixture())
var makeCredentialMocker = InvocationMockerWithThrowingResult<MakeCredentialRequest, MakeCredentialResult>()
.withResult(.fixture())
var silentlyDiscoverCredentialsResult: Result<[Fido2CredentialAutofillView], Error> = .success([])
Expand All @@ -16,7 +15,7 @@ class MockClientFido2Authenticator: ClientFido2AuthenticatorProtocol {
}

func getAssertion(request: BitwardenSdk.GetAssertionRequest) async throws -> BitwardenSdk.GetAssertionResult {
try getAssertionResult.get()
try getAssertionMocker.invoke(param: request)
}

func makeCredential(request: BitwardenSdk.MakeCredentialRequest) async throws -> BitwardenSdk.MakeCredentialResult {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import AuthenticationServices
import BitwardenSdk

@available(iOSApplicationExtension 17.0, *)
extension Fido2CredentialAutofillView {
/// Converts this credential view into an `ASPasskeyCredentialIdentity`.
/// - Returns: A `ASPasskeyCredentialIdentity` from the values of this object.
func toFido2CredentialIdentity() -> ASPasskeyCredentialIdentity {
ASPasskeyCredentialIdentity(
relyingPartyIdentifier: rpId,
userName: safeUsernameForUi,
credentialID: credentialId,
userHandle: userHandle,
recordIdentifier: cipherId
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import BitwardenSdk
import XCTest

@testable import BitwardenShared

class Fido2CredentialAutofillViewExtensionsTests: BitwardenTestCase { // swiftlint:disable:this type_name
// MARK: Tests

/// `toFido2CredentialIdentity()` returns the converted `ASPasskeyCredentialIdentity`.
func test_toFido2CredentialIdentity() throws {
let subject = Fido2CredentialAutofillView(
credentialId: Data(repeating: 1, count: 16),
cipherId: "1",
rpId: "myApp.com",
userNameForUi: "username",
userHandle: Data(repeating: 1, count: 16)
)
let identity = subject.toFido2CredentialIdentity()
XCTAssertTrue(
identity.relyingPartyIdentifier == subject.rpId
&& identity.userName == subject.userNameForUi
&& identity.credentialID == subject.credentialId
&& identity.userHandle == subject.userHandle
&& identity.recordIdentifier == subject.cipherId
)
}

/// `toFido2CredentialIdentity()` returns the converted `ASPasskeyCredentialIdentity`
/// when `userNameForUI` is `nil`.
func test_toFido2CredentialIdentity_userNameForUINil() throws {
let subject = Fido2CredentialAutofillView(
credentialId: Data(repeating: 1, count: 16),
cipherId: "1",
rpId: "myApp.com",
userNameForUi: nil,
userHandle: Data(repeating: 1, count: 16)
)
let identity = subject.toFido2CredentialIdentity()
XCTAssertTrue(
identity.relyingPartyIdentifier == subject.rpId
&& identity.userName == Localizations.unknownAccount
&& identity.credentialID == subject.credentialId
&& identity.userHandle == subject.userHandle
&& identity.recordIdentifier == subject.cipherId
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
/// The service to manage events.
private let eventService: EventService

/// A store to be used on Fido2 flows to get/save credentials.
let fido2CredentialStore: Fido2CredentialStore

/// A helper to be used on Fido2 flows that requires user interaction and extends the capabilities
/// of the `Fido2UserInterface` from the SDK.
let fido2UserInterfaceHelper: Fido2UserInterfaceHelper

/// The service used to manage the credentials available for AutoFill suggestions.
private let identityStore: CredentialIdentityStore

Expand All @@ -60,6 +67,9 @@
/// - clientService: The service that handles common client functionality such as encryption and decryption.
/// - errorReporter: The service used by the application to report non-fatal errors.
/// - eventService: The service to manage events.
/// - fido2UserInterfaceHelper: A helper to be used on Fido2 flows that requires user interaction
/// and extends the capabilities of the `Fido2UserInterface` from the SDK.
/// - fido2CredentialStore: A store to be used on Fido2 flows to get/save credentials.
/// - identityStore: The service used to manage the credentials available for AutoFill suggestions.
/// - pasteboardService: The service used to manage copy/pasting from the device's clipboard.
/// - stateService: The service used by the application to manage account state.
Expand All @@ -70,6 +80,8 @@
clientService: ClientService,
errorReporter: ErrorReporter,
eventService: EventService,
fido2CredentialStore: Fido2CredentialStore,
fido2UserInterfaceHelper: Fido2UserInterfaceHelper,
identityStore: CredentialIdentityStore = ASCredentialIdentityStore.shared,
pasteboardService: PasteboardService,
stateService: StateService,
Expand All @@ -79,6 +91,8 @@
self.clientService = clientService
self.errorReporter = errorReporter
self.eventService = eventService
self.fido2CredentialStore = fido2CredentialStore
self.fido2UserInterfaceHelper = fido2UserInterfaceHelper
self.identityStore = identityStore
self.pasteboardService = pasteboardService
self.stateService = stateService
Expand Down Expand Up @@ -151,7 +165,15 @@

if #available(iOS 17, *) {
let identities = decryptedCiphers.compactMap(\.credentialIdentity)
try await identityStore.replaceCredentialIdentities(identities)
let fido2Identities = try await clientService.platform().fido2()
.authenticator(
userInterface: fido2UserInterfaceHelper,
credentialStore: fido2CredentialStore
)
.credentialsForAutofill()
.compactMap { $0.toFido2CredentialIdentity() }

try await identityStore.replaceCredentialIdentities(identities + fido2Identities)
Logger.application.info("AutofillCredentialService: replaced \(identities.count) credential identities")
} else {
let identities = decryptedCiphers.compactMap(\.passwordCredentialIdentity)
Expand Down Expand Up @@ -210,7 +232,10 @@
private extension CipherView {
@available(iOS 17, *)
var credentialIdentity: (any ASCredentialIdentity)? {
passwordCredentialIdentity
guard shouldGetPasswordCredentialIdentity else {
return nil

Check warning on line 236 in BitwardenShared/Core/Autofill/Services/AutofillCredentialService.swift

View check run for this annotation

Codecov / codecov/patch

BitwardenShared/Core/Autofill/Services/AutofillCredentialService.swift#L236

Added line #L236 was not covered by tests
}
return passwordCredentialIdentity
}

var passwordCredentialIdentity: ASPasswordCredentialIdentity? {
Expand All @@ -228,6 +253,12 @@
recordIdentifier: id
)
}

/// Whether the `ASPasswordCredentialIdentity` should be gotten.
/// Otherwise a passkey identity will be provided.
var shouldGetPasswordCredentialIdentity: Bool {
!hasFido2Credentials || login?.password != nil
}
}

// MARK: - CredentialIdentityStore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class AutofillCredentialServiceTests: BitwardenTestCase {
var clientService: MockClientService!
var errorReporter: MockErrorReporter!
var eventService: MockEventService!
var fido2CredentialStore: MockFido2CredentialStore!
var fido2UserInterfaceHelper: MockFido2UserInterfaceHelper!
var identityStore: MockCredentialIdentityStore!
var pasteboardService: MockPasteboardService!
var stateService: MockStateService!
Expand All @@ -25,6 +27,8 @@ class AutofillCredentialServiceTests: BitwardenTestCase {
clientService = MockClientService()
errorReporter = MockErrorReporter()
eventService = MockEventService()
fido2CredentialStore = MockFido2CredentialStore()
fido2UserInterfaceHelper = MockFido2UserInterfaceHelper()
identityStore = MockCredentialIdentityStore()
pasteboardService = MockPasteboardService()
stateService = MockStateService()
Expand All @@ -35,6 +39,8 @@ class AutofillCredentialServiceTests: BitwardenTestCase {
clientService: clientService,
errorReporter: errorReporter,
eventService: eventService,
fido2CredentialStore: fido2CredentialStore,
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
identityStore: identityStore,
pasteboardService: pasteboardService,
stateService: stateService,
Expand All @@ -49,6 +55,8 @@ class AutofillCredentialServiceTests: BitwardenTestCase {
clientService = nil
errorReporter = nil
eventService = nil
fido2CredentialStore = nil
fido2UserInterfaceHelper = nil
identityStore = nil
pasteboardService = nil
stateService = nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#if DEBUG

import BitwardenSdk
import Foundation

/// Report with traceability about Fido2 flows.
public struct Fido2DebuggingReport {
var allCredentialsResult: Result<[BitwardenSdk.CipherView], Error>?
var findCredentialsResult: Result<[BitwardenSdk.CipherView], Error>?
var getAssertionRequest: GetAssertionRequest?
var getAssertionResult: Result<GetAssertionResult, Error>?
var saveCredentialCipher: Result<BitwardenSdk.Cipher, Error>?
}

/// Fido2 builder for debugging report.
public struct Fido2DebuggingReportBuilder {
/// Builder for Fido2 debugging report.
public static var builder = Fido2DebuggingReportBuilder()

var report = Fido2DebuggingReport()

/// Gets the report for Fido2 debugging.
/// - Returns: Fido2 report.
public func getReport() -> Fido2DebuggingReport? {
report
}

mutating func withAllCredentialsResult(_ result: Result<[BitwardenSdk.CipherView], Error>) {
report.allCredentialsResult = result
}

mutating func withFindCredentialsResult(_ result: Result<[BitwardenSdk.CipherView], Error>) {
report.findCredentialsResult = result
}

mutating func withGetAssertionRequest(_ request: GetAssertionRequest) {
report.getAssertionRequest = request
}

mutating func withGetAssertionResult(_ result: Result<GetAssertionResult, Error>) {
report.getAssertionResult = result
}

mutating func withSaveCredentialCipher(_ credential: Result<BitwardenSdk.Cipher, Error>) {
report.saveCredentialCipher = credential
}
}

#endif
37 changes: 26 additions & 11 deletions BitwardenShared/Core/Platform/Services/ServiceContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -456,16 +456,6 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
trustDeviceService: trustDeviceService
)

let autofillCredentialService = DefaultAutofillCredentialService(
cipherService: cipherService,
clientService: clientService,
errorReporter: errorReporter,
eventService: eventService,
pasteboardService: pasteboardService,
stateService: stateService,
vaultTimeoutService: vaultTimeoutService
)

let authRepository = DefaultAuthRepository(
accountAPIService: apiService,
authService: authService,
Expand Down Expand Up @@ -552,9 +542,34 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
)
)

#if DEBUG
let fido2CredentialStore = DebuggingFido2CredentialStoreService(
fido2CredentialStore: Fido2CredentialStoreService(
cipherService: cipherService,
clientService: clientService,
errorReporter: errorReporter,
syncService: syncService
)
)
#else
let fido2CredentialStore = Fido2CredentialStoreService(
cipherService: cipherService,
clientService: clientService
clientService: clientService,
errorReporter: errorReporter,
syncService: syncService
)
#endif

let autofillCredentialService = DefaultAutofillCredentialService(
cipherService: cipherService,
clientService: clientService,
errorReporter: errorReporter,
eventService: eventService,
fido2CredentialStore: fido2CredentialStore,
fido2UserInterfaceHelper: fido2UserInterfaceHelper,
pasteboardService: pasteboardService,
stateService: stateService,
vaultTimeoutService: vaultTimeoutService
)

self.init(
Expand Down
Loading
Loading
0