Skip to content

Commit

Permalink
[PM-8829] Password/Fido2 credentials autofill on prepare credential l…
Browse files Browse the repository at this point in the history
…ist (#759)
  • Loading branch information
fedemkr authored Jul 26, 2024
1 parent 43b5578 commit 4712b00
Show file tree
Hide file tree
Showing 40 changed files with 1,808 additions and 257 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ extension CredentialProviderViewController: Fido2AppExtensionDelegate {
context?.flowWithUserInteraction ?? false
}

@available(iOSApplicationExtension 17.0, *)
func completeAssertionRequest(assertionCredential: ASPasskeyAssertionCredential) {
extensionContext.completeAssertionRequest(using: assertionCredential)
}

@available(iOSApplicationExtension 17.0, *)
func completeRegistrationRequest(asPasskeyRegistrationCredential: ASPasskeyRegistrationCredential) {
extensionContext.completeRegistrationRequest(using: asPasskeyRegistrationCredential)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,55 @@
import AuthenticationServices
import BitwardenSdk

// MARK: - GetAssertionRequest

extension GetAssertionRequest {
/// Initializes a `GetAssertionRequest` based on `fido2RequestParameters`
/// - Parameter fido2RequestParameters: The Fido2 request parameters.
init(fido2RequestParameters: PasskeyCredentialRequestParameters) {
self = .init(
rpId: fido2RequestParameters.relyingPartyIdentifier,
clientDataHash: fido2RequestParameters.clientDataHash,
allowList: fido2RequestParameters.allowedCredentials.map { credentialId in
PublicKeyCredentialDescriptor(
ty: "public-key",
id: credentialId,
transports: nil
)
},
options: Options(
rk: false,
uv: BitwardenSdk.Uv(preference: fido2RequestParameters.userVerificationPreference)
),
extensions: nil
)
}

/// Initializes a `GetAssertionRequest` based on `passkeyRequest` and its `credentialIdentity`
/// - Parameters:
/// - passkeyRequest: The `ASPasskeyCredentialRequest` of the flow.
/// - credentialIdentity: The `ASPasskeyCredentialIdentity` of the request.
@available(iOSApplicationExtension 17.0, *)
init(passkeyRequest: ASPasskeyCredentialRequest, credentialIdentity: ASPasskeyCredentialIdentity) {
self = .init(
rpId: credentialIdentity.relyingPartyIdentifier,
clientDataHash: passkeyRequest.clientDataHash,
allowList: [
PublicKeyCredentialDescriptor(
ty: "public-key",
id: credentialIdentity.credentialID,
transports: nil
),
],
options: Options(
rk: false,
uv: BitwardenSdk.Uv(preference: passkeyRequest.userVerificationPreference)
),
extensions: nil
)
}
}

// MARK: - MakeCredentialRequest

extension BitwardenSdk.MakeCredentialRequest: CustomDebugStringConvertible {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,67 @@ import XCTest

@testable import BitwardenShared

// MARK: - GetAssertionRequest

class GetAssertionRequestTests: BitwardenTestCase {
// MARK: Tests

/// `init(fido2RequestParameters:)` initializes correctly
func test_init_fido2RequestParameters() {
let allowedCredentials = [
Data(repeating: 2, count: 32),
Data(repeating: 5, count: 32),
]
let passkeyParameters = MockPasskeyCredentialRequestParameters(allowedCredentials: allowedCredentials)

let request = GetAssertionRequest(fido2RequestParameters: passkeyParameters)

XCTAssertEqual(request.clientDataHash, passkeyParameters.clientDataHash)
XCTAssertEqual(request.rpId, passkeyParameters.relyingPartyIdentifier)
XCTAssertFalse(request.options.rk)
XCTAssertEqual(request.options.uv, .preferred)
XCTAssertNil(request.extensions)
XCTAssertEqual(
request.allowList,
allowedCredentials.map { credentialId in
PublicKeyCredentialDescriptor(
ty: "public-key",
id: credentialId,
transports: nil
)
}
)
}

/// `init(passkeyRequest:credentialIdentity:)` initializes correctly
@available(iOS 17.0, *)
func test_init_passkeyRequest_credentialIdentity() {
let passkeyRequest = ASPasskeyCredentialRequest.fixture()
guard let credentialIdentity = passkeyRequest.credentialIdentity as? ASPasskeyCredentialIdentity else {
XCTFail("Credential identity is not ASPasskeyCredentialIdentity.")
return
}

let request = GetAssertionRequest(passkeyRequest: passkeyRequest, credentialIdentity: credentialIdentity)

XCTAssertEqual(request.clientDataHash, passkeyRequest.clientDataHash)
XCTAssertEqual(request.rpId, credentialIdentity.relyingPartyIdentifier)
XCTAssertFalse(request.options.rk)
XCTAssertEqual(request.options.uv, .discouraged)
XCTAssertNil(request.extensions)
XCTAssertEqual(
request.allowList,
[
PublicKeyCredentialDescriptor(
ty: "public-key",
id: credentialIdentity.credentialID,
transports: nil
),
]
)
}
}

// MARK: - MakeCredentialRequestTests

class MakeCredentialRequestTests: BitwardenTestCase {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import AuthenticationServices

/// Protocol to bypass using @available for passkey request parameters and also to be able to do unit tests
/// given that we cannot create an instance of `ASPasskeyCredentialRequestParameters`.
public protocol PasskeyCredentialRequestParameters {
/// A list of allowed credential IDs for this request. An empty list means all credentials are allowed.
var allowedCredentials: [Data] { get }
/// Hash of client data for credential provider to sign as part of the operation.
var clientDataHash: Data { get }
/// The relying party identifier for this request.
var relyingPartyIdentifier: String { get }
/// A preference for whether the authenticator should attempt to verify that it is being used by its owner,
/// such as through a PIN or biometrics.
var userVerificationPreference: ASAuthorizationPublicKeyCredentialUserVerificationPreference { get }
}

@available(iOSApplicationExtension 17.0, *)
extension ASPasskeyCredentialRequestParameters: PasskeyCredentialRequestParameters {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import AuthenticationServices

@testable import BitwardenShared

/// Mock for `PasskeyCredentialRequestParameters` given that
/// we cannot create an instance of `ASPasskeyCredentialRequestParameters`
class MockPasskeyCredentialRequestParameters: PasskeyCredentialRequestParameters {
var relyingPartyIdentifier: String

var clientDataHash: Data

var userVerificationPreference: ASAuthorizationPublicKeyCredentialUserVerificationPreference

var allowedCredentials: [Data]

init(
relyingPartyIdentifier: String = "myApp.com",
clientDataHash: Data = Data(repeating: 1, count: 32),
userVerificationPreference: ASAuthorizationPublicKeyCredentialUserVerificationPreference = .preferred,
allowedCredentials: [Data] = []
) {
self.relyingPartyIdentifier = relyingPartyIdentifier
self.clientDataHash = clientDataHash
self.userVerificationPreference = userVerificationPreference
self.allowedCredentials = allowedCredentials
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,24 @@ protocol AutofillCredentialService: AnyObject {
/// - Parameters:
/// - passkeyRequest: Request to get the credential.
/// - autofillCredentialServiceDelegate: Delegate for autofill credential operations.
/// - fido2UserVerificationMediatorDelegate: Delegate for Fido2 user verification.
/// - fido2UserInterfaceHelperDelegate: Delegate for Fido2 user interface interaction.
/// - Returns: The passkey credential for assertion.
@available(iOS 17.0, *)
func provideFido2Credential(
for passkeyRequest: ASPasskeyCredentialRequest,
autofillCredentialServiceDelegate: AutofillCredentialServiceDelegate,
fido2UserVerificationMediatorDelegate: Fido2UserVerificationMediatorDelegate
fido2UserInterfaceHelperDelegate: Fido2UserInterfaceHelperDelegate
) async throws -> ASPasskeyAssertionCredential

/// Provides a Fido2 credential for Fido2 request parameters.
/// - Parameters:
/// - fido2RequestParameters: The Fido2 request parameters to ge the assertion credential.
/// - fido2UserInterfaceHelperDelegate: Delegate for Fido2 user interface interaction
/// - Returns: The passkey credential for assertion
@available(iOS 17.0, *)
func provideFido2Credential(
for fido2RequestParameters: PasskeyCredentialRequestParameters,
fido2UserInterfaceHelperDelegate: Fido2UserInterfaceHelperDelegate
) async throws -> ASPasskeyAssertionCredential
}

Expand Down Expand Up @@ -271,12 +282,12 @@ extension DefaultAutofillCredentialService: AutofillCredentialService {
}

@available(iOS 17.0, *)
func provideFido2Credential( // swiftlint:disable:this function_body_length
func provideFido2Credential(
for passkeyRequest: ASPasskeyCredentialRequest,
autofillCredentialServiceDelegate: AutofillCredentialServiceDelegate,
fido2UserVerificationMediatorDelegate: Fido2UserVerificationMediatorDelegate
fido2UserInterfaceHelperDelegate: Fido2UserInterfaceHelperDelegate
) async throws -> ASPasskeyAssertionCredential {
guard let credentialIdentiy = passkeyRequest.credentialIdentity as? ASPasskeyCredentialIdentity else {
guard let credentialIdentity = passkeyRequest.credentialIdentity as? ASPasskeyCredentialIdentity else {
throw AppProcessorError.invalidOperation
}

Expand All @@ -285,25 +296,49 @@ extension DefaultAutofillCredentialService: AutofillCredentialService {
throw Fido2Error.userInteractionRequired
}

fido2UserInterfaceHelper.setupDelegate(
fido2UserVerificationMediatorDelegate: fido2UserVerificationMediatorDelegate
let request = GetAssertionRequest(
passkeyRequest: passkeyRequest, credentialIdentity: credentialIdentity
)

let request = GetAssertionRequest(
rpId: credentialIdentiy.relyingPartyIdentifier,
clientDataHash: passkeyRequest.clientDataHash,
allowList: [
PublicKeyCredentialDescriptor(
ty: "public-key",
id: credentialIdentiy.credentialID,
transports: nil
),
],
options: Options(
rk: false,
uv: BitwardenSdk.Uv(preference: passkeyRequest.userVerificationPreference)
),
extensions: nil
return try await provideFido2Credential(
with: request,
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate,
rpId: credentialIdentity.relyingPartyIdentifier,
clientDataHash: passkeyRequest.clientDataHash
)
}

@available(iOS 17.0, *)
func provideFido2Credential(
for fido2RequestParameters: PasskeyCredentialRequestParameters,
fido2UserInterfaceHelperDelegate: Fido2UserInterfaceHelperDelegate
) async throws -> ASPasskeyAssertionCredential {
try await provideFido2Credential(
with: GetAssertionRequest(fido2RequestParameters: fido2RequestParameters),
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate,
rpId: fido2RequestParameters.relyingPartyIdentifier,
clientDataHash: fido2RequestParameters.clientDataHash
)
}

// MARK: Private

/// Provides a Fido2 credential based for the given request.
/// - Parameters:
/// - request: Request to get the assertion credential.
/// - fido2UserInterfaceHelperDelegate: Delegate for Fido2 user interface interaction.
/// - rpId: The relying party identifier of the request.
/// - clientDataHash: The client data hash of the request.
/// - Returns: The passkey assertion credential for the request.
@available(iOS 17.0, *)
private func provideFido2Credential(
with request: GetAssertionRequest,
fido2UserInterfaceHelperDelegate: Fido2UserInterfaceHelperDelegate,
rpId: String,
clientDataHash: Data
) async throws -> ASPasskeyAssertionCredential {
fido2UserInterfaceHelper.setupDelegate(
fido2UserInterfaceHelperDelegate: fido2UserInterfaceHelperDelegate
)

#if DEBUG
Expand All @@ -324,9 +359,9 @@ extension DefaultAutofillCredentialService: AutofillCredentialService {

return ASPasskeyAssertionCredential(
userHandle: assertionResult.userHandle,
relyingParty: credentialIdentiy.relyingPartyIdentifier,
relyingParty: rpId,
signature: assertionResult.signature,
clientDataHash: passkeyRequest.clientDataHash,
clientDataHash: clientDataHash,
authenticatorData: assertionResult.authenticatorData,
credentialID: assertionResult.credentialId
)
Expand Down
Loading

0 comments on commit 4712b00

Please sign in to comment.