Streaming decrypt non-partial-length-encoded packets

This commit is contained in:
Daniel Huigens 2018-05-18 15:46:36 +02:00
parent fb155ffae0
commit 9fcc075f0b
3 changed files with 182 additions and 146 deletions

View File

@ -15,13 +15,18 @@
// License along with this library; if not, write to the Free Software // License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/* eslint-disable callback-return */
/** /**
* @fileoverview Functions for reading and writing packets * @fileoverview Functions for reading and writing packets
* @requires enums * @requires enums
* @requires stream
* @requires util * @requires util
* @module packet/packet * @module packet/packet
*/ */
import enums from '../enums';
import stream from '../stream';
import util from '../util'; import util from '../util';
export default { export default {
@ -110,81 +115,101 @@ export default {
return util.concatUint8Array([new Uint8Array([0x80 | (tag_type << 2) | 2]), util.writeNumber(length, 4)]); return util.concatUint8Array([new Uint8Array([0x80 | (tag_type << 2) | 2]), util.writeNumber(length, 4)]);
}, },
/**
* Whether the packet type supports partial lengths per RFC4880
* @param {Integer} tag_type Tag type
* @returns {Boolean} String of the header
*/
supportsStreaming: function(tag_type) {
return [
enums.packet.literal,
enums.packet.compressed,
enums.packet.symmetricallyEncrypted,
enums.packet.symEncryptedIntegrityProtected,
enums.packet.symEncryptedAEADProtected
].includes(tag_type);
},
/** /**
* Generic static Packet Parser function * Generic static Packet Parser function
* *
* @param {String} input Input stream as string * @param {Uint8Array} input Input stream as string
* @param {integer} position Position to start parsing * @param {Function} callback Function to call with the parsed packet
* @param {integer} len Length of the input from position on * @returns {Boolean} Returns false if the stream was empty and parsing is done, and true otherwise.
* @returns {Object} Returns a parsed module:packet/packet
*/ */
read: function(reader) { read: async function(input, callback) {
return new Promise(async (resolve, reject) => { let reader = stream.getReader(input);
let controller; let controller;
try { try {
const peekedBytes = await reader.peekBytes(2); const peekedBytes = await reader.peekBytes(2);
// some sanity checks if (!peekedBytes || !peekedBytes.length) {
if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) { return false;
reject(new Error("Error during parsing. This message / key probably does not conform to a valid OpenPGP format.")); }
return; // some sanity checks
} if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) {
const headerByte = await reader.readByte(); throw new Error("Error during parsing. This message / key probably does not conform to a valid OpenPGP format.");
let tag = -1; }
let format = -1; const headerByte = await reader.readByte();
let packet_length; let tag = -1;
let format = -1;
let packet_length;
format = 0; // 0 = old format; 1 = new format format = 0; // 0 = old format; 1 = new format
if ((headerByte & 0x40) !== 0) { if ((headerByte & 0x40) !== 0) {
format = 1; format = 1;
} }
let packet_length_type; let packet_length_type;
if (format) { if (format) {
// new format header // new format header
tag = headerByte & 0x3F; // bit 5-0 tag = headerByte & 0x3F; // bit 5-0
} else { } else {
// old format header // old format header
tag = (headerByte & 0x3F) >> 2; // bit 5-2 tag = (headerByte & 0x3F) >> 2; // bit 5-2
packet_length_type = headerByte & 0x03; // bit 1-0 packet_length_type = headerByte & 0x03; // bit 1-0
} }
let bodydata = null; let packet = null;
if (!format) { if (!format) {
// 4.2.1. Old Format Packet Lengths // 4.2.1. Old Format Packet Lengths
switch (packet_length_type) { switch (packet_length_type) {
case 0: case 0:
// The packet has a one-octet length. The header is 2 octets // The packet has a one-octet length. The header is 2 octets
// long. // long.
packet_length = await reader.readByte(); packet_length = await reader.readByte();
break; break;
case 1: case 1:
// The packet has a two-octet length. The header is 3 octets // The packet has a two-octet length. The header is 3 octets
// long. // long.
packet_length = (await reader.readByte() << 8) | await reader.readByte(); packet_length = (await reader.readByte() << 8) | await reader.readByte();
break; break;
case 2: case 2:
// The packet has a four-octet length. The header is 5 // The packet has a four-octet length. The header is 5
// octets long. // octets long.
packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() <<
8) | await reader.readByte(); 8) | await reader.readByte();
break; break;
default: default:
// 3 - The packet is of indeterminate length. The header is 1 // 3 - The packet is of indeterminate length. The header is 1
// octet long, and the implementation must determine how long // octet long, and the implementation must determine how long
// the packet is. If the packet is in a file, this means that // the packet is. If the packet is in a file, this means that
// the packet extends until the end of the file. In general, // the packet extends until the end of the file. In general,
// an implementation SHOULD NOT use indeterminate-length // an implementation SHOULD NOT use indeterminate-length
// packets except where the end of the data will be clear // packets except where the end of the data will be clear
// from the context, and even then it is better to use a // from the context, and even then it is better to use a
// definite length, or a new format header. The new format // definite length, or a new format header. The new format
// headers described below have a mechanism for precisely // headers described below have a mechanism for precisely
// encoding data of indeterminate length. // encoding data of indeterminate length.
packet_length = Infinity; packet_length = Infinity;
break; break;
} }
} else { // 4.2.2. New Format Packet Lengths } else { // 4.2.2. New Format Packet Lengths
const streaming = this.supportsStreaming(tag);
let wasPartialLength;
do {
// 4.2.2.1. One-Octet Lengths // 4.2.2.1. One-Octet Lengths
const lengthByte = await reader.readByte(); const lengthByte = await reader.readByte();
wasPartialLength = false;
if (lengthByte < 192) { if (lengthByte < 192) {
packet_length = lengthByte; packet_length = lengthByte;
// 4.2.2.2. Two-Octet Lengths // 4.2.2.2. Two-Octet Lengths
@ -193,69 +218,55 @@ export default {
// 4.2.2.4. Partial Body Lengths // 4.2.2.4. Partial Body Lengths
} else if (lengthByte > 223 && lengthByte < 255) { } else if (lengthByte > 223 && lengthByte < 255) {
packet_length = 1 << (lengthByte & 0x1F); packet_length = 1 << (lengthByte & 0x1F);
bodydata = new ReadableStream({ wasPartialLength = true;
async start(_controller) { if (!streaming) {
controller = _controller; throw new TypeError('This packet type does not support partial lengths.');
} } else if (!packet) {
}); packet = new ReadableStream({
resolve({ // eslint-disable-next-line no-loop-func
tag: tag, async start(_controller) {
packet: bodydata, controller = _controller;
done: true }
}); });
controller.enqueue(await reader.readBytes(packet_length)); callback({ tag, packet });
let tmplen;
while (true) {
const tmplenByte = await reader.readByte();
if (tmplenByte < 192) {
tmplen = tmplenByte;
controller.enqueue(await reader.readBytes(tmplen));
break;
} else if (tmplenByte >= 192 && tmplenByte < 224) {
tmplen = ((tmplenByte - 192) << 8) + (await reader.readByte()) + 192;
controller.enqueue(await reader.readBytes(tmplen));
break;
} else if (tmplenByte > 223 && tmplenByte < 255) {
tmplen = 1 << (tmplenByte & 0x1F);
controller.enqueue(await reader.readBytes(tmplen));
} else {
tmplen = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << 8) | await reader.readByte();
controller.enqueue(await reader.readBytes(tmplen));
break;
}
} }
// 4.2.2.3. Five-Octet Lengths // 4.2.2.3. Five-Octet Lengths
} else { } else {
packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << packet_length = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() <<
8) | await reader.readByte(); 8) | await reader.readByte();
} }
} if (streaming) {
if (controller) {
controller.enqueue(await reader.readBytes(packet_length));
} else {
// Send the remainder of the packet to the callback as a stream
reader.releaseLock();
packet = stream.subarray(stream.clone(input), 0, packet_length);
await callback({ tag, packet });
// if there wasn't a partial body length // Read the entire packet before parsing the next one
if (bodydata === null) { reader = stream.getReader(input);
bodydata = await reader.readBytes(packet_length); await reader.readBytes(packet_length);
}
const peekedByte = await reader.peekBytes(1);
resolve({
tag: tag,
packet: bodydata,
done: !(peekedByte && peekedByte.length)
});
} else {
const { done, value } = await reader.read();
if (!done && value.length) {
throw new Error('Packets after a packet with partial lengths are not supported');
} else {
controller.close();
} }
} } while(wasPartialLength);
} catch(e) {
if (controller) {
controller.error(e);
} else {
reject(e);
}
} }
});
if (!packet) {
packet = await reader.readBytes(packet_length);
await callback({ tag, packet });
} else if (controller) {
controller.close();
}
} catch(e) {
if (controller) {
controller.error(e);
} else {
throw e;
}
} finally {
reader.releaseLock();
}
return true;
} }
}; };

