WWDC Quick Look 💓 By SwiftGGTeam
Streamline local authorization flows

Streamline local authorization flows

观看原视频

Highlight

这场 Session 讲了 LocalAuthentication 框架在 iOS 16、macOS 13 上的新变化。核心概念是把”认证”(authentication)和”授权”(authorization)区分开。

核心内容

很多 App 都要保护敏感资源:用户资料、私钥、文件解密能力、交易确认按钮。旧做法通常从“验证用户身份”开始:创建 LAContext,让系统显示 Face ID、Touch ID 或密码界面,然后把这个上下文交给 Security 框架继续使用。

这个流程能工作,但开发者要自己串起多个概念。资源是什么,允许执行哪个操作,访问控制列表(ACL)要求什么认证方式,授权结果要绑定到哪个查询里,都要在代码里处理。Session 用 Secure Enclave 私钥签名举例:私钥在 Secure Enclave 中,App 不直接拿到私钥,只请求 Secure Enclave 完成签名操作。

Apple 在 iOS 16 和 macOS 13 的 LocalAuthentication 中加入了面向授权的高层 API。核心抽象是 LARight:一个 Right 代表 App 定义的受保护操作。开发者可以把问题从“用户身份验证”推进到“这个用户现在能不能执行这个操作”。

LARight 适合保护 App 内部资源,例如进入用户资料区。LAPersistedRight 适合跨启动保存授权要求,并由 Secure Enclave 中的唯一密钥支撑。私钥操作仍然必须先授权,公钥操作可以直接执行,这让二次验证、挑战签名这类流程少写很多底层拼装代码。

详细内容

