End to end encryption with schema validation

Recently, I was working on e2e encryption on yjs. The difference is that the private key does not just encrypt the update; only secret content (yText, value of yMap, content of yArray…) will be encrypted. This will still give the server the right to validate the schema of each document. Because for each application, the schema is fixed.

The benefit of this idea, first mergeUpdate and diffUpdate could work correctly because the server could parse it into the correct yjs document (with unreadable content). Second, even if some clients give a broken update (an update that violates the schema), the server could filter and remove it anyway. Third, the server could do the timeline track or some behavior analysis without leaking user data.

I have finished the very beginning of this idea. You can see my implementation on feat: support e2e encryption by himself65 · Pull Request #47 · himself65/refine · GitHub

here is one usage of the API

test('encrypt and decrypt should works', async () => {
  const doc = new Doc()
  const arr = new YArray()
  arr.insert(0, [1, 2, 3])
  doc.getArray().insert(0, [1, 2, 3, arr])
  const deriveKeyPair = await crypto.subtle.generateKey({
    name: 'ECDH',
    namedCurve: 'P-256'
  }, true, ['deriveKey'])
  const signKeyPair = await crypto.subtle.generateKey({
    name: 'ECDSA',
    namedCurve: 'P-256'
  }, true, ['sign', 'verify'])
  const encryptDecryptKey = await crypto.subtle.deriveKey({
    name: 'ECDH',
    public: deriveKeyPair.publicKey
  }, deriveKeyPair.privateKey, {
    name: 'AES-GCM',
    length: 256
  }, true, ['encrypt', 'decrypt'])
  const { iv, encryptedUpdate } = await encryptUpdateV1(
    encryptDecryptKey,
    encodeStateAsUpdate(doc)
  )
  const signature = await crypto.subtle.sign({
    name: 'ECDSA',
    hash: 'SHA-256'
  }, signKeyPair.privateKey, encryptedUpdate)
  type DataChunk = {
    iv: Uint8Array
    encryptedUpdate: Uint8Array
    signature: Uint8Array
  }
  const dataChunk = {
    iv,
    encryptedUpdate,
    signature: new Uint8Array(signature)
  } satisfies DataChunk
  expect(() => decodeUpdate(dataChunk.encryptedUpdate)).not.toThrow()
  await expect(crypto.subtle.verify(
    {
      name: 'ECDSA',
      hash: 'SHA-256'
    }, signKeyPair.publicKey, dataChunk.signature.buffer,
    dataChunk.encryptedUpdate
  )).resolves.toBe(true)

  const update = await decryptUpdateV1(encryptDecryptKey, dataChunk.iv,
    dataChunk.encryptedUpdate)
  expect(() => decodeUpdate(update)).not.toThrow()
  const secondDoc = new Doc()
  applyUpdate(secondDoc, update)
  encodeStateAsUpdate(secondDoc)
  expect(secondDoc.getArray().toJSON()).toEqual([1, 2, 3, [1, 2, 3]])
  expect(encodeStateAsUpdate(secondDoc)).toEqual(encodeStateAsUpdate(doc))
})
2 Likes

Excellent, selective-encryption and schema-support is exactly what Y.js needs to make it more applicable and efficient. Would resolve many of the open discussions we have around security, like Validation, security and middleware - #3 by stefanw.

Do you plan on continue working on this? @Himself65
This really seems like a big missing pieces before Y.js can be widely adapted as a CRDT industry standard.