View File

@ -36,31 +36,40 @@ function List() {
* @param {Uint8Array} A Uint8Array of bytes. * @param {Uint8Array} A Uint8Array of bytes.
*/ */
List.prototype.read = async function (bytes) { List.prototype.read = async function (bytes) {
const reader = stream.getReader(bytes); this.stream = new ReadableStream({
while (true) { pull: async controller => {
const parsed = await packetParser.read(reader); if (!await packetParser.read(bytes, async parsed => {
try {
const tag = enums.read(enums.packet, parsed.tag);
const packet = packets.newPacketFromTag(tag);
packet.packets = new List();
packet.fromStream = util.isStream(parsed.packet);
await packet.read(parsed.packet);
controller.enqueue(packet);
} catch (e) {
if (!config.tolerant ||
parsed.tag === enums.packet.symmetricallyEncrypted ||
parsed.tag === enums.packet.literal ||
parsed.tag === enums.packet.compressed) {
controller.error(e);
}
util.print_debug_error(e);
}
})) {
controller.close();
}
}
});
let pushed = false; // Wait until first few packets have been read
try { const reader = stream.getReader(stream.clone(this.stream));
const tag = enums.read(enums.packet, parsed.tag); while (true) {
const packet = packets.newPacketFromTag(tag); const { done, value } = await reader.read();
this.push(packet); if (!done) {
pushed = true; this.push(value);
await packet.read(parsed.packet); }
if (parsed.done) { if (done || value.fromStream) {
break; break;
}
} catch (e) {
if (!config.tolerant ||
parsed.tag === enums.packet.symmetricallyEncrypted ||
parsed.tag === enums.packet.literal ||
parsed.tag === enums.packet.compressed) {
throw e;
}
util.print_debug_error(e);
if (pushed) {
this.pop(); // drop unsupported packet
}
} }
} }
}; };
@ -76,6 +85,9 @@ List.prototype.write = function () {
for (let i = 0; i < this.length; i++) { for (let i = 0; i < this.length; i++) {
const packetbytes = this[i].write(); const packetbytes = this[i].write();
if (util.isStream(packetbytes)) { if (util.isStream(packetbytes)) {
if (!packetParser.supportsStreaming(this[i].tag)) {
throw new Error('This packet type does not support partial lengths.');
}
let buffer = []; let buffer = [];
let bufferLength = 0; let bufferLength = 0;
const minLength = 512; const minLength = 512;

View File

@ -52,7 +52,9 @@ function transform(input, process = () => undefined, finish = () => undefined) {
function tee(input) { function tee(input) {
if (util.isStream(input)) { if (util.isStream(input)) {
return input.tee(); const teed = input.tee();
teed[0].externalBuffer = teed[1].externalBuffer = input.externalBuffer;
return teed;
} }
return [input, input]; return [input, input];
} }
@ -202,9 +204,14 @@ ReadableStream.prototype.tee = function() {
function Reader(input) { function Reader(input) {
this.stream = input;
if (input.externalBuffer) {
this.externalBuffer = input.externalBuffer.slice();
}
if (util.isStream(input)) { if (util.isStream(input)) {
const reader = input.getReader(); const reader = input.getReader();
this._read = reader.read.bind(reader); this._read = reader.read.bind(reader);
this._releaseLock = reader.releaseLock.bind(reader);
return; return;
} }
let doneReading = false; let doneReading = false;
@ -215,6 +222,7 @@ function Reader(input) {
doneReading = true; doneReading = true;
return { value: input, done: false }; return { value: input, done: false };
}; };
this._releaseLock = () => {};
} }
Reader.prototype.read = async function() { Reader.prototype.read = async function() {
@ -225,6 +233,11 @@ Reader.prototype.read = async function() {
return this._read(); return this._read();
}; };
Reader.prototype.releaseLock = function() {
this.stream.externalBuffer = this.externalBuffer;
this._releaseLock();
};
Reader.prototype.readLine = async function() { Reader.prototype.readLine = async function() {
let buffer = []; let buffer = [];
let returnVal; let returnVal;