js: change chunksize handling and decoding

--

* the nativeApp now sends all data in one base64-encoded string, which
  needs reassembly, but in a much easier way now.

* there are some new performance problems now, especially with
  decrypting data
This commit is contained in:
Maximilian Krambach 2018-06-08 17:54:58 +02:00
parent 7a072270ac
commit c072675f3f
7 changed files with 174 additions and 201 deletions

View File

@ -152,7 +152,7 @@ describe('Encryption and Decryption', function () {
let prm = Gpgmejs.init();
prm.then(function (context) {
context.encrypt(b64data,
inputvalues.encrypt.good.fingerprint).then(
inputvalues.encrypt.good.fingerprint, true).then(
function (answer) {
expect(answer).to.not.be.empty;
expect(answer.data).to.be.a('string');
@ -185,7 +185,7 @@ describe('Encryption and Decryption', function () {
'BEGIN PGP MESSAGE');
expect(answer.data).to.include(
'END PGP MESSAGE');
context.decrypt(answer.data, true).then(
context.decrypt(answer.data).then(
function (result) {
expect(result).to.not.be.empty;
expect(result.data).to.be.a('string');
@ -196,31 +196,4 @@ describe('Encryption and Decryption', function () {
});
}).timeout(3000);
it('Random data, input and output as base64', function (done) {
let data = bigBoringString(0.0001);
let b64data = btoa(data);
let prm = Gpgmejs.init();
prm.then(function (context) {
context.encrypt(b64data,
inputvalues.encrypt.good.fingerprint).then(
function (answer) {
expect(answer).to.not.be.empty;
expect(answer.data).to.be.a('string');
expect(answer.data).to.include(
'BEGIN PGP MESSAGE');
expect(answer.data).to.include(
'END PGP MESSAGE');
context.decrypt(answer.data, true).then(
function (result) {
expect(result).to.not.be.empty;
expect(result.data).to.be.a('string');
expect(result.data).to.equal(b64data);
done();
});
});
});
}).timeout(3000);
});

View File

@ -24,7 +24,7 @@
/* global bigString, inputvalues */
describe('Long running Encryption/Decryption', function () {
for (let i=0; i < 100; i++) {
for (let i=0; i < 101; i++) {
it('Successful encrypt/decrypt completely random data ' +
(i+1) + '/100', function (done) {
let prm = Gpgmejs.init();
@ -43,30 +43,32 @@ describe('Long running Encryption/Decryption', function () {
function(result){
expect(result).to.not.be.empty;
expect(result.data).to.be.a('string');
/*
if (result.data.length !== data.length) {
// console.log('diff: ' +
// (result.data.length - data.length));
console.log('diff: ' +
(result.data.length - data.length));
for (let i=0; i < result.data.length; i++){
if (result.data[i] !== data[i]){
// console.log('position: ' + i);
// console.log('result : ' +
// result.data.charCodeAt(i) +
// result.data[i-2] +
// result.data[i-1] +
// result.data[i] +
// result.data[i+1] +
// result.data[i+2]);
// console.log('original: ' +
// data.charCodeAt(i) +
// data[i-2] +
// data[i-1] +
// data[i] +
// data[i+1] +
// data[i+2]);
console.log('position: ' + i);
console.log('result : ' +
result.data.charCodeAt(i) +
result.data[i-2] +
result.data[i-1] +
result.data[i] +
result.data[i+1] +
result.data[i+2]);
console.log('original: ' +
data.charCodeAt(i) +
data[i-2] +
data[i-1] +
data[i] +
data[i+1] +
data[i+2]);
break;
}
}
}
*/
expect(result.data).to.equal(data);
done();
});

View File

@ -108,6 +108,7 @@ export class Connection{
return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
}
let me = this;
let chunksize = message.chunksize;
return new Promise(function(resolve, reject){
let answer = new Answer(message);
let listener = function(msg) {
@ -115,22 +116,27 @@ export class Connection{
me._connection.onMessage.removeListener(listener);
me._connection.disconnect();
reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
} else if (msg.type === 'error'){
me._connection.onMessage.removeListener(listener);
me._connection.disconnect();
reject(gpgme_error('GNUPG_ERROR', msg.msg));
} else {
let answer_result = answer.add(msg);
let answer_result = answer.collect(msg);
if (answer_result !== true){
me._connection.onMessage.removeListener(listener);
me._connection.disconnect();
reject(answer_result);
} else if (msg.more === true){
me._connection.postMessage({'op': 'getmore'});
} else {
me._connection.onMessage.removeListener(listener);
me._connection.disconnect();
resolve(answer.message);
if (msg.more === true){
me._connection.postMessage({
'op': 'getmore',
'chunksize': chunksize
});
} else {
me._connection.onMessage.removeListener(listener);
me._connection.disconnect();
if (answer.message instanceof Error){
reject(answer.message);
} else {
resolve(answer.message);
}
}
}
}
};
@ -170,19 +176,32 @@ class Answer{
constructor(message){
this.operation = message.operation;
this.expected = message.expected;
this.expect = message.expect;
}
/**
* Add the information to the answer
* @param {Object} msg The message as received with nativeMessaging
* returns true if successfull, gpgme_error otherwise
*/
add(msg){
if (this._response === undefined){
this._response = {};
collect(msg){
if (typeof(msg) !== 'object' || !msg.hasOwnProperty('response')) {
return gpgme_error('CONN_UNEXPECTED_ANSWER');
}
let messageKeys = Object.keys(msg);
if (this._responseb64 === undefined){
//this._responseb64 = [msg.response];
this._responseb64 = msg.response;
return true;
} else {
//this._responseb64.push(msg.response);
this._responseb64 += msg.response;
return true;
}
}
get message(){
if (this._responseb64 === undefined){
return gpgme_error('CONN_UNEXPECTED_ANSWER');
}
// let _decodedResponse = JSON.parse(atob(this._responseb64.join('')));
let _decodedResponse = JSON.parse(atob(this._responseb64));
let _response = {};
let messageKeys = Object.keys(_decodedResponse);
let poa = permittedOperations[this.operation].answer;
if (messageKeys.length === 0){
return gpgme_error('CONN_UNEXPECTED_ANSWER');
@ -191,80 +210,42 @@ class Answer{
let key = messageKeys[i];
switch (key) {
case 'type':
if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
if (_decodedResponse.type === 'error'){
return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
} else if (poa.type.indexOf(_decodedResponse.type) < 0){
return gpgme_error('CONN_UNEXPECTED_ANSWER');
}
break;
case 'more':
case 'base64':
break;
case 'msg':
if (_decodedResponse.type === 'error'){
return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
}
break;
default:
//data should be concatenated
if (poa.data.indexOf(key) >= 0){
if (!this._response.hasOwnProperty(key)){
this._response[key] = '';
}
this._response[key] += msg[key];
}
//params should not change through the message
else if (poa.params.indexOf(key) >= 0){
if (!this._response.hasOwnProperty(key)){
this._response[key] = msg[key];
}
else if (this._response[key] !== msg[key]){
return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]);
}
}
//infos may be json objects etc. Not yet defined.
// Pushing them into arrays for now
else if (poa.infos.indexOf(key) >= 0){
if (!this._response.hasOwnProperty(key)){
this._response[key] = [];
}
if (Array.isArray(msg[key])) {
for (let i=0; i< msg[key].length; i++) {
this._response[key].push(msg[key][i]);
}
} else {
this._response[key].push(msg[key]);
}
}
else {
if (!poa.data.hasOwnProperty(key)){
return gpgme_error('CONN_UNEXPECTED_ANSWER');
}
if( typeof(_decodedResponse[key]) !== poa.data[key] ){
return gpgme_error('CONN_UNEXPECTED_ANSWER');
}
if (_decodedResponse.base64 === true
&& poa.data[key] === 'string'
&& this.expect === undefined
){
_response[key] = decodeURIComponent(
atob(_decodedResponse[key]).split('').map(
function(c) {
return '%' +
('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
} else {
_response[key] = _decodedResponse[key];
}
break;
}
}
return true;
}
/**
* @returns {Object} the assembled message, original data assumed to be
* (javascript-) strings
*/
get message(){
let keys = Object.keys(this._response);
let msg = {};
let poa = permittedOperations[this.operation].answer;
for (let i=0; i < keys.length; i++) {
if (poa.data.indexOf(keys[i]) >= 0
&& this._response.base64 === true
) {
msg[keys[i]] = atob(this._response[keys[i]]);
if (this.expected === 'base64'){
msg[keys[i]] = this._response[keys[i]];
} else {
msg[keys[i]] = decodeURIComponent(
atob(this._response[keys[i]]).split('').map(
function(c) {
return '%' +
('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
} else {
msg[keys[i]] = this._response[keys[i]];
}
}
return msg;
return _response;
}
}

View File

@ -46,7 +46,6 @@ export class GPGME_Message {
constructor(operation){
this.operation = operation;
this._expected = 'string';
}
set operation (op){
@ -59,24 +58,50 @@ export class GPGME_Message {
}
}
}
get operation(){
return this._msg.op;
}
set expected(string){
if (string === 'base64'){
this._expected = 'base64';
/**
* Set the maximum size of responses from gpgme in bytes. Values allowed
* range from 10kB to 1MB. The lower limit is arbitrary, the upper limit
* fixed by browsers' nativeMessaging specifications
*/
set chunksize(value){
if (
Number.isInteger(value) &&
value > 10 * 1024 &&
value <= 1024 * 1024
){
this._chunksize = value;
}
}
get chunksize(){
if (this._chunksize === undefined){
return 1024 * 1023;
} else {
return this._chunksize;
}
}
get expected() {
if (this._expected === 'base64'){
return this._expected;
/**
* If expect is set to 'base64', the response is expected to be base64
* encoded binary
*/
set expect(value){
if (value ==='base64'){
this._expect = value;
}
return 'string';
}
get expect(){
if ( this._expect === 'base64'){
return this._expect;
}
return undefined;
}
/**
* Sets a parameter for the message. Note that the operation has to be set
* first, to be able to check if the parameter is permittted
@ -188,6 +213,7 @@ export class GPGME_Message {
*/
get message(){
if (this.isComplete === true){
this._msg.chunksize = this.chunksize;
return this._msg;
}
else {
@ -201,10 +227,13 @@ export class GPGME_Message {
return new Promise(function(resolve, reject) {
if (me.isComplete === true) {
let conn = new Connection;
if (me._msg.chunksize === undefined){
me._msg.chunksize = 1023*1024;
}
conn.post(me).then(function(response) {
resolve(response);
}, function(reason) {
reject(gpgme_error('GNUPG_ERROR', reason));
reject(reason);
});
}
else {

View File

@ -57,8 +57,8 @@ export class GpgME {
* Keys used to encrypt the message
* @param {GPGME_Key|String|Array<String>|Array<GPGME_Key>} secretKeys
* (optional) Keys used to sign the message
* @param {Boolean} base64 (optional) The data is already considered to be
* in base64 encoding
* @param {Boolean} base64 (optional) The data will be interpreted as
* base64 encoded data
* @param {Boolean} armor (optional) Request the output as armored block
* @param {Boolean} wildcard (optional) If true, recipient information will
* not be added to the message
@ -109,24 +109,20 @@ export class GpgME {
* Decrypt a Message
* @param {String|Object} data text/data to be decrypted. Accepts Strings
* and Objects with a getText method
* @param {Boolean} base64 (optional) Response is expected to be base64
* encoded
* @returns {Promise<Object>} decrypted message:
data: The decrypted data. This may be base64 encoded.
data: The decrypted data.
base64: Boolean indicating whether data is base64 encoded.
mime: A Boolean indicating whether the data is a MIME object.
signatures: Array of signature Objects TODO not yet implemented.
// should be an object that can tell if all signatures are valid .
// should be an object that can tell if all signatures are valid.
* @async
*/
decrypt(data, base64=false){
decrypt(data){
if (data === undefined){
return Promise.reject(gpgme_error('MSG_EMPTY'));
}
let msg = createMessage('decrypt');
if (base64 === true){
msg.expected = 'base64';
}
if (msg instanceof Error){
return Promise.reject(msg);
}
@ -165,10 +161,10 @@ export class GpgME {
}
msg.setParameter('mode', mode);
putData(msg, data);
if (mode === 'detached') {
msg.expected = 'base64';
}
return new Promise(function(resolve,reject) {
if (mode ==='detached'){
msg.expect= 'base64';
}
msg.post().then( function(message) {
if (mode === 'clearsign'){
resolve({

View File

@ -37,11 +37,8 @@
5000 ms would be too short
answer: <Object>
type: <String< The content type of answer expected
data: Array<String> The payload property of the answer. May be
partial and in need of concatenation
params: Array<String> Information that do not change throughout
the message
infos: Array<*> arbitrary information that may result in a list
data: <Object>
the properties expected and their type, eg: {'data':'string'}
}
}
*/
@ -67,9 +64,6 @@ export const permittedOperations = {
allowed: ['string'],
array_allowed: true
},
'chunksize': {
allowed: ['number']
},
'base64': {
allowed: ['boolean']
},
@ -101,9 +95,10 @@ export const permittedOperations = {
},
answer: {
type: ['ciphertext'],
data: ['data'],
params: ['base64'],
infos: []
data: {
'data': 'string',
'base64':'boolean'
}
}
},
@ -119,18 +114,18 @@ export const permittedOperations = {
allowed: ['string'],
allowed_data: ['cms', 'openpgp']
},
'chunksize': {
allowed: ['number'],
},
'base64': {
allowed: ['boolean']
}
},
answer: {
type: ['plaintext'],
data: ['data'],
params: ['base64', 'mime'],
infos: ['signatures']
data: {
'data': 'string',
'base64': 'boolean',
'mime': 'boolean',
'signatures': 'object'
}
}
},
@ -149,9 +144,6 @@ export const permittedOperations = {
allowed: ['string'],
allowed_data: ['cms', 'openpgp']
},
'chunksize': {
allowed: ['number'],
},
'sender': {
allowed: ['string'],
},
@ -169,10 +161,11 @@ export const permittedOperations = {
},
answer: {
type: ['signature', 'ciphertext'],
data: ['data'], // Unless armor mode is used a Base64 encoded binary
// signature. In armor mode a string with an armored
// OpenPGP or a PEM message.
params: ['base64']
data: {
'data': 'string',
'base64':'boolean'
}
}
},
@ -186,9 +179,6 @@ export const permittedOperations = {
allowed: ['string'],
allowed_data: ['cms', 'openpgp']
},
'chunksize': {
allowed: ['number'],
},
'secret': {
allowed: ['boolean']
},
@ -220,9 +210,10 @@ export const permittedOperations = {
},
answer: {
type: ['keys'],
data: [],
params: ['base64'],
infos: ['keys']
data: {
'base64': 'boolean',
'keys': 'object'
}
}
},
@ -233,9 +224,6 @@ export const permittedOperations = {
allowed: ['string'],
allowed_data: ['cms', 'openpgp']
},
'chunksize': {
allowed: ['number'],
},
'keys': {
allowed: ['string'],
array_allowed: true
@ -259,8 +247,10 @@ export const permittedOperations = {
},
answer: {
type: ['keys'],
data: ['data'],
params: ['base64']
data: {
'data': 'string',
'base64': 'boolean'
}
}
},
@ -280,10 +270,10 @@ export const permittedOperations = {
},
},
answer: {
infos: ['result'],
type: [],
data: [],
params: []
data: {
'result': 'Object'
}
}
},
@ -299,15 +289,15 @@ export const permittedOperations = {
allowed: ['string'],
allowed_data: ['cms', 'openpgp']
},
// 'secret': { not yet implemented
// 'secret': { not implemented
// allowed: ['boolean']
// }
},
answer: {
data: [],
params:['success'],
infos: []
data: {
'success': 'boolean'
}
}
},
@ -316,9 +306,10 @@ export const permittedOperations = {
optional: {},
answer: {
type: [''],
data: ['gpgme'],
infos: ['info'],
params:[]
data: {
'gpgme': 'string',
'info': 'object'
}
}
}

View File

@ -339,7 +339,8 @@ function unittests (){
test0.setParameter('keys', hp.validFingerprints);
expect(test0.message).to.not.be.null;
expect(test0.message).to.have.keys('op', 'data', 'keys');
expect(test0.message).to.have.keys('op', 'data', 'keys',
'chunksize');
expect(test0.message.op).to.equal('encrypt');
expect(test0.message.data).to.equal(
mp.valid_encrypt_data);