feat: Add Passkey support to WebAuthn

Refs: https://gitlab.protontech.ch/apple/shared/protoncore/-/merge_requests/1829

Changelog: added
This commit is contained in:
Victor Jalencas 2024-05-31 13:50:18 +00:00
commit e0a9e2b9c1
6 changed files with 54 additions and 30 deletions

View file

@ -1720,4 +1720,20 @@ extension AuthenticatorWithKeyGenerationMock {
}
}
extension LoginTestUser {
func fido2SignatureWithAuthenticationOptions(_ options: AuthenticationOptions) -> Fido2Signature {
.init(signature: Data(
base64Encoded: "MEQCIACXYgPg+2eCHc72pFAan0JhYFaOIqQ++7E9AJwoW3evAiAqYv2S1VG4I4wU/rEeg9ppLx9FmnCfpcMVzqqGdlILkA=="
)!,
credentialID: Data(
[214, 89, 242, 193, 240, 89, 89, 49, 22, 245, 29, 37, 39, 207, 145, 53, 240, 133, 121, 30, 193,
196, 143, 230, 104, 21, 129, 81, 32, 172, 93, 34, 150, 176, 62, 233, 66, 142, 140, 171, 100, 11,
72, 233, 203, 148, 132, 168, 88, 189, 25, 126, 20, 65, 35, 17, 42, 224, 110, 50, 203, 203, 166, 82]
),
authenticatorData: Data(base64Encoded: "+95oJ+45pI2RXXNJa4EVk4mJnlacXl6FI+/xqhhc7H4BAAABHQ==")!,
clientData: Data(base64Encoded: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRm5NbEttV1lXSVhMUl9xZG5YSWtzTFF5Q293Tlg1N3dnSUdQQTQwcUJoRSIsIm9yaWdpbiI6Imh0dHBzOi8vYWNjb3VudC5wcm90b24uYmxhY2sifQ==")!,
authenticationOptions: options)
}
}
#endif

View file

@ -151,6 +151,8 @@ public enum LUITranslation: TranslationsExposing {
case twofa_invalid_state_banner
case twofa_invalid_2fa_key
case twofa_unexpected_authorization_type
case twofa_unexpected_signature
case unavailable_authinfo
public var l10n: String {
switch self {
@ -387,6 +389,12 @@ public enum LUITranslation: TranslationsExposing {
case .twofa_unexpected_authorization_type:
return localized(key: "We received an authorization from a type we don't support yet.",
comment: "Error shown when recieving a successful authorization of unkown type.")
case .twofa_unexpected_signature:
return localized(key: "Unexpected FIDO2 signature.",
comment: "Error shown when receiving a FIDO2/Passkey signature that wasn't requested.")
case .unavailable_authinfo:
return localized(key: "We could not initiate the Secure Password update connection. Please try again.",
comment: "Error shown when changing password on an account which has 2 Factor Auth configured, but can't retrieve the authentication challenge")
}
}
}

View file

@ -248,6 +248,6 @@
"To manage, add, or remove security keys, please use the Proton %@ web application." = "To manage, add, or remove security keys, please use the Proton %@ web application.";
"Unexpected FIDO2 signature" = "Unexpected FIDO2 signature";
"Unexpected FIDO2 signature." = "Unexpected FIDO2 signature.";
"We could not initiate the Secure Password update connection. Please try again." = "We could not initiate the Secure Password update connection. Please try again.";

View file

@ -51,25 +51,32 @@ extension Fido2View {
let controller = makeAuthController(relyingPartyIdentifier: authenticationOptions.relyingPartyIdentifier,
challenge: authenticationOptions.challenge,
allowedCredentials: authenticationOptions.allowedCredentialIds.map {
ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(
credentialID: $0,
transports: ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported
)
}
allowedCredentials: authenticationOptions.allowedCredentialIds
)
controller.performRequests()
}
private func makeAuthController(relyingPartyIdentifier: String,
challenge: Data,
allowedCredentials: [ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor]) -> ASAuthorizationController {
let provider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
allowedCredentials: [Data]) -> ASAuthorizationController {
let fido2Provider = ASAuthorizationSecurityKeyPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
let request = provider.createCredentialAssertionRequest(challenge: challenge)
request.allowedCredentials = allowedCredentials
let fido2Request = fido2Provider.createCredentialAssertionRequest(challenge: challenge)
fido2Request.allowedCredentials = allowedCredentials.map {
ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor(
credentialID: $0,
transports: ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor.Transport.allSupported
)
}
let controller = ASAuthorizationController(authorizationRequests: [request])
let passkeyProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingPartyIdentifier)
let passkeyRequest = passkeyProvider.createCredentialAssertionRequest(challenge: challenge)
passkeyRequest.allowedCredentials = allowedCredentials.map {
ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: $0)
}
let controller = ASAuthorizationController(authorizationRequests: [fido2Request, passkeyRequest])
controller.presentationContextProvider = self
controller.delegate = self
return controller
@ -104,7 +111,14 @@ extension Fido2View.ViewModel: ASAuthorizationControllerDelegate {
provideFido2Signature(Fido2Signature(credentialAssertion: credentialAssertion, authenticationOptions: authenticationOptions))
} else {
PMLog.error("Invalid state: received a signature for which we don't keep the challenge")
bannerState = .error(content: .init(message: "Unexpected FIDO2 signature"))
bannerState = .error(content: .init(message: LUITranslation.twofa_unexpected_signature.l10n))
}
case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
if case .configured(let authenticationOptions) = state {
provideFido2Signature(Fido2Signature(credentialAssertion: credentialAssertion, authenticationOptions: authenticationOptions))
} else {
PMLog.error("Invalid state: received a signature for which we don't keep the challenge")
bannerState = .error(content: .init(message: LUITranslation.twofa_unexpected_signature.l10n))
}
default:
PMLog.error("Received unknown authorization type: \(authorization.credential)", sendToExternal: true)
@ -133,7 +147,7 @@ extension Fido2View.ViewModel: ASAuthorizationControllerPresentationContextProvi
@available(iOS 15.0, macOS 12.0, *)
extension Fido2Signature {
init(credentialAssertion: ASAuthorizationSecurityKeyPublicKeyCredentialAssertion, authenticationOptions: AuthenticationOptions) {
init(credentialAssertion: ASAuthorizationPublicKeyCredentialAssertion, authenticationOptions: AuthenticationOptions) {
self = .init(signature: credentialAssertion.signature,
credentialID: credentialAssertion.credentialID,
authenticatorData: credentialAssertion.rawAuthenticatorData,

View file

@ -23,26 +23,12 @@ import Foundation
import ProtonCoreDataModel
import ProtonCoreNetworking
import ProtonCoreAuthentication
import ProtonCoreServices
public class LoginTestUser {
public let username: String
public let password: String
public let twoFactorCode: String = "123456"
public func fido2SignatureWithAuthenticationOptions(_ options: AuthenticationOptions) -> Fido2Signature {
.init(signature: Data(
base64Encoded: "MEQCIACXYgPg+2eCHc72pFAan0JhYFaOIqQ++7E9AJwoW3evAiAqYv2S1VG4I4wU/rEeg9ppLx9FmnCfpcMVzqqGdlILkA=="
)!,
credentialID: Data(
[214, 89, 242, 193, 240, 89, 89, 49, 22, 245, 29, 37, 39, 207, 145, 53, 240, 133, 121, 30, 193,
196, 143, 230, 104, 21, 129, 81, 32, 172, 93, 34, 150, 176, 62, 233, 66, 142, 140, 171, 100, 11,
72, 233, 203, 148, 132, 168, 88, 189, 25, 126, 20, 65, 35, 17, 42, 224, 110, 50, 203, 203, 166, 82]
),
authenticatorData: Data(base64Encoded: "+95oJ+45pI2RXXNJa4EVk4mJnlacXl6FI+/xqhhc7H4BAAABHQ==")!,
clientData: Data(base64Encoded: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRm5NbEttV1lXSVhMUl9xZG5YSWtzTFF5Q293Tlg1N3dnSUdQQTQwcUJoRSIsIm9yaWdpbiI6Imh0dHBzOi8vYWNjb3VudC5wcm90b24uYmxhY2sifQ==")!,
authenticationOptions: options)
}
public init(username: String, password: String) {
self.username = username

View file

@ -124,7 +124,7 @@ extension PasswordChangeView {
func savePasswordTapped() {
Task { @MainActor in
guard let authInfo = try? await self.passwordChangeService?.fetchAuthInfo() else {
bannerState = .error(content: .init(message: "We could not initiate the Secure Password update connection. Please try again."))
bannerState = .error(content: .init(message: LUITranslation.unavailable_authinfo.l10n))
return
}
self.authInfo = authInfo