Show HN:用于 Electron 应用的 macOS 数据保护钥匙串
Show HN: macOS data protection keychain for Electron apps

原始链接: https://github.com/biw/keychain-store

该软件包为 macOS 上已签名的 Electron 和 Node.js 应用程序提供了一种安全、现代的存储解决方案,利用了**数据保护钥匙串 (Data Protection Keychain)** (SecItem API)。与传统的基于文件的钥匙串不同,该方案支持 iCloud 同步、生物识别验证 (Touch ID) 以及现代代码签名访问组。 ### 主要功能: * **安全性:** 访问仅限于拥有明确代码签名授权的应用程序。无法通过 `security` 命令行工具进行外部检查。 * **多功能性:** 支持存储 UTF-8 字符串和二进制 (`Uint8Array`) 数据。 * **访问控制:** 可配置细粒度的策略,例如“仅生物识别”或“用户在场”验证。 * **同步:** 可选的 iCloud 钥匙串集成,实现跨设备访问。 * **生命周期管理:** 使用不可变 (`accounts`) 和可变 (`mutableAccounts`) 列表来定义严格受控的项目命名空间。 ### 使用方法: 该库需要一个正确签名的宿主应用程序。在开发过程中,开发者必须使用有效的配置文件和钥匙串共享授权对专门的 Electron 运行时进行签名。此外,官方提供了一个配套的 Swift 库供原生 macOS 应用程序使用,以确保 Node.js 和 Swift 组件之间的兼容性和访问共享。该工具是在现代 macOS 工作流程中实现安全、原生 Apple 凭据存储的推荐方案。

抱歉。
相关文章

原文

CI npm version npm downloads

Secure storage for signed Electron and Node apps, backed by the modern macOS Data Protection Keychain.

  • Protect items with code-signing access groups; share only with explicitly entitled apps (no security CLI access)
  • Restrict package access to item names your app declares
  • Optionally require device-owner authentication (Touch ID or password), or Touch ID only
  • Store UTF-8 strings and binary values
import { openKeychainStore } from "keychain-store";

const store = openKeychainStore({
  // touch ID only
  authentication: { accessControl: "biometrics-only" },
  // build in iCloud sync
  iCloudSync: true,
  // support for immutable and mutable accounts
  accounts: ["installation-id"],
  mutableAccounts: ["desktop-token", "desktop-refresh-token"],
});

// Uint8Array containing 32 random bytes
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 declares immutable Keychain items; mutableAccounts declares mutable ones. The store can access their union, while only mutable accounts may be changed or removed. A name belongs in exactly one list, and either list may be omitted.

Method What it does
get(account, "Uint8Array") Returns stored binary data, or null.
get(account, "string") Returns a stored UTF-8 string, or null.
getOrCreate(account) Returns an existing value or creates 32 random bytes.
getOrCreate(account, value) Returns an existing value or creates the supplied string or bytes.
set(account, value) Creates or replaces a mutable item.
remove(account) Removes a mutable item and reports whether it existed.
status(account) Checks an item’s state without returning its value.

The running Electron or Node host must have a valid Apple code signature. By default, the package uses the host’s bundle identifier as its Keychain service and lets macOS use the host’s private Keychain access group. No package identity configuration is required.

Option Purpose
keychainService Optional shared namespace for separately signed apps.
authentication Whether macOS should ask the user to authenticate.
iCloudSync Whether items should synchronize through iCloud Keychain.
accounts Immutable item names the store can access.
mutableAccounts Mutable item names the store can access, change, or remove.

This package stores generic-password items in macOS's Data Protection Keychain. Its native implementation uses the SecItem API with kSecUseDataProtectionKeychain: true, rather than the legacy file-based Keychain used by the older Keychain and SecKeychain APIs. Apple recommends the Data Protection Keychain for new work because it supports modern access groups, iCloud Keychain, and biometric access control. See Apple's keychain implementation guidance.

Aspect Legacy file-based Keychain Data Protection Keychain (this package)
API target Keychain and SecKeychain; SecItem when no Data Protection target is set SecItem with kSecUseDataProtectionKeychain: true
Access model Per-item access control lists (SecAccess) Code-signing entitlement access groups, optionally supplemented by SecAccessControl
iCloud Keychain Not supported Supported with iCloudSync: true
Biometric protection Not supported by its legacy access model Supported with authentication: { accessControl: "biometrics-only" }
Command-line inspection The security CLI can inspect keychain files The security CLI does not directly inspect these items
Keychain Access location Login, System, and other file-based keychains Local Items, or iCloud Keychain for synchronized items
Availability Can be used by processes outside a user-login context Requires a user-login context

Items created through a legacy file-based Keychain API are not automatically available here; migrate them explicitly if needed. The security CLI is likewise not an inspection path for this store's items. Use Keychain Access instead: items appear under Local Items when iCloudSync is false, or iCloud Keychain when it is true.

Set up a signed Electron development runtime

An unmodified Electron runtime identifies itself as Electron, so it is not a good namespace for your app’s development secrets. Instead, run Electron Vite with a cached Electron runtime signed as a separate development app, such as com.example.product.dev. With no keychainService, the same openKeychainStore() call then uses that bundle identifier automatically, keeping local values separate from production.

