Search end-to-end encrypted nodes through an encrypted local index

- Crypto backend: use native AES-GCM from `cryptography` when importable
  (~1.5 GiB/s), falling back to the pure-Python implementation with a
  byte-identical format and identical authentication failures.
- E2E shard: decrypted E2E text is indexed in an in-memory SQLite
  database, persisted only as serialize() -> AES-256-GCM with an HKDF key
  bound to the project and memory-secret identity, authenticated header,
  fresh nonce, 0600 atomic writes. It loads only while the project is
  unlocked; tampered, foreign, wrong-key or rotated shards fail closed and
  rebuild. Search merges shard hits (search_source local_e2e) and reports
  locked/unavailable/slow states explicitly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
HumbleTurtle
2026-09-28 22:56:38 -03:00
co-authored by Claude Opus 5.5
parent bf6acaacc8
commit d86610da8e
4 changed files with 1988 additions and 86 deletions
+152
View File
@@ -0,0 +1,152 @@
"""AES-256-GCM backend selection: known answers, interop and tamper detection.
Both the optional native backend (`cryptography`) and the stdlib-only
pure-Python backend run the same vectors through the public
aes_gcm_encrypt/aes_gcm_decrypt API used by node and bundle encryption.
"""
import importlib.util
import pathlib
import pytest
SCRIPT = pathlib.Path(__file__).resolve().parents[1] / "resources/scripts/napseer_mcp_server.py"
h = bytes.fromhex
GCM_KEY = h("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308")
GCM_IV = h("cafebabefacedbaddecaf888")
GCM_P = h(
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72"
"1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255"
)
GCM_C = h(
"522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa"
"8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad"
)
# The GCM specification (McGrew & Viega) test cases 13-16, AES-256.
KNOWN_ANSWERS = [
("tc13", bytes(32), bytes(12), b"", b"", b"", h("530f8afbc74536b9a963b4f1c4cb738b")),
("tc14", bytes(32), bytes(12), b"", bytes(16), h("cea7403d4d606b6e074ec5d3baf39d18"),
h("d0d1c8a799996bf0265b98b5d48ab919")),
("tc15", GCM_KEY, GCM_IV, b"", GCM_P, GCM_C, h("b094dac5d93471bdec1a502270e3cc6c")),
("tc16", GCM_KEY, GCM_IV, h("feedfacedeadbeeffeedfacedeadbeefabaddad2"), GCM_P[:60], GCM_C[:60],
h("76fc6ece0f4e1768cddf8853bb2d551b")),
]
@pytest.fixture(scope="module")
def mod():
spec = importlib.util.spec_from_file_location("crypto_backend_test_worker", SCRIPT)
module = importlib.util.module_from_spec(spec)
import sys
sys.path.insert(0, str(SCRIPT.parent))
spec.loader.exec_module(module)
yield module
module.select_crypto_backend()
def backends(mod):
names = [mod.CRYPTO_BACKEND_PURE]
if mod.native_crypto_available():
names.append(mod.CRYPTO_BACKEND_NATIVE)
return names
def use(mod, name):
assert mod.select_crypto_backend(name) == name
return name
def test_native_backend_is_selected_when_cryptography_is_importable(mod, monkeypatch):
pytest.importorskip("cryptography")
monkeypatch.delenv("NAPSEER_CRYPTO_BACKEND", raising=False)
assert mod.select_crypto_backend() == mod.CRYPTO_BACKEND_NATIVE
monkeypatch.setenv("NAPSEER_CRYPTO_BACKEND", "pure_python")
assert mod.select_crypto_backend() == mod.CRYPTO_BACKEND_PURE
assert mod.crypto_backend_status()["crypto_backend"] == "pure_python"
def test_pure_backend_is_used_when_native_is_unavailable(mod, monkeypatch):
monkeypatch.setattr(mod, "_NativeAESGCM", None)
monkeypatch.delenv("NAPSEER_CRYPTO_BACKEND", raising=False)
try:
assert mod.select_crypto_backend() == mod.CRYPTO_BACKEND_PURE
status = mod.crypto_backend_status()
assert status["native_crypto_available"] is False
assert "cryptography" in status["crypto_backend_note"]
finally:
monkeypatch.undo()
mod.select_crypto_backend()
@pytest.mark.parametrize("case", KNOWN_ANSWERS, ids=[case[0] for case in KNOWN_ANSWERS])
def test_known_answer_vectors_through_every_backend(mod, case):
_name, key, iv, aad, plaintext, ciphertext, tag = case
for backend in backends(mod):
use(mod, backend)
assert mod.aes_gcm_encrypt(key, iv, aad, plaintext) == ciphertext + tag, backend
assert mod.aes_gcm_decrypt(key, iv, aad, ciphertext + tag) == plaintext, backend
mod.select_crypto_backend()
def test_cross_backend_interop(mod):
pytest.importorskip("cryptography")
key = bytes(range(32))
nonce = bytes(range(12))
aad = b"napseer interop aad"
for size in (0, 1, 15, 16, 17, 1000):
plaintext = bytes((index * 7) & 0xFF for index in range(size))
use(mod, mod.CRYPTO_BACKEND_PURE)
pure = mod.aes_gcm_encrypt(key, nonce, aad, plaintext)
use(mod, mod.CRYPTO_BACKEND_NATIVE)
native = mod.aes_gcm_encrypt(key, nonce, aad, plaintext)
assert pure == native
assert mod.aes_gcm_decrypt(key, nonce, aad, pure) == plaintext
use(mod, mod.CRYPTO_BACKEND_PURE)
assert mod.aes_gcm_decrypt(key, nonce, aad, native) == plaintext
mod.select_crypto_backend()
def test_tamper_detection_raises_the_same_error_in_every_backend(mod):
key = bytes(range(32))
nonce = bytes(12)
for backend in backends(mod):
use(mod, backend)
packed = mod.aes_gcm_encrypt(key, nonce, b"aad", b"synthetic payload")
tampered_body = bytes([packed[0] ^ 1]) + packed[1:]
tampered_tag = packed[:-1] + bytes([packed[-1] ^ 0x80])
wrong_key = bytes([key[0] ^ 1]) + key[1:]
for label, attempt in (
("body", lambda: mod.aes_gcm_decrypt(key, nonce, b"aad", tampered_body)),
("tag", lambda: mod.aes_gcm_decrypt(key, nonce, b"aad", tampered_tag)),
("aad", lambda: mod.aes_gcm_decrypt(key, nonce, b"other", packed)),
("key", lambda: mod.aes_gcm_decrypt(wrong_key, nonce, b"aad", packed)),
):
with pytest.raises(RuntimeError, match="^AES-GCM authentication failed$"):
attempt()
with pytest.raises(RuntimeError, match="missing tag"):
mod.aes_gcm_decrypt(key, nonce, b"aad", b"short")
with pytest.raises(RuntimeError, match="nonce must be 12 bytes"):
mod.aes_gcm_encrypt(key, bytes(16), b"", b"x")
with pytest.raises(RuntimeError, match="key must be 32 bytes"):
mod.aes_gcm_encrypt(bytes(16), nonce, b"", b"x")
mod.select_crypto_backend()
def test_hkdf_sha256_matches_rfc5869_vectors(mod):
# RFC 5869 Appendix A, test cases 1 and 3.
okm = mod.hkdf_sha256(b"\x0b" * 22, bytes(range(13)), bytes(range(0xF0, 0xFA)), 42)
assert okm.hex() == (
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
)
okm = mod.hkdf_sha256(b"\x0b" * 22, b"", b"", 42)
assert okm.hex() == (
"8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"
)
def test_diagnostics_report_the_backend_without_key_material(mod):
status = mod.crypto_backend_status()
assert status["crypto_backend"] in {"native", "pure_python"}
assert set(status) <= {"crypto_backend", "native_crypto_available", "crypto_backend_note"}