在HN上展示:macOS数据保护密钥链用于Electron应用
摘要
一个为macOS上的Electron和Node.js应用提供的安全存储工具,利用现代数据保护密钥链实现生物识别认证和iCloud同步等功能。
查看缓存全文
缓存时间: 2026/08/18 19:02
biw/keychain-store
来源: https://github.com/biw/keychain-store
keychain-store
CI (https://github.com/biw/keychain-store/actions) npm 版本 (https://www.npmjs.com/package/keychain-store) npm 下载量 (https://www.npmjs.com/package/keychain-store)
适用于已签名的 Electron 和 Node 应用的安全存储,其后端是现代的 macOS 数据保护密钥链 (https://support.apple.com/guide/security/keychain-data-protection-secb0694df1a/web)。
- 通过代码签名访问组保护项目;仅与明确授权的应用共享(无
security命令行访问权限) - 将包的访问限制在应用声明的项目名称
- 可选地要求设备所有者认证(触控 ID 或密码),或仅触控 ID
- 存储 UTF-8 字符串和二进制值
安装
pnpm add keychain-store
快速开始
import { openKeychainStore } from "keychain-store";
const store = openKeychainStore({
// 仅触控 ID 认证:
authentication: { accessControl: "biometrics-only" },
// 内置 iCloud 同步
iCloudSync: true,
// 支持不可变和可变账户
accounts: ["installation-id"],
mutableAccounts: ["desktop-token", "desktop-refresh-token"],
});
// 包含 32 个随机字节的 Uint8Array
const installationId = await store.getOrCreate("installation-id");
await store.set("desktop-token", "an application token");
// string | null
const token = await store.get("desktop-token", "string");
// Uint8Array | null
const token = await store.get("desktop-token", "Uint8Array");
accounts 声明不可变的密钥链项目;mutableAccounts 声明可变的项目。存储可以访问它们的并集,但只有可变账户可以更改或删除。一个名称只能属于一个列表,任何一个列表都可以省略。
API
| 方法 | 功能 |
|---|---|
get(account, "Uint8Array") | 返回存储的二进制数据,或 null。 |
get(account, "string") | 返回存储的 UTF-8 字符串,或 null。 |
getOrCreate(account) | 返回现有值或创建 32 个随机字节。 |
getOrCreate(account, value) | 返回现有值或创建提供的字符串或字节。 |
set(account, value) | 创建或替换一个可变项目。 |
remove(account) | 删除一个可变项目并报告其是否存在。 |
status(account) | 检查项目的状态而不返回其值。 |
设置
运行中的 Electron 或 Node 宿主必须具有有效的 Apple 代码签名。默认情况下,此包使用宿主的 bundle identifier 作为其密钥链服务,并让 macOS 使用宿主的私有密钥链访问组。无需配置包身份。
| 选项 | 用途 |
|---|---|
keychainService | 可选的共享命名空间,用于单独签名的应用。 |
authentication | macOS 是否应该要求用户进行认证。 |
iCloudSync | 项目是否应通过 iCloud 密钥链同步。 |
accounts | 存储可访问的不可变项目名称。 |
mutableAccounts | 存储可访问、更改或删除的可变项目名称。 |
数据保护密钥链
此包将通用密码项目存储在 macOS 的 数据保护密钥链 中。其原生实现使用 SecItem API 并设置 kSecUseDataProtectionKeychain: true,而非旧版基于文件的密钥链(由较旧的 Keychain 和 SecKeychain API 使用)。Apple 建议在新工作中使用数据保护密钥链,因为它支持现代访问组、iCloud 密钥链和生物识别访问控制。请参阅 Apple 的密钥链实施指南 (https://developer.apple.com/documentation/technotes/tn3137-on-mac-keychains)。
| 方面 | 旧版基于文件的密钥链 | 数据保护密钥链(本包) |
|---|---|---|
| API 目标 | Keychain 和 SecKeychain;当未设置数据保护目标时使用 SecItem | 使用 kSecUseDataProtectionKeychain: true 的 SecItem |
| 访问模型 | 基于项目的访问控制列表(SecAccess) | 代码签名授权访问组,可选择性地由 SecAccessControl 补充 |
| iCloud 密钥链 | 不支持 | 支持,设置 iCloudSync: true |
| 生物识别保护 | 其旧版访问模型不支持 | 支持,设置 authentication: { accessControl: "biometrics-only" } |
| 命令行检查 | security CLI 可以检查密钥链文件 | security CLI 不能直接检查这些项目 |
| 密钥链访问位置 | 登录、系统和其他基于文件的密钥链 | 本地项目,或用于同步项目的 iCloud 密钥链 |
| 可用性 | 可以被用户登录上下文之外的进程使用 | 需要用户登录上下文 |
通过旧版基于文件的密钥链 API 创建的项目在此处不可用;如有需要,请明确迁移它们。同样,security CLI 也不是检查此存储项目的途径。请改用“钥匙串访问”(Keychain Access):当 iCloudSync 为 false 时,项目出现在 本地项目 下;为 true 时,出现在 iCloud 密钥链 下。
本地开发
设置已签名的 Electron 开发运行时
未修改的 Electron 运行时将自身标识为 Electron,因此它不适合作为应用开发密钥的命名空间。相反,使用 Electron Vite 运行一个缓存的、已签名的、作为单独开发应用(例如 com.example.product.dev)的 Electron 运行时。不设置 keychainService 时,相同的 openKeychainStore() 调用会自动使用该 bundle identifier,使本地值与生产环境隔离。
1. 创建开发签名配置文件
在 Apple 开发者门户中,注册 com.example.product.dev 并为其创建 macOS 开发描述文件(provisioning profile)。启用钥匙串共享(Keychain Sharing)。该配置文件必须允许此完整的访问组:
ABCDE12345.com.example.product.dev
将 ABCDE12345 替换为您的 Apple 开发者团队 ID。Xcode 可以为您创建配置文件:创建一个带有该 bundle identifier 的临时 macOS 应用目标,选择您的团队,添加钥匙串共享功能,然后构建一次。
2. 签名 Electron 副本
将此副本保存在 node_modules 之外的用户缓存中;每当 Electron 版本、开发证书或描述文件更改时,请重新创建它。创建一个主授权(entitlement)文件,包含您的完整标识符和 Electron 的正常运行时授权:
com.apple.application-identifier
ABCDE12345.com.example.product.dev
com.apple.developer.team-identifier
ABCDE12345
keychain-access-groups
ABCDE12345.com.example.product.dev
com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
com.apple.security.cs.disable-library-validation
首先签名 Electron 的辅助应用(Helper apps)。它们不需要您的钥匙串访问组;以下最小辅助应用授权文件足以用于标准的 Electron 开发运行时:
com.apple.security.cs.allow-jit
com.apple.security.cs.allow-unsigned-executable-memory
com.apple.security.cs.disable-library-validation
将两个文件保存为 electron-development.entitlements.plist 和 electron-helper.entitlements.plist,然后运行:
export DEVELOPMENT_BUNDLE_ID="com.example.product.dev"
export DEVELOPMENT_SIGNING_IDENTITY="Apple Development: Your Name (ABCDE12345)"
export DEVELOPMENT_PROVISIONING_PROFILE="/path/to/development.provisionprofile"
export RUNTIME_DIR="$HOME/Library/Caches/example-product/electron-dev"
ditto node_modules/electron/dist "$RUNTIME_DIR"
export ELECTRON_APP="$RUNTIME_DIR/Electron.app"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $DEVELOPMENT_BUNDLE_ID" \
"$ELECTRON_APP/Contents/Info.plist"
cp "$DEVELOPMENT_PROVISIONING_PROFILE" "$ELECTRON_APP/Contents/embedded.provisionprofile"
for helper in "$ELECTRON_APP"/Contents/Frameworks/Electron\ Helper*.app; do
codesign --force --sign "$DEVELOPMENT_SIGNING_IDENTITY" --options runtime \
--timestamp=none --entitlements electron-helper.entitlements.plist "$helper"
done
codesign --force --sign "$DEVELOPMENT_SIGNING_IDENTITY" --options runtime --timestamp=none \
--generate-entitlement-der --entitlements electron-development.entitlements.plist "$ELECTRON_APP"
codesign --verify --deep --strict --verbose=2 "$ELECTRON_APP"
3. 将此运行时用于开发
在启动 Electron Vite(通常是启动 electron-vite dev 的脚本)之前设置以下环境变量:
export ELECTRON_OVERRIDE_DIST_PATH="$RUNTIME_DIR"
export ELECTRON_EXEC_PATH="$ELECTRON_APP/Contents/MacOS/Electron"
除非您有意在应用间共享项目,否则请省略 keychainService。共享服务还需要在开发运行时中包含其匹配的钥匙串访问组授权。
谁可以访问这些项目?
macOS 允许每个使用匹配的钥匙串访问组授权签名的应用访问。请确保您的签名证书、私钥和授权配置安全。
认证
认证控制 macOS 是否要求用户验证访问。它并不决定哪些应用可以访问项目:签名的宿主身份和钥匙串访问组始终决定这一点。
选择一种认证边界。此包不会组合它们,因此一个操作不会产生两个提示。
项目访问控制
authentication: { accessControl: ... } 将要求与项目一起存储。它应用于有授权的应用读取该项目时,即使该应用未使用此包。
| 值 | 结果 |
|---|---|
user-presence | 需要 macOS 设备所有者认证才能读取项目。 |
biometrics-only | 需要触控 ID 才能读取项目。 |
user-presence 允许 macOS 设备所有者认证,例如触控 ID 或用户的密码。在未注册触控 ID 的 Mac 上,biometrics-only 会失败;它不会使用 Apple Watch 或附近的 iPhone。
操作认证
authentication: { operationAuth: ... } 要求当前应用在每个包操作前进行认证。它不会更改存储的项目,因此另一个有授权的应用不需要进行相同的提示。
| 值 | 结果 |
|---|---|
user-presence | 需要 macOS 设备所有者认证。 |
biometrics-only | 需要触控 ID。 |
当不需要额外的用户验证提示时,使用 authentication: "none"。它不会使项目公开;只有满足配置的签名和授权策略的应用才能访问它们。
使用新账户更改项目访问控制
项目的 accessControl 策略是持久性的。要更改它,请使用新策略创建一个新账户,并将您的应用数据迁移过去。对于加密密钥,这通常意味着使用新密钥重新加密应用数据。
operationAuth 不与项目一起存储,可以独立更改。
iCloud 同步
设置 iCloudSync: true 以要求 macOS 通过 iCloud 密钥链同步存储的项目。更改设置永远不会删除现有项目。get() 保持只读。
如果项目仅在相反的同步设置下存在,则拒绝操作并返回 synchronization_migration_required。getOrCreate() 在配置的范围内添加一个副本;它不会覆盖或删除现有副本。此包不检查用户是否登录到 Apple 账户或是否启用了 iCloud 密钥链。即使 macOS 当前无法同步项目,创建也可能在本地成功;成功仅意味着密钥链接受了它,而不是另一台设备收到了它。如果 Security 框架无法创建或访问项目,操作将拒绝并返回其密钥链错误。
值表示
使用字符串表示 UTF-8 文本,使用 Uint8Array 表示二进制数据。调用 get() 时明确选择表示形式。如果请求 "string" 但项目不包含有效的 UTF-8 文本,则拒绝操作并返回 item_not_utf8。
与另一个应用共享密钥
在每个共享此存储的应用中设置相同的 keychainService。该包将访问组推导为运行应用的 Team ID 后跟此值。
const sharedStore = openKeychainStore({
keychainService: "com.example.product.shared",
authentication: "none",
iCloudSync: false,
accounts: ["installation-id"],
mutableAccounts: ["desktop-token"],
});
每个应用必须在其签名授权中包含生成的完整访问组。使用 Electron Builder (https://www.electron.build/mac/) 时,将其添加到 macOS 授权 plist 中。使用 Electron Forge (https://www.electronforge.io/guides/code-signing/code-signing-macos) 时,通过 packagerConfig.osxSign 传递该 plist。
keychain-access-groups
ABCDE12345.com.example.product.shared
com.apple.security.cs.allow-jit
从 Swift 中使用
此仓库还提供了 KeychainStore Swift Package Manager 库,用于已签名的原生 macOS 目标。它通过 Swift Package Manager 分发,而非 npm 包。
从此仓库添加 KeychainStore 库产品:
.package(url: "https://github.com/biw/keychain-store.git", branch: "main")
一旦仓库有带标签的发布版本,请改用版本要求。Swift 库使用与 Node 包相同的项目格式和声明的账户策略。
import KeychainStore
let store = try KeychainStoreSwift(
accounts: ["installation-id"],
authentication: .accessControl(.userPresence),
mutableAccounts: ["desktop-token"],
)
try await store.ensure("installation-id")
let token = try await store.get("desktop-token")
ensure() 创建一个项目而不返回其字节,当原生代码拥有加密工作流时,这很有用。
同步 Swift API
KeychainStoreSwiftSync 提供相同的声明账户方法,但仅使用 authentication: .none。其操作可能会阻塞调用线程,因此除非需要同步边界,否则请使用异步存储。
import KeychainStore
let store = try KeychainStoreSwiftSync(
accounts: ["installation-id"],
mutableAccounts: ["desktop-token"],
)
try store.ensure("installation-id")
let id = try store.get("installation-id")
许可证
MIT
相似文章
Show HN: Bramble – 本地优先的密码管理器
Bramble 是一款本地优先的密码管理器,将加密的密码库存储在用户自己的设备上,提供浏览器扩展、iOS 和 Android 应用,支持点对点同步、密钥和无缝自动填充。
Show HN: 使用密钥驱动加密的匿名年龄验证
ONE 是一个以隐私为中心的身份基础设施,它使用密钥驱动加密进行匿名年龄验证和人类证明,允许用户控制他们的数据,而无需暴露个人可识别信息。
@dreamsofcode_io: 现在正是考虑将你的 SSH 密钥放在硬件安全密钥(如 Yubikey)上的好时机。
一条推文建议将 SSH 密钥使用硬件安全密钥(如 Yubikey)进行保护,并提及 npm、PyPI 和 Crates.io 上正在活跃的跨生态系统供应链攻击(TrapDoor),该攻击涉及恶意包和窃取加密货币的恶意软件。
Show HN: Y – 一个基于Electron的可塑编码代理桌面应用
Y是一个可塑的、以聊天为先的桌面应用,能够并行运行像Claude Code和Codex这样的本地编码代理,并具备自我修改的UI功能。
Coldkey – 后量子 age 密钥生成与纸质备份工具
Coldkey 是一个命令行工具,用于生成后量子 age 加密密钥,并创建带有 QR 码的可打印 HTML 备份,以实现安全的离线存储。