# -*- coding: utf-8 -*- """Phase 3.2 - Agent 凭据加解密 (Fernet 对称加密)。 主密钥获取策略 (防竞态): - 优先环境变量 AGENT_MASTER_KEY。 - 否则读写 /app/secrets/agent_master_key: * 部署阶段由 patch 显式生成并落盘 (原子写), 容器重启/提交后稳定存在。 * 运行时只读, 文件存在即读取, 绝不重新生成 —— 避免多进程/多请求并发 重建密钥导致新旧密文无法互解。 * 仅当文件确实不存在时, 才在文件锁保护下生成 (双重检查, 原子替换)。 - 存储格式: 密文以 "enc:" 前缀标识; 非 "enc:" 视为旧明文 (向后兼容, 解密原样返回)。 """ import os import logging import fcntl from cryptography.fernet import Fernet logger = logging.getLogger(__name__) _MASTER_KEY_FILE = "/app/secrets/agent_master_key" _fernet_instance = None def _generate_and_persist_key() -> bytes: key = Fernet.generate_key() os.makedirs(os.path.dirname(_MASTER_KEY_FILE), exist_ok=True) tmp = _MASTER_KEY_FILE + ".tmp" with open(tmp, "w") as f: f.write(key.decode()) f.flush() os.fsync(f.fileno()) try: os.chmod(tmp, 0o600) except Exception: pass os.replace(tmp, _MASTER_KEY_FILE) # 原子替换, 避免读到半截文件 return key def get_master_key() -> bytes: env_key = os.environ.get("AGENT_MASTER_KEY") if env_key: return env_key.encode() if isinstance(env_key, str) else env_key # 运行时只读: 文件已存在则直接读取 (部署阶段已生成) if os.path.exists(_MASTER_KEY_FILE): try: with open(_MASTER_KEY_FILE, "r") as f: k = f.read().strip() if k: return k.encode() except Exception as e: logger.warning("[credential] 读取主密钥失败: %s" % e) # 文件缺失: 加锁串行生成, 防多进程并发重建 lockf = open(_MASTER_KEY_FILE + ".lock", "w") try: fcntl.flock(lockf, fcntl.LOCK_EX) if os.path.exists(_MASTER_KEY_FILE): # 双重检查 with open(_MASTER_KEY_FILE, "r") as f: k = f.read().strip() if k: return k.encode() return _generate_and_persist_key() finally: fcntl.flock(lockf, fcntl.LOCK_UN) try: lockf.close() except Exception: pass def _fernet() -> Fernet: global _fernet_instance if _fernet_instance is None: _fernet_instance = Fernet(get_master_key()) return _fernet_instance def encrypt_secret(plain): if plain is None or plain == "": return None if isinstance(plain, str) and plain.startswith("enc:"): return plain # 已是密文, 幂等 try: return "enc:" + _fernet().encrypt(plain.encode()).decode() except Exception as e: logger.error("[credential] 加密失败: %s" % e) return None def decrypt_secret(cipher): if cipher is None or cipher == "": return None if isinstance(cipher, str) and cipher.startswith("enc:"): try: return _fernet().decrypt(cipher[4:].encode()).decode() except Exception as e: logger.error("[credential] 解密失败(密钥不匹配或数据损坏): %s" % e) return None return cipher # 旧明文, 向后兼容 def mask_secret(plain): if not plain: return "" return ("*" * 8) + plain[-4:]