版本: 0.2.0 | 更新: 2026-06-24
| 原则 | 说明 |
|---|---|
| 最小权限 | 使用连接数据库时建议使用最低必要权限的账号 |
| 传输加密 | 所有网络传输支持 SSH/SSL 加密 |
| 存储加密 | 敏感信息 (密码) 使用 AES-GCM 加密存储 |
| 参数化查询 | 所有用户输入通过参数化查询,杜绝 SQL 注入 |
| 密钥本地化 | 加密密钥由本地机器标识派生,不通过网络传输 |
| 无遥测 | 不上传任何用户数据、查询日志或统计信息 |
用户密码
↓
cryptography.Fernet (AES-128-GCM)
↓
PBKDF2HMAC (SHA-256, 100000 iterations)
↓
机器标识 (COMPUTERNAME / hostname)
↓
加密密文 → SQLite (local_config.connections.password)
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def _get_machine_key() -> bytes:
machine_id = os.environ.get("COMPUTERNAME", "default").encode()
salt = b"opennavicat-salt-v1"
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000)
return base64.urlsafe_b64encode(kdf.derive(machine_id))
_cipher = Fernet(_get_machine_key())
def encrypt_password(plaintext: str) -> str:
return _cipher.encrypt(plaintext.encode()).decode()
def decrypt_password(ciphertext: str) -> str:
return _cipher.decrypt(ciphertext.encode()).decode()安全边界:
- 密钥文件存储在
APPDATA/OpenNavicat/data/.machine_key - 更换机器后无法解密原机器存储的密码
- 解密仅在内存中进行,不落盘
┌──────────┐ SSH Encrypted ┌──────────┐ MySQL Protocol ┌──────────┐
│ OpenNav │ ────────────────────→│ Bastion │ ────────────────────→│ DB │
│ (Client) │ Port 22 │ (Jump) │ Port 3306 │ (Server) │
└──────────┘ └──────────┘ └──────────┘
│ │
│ Local Forward │
│ 127.0.0.1:33060 → db:3306 │
实现: dal/ssh_tunnel.py — 使用 paramiko 的 request_port_forward
支持的认证方式:
- 密码认证
- RSA 私钥认证
- AutoAddPolicy (自动接受未知主机密钥)
MySQL 原生 SSL 连接配置:
conn_info.use_ssl = True
conn_info.ssl_ca = "/path/to/ca.pem"
conn_info.ssl_cert = "/path/to/client-cert.pem"
conn_info.ssl_key = "/path/to/client-key.pem"aiomysql 底层通过 PyMySQL 的 SSL Context 建立加密连接。
允许同时使用 SSH 隧道和 SSL 加密,实现双层传输加密。
所有用户输入通过 参数化查询 传入:
# ✅ 安全: 参数化
await cur.execute("SELECT * FROM users WHERE email = %s", (user_input,))
# ❌ 危险: 字符串拼接
await cur.execute(f"SELECT * FROM users WHERE email = '{user_input}'")aiomysql/pymysql 原生支持 %s 占位符的参数化查询。
class ConnectionPool:
_connectors: dict[str, BaseConnector] # connection_id → connector
_tunnels: dict[str, SSHTunnel] # connection_id → tunnel
def close(self, connection_id: str):
# 先关闭数据连接
connector = self._connectors.pop(connection_id, None)
# 再关闭 SSH 隧道
tunnel = self._tunnels.pop(connection_id, None)
tunnel.close()断开顺序: 数据库连接 → SSH 隧道,确保资源完全释放。
| 存储内容 | 存储位置 | 加密 |
|---|---|---|
| 连接配置 | APPDATA/OpenNavicat/data/connections.sqlite |
密码字段加密 |
| 应用设置 | APPDATA/OpenNavicat/settings.json |
不加密 (不含敏感信息) |
| 查询历史 | SQLite settings 表 |
不加密 (用户可配置清除) |
| 加密密钥 | APPDATA/OpenNavicat/data/.machine_key |
文件系统权限保护 |
| 风险 | 防护措施 |
|---|---|
| 敏感数据泄露给 LLM | Schema Context 仅含表名/列名/类型,不含数据 |
| SQL 注入通过 AI 生成 | 生成的 SQL 仍然经由参数化查询执行 |
| API Key 泄露 | 通过环境变量注入,不持久化到配置文件 |
| 第三方 AI 服务不可用 | 自动降级: 提供明确的错误提示,不静默失败 |
AI Service 发送给 LLM 的内容:
- ✅ 表名、列名、数据类型
- ❌ 实际数据行
- ❌ 密码/密钥
- ❌ 数据库连接信息
| 日志类型 | 内容 | 安全级别 |
|---|---|---|
| 连接日志 | 连接时间、用户、主机 (不记录密码) | INFO |
| 查询日志 | SQL 语句 (可配置关闭) | DEBUG |
| 错误日志 | 错误信息 (可能包含 SQL) | WARNING |
| AI 日志 | 发送给 LLM 的内容摘要 | DEBUG |
所有日志默认输出到 stderr,不自动落盘。
| 依赖 | 版本锁定 | 说明 |
|---|---|---|
| poetry.lock | ✅ 推荐 | 锁定所有传递依赖版本 |
| pyproject.toml | ✅ 版本范围 | 指定主版本号,接受修复版本 |
| CI/CD 依赖扫描 | 📋 计划 | 集成 pip-audit 或 safety |
- 密码使用 Fernet 加密存储
- 数据库连接使用 SSH/SSL 加密
- 所有用户输入使用参数化查询
- AI 请求不包含实际数据
- 日志不包含密码/密钥
- 加密密钥不通过网络传输
- 连接资源及时释放 (close on disconnect)
- CI/CD 流水线中包含依赖安全扫描