Last active
July 30, 2022 14:38
-
-
Save neckaros/4213967da02efcf12913f868759d2b7e to your computer and use it in GitHub Desktop.
Create a crypto.subtle transform stream to transform AES-CBC encoded stream to decrypted data stream. Require a browser that implement pipeThough: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/pipeThrough#Browser_compatibility
This file contains 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
import { Readable } from "stream"; | |
import { CryptoAesCBC } from "./crypt"; | |
import { AesDecryptStream, AesEncryptStream } from "./cryptostream"; | |
import { ReadableStream, WritableStream } from "stream/web"; | |
describe("Decrypt", () => { | |
it("shoud decrypt file below 16 bits", async () => { | |
const buf = Buffer.alloc(12); // Creating a new Buffer | |
buf.write("Hello world!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encrypted = await crypto.encryptFile(buf); | |
const metadata = CryptoAesCBC.getCryptoFileMetadata(encrypted); | |
const transformStream = new AesDecryptStream(key, CryptoAesCBC.bufferToUint8Array(metadata.iv)); | |
const buffers = []; | |
const encryptedPart = Buffer.from(metadata.encrypted); | |
const stream = Readable.toWeb(Readable.from(encryptedPart)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const finalBuffer = Buffer.concat(buffers); | |
expect(finalBuffer).toStrictEqual(buf); | |
}); | |
it("shoud decrypt file above 16 bits", async () => { | |
const buf = Buffer.alloc(19); // Creating a new Buffer | |
buf.write("Hello and welcome world!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encrypted = await crypto.encryptFile(buf); | |
const metadata = CryptoAesCBC.getCryptoFileMetadata(encrypted); | |
const transformStream = new AesDecryptStream(key, CryptoAesCBC.bufferToUint8Array(metadata.iv)); | |
const buffers = []; | |
const encryptedPart = Buffer.from(metadata.encrypted); | |
const stream = Readable.toWeb(Readable.from(encryptedPart)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const finalBuffer = Buffer.concat(buffers); | |
expect(finalBuffer).toStrictEqual(buf); | |
}); | |
it("shoud decrypt file multiple of 16bits", async () => { | |
const buf = Buffer.alloc(32); // Creating a new Buffer | |
buf.write("Hello and welcome world my friend!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encrypted = await crypto.encryptFile(buf); | |
const metadata = CryptoAesCBC.getCryptoFileMetadata(encrypted); | |
const transformStream = new AesDecryptStream(key, CryptoAesCBC.bufferToUint8Array(metadata.iv)); | |
const buffers = []; | |
const encryptedPart = Buffer.from(metadata.encrypted); | |
const stream = Readable.toWeb(Readable.from(encryptedPart)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const finalBuffer = Buffer.concat(buffers); | |
expect(finalBuffer).toStrictEqual(buf); | |
}); | |
it("shoud decrypt large files", async () => { | |
const buf = Buffer.alloc(95); // Creating a new Buffer | |
buf.write("Hello and welcome world my friend this is a very long message not sure it will be 95 bits long tho!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encrypted = await crypto.encryptFile(buf); | |
const metadata = CryptoAesCBC.getCryptoFileMetadata(encrypted); | |
const transformStream = new AesDecryptStream(key, CryptoAesCBC.bufferToUint8Array(metadata.iv)); | |
const buffers = []; | |
const encryptedPart = Buffer.from(metadata.encrypted); | |
const stream = Readable.toWeb(Readable.from(encryptedPart)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const finalBuffer = Buffer.concat(buffers); | |
expect(finalBuffer).toStrictEqual(buf); | |
}); | |
}); | |
describe("Encrypt", () => { | |
it("shoud encrypt file below 16 bits", async () => { | |
const buf = Buffer.alloc(12); // Creating a new Buffer | |
buf.write("Hello world!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encryptedReference = await crypto.encryptFile(buf); | |
const metadataReference = CryptoAesCBC.getCryptoFileMetadata(encryptedReference); | |
const transformStream = new AesEncryptStream(key, CryptoAesCBC.bufferToUint8Array(metadataReference.iv)); | |
const buffers = []; | |
const stream = Readable.toWeb(Readable.from(buf)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const decrypted = new Uint8Array(Buffer.concat(buffers)); | |
const referenceUi = new Uint8Array(metadataReference.encrypted); | |
expect(decrypted).toStrictEqual(referenceUi); | |
}); | |
it("shoud decrypt file above 16 bits", async () => { | |
const buf = Buffer.alloc(19); // Creating a new Buffer | |
buf.write("Hello and welcome world!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encryptedReference = await crypto.encryptFile(buf); | |
const metadataReference = CryptoAesCBC.getCryptoFileMetadata(encryptedReference); | |
const transformStream = new AesEncryptStream(key, CryptoAesCBC.bufferToUint8Array(metadataReference.iv)); | |
const buffers = []; | |
const stream = Readable.toWeb(Readable.from(buf)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const decrypted = new Uint8Array(Buffer.concat(buffers)); | |
const referenceUi = new Uint8Array(metadataReference.encrypted); | |
expect(decrypted).toStrictEqual(referenceUi); | |
}); | |
it("shoud encrypt file multiple of 16bits", async () => { | |
const buf = Buffer.alloc(32); // Creating a new Buffer | |
buf.write("Hello and welcome world my friend!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encryptedReference = await crypto.encryptFile(buf); | |
const metadataReference = CryptoAesCBC.getCryptoFileMetadata(encryptedReference); | |
const transformStream = new AesEncryptStream(key, CryptoAesCBC.bufferToUint8Array(metadataReference.iv)); | |
const buffers = []; | |
const stream = Readable.toWeb(Readable.from(buf)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const decrypted = new Uint8Array(Buffer.concat(buffers)); | |
const referenceUi = new Uint8Array(metadataReference.encrypted); | |
expect(decrypted).toStrictEqual(referenceUi); | |
}); | |
it("shoud encrypt large files", async () => { | |
const buf = Buffer.alloc(95); // Creating a new Buffer | |
buf.write("Hello and welcome world my friend this is a very long message not sure it will be 95 bits long tho!"); | |
const crypto = new CryptoAesCBC('testkey'); | |
const key = await crypto.getKey() | |
const encryptedReference = await crypto.encryptFile(buf); | |
const metadataReference = CryptoAesCBC.getCryptoFileMetadata(encryptedReference); | |
const transformStream = new AesEncryptStream(key, CryptoAesCBC.bufferToUint8Array(metadataReference.iv)); | |
const buffers = []; | |
const stream = Readable.toWeb(Readable.from(buf)); | |
stream.pipeThrough(transformStream); | |
// node.js readable streams implement the async iterator protocol | |
for await (const data of transformStream.readable) { | |
buffers.push(data); | |
} | |
const decrypted = new Uint8Array(Buffer.concat(buffers)); | |
const referenceUi = new Uint8Array(metadataReference.encrypted); | |
expect(decrypted).toStrictEqual(referenceUi); | |
}); | |
}); |
This file contains 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
import { webcrypto } from 'crypto' // for NodeJS; for web also below type webcrypto.CryptoKey becomes just CryptoKey | |
import { ReadableStreamDefaultController, ReadableStream, WritableStream } from "node:stream/web";// for NodeJS; | |
const fullPadding = new Uint8Array([0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10]) | |
class BlockUnpacker { | |
public onChunk?: (chunk: Uint8Array) => void; | |
public onClose?: () => void; | |
public controller?: ReadableStreamDefaultController<ArrayBufferView>; | |
private dataBuffer: Uint8Array; | |
private total: number; | |
constructor(private key: webcrypto.CryptoKey, private iv: Uint8Array) { | |
this.dataBuffer = new Uint8Array(0); | |
this.total = 0; | |
} | |
public async addBinaryData(addData: ArrayBufferView) { | |
if(!this.onChunk) throw new Error('No onChunk defined') | |
try { | |
const addDataUiReceived = addData as Uint8Array; | |
//Concatenate with previous buffer | |
const addDataUiWithBuffer = new Uint8Array(this.dataBuffer.length + addDataUiReceived.length); | |
addDataUiWithBuffer.set(this.dataBuffer, 0); | |
addDataUiWithBuffer.set(addDataUiReceived, this.dataBuffer.length); | |
// only get a block multiple of 16 and buffer the rest (we keep 16 for final padding) | |
const currentBlockLength = Math.floor(addDataUiWithBuffer.length / 16) * 16 - 16; | |
if (currentBlockLength <= 0) { | |
this.dataBuffer = addDataUiWithBuffer; | |
return; | |
} //not enough data to process yet | |
const addDataUi = addDataUiWithBuffer.slice(0, currentBlockLength); | |
this.dataBuffer = addDataUiWithBuffer.slice(currentBlockLength); | |
this.total = this.total + addDataUi.length; | |
// construct a fake full padding and encode it with the IV of the coming block | |
const nextIv = addDataUi.slice(addDataUi.length - 16, addDataUi.length); | |
const encryptedPadding32 = await this.encrypt(fullPadding, nextIv, this.key); | |
const encryptedPadding = encryptedPadding32.slice(0,16); // Extract only the encrypted padding part and not the new useless padding of the padding | |
// add the encrypted padding and decrypt all | |
const datawithpadding = new Uint8Array(addDataUi.length + 16); | |
datawithpadding.set(addDataUi, 0); | |
datawithpadding.set(new Uint8Array(encryptedPadding), addDataUi.length); | |
const decData = new Uint8Array(await this.decrypt(datawithpadding, this.iv, this.key)); | |
// send decrypted data | |
this.onChunk(decData); | |
// next block iv is last block (without padding) | |
this.iv = nextIv; | |
} catch (e) { | |
throw new Error("Error decrypting data block") | |
} | |
} | |
public async decrypt(arrayBuffer: Uint8Array, iv: Uint8Array, cryptoKey: webcrypto.CryptoKey) { | |
const buff = await webcrypto.subtle.decrypt({ | |
iv, | |
name: 'AES-CBC', | |
}, cryptoKey, arrayBuffer); | |
return buff; | |
}; | |
public async encrypt(arrayBuffer: Uint8Array, iv: Uint8Array, cryptoKey: webcrypto.CryptoKey) { | |
const buff = await webcrypto.subtle.encrypt({ | |
iv, | |
name: 'AES-CBC', | |
}, cryptoKey, arrayBuffer); | |
return buff; | |
}; | |
public async close() { | |
if(!this.onChunk) throw new Error('No onChunk defined') | |
if(!this.controller) throw new Error('No controller defined') | |
if (this.dataBuffer.length > 0) { | |
const decBuffer = await this.decrypt(this.dataBuffer, this.iv, this.key); | |
const decData = new Uint8Array(decBuffer); | |
this.onChunk(decData); | |
} | |
this.controller.close(); | |
} | |
} | |
export class AesDecryptStream { | |
public readable: ReadableStream; | |
public writable: WritableStream; | |
constructor(key: webcrypto.CryptoKey, iv: Uint8Array) { | |
const unpacker = new BlockUnpacker(key, iv); | |
this.readable = new ReadableStream({ | |
start(controller) { | |
unpacker.onChunk = (chunk) => controller.enqueue(chunk); | |
unpacker.controller = controller; | |
}, | |
}); | |
this.writable = new WritableStream({ | |
write(uint8Array) { | |
return unpacker.addBinaryData(uint8Array); | |
}, | |
close() { | |
unpacker.close(); | |
} | |
}); | |
} | |
} | |
class BlockPacker { | |
public onChunk?: (chunk: Uint8Array) => void; | |
public onClose?: () => void; | |
public controller?: ReadableStreamDefaultController<ArrayBufferView>; | |
private dataBuffer: Uint8Array; | |
constructor(private key: webcrypto.CryptoKey, private iv: Uint8Array) { | |
this.dataBuffer = new Uint8Array(0); | |
} | |
public async addBinaryData(addData: ArrayBufferView) { | |
if(!this.onChunk) throw new Error('No onChunk defined') | |
try { | |
const addDataUiReceived = addData as Uint8Array; | |
//Concatenate with previous buffer | |
const addDataUiWithBuffer = new Uint8Array(this.dataBuffer.length + addDataUiReceived.length); | |
addDataUiWithBuffer.set(this.dataBuffer, 0); | |
addDataUiWithBuffer.set(addDataUiReceived, this.dataBuffer.length); | |
// only get a block multiple of 16 and buffer the rest (we keep 16 for final padding) | |
const currentBlockLength = Math.floor(addDataUiWithBuffer.length / 16) * 16 - 16; | |
if (currentBlockLength <= 0) { | |
this.dataBuffer = addDataUiWithBuffer; | |
return; | |
} //not enough data to process yet | |
const addDataUi = addDataUiWithBuffer.slice(0, currentBlockLength); | |
this.dataBuffer = addDataUiWithBuffer.slice(currentBlockLength); | |
const encrypted = new Uint8Array(await this.encrypt(addDataUi, this.iv, this.key)); | |
// add the encrypted padding and decrypt all | |
const datawithoutpadding = new Uint8Array(encrypted.slice(0, addDataUi.length)); | |
// next block iv is last block | |
this.iv = datawithoutpadding.slice(-16); | |
// send decrypted data | |
this.onChunk(datawithoutpadding); | |
} catch (e) { | |
throw new Error("Error encrypting data block") | |
} | |
} | |
public async encrypt(arrayBuffer: Uint8Array, iv: Uint8Array, cryptoKey: webcrypto.CryptoKey) { | |
const buff = await webcrypto.subtle.encrypt({ | |
iv, | |
name: 'AES-CBC', | |
}, cryptoKey, arrayBuffer); | |
return buff; | |
}; | |
public async close() { | |
if(!this.onChunk) throw new Error('No onChunk defined') | |
if(!this.controller) throw new Error('No controller defined') | |
if (this.dataBuffer.length > 0) { | |
const decBuffer = await this.encrypt(this.dataBuffer, this.iv, this.key); | |
const decData = new Uint8Array(decBuffer); | |
this.onChunk(decData); | |
} else if(this.dataBuffer.length === 0) { | |
const decBuffer = await this.encrypt(fullPadding, this.iv, this.key); | |
const decData = new Uint8Array(decBuffer); | |
this.onChunk(decData); | |
} | |
this.controller.close(); | |
} | |
} | |
export class AesEncryptStream { | |
public readable: ReadableStream; | |
public writable: WritableStream; | |
constructor(key: webcrypto.CryptoKey, iv: Uint8Array) { | |
const unpacker = new BlockPacker(key, iv); | |
this.readable = new ReadableStream({ | |
start(controller) { | |
unpacker.onChunk = (chunk) => controller.enqueue(chunk); | |
unpacker.controller = controller; | |
}, | |
}); | |
this.writable = new WritableStream({ | |
write(uint8Array) { | |
return unpacker.addBinaryData(uint8Array); | |
}, | |
close() { | |
unpacker.close(); | |
} | |
}); | |
} | |
} |
This file contains 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
self.addEventListener('fetch', (event: FetchEvent) => { | |
const url = new URL(event.request.url); | |
if (url.searchParams.has('test')) { | |
let size: number; | |
event.respondWith( | |
fetch(event.request).then(r => { | |
size = +r.headers.get('Content-Length')!; | |
return r.body | |
}) | |
.then(async rs => (rs as any).pipeThrough( | |
new CryptTransformStream((await getYourCryptoKey(), getIvfromB64Url('KEcl33lrSD8fjYMBNMGesQ'), size) | |
)).then(rs => new Response((rs! as any), { | |
headers: {'Content-Type': 'image/jpeg'} | |
})) | |
); | |
} | |
}); | |
public static getIvfromB64Url = (b64url: string) => { | |
return base64ToBuffer(b64urltob64(b64url)); | |
} | |
public static base64ToBuffer(base64: string): Uint8Array { | |
const binstr = atob(base64); | |
const buf = new Uint8Array(binstr.length); | |
Array.prototype.forEach.call(binstr, (ch: any, i: number) => { | |
buf[i] = ch.charCodeAt(0); | |
}); | |
return buf; | |
} | |
public static b64urltob64(b64url: string) { | |
return b64url.replace(/-/g, "+").replace(/_/g, "/").padEnd(b64url.length + (4 - b64url.length % 4) % 4, '='); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment