nedb-engine 2.2.12__tar.gz → 2.2.13__tar.gz
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.
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/PKG-INFO +2 -1
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/pyproject.toml +8 -4
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/__init__.py +1 -1
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/crypto.py +104 -49
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/.gitignore +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/LICENSE +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/README.md +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/client/node/README.md +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/client/python/README.md +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/autoindex.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/backends/__init__.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/backends/redis_backend.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/cascade.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/concurrent.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/engine.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/index.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/log.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/merkle.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/mongo.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/query.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/redis_compat.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/relations.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/resp2.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/server.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/snapshot.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/sql.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/store.py +0 -0
- {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/wrap_redis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedb-engine
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.13
|
|
4
4
|
Summary: NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd).
|
|
5
5
|
Project-URL: Homepage, https://github.com/aiassistsecure/nedb
|
|
6
6
|
Project-URL: Repository, https://github.com/aiassistsecure/nedb
|
|
@@ -14,6 +14,7 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Topic :: Database :: Database Engines/Servers
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
|
+
Requires-Dist: pycryptodome>=3.19
|
|
17
18
|
Provides-Extra: encryption
|
|
18
19
|
Requires-Dist: cryptography>=41; extra == 'encryption'
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "nedb-engine"
|
|
13
|
-
version = "2.2.
|
|
13
|
+
version = "2.2.13"
|
|
14
14
|
description = "NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd)."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.8"
|
|
@@ -26,11 +26,15 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Database :: Database Engines/Servers",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
-
dependencies = [
|
|
29
|
+
dependencies = [
|
|
30
|
+
# AES-256-GCM at-rest encryption (pycryptodome — pre-built wheels for all platforms
|
|
31
|
+
# including Windows MinGW, no cffi / C compiler required).
|
|
32
|
+
# Also accepted: cryptography>=41 (falls back automatically if pycryptodome absent).
|
|
33
|
+
"pycryptodome>=3.19",
|
|
34
|
+
]
|
|
30
35
|
|
|
31
36
|
[project.optional-dependencies]
|
|
32
|
-
#
|
|
33
|
-
# Install with: pip install nedb-engine[encryption]
|
|
37
|
+
# Alternative crypto backend (for installations that already have cryptography).
|
|
34
38
|
encryption = ["cryptography>=41"]
|
|
35
39
|
|
|
36
40
|
[project.scripts]
|
|
@@ -24,26 +24,52 @@ TMK sources (priority order):
|
|
|
24
24
|
HKDF normalization: the TMK may be any length ≥ 16 bytes; it is always
|
|
25
25
|
stretched / compressed to exactly 32 bytes via HKDF-SHA256 before use, so
|
|
26
26
|
passphrases and key files of any size are accepted safely.
|
|
27
|
+
|
|
28
|
+
Backend: pycryptodome (primary, cross-platform, pre-built wheels for all OSes
|
|
29
|
+
including Windows MinGW — no cffi / C compiler required). Falls back to
|
|
30
|
+
cryptography if pycryptodome is not available (backwards compatibility for
|
|
31
|
+
existing installations that already have cryptography).
|
|
32
|
+
|
|
33
|
+
Install:
|
|
34
|
+
pip install nedb-engine[encryption] # installs pycryptodome
|
|
27
35
|
"""
|
|
28
36
|
from __future__ import annotations
|
|
29
37
|
|
|
30
38
|
import base64
|
|
31
|
-
import hashlib
|
|
32
39
|
import json
|
|
33
40
|
import os
|
|
34
41
|
from typing import Optional
|
|
35
42
|
|
|
36
|
-
#
|
|
43
|
+
# ── Backend detection ────────────────────────────────────────────────────────
|
|
44
|
+
# pycryptodome is the primary backend: pre-built binary wheels for all
|
|
45
|
+
# platforms (Linux / macOS / Windows x86 / Windows arm64 / Windows MinGW)
|
|
46
|
+
# with no cffi dependency — installs everywhere without a C compiler.
|
|
47
|
+
_BACKEND: Optional[str] = None
|
|
48
|
+
_HAVE_CRYPTO = False
|
|
49
|
+
|
|
37
50
|
try:
|
|
38
|
-
from
|
|
39
|
-
from
|
|
40
|
-
from
|
|
41
|
-
|
|
51
|
+
from Crypto.Cipher import AES as _PCD_AES # type: ignore[import]
|
|
52
|
+
from Crypto.Protocol.KDF import HKDF as _PCD_HKDF # type: ignore[import]
|
|
53
|
+
from Crypto.Hash import SHA256 as _PCD_SHA256 # type: ignore[import]
|
|
54
|
+
_BACKEND = "pycryptodome"
|
|
55
|
+
_HAVE_CRYPTO = True
|
|
42
56
|
except ImportError:
|
|
43
|
-
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
if not _HAVE_CRYPTO:
|
|
60
|
+
# Fallback: cryptography (older installations / explicit [encryption] extra)
|
|
61
|
+
try:
|
|
62
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM as _CG_AESGCM # type: ignore[import]
|
|
63
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF as _CG_HKDF # type: ignore[import]
|
|
64
|
+
from cryptography.hazmat.primitives import hashes as _CG_hashes # type: ignore[import]
|
|
65
|
+
_BACKEND = "cryptography"
|
|
66
|
+
_HAVE_CRYPTO = True
|
|
67
|
+
except ImportError:
|
|
68
|
+
pass
|
|
44
69
|
|
|
45
70
|
KEY_LEN = 32 # 256-bit
|
|
46
71
|
NONCE_LEN = 12 # 96-bit GCM nonce (standard recommendation)
|
|
72
|
+
TAG_LEN = 16 # 128-bit GCM authentication tag
|
|
47
73
|
|
|
48
74
|
# Additional Authenticated Data tags — bind ciphertext to its purpose.
|
|
49
75
|
_AAD_DEK = b"NEDB-DEK-v1"
|
|
@@ -53,16 +79,30 @@ _AAD_DATA = b"NEDB-data-v1"
|
|
|
53
79
|
def _require_crypto() -> None:
|
|
54
80
|
if not _HAVE_CRYPTO:
|
|
55
81
|
raise ImportError(
|
|
56
|
-
"
|
|
82
|
+
"NEDB encryption at rest requires pycryptodome or cryptography.\n"
|
|
83
|
+
"Install with: pip install 'nedb-engine[encryption]'\n"
|
|
84
|
+
" (or: pip install pycryptodome)"
|
|
57
85
|
)
|
|
58
86
|
|
|
59
87
|
|
|
88
|
+
# ── Key derivation ────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
60
90
|
def derive_key(material: bytes) -> bytes:
|
|
61
91
|
"""Normalise any-length key material to exactly 32 bytes via HKDF-SHA256."""
|
|
62
92
|
_require_crypto()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
if _BACKEND == "pycryptodome":
|
|
94
|
+
return _PCD_HKDF(
|
|
95
|
+
master=material, key_len=KEY_LEN,
|
|
96
|
+
salt=b"NEDB-hkdf-v1",
|
|
97
|
+
hashmod=_PCD_SHA256,
|
|
98
|
+
context=b"nedb-key",
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
h = _CG_HKDF(
|
|
102
|
+
algorithm=_CG_hashes.SHA256(), length=KEY_LEN,
|
|
103
|
+
salt=b"NEDB-hkdf-v1", info=b"nedb-key",
|
|
104
|
+
)
|
|
105
|
+
return h.derive(material)
|
|
66
106
|
|
|
67
107
|
|
|
68
108
|
def resolve_tmk(tmk_arg: Optional[bytes] = None) -> Optional[bytes]:
|
|
@@ -86,24 +126,40 @@ def resolve_tmk(tmk_arg: Optional[bytes] = None) -> Optional[bytes]:
|
|
|
86
126
|
return derive_key(material)
|
|
87
127
|
|
|
88
128
|
|
|
89
|
-
# ── Low-level primitives
|
|
129
|
+
# ── Low-level primitives ──────────────────────────────────────────────────────
|
|
130
|
+
# On-disk format: nonce‖ciphertext‖tag (12 + len + 16 bytes)
|
|
131
|
+
# Both backends produce and consume the same byte layout for full compatibility
|
|
132
|
+
# with databases created by either backend.
|
|
90
133
|
|
|
91
|
-
def encrypt_bytes(plaintext: bytes, dek: bytes) -> bytes:
|
|
134
|
+
def encrypt_bytes(plaintext: bytes, dek: bytes, aad: bytes = _AAD_DATA) -> bytes:
|
|
92
135
|
"""AES-256-GCM encrypt. Returns nonce‖ciphertext‖tag (12 + len + 16 bytes)."""
|
|
93
136
|
_require_crypto()
|
|
94
137
|
nonce = os.urandom(NONCE_LEN)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
if _BACKEND == "pycryptodome":
|
|
139
|
+
cipher = _PCD_AES.new(dek, _PCD_AES.MODE_GCM, nonce=nonce)
|
|
140
|
+
cipher.update(aad)
|
|
141
|
+
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
|
|
142
|
+
return nonce + ciphertext + tag
|
|
143
|
+
else:
|
|
144
|
+
ct_with_tag = _CG_AESGCM(dek).encrypt(nonce, plaintext, aad)
|
|
145
|
+
return nonce + ct_with_tag
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def decrypt_bytes(data: bytes, dek: bytes, aad: bytes = _AAD_DATA) -> bytes:
|
|
149
|
+
"""AES-256-GCM decrypt. Raises ValueError / InvalidTag on tampering."""
|
|
101
150
|
_require_crypto()
|
|
102
|
-
nonce
|
|
103
|
-
|
|
151
|
+
nonce = data[:NONCE_LEN]
|
|
152
|
+
ciphertext = data[NONCE_LEN:-TAG_LEN]
|
|
153
|
+
tag = data[-TAG_LEN:]
|
|
154
|
+
if _BACKEND == "pycryptodome":
|
|
155
|
+
cipher = _PCD_AES.new(dek, _PCD_AES.MODE_GCM, nonce=nonce)
|
|
156
|
+
cipher.update(aad)
|
|
157
|
+
return cipher.decrypt_and_verify(ciphertext, tag)
|
|
158
|
+
else:
|
|
159
|
+
return _CG_AESGCM(dek).decrypt(nonce, ciphertext + tag, aad)
|
|
104
160
|
|
|
105
161
|
|
|
106
|
-
# ── DEK management
|
|
162
|
+
# ── DEK management ────────────────────────────────────────────────────────────
|
|
107
163
|
|
|
108
164
|
KEY_ENC_FILE = "key.enc"
|
|
109
165
|
|
|
@@ -121,16 +177,29 @@ def wrap_dek(dek: bytes, tmk: bytes) -> dict:
|
|
|
121
177
|
"""Encrypt the DEK with the TMK → a JSON-serialisable dict."""
|
|
122
178
|
_require_crypto()
|
|
123
179
|
nonce = os.urandom(NONCE_LEN)
|
|
124
|
-
|
|
125
|
-
|
|
180
|
+
if _BACKEND == "pycryptodome":
|
|
181
|
+
cipher = _PCD_AES.new(tmk, _PCD_AES.MODE_GCM, nonce=nonce)
|
|
182
|
+
cipher.update(_AAD_DEK)
|
|
183
|
+
ct, tag = cipher.encrypt_and_digest(dek)
|
|
184
|
+
ct_with_tag = ct + tag
|
|
185
|
+
else:
|
|
186
|
+
ct_with_tag = _CG_AESGCM(tmk).encrypt(nonce, dek, _AAD_DEK)
|
|
187
|
+
return {"v": 1, "alg": "AES-256-GCM", "n": nonce.hex(), "ct": ct_with_tag.hex()}
|
|
126
188
|
|
|
127
189
|
|
|
128
190
|
def unwrap_dek(wrapped: dict, tmk: bytes) -> bytes:
|
|
129
|
-
"""Decrypt the DEK using the TMK. Raises
|
|
191
|
+
"""Decrypt the DEK using the TMK. Raises if the TMK is wrong or data tampered."""
|
|
130
192
|
_require_crypto()
|
|
131
|
-
nonce
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
nonce = bytes.fromhex(wrapped["n"])
|
|
194
|
+
ct_with_tag = bytes.fromhex(wrapped["ct"])
|
|
195
|
+
ct = ct_with_tag[:-TAG_LEN]
|
|
196
|
+
tag = ct_with_tag[-TAG_LEN:]
|
|
197
|
+
if _BACKEND == "pycryptodome":
|
|
198
|
+
cipher = _PCD_AES.new(tmk, _PCD_AES.MODE_GCM, nonce=nonce)
|
|
199
|
+
cipher.update(_AAD_DEK)
|
|
200
|
+
return cipher.decrypt_and_verify(ct, tag)
|
|
201
|
+
else:
|
|
202
|
+
return _CG_AESGCM(tmk).decrypt(nonce, ct_with_tag, _AAD_DEK)
|
|
134
203
|
|
|
135
204
|
|
|
136
205
|
def load_or_create_dek(data_dir: str, tmk: bytes) -> bytes:
|
|
@@ -143,7 +212,6 @@ def load_or_create_dek(data_dir: str, tmk: bytes) -> bytes:
|
|
|
143
212
|
with open(path, encoding="utf-8") as fh:
|
|
144
213
|
wrapped = json.load(fh)
|
|
145
214
|
return unwrap_dek(wrapped, tmk)
|
|
146
|
-
# New database — generate a fresh DEK and persist it wrapped.
|
|
147
215
|
dek = generate_dek()
|
|
148
216
|
_save_wrapped_dek(data_dir, dek, tmk)
|
|
149
217
|
return dek
|
|
@@ -168,13 +236,9 @@ def rewrap_dek(data_dir: str, old_tmk: bytes, new_tmk: bytes) -> None:
|
|
|
168
236
|
_save_wrapped_dek(data_dir, dek, new_tmk)
|
|
169
237
|
|
|
170
238
|
|
|
171
|
-
# ── AOF line helpers
|
|
239
|
+
# ── AOF line helpers ──────────────────────────────────────────────────────────
|
|
172
240
|
|
|
173
241
|
def aof_encode(op_json: str, dek: Optional[bytes]) -> str:
|
|
174
|
-
"""
|
|
175
|
-
Encode one AOF line. If dek is set, the JSON is encrypted and the line
|
|
176
|
-
is a compact JSON envelope. Otherwise the original JSON is returned as-is.
|
|
177
|
-
"""
|
|
178
242
|
if dek is None:
|
|
179
243
|
return op_json
|
|
180
244
|
ct = encrypt_bytes(op_json.encode(), dek)
|
|
@@ -183,10 +247,6 @@ def aof_encode(op_json: str, dek: Optional[bytes]) -> str:
|
|
|
183
247
|
|
|
184
248
|
|
|
185
249
|
def aof_decode(line: str, dek: Optional[bytes]) -> str:
|
|
186
|
-
"""
|
|
187
|
-
Decode one AOF line. Handles both encrypted and plain lines transparently
|
|
188
|
-
so a database can be opened with or without a DEK.
|
|
189
|
-
"""
|
|
190
250
|
stripped = line.strip()
|
|
191
251
|
if not stripped:
|
|
192
252
|
return stripped
|
|
@@ -197,24 +257,21 @@ def aof_decode(line: str, dek: Optional[bytes]) -> str:
|
|
|
197
257
|
ct = base64.b64decode(env["ct"])
|
|
198
258
|
return decrypt_bytes(ct, dek).decode()
|
|
199
259
|
except Exception:
|
|
200
|
-
pass
|
|
260
|
+
pass
|
|
201
261
|
return stripped
|
|
202
262
|
|
|
203
263
|
|
|
204
|
-
# ── Snapshot helpers
|
|
264
|
+
# ── Snapshot helpers ──────────────────────────────────────────────────────────
|
|
205
265
|
|
|
206
266
|
def snapshot_encode(content: bytes, dek: Optional[bytes]) -> bytes:
|
|
207
|
-
"""Encrypt the entire snapshot.json content if a DEK is set."""
|
|
208
267
|
if dek is None:
|
|
209
268
|
return content
|
|
210
269
|
ct = encrypt_bytes(content, dek)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return envelope.encode()
|
|
270
|
+
return json.dumps({"enc": 1, "ct": base64.b64encode(ct).decode()},
|
|
271
|
+
separators=(",", ":")).encode()
|
|
214
272
|
|
|
215
273
|
|
|
216
274
|
def snapshot_decode(raw: bytes, dek: Optional[bytes]) -> bytes:
|
|
217
|
-
"""Decrypt snapshot.json content if it's encrypted."""
|
|
218
275
|
if dek is None:
|
|
219
276
|
return raw
|
|
220
277
|
try:
|
|
@@ -224,16 +281,14 @@ def snapshot_decode(raw: bytes, dek: Optional[bytes]) -> bytes:
|
|
|
224
281
|
return decrypt_bytes(ct, dek)
|
|
225
282
|
except Exception:
|
|
226
283
|
pass
|
|
227
|
-
return raw
|
|
284
|
+
return raw
|
|
228
285
|
|
|
229
286
|
|
|
230
|
-
# ── BlobStore chunk helpers
|
|
287
|
+
# ── BlobStore chunk helpers ───────────────────────────────────────────────────
|
|
231
288
|
|
|
232
289
|
def chunk_encode(compressed_bytes: bytes, dek: Optional[bytes]) -> bytes:
|
|
233
|
-
"""Encrypt a compressed chunk before storing it. Toggle-able."""
|
|
234
290
|
return encrypt_bytes(compressed_bytes, dek) if dek is not None else compressed_bytes
|
|
235
291
|
|
|
236
292
|
|
|
237
293
|
def chunk_decode(stored_bytes: bytes, dek: Optional[bytes]) -> bytes:
|
|
238
|
-
"""Decrypt a stored chunk. Toggle-able."""
|
|
239
294
|
return decrypt_bytes(stored_bytes, dek) if dek is not None else stored_bytes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|