1
0
mirror of https://gitlab.com/timvisee/send.git synced 2024-11-10 05:02:45 +01:00
send/app/ece.js

295 lines
7.3 KiB
JavaScript
Raw Normal View History

2018-06-04 19:47:55 +02:00
require('buffer');
2018-06-21 02:05:33 +02:00
import { ReadableStream, TransformStream } from 'web-streams-polyfill';
2018-06-04 19:47:55 +02:00
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
const KEY_LENGTH = 16;
const MODE_ENCRYPT = 'encrypt';
const MODE_DECRYPT = 'decrypt';
2018-06-21 02:05:33 +02:00
const RS = 1024 * 1024;
2018-06-04 19:47:55 +02:00
const encoder = new TextEncoder();
function generateSalt(len) {
const randSalt = new Uint8Array(len);
window.crypto.getRandomValues(randSalt);
return randSalt.buffer;
}
2018-06-21 02:05:33 +02:00
export class ECETransformer {
2018-06-04 19:47:55 +02:00
constructor(mode, ikm, rs, salt) {
this.mode = mode;
this.prevChunk;
this.seq = 0;
this.firstchunk = true;
2018-06-21 02:05:33 +02:00
this.rs = rs;
2018-06-04 19:47:55 +02:00
this.ikm = ikm.buffer;
2018-06-21 02:05:33 +02:00
this.salt = salt;
2018-06-04 19:47:55 +02:00
}
async generateKey() {
const inputKey = await window.crypto.subtle.importKey(
'raw',
this.ikm,
'HKDF',
false,
['deriveKey']
);
return window.crypto.subtle.deriveKey(
{
name: 'HKDF',
2018-06-21 02:05:33 +02:00
salt: this.salt,
2018-06-04 19:47:55 +02:00
info: encoder.encode('Content-Encoding: aes128gcm\0'),
hash: 'SHA-256'
},
inputKey,
{
name: 'AES-GCM',
length: 128
},
false,
['encrypt', 'decrypt']
);
}
async generateNonceBase() {
const inputKey = await window.crypto.subtle.importKey(
'raw',
this.ikm,
'HKDF',
false,
['deriveKey']
);
const base = await window.crypto.subtle.exportKey(
'raw',
await window.crypto.subtle.deriveKey(
{
name: 'HKDF',
2018-06-21 02:05:33 +02:00
salt: this.salt,
2018-06-04 19:47:55 +02:00
info: encoder.encode('Content-Encoding: nonce\0'),
hash: 'SHA-256'
},
inputKey,
{
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
)
);
return Buffer.from(base.slice(0, NONCE_LENGTH));
}
generateNonce(seq) {
2018-06-21 02:05:33 +02:00
if (seq > 0xffffffff) {
throw new Error('record sequence number exceeds limit');
}
const nonce = Buffer.from(this.nonceBase);
2018-06-04 19:47:55 +02:00
const m = nonce.readUIntBE(nonce.length - 4, 4);
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
nonce.writeUIntBE(xor, nonce.length - 4, 4);
return nonce;
}
pad(data, isLast) {
const len = data.length;
if (len + TAG_LENGTH >= this.rs) {
throw new Error('data too large for record size');
}
if (isLast) {
const padding = Buffer.alloc(1);
padding.writeUInt8(2, 0);
return Buffer.concat([data, padding]);
} else {
const padding = Buffer.alloc(this.rs - len - TAG_LENGTH);
padding.fill(0);
padding.writeUInt8(1, 0);
return Buffer.concat([data, padding]);
}
}
unpad(data, isLast) {
for (let i = data.length - 1; i >= 0; i--) {
if (data[i]) {
if (isLast) {
if (data[i] !== 2) {
throw new Error('delimiter of final record is not 2');
}
} else {
if (data[i] !== 1) {
throw new Error('delimiter of not final record is not 1');
}
}
return data.slice(0, i);
}
}
throw new Error('no delimiter found');
}
createHeader() {
const nums = Buffer.alloc(5);
nums.writeUIntBE(this.rs, 0, 4);
nums.writeUIntBE(0, 4, 1);
2018-06-21 02:05:33 +02:00
return Buffer.concat([Buffer.from(this.salt), nums]);
2018-06-04 19:47:55 +02:00
}
//salt is arraybuffer, rs is int, length is int
readHeader(buffer) {
if (buffer.length < 21) {
throw new Error('chunk too small for reading header');
}
const header = {};
header.salt = buffer.buffer.slice(0, KEY_LENGTH);
header.rs = buffer.readUIntBE(KEY_LENGTH, 4);
const idlen = buffer.readUInt8(KEY_LENGTH + 4);
header.length = idlen + KEY_LENGTH + 5;
return header;
}
async encryptRecord(buffer, seq, isLast) {
const nonce = this.generateNonce(seq);
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
2018-06-21 02:05:33 +02:00
this.key,
2018-06-04 19:47:55 +02:00
this.pad(buffer, isLast)
);
return Buffer.from(encrypted);
}
async decryptRecord(buffer, seq, isLast) {
const nonce = this.generateNonce(seq);
const data = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: nonce,
tagLength: 128
},
2018-06-21 02:05:33 +02:00
this.key,
2018-06-04 19:47:55 +02:00
buffer
);
return this.unpad(Buffer.from(data), isLast);
}
async start(controller) {
if (this.mode === MODE_ENCRYPT) {
2018-06-21 02:05:33 +02:00
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
2018-06-04 19:47:55 +02:00
controller.enqueue(this.createHeader());
} else if (this.mode !== MODE_DECRYPT) {
throw new Error('mode must be either encrypt or decrypt');
}
}
async transformPrevChunk(isLast, controller) {
if (this.mode === MODE_ENCRYPT) {
controller.enqueue(
await this.encryptRecord(this.prevChunk, this.seq, isLast)
);
this.seq++;
} else {
if (this.seq === 0) {
//the first chunk during decryption contains only the header
const header = this.readHeader(this.prevChunk);
2018-06-21 02:05:33 +02:00
this.salt = header.salt;
2018-06-04 19:47:55 +02:00
this.rs = header.rs;
2018-06-21 02:05:33 +02:00
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
2018-06-04 19:47:55 +02:00
} else {
controller.enqueue(
await this.decryptRecord(this.prevChunk, this.seq - 1, isLast)
);
}
this.seq++;
}
}
async transform(chunk, controller) {
if (!this.firstchunk) {
await this.transformPrevChunk(false, controller);
}
this.firstchunk = false;
this.prevChunk = Buffer.from(chunk.buffer);
}
async flush(controller) {
if (this.prevChunk) {
await this.transformPrevChunk(true, controller);
}
}
}
2018-06-21 02:05:33 +02:00
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);
}
}