Don't mutate prototypes of Uint8Array, ReadableStream and ReadableStreamDefaultWriter

This commit is contained in:
Daniel Huigens 2018-05-15 18:56:25 +02:00
parent 70f0e1d2f5
commit 4ada3fa590
13 changed files with 145 additions and 121 deletions

View File

@ -7,6 +7,7 @@
* @requires asmcrypto.js
* @requires hash.js
* @requires crypto/hash/md5
* @requires stream
* @requires util
* @module crypto/hash
*/
@ -19,6 +20,7 @@ import sha384 from 'hash.js/lib/hash/sha/384';
import sha512 from 'hash.js/lib/hash/sha/512';
import { ripemd160 } from 'hash.js/lib/hash/ripemd';
import md5 from './md5';
import stream from '../../stream';
import util from '../../util';
const rusha = new Rusha();
@ -36,7 +38,7 @@ function node_hash(type) {
function hashjs_hash(hash) {
return function(data) {
const hashInstance = hash();
return data.transform((done, value) => {
return stream.transform(data, (done, value) => {
if (!done) {
hashInstance.update(value);
} else {

View File

@ -4,6 +4,7 @@
* @requires crypto/public_key
* @requires crypto/pkcs1
* @requires enums
* @requires stream
* @requires util
* @module crypto/signature
*/
@ -12,6 +13,7 @@ import BN from 'bn.js';
import publicKey from './public_key';
import pkcs1 from './pkcs1';
import enums from '../enums';
import stream from '../stream';
import util from '../util';
export default {
@ -29,7 +31,7 @@ export default {
* @async
*/
verify: async function(algo, hash_algo, msg_MPIs, pub_MPIs, data) {
data = await data.readToEnd();
data = await stream.readToEnd(data);
switch (algo) {
case enums.publicKey.rsa_encrypt_sign:
case enums.publicKey.rsa_encrypt:
@ -83,7 +85,7 @@ export default {
* @async
*/
sign: async function(algo, hash_algo, key_params, data) {
data = await data.readToEnd();
data = await stream.readToEnd(data);
switch (algo) {
case enums.publicKey.rsa_encrypt_sign:
case enums.publicKey.rsa_encrypt:

View File

@ -19,6 +19,7 @@
* @requires encoding/base64
* @requires enums
* @requires config
* @requires stream
* @requires util
* @module encoding/armor
*/
@ -26,6 +27,7 @@
import base64 from './base64.js';
import enums from '../enums.js';
import config from '../config';
import stream from '../stream';
import util from '../util';
/**
@ -166,7 +168,7 @@ const crc_table = [
function createcrc24(input) {
let crc = 0xB704CE;
return input.transform((done, value) => {
return stream.transform(input, (done, value) => {
if (!done) {
for (let index = 0; index < value.length; index++) {
crc = (crc << 8) ^ crc_table[((crc >> 16) ^ value[index]) & 0xff];
@ -311,7 +313,7 @@ function armor(messagetype, body, partindex, parttotal, customComment) {
body = body.data;
}
let bodyClone;
[body, bodyClone] = body.tee();
[body, bodyClone] = stream.tee(body);
const result = [];
switch (messagetype) {
case enums.armor.multipart_section:

View File

@ -12,10 +12,12 @@
*/
/**
* @requires stream
* @requires util
* @module encoding/base64
*/
import stream from '../stream';
import util from '../util';
const b64s = util.str_to_Uint8Array('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'); // Standard radix-64
@ -37,7 +39,7 @@ function s2r(t, u = false) {
let l = 0;
let s = 0;
return t.transform((done, value) => {
return stream.transform(t, (done, value) => {
const r = [];
if (!done) {
@ -106,7 +108,7 @@ function r2s(t, u) {
let s = 0;
let a = 0;
return t.transform((done, value) => {
return stream.transform(t, (done, value) => {
if (!done) {
const r = [];
const tl = value.length;

View File

@ -101,10 +101,10 @@ export { default as KDFParams } from './type/kdf_params';
export { default as OID } from './type/oid';
/**
* @see module:type/oid
* @name module:openpgp.OID
* @see module:stream
* @name module:openpgp.stream
*/
export { default as Stream } from './type/stream';
export { default as stream } from './stream';
/**
* @see module:encoding/armor

View File

@ -24,6 +24,7 @@
* @requires key
* @requires config
* @requires enums
* @requires stream
* @requires util
* @requires polyfills
* @requires worker/async_proxy
@ -43,6 +44,7 @@ import { CleartextMessage } from './cleartext';
import { generate, reformat } from './key';
import config from './config/config';
import enums from './enums';
import stream from './stream';
import util from './util';
import AsyncProxy from './worker/async_proxy';
@ -596,12 +598,12 @@ async function parseMessage(message, format, asStream) {
if (format === 'binary') {
data = message.getLiteralData();
if (!asStream && util.isStream(data)) {
data = await data.readToEnd();
data = await stream.readToEnd(data);
}
} else if (format === 'utf8') {
data = message.getText();
if (!asStream && util.isStream(data)) {
data = await data.readToEnd(chunks => chunks.join(''));
data = await stream.readToEnd(data, chunks => chunks.join(''));
}
} else {
throw new Error('Invalid format');

View File

@ -17,10 +17,12 @@
/**
* @requires enums
* @requires stream
* @requires util
*/
import enums from '../enums';
import stream from '../stream';
import util from '../util';
/**
@ -67,7 +69,7 @@ Literal.prototype.getText = function() {
let text;
if (this.text === null) {
let lastChar = '';
[this.data, this.text] = this.data.tee();
[this.data, this.text] = stream.tee(this.data);
this.text = stream.transform(this.text, value => {
const text = lastChar + util.Uint8Array_to_str(value);
// decode UTF8 and normalize EOL to \n
@ -82,7 +84,7 @@ Literal.prototype.getText = function() {
return normalized.slice(0, -1);
}, () => lastChar);
}
[text, this.text] = this.text.tee();
[text, this.text] = stream.tee(this.text);
return text;
};
@ -140,7 +142,7 @@ Literal.prototype.getFilename = function() {
* @returns {module:packet.Literal} object representation
*/
Literal.prototype.read = async function(bytes) {
const reader = bytes.getReader();
const reader = stream.getReader(bytes);
// - A one-octet field that describes how the data is formatted.
const format = enums.read(enums.literal, await reader.readByte());

View File

@ -4,6 +4,7 @@
* @requires packet/packet
* @requires config
* @requires enums
* @requires stream
* @requires util
*/
@ -11,6 +12,7 @@ import * as packets from './all_packets';
import packetParser from './packet';
import config from '../config';
import enums from '../enums';
import stream from '../stream';
import util from '../util';
/**
@ -34,7 +36,7 @@ function List() {
* @param {Uint8Array} A Uint8Array of bytes.
*/
List.prototype.read = async function (bytes) {
const reader = bytes.getReader();
const reader = stream.getReader(bytes);
while (true) {
const parsed = await packetParser.read(reader);
@ -78,7 +80,7 @@ List.prototype.write = function () {
let bufferLength = 0;
const minLength = 512;
arr.push(packetParser.writeTag(this[i].tag));
arr.push(packetbytes.transform((done, value) => {
arr.push(stream.transform(packetbytes, (done, value) => {
if (!done) {
buffer.push(value);
bufferLength += value.length;

View File

@ -19,6 +19,7 @@
* @requires asmcrypto.js
* @requires crypto
* @requires enums
* @requires stream
* @requires util
*/
@ -26,6 +27,7 @@ import { AES_CFB_Decrypt, AES_CFB_Encrypt } from 'asmcrypto.js/src/aes/cfb/expor
import crypto from '../crypto';
import enums from '../enums';
import stream from '../stream';
import util from '../util';
const nodeCrypto = util.getNodeCrypto();
@ -61,7 +63,7 @@ function SymEncryptedIntegrityProtected() {
}
SymEncryptedIntegrityProtected.prototype.read = async function (bytes) {
const reader = bytes.getReader();
const reader = stream.getReader(bytes);
// - A one-octet version number. The only currently defined value is 1.
if (await reader.readByte() !== VERSION) {
@ -92,7 +94,7 @@ SymEncryptedIntegrityProtected.prototype.encrypt = async function (sessionKeyAlg
const prefix = util.concatUint8Array([prefixrandom, repeat]);
const mdc = new Uint8Array([0xD3, 0x14]); // modification detection code packet
let [tohash, tohashClone] = util.concatUint8Array([bytes, mdc]).tee();
let [tohash, tohashClone] = stream.tee(util.concatUint8Array([bytes, mdc]));
const hash = crypto.hash.sha1(util.concatUint8Array([prefix, tohashClone]));
tohash = util.concatUint8Array([tohash, hash]);
@ -100,7 +102,7 @@ SymEncryptedIntegrityProtected.prototype.encrypt = async function (sessionKeyAlg
this.encrypted = aesEncrypt(sessionKeyAlgorithm, util.concatUint8Array([prefix, tohash]), key);
} else {
this.encrypted = crypto.cfb.encrypt(prefixrandom, sessionKeyAlgorithm, tohash, key, false);
this.encrypted = this.encrypted.subarray(0, prefix.length + tohash.length);
this.encrypted = stream.subarray(this.encrypted, 0, prefix.length + tohash.length);
}
return true;
};
@ -113,7 +115,7 @@ SymEncryptedIntegrityProtected.prototype.encrypt = async function (sessionKeyAlg
* @async
*/
SymEncryptedIntegrityProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) {
const [encrypted, encryptedClone] = this.encrypted.tee();
const [encrypted, encryptedClone] = stream.tee(this.encrypted);
let decrypted;
if (sessionKeyAlgorithm.substr(0, 3) === 'aes') { // AES optimizations. Native code for node, asmCrypto for browser.
decrypted = aesDecrypt(sessionKeyAlgorithm, encrypted, key);
@ -122,20 +124,20 @@ SymEncryptedIntegrityProtected.prototype.decrypt = async function (sessionKeyAlg
}
let decryptedClone;
[decrypted, decryptedClone] = decrypted.tee();
[decrypted, decryptedClone] = stream.tee(decrypted);
// there must be a modification detection code packet as the
// last packet and everything gets hashed except the hash itself
const encryptedPrefix = await encryptedClone.subarray(0, crypto.cipher[sessionKeyAlgorithm].blockSize + 2).readToEnd();
const encryptedPrefix = await stream.readToEnd(stream.subarray(encryptedClone, 0, crypto.cipher[sessionKeyAlgorithm].blockSize + 2));
const prefix = crypto.cfb.mdc(sessionKeyAlgorithm, key, encryptedPrefix);
let [bytes, bytesClone] = decrypted.subarray(0, -20).tee();
let [bytes, bytesClone] = stream.tee(stream.subarray(decrypted, 0, -20));
const tohash = util.concatUint8Array([prefix, bytes]);
this.hash = util.Uint8Array_to_str(await crypto.hash.sha1(tohash).readToEnd());
const mdc = util.Uint8Array_to_str(await decryptedClone.subarray(-20).readToEnd());
this.hash = util.Uint8Array_to_str(await stream.readToEnd(crypto.hash.sha1(tohash)));
const mdc = util.Uint8Array_to_str(await stream.readToEnd(stream.subarray(decryptedClone, -20)));
if (this.hash !== mdc) {
throw new Error('Modification detected.');
} else {
await this.packets.read(bytesClone.subarray(0, -2));
await this.packets.read(stream.subarray(bytesClone, 0, -2));
}
return true;
@ -156,7 +158,7 @@ function aesEncrypt(algo, pt, key) {
return nodeEncrypt(algo, pt, key);
} // asm.js fallback
const cfb = new AES_CFB_Encrypt(key);
return pt.transform((done, value) => {
return stream.transform(pt, (done, value) => {
if (!done) {
return cfb.process(value).result;
}
@ -170,14 +172,14 @@ function aesDecrypt(algo, ct, key) {
pt = nodeDecrypt(algo, ct, key);
} else { // asm.js fallback
const cfb = new AES_CFB_Decrypt(key);
pt = ct.transform((done, value) => {
pt = stream.transform(ct, (done, value) => {
if (!done) {
return cfb.process(value).result;
}
return cfb.finish().result;
});
}
return pt.subarray(crypto.cipher[algo].blockSize + 2); // Remove random prefix
return stream.subarray(pt, crypto.cipher[algo].blockSize + 2); // Remove random prefix
}
function nodeEncrypt(algo, prefix, pt, key) {

View File

@ -1,7 +1,7 @@
import util from '../util';
import util from './util';
function concat(arrays) {
const readers = arrays.map(entry => entry.getReader());
const readers = arrays.map(getReader);
let current = 0;
return new ReadableStream({
async pull(controller) {
@ -17,7 +17,80 @@ function concat(arrays) {
});
}
export default { concat };
function getReader(input) {
return new Reader(input);
}
function transform(input, fn) {
if (util.isStream(input)) {
const reader = getReader(input);
return new ReadableStream({
async pull(controller) {
try {
const { done, value } = await reader.read();
const result = await fn(done, value);
if (result) controller.enqueue(result);
else if (!done) await this.pull(controller); // ??? Chrome bug?
if (done) controller.close();
} catch(e) {
controller.error(e);
}
}
});
}
const result1 = fn(false, input);
const result2 = fn(true, undefined);
if (result1 && result2) return util.concatUint8Array([result1, result2]);
return result1 || result2;
}
function tee(input) {
if (util.isStream(input)) {
return input.tee();
}
return [input, input];
}
function subarray(input, begin=0, end=Infinity) {
if (util.isStream(input)) {
if (begin >= 0 && end >= 0) {
const reader = getReader(input);
let bytesRead = 0;
return new ReadableStream({
async pull (controller) {
const { done, value } = await reader.read();
if (!done && bytesRead < end) {
if (bytesRead + value.length >= begin) {
controller.enqueue(value.subarray(Math.max(begin - bytesRead, 0), end - bytesRead));
}
bytesRead += value.length;
await this.pull(controller); // Only necessary if the above call to enqueue() didn't happen
} else {
controller.close();
}
}
});
}
return new ReadableStream({
pull: async controller => {
// TODO: Don't read entire stream into memory here.
controller.enqueue((await readToEnd(input)).subarray(begin, end));
controller.close();
}
});
}
return input.subarray(begin, end);
}
async function readToEnd(input, join) {
if (util.isStream(input)) {
return getReader(input).readToEnd(join);
}
return input;
}
export default { concat, getReader, transform, tee, subarray, readToEnd };
/*const readerAcquiredMap = new Map();
@ -33,60 +106,23 @@ ReadableStream.prototype.getReader = function() {
};*/
ReadableStream.prototype.transform = function(fn) {
const reader = this.getReader();
return new ReadableStream({
async pull(controller) {
try {
const { done, value } = await reader.read();
const result = await fn(done, value);
if (result) controller.enqueue(result);
else if (!done) await this.pull(controller); // ??? Chrome bug?
if (done) controller.close();
} catch(e) {
controller.error(e);
}
}
});
};
ReadableStream.prototype.readToEnd = async function(join) {
return this.getReader().readToEnd(join);
};
Uint8Array.prototype.getReader = function() {
function Reader(input) {
if (util.isStream(input)) {
const reader = input.getReader();
this._read = reader.read.bind(reader);
return;
}
let doneReading = false;
const reader = Object.create(ReadableStreamDefaultReader.prototype);
reader._read = async () => {
this._read = async () => {
if (doneReading) {
return { value: undefined, done: true };
}
doneReading = true;
return { value: this, done: false };
return { value: input, done: false };
};
return reader;
};
}
Uint8Array.prototype.transform = function(fn) {
const result1 = fn(false, this);
const result2 = fn(true, undefined);
if (result1 && result2) return util.concatUint8Array([result1, result2]);
return result1 || result2;
};
Uint8Array.prototype.tee = function() {
return [this, this];
};
Uint8Array.prototype.readToEnd = async function() {
return this;
};
const ReadableStreamDefaultReader = new ReadableStream().getReader().constructor;
ReadableStreamDefaultReader.prototype._read = ReadableStreamDefaultReader.prototype.read;
ReadableStreamDefaultReader.prototype.read = async function() {
Reader.prototype.read = async function() {
if (this.externalBuffer && this.externalBuffer.length) {
const value = this.externalBuffer.shift();
return { done: false, value };
@ -94,7 +130,7 @@ ReadableStreamDefaultReader.prototype.read = async function() {
return this._read();
};
ReadableStreamDefaultReader.prototype.readLine = async function() {
Reader.prototype.readLine = async function() {
let buffer = [];
let returnVal;
while (!returnVal) {
@ -116,7 +152,7 @@ ReadableStreamDefaultReader.prototype.readLine = async function() {
return returnVal;
};
ReadableStreamDefaultReader.prototype.readByte = async function() {
Reader.prototype.readByte = async function() {
const { done, value } = await this.read();
if (done) return;
const byte = value[0];
@ -124,7 +160,7 @@ ReadableStreamDefaultReader.prototype.readByte = async function() {
return byte;
};
ReadableStreamDefaultReader.prototype.readBytes = async function(length) {
Reader.prototype.readBytes = async function(length) {
const buffer = [];
let bufferLength = 0;
while (true) {
@ -143,20 +179,20 @@ ReadableStreamDefaultReader.prototype.readBytes = async function(length) {
}
};
ReadableStreamDefaultReader.prototype.peekBytes = async function(length) {
Reader.prototype.peekBytes = async function(length) {
const bytes = await this.readBytes(length);
this.unshift(bytes);
return bytes;
};
ReadableStreamDefaultReader.prototype.unshift = function(...values) {
Reader.prototype.unshift = function(...values) {
if (!this.externalBuffer) {
this.externalBuffer = [];
}
this.externalBuffer.unshift(...values.filter(value => value && value.length));
};
ReadableStreamDefaultReader.prototype.substream = function() {
Reader.prototype.substream = function() {
return new ReadableStream({ pull: pullFrom(this) });
};
@ -171,35 +207,7 @@ function pullFrom(reader) {
};
}
ReadableStream.prototype.subarray = function(begin=0, end=Infinity) {
if (begin >= 0 && end >= 0) {
const reader = this.getReader();
let bytesRead = 0;
return new ReadableStream({
async pull (controller) {
const { done, value } = await reader.read();
if (!done && bytesRead < end) {
if (bytesRead + value.length >= begin) {
controller.enqueue(value.subarray(Math.max(begin - bytesRead, 0), end - bytesRead));
}
bytesRead += value.length;
await this.pull(controller); // Only necessary if the above call to enqueue() didn't happen
} else {
controller.close();
}
}
});
}
return new ReadableStream({
pull: async controller => {
// TODO: Don't read entire stream into memory here.
controller.enqueue((await this.readToEnd()).subarray(begin, end));
controller.close();
}
});
};
ReadableStreamDefaultReader.prototype.readToEnd = async function(join=util.concatUint8Array) {
Reader.prototype.readToEnd = async function(join=util.concatUint8Array) {
const result = [];
while (true) {
const { done, value } = await this.read();

View File

@ -29,7 +29,7 @@ import rfc2822 from 'address-rfc2822';
import config from './config';
import util from './util'; // re-import module to access util functions
import b64 from './encoding/base64';
import Stream from './type/stream';
import Stream from './stream';
const isIE11 = typeof navigator !== 'undefined' && !!navigator.userAgent.match(/Trident\/7\.0.*rv:([0-9.]+).*\).*Gecko$/);
@ -421,7 +421,7 @@ export default {
print_entire_stream: function (str, stream, fn = result => result) {
const teed = stream.tee();
teed[1].readToEnd().then(result => {
stream.readToEnd(teed[1]).then(result => {
console.log(str + ': ', fn(result));
});
return teed[0];

View File

@ -9,7 +9,7 @@ const input = require('./testInputs.js');
function stringify(array) {
if (openpgp.util.isStream(array)) {
return array.readToEnd().then(stringify);
return openpgp.stream.readToEnd(array).then(stringify);
}
if (!openpgp.util.isUint8Array(array)) {

View File

@ -21,7 +21,7 @@ describe('Streaming', function() {
data,
passwords: ['test'],
});
const msgAsciiArmored = util.Uint8Array_to_str(await encrypted.data.readToEnd());
const msgAsciiArmored = util.Uint8Array_to_str(await openpgp.stream.readToEnd(encrypted.data));
const message = await openpgp.message.readArmored(msgAsciiArmored);
const decrypted = await openpgp.decrypt({
passwords: ['test'],
@ -48,7 +48,7 @@ describe('Streaming', function() {
data,
passwords: ['test'],
});
const msgAsciiArmored = util.Uint8Array_to_str(await encrypted.data.readToEnd());
const msgAsciiArmored = util.Uint8Array_to_str(await openpgp.stream.readToEnd(encrypted.data));
const message = await openpgp.message.readArmored(msgAsciiArmored);
const decrypted = await openpgp.decrypt({
passwords: ['test'],
@ -85,6 +85,6 @@ describe('Streaming', function() {
format: 'binary'
});
expect(util.isStream(decrypted.data)).to.be.true;
expect(await decrypted.data.readToEnd()).to.deep.equal(util.concatUint8Array(plaintext));
expect(await openpgp.stream.readToEnd(decrypted.data)).to.deep.equal(util.concatUint8Array(plaintext));
});
});