文档中心 / 开发者

Swift SDK(iOS / macOS)

在 iOS 与 macOS App 中集成 MimiKeySDK:配置回调、授权、选条目、读取、新建、修改与撤销,以及等待界面和错误处理。

概览

MimiKeySDK 是第三方授权 v3 的 Apple 平台实现。它负责切换到 MimiKey、等待用户在批准卡上操作、换取和续期令牌、为每次调用签名,并在本机解开密封结果。你的代码只调用几个 async 方法。

  • 平台:iOS 17+ / macOS 14+,Swift 5.9 工具链。
  • 依赖:仅 CryptoKit、Security、SwiftUI,无第三方库。
  • 客户端类型:public(无 client_secret,用 PKCE 换 token)。

提示 下载 SDK 源码包与可直接运行的示例 App,见 SDK 下载与示例 App。

准备工作

  1. 在 MimiKey for Mac 侧栏「开发者接入」中登记一个公开客户端,回调地址填你的自定义 scheme,例如 com.example.ai://mimikey/callback。记下 client_id(pmc_…)。参见获取 client_id。
  2. 在测试设备上安装 MimiKey 并登录账号。

安装

在 Xcode 中选择 File → Add Package Dependencies…,点 Add Local… 选择解压后的 MimiKeySDK 目录,把产品 MimiKeySDK 加到你的 App target。

使用 Package.swift 时:

// swift-tools-version:5.9
dependencies: [
    .package(path: "../MimiKeySDK"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "MimiKeySDK", package: "MimiKeySDK"),
    ]),
]

配置 Info.plist

需要两项:注册你的回调 scheme,并声明你要查询 mimikey scheme(SDK 用它判断 MimiKey 是否已安装)。

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.ai</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.example.ai</string>
    </array>
  </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
  <string>mimikey</string>
</array>
  • 回调 URL 必须与登记的 redirect_uris 之一逐字一致。
  • iOS 上缺少 LSApplicationQueriesSchemes 时,SDK 会一直认为 MimiKey 未安装并抛出 notInstalled。
  • macOS 同样需要 CFBundleURLTypes;LSApplicationQueriesSchemes 在 Mac 上无害,SDK 改用 NSWorkspace 判断安装。
  • 沙盒化的 Mac App 需要 com.apple.security.network.client 权限。

创建客户端

在整个 App 中保持一个 MimiKeyClient 实例:

import MimiKeySDK

let mimikey = MimiKeyClient(configuration: MimiKeyConfiguration(
    clientID: "pmc_xxxxxxxxxxxxxxxx",
    redirectURL: URL(string: "com.example.ai://mimikey/callback")!
))

MimiKeyConfiguration(clientID:redirectURL:origin:downloadURL:) 的参数:

参数默认值说明
clientID—登记得到的 client_id
redirectURL—登记过的回调地址
originhttps://password.tminos.comMimiKey 服务器地址
downloadURLhttps://password.tminos.comnotInstalled 错误中引导用户下载的地址

MimiKeyClient.init 还接受可选参数 store(自定义密钥存储,默认系统钥匙串)、urlSession、appSwitcher、pollInterval(默认 2 秒)与 authorizeTimeout(默认 600 秒),通常只在测试中使用。

把回调交给 SDK

把 App 收到的每一个 URL 都交给 handleCallback(_:)。它按 state 匹配正在等待的流程;不是 MimiKey 回调或 state 不符时返回 false 并忽略。

SwiftUI(iOS 与 macOS)

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    mimikey.handleCallback(url)
                }
        }
    }
}

UIKit

// SceneDelegate
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    for context in URLContexts {
        mimikey.handleCallback(context.url)
    }
}

AppKit

func applicationWillFinishLaunching(_ notification: Notification) {
    NSAppleEventManager.shared().setEventHandler(
        self,
        andSelector: #selector(handleGetURL(_:withReplyEvent:)),
        forEventClass: AEEventClass(kInternetEventClass),
        andEventID: AEEventID(kAEGetURL)
    )
}

@objc func handleGetURL(_ event: NSAppleEventDescriptor, withReplyEvent reply: NSAppleEventDescriptor) {
    guard let string = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
          let url = URL(string: string) else { return }
    mimikey.handleCallback(url)
}

提示 回调 URL 只带 code、request_id、state、status,不携带任何密文或明文。结果总是由 SDK 用签名请求取回,再用本机的 seal 私钥解开。

授权

let grant = try await mimikey.authorize()
print(grant.grantID, grant.approvalKeyFingerprint, grant.vaults)

