操作系统密钥链技能Skill os-keychain

这个技能提供安全存储和管理凭证的功能,利用操作系统原生的密钥链服务(如Windows的Credential Manager、macOS的Keychain Services和Linux的Secret Service API),确保跨平台兼容性、高安全性和性能优化。关键词:密钥链、凭证存储、安全、跨平台、OS原生加密、身份认证、缓存优化、批处理操作。

身份认证 0 次安装 0 次浏览 更新于 3/15/2026

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. 核心原则

  1. 测试驱动开发优先 - 在实现凭证操作前先写测试
  2. 性能感知 - 缓存凭证,批处理操作,最小化密钥链调用
  3. 平台原生存储 - 对所有凭证使用操作系统密钥链服务
  4. 访问隔离 - 唯一服务名防止交叉污染
  5. 默认安全 - 自动拒绝不安全后端
  6. 跨平台支持 - 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 主要功能

  1. 安全存储秘密使用操作系统原生加密
  2. 检索秘密带有适当的访问控制验证
  3. 管理凭证生命周期包括轮换和删除
  4. 抽象平台差异用于跨平台代码
  5. 与加密技能集成用于主密钥存储

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. 总结

关键目标

  1. 测试驱动开发工作流:在实现凭证操作前先写测试
  2. 性能优化:缓存凭证,批处理操作,延迟加载
  3. 平台原生存储:对所有凭证使用操作系统密钥链服务
  4. 访问隔离:唯一服务名防止交叉污染
  5. 默认安全:自动拒绝不安全后端

安全提醒

  • 环境变量中的凭证不安全
  • 基于文件的凭证存储不安全
  • 始终在应用启动时验证钥匙环后端
  • 记录凭证操作但绝不记录值
  • 保持操作系统更新以解决密钥链漏洞

参考

  • references/advanced-patterns.md - 跨平台模式,迁移,测试
  • references/security-examples.md - 完整平台实现
  • references/threat-model.md - 攻击场景和缓解措施

操作系统密钥链是你的第一道防线。误用会否定所有下游加密。