js: more Keyring/Key handling

--

* src/Keys.js
  - made setKeyData more consistent with other methods
  - added convenience methods (Key.armored, Key.hasSecret)
  - Added a Key delete function

* src/Keyring.js:
  - added a getkeysArmored which allows for bulk export of public Keys

gpgmejs:
  - removed deleteKey. It is now a method of the Key itself
  - Encrypt: Added some common options as parameter, and the
    possibility to set all allowed flags via an additional Object
This commit is contained in:
Maximilian Krambach 2018-05-30 17:05:54 +02:00
parent 53ce2b94bc
commit 332b4adbcc
4 changed files with 227 additions and 104 deletions

View File

@ -71,6 +71,10 @@ const err_list = {
msg:'This key does not exist in GPG', msg:'This key does not exist in GPG',
type: 'error' type: 'error'
}, },
'KEY_NO_INIT': {
msg:'This property has not been retrieved yet from GPG',
type: 'error'
}
// generic // generic
'PARAM_WRONG':{ 'PARAM_WRONG':{
msg: 'Invalid parameter was found', msg: 'Invalid parameter was found',

View File

@ -93,56 +93,31 @@ export class GPGME_Key {
} else if (this._data.fingerprint !== data.fingerprint){ } else if (this._data.fingerprint !== data.fingerprint){
return gpgme_error('KEY_INVALID'); return gpgme_error('KEY_INVALID');
} }
let dataKeys = Object.keys(data);
let booleans = ['expired', 'disabled','invalid','can_encrypt', for (let i=0; i< dataKeys.length; i++){
'can_sign','can_certify','can_authenticate','secret', if (!validKeyProperties.hasOwnProperty(dataKeys[i])){
'is_qualified'];
for (let b =0; b < booleans.length; b++) {
if (
!data.hasOwnProperty(booleans[b]) ||
typeof(data[booleans[b]]) !== 'boolean'
){
return gpgme_error('KEY_INVALID'); return gpgme_error('KEY_INVALID');
} }
this._data[booleans[b]] = data[booleans[b]]; if (validKeyProperties[dataKeys[i]](data[dataKeys[i]]) !== true ){
}
if (typeof(data.protocol) !== 'string'){
return gpgme_error('KEY_INVALID');
}
// TODO check valid protocols?
this._data.protocol = data.protocol;
if (typeof(data.owner_trust) !== 'string'){
return gpgme_error('KEY_INVALID');
}
// TODO check valid values?
this._data.owner_trust = data.owner_trust;
// TODO: what about origin ?
if (!Number.isInteger(data.last_update)){
return gpgme_error('KEY_INVALID');
}
this._data.last_update = data.last_update;
this._data.subkeys = [];
if (data.hasOwnProperty('subkeys')){
if (!Array.isArray(data.subkeys)){
return gpgme_error('KEY_INVALID'); return gpgme_error('KEY_INVALID');
} }
for (let i=0; i< data.subkeys.length; i++) { switch (dataKeys[i]){
this._data.subkeys.push( case 'subkeys':
new GPGME_Subkey(data.subkeys[i])); this._data.subkeys = [];
} for (let i=0; i< data.subkeys.length; i++) {
} this._data.subkeys.push(
new GPGME_Subkey(data.subkeys[i]));
this._data.userids = []; }
if (data.hasOwnProperty('userids')){ break;
if (!Array.isArray(data.userids)){ case 'userids':
return gpgme_error('KEY_INVALID'); this._data.userids = [];
} for (let i=0; i< data.userids.length; i++) {
for (let i=0; i< data.userids.length; i++) { this._data.userids.push(
this._data.userids.push( new GPGME_UserId(data.userids[i]));
new GPGME_UserId(data.userids[i])); }
break;
default:
this._data[dataKeys[i]] = data[dataKeys[i]];
} }
} }
return this; return this;
@ -161,7 +136,9 @@ export class GPGME_Key {
if (cached === false) { if (cached === false) {
let me = this; let me = this;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
if (property === 'armor'){ if (!validKeyProperties.hasOwnProperty(property)){
reject('PARAM_WRONG');
} else if (property === 'armored'){
resolve(me.getArmor()); resolve(me.getArmor());
} else if (property === 'hasSecret'){ } else if (property === 'hasSecret'){
resolve(me.getHasSecret()); resolve(me.getHasSecret());
@ -173,15 +150,23 @@ export class GPGME_Key {
}); });
} }
}); });
} else { } else {
if (!this._data.hasOwnProperty(property)){ if (!validKeyProperties.hasOwnProperty(property)){
return gpgme_error('PARAM_WRONG'); return gpgme_error('PARAM_WRONG');
}
if (!this._data.hasOwnProperty(property)){
return gpgme_error('KEY_NO_INIT');
} else { } else {
return (this._data[property]); return (this._data[property]);
} }
} }
} }
get armored () {
return this.get('armored');
//TODO exception if empty
}
/** /**
* Reloads the Key from gnupg * Reloads the Key from gnupg
*/ */
@ -207,15 +192,6 @@ export class GPGME_Key {
}); });
} }
/**
* Get the armored block of the non- secret parts of the Key.
* @returns {String} the armored Key block.
* Notice that this may be outdated cached info. Use the async getArmor if
* you need the most current info
*/
// get armor(){ TODO }
/** /**
* Query the armored block of the non- secret parts of the Key directly * Query the armored block of the non- secret parts of the Key directly
* from gpg. * from gpg.
@ -279,6 +255,49 @@ export class GPGME_Key {
}) })
}); });
} }
/**
* Convenience function to be directly used as properties of the Key
* Notice that these rely on cached info and may be outdated. Use the async
* get(property, false) if you need the most current info
*/
/**
* @returns {String} The armored public Key block
*/
get armored(){
return this.get('armored', true);
}
/**
* @returns {Boolean} If the key is considered a "private Key",
* i.e. owns a secret subkey.
*/
get hasSecret(){
return this.get('hasSecret', true);
}
/**
* Deletes the public Key from the GPG Keyring. Note that a deletion of a
* secret key is not supported by the native backend.
* @returns {Boolean} Success if key was deleted, rejects with a GPG error
* otherwise
*/
delete(){
let me = this;
return new Promise(function(resolve, reject){
if (!me._data.fingerprint){
reject(gpgme_error('KEY_INVALID'));
}
let msg = createMessage('delete');
msg.setParameter('key', me._data.fingerprint);
msg.post().then(function(result){
resolve(result.success);
}, function(error){
reject(error);
})
});
}
} }
/** /**
@ -453,3 +472,78 @@ const validSubKeyProperties = {
return (Number.isInteger(value) && value > 0); return (Number.isInteger(value) && value > 0);
} }
} }
const validKeyProperties = {
//TODO better validation?
'fingerprint': function(value){
return isFingerprint(value);
},
'armored': function(value){
return typeof(value === 'string');
},
'revoked': function(value){
return typeof(value) === 'boolean';
},
'expired': function(value){
return typeof(value) === 'boolean';
},
'disabled': function(value){
return typeof(value) === 'boolean';
},
'invalid': function(value){
return typeof(value) === 'boolean';
},
'can_encrypt': function(value){
return typeof(value) === 'boolean';
},
'can_sign': function(value){
return typeof(value) === 'boolean';
},
'can_certify': function(value){
return typeof(value) === 'boolean';
},
'can_authenticate': function(value){
return typeof(value) === 'boolean';
},
'secret': function(value){
return typeof(value) === 'boolean';
},
'is_qualified': function(value){
return typeof(value) === 'boolean';
},
'protocol': function(value){
return typeof(value) === 'string';
//TODO check for implemented ones
},
'issuer_serial': function(value){
return typeof(value) === 'string';
},
'issuer_name': function(value){
return typeof(value) === 'string';
},
'chain_id': function(value){
return typeof(value) === 'string';
},
'owner_trust': function(value){
return typeof(value) === 'string';
},
'last_update': function(value){
return (Number.isInteger(value));
//TODO undefined/null possible?
},
'origin': function(value){
return (Number.isInteger(value));
},
'subkeys': function(value){
return (Array.isArray(value));
},
'userids': function(value){
return (Array.isArray(value));
},
'tofu': function(value){
return (Array.isArray(value));
},
'hasSecret': function(value){
return typeof(value) === 'boolean';
}
}

View File

@ -20,7 +20,7 @@
import {createMessage} from './Message' import {createMessage} from './Message'
import {GPGME_Key, createKey} from './Key' import {GPGME_Key, createKey} from './Key'
import { isFingerprint } from './Helpers'; import { isFingerprint, toKeyIdArray } from './Helpers';
import { gpgme_error } from './Errors'; import { gpgme_error } from './Errors';
export class GPGME_Keyring { export class GPGME_Keyring {
@ -43,10 +43,10 @@ export class GPGME_Keyring {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
let msg; let msg;
msg = createMessage('keylist'); msg = createMessage('keylist');
if (pattern && typeof(pattern) === 'string'){ if (pattern !== undefined){
msg.setParameter('keys', pattern); msg.setParameter('keys', pattern);
} }
msg.setParameter('sigs', true); //TODO do we need this? msg.setParameter('sigs', true);
msg.post().then(function(result){ msg.post().then(function(result){
let resultset = []; let resultset = [];
let promises = []; let promises = [];
@ -72,10 +72,30 @@ export class GPGME_Keyring {
}); });
}); });
} }
// TODO:
// deleteKey(key, include_secret=false) /**
// getKeysArmored(pattern) //just dump all armored keys * Fetches the armored public Key blocks for all Keys matchin the pattern
* (if no pattern is given, fetches all known to gnupg)
* @param {String|Array<String>} pattern (optional)
* @returns {Promise<String>} Armored Key blocks
*/
getKeysArmored(pattern) {
if (pattern)
return new Promise(function(resolve, reject) {
let msg = createMessage('export');
msg.setParameter('armor', true);
if (pattern !== undefined){
msg.setParameter('keys', pattern);
}
msg.post().then(function(result){
resolve(result.data);
}, function(error){
reject(error);
});
}
// getDefaultKey() Big TODO // getDefaultKey() Big TODO
// importKeys(armoredKeys) // importKeys(armoredKeys)
// generateKey --> TODO (Andre noch anfragen!)
}; };

