mindforge-memory 6.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MindForge.py +20 -0
- mindforge/__init__.py +111 -0
- mindforge/adapters/__init__.py +18 -0
- mindforge/adapters/claude_adapter.py +84 -0
- mindforge/adapters/crewai_memory.py +76 -0
- mindforge/adapters/generic_api.py +183 -0
- mindforge/adapters/langgraph_store.py +463 -0
- mindforge/adapters/openclaw_adapter.py +91 -0
- mindforge/api/__init__.py +1 -0
- mindforge/api/server.py +1567 -0
- mindforge/cli/__init__.py +1 -0
- mindforge/cli/main.py +13607 -0
- mindforge/core/__init__.py +37 -0
- mindforge/core/embedding.py +847 -0
- mindforge/core/encryption.py +597 -0
- mindforge/core/indexer.py +363 -0
- mindforge/core/mindforge.py +6298 -0
- mindforge/core/paths.py +43 -0
- mindforge/core/query.py +501 -0
- mindforge/core/storage.py +18656 -0
- mindforge/core/types.py +314 -0
- mindforge/core/version.py +56 -0
- mindforge/daemon/__init__.py +0 -0
- mindforge/daemon/server.py +277 -0
- mindforge/mcp/__init__.py +4 -0
- mindforge/mcp/server.py +2559 -0
- mindforge/modules/__init__.py +138 -0
- mindforge/modules/categorizer.py +151 -0
- mindforge/modules/conflict_detector.py +356 -0
- mindforge/modules/connectors.py +331 -0
- mindforge/modules/context_window.py +337 -0
- mindforge/modules/event_bus.py +572 -0
- mindforge/modules/evolution.py +210 -0
- mindforge/modules/experience.py +402 -0
- mindforge/modules/federated.py +722 -0
- mindforge/modules/federated_acl.py +393 -0
- mindforge/modules/follow_up.py +315 -0
- mindforge/modules/hybrid_search.py +565 -0
- mindforge/modules/importer.py +374 -0
- mindforge/modules/integrator.py +196 -0
- mindforge/modules/intent_router.py +359 -0
- mindforge/modules/knowledge_graph.py +395 -0
- mindforge/modules/memory_compressor.py +592 -0
- mindforge/modules/memory_decay.py +474 -0
- mindforge/modules/memory_passport.py +250 -0
- mindforge/modules/multi_agent.py +534 -0
- mindforge/modules/multimodal.py +304 -0
- mindforge/modules/outcome_learning.py +186 -0
- mindforge/modules/personality.py +275 -0
- mindforge/modules/principled_forgetting.py +258 -0
- mindforge/modules/privacy.py +736 -0
- mindforge/modules/recall.py +263 -0
- mindforge/modules/session_focus.py +305 -0
- mindforge/modules/share_conflict.py +521 -0
- mindforge/modules/skill_extractor.py +340 -0
- mindforge/modules/skill_pack.py +205 -0
- mindforge/modules/timeline_viz.py +206 -0
- mindforge/modules/v6.py +931 -0
- mindforge_memory-6.4.1.dist-info/METADATA +281 -0
- mindforge_memory-6.4.1.dist-info/RECORD +64 -0
- mindforge_memory-6.4.1.dist-info/WHEEL +5 -0
- mindforge_memory-6.4.1.dist-info/entry_points.txt +3 -0
- mindforge_memory-6.4.1.dist-info/licenses/LICENSE +21 -0
- mindforge_memory-6.4.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MindForge 加密引擎
|
|
3
|
+
AES-256-GCM + 版本化密钥派生(v6.3.1 起默认 scrypt,内存难解抗 GPU 暴力破解;
|
|
4
|
+
PBKDF2-SHA256 保留用于解密历史密钥文件与密文)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import base64
|
|
12
|
+
import threading # v5.4.7 修复 H-4:全局引擎初始化线程安全
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Tuple
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
# 懒加载 cryptography:避免 CLI 启动时导入耗时(低配电脑 300ms+)
|
|
18
|
+
_CRYPTO_MODULE = None
|
|
19
|
+
|
|
20
|
+
def _get_crypto():
|
|
21
|
+
global _CRYPTO_MODULE
|
|
22
|
+
if _CRYPTO_MODULE is None:
|
|
23
|
+
try:
|
|
24
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
25
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
26
|
+
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
|
|
27
|
+
from cryptography.hazmat.primitives import hashes
|
|
28
|
+
# v6.3.1: 增加 Scrypt(第 4 位);下标解包保持向后兼容
|
|
29
|
+
_CRYPTO_MODULE = (AESGCM, PBKDF2HMAC, hashes, Scrypt)
|
|
30
|
+
except ImportError:
|
|
31
|
+
_CRYPTO_MODULE = False
|
|
32
|
+
return _CRYPTO_MODULE
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# KDF 参数版本化
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# 当前推荐的 PBKDF2 迭代次数(OWASP 2023 推荐:SHA-256 至少 600,000)
|
|
39
|
+
# v6.3.1 起仅用于解密历史密钥文件;新密钥文件默认 scrypt
|
|
40
|
+
PBKDF2_ITERATIONS_CURRENT = 600_000
|
|
41
|
+
|
|
42
|
+
# 历史最低迭代次数(v5.5.10 及之前使用 60,000)
|
|
43
|
+
# 用于解密旧数据时的向后兼容,不作为新加密的默认值
|
|
44
|
+
PBKDF2_ITERATIONS_LEGACY = 60_000
|
|
45
|
+
|
|
46
|
+
# v6.3.1: 当前默认 KDF —— scrypt(内存难解,抗 GPU/ASIC 并行暴力破解)
|
|
47
|
+
# N=2^16, r=8, p=1 → 64 MiB 内存墙,实测约 0.3s/次派生(单次启动开销)
|
|
48
|
+
SCRYPT_N_CURRENT = 65_536
|
|
49
|
+
SCRYPT_R_CURRENT = 8
|
|
50
|
+
SCRYPT_P_CURRENT = 1
|
|
51
|
+
|
|
52
|
+
# 密钥文件格式版本(v6.3 = scrypt 默认;5.6 = PBKDF2 版本化;更早 = legacy)
|
|
53
|
+
KEY_FILE_VERSION = "6.3"
|
|
54
|
+
|
|
55
|
+
# KDF 算法标识
|
|
56
|
+
KDF_ALGORITHM = "PBKDF2-SHA256" # 历史标识,保留供兼容路径与旧数据描述
|
|
57
|
+
KDF_ALGORITHM_LEGACY = "PBKDF2-SHA256"
|
|
58
|
+
KDF_ALGORITHM_CURRENT = "scrypt"
|
|
59
|
+
|
|
60
|
+
# 密钥验证令牌(用于验证密码是否正确,而无需解密实际数据)
|
|
61
|
+
_KEY_VERIFY_TOKEN = "MindForge-Key-Verify-v1"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class KDFParams:
|
|
66
|
+
"""KDF 参数集合,用于版本化密钥派生
|
|
67
|
+
|
|
68
|
+
存储在密钥文件和密文头中,确保解密时使用与加密时相同的参数,
|
|
69
|
+
从而支持 KDF 参数的平滑升级(60k → 600k PBKDF2 → v6.3.1 scrypt)。
|
|
70
|
+
|
|
71
|
+
algorithm 为 "scrypt" 时:iterations 字段承载 N(信息性,便于旧读端识别成本量级),
|
|
72
|
+
n / r / p 为实际派生参数;algorithm 为 "PBKDF2-SHA256" 时仅使用 iterations。
|
|
73
|
+
"""
|
|
74
|
+
algorithm: str = KDF_ALGORITHM_CURRENT
|
|
75
|
+
iterations: int = SCRYPT_N_CURRENT
|
|
76
|
+
salt_length: int = 16
|
|
77
|
+
n: int = SCRYPT_N_CURRENT
|
|
78
|
+
r: int = SCRYPT_R_CURRENT
|
|
79
|
+
p: int = SCRYPT_P_CURRENT
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict:
|
|
82
|
+
result = {
|
|
83
|
+
"algorithm": self.algorithm,
|
|
84
|
+
"iterations": self.iterations,
|
|
85
|
+
"salt_length": self.salt_length,
|
|
86
|
+
}
|
|
87
|
+
if self.algorithm == KDF_ALGORITHM_CURRENT:
|
|
88
|
+
result["n"] = self.n
|
|
89
|
+
result["r"] = self.r
|
|
90
|
+
result["p"] = self.p
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_dict(cls, data: dict) -> "KDFParams":
|
|
95
|
+
"""从字典构造,缺失字段用安全默认值
|
|
96
|
+
|
|
97
|
+
向后兼容:旧密钥文件可能只有 "iterations" 字段,没有 "algorithm" 等,
|
|
98
|
+
此时按 PBKDF2 处理(历史上唯一的选择)。
|
|
99
|
+
v6.3.1: algorithm == "scrypt" 时读取 n / r / p。
|
|
100
|
+
"""
|
|
101
|
+
algorithm = data.get("algorithm", KDF_ALGORITHM_LEGACY)
|
|
102
|
+
iterations = int(data.get("iterations", PBKDF2_ITERATIONS_LEGACY))
|
|
103
|
+
if algorithm == KDF_ALGORITHM_CURRENT:
|
|
104
|
+
# scrypt:iterations 承载 N(信息性);实际派生用 n/r/p
|
|
105
|
+
n = int(data.get("n", iterations))
|
|
106
|
+
return cls(
|
|
107
|
+
algorithm=algorithm,
|
|
108
|
+
iterations=n,
|
|
109
|
+
salt_length=int(data.get("salt_length", 16)),
|
|
110
|
+
n=n,
|
|
111
|
+
r=int(data.get("r", SCRYPT_R_CURRENT)),
|
|
112
|
+
p=int(data.get("p", SCRYPT_P_CURRENT)),
|
|
113
|
+
)
|
|
114
|
+
return cls(
|
|
115
|
+
algorithm=algorithm,
|
|
116
|
+
iterations=iterations,
|
|
117
|
+
salt_length=int(data.get("salt_length", 16)),
|
|
118
|
+
n=SCRYPT_N_CURRENT,
|
|
119
|
+
r=SCRYPT_R_CURRENT,
|
|
120
|
+
p=SCRYPT_P_CURRENT,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _derive_key(password: str, salt: bytes, params: KDFParams) -> bytes:
|
|
125
|
+
"""按 KDF 参数分派密钥派生算法(v6.3.1)
|
|
126
|
+
|
|
127
|
+
- algorithm == "scrypt": Scrypt(N=params.n, r=params.r, p=params.p),内存难解
|
|
128
|
+
- 其余(含 "PBKDF2-SHA256"): PBKDF2-HMAC-SHA256(iterations)
|
|
129
|
+
|
|
130
|
+
统一在此分派,派生失败抛 SecurityError(不泄露具体参数给调用方日志)。
|
|
131
|
+
"""
|
|
132
|
+
crypto = _get_crypto()
|
|
133
|
+
if not crypto:
|
|
134
|
+
# P1-008: 移除 fallback,cryptography 缺失时直接拒绝
|
|
135
|
+
raise SecurityError(
|
|
136
|
+
"cryptography library is required for encryption. "
|
|
137
|
+
"Install with: pip install cryptography"
|
|
138
|
+
)
|
|
139
|
+
try:
|
|
140
|
+
if params.algorithm == KDF_ALGORITHM_CURRENT:
|
|
141
|
+
_, _, _, Scrypt = crypto
|
|
142
|
+
kdf = Scrypt(
|
|
143
|
+
salt=salt,
|
|
144
|
+
length=32,
|
|
145
|
+
n=params.n,
|
|
146
|
+
r=params.r,
|
|
147
|
+
p=params.p,
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
_, PBKDF2HMAC, hashes, _ = crypto
|
|
151
|
+
kdf = PBKDF2HMAC(
|
|
152
|
+
algorithm=hashes.SHA256(),
|
|
153
|
+
length=32,
|
|
154
|
+
salt=salt,
|
|
155
|
+
iterations=params.iterations,
|
|
156
|
+
)
|
|
157
|
+
return kdf.derive(password.encode("utf-8"))
|
|
158
|
+
except SecurityError:
|
|
159
|
+
raise
|
|
160
|
+
except Exception as exc: # 参数非法 / OpenSSL 资源不足等
|
|
161
|
+
raise SecurityError(f"密钥派生失败(KDF 参数非法或资源不足):{exc}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SecurityError(Exception):
|
|
165
|
+
"""安全相关异常"""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class EncryptedBlob:
|
|
171
|
+
"""加密数据块
|
|
172
|
+
|
|
173
|
+
v5.6.0: 新增 kdf_params 字段,实现密文级 KDF 参数版本化。
|
|
174
|
+
向后兼容:from_dict 时若缺 kdf_params 则假定为 legacy 参数。
|
|
175
|
+
"""
|
|
176
|
+
ciphertext: bytes
|
|
177
|
+
nonce: bytes
|
|
178
|
+
salt: bytes
|
|
179
|
+
tag: Optional[bytes] = None
|
|
180
|
+
algorithm: str = "AES-256-GCM" # 加密算法标识
|
|
181
|
+
kdf_params: Optional[KDFParams] = None # v5.6.0: 密文级 KDF 参数
|
|
182
|
+
|
|
183
|
+
def to_dict(self) -> dict:
|
|
184
|
+
result = {
|
|
185
|
+
"ciphertext": base64.b64encode(self.ciphertext).decode(),
|
|
186
|
+
"nonce": base64.b64encode(self.nonce).decode(),
|
|
187
|
+
"salt": base64.b64encode(self.salt).decode(),
|
|
188
|
+
"tag": base64.b64encode(self.tag).decode() if self.tag else None,
|
|
189
|
+
"algorithm": self.algorithm,
|
|
190
|
+
}
|
|
191
|
+
if self.kdf_params:
|
|
192
|
+
result["kdf_params"] = self.kdf_params.to_dict()
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_dict(cls, data: dict) -> "EncryptedBlob":
|
|
197
|
+
kdf_params = None
|
|
198
|
+
if "kdf_params" in data and data["kdf_params"]:
|
|
199
|
+
kdf_params = KDFParams.from_dict(data["kdf_params"])
|
|
200
|
+
return cls(
|
|
201
|
+
ciphertext=base64.b64decode(data["ciphertext"]),
|
|
202
|
+
nonce=base64.b64decode(data["nonce"]),
|
|
203
|
+
salt=base64.b64decode(data["salt"]),
|
|
204
|
+
tag=base64.b64decode(data["tag"]) if data.get("tag") else None,
|
|
205
|
+
algorithm=data.get("algorithm", "AES-256-GCM"),
|
|
206
|
+
kdf_params=kdf_params,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class EncryptionEngine:
|
|
211
|
+
"""加密引擎"""
|
|
212
|
+
|
|
213
|
+
def __init__(self, key: bytes, kdf_params: Optional[KDFParams] = None):
|
|
214
|
+
self._key = key
|
|
215
|
+
self._kdf_params = kdf_params # v5.6.0: 记录派生此密钥所用的 KDF 参数
|
|
216
|
+
crypto = _get_crypto()
|
|
217
|
+
if not crypto:
|
|
218
|
+
# P1-008: 移除 HMAC-XOR fallback,cryptography 缺失时直接拒绝初始化
|
|
219
|
+
raise SecurityError(
|
|
220
|
+
"cryptography library is required for encryption. "
|
|
221
|
+
"Install with: pip install cryptography"
|
|
222
|
+
)
|
|
223
|
+
AESGCM = crypto[0]
|
|
224
|
+
self._aesgcm = AESGCM(key)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def kdf_params(self) -> Optional[KDFParams]:
|
|
228
|
+
"""派生此密钥所用的 KDF 参数(只读)"""
|
|
229
|
+
return self._kdf_params
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def from_password(
|
|
233
|
+
cls,
|
|
234
|
+
password: str,
|
|
235
|
+
salt: Optional[bytes] = None,
|
|
236
|
+
iterations: Optional[int] = None,
|
|
237
|
+
kdf_params: Optional["KDFParams"] = None,
|
|
238
|
+
) -> Tuple["EncryptionEngine", bytes]:
|
|
239
|
+
"""从密码派生密钥
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
password: 用户密码
|
|
243
|
+
salt: 盐值,为 None 时随机生成
|
|
244
|
+
iterations: PBKDF2 迭代次数。传 None 且未传 kdf_params 时
|
|
245
|
+
使用当前默认 KDF(v6.3.1 起 scrypt);传具体值则强制 PBKDF2
|
|
246
|
+
路径(历史密钥文件/密文解密用)
|
|
247
|
+
kdf_params: 完整 KDF 参数(优先级最高),按其 algorithm 分派派生算法
|
|
248
|
+
|
|
249
|
+
v5.6.0: 新增 iterations 参数,支持版本化 KDF。
|
|
250
|
+
v6.3.1: 新增 kdf_params 参数;默认 KDF 由 PBKDF2-600k 升级为 scrypt
|
|
251
|
+
(N=2^16, r=8, p=1,64 MiB 内存墙,抗 GPU/ASIC 并行暴力破解)。
|
|
252
|
+
传 iterations(非 None)为 PBKDF2 兼容路径,行为与旧版一致。
|
|
253
|
+
"""
|
|
254
|
+
if salt is None:
|
|
255
|
+
salt = os.urandom(16)
|
|
256
|
+
|
|
257
|
+
if kdf_params is not None:
|
|
258
|
+
params = kdf_params
|
|
259
|
+
elif iterations is not None:
|
|
260
|
+
# 显式迭代次数 = 历史兼容路径,强制 PBKDF2
|
|
261
|
+
params = KDFParams(
|
|
262
|
+
algorithm=KDF_ALGORITHM_LEGACY,
|
|
263
|
+
iterations=iterations,
|
|
264
|
+
salt_length=len(salt),
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
params = KDFParams(salt_length=len(salt))
|
|
268
|
+
|
|
269
|
+
key = _derive_key(password, salt, params)
|
|
270
|
+
# 保留实际盐长,便于审计
|
|
271
|
+
if params.salt_length != len(salt):
|
|
272
|
+
params = KDFParams(
|
|
273
|
+
algorithm=params.algorithm,
|
|
274
|
+
iterations=params.iterations,
|
|
275
|
+
salt_length=len(salt),
|
|
276
|
+
n=params.n,
|
|
277
|
+
r=params.r,
|
|
278
|
+
p=params.p,
|
|
279
|
+
)
|
|
280
|
+
return cls(key, kdf_params=params), salt
|
|
281
|
+
|
|
282
|
+
def encrypt(self, plaintext: str) -> EncryptedBlob:
|
|
283
|
+
"""加密文本
|
|
284
|
+
|
|
285
|
+
salt 参与 AEAD 关联数据(AAD),使每条密文绑定唯一 salt。
|
|
286
|
+
虽然 AES-GCM 的 nonce 已提供机密性和完整性,将 salt 作为 AAD
|
|
287
|
+
可以防止密文头篡改(nonce/salt 不匹配时解密失败)。
|
|
288
|
+
"""
|
|
289
|
+
if not isinstance(plaintext, str):
|
|
290
|
+
raise SecurityError("encrypt() 要求 str 类型输入")
|
|
291
|
+
plaintext_bytes = plaintext.encode("utf-8")
|
|
292
|
+
nonce = os.urandom(12)
|
|
293
|
+
salt = os.urandom(16)
|
|
294
|
+
|
|
295
|
+
# salt 作为 AAD 参与认证加密,非装饰性字段
|
|
296
|
+
ciphertext = self._aesgcm.encrypt(nonce, plaintext_bytes, salt)
|
|
297
|
+
return EncryptedBlob(
|
|
298
|
+
ciphertext=ciphertext,
|
|
299
|
+
nonce=nonce,
|
|
300
|
+
salt=salt,
|
|
301
|
+
algorithm="AES-256-GCM",
|
|
302
|
+
kdf_params=self._kdf_params,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def decrypt(self, blob: EncryptedBlob) -> str:
|
|
306
|
+
"""解密文本"""
|
|
307
|
+
if not isinstance(blob, EncryptedBlob):
|
|
308
|
+
raise SecurityError("decrypt() 要求 EncryptedBlob 类型输入")
|
|
309
|
+
if not blob.nonce or not blob.ciphertext:
|
|
310
|
+
raise SecurityError("EncryptedBlob 缺少 nonce 或 ciphertext 字段")
|
|
311
|
+
if not blob.salt:
|
|
312
|
+
raise SecurityError("EncryptedBlob 缺少 salt 字段")
|
|
313
|
+
try:
|
|
314
|
+
# salt 作为 AAD 参与解密,篡改 salt 会导致认证失败
|
|
315
|
+
plaintext = self._aesgcm.decrypt(blob.nonce, blob.ciphertext, blob.salt)
|
|
316
|
+
return plaintext.decode("utf-8")
|
|
317
|
+
except Exception:
|
|
318
|
+
raise SecurityError("解密失败:数据损坏或密钥不正确")
|
|
319
|
+
|
|
320
|
+
def hash(self, data: str) -> str:
|
|
321
|
+
"""计算数据哈希"""
|
|
322
|
+
if not isinstance(data, str):
|
|
323
|
+
raise SecurityError("hash() 要求 str 类型输入")
|
|
324
|
+
return hashlib.sha256(data.encode()).hexdigest()
|
|
325
|
+
|
|
326
|
+
def verify_hash(self, data: str, hash_value: str) -> bool:
|
|
327
|
+
"""验证哈希"""
|
|
328
|
+
return hmac.compare_digest(self.hash(data), hash_value)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
_global_engine: Optional[EncryptionEngine] = None
|
|
332
|
+
_init_lock = threading.Lock() # v5.4.7 修复 H-4:全局引擎初始化线程安全
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _write_key_file(key_path: Path, salt: bytes, kdf_params: KDFParams,
|
|
336
|
+
engine: EncryptionEngine):
|
|
337
|
+
"""安全地写入密钥文件
|
|
338
|
+
|
|
339
|
+
v5.6.0: 抽出为独立函数,供 init_engine 和 rekey 共用。
|
|
340
|
+
v5.6.0: 新增 verify_blob 验证令牌,用于快速验证密码正确性。
|
|
341
|
+
"""
|
|
342
|
+
verify_blob = engine.encrypt(_KEY_VERIFY_TOKEN)
|
|
343
|
+
key_content = json.dumps({
|
|
344
|
+
"salt": base64.b64encode(salt).decode(),
|
|
345
|
+
"version": KEY_FILE_VERSION,
|
|
346
|
+
"kdf": kdf_params.algorithm,
|
|
347
|
+
"iterations": kdf_params.iterations,
|
|
348
|
+
"kdf_params": kdf_params.to_dict(),
|
|
349
|
+
"verify_blob": verify_blob.to_dict(),
|
|
350
|
+
}, indent=2)
|
|
351
|
+
|
|
352
|
+
import sys
|
|
353
|
+
if sys.platform != "win32":
|
|
354
|
+
# Unix/Linux/macOS: 使用 os.open 以 0o600 权限创建文件
|
|
355
|
+
import stat
|
|
356
|
+
fd = os.open(str(key_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, stat.S_IRUSR | stat.S_IWUSR)
|
|
357
|
+
try:
|
|
358
|
+
os.write(fd, key_content.encode("utf-8"))
|
|
359
|
+
finally:
|
|
360
|
+
os.close(fd)
|
|
361
|
+
else:
|
|
362
|
+
# Windows: 先写文件再尝试设置 ACL
|
|
363
|
+
with open(key_path, "w", encoding="utf-8") as f:
|
|
364
|
+
f.write(key_content)
|
|
365
|
+
try:
|
|
366
|
+
import subprocess
|
|
367
|
+
import os as _os
|
|
368
|
+
# v5.6.2 安全修复:getlogin() 在服务/容器环境可能失败,多策略获取用户名
|
|
369
|
+
username = None
|
|
370
|
+
try:
|
|
371
|
+
username = _os.getlogin()
|
|
372
|
+
except OSError:
|
|
373
|
+
pass
|
|
374
|
+
if not username:
|
|
375
|
+
username = _os.environ.get("USERNAME") or _os.environ.get("USER")
|
|
376
|
+
if not username:
|
|
377
|
+
import getpass
|
|
378
|
+
try:
|
|
379
|
+
username = getpass.getuser()
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
if username:
|
|
383
|
+
subprocess.run(
|
|
384
|
+
["icacls", str(key_path), "/inheritance:r", "/grant:r", f"{username}:F"],
|
|
385
|
+
capture_output=True, timeout=5, check=False
|
|
386
|
+
)
|
|
387
|
+
except Exception:
|
|
388
|
+
pass # icacls 失败不阻断流程
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _verify_key_password(engine: EncryptionEngine, key_data: dict) -> bool:
|
|
392
|
+
"""使用验证令牌检查密码是否正确
|
|
393
|
+
|
|
394
|
+
v5.6.0: 新密钥文件包含 verify_blob 字段。旧文件没有则跳过验证
|
|
395
|
+
(向后兼容,避免老用户升级后被锁)。
|
|
396
|
+
"""
|
|
397
|
+
if "verify_blob" not in key_data:
|
|
398
|
+
return True # legacy 密钥文件,无法验证,放行
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
blob = EncryptedBlob.from_dict(key_data["verify_blob"])
|
|
402
|
+
decrypted = engine.decrypt(blob)
|
|
403
|
+
# v5.6.2 安全修复:使用常量时间比较,防止时序攻击
|
|
404
|
+
return hmac.compare_digest(decrypted, _KEY_VERIFY_TOKEN)
|
|
405
|
+
except Exception:
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_key_params(key_file: str) -> Optional[KDFParams]:
|
|
410
|
+
"""读取密钥文件中的 KDF 参数
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
KDFParams,如果密钥文件不存在则返回 None
|
|
414
|
+
"""
|
|
415
|
+
key_path = Path(key_file)
|
|
416
|
+
if not key_path.exists():
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
with open(key_path, "r", encoding="utf-8") as f:
|
|
420
|
+
key_data = json.load(f)
|
|
421
|
+
|
|
422
|
+
# v5.6.0+: 优先使用完整的 kdf_params 结构
|
|
423
|
+
if "kdf_params" in key_data:
|
|
424
|
+
return KDFParams.from_dict(key_data["kdf_params"])
|
|
425
|
+
|
|
426
|
+
# 向后兼容:旧格式只有 iterations 字段
|
|
427
|
+
return KDFParams(
|
|
428
|
+
algorithm=key_data.get("kdf", KDF_ALGORITHM),
|
|
429
|
+
iterations=int(key_data.get("iterations", PBKDF2_ITERATIONS_LEGACY)),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def init_engine(password: str, key_file: str = "./data/.key") -> EncryptionEngine:
|
|
434
|
+
"""初始化全局加密引擎
|
|
435
|
+
|
|
436
|
+
v5.2.2 修复:显式指定文件 encoding='utf-8',避免在中文/Windows 系统上
|
|
437
|
+
出现 UnicodeDecodeError 或编码不一致问题。
|
|
438
|
+
v5.4.7 修复 H-4:添加线程锁保护全局引擎初始化。
|
|
439
|
+
v5.4.7 修复 H-2:密钥文件创建时即设置受限权限,避免权限窗口期。
|
|
440
|
+
v5.6.0 升级:版本化 KDF 参数——读取密钥文件中的 KDF 参数解密旧数据。
|
|
441
|
+
v6.3.1 升级:新密钥文件默认 scrypt(64 MiB 内存墙);历史密钥文件
|
|
442
|
+
(PBKDF2 600k / 60k / legacy)按原参数解密,兼容不变。
|
|
443
|
+
"""
|
|
444
|
+
global _global_engine
|
|
445
|
+
|
|
446
|
+
with _init_lock:
|
|
447
|
+
key_path = Path(key_file)
|
|
448
|
+
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
449
|
+
|
|
450
|
+
if key_path.exists():
|
|
451
|
+
with open(key_path, "r", encoding="utf-8") as f:
|
|
452
|
+
key_data = json.load(f)
|
|
453
|
+
salt = base64.b64decode(key_data["salt"])
|
|
454
|
+
|
|
455
|
+
# v5.6.0: 从密钥文件读取 KDF 参数,向后兼容
|
|
456
|
+
kdf_params = get_key_params(key_file)
|
|
457
|
+
if kdf_params is None:
|
|
458
|
+
kdf_params = KDFParams(
|
|
459
|
+
algorithm=KDF_ALGORITHM_LEGACY,
|
|
460
|
+
iterations=PBKDF2_ITERATIONS_LEGACY,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
# v6.3.1: 传完整 kdf_params 按算法分派(scrypt / PBKDF2 均可)
|
|
464
|
+
engine, _ = EncryptionEngine.from_password(
|
|
465
|
+
password, salt, kdf_params=kdf_params
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# v5.6.0: 验证密码正确性(有 verify_blob 时才验证)
|
|
469
|
+
if not _verify_key_password(engine, key_data):
|
|
470
|
+
raise SecurityError("密码错误")
|
|
471
|
+
|
|
472
|
+
# v5.7.3 安全修复:legacy 密钥文件(无 verify_blob)不再自动升级重写。
|
|
473
|
+
# 原实现:legacy 文件 _verify_key_password 恒返回 True → 密码即使错误也会
|
|
474
|
+
# 用错误密码派生的 engine 重写密钥文件并写入新格式 verify_blob → 此后
|
|
475
|
+
# 用户输入正确密码也无法通过校验,被永久锁死(只能用正确密码解密旧数据,
|
|
476
|
+
# 却因 verify_blob 校验失败而拒绝启动)。
|
|
477
|
+
# 现改为:legacy 文件保持只读加载,格式升级统一交由显式 `rekey` 命令
|
|
478
|
+
# (rekey 会先验证旧密码再写入新格式,见 rekey_engine)。
|
|
479
|
+
if "verify_blob" not in key_data:
|
|
480
|
+
import logging as _logging
|
|
481
|
+
_logging.getLogger("core.encryption").warning(
|
|
482
|
+
"检测到 legacy 密钥文件(无 verify_blob)。为避免密码校验失效,"
|
|
483
|
+
"已跳过自动格式升级;如需升级请运行 `mindforge rekey`。"
|
|
484
|
+
)
|
|
485
|
+
else:
|
|
486
|
+
# 新密钥文件:使用当前推荐的 KDF 参数(v6.3.1 起为 scrypt)
|
|
487
|
+
kdf_params = KDFParams()
|
|
488
|
+
engine, salt = EncryptionEngine.from_password(password)
|
|
489
|
+
_write_key_file(key_path, salt, kdf_params, engine)
|
|
490
|
+
|
|
491
|
+
_global_engine = engine
|
|
492
|
+
return engine
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def rekey_engine(
|
|
496
|
+
old_password: str,
|
|
497
|
+
new_password: str,
|
|
498
|
+
key_file: str = "./data/.key",
|
|
499
|
+
new_iterations: Optional[int] = None,
|
|
500
|
+
) -> Tuple[EncryptionEngine, EncryptionEngine]:
|
|
501
|
+
"""更换密钥(密码变更或 KDF 参数升级)
|
|
502
|
+
|
|
503
|
+
生成新的加密密钥并更新密钥文件,同时返回新旧引擎供调用者
|
|
504
|
+
对已加密数据进行重加密。
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
old_password: 旧密码
|
|
508
|
+
new_password: 新密码(可与旧密码相同,仅升级 KDF 参数)
|
|
509
|
+
key_file: 密钥文件路径
|
|
510
|
+
new_iterations: 新的迭代次数,默认使用当前推荐值
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
(old_engine, new_engine) 旧引擎和新引擎
|
|
514
|
+
|
|
515
|
+
Raises:
|
|
516
|
+
SecurityError: 旧密码验证失败或密钥文件不存在
|
|
517
|
+
"""
|
|
518
|
+
key_path = Path(key_file)
|
|
519
|
+
if not key_path.exists():
|
|
520
|
+
raise SecurityError("密钥文件不存在,无法执行 rekey")
|
|
521
|
+
|
|
522
|
+
# 1. 用旧密码和旧参数解密(验证旧密码)
|
|
523
|
+
old_kdf_params = get_key_params(key_file)
|
|
524
|
+
if old_kdf_params is None:
|
|
525
|
+
old_kdf_params = KDFParams(
|
|
526
|
+
algorithm=KDF_ALGORITHM_LEGACY,
|
|
527
|
+
iterations=PBKDF2_ITERATIONS_LEGACY,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
with open(key_path, "r", encoding="utf-8") as f:
|
|
531
|
+
key_data = json.load(f)
|
|
532
|
+
old_salt = base64.b64decode(key_data["salt"])
|
|
533
|
+
|
|
534
|
+
# v6.3.1: 完整 kdf_params 分派,支持从 scrypt 或 PBKDF2 密钥文件 rekey
|
|
535
|
+
old_engine, _ = EncryptionEngine.from_password(
|
|
536
|
+
old_password, old_salt, kdf_params=old_kdf_params
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
# 1.5 验证旧密码正确性(关键安全检查)
|
|
540
|
+
if not _verify_key_password(old_engine, key_data):
|
|
541
|
+
raise SecurityError("旧密码错误")
|
|
542
|
+
|
|
543
|
+
# 2. 生成新的 KDF 参数和密钥
|
|
544
|
+
# v6.3.1: 默认升级到 scrypt;显式传 new_iterations 则维持 PBKDF2
|
|
545
|
+
# 语义(兼容旧调用方与测试)
|
|
546
|
+
if new_iterations is None:
|
|
547
|
+
new_kdf_params = KDFParams()
|
|
548
|
+
else:
|
|
549
|
+
new_kdf_params = KDFParams(
|
|
550
|
+
algorithm=KDF_ALGORITHM_LEGACY,
|
|
551
|
+
iterations=new_iterations,
|
|
552
|
+
)
|
|
553
|
+
new_salt = os.urandom(16)
|
|
554
|
+
new_engine, _ = EncryptionEngine.from_password(
|
|
555
|
+
new_password, new_salt, kdf_params=new_kdf_params
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# 3-4. 写入新密钥文件 + 更新全局引擎(原子临界区)
|
|
559
|
+
# P0 修复:持 _init_lock 确保 key_file 和 _global_engine 原子切换,
|
|
560
|
+
# 防止多线程下另一个线程看到新密钥但旧引擎(或反之)的不一致状态。
|
|
561
|
+
with _init_lock:
|
|
562
|
+
# 3. 写入新的密钥文件(原子操作前先备份)
|
|
563
|
+
backup_path = key_path.with_suffix(key_path.suffix + ".bak")
|
|
564
|
+
if key_path.exists():
|
|
565
|
+
import shutil
|
|
566
|
+
shutil.copy2(key_path, backup_path)
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
_write_key_file(key_path, new_salt, new_kdf_params, new_engine)
|
|
570
|
+
except Exception:
|
|
571
|
+
# 写入失败时恢复备份
|
|
572
|
+
import shutil
|
|
573
|
+
if backup_path.exists():
|
|
574
|
+
shutil.copy2(backup_path, key_path)
|
|
575
|
+
raise
|
|
576
|
+
|
|
577
|
+
# 4. 更新全局引擎(在锁内,与 key_file 写入原子)
|
|
578
|
+
global _global_engine
|
|
579
|
+
_global_engine = new_engine
|
|
580
|
+
|
|
581
|
+
return old_engine, new_engine
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def get_engine() -> EncryptionEngine:
|
|
585
|
+
"""获取全局加密引擎(线程安全读取)"""
|
|
586
|
+
with _init_lock:
|
|
587
|
+
engine = _global_engine
|
|
588
|
+
if engine is None:
|
|
589
|
+
raise SecurityError("加密引擎未初始化,请先调用 init_engine()")
|
|
590
|
+
return engine
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _set_global_engine(engine: EncryptionEngine) -> None:
|
|
594
|
+
"""恢复全局加密引擎(rekey 回滚用),线程安全"""
|
|
595
|
+
global _global_engine
|
|
596
|
+
with _init_lock:
|
|
597
|
+
_global_engine = engine
|