用 LAContext 授权一次 Secure Enclave 签名(04:58

Session 先回顾现有做法。签名前,App 要取出 Secure Enclave 私钥关联的访问控制列表。这里依赖 Security 框架的 SecItemCopyMatching,并在查询中要求返回属性。

let query: [String: Any] = [
    kSecClass as String: kSecClassKey,
    kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
    kSecAttrApplicationTag as String: "com.example.app.key",
    kSecReturnAttributes as String: true,
]

var item: CFTypeRef? = nil
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let attrs = item as? NSDictionary, let accessControl = attrs[kSecAttrAccessControl] else {
    throw .aclNotFound
}

关键点:

  • kSecClassKey 表示查询目标是密钥。
  • kSecAttrTokenIDSecureEnclave 把范围限制在 Secure Enclave 里的密钥。
  • kSecAttrApplicationTag 用稳定标签找到 App 创建的那把密钥。
  • kSecReturnAttributes 要求返回密钥属性,从而读取 kSecAttrAccessControl
  • SecItemCopyMatching 成功后,代码从返回属性中取出 ACL;失败就抛出 aclNotFound

拿到 ACL 后,LAContext 可以在 App 选择的时机触发授权 UI(05:15)。

let context = LAContext()
try await context.evaluateAccessControl(accessControl as! SecAccessControl, 
                      operation: .useKeySign, 
                       localizedReason: "Authentication is required to proceed")

关键点:

  • LAContext() 创建本次授权使用的上下文。
  • evaluateAccessControl 直接评估前面取出的 ACL。
  • operation: .useKeySign 指明这次授权是为了执行签名操作。
  • localizedReason 是用户在系统授权界面中看到的原因说明。

授权完成后,要把同一个 LAContext 绑定到取私钥引用的查询里(05:44)。

let query: [String: Any] = [
    kSecClass as String: kSecClassKey,
    kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
    kSecAttrApplicationTag as String: "com.example.app.key",
    kSecReturnRef as String: true,
    kSecUseAuthenticationContext as String: context
]

var item: CFTypeRef? = nil
guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, item != nil else { 
    throw .keyNotFound
}

关键点:

  • kSecReturnRef 要求返回可用于后续操作的密钥引用。
  • kSecUseAuthenticationContext 把已经授权过的 context 传给 Security 查询。
  • 同一个上下文绑定到私钥引用后,执行签名时不会再弹一次认证界面。
  • 这个免重复提示的效果持续到 LAContext 失效。

最后,App 用返回的 SecKey 执行签名(06:00)。

let privateKey = item as! SecKey

var error: Unmanaged<CFError>?
guard let sgt = SecKeyCreateSignature(privateKey, self.algorithm, blob, &error) as Data? else {
    throw .signatureFailure
}

关键点:

  • item 被转换为 SecKey,它是私钥引用。
  • SecKeyCreateSignature 请求 Secure Enclave 用私钥对 blob 签名。
  • 私钥本身不会交给 App;App 只得到签名结果。
  • 签名失败时,代码抛出 signatureFailure

用 LARight 表达 App 自己的受保护操作(08:28

如果 App 只想保护一个应用内资源,例如用户资料页,直接使用 LARight 更简单。Right 的要求可以写成“需要生物识别,允许设备密码作为 fallback”。

// LARight: Basic usage

func login() async {
   self.loginRight = LARight(requirement: .biometry(fallback: .devicePasscode))
   do {
       try await loginRight.checkCanAuthorize()
   } catch {
       navigateTo(section: .public)
       return
   }
   do {
      try await self.loginRight.authorize(localizedReason: self.localizedReason)
 navigateTo(section: .protected)
   } catch {
      showError(.authenticationRequired)
   }
}

关键点:

  • LARight(requirement:) 创建一个受保护的授权对象。
  • .biometry(fallback: .devicePasscode) 表示优先使用生物识别,也允许用设备密码完成授权。
  • checkCanAuthorize() 先检查当前用户能否获得这个 Right,不能授权时进入公开区域。
  • authorize(localizedReason:) 触发系统驱动的授权 UI。
  • 授权成功后进入受保护区域,失败时显示认证必需的错误。

Right 有状态。它从 unknown 进入 authorizing,再根据结果进入 authorized 或 notAuthorized。App 可以读取 state,也可以用 KVO、Combine,或者监听默认 NotificationCenter 中的 didBecomeAuthorizeddidBecomeUnauthorized 通知(09:38)。

登出时,App 主动撤销这个 Right(11:01)。

// LARight: Basic usage

func login() async {
   self.loginRight = LARight(requirement: .biometry(fallback: .devicePasscode))
   // ...
   do {
       try await self.loginRight.authorize(localizedReason: self.localizedReason)
  navigateTo(section: .protected)
   } catch {
       showError(.authenticationRequired)
   }
}

func logout() async {   
   await self.loginRight.deauthorize()
}

关键点:

  • self.loginRight 需要被强引用保存;实例释放后授权状态也会丢失。
  • deauthorize() 把已授权的 Right 变回未授权状态。
  • 下次登录时,用户需要重新走授权流程。
  • 这个模型适合“本次 App 运行期间有效”的保护场景。

用 LAPersistedRight 做跨会话二次验证(13:44

LAPersistedRight 由 Right Store 创建。保存后,它由唯一的 Secure Enclave 密钥支撑,授权要求保持不可变。公钥可以直接导出;私钥签名、解密、密钥交换等操作必须先通过授权。

// LAPersistedRight: Retrieval and private key usage

func generateClientKeys() async throws -> Data {
    let login2FA = LARight(requirement: .biometryCurrentSet)
    let persisted2FA = try await LARightStore.shared.saveRight(login2FA, identifier: "2fa")
    return try await persisted2FA.key.publicKey.bytes
}

func signChallenge(_ challenge: Data, algorithm: SecKeyAlgorithm) async throws -> Data {
    let persisted2FA = try await LARightStore.shared.right(forIdentifier: "2fa")
    let localizedReason = "Biometric authentication is required to proceed"
    try await persisted2FA.authorize(localizedReason: localizedReason)
    return try await persisted2FA.key.sign(challenge, algorithm: algorithm)
}

关键点:

  • .biometryCurrentSet 要求授权绑定到创建 Right 时设备上已登记的生物识别集合。
  • LARightStore.shared.saveRight 把普通 Right 保存为持久化 Right,并使用唯一 identifier。
  • persisted2FA.key.publicKey.bytes 导出公钥字节,适合交给后端保存或验证。
  • right(forIdentifier:) 在之后的 App 会话中取回同一个持久化 Right。
  • authorize(localizedReason:) 成功后,私钥才允许执行受保护操作。
  • persisted2FA.key.sign 用私钥签名后端下发的 challenge。

这个示例对应一个常见登录增强流程:注册阶段生成设备绑定的公钥,后端保存公钥;登录或高风险操作时,后端下发 challenge,设备在用户完成生物识别后用私钥签名,后端用公钥验证签名。

核心启发

  • 做什么:给敏感页面加本地解锁。 例如密码库、健康记录、财务详情页。为什么值得做:LARight 可以直接表达“进入这个区域前要授权”。怎么开始:为页面持有一个 LARight(requirement: .biometry(fallback: .devicePasscode)),进入前调用 checkCanAuthorize()authorize(localizedReason:)

  • 做什么:给登出按钮做真正的本地撤销。 为什么值得做:Right 的授权状态可以由 App 主动撤销,避免用户离开设备后受保护区域继续可见。怎么开始:在登出、切换账号或进入后台策略触发时调用 deauthorize(),再次访问时重新授权。

  • 做什么:用 Secure Enclave 做设备绑定的二次验证。 为什么值得做:LAPersistedRight 背后的私钥只能在授权后使用,适合高风险操作签名。怎么开始:注册时保存 LARight(requirement: .biometryCurrentSet) 并上传公钥;操作时取回 Right,授权后对后端 challenge 签名。

  • 做什么:把旧的 LAContext + Security 代码逐步收敛。 为什么值得做:如果只是保护 App 内容,LARight 能减少手写 ACL 查询、上下文绑定和状态管理。怎么开始:保留需要底层密钥控制的路径,把普通内容解锁改成 Right 模型,再用状态观察刷新 UI。

关联 Session

评论

GitHub Issues · utterances