diff --git a/src/crypto/hash/index.js b/src/crypto/hash/index.js index fefa98c0..8c45d532 100644 --- a/src/crypto/hash/index.js +++ b/src/crypto/hash/index.js @@ -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 { diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 7549c98d..5b7f166f 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -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: diff --git a/src/encoding/armor.js b/src/encoding/armor.js index 8bdcce34..6c3b2e31 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -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: diff --git a/src/encoding/base64.js b/src/encoding/base64.js index 18f2e634..3d735d79 100644 --- a/src/encoding/base64.js +++ b/src/encoding/base64.js @@ -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; diff --git a/src/index.js b/src/index.js index 3fceffa6..f2245cdd 100644 --- a/src/index.js +++ b/src/index.js @@ -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 diff --git a/src/openpgp.js b/src/openpgp.js index cd9f3cd8..ac1e9946 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -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'); diff --git a/src/packet/literal.js b/src/packet/literal.js index 6ee33a74..6e2c56a6 100644 --- a/src/packet/literal.js +++ b/src/packet/literal.js @@ -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()); diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index 314c07ae..b78b9bc0 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -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; diff --git a/src/packet/sym_encrypted_integrity_protected.js b/src/packet/sym_encrypted_integrity_protected.js index 9fcc58d4..d80a664c 100644 --- a/src/packet/sym_encrypted_integrity_protected.js +++ b/src/packet/sym_encrypted_integrity_protected.js @@ -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) { diff --git a/src/type/stream.js b/src/stream.js similarity index 53% rename from src/type/stream.js rename to src/stream.js index 467b1ee5..274a1b4a 100644 --- a/src/type/stream.js +++ b/src/stream.js @@ -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(); diff --git a/src/util.js b/src/util.js index 641d8bcd..a7b6701a 100644 --- a/src/util.js +++ b/src/util.js @@ -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]; diff --git a/test/general/packet.js b/test/general/packet.js index 367cc213..a0ea4257 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -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)) { diff --git a/test/general/streaming.js b/test/general/streaming.js index 782692a6..071892ac 100644 --- a/test/general/streaming.js +++ b/test/general/streaming.js @@ -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)); }); });