Passkeys

什么是Passkeys?

Passkeys是苹果提供的一种账户密码验证方式的替代品,它创建账户和登录账户无需用户输入密码。它只需要验证用户的TouchId或者FaceId。

预置条件

  1. 用户设备iOS 16.0+ 、iPadOS 16.0+ 、Mac Catalyst 16.0+
  2. 用户设备启用iCloud钥匙串。
  3. 开发者Xcode 14.0+
  4. 开发者在服务器根目录放置文件apple-app-site-association。放置后,用户可以通过https:///.well-known/apple-app-site-association 直接访问下载。比如域名是lcslearn.top,通过访问https://lcslearn.top/.well-known/apple-app-site-association可以直接下载。资料参考https://developer.apple.com/documentation/xcode/supporting-associated-domains#Add-the-associated-domain-file-to-your-website

原理

客户端生成公私钥对,把公钥共享给服务器端。服务器端用公钥对用户的数据进行验证,以进行创建账户和登录操作。

在创建和登录的过程中会用到faceid和touchid,这两项是生物识别的技术,可以识别用户是否本人。然后取出存储在本地设备的信息。这个是通用的。如果是账户密码,这个本地信息就是密码。如果是passkeys,这个本地信息就是私钥签名后的数据。然后把这个本地信息发送到服务器进行鉴定。

服务器端存储的是公钥,这个公钥对黑客价值较低,黑客拿到也没用。所以passkeys比账户密码安全。客户端设备存储私钥,用户不用记密码了,也不用保存私钥,私钥对用户不可见,每次需要使用私钥的时候,需要使用faceid或者touchid验证。用户只需要验证faceid和touchid,所以更简单,体验更好。
每一对密钥都会绑定到一个域名,所以不存在钓鱼攻击了。

一句话,Passkeys是非常安全、易用、便捷的账户管理方式,非常推荐使用!

时序图

2024-01-17T02:48:08.png

代码

//创建账户
func signUpWith(userName: String, anchor: ASPresentationAnchor) {
        self.authenticationAnchor = anchor
        let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)

        // Fetch the challenge from the server. The challenge needs to be unique for each request.
        // The userID is the identifier for the user's account.
        let challenge = "123456789000".data(using: .utf8)!
        let userID = Data(UUID().uuidString.utf8)

        let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challenge,
                                                                                                  name: userName, userID: userID)

        // Use only ASAuthorizationPlatformPublicKeyCredentialRegistrationRequests or
        // ASAuthorizationSecurityKeyPublicKeyCredentialRegistrationRequests here.
        let authController = ASAuthorizationController(authorizationRequests: [ registrationRequest ] )
        authController.delegate = self
        authController.presentationContextProvider = self
        authController.performRequests()
        isPerformingModalReqest = true
    }
 //登录
 func signInWith(anchor: ASPresentationAnchor, preferImmediatelyAvailableCredentials: Bool) {
        self.authenticationAnchor = anchor
        let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)

        // Fetch the challenge from the server. The challenge needs to be unique for each request.
        let challenge = "1234567890".data(using: .utf8)

        let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge!)

        // Also allow the user to use a saved password, if they have one.
        let passwordCredentialProvider = ASAuthorizationPasswordProvider()
        let passwordRequest = passwordCredentialProvider.createRequest()

        // Pass in any mix of supported sign-in request types.
        let authController = ASAuthorizationController(authorizationRequests: [ assertionRequest, passwordRequest ] )
        authController.delegate = self
        authController.presentationContextProvider = self

        if preferImmediatelyAvailableCredentials {
            // If credentials are available, presents a modal sign-in sheet.
            // If there are no locally saved credentials, no UI appears and
            // the system passes ASAuthorizationError.Code.canceled to call
            // `AccountManager.authorizationController(controller:didCompleteWithError:)`.
            authController.performRequests(options: .preferImmediatelyAvailableCredentials)
        } else {
            // If credentials are available, presents a modal sign-in sheet.
            // If there are no locally saved credentials, the system presents a QR code to allow signing in with a
            // passkey from a nearby device.
            authController.performRequests()
        }

        isPerformingModalReqest = true
    }
    
 //处理回调
 func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        let logger = Logger()
        switch authorization.credential {
        case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration:
            logger.log("A new passkey was registered: \(credentialRegistration)")
            // Verify the attestationObject and clientDataJSON with your service.
            // The attestationObject contains the user's new public key to store and use for subsequent sign-ins.
            // let attestationObject = credentialRegistration.rawAttestationObject
            // let clientDataJSON = credentialRegistration.rawClientDataJSON

            // After the server verifies the registration and creates the user account, sign in the user with the new account.
            didFinishSignIn()
        case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
            logger.log("A passkey was used to sign in: \(credentialAssertion)")
            // Verify the below signature and clientDataJSON with your service for the given userID.
            // let signature = credentialAssertion.signature
            // let clientDataJSON = credentialAssertion.rawClientDataJSON
            // let userID = credentialAssertion.userID

            // After the server verifies the assertion, sign in the user.
            didFinishSignIn()
        case let passwordCredential as ASPasswordCredential:
            logger.log("A password was provided: \(passwordCredential)")
            // Verify the userName and password with your service.
            // let userName = passwordCredential.user
            // let password = passwordCredential.password

            // After the server verifies the userName and password, sign in the user.
            didFinishSignIn()
        default:
            fatalError("Received unknown authorization type.")
        }

        isPerformingModalReqest = false
    }