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))
})