OS 密钥链技能
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
强制阅读协议:在实现凭证存储之前,请阅读
references/advanced-patterns.md了解跨平台模式,以及references/security-examples.md了解平台特定实现。
1. 概述
1.1 目的和范围
这个技能提供使用操作系统原生密钥链服务的安全凭证存储:
- Windows:凭据管理器(DPAPI 支持)
- macOS:密钥链服务(安全芯片集成)
- Linux:秘密服务 API(GNOME 钥匙环,KWallet)
1.2 风险评估
风险等级:高
理由:
- 存储主密钥和敏感凭证
- 泄露会暴露所有依赖系统
- 平台 API 误用导致不安全存储
- 权限提升可以访问所有凭证
攻击面:
- 进程间通信(D-Bus,XPC)
- 访问控制配置错误
- 内存泄露攻击
- 权限提升以访问密钥链
2. 核心原则
- 测试驱动开发优先 - 在实现凭证操作前先写测试
- 性能感知 - 缓存凭证,批处理操作,最小化密钥链调用
- 平台原生存储 - 对所有凭证使用操作系统密钥链服务
- 访问隔离 - 唯一服务名防止交叉污染
- 默认安全 - 自动拒绝不安全后端
- 跨平台支持 - Windows、macOS、Linux 的统一 API
2.1 安全原则
- 绝不在环境变量或文件中存储秘密
- 绝不记录凭证值或带有标识符的访问模式
- 始终使用平台原生密钥链服务
- 始终在凭证访问前验证应用身份
- 始终为每种凭证类型使用唯一服务名
3. 实现工作流(测试驱动开发)
步骤 1:先写失败测试
import pytest
from unittest.mock import MagicMock, patch
class TestCredentialStoreOperations:
"""凭证存储的测试驱动开发测试 - 先写这些。"""
def test_store_credential_success(self):
"""测试在密钥链中存储凭证。"""
# 安排
store = SecureCredentialStore("test-service")
# 行动
store.store("api-key", "sk-test-12345")
# 断言
assert store.exists("api-key") is True
assert store.retrieve("api-key") == "sk-test-12345"
def test_retrieve_nonexistent_raises_keyerror(self):
"""测试检索不存在凭证引发 KeyError。"""
store = SecureCredentialStore("test-service")
with pytest.raises(KeyError, match="Credential not found"):
store.retrieve("nonexistent-key")
def test_delete_removes_credential(self):
"""测试删除完全移除凭证。"""
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):
"""测试凭证按命名空间隔离。"""
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):
"""测试拒绝不安全钥匙环后端。"""
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)
步骤 2:实现最小通过代码
import keyring
from keyring.errors import KeyringError
import logging
logger = logging.getLogger(__name__)
class SecureCredentialStore:
"""最小实现以通过测试。"""
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
步骤 3:使用性能模式重构
测试通过后,添加缓存和日志记录(见性能模式部分)。
步骤 4:运行完整验证
# 运行所有测试覆盖
pytest tests/security/test_keychain.py -v --cov=src/security/keychain
# 运行安全特定测试
pytest tests/security/ -k "keychain or credential" -v
# 验证日志中无凭证泄露
grep -r "sk-\|password\|secret" logs/ && echo "FAIL: Credentials in logs"
4. 性能模式
4.1 凭证缓存
# 坏:重复密钥链访问
class SlowCredentialStore:
def get_api_key(self):
return keyring.get_password(self._service, "api-key") # 每次调用慢速 IPC
# 好:带 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 批处理操作
# 坏:单个密钥链调用
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 个单独 IPC 调用
# 好:单次初始化的批量加载
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]:
"""优化批量加载多个凭证。"""
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]
# 使用 - 启动时单次初始化
loader = BatchCredentialLoader("secrets", ["db-password", "api-key", "encryption-key"])
4.3 延迟加载
# 坏:导入时加载所有凭证
class EagerStore:
def __init__(self):
self.db_password = keyring.get_password("jarvis", "db") # 立即加载
self.api_key = keyring.get_password("jarvis", "api")
# 好:仅在访问时加载
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]
# 使用 - 首次访问时加载凭证
store = LazyCredentialStore("api-keys")
# 尚无密钥链调用
key = store.openai_key # 首次访问触发加载
4.4 连接重用
# 坏:每次创建新后端
def get_credential(key: str) -> str:
store = SecureCredentialStore("service") # 每次调用后端验证
return store.retrieve(key)
# 好:存储实例的单例模式
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]
# 使用 - 重用现有存储实例
store = CredentialStoreFactory.get_store("api-keys")
4.5 内存安全处理
# 坏:凭证持久化在内存中
class UnsafeStore:
def get_credential(self, key: str) -> str:
secret = keyring.get_password(self._service, key)
self.last_retrieved = secret # 在内存中持久化
return secret
# 好:带清理的安全内存处理
import ctypes
import gc
class SecureMemoryStore:
def retrieve_and_use(self, key: str, callback) -> None:
"""检索凭证,使用它,然后从内存清除。"""
secret = keyring.get_password(self._service, key)
if secret is None:
raise KeyError(f"Credential not found: {key}")
try:
callback(secret)
finally:
# 覆盖内存中的字符串(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):
"""安全凭证访问的上下文管理器。"""
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)
# 使用
store = SecureMemoryStore("secrets")
with store.with_credential("api-key") as api_key:
make_api_call(api_key)
# 上下文退出后凭证清除
5. 核心职责
5.1 主要功能
- 安全存储秘密使用操作系统原生加密
- 检索秘密带有适当的访问控制验证
- 管理凭证生命周期包括轮换和删除
- 抽象平台差异用于跨平台代码
- 与加密技能集成用于主密钥存储
6. 技术栈
6.1 推荐库
| 平台 | 库 | API | 备注 |
|---|---|---|---|
| Python(跨平台) | keyring |
统一 | 自动检测后端 |
| macOS | Security.framework |
密钥链服务 | 原生 Swift/ObjC |
| Windows | Windows.Security.Credentials |
凭据管理器 | WinRT API |
| Linux | libsecret |
秘密服务 D-Bus | GNOME 钥匙环后端 |
6.2 平台要求
- macOS:10.15+(密钥链访问改进)
- Windows:10 1903+(凭据防护支持)
- Linux:libsecret 0.20+,GNOME 钥匙环 3.36+
7. 实现模式
7.1 跨平台 Python 实现
import keyring
from keyring.errors import KeyringError
import logging
logger = logging.getLogger(__name__)
class SecureCredentialStore:
"""使用操作系统密钥链的跨平台凭证存储。"""
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_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:
"""安全存储凭证。"""
keyring.set_password(self._service, key, secret)
logger.info("keychain.credential.stored", extra={'key': key})
def retrieve(self, key: str) -> str:
"""检索凭证。如果未找到则引发 KeyError。"""
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)
logger.info("keychain.credential.deleted", extra={'key': key})
def exists(self, key: str) -> bool:
"""检查凭证是否存在。"""
return keyring.get_password(self._service, key) is not None
7.2 平台特定实现
有关高级功能的详细平台特定实现:
- macOS 密钥链(ACLs,Touch ID,安全芯片):见
references/security-examples.md#macos-keychain - Windows 凭据管理器(DPAPI,凭据防护):见
references/security-examples.md#windows-credential-manager - Linux 秘密服务(D-Bus,GNOME 钥匙环):见
references/security-examples.md#linux-secret-service
8. 安全标准
8.1 已知漏洞
| CVE | 严重性 | 平台 | 缓解措施 |
|---|---|---|---|
| CVE-2023-21726 | 高(7.8) | Windows | Windows 更新 2023 年 1 月 |
| CVE-2024-54490 | 高 | macOS | 更新至 macOS 15.2+ |
| CVE-2024-44162 | 高 | macOS | 更新至 macOS 14.7+ |
| CVE-2024-44243 | 高 | macOS | 更新至 macOS 15.2+ |
| CVE-2024-1086 | 高(7.8) | Linux | 内核 6.6.15+ |
8.2 OWASP 映射
| OWASP 2025 | 实现 |
|---|---|
| A01:访问控制破坏 | 操作系统级 ACLs,应用沙箱化 |
| A02:密码学故障 | 平台原生加密 |
| A04:不安全设计 | 深度防御,最小权限 |
| A07:识别故障 | 按服务凭证隔离 |
8.3 平台安全功能
macOS:安全芯片,每项 ACLs,代码签名,Touch ID 门控
Windows:DPAPI 加密,凭据防护,基于虚拟化的安全
Linux:D-Bus 访问控制,集合锁定,会话钥匙环隔离
详细威胁分析见 references/threat-model.md。
9. 常见错误
9.1 关键反模式
环境变量存储秘密
# 绝不:在 /proc,日志中可见
api_key = os.environ.get('API_KEY')
# 始终:操作系统密钥链
api_key = SecureCredentialStore("api").retrieve("api-key")
硬编码凭证
# 绝不:在源代码中
DATABASE_PASSWORD = "production-password-123"
# 始终:运行时检索
password = SecureCredentialStore("database").retrieve("password")
不安全文件存储
# 绝不:明文文件
with open('~/.config/app/credentials.json') as f:
creds = json.load(f)
# 始终:平台密钥链
token = SecureCredentialStore("app").retrieve("access-token")
记录凭证
# 绝不:记录值
logger.info(f"Retrieved API key: {api_key}")
# 始终:仅记录元数据
logger.info("credential.retrieved", extra={'service': service, 'key': key})
单一服务名
# 绝不:所有凭证在一个服务下
store = SecureCredentialStore("jarvis")
# 始终:按凭证类型命名空间
db_store = SecureCredentialStore("database")
api_store = SecureCredentialStore("api-keys")
10. 预实现检查清单
阶段 1:编写代码前
- [ ] 阅读
references/advanced-patterns.md了解跨平台模式 - [ ] 阅读
references/security-examples.md了解平台实现 - [ ] 查看
references/threat-model.md中的威胁模型 - [ ] 识别所需凭证命名空间
- [ ] 设计凭证操作的测试用例
- [ ] 规划性能缓存策略
阶段 2:实现期间
- [ ] 先写失败测试(测试驱动开发工作流)
- [ ] 实现最小代码通过测试
- [ ] 添加带 TTL 的凭证缓存
- [ ] 实现多个凭证的批量加载
- [ ] 对可选凭证使用延迟加载
- [ ] 添加敏感操作的内存安全处理
- [ ] 启动时验证安全钥匙环后端
- [ ] 记录操作但不记录凭证值
阶段 3:提交前
- [ ] 所有测试通过
pytest -v - [ ] 测试夹具或日志中无凭证
- [ ] 跨平台测试验证
- [ ] 内存泄露测试通过
- [ ] 安全扫描显示无凭证泄露
- [ ] 反模式代码审查完成
平台特定验证
- [ ] macOS:密钥链访问的代码签名验证
- [ ] Windows:凭据防护兼容性测试
- [ ] Linux:秘密服务守护进程运行,D-Bus 可访问
- [ ] 操作系统安全更新应用(检查上述 CVE 列表)
11. 总结
关键目标
- 测试驱动开发工作流:在实现凭证操作前先写测试
- 性能优化:缓存凭证,批处理操作,延迟加载
- 平台原生存储:对所有凭证使用操作系统密钥链服务
- 访问隔离:唯一服务名防止交叉污染
- 默认安全:自动拒绝不安全后端
安全提醒
- 环境变量中的凭证不安全
- 基于文件的凭证存储不安全
- 始终在应用启动时验证钥匙环后端
- 记录凭证操作但绝不记录值
- 保持操作系统更新以解决密钥链漏洞
参考
references/advanced-patterns.md- 跨平台模式,迁移,测试references/security-examples.md- 完整平台实现references/threat-model.md- 攻击场景和缓解措施
操作系统密钥链是你的第一道防线。误用会否定所有下游加密。