Enigma: Client-side keys generation (with OpenPGP.js)

Fixed deletion of key pairs (error: Private key must be deleted before public key can be deleted)
pull/291/head
Aleksander Machniak 9 years ago
parent 559e5d7bd4
commit d5501a8bb2

@ -24,7 +24,6 @@ Implemented features:
TODO (must have):
-----------------
- Keys export to file
- Client-side keys generation (with OpenPGP.js?)
TODO (later):
-------------

@ -3,9 +3,6 @@
window.rcmail && rcmail.addEventListener('init', function(evt) {
if (rcmail.env.task == 'settings') {
rcmail.register_command('plugin.enigma', function() { rcmail.goto_url('plugin.enigma') }, true);
rcmail.register_command('plugin.enigma-key-import', function() { rcmail.enigma_key_import() }, true);
// rcmail.register_command('plugin.enigma-key-export', function() { rcmail.enigma_key_export() }, true);
rcmail.register_command('plugin.enigma-key-delete', function(props) { return rcmail.enigma_key_delete(); });
if (rcmail.gui_objects.keyslist) {
rcmail.keys_list = new rcube_list_widget(rcmail.gui_objects.keyslist,
@ -28,7 +25,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('search', function(props) {return rcmail.enigma_search(props); }, true);
rcmail.register_command('reset-search', function(props) {return rcmail.enigma_search_reset(props); }, true);
rcmail.register_command('plugin.enigma-import', function() { rcmail.enigma_import(); }, true);
// rcmail.register_command('plugin.enigma-export', function() { rcmail.enigma_export(); }, true);
// rcmail.register_command('plugin.enigma-export', function() { rcmail.enigma_key_export(); }, true);
rcmail.register_command('plugin.enigma-key-import', function() { rcmail.enigma_key_import() }, true);
rcmail.register_command('plugin.enigma-key-delete', function(props) { return rcmail.enigma_key_delete(); });
rcmail.register_command('plugin.enigma-key-create', function(props) { return rcmail.enigma_key_create(); }, true);
rcmail.register_command('plugin.enigma-key-save', function(props) { return rcmail.enigma_key_create_save(); }, true);
}
}
else if (rcmail.env.task == 'mail') {
@ -59,6 +60,65 @@ rcube_webmail.prototype.enigma_key_import = function()
this.enigma_loadframe('&_action=plugin.enigmakeys&_a=import');
};
// Display key(s) generation form
rcube_webmail.prototype.enigma_key_create = function()
{
this.enigma_loadframe('&_action=plugin.enigmakeys&_a=create');
};
// Generate key(s) and submit them
rcube_webmail.prototype.enigma_key_create_save = function()
{
var options, lock,
user = $('#key-ident > option').filter(':selected').text(),
password = $('#key-pass').val(),
confirm = $('#key-pass-confirm').val(),
size = $('#key-size').val();
// validate the form
if (!password || !confirm)
return alert(this.gettext('enigma.formerror'));
if (password != confirm)
return alert(this.gettext('enigma.passwordsdiffer'));
if (user.match(/^<[^>]+>$/))
return alert(this.gettext('enigma.nonameident'));
// generate keys
// use OpenPGP.js if browser supports required features
if (window.openpgp && window.crypto && (window.crypto.getRandomValues || window.crypto.subtle)) {
lock = this.set_busy(true, 'enigma.keygenerating');
options = {
numBits: size,
userId: user,
passphrase: password
};
openpgp.generateKeyPair(options).then(function(keypair) {
// success
post = {_a: 'import', _keys: keypair.privateKeyArmored};
// send request to server
rcmail.http_post('plugin.enigmakeys', post, lock);
}).catch(function(error) {
// failure
rcmail.set_busy(false, null, lock);
rcmail.display_message(rcmail.gettext('enigma.keygenerateerror'), 'error');
});
}
// generate keys on the server
else {
// @TODO
}
};
// Action executed after successful key generation and import
rcube_webmail.prototype.enigma_key_create_success = function()
{
parent.rcmail.enigma_list(1);
};
// Delete key(s)
rcube_webmail.prototype.enigma_key_delete = function()
{
@ -130,8 +190,8 @@ rcube_webmail.prototype.enigma_loadframe = function(url)
return;
}
this.set_busy(true);
frm.location.href = this.env.comm_path + '&_framed=1' + url;
this.env.frame_lock = this.set_busy(true, 'loading');
frm.location.href = this.env.comm_path + '&_framed=1&' + url;
}
};

@ -180,8 +180,9 @@ class enigma_driver_gnupg extends enigma_driver
{
$list = $this->list_keys($keyid);
if (is_array($list))
return array_shift($list);
if (is_array($list)) {
return $list[key($list)];
}
// error
return $list;
@ -196,9 +197,25 @@ class enigma_driver_gnupg extends enigma_driver
// delete public key
$result = $this->delete_pubkey($keyid);
// if not found, delete private key
if ($result !== true && $result->getCode() == enigma_error::E_KEYNOTFOUND) {
$result = $this->delete_privkey($keyid);
// error handling
if ($result !== true) {
$code = $result->getCode();
// if not found, delete private key
if ($code == enigma_error::E_KEYNOTFOUND) {
$result = $this->delete_privkey($keyid);
}
// need to delete private key first
else if ($code == enigma_error::E_DELKEY) {
$key = $this->get_key($keyid);
for ($i = count($key->subkeys) - 1; $i >= 0; $i--) {
$type = $key->subkeys[$i]->can_encrypt ? 'priv' : 'pub';
$result = $this->{'delete_' . $type . 'key'}($key->subkeys[$i]->id);
if ($result !== true) {
return $result;
}
}
}
}
return $result;
@ -307,17 +324,17 @@ class enigma_driver_gnupg extends enigma_driver
$ekey->name = trim($ekey->users[0]->name . ' <' . $ekey->users[0]->email . '>');
foreach ($key->getSubKeys() as $idx => $subkey) {
$skey = new enigma_subkey();
$skey->id = $subkey->getId();
$skey->revoked = $subkey->isRevoked();
$skey->created = $subkey->getCreationDate();
$skey->expires = $subkey->getExpirationDate();
$skey->fingerprint = $subkey->getFingerprint();
$skey->has_private = $subkey->hasPrivate();
$skey->can_sign = $subkey->canSign();
$skey->can_encrypt = $subkey->canEncrypt();
$ekey->subkeys[$idx] = $skey;
$skey = new enigma_subkey();
$skey->id = $subkey->getId();
$skey->revoked = $subkey->isRevoked();
$skey->created = $subkey->getCreationDate();
$skey->expires = $subkey->getExpirationDate();
$skey->fingerprint = $subkey->getFingerprint();
$skey->has_private = $subkey->hasPrivate();
$skey->can_sign = $subkey->canSign();
$skey->can_encrypt = $subkey->canEncrypt();
$ekey->subkeys[$idx] = $skey;
};
$ekey->id = $ekey->subkeys[0]->id;

@ -59,6 +59,10 @@ class enigma_ui
$this->key_import();
break;
case 'create':
$this->key_create();
break;
case 'search':
case 'list':
$this->key_list();
@ -393,12 +397,27 @@ class enigma_ui
}
/**
* Key import page handler
* Key import (page) handler
*/
private function key_import()
{
// Import process
if ($_FILES['_file']['tmp_name'] && is_uploaded_file($_FILES['_file']['tmp_name'])) {
if ($data = rcube_utils::get_input_value('_keys', rcube_utils::INPUT_POST)) {
// Import from generation form (ajax request)
$this->enigma->load_engine();
$result = $this->enigma->engine->import_key($data);
if (is_array($result)) {
$this->rc->output->command('enigma_key_create_success');
$this->rc->output->show_message('enigma.keygeneratesuccess', 'confirmation');
}
else {
$this->rc->output->show_message('enigma.keysimportfailed', 'error');
}
$this->rc->output->send();
}
else if ($_FILES['_file']['tmp_name'] && is_uploaded_file($_FILES['_file']['tmp_name'])) {
$this->enigma->load_engine();
$result = $this->enigma->engine->import_key($_FILES['_file']['tmp_name'], true);
@ -407,8 +426,9 @@ class enigma_ui
if ($result['imported']) {
$this->rc->output->command('parent.enigma_list', 1);
}
else
else {
$this->rc->output->command('parent.enigma_loadframe');
}
$this->rc->output->show_message('enigma.keysimportsuccess', 'confirmation',
array('new' => $result['imported'], 'old' => $result['unchanged']));
@ -463,6 +483,66 @@ class enigma_ui
return $out;
}
/**
* Key generation page handler
*/
private function key_create()
{
$this->enigma->include_script('openpgp.min.js');
$this->rc->output->add_handlers(array(
'keyform' => array($this, 'tpl_key_create_form'),
));
$this->rc->output->set_pagetitle($this->enigma->gettext('keygenerate'));
$this->rc->output->send('enigma.keycreate');
}
/**
* Template object for key generation form
*/
function tpl_key_create_form($attrib)
{
$attrib += array('id' => 'rcmKeyCreateForm');
$table = new html_table(array('cols' => 2));
// get user's identities
$identities = $this->rc->user->list_identities(null, true);
// Identity
$select = new html_select(array('name' => 'identity', 'id' => 'key-ident'));
foreach ((array) $identities as $idx => $ident) {
$name = empty($ident['name']) ? ('<' . $ident['email'] . '>') : $ident['ident'];
$select->add($name, $idx);
}
$table->add('title', html::label('key-name', rcube::Q($this->enigma->gettext('newkeyident'))));
$table->add(null, $select->show(0));
// Key size
$select = new html_select(array('name' => 'size', 'id' => 'key-size'));
$select->add($this->enigma->gettext('key2048'), '2048');
$select->add($this->enigma->gettext('key4096'), '4096');
$table->add('title', html::label('key-size', rcube::Q($this->enigma->gettext('newkeysize'))));
$table->add(null, $select->show());
// Password and confirm password
$table->add('title', html::label('key-pass', rcube::Q($this->enigma->gettext('newkeypass'))));
$table->add(null, rcube_output::get_edit_field('password', '',
array('id' => 'key-pass', 'size' => $attrib['size'], 'required' => true), 'password'));
$table->add('title', html::label('key-pass-confirm', rcube::Q($this->enigma->gettext('newkeypassconfirm'))));
$table->add(null, rcube_output::get_edit_field('password-confirm', '',
array('id' => 'key-pass-confirm', 'size' => $attrib['size'], 'required' => true), 'password'));
$this->rc->output->add_gui_object('keyform', $attrib['id']);
$this->rc->output->add_label('enigma.keygenerating', 'enigma.formerror',
'enigma.passwordsdiffer', 'enigma.keygenerateerror', 'enigma.nonameident');
return $this->rc->output->form_tag(array(), $table->show($attrib));
}
/**
* Key deleting
*/

@ -1,5 +1,21 @@
<?php
/**
+-----------------------------------------------------------------------+
| plugins/enigma/localization/<lang>.inc |
| |
| Localization file of the Roundcube Webmail ACL plugin |
| Copyright (C) 2012-2015, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
+-----------------------------------------------------------------------+
For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/enigma/
*/
$labels = array();
$labels['encryption'] = 'Encryption';
$labels['enigmacerts'] = 'S/MIME Certificates';
@ -36,6 +52,14 @@ $labels['keyrevoke'] = 'Revoke';
$labels['keysend'] = 'Send public key in a message';
$labels['keychpass'] = 'Change password';
$labels['newkeyident'] = 'Identity:';
$labels['newkeypass'] = 'Password:';
$labels['newkeypassconfirm'] = 'Confirm password:';
$labels['newkeysize'] = 'Key size:';
$labels['key2048'] = '2048 bits - default';
$labels['key4096'] = '4096 bits - more secure';
$labels['keygenerating'] = 'Generating keys...';
$labels['encryptionoptions'] = 'Encryption options...';
$labels['encryptmsg'] = 'Encrypt this message';
$labels['signmsg'] = 'Digitally sign this message';
@ -69,4 +93,10 @@ $messages['keyremovesuccess'] = 'Key(s) deleted successfulyl';
$messages['keyremoveerror'] = 'Unable to delete selected key(s).';
$messages['keyimporttext'] = 'You can import private and public key(s) or revocation signatures in ASCII-Armor format.';
$messages['formerror'] = 'Please, fill the form. All fields are required!';
$messages['passwordsdiffer'] = 'Passwords do not match!';
$messages['nonameident'] = 'Idenity must have a user name defined!';
$messages['keygenerateerror'] = 'Failed to generate a key pair';
$messages['keygeneratesuccess'] = 'A key pair generated and imported successfully.';
?>

File diff suppressed because one or more lines are too long

@ -0,0 +1,23 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<link rel="stylesheet" type="text/css" href="/this/enigma.css" />
</head>
<body class="iframe">
<div id="keyimport-title" class="boxtitle"><roundcube:label name="enigma.createkeys" /></div>
<div id="import-form" class="boxcontent">
<roundcube:object name="keyform" class="propform" size="40" textareacols="40" textarearows="6" />
<div id="formfooter">
<div class="footerleft">
<roundcube:button command="plugin.enigma-key-save" type="input" class="button mainaction" label="save" />
</div>
</div>
</div>
</body>
</html>

@ -22,10 +22,7 @@
<div id="mainscreen" class="enigma">
<div id="keystoolbar">
<!--
<roundcube:button command="plugin.enigma-key-create" type="link" class="buttonPas create" classAct="button create" classSel="button createSel" title="enigma.createkeys" content=" " />
<span class="separator">&nbsp;</span>
-->
<roundcube:button command="plugin.enigma-key-import" type="link" class="buttonPas import" classAct="button import" classSel="button importSel" title="enigma.importkeys" content=" " />
<!--
<roundcube:button command="plugin.enigma-key-export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="enigma.exportkeys" content=" " />

@ -0,0 +1,23 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<link rel="stylesheet" type="text/css" href="/this/enigma.css" />
</head>
<body class="iframe">
<h1 class="boxtitle"><roundcube:label name="enigma.createkeys" /></h1>
<div id="key-details" class="boxcontent">
<roundcube:object name="keyform" class="propform" size="40" textareacols="40" textarearows="6" />
</div>
<div class="footerleft formbuttons">
<roundcube:button command="plugin.enigma-key-save" type="input" class="button mainaction" label="save" />
</div>
<roundcube:include file="/includes/footer.html" />
</body>
</html>

@ -44,7 +44,7 @@
<roundcube:button command="lastpage" type="link" class="icon lastpage disabled" classAct="icon lastpage" title="lastpage" label="last" />
</div>
<div class="boxfooter">
<roundcube:button command="plugin.enigma-key-create" type="link" title="enigma.keycreate" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" label="enigma.keyadd" /><roundcube:button name="moreactions" id="keyoptionslink" type="link" title="enigma.keyactions" class="listbutton groupactions" onclick="return UI.toggle_popup('keyoptions',event)" innerClass="inner" label="enigma.arialabelkeyoptions" aria-haspopup="true" aria-expanded="false" aria-owns="keyoptionsmenu" />
<roundcube:button command="plugin.enigma-key-create" type="link" title="enigma.createkeys" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" label="enigma.keyadd" /><roundcube:button name="moreactions" id="keyoptionslink" type="link" title="enigma.keyactions" class="listbutton groupactions" onclick="return UI.toggle_popup('keyoptions',event)" innerClass="inner" label="enigma.arialabelkeyoptions" aria-haspopup="true" aria-expanded="false" aria-owns="keyoptionsmenu" />
<span class="countdisplay" aria-live="polite" aria-relevant="text">
<span class="voice"><roundcube:label name="enigma.enigmakeys" /></span>
<roundcube:object name="countdisplay" />

Loading…
Cancel
Save