fox-send/app/ece.js

307 lines
7.9 KiB
JavaScript
Raw Normal View History

2018-07-19 21:46:12 +00:00
import { transformStream } from './streams';
2020-07-28 02:23:03 +00:00
import { concat } from './utils';
2018-06-04 17:47:55 +00:00
const NONCE_LENGTH = 12;
const TAG_LENGTH = 16;
const KEY_LENGTH = 16;
const MODE_ENCRYPT = 'encrypt';
const MODE_DECRYPT = 'decrypt';
2018-07-26 05:26:11 +00:00
export const ECE_RECORD_SIZE = 1024 * 64;
2018-06-04 17:47:55 +00:00
const encoder = new TextEncoder();
function generateSalt(len) {
const randSalt = new Uint8Array(len);
2018-07-06 22:49:50 +00:00
crypto.getRandomValues(randSalt);
2018-06-04 17:47:55 +00:00
return randSalt.buffer;
}
2018-06-21 20:57:53 +00:00
class ECETransformer {
2018-06-04 17:47:55 +00:00
constructor(mode, ikm, rs, salt) {
this.mode = mode;
this.prevChunk;
this.seq = 0;
this.firstchunk = true;
2018-06-21 00:05:33 +00:00
this.rs = rs;
2018-06-04 17:47:55 +00:00
this.ikm = ikm.buffer;
2018-06-21 00:05:33 +00:00
this.salt = salt;
2018-06-04 17:47:55 +00:00
}
async generateKey() {
2018-07-06 22:49:50 +00:00
const inputKey = await crypto.subtle.importKey(
2018-06-04 17:47:55 +00:00
'raw',
this.ikm,
'HKDF',
false,
['deriveKey']
);
2018-07-06 22:49:50 +00:00
return crypto.subtle.deriveKey(
2018-06-04 17:47:55 +00:00
{
name: 'HKDF',
2018-06-21 00:05:33 +00:00
salt: this.salt,
2018-06-04 17:47:55 +00:00
info: encoder.encode('Content-Encoding: aes128gcm\0'),
hash: 'SHA-256'
},
inputKey,
{
name: 'AES-GCM',
length: 128
},
true, // Edge polyfill requires key to be extractable to encrypt :/
2018-06-04 17:47:55 +00:00
['encrypt', 'decrypt']
);
}
async generateNonceBase() {
2018-07-06 22:49:50 +00:00
const inputKey = await crypto.subtle.importKey(
2018-06-04 17:47:55 +00:00
'raw',
this.ikm,
'HKDF',
false,
['deriveKey']
);
2018-07-06 22:49:50 +00:00
const base = await crypto.subtle.exportKey(
2018-06-04 17:47:55 +00:00
'raw',
2018-07-06 22:49:50 +00:00
await crypto.subtle.deriveKey(
2018-06-04 17:47:55 +00:00
{
name: 'HKDF',
2018-06-21 00:05:33 +00:00
salt: this.salt,
2018-06-04 17:47:55 +00:00
info: encoder.encode('Content-Encoding: nonce\0'),
hash: 'SHA-256'
},
inputKey,
{
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
)
);
2020-07-28 02:23:03 +00:00
return base.slice(0, NONCE_LENGTH);
2018-06-04 17:47:55 +00:00
}
generateNonce(seq) {
2018-06-21 00:05:33 +00:00
if (seq > 0xffffffff) {
throw new Error('record sequence number exceeds limit');
}
2020-07-28 02:23:03 +00:00
const nonce = new DataView(this.nonceBase.slice());
const m = nonce.getUint32(nonce.byteLength - 4);
2018-06-04 17:47:55 +00:00
const xor = (m ^ seq) >>> 0; //forces unsigned int xor
2020-07-28 02:23:03 +00:00
nonce.setUint32(nonce.byteLength - 4, xor);
return new Uint8Array(nonce.buffer);
2018-06-04 17:47:55 +00:00
}
pad(data, isLast) {
const len = data.length;
if (len + TAG_LENGTH >= this.rs) {
throw new Error('data too large for record size');
}
if (isLast) {
2020-07-28 02:23:03 +00:00
return concat(data, Uint8Array.of(2));
2018-06-04 17:47:55 +00:00
} else {
2020-07-28 02:23:03 +00:00
const padding = new Uint8Array(this.rs - len - TAG_LENGTH);
padding[0] = 1;
return concat(data, padding);
2018-06-04 17:47:55 +00:00
}
}
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() {
2020-07-28 02:23:03 +00:00
const nums = new DataView(new ArrayBuffer(5));
nums.setUint32(0, this.rs);
return concat(new Uint8Array(this.salt), new Uint8Array(nums.buffer));
2018-06-04 17:47:55 +00:00
}
readHeader(buffer) {
if (buffer.length < 21) {
throw new Error('chunk too small for reading header');
}
const header = {};
2020-07-28 02:23:03 +00:00
const dv = new DataView(buffer.buffer);
header.salt = buffer.slice(0, KEY_LENGTH);
header.rs = dv.getUint32(KEY_LENGTH);
const idlen = dv.getUint8(KEY_LENGTH + 4);
2018-06-04 17:47:55 +00:00
header.length = idlen + KEY_LENGTH + 5;
return header;
}
async encryptRecord(buffer, seq, isLast) {
const nonce = this.generateNonce(seq);
2018-07-06 22:49:50 +00:00
const encrypted = await crypto.subtle.encrypt(
2018-06-04 17:47:55 +00:00
{ name: 'AES-GCM', iv: nonce },
2018-06-21 00:05:33 +00:00
this.key,
2018-06-04 17:47:55 +00:00
this.pad(buffer, isLast)
);
2020-07-28 02:23:03 +00:00
return new Uint8Array(encrypted);
2018-06-04 17:47:55 +00:00
}
async decryptRecord(buffer, seq, isLast) {
const nonce = this.generateNonce(seq);
2018-07-06 22:49:50 +00:00
const data = await crypto.subtle.decrypt(
2018-06-04 17:47:55 +00:00
{
name: 'AES-GCM',
iv: nonce,
tagLength: 128
},
2018-06-21 00:05:33 +00:00
this.key,
2018-06-04 17:47:55 +00:00
buffer
);
2020-07-28 02:23:03 +00:00
return this.unpad(new Uint8Array(data), isLast);
2018-06-04 17:47:55 +00:00
}
async start(controller) {
if (this.mode === MODE_ENCRYPT) {
2018-06-21 00:05:33 +00:00
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
2018-06-04 17:47:55 +00: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 00:05:33 +00:00
this.salt = header.salt;
2018-06-04 17:47:55 +00:00
this.rs = header.rs;
2018-06-21 00:05:33 +00:00
this.key = await this.generateKey();
this.nonceBase = await this.generateNonceBase();
2018-06-04 17:47:55 +00: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;
2020-07-28 02:23:03 +00:00
this.prevChunk = new Uint8Array(chunk.buffer);
2018-06-04 17:47:55 +00:00
}
async flush(controller) {
2018-07-05 19:40:49 +00:00
//console.log('ece stream ends')
2018-06-04 17:47:55 +00:00
if (this.prevChunk) {
await this.transformPrevChunk(true, controller);
}
}
}
2018-06-21 00:05:33 +00:00
2018-06-29 16:36:08 +00:00
class StreamSlicer {
constructor(rs, mode) {
this.mode = mode;
this.rs = rs;
2018-07-06 22:49:50 +00:00
this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21;
2018-06-29 16:36:08 +00:00
this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved
2018-07-05 19:40:49 +00:00
this.offset = 0;
2018-06-29 16:36:08 +00:00
}
send(buf, controller) {
controller.enqueue(buf);
2018-07-05 19:40:49 +00:00
if (this.chunkSize === 21 && this.mode === MODE_DECRYPT) {
2018-06-29 16:36:08 +00:00
this.chunkSize = this.rs;
}
2018-07-05 19:40:49 +00:00
this.partialChunk = new Uint8Array(this.chunkSize);
2018-07-18 23:39:14 +00:00
this.offset = 0;
2018-06-29 16:36:08 +00:00
}
2018-07-05 19:40:49 +00:00
//reslice input into record sized chunks
2018-06-29 16:36:08 +00:00
transform(chunk, controller) {
2018-07-05 19:40:49 +00:00
//console.log('Received chunk with %d bytes.', chunk.byteLength)
2018-06-29 16:36:08 +00:00
let i = 0;
2018-07-05 19:40:49 +00:00
if (this.offset > 0) {
2018-07-06 22:49:50 +00:00
const len = Math.min(chunk.byteLength, this.chunkSize - this.offset);
2018-07-05 19:40:49 +00:00
this.partialChunk.set(chunk.slice(0, len), this.offset);
2018-06-29 16:36:08 +00:00
this.offset += len;
i += len;
if (this.offset === this.chunkSize) {
this.send(this.partialChunk, controller);
}
}
2018-07-05 19:40:49 +00:00
while (i < chunk.byteLength) {
2018-07-18 23:39:14 +00:00
const remainingBytes = chunk.byteLength - i;
if (remainingBytes >= this.chunkSize) {
2018-06-29 16:36:08 +00:00
const record = chunk.slice(i, i + this.chunkSize);
i += this.chunkSize;
this.send(record, controller);
} else {
2018-07-18 23:39:14 +00:00
const end = chunk.slice(i, i + remainingBytes);
i += end.byteLength;
2018-06-29 16:36:08 +00:00
this.partialChunk.set(end);
2018-07-18 23:39:14 +00:00
this.offset = end.byteLength;
2018-06-29 16:36:08 +00:00
}
}
}
flush(controller) {
if (this.offset > 0) {
controller.enqueue(this.partialChunk.slice(0, this.offset));
}
2018-06-21 00:05:33 +00:00
}
}
/*
2018-07-26 05:26:11 +00:00
input: a ReadableStream containing data to be transformed
2018-07-14 00:05:19 +00:00
key: Uint8Array containing key of size KEY_LENGTH
2018-06-21 00:05:33 +00:00
rs: int containing record size, optional
salt: ArrayBuffer containing salt of KEY_LENGTH length, optional
*/
2018-07-26 05:26:11 +00:00
export function encryptStream(
input,
key,
rs = ECE_RECORD_SIZE,
salt = generateSalt(KEY_LENGTH)
) {
const mode = 'encrypt';
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
return transformStream(inputStream, new ECETransformer(mode, key, rs, salt));
}
2018-07-05 19:40:49 +00:00
2018-07-26 05:26:11 +00:00
/*
input: a ReadableStream containing data to be transformed
key: Uint8Array containing key of size KEY_LENGTH
rs: int containing record size, optional
*/
export function decryptStream(input, key, rs = ECE_RECORD_SIZE) {
const mode = 'decrypt';
const inputStream = transformStream(input, new StreamSlicer(rs, mode));
return transformStream(inputStream, new ECETransformer(mode, key, rs));
2018-06-21 00:05:33 +00:00
}