Skip to content

Instantly share code, notes, and snippets.

@neckaros
Last active July 30, 2022 14:38
Show Gist options
  • Save neckaros/4213967da02efcf12913f868759d2b7e to your computer and use it in GitHub Desktop.
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
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);
});
});
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();
}
});
}
}
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