Backpressure and cancellation in sign/verify

This commit is contained in:
Daniel Huigens 2018-06-21 18:25:28 +02:00
parent d2ba6b3c6c
commit 0db32bea39
5 changed files with 254 additions and 24 deletions

View File

@ -283,14 +283,17 @@ function dearmor(input) {
await stream.pipe(readable, writable, {
preventClose: true
});
const checksumVerifiedString = await stream.readToEnd(checksumVerified);
const writer = stream.getWriter(writable);
await writer.ready;
if (checksum !== checksumVerifiedString && (checksum || config.checksum_required)) {
await writer.abort(new Error("Ascii armor integrity check on message failed: '" + checksum + "' should be '" +
checksumVerifiedString + "'"));
} else {
try {
const checksumVerifiedString = await stream.readToEnd(checksumVerified);
if (checksum !== checksumVerifiedString && (checksum || config.checksum_required)) {
throw new Error("Ascii armor integrity check on message failed: '" + checksum + "' should be '" +
checksumVerifiedString + "'");
}
await writer.ready;
await writer.close();
} catch(e) {
await writer.abort(e);
}
});
} catch(e) {

View File

@ -297,9 +297,8 @@ export function encryptKey({ privateKey, passphrase }) {
* @async
* @static
*/
export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, asStream, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date(), fromUserId={}, toUserId={} }) {
export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, asStream=util.isStream(data), detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date(), fromUserId={}, toUserId={} }) {
checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords);
if (asStream === undefined) asStream = util.isStream(data);
if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported
return asyncProxy.delegate('encrypt', { data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, asStream, detached, signature, returnSessionKey, wildcard, date, fromUserId, toUserId });
@ -352,9 +351,8 @@ export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, se
* @async
* @static
*/
export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKeys, format='utf8', asStream, signature=null, date=new Date() }) {
export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKeys, format='utf8', asStream=message.fromStream, signature=null, date=new Date() }) {
checkMessage(message); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords); sessionKeys = toArray(sessionKeys);
if (asStream === undefined) asStream = message.fromStream;
if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported
return asyncProxy.delegate('decrypt', { message, privateKeys, passwords, sessionKeys, publicKeys, format, asStream, signature, date });
@ -416,10 +414,9 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe
* @async
* @static
*/
export function sign({ data, dataType, privateKeys, armor=true, asStream, detached=false, date=new Date(), fromUserId={} }) {
export function sign({ data, dataType, privateKeys, armor=true, asStream=util.isStream(data), detached=false, date=new Date(), fromUserId={} }) {
checkData(data);
privateKeys = toArray(privateKeys);
if (asStream === undefined) asStream = util.isStream(data);
if (asyncProxy) { // use web worker if available
return asyncProxy.delegate('sign', {
@ -459,10 +456,9 @@ export function sign({ data, dataType, privateKeys, armor=true, asStream, detach
* @async
* @static
*/
export function verify({ message, publicKeys, asStream, signature=null, date=new Date() }) {
export function verify({ message, publicKeys, asStream=message.fromStream, signature=null, date=new Date() }) {
checkCleartextOrMessage(message);
publicKeys = toArray(publicKeys);
if (asStream === undefined) asStream = message.fromStream;
if (asyncProxy) { // use web worker if available
return asyncProxy.delegate('verify', { message, publicKeys, asStream, signature, date });
@ -470,10 +466,27 @@ export function verify({ message, publicKeys, asStream, signature=null, date=new
return Promise.resolve().then(async function() {
const result = {};
result.signatures = signature ? await message.verifyDetached(signature, publicKeys, date) : await message.verify(publicKeys, date, asStream);
const signatures = signature ? await message.verifyDetached(signature, publicKeys, date) : await message.verify(publicKeys, date, asStream);
result.data = message instanceof CleartextMessage ? message.getText() : message.getLiteralData();
result.data = await convertStream(result.data, asStream);
result.signatures = stream.readToEnd(result.signatures, arr => arr);
if (asStream) {
result.data = stream.transformPair(signatures, async (readable, writable) => {
const signatures = stream.readToEnd(readable, arr => arr);
result.signatures = signatures.catch(() => []);
await stream.pipe(result.data, writable, {
preventClose: true
});
const writer = stream.getWriter(writable);
try {
await signatures;
await writer.close();
} catch(e) {
await writer.abort(e);
}
});
} else {
result.signatures = await stream.readToEnd(signatures, arr => arr);
}
return result;
}).catch(onError.bind(null, 'Error verifying cleartext signed message'));
}

View File

@ -266,7 +266,7 @@ export default {
return done || !value || !value.length;
} catch(e) {
if (writer) {
writer.abort(e);
await writer.abort(e);
return true;
} else {
throw e;

View File

@ -41,15 +41,19 @@ async function pipe(input, target, options) {
if (!util.isStream(input)) {
input = toStream(input);
}
if (input.externalBuffer) {
const writer = target.getWriter();
for (let i = 0; i < input.externalBuffer.length; i++) {
await writer.ready;
writer.write(input.externalBuffer[i]);
try {
if (input.externalBuffer) {
const writer = target.getWriter();
for (let i = 0; i < input.externalBuffer.length; i++) {
await writer.ready;
await writer.write(input.externalBuffer[i]);
}
writer.releaseLock();
}
writer.releaseLock();
return await input.pipeTo(target, options).catch(function() {});
} catch(e) {
util.print_debug_error(e);
}
return input.pipeTo(target, options).catch(function() {});
}
function transformRaw(input, options) {
@ -184,6 +188,7 @@ function passiveClone(input) {
await writer.write(value);
}
} catch(e) {
controller.error(e);
await writer.abort(e);
}
});

View File

@ -159,6 +159,40 @@ describe('Streaming', function() {
expect(canceled).to.be.true;
});
it('Sign: Input stream should be canceled when canceling encrypted stream', async function() {
const privKey = (await openpgp.key.readArmored(priv_key)).keys[0];
await privKey.decrypt(passphrase);
let plaintext = [];
let i = 0;
let canceled = false;
const data = new ReadableStream({
async pull(controller) {
await new Promise(setTimeout);
if (i++ < 10) {
let randomBytes = await openpgp.crypto.random.getRandomBytes(1024);
controller.enqueue(randomBytes);
plaintext.push(randomBytes);
} else {
controller.close();
}
},
cancel() {
canceled = true;
}
});
const encrypted = await openpgp.sign({
data,
privateKeys: privKey
});
const reader = openpgp.stream.getReader(encrypted.data);
expect(await reader.readBytes(1024)).to.match(/^-----BEGIN PGP MESSAGE-----\r\n/);
if (i > 10) throw new Error('Data did not arrive early.');
reader.releaseLock();
await openpgp.stream.cancel(encrypted.data);
expect(canceled).to.be.true;
});
it('Encrypt and decrypt larger message roundtrip', async function() {
let plaintext = [];
let i = 0;
@ -418,6 +452,52 @@ describe('Streaming', function() {
}
});
it('Sign/verify: Detect armor checksum error (unsafe_stream=true)', async function() {
let unsafe_streamValue = openpgp.config.unsafe_stream;
openpgp.config.unsafe_stream = true;
try {
const pubKey = (await openpgp.key.readArmored(pub_key)).keys[0];
const privKey = (await openpgp.key.readArmored(priv_key)).keys[0];
await privKey.decrypt(passphrase);
let plaintext = [];
let i = 0;
const data = new ReadableStream({
async pull(controller) {
await new Promise(resolve => setTimeout(resolve, 100));
if (i++ < 10) {
let randomBytes = await openpgp.crypto.random.getRandomBytes(1024);
controller.enqueue(randomBytes);
plaintext.push(randomBytes);
} else {
controller.close();
}
}
});
const encrypted = await openpgp.sign({
data,
privateKeys: privKey
});
const msgAsciiArmored = encrypted.data;
const message = await openpgp.message.readArmored(openpgp.stream.transform(msgAsciiArmored, value => {
if (value.length > 1000) return value.slice(0, 499) + 'a' + value.slice(500);
return value;
}));
const decrypted = await openpgp.verify({
publicKeys: pubKey,
message
});
expect(util.isStream(decrypted.data)).to.be.true;
expect(await openpgp.stream.getReader(openpgp.stream.clone(decrypted.data)).readBytes(10)).not.to.deep.equal(plaintext[0]);
if (i > 10) throw new Error('Data did not arrive early.');
await expect(openpgp.stream.readToEnd(decrypted.data)).to.be.rejectedWith('Ascii armor integrity check on message failed');
expect(await decrypted.signatures).to.exist.and.have.length(0);
} finally {
openpgp.config.unsafe_stream = unsafe_streamValue;
}
});
it('Encrypt and decrypt larger message roundtrip (draft04)', async function() {
let aead_protectValue = openpgp.config.aead_protect;
let aead_chunk_size_byteValue = openpgp.config.aead_chunk_size_byte;
@ -551,6 +631,59 @@ describe('Streaming', function() {
}
});
it('Sign/verify: Input stream should be canceled when canceling decrypted stream (draft04)', async function() {
let aead_protectValue = openpgp.config.aead_protect;
let aead_chunk_size_byteValue = openpgp.config.aead_chunk_size_byte;
openpgp.config.aead_protect = true;
openpgp.config.aead_chunk_size_byte = 4;
try {
const pubKey = (await openpgp.key.readArmored(pub_key)).keys[0];
const privKey = (await openpgp.key.readArmored(priv_key)).keys[0];
await privKey.decrypt(passphrase);
let plaintext = [];
let i = 0;
let canceled = false;
const data = new ReadableStream({
async pull(controller) {
await new Promise(resolve => setTimeout(resolve, 10));
if (i++ < 10) {
let randomBytes = await openpgp.crypto.random.getRandomBytes(1024);
controller.enqueue(randomBytes);
plaintext.push(randomBytes);
} else {
controller.close();
}
},
cancel() {
canceled = true;
}
});
const encrypted = await openpgp.sign({
data,
privateKeys: privKey
});
const msgAsciiArmored = encrypted.data;
const message = await openpgp.message.readArmored(msgAsciiArmored);
const decrypted = await openpgp.verify({
publicKeys: pubKey,
message
});
expect(util.isStream(decrypted.data)).to.be.true;
const reader = openpgp.stream.getReader(decrypted.data);
expect(await reader.readBytes(1024)).to.deep.equal(plaintext[0]);
if (i > 10) throw new Error('Data did not arrive early.');
reader.releaseLock();
await openpgp.stream.cancel(decrypted.data, new Error('canceled by test'));
expect(canceled).to.be.true;
expect(await decrypted.signatures).to.exist.and.have.length(0);
} finally {
openpgp.config.aead_protect = aead_protectValue;
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteValue;
}
});
it("Don't pull entire input stream when we're not pulling encrypted stream", async function() {
let plaintext = [];
let i = 0;
@ -577,6 +710,36 @@ describe('Streaming', function() {
expect(i).to.be.lessThan(50);
});
it("Sign: Don't pull entire input stream when we're not pulling signed stream", async function() {
const pubKey = (await openpgp.key.readArmored(pub_key)).keys[0];
const privKey = (await openpgp.key.readArmored(priv_key)).keys[0];
await privKey.decrypt(passphrase);
let plaintext = [];
let i = 0;
const data = new ReadableStream({
async pull(controller) {
if (i++ < 100) {
let randomBytes = await openpgp.crypto.random.getRandomBytes(1024);
controller.enqueue(randomBytes);
plaintext.push(randomBytes);
} else {
controller.close();
}
await new Promise(setTimeout);
}
});
const encrypted = await openpgp.sign({
data,
privateKeys: privKey
});
const reader = openpgp.stream.getReader(encrypted.data);
expect(await reader.readBytes(1024)).to.match(/^-----BEGIN PGP MESSAGE-----\r\n/);
if (i > 10) throw new Error('Data did not arrive early.');
await new Promise(resolve => setTimeout(resolve, 3000));
expect(i).to.be.lessThan(50);
});
it("Don't pull entire input stream when we're not pulling decrypted stream (draft04)", async function() {
let aead_protectValue = openpgp.config.aead_protect;
let aead_chunk_size_byteValue = openpgp.config.aead_chunk_size_byte;
@ -619,4 +782,50 @@ describe('Streaming', function() {
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteValue;
}
});
it("Sign/verify: Don't pull entire input stream when we're not pulling verified stream (draft04)", async function() {
let aead_protectValue = openpgp.config.aead_protect;
let aead_chunk_size_byteValue = openpgp.config.aead_chunk_size_byte;
openpgp.config.aead_protect = true;
openpgp.config.aead_chunk_size_byte = 4;
try {
const pubKey = (await openpgp.key.readArmored(pub_key)).keys[0];
const privKey = (await openpgp.key.readArmored(priv_key)).keys[0];
await privKey.decrypt(passphrase);
let plaintext = [];
let i = 0;
const data = new ReadableStream({
async pull(controller) {
if (i++ < 100) {
let randomBytes = await openpgp.crypto.random.getRandomBytes(1024);
controller.enqueue(randomBytes);
plaintext.push(randomBytes);
} else {
controller.close();
}
await new Promise(setTimeout);
}
});
const encrypted = await openpgp.sign({
data,
privateKeys: privKey
});
const msgAsciiArmored = encrypted.data;
const message = await openpgp.message.readArmored(msgAsciiArmored);
const decrypted = await openpgp.verify({
publicKeys: pubKey,
message
});
expect(util.isStream(decrypted.data)).to.be.true;
const reader = openpgp.stream.getReader(decrypted.data);
expect(await reader.readBytes(1024)).to.deep.equal(plaintext[0]);
if (i > 10) throw new Error('Data did not arrive early.');
await new Promise(resolve => setTimeout(resolve, 3000));
expect(i).to.be.lessThan(50);
} finally {
openpgp.config.aead_protect = aead_protectValue;
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteValue;
}
});
});