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.
Files changed (28) hide show
  1. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/PKG-INFO +2 -1
  2. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/pyproject.toml +8 -4
  3. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/__init__.py +1 -1
  4. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/crypto.py +104 -49
  5. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/.gitignore +0 -0
  6. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/LICENSE +0 -0
  7. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/README.md +0 -0
  8. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/client/node/README.md +0 -0
  9. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/client/python/README.md +0 -0
  10. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/autoindex.py +0 -0
  11. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/backends/__init__.py +0 -0
  12. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/backends/redis_backend.py +0 -0
  13. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/cascade.py +0 -0
  14. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/concurrent.py +0 -0
  15. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/engine.py +0 -0
  16. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/index.py +0 -0
  17. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/log.py +0 -0
  18. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/merkle.py +0 -0
  19. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/mongo.py +0 -0
  20. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/query.py +0 -0
  21. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/redis_compat.py +0 -0
  22. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/relations.py +0 -0
  23. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/resp2.py +0 -0
  24. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/server.py +0 -0
  25. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/snapshot.py +0 -0
  26. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/sql.py +0 -0
  27. {nedb_engine-2.2.12 → nedb_engine-2.2.13}/python/nedb/store.py +0 -0
  28. {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.12
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.12"
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 = [] # no required binary deps — installs on any platform without a C compiler
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
- # AES-256-GCM at-rest encryption. Only needed if you set NEDB_TMK / use tmk= in NEDB(path, tmk=...).
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]
@@ -47,4 +47,4 @@ __all__ = [
47
47
  "wrap_redis", "WrappedRedis",
48
48
  "_native", "__has_native__",
49
49
  ]
50
- __version__ = "2.2.12"
50
+ __version__ = "2.2.13"
@@ -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
- # Optional dependency graceful error if not installed.
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 cryptography.hazmat.primitives.ciphers.aead import AESGCM
39
- from cryptography.hazmat.primitives.kdf.hkdf import HKDF
40
- from cryptography.hazmat.primitives import hashes
41
- _HAVE_CRYPTO = True
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
- _HAVE_CRYPTO = False
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
- "pip install cryptography is required for NEDB encryption at rest."
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
- h = HKDF(algorithm=hashes.SHA256(), length=KEY_LEN,
64
- salt=b"NEDB-hkdf-v1", info=b"nedb-key")
65
- return h.derive(material)
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
- ct = AESGCM(dek).encrypt(nonce, plaintext, _AAD_DATA)
96
- return nonce + ct
97
-
98
-
99
- def decrypt_bytes(data: bytes, dek: bytes) -> bytes:
100
- """AES-256-GCM decrypt. Raises cryptography.exceptions.InvalidTag on tampering."""
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, ct = data[:NONCE_LEN], data[NONCE_LEN:]
103
- return AESGCM(dek).decrypt(nonce, ct, _AAD_DATA)
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
- ct = AESGCM(tmk).encrypt(nonce, dek, _AAD_DEK)
125
- return {"v": 1, "alg": "AES-256-GCM", "n": nonce.hex(), "ct": ct.hex()}
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 InvalidTag if the TMK is wrong."""
191
+ """Decrypt the DEK using the TMK. Raises if the TMK is wrong or data tampered."""
130
192
  _require_crypto()
131
- nonce = bytes.fromhex(wrapped["n"])
132
- ct = bytes.fromhex(wrapped["ct"])
133
- return AESGCM(tmk).decrypt(nonce, ct, _AAD_DEK)
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 # fall through to plain read (migration from unencrypted)
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
- envelope = json.dumps({"enc": 1, "ct": base64.b64encode(ct).decode()},
212
- separators=(",", ":"))
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 # plain (migration from unencrypted)
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