From 129fa919b935d97d995bc6b457c7f6984c06e825 Mon Sep 17 00:00:00 2001 From: Maximilian Krambach Date: Wed, 22 Aug 2018 16:32:31 +0200 Subject: [PATCH] js: improve decryption performance -- * src/Connection.js, src/Helpers.js: performance of decoding incoming base64 data was improved to about 4 times the speed by introducing two more efficient functions (thanks to rrenkert@intevation.de for finding and testing them) * src/gpgmejs.js: Decrypted data will now return as Uint8Array, if the caller does not wish for a decoding. Decoding binary data will return invalid data, and a Uint8Array may be desired. This can be indicated by using the (new) 'binary' option in decrypt. * src/Errors.js A new error in case this decoding fails * src/Message.js, src/Connection.js: expected is change from base64 to binary, to avoid confusion later on. --- lang/js/src/Connection.js | 24 +++++++------ lang/js/src/Errors.js | 4 +++ lang/js/src/Helpers.js | 71 ++++++++++++++++++++++++++++++++++++++- lang/js/src/Message.js | 2 +- lang/js/src/gpgmejs.js | 18 ++++++---- 5 files changed, 100 insertions(+), 19 deletions(-) diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js index 928ac681..8756cce1 100644 --- a/lang/js/src/Connection.js +++ b/lang/js/src/Connection.js @@ -26,7 +26,7 @@ import { permittedOperations } from './permittedOperations'; import { gpgme_error } from './Errors'; import { GPGME_Message, createMessage } from './Message'; -import { decode } from './Helpers'; +import { decode, atobArray, Utf8ArrayToStr } from './Helpers'; /** * A Connection handles the nativeMessaging interaction via a port. As the @@ -223,8 +223,9 @@ class Answer{ } } /** - * Returns the base64 encoded answer data with the content verified - * against {@link permittedOperations}. + * Decodes and verifies the base64 encoded answer data. Verified against + * {@link permittedOperations}. + * @returns {Object} The readable gpnupg answer */ getMessage (){ if (this._response_b64 === null){ @@ -264,14 +265,15 @@ class Answer{ } if (_decodedResponse.base64 === true && poa.data[key] === 'string' - && this.expected !== 'base64' - ){ - _response[key] = decodeURIComponent( - atob(_decodedResponse[key]).split('').map( - function (c) { - return '%' + - ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); + ) { + if (this.expected === 'binary'){ + _response[key] = atobArray(_decodedResponse[key]); + _response.binary = true; + } else { + _response[key] = Utf8ArrayToStr( + atobArray(_decodedResponse[key])); + _response.binary = false; + } } else { _response[key] = decode(_decodedResponse[key]); } diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js index 73418028..145c3a59 100644 --- a/lang/js/src/Errors.js +++ b/lang/js/src/Errors.js @@ -103,6 +103,10 @@ export const err_list = { msg: 'Invalid parameter was found', type: 'error' }, + 'DECODE_FAIL': { + msg: 'Decoding failed due to unexpected data', + type: 'error' + }, 'PARAM_IGNORED': { msg: 'An parameter was set that has no effect in gpgmejs', type: 'warning' diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js index ba4277ab..9fa5775b 100644 --- a/lang/js/src/Helpers.js +++ b/lang/js/src/Helpers.js @@ -134,4 +134,73 @@ export function decode (property){ return property; } return property; -} \ No newline at end of file +} + +/** + * Turns a base64 encoded string into an uint8 array + * @param {String} base64 encoded String + * @returns {Uint8Array} + * adapted from https://gist.github.com/borismus/1032746 + */ +export function atobArray (base64) { + if (typeof (base64) !== 'string'){ + throw gpgme_error('DECODE_FAIL'); + } + const raw = window.atob(base64); + const rawLength = raw.length; + let array = new Uint8Array(new ArrayBuffer(rawLength)); + for (let i = 0; i < rawLength; i++) { + array[i] = raw.charCodeAt(i); + } + return array; +} + +/** + * Turns a Uint8Array into an utf8-String + * @param {*} array Uint8Array + * @returns {String} + * Taken and slightly adapted from + * http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt + * (original header: + * utf.js - UTF-8 <=> UTF-16 convertion + * + * Copyright (C) 1999 Masanao Izumo + * Version: 1.0 + * LastModified: Dec 25 1999 + * This library is free. You can redistribute it and/or modify it. + * ) + */ +export function Utf8ArrayToStr (array) { + let out, i, len, c, char2, char3; + out = ''; + len = array.length; + i = 0; + if (array instanceof Uint8Array === false){ + throw gpgme_error('DECODE_FAIL'); + } + while (i < len) { + c = array[i++]; + switch (c >> 4) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += String.fromCharCode(c); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = array[i++]; + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = array[i++]; + char3 = array[i++]; + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; + default: + break; + } + } + return out; +} diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js index b83caf6d..48813df7 100644 --- a/lang/js/src/Message.js +++ b/lang/js/src/Message.js @@ -64,7 +64,7 @@ export class GPGME_Message { } set expected (value){ - if (value === 'base64'){ + if (value === 'binary'){ this._expected = value; } } diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js index 7692298f..513e4a56 100644 --- a/lang/js/src/gpgmejs.js +++ b/lang/js/src/gpgmejs.js @@ -30,8 +30,8 @@ import { createSignature } from './Signature'; /** * @typedef {Object} decrypt_result - * @property {String} data The decrypted data - * @property {Boolean} base64 indicating whether data is base64 encoded. + * @property {String|Uint8Array} data The decrypted data + * @property {Boolean} binary indicating whether data is an Uint8Array. * @property {Boolean} is_mime (optional) the data claims to be a MIME * object. * @property {String} file_name (optional) the original file name @@ -51,7 +51,8 @@ import { createSignature } from './Signature'; /** * @typedef {Object} encrypt_result The result of an encrypt operation * @property {String} data The encrypted message - * @property {Boolean} base64 Indicating whether data is base64 encoded. + * @property {Boolean} binary Indicating whether returning payload data is an + * Uint8Array. */ /** @@ -174,10 +175,12 @@ export class GpgME { * Strings and Objects with a getText method * @param {Boolean} base64 (optional) false if the data is an armored * block, true if it is base64 encoded binary data + * @param {Boolean} binary (optional) if true, treat the decoded data as + * binary, and return the data as Uint8Array * @returns {Promise} Decrypted Message and information * @async */ - decrypt (data, base64=false){ + decrypt (data, base64=false, binary){ if (data === undefined){ return Promise.reject(gpgme_error('MSG_EMPTY')); } @@ -189,11 +192,14 @@ export class GpgME { if (base64 === true){ msg.setParameter('base64', true); } + if (binary === true){ + msg.expected = 'binary'; + } putData(msg, data); return new Promise(function (resolve, reject){ msg.post().then(function (result){ let _result = { data: result.data }; - _result.base64 = result.base64 ? true: false; + _result.binary = result.binary ? true: false; if (result.hasOwnProperty('dec_info')){ _result.is_mime = result.dec_info.is_mime ? true: false; if (result.dec_info.file_name) { @@ -251,7 +257,7 @@ export class GpgME { putData(msg, data); return new Promise(function (resolve,reject) { if (mode ==='detached'){ - msg.expected ='base64'; + msg.expected ='binary'; } msg.post().then( function (message) { if (mode === 'clearsign'){