在HN上展示:macOS数据保护密钥链用于Electron应用

Hacker News Top 工具

摘要

一个为macOS上的Electron和Node.js应用提供的安全存储工具,利用现代数据保护密钥链实现生物识别认证和iCloud同步等功能。

大家好,HN,<p>我一直在开发Hansel [1](一个你可以用代理查询的加密个人数据存储),并且没有一个好方法来使用现代macOS数据保护密钥链。<p>Electron的safeStorage [2]使用基于文件的遗留密钥链,这允许其他应用/代理通过`security` CLI查询它。当你有十几个代理在后台运行时,这不太好!数据保护密钥链很好,因为它通过代码签名访问组限制访问,并允许你设置访问规则如Touch ID和/或密码。<p>1: <a href="https://hansel.so/" rel="nofollow">https://hansel.so/</a><p>2. <a href="https://www.electronjs.org/docs/latest/api/safe-storage" rel="nofollow">https://www.electronjs.org/docs/latest/api/safe-storage</a>
查看原文
查看缓存全文

缓存时间: 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可选的共享命名空间,用于单独签名的应用。
authenticationmacOS 是否应该要求用户进行认证。
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: trueSecItem
访问模型基于项目的访问控制列表(SecAccess代码签名授权访问组,可选择性地由 SecAccessControl 补充
iCloud 密钥链不支持支持,设置 iCloudSync: true
生物识别保护其旧版访问模型不支持支持,设置 authentication: { accessControl: "biometrics-only" }
命令行检查security CLI 可以检查密钥链文件security CLI 不能直接检查这些项目
密钥链访问位置登录、系统和其他基于文件的密钥链本地项目,或用于同步项目的 iCloud 密钥链
可用性可以被用户登录上下文之外的进程使用需要用户登录上下文

通过旧版基于文件的密钥链 API 创建的项目在此处不可用;如有需要,请明确迁移它们。同样,security CLI 也不是检查此存储项目的途径。请改用“钥匙串访问”(Keychain Access):当 iCloudSyncfalse 时,项目出现在 本地项目 下;为 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.plistelectron-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_requiredgetOrCreate() 在配置的范围内添加一个副本;它不会覆盖或删除现有副本。此包不检查用户是否登录到 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 – 本地优先的密码管理器

Hacker News Top

Bramble 是一款本地优先的密码管理器,将加密的密码库存储在用户自己的设备上,提供浏览器扩展、iOS 和 Android 应用,支持点对点同步、密钥和无缝自动填充。

Show HN: 使用密钥驱动加密的匿名年龄验证

Hacker News Top

ONE 是一个以隐私为中心的身份基础设施,它使用密钥驱动加密进行匿名年龄验证和人类证明,允许用户控制他们的数据,而无需暴露个人可识别信息。