View File

@ -46,28 +46,48 @@ export class GpgME {
} }
/** /**
* @param {String} data text/data to be encrypted as String * Encrypt (and optionally sign) a Message
* @param {String|Object} data text/data to be encrypted as String. Also accepts Objects with a getText method
* @param {GPGME_Key|String|Array<String>|Array<GPGME_Key>} publicKeys Keys used to encrypt the message * @param {GPGME_Key|String|Array<String>|Array<GPGME_Key>} publicKeys 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} armor (optional) Request the output as armored block
* @param {Boolean} wildcard (optional) If true, recipient information will not be added to the message * @param {Boolean} wildcard (optional) If true, recipient information will not be added to the message
* @param {Object} additional use additional gpg options (refer to src/permittedOperations)
* @returns {Promise<Object>} Encrypted message:
* data: The encrypted message
* base64: Boolean indicating whether data is base64 encoded.
* @async
*/ */
encrypt(data, publicKeys, base64=false, wildcard=false){ encrypt(data, publicKeys, secretKeys, base64=false, armor=true,
wildcard=false, additional = {}
){
let msg = createMessage('encrypt'); let msg = createMessage('encrypt');
if (msg instanceof Error){ if (msg instanceof Error){
return Promise.reject(msg) return Promise.reject(msg)
} }
// TODO temporary msg.setParameter('armor', armor);
msg.setParameter('armor', true);
msg.setParameter('always-trust', true); msg.setParameter('always-trust', true);
if (base64 === true) { if (base64 === true) {
msg.setParameter('base64', true); msg.setParameter('base64', true);
} }
let pubkeys = toKeyIdArray(publicKeys); let pubkeys = toKeyIdArray(publicKeys);
msg.setParameter('keys', pubkeys); msg.setParameter('keys', pubkeys);
let sigkeys = toKeyIdArray(secretKeys);
if (sigkeys.length > 0) {
msg.setParameter('signing_keys', sigkeys);
}
putData(msg, data); putData(msg, data);
if (wildcard === true){ if (wildcard === true){
msg.setParameter('throw-keyids', true); msg.setParameter('throw-keyids', true);
}; };
if (additional){
let additional_Keys = Object.keys(additional);
for (let k = 0; k < additional_Keys.length; k++) {
msg.setParameter(additional_Keys[k],
additional[additional_Keys[k]]);
}
}
if (msg.isComplete === true){ if (msg.isComplete === true){
return msg.post(); return msg.post();
} else { } else {
@ -76,16 +96,17 @@ export class GpgME {
} }
/** /**
* @param {String} data TODO base64? Message with the encrypted data * Decrypt a Message
* @param {Boolean} base64 (optional) Response should stay base64 * @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: * @returns {Promise<Object>} decrypted message:
data: The decrypted data. This may be base64 encoded. data: The decrypted data. This may be base64 encoded.
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.
info: An optional object with extra information. signatures: Array of signature Objects TODO not yet implemented.
// should be an object that can tell if all signatures are valid etc.
* @async * @async
*/ */
decrypt(data, base64=false){ decrypt(data, base64=false){
if (data === undefined){ if (data === undefined){
return Promise.reject(gpgme_error('MSG_EMPTY')); return Promise.reject(gpgme_error('MSG_EMPTY'));
@ -99,10 +120,22 @@ export class GpgME {
} }
putData(msg, data); putData(msg, data);
return msg.post(); return msg.post();
} }
sign(data, keys, mode='clearsign', base64=false) { //sender /**
* Sign a Message
* @param {String|Object} data text/data to be decrypted. Accepts Strings and Objects with a gettext methos
* @param {GPGME_Key|String|Array<String>|Array<GPGME_Key>} keys The key/keys to use for signing
* @param {*} mode The signing mode. Currently supported:
* 'clearsign': (default) The Message is embedded into the signature
* 'detached': The signature is stored separately
* @param {*} base64 input is considered base64
* @returns {Promise<Object>}
* data: The resulting data. In clearsign mode this includes the signature
* signature: The detached signature (if in detached mode)
* @async
*/
sign(data, keys, mode='clearsign', base64=false) {
if (data === undefined){ if (data === undefined){
return Promise.reject(gpgme_error('MSG_EMPTY')); return Promise.reject(gpgme_error('MSG_EMPTY'));
} }
@ -139,38 +172,10 @@ export class GpgME {
}) })
}); });
} }
deleteKey(key, delete_secret = false, no_confirm = false){
return Promise.reject(gpgme_error('NOT_YET_IMPLEMENTED'));
let msg = createMessage('deletekey');
if (msg instanceof Error){
return Promise.reject(msg);
}
let key_arr = toKeyIdArray(key);
if (key_arr.length !== 1){
return Promise.reject(
gpgme_error('GENERIC_ERROR'));
// TBD should always be ONE key?
}
msg.setParameter('key', key_arr[0]);
if (delete_secret === true){
msg.setParameter('allow_secret', true);
// TBD
}
if (no_confirm === true){ //TODO: Do we want this hidden deep in the code?
msg.setParameter('delete_force', true);
// TBD
}
if (msg.isComplete === true){
return msg.post();
} else {
return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
}
}
} }
/** /**
* Sets the data of the message * Sets the data of the message, setting flags according on the data type
* @param {GPGME_Message} message The message where this data will be set * @param {GPGME_Message} message The message where this data will be set
* @param {*} data The data to enter * @param {*} data The data to enter
*/ */