Show HN: Electron 앱용 macOS 데이터 보호 키체인
요약
이 글은 Electron 및 Node 앱을 위한 안전한 저장소 라이브러리 'keychain-store'를 소개합니다. 이 라이브러리는 최신 macOS 데이터 보호 키체인 기능을 활용하여, 코드 서명 액세스 그룹 기반의 강력한 보안 환경에서 데이터를 저장할 수 있게 합니다. 앱이 선언된 항목 이름으로 접근을 제한하며, Touch ID 또는 암호 인증 옵션을 제공하여 높은 수준의 보안성을 보장합니다.
핵심 포인트
- macOS 데이터 보호 키체인을 활용한 안전한 앱 전용 저장소 구현
- 코드 서명 액세스 그룹 기반으로 권한이 명시된 앱만 접근 가능
- Touch ID/암호 등 생체 인식 및 인증 옵션 제공
- Electron 및 Node 환경에서 사용 가능한 라이브러리 형태
keychain-store
Electron 및 Node 앱용 안전한 저장소로, 최신 macOS 데이터 보호 키체인(Data Protection Keychain)을 기반으로 합니다.
- 코드 서명 액세스 그룹을 사용하여 항목 보호; 명시적으로 권한을 부여받은 앱과만 공유 가능 (
securityCLI 접근 불가) - 패키지 액세스를 앱이 선언하는 항목 이름으로 제한
- 선택적으로 기기 소유자 인증(Touch ID 또는 암호) 또는 Touch ID 전용 요구
- UTF-8 문자열 및 바이너리 값 저장
설치 (Install)
pnpm add keychain-store
빠른 시작 (Quick start)
import { openKeychainStore } from "keychain-store";
const store = openKeychainStore({
...
accounts는 불변(immutable) 키체인 항목을 선언하며, mutableAccounts는 가변(mutable) 항목을 선언합니다. 스토어는 이 둘의 합집합에 접근할 수 있지만, 변경하거나 제거할 수 있는 것은 가변 계정만 가능합니다. 이름은 정확히 하나의 목록에 속해야 하며, 두 목록 중 어느 쪽도 생략될 수 있습니다.
API
| Method | 기능 |
|---|---|
get(account, "Uint8Array") | 저장된 바이너리 데이터를 반환하거나, null을 반환합니다. |
| ... |
설정 (Setup)
실행되는 Electron 또는 Node 호스트는 유효한 Apple 코드 서명을 가지고 있어야 합니다. 기본적으로 이 패키지는 호스트의 번들 식별자(bundle identifier)를 키체인 서비스로 사용하고, macOS가 호스트의 개인 키체인 액세스 그룹을 사용하도록 합니다. 별도의 패키지 ID 구성은 필요하지 않습니다.
| Option | 목적 |
|---|---|
keychainService | 개별적으로 서명된 앱들을 위한 선택적 공유 네임스페이스입니다. |
| ... |
데이터 보호 키체인 (Data Protection Keychain)
이 패키지는 macOS의 **데이터 보호 키체인(Data Protection Keychain)**에 일반 비밀번호 항목을 저장합니다. 네이티브 구현은 이전 버전의 파일 기반 키체인과 Keychain 및 SecKeychain API를 사용하는 대신, kSecUseDataProtectionKeychain: true를 사용한 SecItem API를 활용합니다. Apple은 현대적인 접근 그룹(access groups), iCloud 키체인, 생체 인식 접근 제어를 지원하기 때문에 새로운 작업에는 데이터 보호 키체인을 권장합니다. Apple의 키체인 구현 가이드를 참조하십시오.
| 측면 (Aspect) | 레거시 파일 기반 키체인 (Legacy file-based Keychain) | 데이터 보호 키체인 (Data Protection Keychain, 본 패키지) |
|---|---|---|
| API 대상 (API target) | Keychain 및 SecKeychain; Data Protection 대상을 설정하지 않은 경우의 SecItem | kSecUseDataProtectionKeychain: true를 사용한 SecItem |
| ... | ||
Items created through a legacy file-based Keychain API는 여기에 자동으로 사용할 수 없습니다. 필요한 경우 명시적으로 마이그레이션해야 합니다. 마찬가지로, security CLI도 이 스토어의 항목을 검사하는 경로는 아닙니다. 대신 키체인 접근(Keychain Access)을 사용하십시오: iCloudSync가 false일 때는 로컬 항목(Local Items) 아래에, true일 때는 iCloud 키체인(iCloud Keychain) 아래에 항목이 나타납니다. |
로컬 개발 (Local development)
<details> <summary>서명된 Electron 개발 런타임 설정하기</summary>수정되지 않은 Electron 런타임은 자신을 Electron으로 식별하므로, 앱의 개발 비밀에 적합한 네임스페이스가 아닙니다. 대신, 캐시된 Electron 런타임을 별도의 개발 앱(예: com.example.product.dev)으로 서명하여 Electron Vite를 실행하십시오. keychainService가 없으면, 동일한 openKeychainStore() 호출이 해당 번들 식별자를 자동으로 사용하여 로컬 값을 프로덕션 값과 분리하게 됩니다.
1. 개발 서명 프로파일 생성
Apple Developer 포털에서 com.example.product.dev를 등록하고 이를 위한 macOS 개발 Provisioning Profile을 생성합니다. Keychain Sharing 기능을 활성화해야 합니다. 이 프로파일은 다음 전체 접근 그룹을 허용해야 합니다:
ABCDE12345.com.example.product.dev
여기서 ABCDE12345를 사용자의 Apple Developer Team ID로 대체합니다. Xcode를 사용하여 프로파일을 생성할 수 있습니다: 임시 macOS 앱 타겟을 해당 번들 식별자로 만들고, 팀(Team)을 선택한 다음, Keychain Sharing 기능을 추가하고 한 번 빌드하면 됩니다.
2. Electron 복사본 서명하기
이 복사본은 node_modules 외부의 사용자 캐시 폴더에 보관해야 하며, Electron 버전, 개발 인증서 또는 Provisioning Profile이 변경될 때마다 다시 생성해야 합니다. 전체 식별자(identifiers)와 Electron의 일반 런타임 권한(runtime entitlements)을 포함하는 메인 Entitlement 파일을 만듭니다:
<?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">
...
먼저 Electron의 헬퍼 앱(helper apps)을 서명합니다. 이들은 사용자의 Keychain 접근 그룹이 필요하지 않으며, 표준 Electron 개발 런타임에는 이 최소한의 헬퍼 Entitlement 파일만으로 충분합니다:
<?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">
...
두 파일을 각각 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"
...
3. 개발에 해당 런타임 사용하기
이 설정들을 Electron Vite 실행 명령어(보통 electron-vite dev를 시작하는 스크립트 내부) 전에 설정합니다:
export ELECTRON_OVERRIDE_DIST_PATH="$RUNTIME_DIR"
export ELECTRON_EXEC_PATH="$ELECTRON_APP/Contents/MacOS/Electron"
앱 간에 항목을 의도적으로 공유하지 않는 한 keychainService는 생략하세요. 공유 서비스의 경우 개발 런타임에서 해당 매칭되는 Keychain access-group entitlement이 필요합니다.
누가 이 항목들에 접근할 수 있나요?
macOS는 매칭되는 Keychain access-group entitlement으로 서명된 모든 앱에 접근을 허용합니다. 서명 인증서, 개인 키 및 entitlement 구성을 안전하게 보관하세요.
인증 (Authentication)
인증은 macOS가 사용자에게 접근을 확인하도록 요청할지 여부를 제어합니다. 이는 어떤 앱이 항목에 접근할 수 있는지 결정하지 않으며, 서명된 호스트 ID와 Keychain access group이 항상 그 역할을 합니다.
하나의 인증 경계를 선택하세요. 패키지는 이들을 결합하지 않으므로, 하나의 작업으로 두 개의 프롬프트를 생성하지 않습니다.
항목 접근 제어 (Item access control)
authentication: { accessControl: ... }는 항목에 요구 사항을 저장합니다. 이는 해당 앱이 이 패키지를 사용하지 않더라도 권한이 있는(entitled) 모든 앱이 그 항목을 읽을 때 적용됩니다.
| 값 | 결과 |
|---|---|
user-presence | 항목을 읽으려면 macOS 장치 소유자 인증이 필요합니다. |
biometrics-only | 항목을 읽으려면 Touch ID가 필요합니다. |
user-presence는 Touch ID 또는 사용자의 비밀번호와 같은 macOS 장치 소유자 인증을 허용합니다. biometrics-only는 등록된 Touch ID가 없는 Mac에서는 실패하며, Apple Watch나 근처의 iPhone은 사용하지 않습니다.
작업 인증 (Operation authentication)
authentication: { operationAuth: ... }는 각 패키지 작업 전에 현재 앱에게 인증을 요청합니다. 이는 저장된 항목을 변경하지 않으므로, 다른 권한이 있는 앱도 동일한 프롬프트를 표시할 필요가 없습니다.
| Value | Result |
|---|---|
user-presence | macOS 장치 소유자 인증이 필요합니다. |
biometrics-only | Touch ID가 필요합니다. |
추가 사용자 인증 프롬프트가 필요하지 않은 경우 authentication: "none"을 사용하십시오. 이는 항목을 공개적으로 만드는 것이 아니며, 설정된 서명 및 권한 정책을 만족하는 앱만이 접근할 수 있습니다.
새로운 계정을 사용하여 항목 액세스 제어 변경하기
항목의 accessControl 정책은 영구적입니다. 이를 변경하려면 새 정책을 가진 새 계정을 만들고 애플리케이션 데이터를 해당 계정으로 마이그레이션해야 합니다. 암호화 키의 경우, 일반적으로 새로운 키로 애플리케이션 데이터를 재암호화하는 것을 의미합니다. operationAuth는 항목과 함께 저장되지 않으며 독립적으로 변경될 수 있습니다.
iCloud 동기화
iCloudSync: true로 설정하여 macOS가 iCloud Keychain을 통해 스토어의 항목들을 동기화하도록 요청하십시오. 설정을 변경해도 기존 항목이 삭제되지는 않습니다.
get()은 읽기 전용으로 유지됩니다. 만약 항목이 반대 동기화 설정만 가지고 존재할 경우, synchronization_migration_required 오류와 함께 거부됩니다. getOrCreate()는 구성된 범위에 복사본을 추가하며, 기존 복사본을 덮어쓰거나 제거하지 않습니다.
이 패키지는 사용자가 Apple Account에 로그인했는지 또는 iCloud Keychain이 활성화되었는지를 확인하지 않습니다. macOS가 현재 항목을 동기화할 수 없는 경우에도 로컬에서 생성이 성공할 수 있습니다. 이는 키체인이 이를 수락했다는 의미일 뿐, 다른 장치가 받았다는 것을 의미하지는 않습니다. Security framework가 항목을 생성하거나 액세스할 수 없는 경우, 해당 작업은 Keychain 오류와 함께 거부됩니다.
값 표현 방식
UTF-8 텍스트에는 문자열(strings)을, 이진 데이터에는 Uint8Array를 사용하십시오. get()을 호출할 때 명시적으로 표현 방식을 선택해야 합니다. 항목에 유효한 UTF-8 텍스트가 포함되어 있지 않은 경우 "string" 요청은 item_not_utf8 오류와 함께 거부됩니다.
다른 앱과 키 공유하기
이 스토어를 공유하는 모든 앱에서 동일한 keychainService를 설정해야 합니다. 이 패키지는 실행 중인 앱의 Team ID 뒤에 이 값을 붙여 접근 그룹을 파생합니다.
const sharedStore = openKeychainStore({
keychainService: "com.example.product.shared",
authentication: "none",
...
각 앱은 결과로 나오는 전체 접근 그룹을 서명 엔타이틀먼트(signing entitlements)에 포함해야 합니다. Electron Builder를 사용한다면, 이를 macOS 엔타이틀먼트 plist 파일에 추가하세요. Electron Forge를 사용한다면, 해당 plist 파일을 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">
...
Swift 사용법
이 저장소는 또한 서명된 네이티브 macOS 타겟을 위한 KeychainStore Swift Package Manager 라이브러리를 제공합니다. 이는 npm 패키지가 아닌 Swift Package Manager를 통해 배포됩니다. 이 저장소에서 KeychainStore 라이브러리 제품을 추가하세요:
.package(url: "https://github.com/biw/keychain-store.git", branch: "main")
이 저장소가 태그된 릴리스를 갖게 되면 버전 요구 사항을 사용하는 것이 좋습니다. Swift 라이브러리는 Node 패키지와 동일한 항목 형식(item format) 및 선언 계정 정책(declared-account policy)을 사용합니다.
import KeychainStore
let store = try KeychainStoreSwift(...)
ensure()는 바이트를 반환하지 않고 항목을 생성하므로, 네이티브 코드가 암호화 워크플로우(encryption workflow)를 소유할 때 유용합니다.
동기식 Swift API
KeychainStoreSwiftSync는 동일한 선언 계정 메서드를 제공하지만 authentication: .none만 사용합니다. 이 작업은 호출 스레드를 차단할 수 있으므로, 동기 경계(synchronous boundary)가 필요하지 않은 경우 비동기 스토어를 사용하는 것이 좋습니다.
import KeychainStore
let store = try KeychainStoreSwiftSync(...)
라이선스
MIT
AI 자동 생성 콘텐츠
본 콘텐츠는 HN Claude Code Search의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기