authorize() 会:

  1. 生成本次授权专属的 seal 密钥对(X25519)、审批密钥对(Ed25519)、PKCE verifier 与 state;
  2. 打开 mimikey://authorize,MimiKey 显示同意卡;
  3. 等待回调(最长 authorizeTimeout,默认 600 秒),用 code 与 PKCE verifier 换取令牌;
  4. 把密钥与令牌写入钥匙串,返回 MimiKeyGrant。
MimiKeyGrant 属性说明
grantID授权 ID
accountID授权用户的账号 ID
vaults用户勾选的库([MimiKeyVault],含 id、name、kind、mode)
approvalKeyFingerprint审批钥指纹(16 位十六进制),与同意卡上显示的一致

授权只能通过回调完成:如果用户没有在 MimiKey 里操作就切回你的 App,authorize() 会继续等待,直到超时、cancelPending() 或任务被取消。

不出网读取本机状态:

mimikey.isAuthorized   // 本机是否持有可用的授权
mimikey.grant          // 授权时保存的 MimiKeyGrant,未授权时为 nil
mimikey.reset()        // 只清除本机凭据,不通知服务器(例如你的 App 退出登录时)

再次调用 authorize() 会生成新的授权并覆盖本机保存的旧授权。

选一个条目

let item = try await mimikey.pickItem(
    hint: MimiKeyHint(url: "https://bank.example.com", kind: .login),
    reason: "登录示例银行完成对账"
)
item.id                 // 条目 ID
item.revision           // 修订号
item.record.username
item.record.password    // 只在你的进程内存里

MimiKey 显示选条目卡:候选来自授权的库,按 hint 排序(网址同域优先,其次标题包含,再次类型相同),用户可搜索;点「用这条」后结果密封返回。

参数说明
hintMimiKeyHint(url:title:kind:),全部可选,只影响排序
reason显示在卡上的用途说明(最多 500 字符,服务器可见)
vaultID可选;nil 表示在本授权的全部库中挑选

读取已知条目

let same = try await mimikey.getItem(id: item.id, vaultID: grant.vaults[0].id)
  • ask 策略:MimiKey 显示读取批准卡,用户点「允许一次」。
  • trust 策略:请求创建即为已批准,打开 MimiKey 后由本机直接执行,卡上只闪一行「已按你的信任设置提供」。

新建条目

let created = try await mimikey.createItem(
    kind: .login,
    prefill: MimiKeyPrefill(
        title: "示例银行",
        url: "https://bank.example.com/login",
        usernameHint: "me@example.com"
    ),
    reason: "登录示例银行完成对账"
)
if let item = created.item {
    // 用户存入了授权范围内的库:拿到完整条目
} else {
    // created.savedOutOfScope == true:用户存到了授权范围之外的库,本次拿不到明文
}

MimiKey 显示安全卡:即该类型的编辑表单,密码由用户在 MimiKey 里输入,你的 App 从不经手。

参数说明
kindMimiKeyItemKind:.login、.creditCard、.bankCard、.apiKey、.server、.secureNote
prefillMimiKeyPrefill(title:url:usernameHint:extraHint:),预填到表单,用户可修改;不得放入任何凭据
reason显示在卡上的用途说明
vaultID可选;默认是本授权的第一个库
MimiKeyCreated 属性说明
requestID请求 ID
item完整条目;存入授权范围外的库时为 nil
summaryMimiKeyItemSummary(id、title、url、kind),不含密码与备注
savedOutOfScope是否存到了授权范围之外

同一授权、同一个库中已有一个网址主机名相同的待处理新建请求时,服务器会返回已有请求而不是新建,SDK 继续等待它。

修改条目

let updated = try await mimikey.updateItem(
    id: item.id,
    vaultID: grant.vaults[0].id,
    patch: [.password: "新密码"],
    reason: "已在网站上更换密码,同步到 MimiKey"
)
updated.itemID      // 条目 ID
updated.revision    // 写入后的新修订号

SDK 先创建草稿请求拿到 request_id,再把补丁 {"patch":{…}} 密封给账号身份公钥后上传;服务器只见密文。用户在修改卡上看到每个字段的原值与新值,点「保存修改」后由设备写入新修订。

patch 的键是 MimiKeyField:标准字段 .title、.username、.password、.url、.notes,或用 .extra("card_number") 指定该类型的扩展字段。你的 App 不会收到修改后的明文,只收到新修订号。

撤销

try await mimikey.revoke()

调用 POST /api/oauth/revoke(以审批钥签名),然后清空本机钥匙串中的全部凭据。如果服务器上授权已经失效,revoke() 仍会清空本机并正常返回。

等待界面

