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。
准备工作
- 在 MimiKey for Mac 侧栏「开发者接入」中登记一个公开客户端,回调地址填你的自定义 scheme,例如
com.example.ai://mimikey/callback。记下client_id(pmc_…)。参见获取 client_id。 - 在测试设备上安装 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 | — | 登记过的回调地址 |
origin | https://password.tminos.com | MimiKey 服务器地址 |
downloadURL | https://password.tminos.com | notInstalled 错误中引导用户下载的地址 |
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() 会:
- 生成本次授权专属的 seal 密钥对(X25519)、审批密钥对(Ed25519)、PKCE verifier 与
state; - 打开
mimikey://authorize,MimiKey 显示同意卡; - 等待回调(最长
authorizeTimeout,默认 600 秒),用code与 PKCE verifier 换取令牌; - 把密钥与令牌写入钥匙串,返回
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 排序(网址同域优先,其次标题包含,再次类型相同),用户可搜索;点「用这条」后结果密封返回。
| 参数 | 说明 |
|---|---|
hint | MimiKeyHint(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 从不经手。
| 参数 | 说明 |
|---|---|
kind | MimiKeyItemKind:.login、.creditCard、.bankCard、.apiKey、.server、.secureNote |
prefill | MimiKeyPrefill(title:url:usernameHint:extraHint:),预填到表单,用户可修改;不得放入任何凭据 |
reason | 显示在卡上的用途说明 |
vaultID | 可选;默认是本授权的第一个库 |
MimiKeyCreated 属性 | 说明 |
|---|---|
requestID | 请求 ID |
item | 完整条目;存入授权范围外的库时为 nil |
summary | MimiKeyItemSummary(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 |
waitingReason | consent、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 | 授权码或请求过期,或在有效期内一直没有结果 | 允许用户重试 |
.notSignedIn | MimiKey 尚未登录账号 | 提示用户先登录 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 的凭据存在系统钥匙串的通用密码项中:
| 项 | 值 |
|---|---|
| service | com.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。