Show HN: macOS data protection keychain for Electron apps

Aug 19, 2026 12:25 AM - 2 hours ago 1

CI npm version npm downloads

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

  • Protect items pinch code-signing entree groups; stock only pinch explicitly entitled apps (no information CLI access)
  • Restrict package entree to point names your app declares
  • Optionally require device-owner authentication (Touch ID aliases password), aliases 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 successful 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 exertion token"); // drawstring | 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 entree their union, while only mutable accounts whitethorn beryllium changed aliases removed. A sanction belongs in exactly 1 list, and either database whitethorn beryllium omitted.

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

The moving Electron aliases Node big must person a valid Apple codification signature. By default, the package uses the host’s bundle identifier arsenic its Keychain work and lets macOS usage the host’s private Keychain entree group. No package personality configuration is required.

Option Purpose
keychainService Optional shared namespace for separately signed apps.
authentication Whether macOS should inquire the personification to authenticate.
iCloudSync Whether items should synchronize done iCloud Keychain.
accounts Immutable point names the shop tin access.
mutableAccounts Mutable point names the shop tin access, change, aliases remove.

This package stores generic-password items successful macOS's Data Protection Keychain. Its native implementation uses the SecItem API pinch kSecUseDataProtectionKeychain: true, alternatively than the legacy file-based Keychain utilized by the older Keychain and SecKeychain APIs. Apple recommends the Data Protection Keychain for caller activity because it supports modern entree groups, iCloud Keychain, and biometric entree control. See Apple's keychain implementation guidance.

Aspect Legacy file-based Keychain Data Protection Keychain (this package)
API target Keychain and SecKeychain; SecItem erstwhile nary Data Protection target is set SecItem pinch kSecUseDataProtectionKeychain: true
Access model Per-item entree power lists (SecAccess) Code-signing entitlement entree groups, optionally supplemented by SecAccessControl
iCloud Keychain Not supported Supported pinch iCloudSync: true
Biometric protection Not supported by its bequest entree model Supported pinch authentication: { accessControl: "biometrics-only" }
Command-line inspection The information CLI tin inspect keychain files The information CLI does not straight inspect these items
Keychain Access location Login, System, and different file-based keychains Local Items, aliases iCloud Keychain for synchronized items
Availability Can beryllium utilized by processes extracurricular a user-login context Requires a user-login context

Items created done a bequest file-based Keychain API are not automatically disposable here; migrate them explicitly if needed. The information CLI is likewise not an inspection way for this store's items. Use Keychain Access instead: items look nether Local Items erstwhile iCloudSync is false, or iCloud Keychain erstwhile it is true.

Set up a signed Electron improvement runtime

An unmodified Electron runtime identifies itself arsenic Electron, truthful it is not a bully namespace for your app’s improvement secrets. Instead, tally Electron Vite pinch a cached Electron runtime signed as a abstracted improvement app, specified arsenic com.example.product.dev. With nary keychainService, the same openKeychainStore() telephone past uses that bundle identifier automatically, keeping section values separate from production.

1. Create a improvement signing profile

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

ABCDE12345.com.example.product.dev

Replace ABCDE12345 pinch your Apple Developer Team ID. Xcode tin create the floor plan for you: make a impermanent macOS app target pinch that bundle identifier, take your Team, adhd the Keychain Sharing capability, and build it once.

2. Sign a transcript of Electron

Keep this transcript successful a personification cache extracurricular node_modules; recreate it whenever the Electron version, development certificate, aliases provisioning floor plan changes. Create a main entitlement record 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 request your Keychain entree group; this minimal helper entitlement record is capable for a modular Electron improvement 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 2 files arsenic electron-development.entitlements.plist and electron-helper.entitlements.plist, past 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 earlier your Electron Vite motorboat (usually successful the book 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 stock items betwixt apps. A shared service also needs its matching Keychain access-group entitlement successful the improvement runtime.

Who tin entree these items?

macOS gives entree to each app signed pinch matching Keychain access-group entitlements. Keep your signing certificates, backstage keys, and entitlement configuration secure.

Authentication controls whether macOS asks the personification to verify access. It does not determine which apps can entree an item: the signed big personality and Keychain entree group ever do that.

Choose 1 authentication boundary. The package does not harvester them, truthful 1 cognition does not produce 2 prompts.

authentication: { accessControl: ... } stores the request pinch the item. It applies whenever an entitled app sounds that item, moreover if that app does not usage this package.

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

user-presence permits macOS device-owner authentication, specified arsenic Touch ID aliases the user’s password. biometrics-only fails connected a Mac without enrolled Touch ID; it does not usage an Apple Watch aliases a nearby iPhone.

authentication: { operationAuth: ... } asks the existent app to authenticate earlier each package operation. It does not alteration the stored item, truthful different 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" erstwhile nary other user-verification punctual is required. It does not make items public; only apps that fulfill the configured signing and entitlement argumentation tin entree them.

Change point entree power pinch a caller account

An item’s accessControl argumentation is persistent. To alteration it, create a caller relationship pinch the new policy and migrate your exertion information to it. For an encryption key, that usually means re-encrypting the exertion information pinch the caller key. operationAuth is not stored pinch the point and can alteration independently.

Set iCloudSync: existent to inquire macOS to synchronize the store’s items done iCloud Keychain. Changing the mounting ne'er deletes an existing item.

get() remains read-only. If an point exists only pinch the other synchronization setting, it rejects pinch synchronization_migration_required. getOrCreate() adds a transcript successful the configured scope; it does not overwrite aliases region the existing copy.

The package does not cheque whether the personification is signed successful to an Apple Account aliases has iCloud Keychain enabled. Creation tin win locally moreover erstwhile macOS cannot presently synchronize the item; success means only that Keychain accepted it, not that different instrumentality received it. If the Security framework cannot create aliases entree an item, the cognition rejects pinch its Keychain error.

Use strings for UTF-8 matter and Uint8Array for binary data. Choose the practice explicitly when calling get(). A petition for "string" rejects pinch item_not_utf8 if the point does not contain valid UTF-8 text.

Share keys pinch different app

Set the aforesaid keychainService successful each app that shares this store. The package derives the access group arsenic the moving 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 see the resulting complete entree group successful its signing entitlements. With Electron Builder, adhd it to the macOS entitlements plist. With Electron Forge, walk that plist done packagerConfig.osxSign.

</dict> </plist>'><?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/> <!-- different entitlements --> </dict> </plist>

This repository besides provides the KeychainStore Swift Package Manager room for signed native macOS targets. It is distributed done Swift Package Manager, not the npm package. Add the KeychainStore room merchandise from this repository:

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

Use a type request alternatively erstwhile the repository has a tagged release. The Swift room uses the aforesaid point format and declared-account argumentation arsenic 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 point without returning its bytes, which is useful erstwhile autochthonal codification owns the encryption workflow.

KeychainStoreSwiftSync offers the aforesaid declared-account methods, but uses only authentication: .none. Its operations tin artifact the calling thread, truthful usage the async shop unless a synchronous bound 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

More