1. Create a development signing profile

In the Apple Developer portal, register com.example.product.dev and create a macOS development provisioning profile for it. Enable Keychain Sharing. The profile must allow this complete access group:

ABCDE12345.com.example.product.dev

Replace ABCDE12345 with your Apple Developer Team ID. Xcode can create the profile for you: make a temporary macOS app target with that bundle identifier, choose your Team, add the Keychain Sharing capability, and build it once.

2. Sign a copy of Electron

Keep this copy in a user cache outside node_modules; recreate it whenever the Electron version, development certificate, or provisioning profile changes. Create a main entitlement file containing your complete identifiers and Electron’s normal runtime entitlements:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.application-identifier</key>
  <string>ABCDE12345.com.example.product.dev</string>
  <key>com.apple.developer.team-identifier</key>
  <string>ABCDE12345</string>
  <key>keychain-access-groups</key>
  <array>
    <string>ABCDE12345.com.example.product.dev</string>
  </array>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>
</dict>
</plist>

Sign Electron’s helper apps first. They do not need your Keychain access group; this minimal helper entitlement file is enough for a standard Electron development runtime:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>
</dict>
</plist>

Save the two files as electron-development.entitlements.plist and electron-helper.entitlements.plist, then run:

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. Use that runtime for development

Set these before your Electron Vite launch (usually in the script that starts electron-vite dev):

export ELECTRON_OVERRIDE_DIST_PATH="$RUNTIME_DIR"
export ELECTRON_EXEC_PATH="$ELECTRON_APP/Contents/MacOS/Electron"

Keep keychainService omitted unless you intentionally share items between apps. A shared service also needs its matching Keychain access-group entitlement in the development runtime.

Who can access these items?

macOS gives access to every app signed with matching Keychain access-group entitlements. Keep your signing certificates, private keys, and entitlement configuration secure.

Authentication controls whether macOS asks the user to verify access. It does not decide which apps can access an item: the signed host identity and Keychain access group always do that.

Choose one authentication boundary. The package does not combine them, so one operation does not produce two prompts.

authentication: { accessControl: ... } stores the requirement with the item. It applies whenever an entitled app reads that item, even if that app does not use this package.

Value Result
user-presence Requires macOS device-owner authentication to read the item.
biometrics-only Requires Touch ID to read the item.

user-presence permits macOS device-owner authentication, such as Touch ID or the user’s password. biometrics-only fails on a Mac without enrolled Touch ID; it does not use an Apple Watch or a nearby iPhone.

authentication: { operationAuth: ... } asks the current app to authenticate before each package operation. It does not change the stored item, so another entitled app is not required to make the same prompt.

Value Result
user-presence Requires macOS device-owner authentication.
biometrics-only Requires Touch ID.

Use authentication: "none" when no extra user-verification prompt is required. It does not make items public; only apps that satisfy the configured signing and entitlement policy can access them.

Change item access control with a new account

An item’s accessControl policy is persistent. To change it, create a new account with the new policy and migrate your application data to it. For an encryption key, that normally means re-encrypting the application data with the new key. operationAuth is not stored with the item and can change independently.

Set iCloudSync: true to ask macOS to synchronize the store’s items through iCloud Keychain. Changing the setting never deletes an existing item.

get() remains read-only. If an item exists only with the opposite synchronization setting, it rejects with synchronization_migration_required. getOrCreate() adds a copy in the configured scope; it does not overwrite or remove the existing copy.

The package does not check whether the user is signed in to an Apple Account or has iCloud Keychain enabled. Creation can succeed locally even when macOS cannot currently synchronize the item; success means only that Keychain accepted it, not that another device received it. If the Security framework cannot create or access an item, the operation rejects with its Keychain error.

Use strings for UTF-8 text and Uint8Array for binary data. Choose the representation explicitly when calling get(). A request for "string" rejects with item_not_utf8 if the item does not contain valid UTF-8 text.

Share keys with another app

Set the same keychainService in every app that shares this store. The package derives the access group as the running app’s Team ID followed by this value.

const sharedStore = openKeychainStore({
  keychainService: "com.example.product.shared",
  authentication: "none",
  iCloudSync: false,
  accounts: ["installation-id"],
  mutableAccounts: ["desktop-token"],
});

Each app must include the resulting complete access group in its signing entitlements. With Electron Builder, add it to the macOS entitlements plist. With Electron Forge, pass that plist through packagerConfig.osxSign.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>keychain-access-groups</key>
  <array>
    <string>ABCDE12345.com.example.product.shared</string>
  </array>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <!-- other entitlements -->
</dict>
</plist>

This repository also provides the KeychainStore Swift Package Manager library for signed native macOS targets. It is distributed through Swift Package Manager, not the npm package. Add the KeychainStore library product from this repository:

.package(url: "https://github.com/biw/keychain-store.git", branch: "main")

Use a version requirement instead once the repository has a tagged release. The Swift library uses the same item format and declared-account policy as the Node package.

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() creates an item without returning its bytes, which is useful when native code owns the encryption workflow.

KeychainStoreSwiftSync offers the same declared-account methods, but uses only authentication: .none. Its operations can block the calling thread, so use the async store unless a synchronous boundary is required.

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

联系我们 contact @ memedata.com