Skip to content

Pass encryption - #14

Merged
anurag-p6 merged 12 commits into
IPFS-Meshkit:mainfrom
anurag-p6:pass-encryption
Jul 28, 2026
Merged

Pass encryption#14
anurag-p6 merged 12 commits into
IPFS-Meshkit:mainfrom
anurag-p6:pass-encryption

Conversation

@anurag-p6

Copy link
Copy Markdown
Collaborator

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.

anurag-p6 added 12 commits July 28, 2026 00:54
- 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
@anurag-p6
anurag-p6 merged commit 24021ed into IPFS-Meshkit:main Jul 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant