Secure login with iCloud Keychain verification codes
Privacy & Security 入门 15m

使用 iCloud 钥匙串验证码安全登录

Secure login with iCloud Keychain verification codes

2021年6月10日

在 Apple 官方观看视频

一句话判断

iCloud 钥匙串内置了 TOTP 验证码生成器 —— 用户不再需要 Google Authenticator,Safari 在登录时自动从钥匙串填充验证码,这对开发者来说意味着你应该立刻在登录流程中支持 TOTP 二步验证。

这场 Session 讲了什么

iOS 15 和 macOS 12 的 Safari/iCloud Keychain 新增了内置的验证码(Verification Code)管理功能。用户可以在 iCloud Keychain 中存储和生成基于时间的一次性密码(TOTP,Time-based One-Time Password)。

工作流程:网站在设置二步验证时生成一个 TOTP secret(通常以二维码形式展示),用户在 iOS 15 中可以直接用相机扫描二维码添加到钥匙串。之后每次登录时,Safari 会自动在用户名/密码下方的输入框中填充当前有效的 6 位验证码。

对开发者的影响:你的网站需要支持标准的 TOTP 协议(RFC 6238)。设置二维码的格式要遵循 otpauth://totp/ URI scheme,这样 iOS 的相机才能识别并解析。

值得深挖的点

otpauth URI 的格式规范。标准的 TOTP 设置二维码格式为 otpauth://totp/Issuer:Account?secret=XXXX&issuer=IssuerName。iOS 15 的相机识别器要求 issuer 参数必须存在且和标签中的 Issuer 一致。如果你的二维码格式不规范(缺少 issuer 或使用了非标准参数),相机可能无法解析。建议使用 Google Authenticator 兼容的格式。

自动填充的触发条件。Safari 会在检测到 autocomplete="one-time-code" 属性的 <input> 元素时,自动从钥匙串中查找匹配的 TOTP 并填充。匹配规则基于网站的域名和 TOTP 记录的 issuer。如果你的登录表单没有这个属性,Safari 也会尝试通过启发式方法识别验证码输入框,但显式声明更可靠。

代码片段

Web 端生成 TOTP 设置二维码

<!-- 登录表单中的验证码输入框 -->
<form action="/login" method="POST">
    <input type="text" name="username" autocomplete="username">
    <input type="password" name="password" autocomplete="current-password">
    <!-- 关键:使用 one-time-code 自动填充类型 -->
    <input type="text" name="code" inputmode="numeric"
           autocomplete="one-time-code"
           pattern="[0-9]*" maxlength="6"
           placeholder="验证码">
    <button type="submit">登录</button>
</form>

服务端生成 TOTP Secret

import CryptoKit
import Foundation

func generateTOTPSetupURI(account: String, issuer: String, secret: String) -> String {
    // 生成标准 otpauth URI
    let encodedIssuer = issuer.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? issuer
    let encodedAccount = account.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? account
    return "otpauth://totp/\(encodedIssuer):\(encodedAccount)?secret=\(secret)&issuer=\(encodedIssuer)"
}

// 生成二维码供用户扫描
func generateTOTPQRCode(account: String, issuer: String) {
    let secret = generateRandomSecret()  // Base32 编码的随机密钥
    let uri = generateTOTPSetupURI(account: account, issuer: issuer, secret: secret)
    // 将 URI 编码为二维码展示给用户
    let qrCodeImage = generateQRCode(from: uri)
    // 同时在服务端存储 secret 和用户的关联
    storeTOTPSecret(secret, for: account)
}

验证 TOTP 验证码

import CryptoKit
import Foundation

func verifyTOTP(code: String, secret: String) -> Bool {
    // 获取当前时间步(30 秒为一个时间窗口)
    let timeStep = Int(Date().timeIntervalSince1970 / 30)
    // 将时间步转为大端序的 8 字节数据
    var timeBytes = withUnsafeBytes(of: timeStep.bigEndian) { Data($0) }

    // 用 HMAC-SHA1 计算哈希
    let keyData = base32Decode(secret)!
    let hmac = HMAC<Insecure.SHA1>.authenticationCode(
        for: timeBytes,
        using: SymmetricKey(data: keyData)
    )
    let hash = Data(hmac)

    // 动态截取
    let offset = Int(hash.last! & 0x0F)
    let codeValue = (Int(hash[offset]) & 0x7F) << 24
        | (Int(hash[offset + 1]) & 0xFF) << 16
        | (Int(hash[offset + 2]) & 0xFF) << 8
        | (Int(hash[offset + 3]) & 0xFF)

    let totp = String(format: "%06d", codeValue % 1000000)
    return totp == code
}

最佳实践

  1. 登录页面的验证码输入框必须加 autocomplete="one-time-code"。这一行 HTML 就能让 Safari 自动填充,用户体验从”打开验证器 app -> 复制 -> 切回浏览器 -> 粘贴”变成”什么都不用做”。
  2. TOTP Secret 只展示一次。用户添加到钥匙串后,服务端应该标记设置完成,不再显示二维码或 Secret。如果用户丢失了钥匙串访问,通过备选验证方式(邮箱/手机号)重新设置。
  3. 支持备选验证方式。不是所有用户都用 iCloud Keychain,保留短信、邮件、Authenticator app 等备选方案。

还有什么值得关注

  • iCloud Keychain 的验证码在所有登录同一 Apple ID 的设备间同步(端到端加密)。
  • macOS 上的 Safari 也支持同样的自动填充行为。
  • 如果你的 app 使用 WebViews 登录,WKWebView 在 iOS 15 中也支持验证码自动填充。
WWDC 2021