Pass encryption - #14
Merged
Merged
Conversation
- Implement encrypt(data, opts) and decrypt(data, password) using @noble/ciphers (AES-256-GCM) and @noble/hashes (PBKDF2-SHA256) - EMSH wire format: MAGIC(4)|VERSION(1)|ITERATIONS(4)|SALT(16)|NONCE(12)|CT+TAG - Fresh random 128-bit salt + 96-bit nonce on every encrypt() call - isEncryptedPayload() fast structural check on magic/version bytes - DEFAULT_ITERATIONS = 200_000 (OWASP 2023 PBKDF2-SHA256 minimum) - Export encrypt/decrypt/isEncryptedPayload/DEFAULT_ITERATIONS from core index - Add @noble/* to tsup external list (runtime deps, not bundled)
Tests cover: - isEncryptedPayload: magic/version detection, edge cases (empty, subarray, wrong version) - encrypt: output size, EMSH validity, random salt/nonce, empty payload, 1MB payload, custom/default iterations in wire format, all error conditions - decrypt: round-trip for text/JSON/binary/empty/1MB, self-describing iterations, subarray byteOffset handling, wrong password, tampered ciphertext, corrupted salt/nonce, truncated payload, generic error message (anti-oracle)
…Client + Meshkit interfaces - UploadOptions.encrypt?: EncryptOptions — opt-in encryption on upload - RetrieveOptions.password?: string — opt-in decryption on retrieve - MeshkitClient.upload(data, options?) — backward-compatible signature - MeshkitClient.retrieve(cid, options?) — backward-compatible signature - Meshkit facade interface updated to match - Export UploadOptions / RetrieveOptions from core public index
upload(data, options?): - if options.encrypt is set, encrypt(data, opts) before ipfs.add() - CID is from Kubo over the encrypted blob retrieve(cid, options?): - reassemble streaming chunks as before - if options.password is set AND isEncryptedPayload(raw), decrypt transparently - no password → raw bytes returned unchanged (backward compatible)
upload(data, options?): - if options.encrypt is set, encrypt before computing the CID - CID is sha256 of the encrypted blob (not the plaintext) - different passwords on same plaintext → different CIDs (by design) retrieve(cid, options?): - if options.password is set AND isEncryptedPayload(raw), decrypt - no password → raw bytes returned unchanged (backward compatible) Also fix pre-existing noUncheckedIndexedAccess TS errors in listAllObjects (match[1] defaulted to empty string; sizeStr defaulted to " 0\)
…acade upload(data, options?) and retrieve(cid, options?) pass options straight through to withFailover — no crypto logic in the facade layer. Backward compatible: all existing call sites work without changes.
schemas/storage.ts:
- Add uploadShape (raw ZodRawShapeCompat) for MCP registration
- uploadSchema wraps uploadShape with .refine() for type inference
- Add password? and pbkdf2Iterations? to uploadShape
- Add password? to retrieveSchema
- Fix pre-existing ZodEffects issue: registerStorageTools now uses
uploadShape (not uploadSchema) for server.tool() registration
tools/storage.ts:
- handleUpload: build UploadOptions from password + pbkdf2Iterations
- handleRetrieve: build RetrieveOptions from password
- Add encrypted: bool to upload result for caller awareness
meshkit/src/index.ts:
- Re-export UploadOptions, RetrieveOptions, EncryptOptions,
encrypt, decrypt, isEncryptedPayload, DEFAULT_ITERATIONS
packages/core/package.json, packages/node/package.json:
- Add types + exports fields so tsc --build can resolve workspace packages
create-client.test.ts (+7 tests): - upload with encrypt: EMSH blob sent to Kubo (not plaintext) - two encrypted uploads → different blobs (random salt) - retrieve without password returns raw encrypted bytes - retrieve with correct password decrypts transparently - retrieve with wrong password throws MeshkitError - retrieve with password on plaintext content is a no-op create-filone-client.test.ts (+6 tests): - encrypted upload sends EMSH blob to S3 - two encrypted uploads → different CIDs (random salt) - retrieve with correct password decrypts - retrieve without password returns raw bytes - retrieve with wrong password throws MeshkitError - retrieve with password on plaintext is a no-op mcp/tools/storage.test.ts (fix + 5 new tests): - Fix existing assertions for (bytes, undefined) calling convention - handleUpload with password passes encrypt options - handleUpload with custom iterations passes both fields - handleRetrieve with password passes decrypt options - handleUpload result includes encrypted: true/false flag
Phase 1: start node, upload text/JSON/binary/empty with encryption Phase 2: retrieve without password → EMSH blob returned (not plaintext) Phase 3: retrieve with correct password → original content restored Phase 4: wrong password → MeshkitError thrown Phase 5: same plaintext × 2 → different CIDs (random salt per call) Phase 6: unencrypted upload/retrieve regression guard Phase 7: graceful shutdown Uses iterations: 1_000 to keep test runtime fast while still exercising the full PBKDF2 code path. Skipped when SKIP_INTEGRATION=1 or Kubo is absent.
- AES-256-GCM / PBKDF2-SHA256 encryption on both Kubo and S3 backends - EMSH wire format: magic + version + iterations + salt + nonce + ciphertext+tag - encrypt() / decrypt() / isEncryptedPayload() exported from package root - DEFAULT_ITERATIONS = 200_000 (OWASP 2023 minimum) - iterations floor guard on encrypt() (< 1_000 throws) - iterations ceiling guard on decrypt() (> 10_000_000 throws, DoS prevention) - MCP tools: ipfs_upload and ipfs_retrieve accept password + pbkdf2Iterations - Fix integration test: pass localNode as object so custom port is forwarded - Resolve 7 CVEs (brace-expansion, fast-uri, postcss, shell-quote, hono, esbuild) - Upgrade vitest/coverage-v8 3.x -> 4.x, zod 3.x -> 4.x, @types/node 25 -> 26 - Fix @ipfs-meshkit/meshkit pin in mcp from stale registry 1.0.2 to ^1.2.0 - Update README, SECURITY.md, CHANGELOG with full encryption process docs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Client-side encrypted storage — both the Kubo and S3 backends now support transparent AES-256-GCM encryption. Data is encrypted locally before it leaves the device; the IPFS network and storage provider only ever see the ciphertext.