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(); let prm = Gpgmejs.init();
prm.then(function (context) { prm.then(function (context) {
context.encrypt(b64data, context.encrypt(b64data,
inputvalues.encrypt.good.fingerprint).then( inputvalues.encrypt.good.fingerprint, true).then(
function (answer) { function (answer) {
expect(answer).to.not.be.empty; expect(answer).to.not.be.empty;
expect(answer.data).to.be.a('string'); expect(answer.data).to.be.a('string');
@ -185,7 +185,7 @@ describe('Encryption and Decryption', function () {
'BEGIN PGP MESSAGE'); 'BEGIN PGP MESSAGE');
expect(answer.data).to.include( expect(answer.data).to.include(
'END PGP MESSAGE'); 'END PGP MESSAGE');
context.decrypt(answer.data, true).then( context.decrypt(answer.data).then(
function (result) { function (result) {
expect(result).to.not.be.empty; expect(result).to.not.be.empty;
expect(result.data).to.be.a('string'); expect(result.data).to.be.a('string');
@ -196,31 +196,4 @@ describe('Encryption and Decryption', function () {
}); });
}).timeout(3000); }).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 */ /* global bigString, inputvalues */
describe('Long running Encryption/Decryption', function () { 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 ' + it('Successful encrypt/decrypt completely random data ' +
(i+1) + '/100', function (done) { (i+1) + '/100', function (done) {
let prm = Gpgmejs.init(); let prm = Gpgmejs.init();
@ -43,30 +43,32 @@ describe('Long running Encryption/Decryption', function () {
function(result){ function(result){
expect(result).to.not.be.empty; expect(result).to.not.be.empty;
expect(result.data).to.be.a('string'); expect(result.data).to.be.a('string');
/*
if (result.data.length !== data.length) { if (result.data.length !== data.length) {
// console.log('diff: ' + console.log('diff: ' +
// (result.data.length - data.length)); (result.data.length - data.length));
for (let i=0; i < result.data.length; i++){ for (let i=0; i < result.data.length; i++){
if (result.data[i] !== data[i]){ if (result.data[i] !== data[i]){
// console.log('position: ' + i); console.log('position: ' + i);
// console.log('result : ' + console.log('result : ' +
// result.data.charCodeAt(i) + result.data.charCodeAt(i) +
// result.data[i-2] + result.data[i-2] +
// result.data[i-1] + result.data[i-1] +
// result.data[i] + result.data[i] +
// result.data[i+1] + result.data[i+1] +
// result.data[i+2]); result.data[i+2]);
// console.log('original: ' + console.log('original: ' +
// data.charCodeAt(i) + data.charCodeAt(i) +
// data[i-2] + data[i-2] +
// data[i-1] + data[i-1] +
// data[i] + data[i] +
// data[i+1] + data[i+1] +
// data[i+2]); data[i+2]);
break; break;
} }
} }
} }
*/
expect(result.data).to.equal(data); expect(result.data).to.equal(data);
done(); done();
}); });

View File

@ -108,6 +108,7 @@ export class Connection{
return Promise.reject(gpgme_error('MSG_INCOMPLETE')); return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
} }
let me = this; let me = this;
let chunksize = message.chunksize;
return new Promise(function(resolve, reject){ return new Promise(function(resolve, reject){
let answer = new Answer(message); let answer = new Answer(message);
let listener = function(msg) { let listener = function(msg) {
@ -115,22 +116,27 @@ export class Connection{
me._connection.onMessage.removeListener(listener); me._connection.onMessage.removeListener(listener);
me._connection.disconnect(); me._connection.disconnect();
reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); 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 { } else {
let answer_result = answer.add(msg); let answer_result = answer.collect(msg);
if (answer_result !== true){ if (answer_result !== true){
me._connection.onMessage.removeListener(listener); me._connection.onMessage.removeListener(listener);
me._connection.disconnect(); me._connection.disconnect();
reject(answer_result); reject(answer_result);
} else if (msg.more === true){
me._connection.postMessage({'op': 'getmore'});
} else { } else {
me._connection.onMessage.removeListener(listener); if (msg.more === true){
me._connection.disconnect(); me._connection.postMessage({
resolve(answer.message); '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){ constructor(message){
this.operation = message.operation; this.operation = message.operation;
this.expected = message.expected; this.expect = message.expect;
} }
/** collect(msg){
* Add the information to the answer if (typeof(msg) !== 'object' || !msg.hasOwnProperty('response')) {
* @param {Object} msg The message as received with nativeMessaging return gpgme_error('CONN_UNEXPECTED_ANSWER');
* returns true if successfull, gpgme_error otherwise
*/
add(msg){
if (this._response === undefined){
this._response = {};
} }
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; let poa = permittedOperations[this.operation].answer;
if (messageKeys.length === 0){ if (messageKeys.length === 0){
return gpgme_error('CONN_UNEXPECTED_ANSWER'); return gpgme_error('CONN_UNEXPECTED_ANSWER');
@ -191,80 +210,42 @@ class Answer{
let key = messageKeys[i]; let key = messageKeys[i];
switch (key) { switch (key) {
case 'type': 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'); return gpgme_error('CONN_UNEXPECTED_ANSWER');
} }
break; break;
case 'more': case 'base64':
break;
case 'msg':
if (_decodedResponse.type === 'error'){
return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg));
}
break; break;
default: default:
//data should be concatenated if (!poa.data.hasOwnProperty(key)){
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 {
return gpgme_error('CONN_UNEXPECTED_ANSWER'); 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; break;
} }
} }
return true; return _response;
}
/**
* @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;
} }
} }

View File

@ -46,7 +46,6 @@ export class GPGME_Message {
constructor(operation){ constructor(operation){
this.operation = operation; this.operation = operation;
this._expected = 'string';
} }
set operation (op){ set operation (op){
@ -59,24 +58,50 @@ export class GPGME_Message {
} }
} }
} }
get operation(){ get operation(){
return this._msg.op; return this._msg.op;
} }
set expected(string){ /**
if (string === 'base64'){ * Set the maximum size of responses from gpgme in bytes. Values allowed
this._expected = 'base64'; * 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'){ * If expect is set to 'base64', the response is expected to be base64
return this._expected; * 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 * 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 * first, to be able to check if the parameter is permittted
@ -188,6 +213,7 @@ export class GPGME_Message {
*/ */
get message(){ get message(){
if (this.isComplete === true){ if (this.isComplete === true){
this._msg.chunksize = this.chunksize;
return this._msg; return this._msg;
} }
else { else {
@ -201,10 +227,13 @@ export class GPGME_Message {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
if (me.isComplete === true) { if (me.isComplete === true) {
let conn = new Connection; let conn = new Connection;
if (me._msg.chunksize === undefined){
me._msg.chunksize = 1023*1024;
}
conn.post(me).then(function(response) { conn.post(me).then(function(response) {
resolve(response); resolve(response);
}, function(reason) { }, function(reason) {
reject(gpgme_error('GNUPG_ERROR', reason)); reject(reason);
}); });
} }
else { else {

View File

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

View File

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

View File

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