打开 MimiKey 之后,用户可能什么都不做就切回你的 App。SDK 同时等待回调、每 2 秒轮询一次请求状态、并在 App 回到前台时立即查询;超时以服务器返回的 expires_at 为准。你的等待界面必须提供「重试」与「取消」两个出口:

@MainActor
final class Model: ObservableObject {
    @Published var waiting: MimiKeyWaiting?
    init() {
        mimikey.onWaiting = { [weak self] w in self?.waiting = w }   // 在主线程回调
    }
    func retry() { Task { try? await mimikey.retryOpen() } }          // 再次打开 MimiKey
    func cancel() { mimikey.cancelPending() }                         // 等待中的调用抛出 .cancelled
}
MimiKeyWaiting 属性说明
phase.authorize 或 .request(op:)
requestID请求 ID(授权阶段为 nil)
waitingOn轮到谁:user 或 device
waitingReasonconsent、approval、pick、input、review、no_unlocked_device
expiresAt过期时间
message中文参考文案

建议的提示口径:

waitingOn / waitingReason提示
user / consent等你在 MimiKey 里确认授权
user / approval等你在 MimiKey 里批准这次使用
user / pick等你在 MimiKey 里选一个条目
user / input等你在 MimiKey 的安全卡里填写并保存
user / review等你在 MimiKey 里确认修改
device / no_unlocked_device已批准,等你的任一设备解锁 MimiKey 后执行

cancelPending() 会尽力把服务器上的请求置为 cancelled。直接取消调用所在的 Task 效果相同。

可选的 MimiKeyButton 提供一个跟随宿主样式的按钮,默认文案「用 MimiKey 填入」:

MimiKeyButton { Task { try await pick() } }

错误处理

所有方法抛出 MimiKeyError:

错误含义建议处理
.notInstalled(downloadURL:)设备上没有 MimiKey引导用户前往 downloadURL 安装
.notAuthorized本机没有授权调用 authorize()
.cancelled你取消了等待,或请求在服务器上被取消无需提示
.denied用户在卡上拒绝尊重用户的决定,不要立即重试
.expired授权码或请求过期,或在有效期内一直没有结果允许用户重试
.notSignedInMimiKey 尚未登录账号提示用户先登录 MimiKey
.revoked授权已被撤销、令牌家族失效或接入方被停用;SDK 已清空本机凭据引导用户重新 authorize()
.conflict修改期间条目已被他处更改重新取得最新条目后再修改
.network(Error)网络请求失败稍后重试
.protocol(String)其他协议或服务器错误,字符串为错误码记录错误码,参见错误码
do {
    let item = try await mimikey.pickItem(reason: "登录示例银行")
    use(item)
} catch MimiKeyError.notInstalled(let url) {
    showInstallPrompt(url)
} catch MimiKeyError.revoked, MimiKeyError.notAuthorized {
    _ = try await mimikey.authorize()
} catch MimiKeyError.denied, MimiKeyError.cancelled {
    // 用户的决定,不打扰
} catch {
    showError(error.localizedDescription)
}

令牌由 SDK 自动管理:access token 剩余不足 30 秒时先用 refresh token 续期;调用返回 token_expired 时续期后重试一次。refresh_token_reused、grant_revoked、client_disabled 等归入 .revoked,SDK 不再自动重试。

提示 修改时条目若已被别处改过,MimiKey 会保留用户手上的版本、以原因 conflict 拒绝这次请求,SDK 抛出 .conflict。处理方式:重新 getItem 取得最新条目,再发起修改。

钥匙串存储

SDK 把每个 client 的凭据存在系统钥匙串的通用密码项中:

项值
servicecom.tminos.mimikey.sdk.<clientID>
访问性kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly(不同步、不迁移到其他设备)
内容审批钥种子、seal 私钥、access / refresh token 与过期时间、grant_id、account_id、账号身份公钥、授权库列表

需要在 App 与扩展之间共享时,可传入 MimiKeyKeychain(clientID:accessGroup:) 作为 store。SDK 不写日志、不缓存明文,解密缓冲区用后清零。

测试

  • 单元测试:注入 MimiKeyMemoryStore() 作为 store,并实现 MimiKeyAppSwitching 协议替代真实的 App 切换,即可在没有 MimiKey 的环境中测试你的流程。
  • 模拟器:iOS 模拟器上通常没有安装 MimiKey,调用会抛出 notInstalled。端到端测试请使用装有 MimiKey 的真机或 Mac。
  • 示例 App:Examples/MimiKeyDemo 演示授权、选条目、新建、修改和撤销的完整流程,见 SDK 下载与示例 App。

最后更新: 2026-10-23