Agent skill
os-keychain
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/os-keychain
SKILL.md
OS Keychain Skill
name: os-keychain version: 1.1.0 domain: security/credential-storage risk_level: HIGH languages: [python, typescript, rust, go] frameworks: [keyring, security-framework, libsecret] requires_security_review: true compliance: [GDPR, HIPAA, PCI-DSS, SOC2] last_updated: 2025-01-15
MANDATORY READING PROTOCOL: Before implementing credential storage, read
references/advanced-patterns.mdfor cross-platform patterns andreferences/security-examples.mdfor platform-specific implementations.
1. Overview
1.1 Purpose and Scope
This skill provides secure credential storage using OS-native keychain services:
- Windows: Credential Manager (DPAPI-backed)
- macOS: Keychain Services (Secure Enclave integration)
- Linux: Secret Service API (GNOME Keyring, KWallet)
1.2 Risk Assessment
Risk Level: HIGH
Justification:
- Master keys and sensitive credentials stored
- Compromise exposes all dependent systems
- Platform API misuse leads to insecure storage
- Privilege escalation can access all credentials
Attack Surface:
- Inter-process communication (D-Bus, XPC)
- Access control misconfigurations
- Memory disclosure attacks
- Privilege escalation to access keychain
2. Core Principles
- TDD First - Write tests before implementing credential operations
- Performance Aware - Cache credentials, batch operations, minimize keychain calls
- Platform-native storage - Use OS keychain services for all credentials
- Access isolation - Unique service names prevent cross-contamination
- Secure by default - Reject insecure backends automatically
- Cross-platform support - Unified API across Windows, macOS, Linux
2.1 Security Principles
- NEVER store secrets in environment variables or files
- NEVER log credential values or access patterns with identifiers
- ALWAYS use platform-native keychain services
- ALWAYS validate application identity before credential access
- ALWAYS use unique service names per credential type
3. Implementation Workflow (TDD)
Step 1: Write Failing Test First
import pytest
from unittest.mock import MagicMock, patch
class TestCredentialStoreOperations:
"""TDD tests for credential store - write these FIRST."""
def test_store_credential_success(self):
"""Test storing a credential in keychain."""
# Arrange
store = SecureCredentialStore("test-service")
# Act
store.store("api-key", "sk-test-12345")
# Assert
assert store.exists("api-key") is True
assert store.retrieve("api-key") == "sk-test-12345"
def test_retrieve_nonexistent_raises_keyerror(self):
"""Test retrieving nonexistent credential raises KeyError."""
store = SecureCredentialStore("test-service")
with pytest.raises(KeyError, match="Credential not found"):
store.retrieve("nonexistent-key")
def test_delete_removes_credential(self):
"""Test deletion completely removes credential."""
store = SecureCredentialStore("test-service")
store.store("temp-key", "temp-value")
store.delete("temp-key")
assert store.exists("temp-key") is False
def test_credential_isolation_between_namespaces(self):
"""Test credentials are isolated by namespace."""
store1 = SecureCredentialStore("namespace-a")
store2 = SecureCredentialStore("namespace-b")
store1.store("shared-key", "value-a")
store2.store("shared-key", "value-b")
assert store1.retrieve("shared-key") == "value-a"
assert store2.retrieve("shared-key") == "value-b"
def test_rejects_insecure_backend(self):
"""Test rejection of insecure keyring backends."""
import keyring
from keyring.backends import null
original = keyring.get_keyring()
try:
keyring.set_keyring(null.Keyring())
with pytest.raises(RuntimeError, match="Insecure"):
SecureCredentialStore("test")
finally:
keyring.set_keyring(original)
Step 2: Implement Minimum to Pass
import keyring
from keyring.errors import KeyringError
import logging
logger = logging.getLogger(__name__)
class SecureCredentialStore:
"""Minimal implementation to pass tests."""
SERVICE_PREFIX = "com.jarvis.assistant"
def __init__(self, namespace: str):
self._service = f"{self.SERVICE_PREFIX}.{namespace}"
self._verify_backend()
def _verify_backend(self):
backend = keyring.get_keyring()
backend_name = type(backend).__name__
insecure = ['PlaintextKeyring', 'NullKeyring', 'ChainerBackend']
if backend_name in insecure:
raise RuntimeError(f"Insecure keyring backend: {backend_name}")
def store(self, key: str, secret: str) -> None:
keyring.set_password(self._service, key, secret)
def retrieve(self, key: str) -> str:
secret = keyring.get_password(self._service, key)
if secret is None:
raise KeyError(f"Credential not found: {key}")
return secret
def delete(self, key: str) -> None:
keyring.delete_password(self._service, key)
def exists(self, key: str) -> bool:
return keyring.get_password(self._service, key) is not None
Step 3: Refactor with Performance Patterns
After tests pass, add caching and logging (see Performance Patterns section).
Step 4: Run Full Verification
# Run all tests with coverage
pytest tests/security/test_keychain.py -v --cov=src/security/keychain
# Run security-specific tests
pytest tests/security/ -k "keychain or credential" -v
# Verify no credential leaks in logs
grep -r "sk-\|password\|secret" logs/ && echo "FAIL: Credentials in logs"
4. Performance Patterns
4.1 Credential Caching
# BAD: Repeated keychain access
class SlowCredentialStore:
def get_api_key(self):
return keyring.get_password(self._service, "api-key") # Slow IPC every call
# GOOD: In-memory cache with TTL
from functools import lru_cache
from threading import Lock
import time
class CachedCredentialStore:
def __init__(self, namespace: str, cache_ttl: int = 300):
self._service = f"com.jarvis.{namespace}"
self._cache: dict[str, tuple[str, float]] = {}
self._lock = Lock()
self._ttl = cache_ttl
def retrieve(self, key: str) -> str:
with self._lock:
if key in self._cache:
value, timestamp = self._cache[key]
if time.time() - timestamp < self._ttl:
return value
secret = keyring.get_password(self._service, key)
if secret is None:
raise KeyError(f"Credential not found: {key}")
self._cache[key] = (secret, time.time())
return secret
def invalidate(self, key: str = None):
with self._lock:
if key:
self._cache.pop(key, None)
else:
self._cache.clear()
4.2 Batch Operations
# BAD: Individual keychain calls
def load_all_credentials():
db_pass = keyring.get_password("jarvis", "db-password")
api_key = keyring.get_password("jarvis", "api-key")
secret = keyring.get_password("jarvis", "encryption-key")
return db_pass, api_key, secret # 3 separate IPC calls
# GOOD: Batch loading with single initialization
class BatchCredentialLoader:
def __init__(self, namespace: str, keys: list[str]):
self._service = f"com.jarvis.{namespace}"
self._credentials = self._load_batch(keys)
def _load_batch(self, keys: list[str]) -> dict[str, str]:
"""Load multiple credentials in optimized batch."""
result = {}
for key in keys:
value = keyring.get_password(self._service, key)
if value:
result[key] = value
return result
def get(self, key: str) -> str:
if key not in self._credentials:
raise KeyError(f"Credential not loaded: {key}")
return self._credentials[key]
# Usage - single initialization at startup
loader = BatchCredentialLoader("secrets", ["db-password", "api-key", "encryption-key"])
4.3 Lazy Loading
# BAD: Load all credentials at import
class EagerStore:
def __init__(self):
self.db_password = keyring.get_password("jarvis", "db") # Loaded immediately
self.api_key = keyring.get_password("jarvis", "api")
# GOOD: Load only when accessed
class LazyCredentialStore:
def __init__(self, namespace: str):
self._service = f"com.jarvis.{namespace}"
self._cache: dict[str, str] = {}
def __getattr__(self, name: str) -> str:
if name.startswith('_'):
raise AttributeError(name)
if name not in self._cache:
value = keyring.get_password(self._service, name.replace('_', '-'))
if value is None:
raise KeyError(f"Credential not found: {name}")
self._cache[name] = value
return self._cache[name]
# Usage - credentials loaded on first access
store = LazyCredentialStore("api-keys")
# No keychain calls yet
key = store.openai_key # First access triggers load
4.4 Connection Reuse
# BAD: Create new backend each time
def get_credential(key: str) -> str:
store = SecureCredentialStore("service") # Backend verification each call
return store.retrieve(key)
# GOOD: Singleton pattern for store instances
class CredentialStoreFactory:
_instances: dict[str, 'SecureCredentialStore'] = {}
_lock = Lock()
@classmethod
def get_store(cls, namespace: str) -> 'SecureCredentialStore':
with cls._lock:
if namespace not in cls._instances:
cls._instances[namespace] = SecureCredentialStore(namespace)
return cls._instances[namespace]
# Usage - reuses existing store instance
store = CredentialStoreFactory.get_store("api-keys")
4.5 Memory-Safe Handling
# BAD: Credentials persist in memory
class UnsafeStore:
def get_credential(self, key: str) -> str:
secret = keyring.get_password(self._service, key)
self.last_retrieved = secret # Persists in memory
return secret
# GOOD: Secure memory handling with cleanup
import ctypes
import gc
class SecureMemoryStore:
def retrieve_and_use(self, key: str, callback) -> None:
"""Retrieve credential, use it, then clear from memory."""
secret = keyring.get_password(self._service, key)
if secret is None:
raise KeyError(f"Credential not found: {key}")
try:
callback(secret)
finally:
# Overwrite string in memory (best effort in Python)
if secret:
secret_bytes = secret.encode()
ctypes.memset(id(secret_bytes) + 32, 0, len(secret_bytes))
del secret
gc.collect()
def with_credential(self, key: str):
"""Context manager for secure credential access."""
class CredentialContext:
def __init__(ctx_self, store, key):
ctx_self._store = store
ctx_self._key = key
ctx_self._value = None
def __enter__(ctx_self):
ctx_self._value = keyring.get_password(
ctx_self._store._service, ctx_self._key
)
return ctx_self._value
def __exit__(ctx_self, *args):
if ctx_self._value:
del ctx_self._value
gc.collect()
return CredentialContext(self, key)
# Usage
store = SecureMemoryStore("secrets")
with store.with_credential("api-key") as api_key:
make_api_call(api_key)
# Credential cleared after context exits
5. Core Responsibilities
5.1 Primary Functions
- Store secrets securely using OS-native encryption
- Retrieve secrets with proper access control verification
- Manage credential lifecycle including rotation and deletion
- Abstract platform differences for cross-platform code
- Integrate with encryption skill for master key storage
6. Technology Stack
6.1 Recommended Libraries
| Platform | Library | API | Notes |
|---|---|---|---|
| Python (cross-platform) | keyring |
Unified | Auto-detects backend |
| macOS | Security.framework |
Keychain Services | Native Swift/ObjC |
| Windows | Windows.Security.Credentials |
Credential Manager | WinRT API |
| Linux | libsecret |
Secret Service D-Bus | GNOME Keyring backend |
6.2 Platform Requirements
- macOS: 10.15+ (Keychain Access improvements)
- Windows: 10 1903+ (Credential Guard support)
- Linux: libsecret 0.20+, GNOME Keyring 3.36+
7. Implementation Patterns
7.1 Cross-Platform Python Implementation
import keyring
from keyring.errors import KeyringError
import logging
logger = logging.getLogger(__name__)
class SecureCredentialStore:
"""Cross-platform credential storage using OS keychain."""
SERVICE_PREFIX = "com.jarvis.assistant"
def __init__(self, namespace: str):
self._service = f"{self.SERVICE_PREFIX}.{namespace}"
self._verify_backend()
def _verify_backend(self):
"""Verify secure keyring backend is available."""
backend = keyring.get_keyring()
backend_name = type(backend).__name__
insecure_backends = ['PlaintextKeyring', 'NullKeyring', 'ChainerBackend']
if backend_name in insecure_backends:
raise RuntimeError(f"Insecure keyring backend: {backend_name}")
logger.info("keychain.backend.initialized", extra={'backend': backend_name})
def store(self, key: str, secret: str) -> None:
"""Store a credential securely."""
keyring.set_password(self._service, key, secret)
logger.info("keychain.credential.stored", extra={'key': key})
def retrieve(self, key: str) -> str:
"""Retrieve a credential. Raises KeyError if not found."""
secret = keyring.get_password(self._service, key)
if secret is None:
raise KeyError(f"Credential not found: {key}")
return secret
def delete(self, key: str) -> None:
"""Delete a credential."""
keyring.delete_password(self._service, key)
logger.info("keychain.credential.deleted", extra={'key': key})
def exists(self, key: str) -> bool:
"""Check if credential exists."""
return keyring.get_password(self._service, key) is not None
7.2 Platform-Specific Implementations
For detailed platform-specific implementations with advanced features:
- macOS Keychain (ACLs, Touch ID, Secure Enclave): See
references/security-examples.md#macos-keychain - Windows Credential Manager (DPAPI, Credential Guard): See
references/security-examples.md#windows-credential-manager - Linux Secret Service (D-Bus, GNOME Keyring): See
references/security-examples.md#linux-secret-service
8. Security Standards
8.1 Known Vulnerabilities
| CVE | Severity | Platform | Mitigation |
|---|---|---|---|
| CVE-2023-21726 | High (7.8) | Windows | Windows Update Jan 2023 |
| CVE-2024-54490 | High | macOS | Update to macOS 15.2+ |
| CVE-2024-44162 | High | macOS | Update to macOS 14.7+ |
| CVE-2024-44243 | High | macOS | Update to macOS 15.2+ |
| CVE-2024-1086 | High (7.8) | Linux | Kernel 6.6.15+ |
8.2 OWASP Mapping
| OWASP 2025 | Implementation |
|---|---|
| A01: Broken Access Control | OS-level ACLs, app sandboxing |
| A02: Cryptographic Failures | Platform-native encryption |
| A04: Insecure Design | Defense in depth, least privilege |
| A07: Identification Failures | Credential isolation per service |
8.3 Platform Security Features
macOS: Secure Enclave, per-item ACLs, code signing, Touch ID gating
Windows: DPAPI encryption, Credential Guard, virtualization-based security
Linux: D-Bus access control, collection locking, session keyring isolation
For detailed threat analysis, see references/threat-model.md.
9. Common Mistakes
9.1 Critical Anti-Patterns
Environment Variables for Secrets
# NEVER: Visible in /proc, logs
api_key = os.environ.get('API_KEY')
# ALWAYS: OS keychain
api_key = SecureCredentialStore("api").retrieve("api-key")
Hardcoded Credentials
# NEVER: In source code
DATABASE_PASSWORD = "production-password-123"
# ALWAYS: Retrieved at runtime
password = SecureCredentialStore("database").retrieve("password")
Insecure File Storage
# NEVER: Plain files
with open('~/.config/app/credentials.json') as f:
creds = json.load(f)
# ALWAYS: Platform keychain
token = SecureCredentialStore("app").retrieve("access-token")
Logging Credentials
# NEVER: Log values
logger.info(f"Retrieved API key: {api_key}")
# ALWAYS: Log metadata only
logger.info("credential.retrieved", extra={'service': service, 'key': key})
Single Service Name
# NEVER: All credentials under one service
store = SecureCredentialStore("jarvis")
# ALWAYS: Namespace by credential type
db_store = SecureCredentialStore("database")
api_store = SecureCredentialStore("api-keys")
10. Pre-Implementation Checklist
Phase 1: Before Writing Code
- Read
references/advanced-patterns.mdfor cross-platform patterns - Read
references/security-examples.mdfor platform implementations - Review threat model in
references/threat-model.md - Identify required credential namespaces
- Design test cases for credential operations
- Plan caching strategy for performance
Phase 2: During Implementation
- Write failing tests first (TDD workflow)
- Implement minimum code to pass tests
- Add credential caching with TTL
- Implement batch loading for multiple credentials
- Use lazy loading for optional credentials
- Add memory-safe handling for sensitive operations
- Verify secure keyring backend at startup
- Log operations without credential values
Phase 3: Before Committing
- All tests pass with
pytest -v - No credentials in test fixtures or logs
- Cross-platform tests verified
- Memory leak tests pass
- Security scan shows no credential leaks
- Code review for anti-patterns complete
Platform-Specific Verification
- macOS: Code signing verified for Keychain access
- Windows: Credential Guard compatibility tested
- Linux: Secret Service daemon running, D-Bus accessible
- OS security updates applied (check CVE list above)
11. Summary
Key Objectives
- TDD workflow: Write tests before implementing credential operations
- Performance optimization: Cache credentials, batch operations, lazy loading
- Platform-native storage: Use OS keychain services for all credentials
- Access isolation: Unique service names prevent cross-contamination
- Secure by default: Reject insecure backends automatically
Security Reminders
- Credentials in environment variables are NOT secure
- File-based credential storage is NOT secure
- Always verify keyring backend at application startup
- Log credential operations but NEVER values
- Keep OS updated to address keychain vulnerabilities
References
references/advanced-patterns.md- Cross-platform patterns, migration, testingreferences/security-examples.md- Complete platform implementationsreferences/threat-model.md- Attack scenarios and mitigations
The OS keychain is your first line of defense. Misuse negates all downstream encryption.
Didn't find tool you were looking for?