⚡

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

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

身份认证 0 次安装 54 次浏览 更新于 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 - 攻击场景和缓解措施

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