add streaming
This commit is contained in:
parent
34cb970f11
commit
1bd7e4d486
16 changed files with 438 additions and 187 deletions
117
app/ece.js
117
app/ece.js
|
@ -1,10 +1,12 @@
|
|||
require('buffer');
|
||||
import { ReadableStream, TransformStream } from 'web-streams-polyfill';
|
||||
|
||||
const NONCE_LENGTH = 12;
|
||||
const TAG_LENGTH = 16;
|
||||
const KEY_LENGTH = 16;
|
||||
const MODE_ENCRYPT = 'encrypt';
|
||||
const MODE_DECRYPT = 'decrypt';
|
||||
const RS = 1024 * 1024;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
|
@ -14,27 +16,15 @@ function generateSalt(len) {
|
|||
return randSalt.buffer;
|
||||
}
|
||||
|
||||
/*
|
||||
mode: string, either 'encrypt' or 'decrypt'
|
||||
ikm: Uint8Array containing key of KEY_LENGTH length
|
||||
rs: int containing record size, optional
|
||||
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
|
||||
The transform stream takes data as UInt8Arrays on the writable side, and outputs
|
||||
UInt8Arrays on the readable side.
|
||||
*/
|
||||
export default class ECETransformer {
|
||||
export class ECETransformer {
|
||||
constructor(mode, ikm, rs, salt) {
|
||||
this.mode = mode;
|
||||
this.prevChunk;
|
||||
this.params = {};
|
||||
this.seq = 0;
|
||||
this.firstchunk = true;
|
||||
this.rs = rs || 1024;
|
||||
this.rs = rs;
|
||||
this.ikm = ikm.buffer;
|
||||
this.params.salt = salt;
|
||||
if (!salt) {
|
||||
this.params.salt = generateSalt(KEY_LENGTH);
|
||||
}
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
async generateKey() {
|
||||
|
@ -49,7 +39,7 @@ export default class ECETransformer {
|
|||
return window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: this.params.salt,
|
||||
salt: this.salt,
|
||||
info: encoder.encode('Content-Encoding: aes128gcm\0'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
|
@ -77,7 +67,7 @@ export default class ECETransformer {
|
|||
await window.crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'HKDF',
|
||||
salt: this.params.salt,
|
||||
salt: this.salt,
|
||||
info: encoder.encode('Content-Encoding: nonce\0'),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
|
@ -95,15 +85,14 @@ export default class ECETransformer {
|
|||
}
|
||||
|
||||
generateNonce(seq) {
|
||||
const nonce = Buffer.from(this.params.nonceBase);
|
||||
if (seq > 0xffffffff) {
|
||||
throw new Error('record sequence number exceeds limit');
|
||||
}
|
||||
const nonce = Buffer.from(this.nonceBase);
|
||||
const m = nonce.readUIntBE(nonce.length - 4, 4);
|
||||
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
|
||||
nonce.writeUIntBE(xor, nonce.length - 4, 4);
|
||||
|
||||
const m2 = nonce.readUIntBE(nonce.length - 8, 4);
|
||||
const xor2 = (m2 ^ (seq >>> 4)) >>> 0;
|
||||
nonce.writeUIntBE(xor2, nonce.length - 8, 4);
|
||||
|
||||
return nonce;
|
||||
}
|
||||
|
||||
|
@ -147,7 +136,7 @@ export default class ECETransformer {
|
|||
const nums = Buffer.alloc(5);
|
||||
nums.writeUIntBE(this.rs, 0, 4);
|
||||
nums.writeUIntBE(0, 4, 1);
|
||||
return Buffer.concat([Buffer.from(this.params.salt), nums]);
|
||||
return Buffer.concat([Buffer.from(this.salt), nums]);
|
||||
}
|
||||
|
||||
//salt is arraybuffer, rs is int, length is int
|
||||
|
@ -167,7 +156,7 @@ export default class ECETransformer {
|
|||
const nonce = this.generateNonce(seq);
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: nonce },
|
||||
this.params.key,
|
||||
this.key,
|
||||
this.pad(buffer, isLast)
|
||||
);
|
||||
return Buffer.from(encrypted);
|
||||
|
@ -181,7 +170,7 @@ export default class ECETransformer {
|
|||
iv: nonce,
|
||||
tagLength: 128
|
||||
},
|
||||
this.params.key,
|
||||
this.key,
|
||||
buffer
|
||||
);
|
||||
|
||||
|
@ -190,8 +179,8 @@ export default class ECETransformer {
|
|||
|
||||
async start(controller) {
|
||||
if (this.mode === MODE_ENCRYPT) {
|
||||
this.params.key = await this.generateKey();
|
||||
this.params.nonceBase = await this.generateNonceBase();
|
||||
this.key = await this.generateKey();
|
||||
this.nonceBase = await this.generateNonceBase();
|
||||
controller.enqueue(this.createHeader());
|
||||
} else if (this.mode !== MODE_DECRYPT) {
|
||||
throw new Error('mode must be either encrypt or decrypt');
|
||||
|
@ -208,10 +197,10 @@ export default class ECETransformer {
|
|||
if (this.seq === 0) {
|
||||
//the first chunk during decryption contains only the header
|
||||
const header = this.readHeader(this.prevChunk);
|
||||
this.params.salt = header.salt;
|
||||
this.salt = header.salt;
|
||||
this.rs = header.rs;
|
||||
this.params.key = await this.generateKey();
|
||||
this.params.nonceBase = await this.generateNonceBase();
|
||||
this.key = await this.generateKey();
|
||||
this.nonceBase = await this.generateNonceBase();
|
||||
} else {
|
||||
controller.enqueue(
|
||||
await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
|
||||
|
@ -235,3 +224,71 @@ export default class ECETransformer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BlobSlicer {
|
||||
constructor(blob, rs, mode) {
|
||||
this.blob = blob;
|
||||
this.index = 0;
|
||||
this.mode = mode;
|
||||
this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : rs;
|
||||
}
|
||||
|
||||
pull(controller) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const bytesLeft = this.blob.size - this.index;
|
||||
if (bytesLeft <= 0) {
|
||||
controller.close();
|
||||
return resolve();
|
||||
}
|
||||
let size = 1;
|
||||
if (this.mode === MODE_DECRYPT && this.index === 0) {
|
||||
size = Math.min(21, bytesLeft);
|
||||
} else {
|
||||
size = Math.min(this.chunkSize, bytesLeft);
|
||||
}
|
||||
const blob = this.blob.slice(this.index, this.index + size);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
controller.enqueue(new Uint8Array(reader.result));
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(blob);
|
||||
this.index += size;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class BlobSliceStream extends ReadableStream {
|
||||
constructor(blob, size, mode) {
|
||||
super(new BlobSlicer(blob, size, mode));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
input: a blob containing data to be transformed
|
||||
key: Uint8Array containing key of size KEY_LENGTH
|
||||
mode: string, either 'encrypt' or 'decrypt'
|
||||
rs: int containing record size, optional
|
||||
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
|
||||
*/
|
||||
|
||||
export default class ECE {
|
||||
constructor(input, key, mode, rs, salt) {
|
||||
if (rs === undefined) {
|
||||
rs = RS;
|
||||
}
|
||||
if (salt === undefined) {
|
||||
salt = generateSalt(KEY_LENGTH);
|
||||
}
|
||||
|
||||
this.streamInfo = {
|
||||
recordSize: rs,
|
||||
fileSize: input.size + 16 * Math.floor(input.size / (rs - 17))
|
||||
};
|
||||
input = new BlobSliceStream(input, rs, mode);
|
||||
|
||||
const ts = new TransformStream(new ECETransformer(mode, key, rs, salt));
|
||||
this.stream = input.pipeThrough(ts);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue