#89 Improved public/private key handling

This commit is contained in:
the-djmaze 2022-01-28 15:02:10 +01:00
parent 0560e22674
commit f8cbd5d129
9 changed files with 160 additions and 109 deletions

View file

@ -13,7 +13,8 @@ import { Settings } from 'Common/Globals';
export class OpenPgpUserSettings /*extends AbstractViewSettings*/ { export class OpenPgpUserSettings /*extends AbstractViewSettings*/ {
constructor() { constructor() {
this.gnupgkeys = PgpUserStore.gnupgKeys; this.gnupgPublicKeys = PgpUserStore.gnupgPublicKeys;
this.gnupgPrivateKeys = PgpUserStore.gnupgPrivateKeys;
this.openpgpkeysPublic = PgpUserStore.openpgpPublicKeys; this.openpgpkeysPublic = PgpUserStore.openpgpPublicKeys;
this.openpgpkeysPrivate = PgpUserStore.openpgpPrivateKeys; this.openpgpkeysPrivate = PgpUserStore.openpgpPrivateKeys;

View file

@ -58,11 +58,7 @@ class OpenPgpKeyModel {
this.key = key; this.key = key;
const aEmails = []; const aEmails = [];
if (key.users) { if (key.users) {
key.users.forEach(user => { key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
if (user.userID.email) {
aEmails.push(user.userID.email);
}
});
} }
this.id = key.getKeyID().toHex(); this.id = key.getKeyID().toHex();
this.fingerprint = key.getFingerprint(); this.fingerprint = key.getFingerprint();
@ -101,7 +97,8 @@ export const PgpUserStore = new class {
* [ {email, can_encrypt, can_sign}, ... ] * [ {email, can_encrypt, can_sign}, ... ]
*/ */
this.gnupgKeyring; this.gnupgKeyring;
this.gnupgKeys = ko.observableArray(); this.gnupgPublicKeys = ko.observableArray();
this.gnupgPrivateKeys = ko.observableArray();
// OpenPGP.js // OpenPGP.js
this.openpgpPublicKeys = ko.observableArray(); this.openpgpPublicKeys = ko.observableArray();
@ -159,12 +156,42 @@ export const PgpUserStore = new class {
if (Settings.capa(Capa.GnuPG)) { if (Settings.capa(Capa.GnuPG)) {
this.gnupgKeyring = null; this.gnupgKeyring = null;
this.gnupgKeys([]); this.gnupgPublicKeys([]);
this.gnupgPrivateKeys([]);
Remote.request('GnupgGetKeys', Remote.request('GnupgGetKeys',
(iError, oData) => { (iError, oData) => {
if (oData && oData.Result) { if (oData && oData.Result) {
this.gnupgKeyring = oData.Result; this.gnupgKeyring = oData.Result;
this.gnupgKeys(Object.values(oData.Result)); const initKey = (key, isPrivate) => {
const aEmails = [];
key.id = key.subkeys[0].keyid;
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
key.emails = aEmails;
key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper();
key.remove = () => {
if (key.askDelete()) {
Remote.request('GnupgDeleteKey',
(iError, oData) => {
if (oData && oData.Result) {
if (isPrivate) {
PgpUserStore.gnupgPrivateKeys.remove(key);
} else {
PgpUserStore.gnupgPublicKeys.remove(key);
}
delegateRunOnDestroy(key);
}
}, {
KeyId: key.id,
isPrivate: isPrivate
}
);
}
}
return key;
};
this.gnupgPublicKeys(oData.Result.public.map(key => initKey(key, 0)));
this.gnupgPrivateKeys(oData.Result.private.map(key => initKey(key, 1)));
console.log('gnupg ready'); console.log('gnupg ready');
} }
} }
@ -200,8 +227,6 @@ export const PgpUserStore = new class {
keyPair.revocationCertificate keyPair.revocationCertificate
keyPair.onServer keyPair.onServer
keyPair.inGnuPG keyPair.inGnuPG
keyPair.uid.name
keyPair.uid.email
*/ */
storeKeyPair(keyPair, callback) { storeKeyPair(keyPair, callback) {
// if (Settings.capa(Capa.GnuPG)) { // if (Settings.capa(Capa.GnuPG)) {
@ -213,9 +238,6 @@ export const PgpUserStore = new class {
callback && callback(iError, oData); callback && callback(iError, oData);
}, keyPair }, keyPair
); );
// storeKeys(publicKeysItem);
// storeKeys(privateKeysItem);
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => { openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
PgpUserStore.openpgpPublicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key)); PgpUserStore.openpgpPublicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
storeOpenPgpKeys(PgpUserStore.openpgpPublicKeys, publicKeysItem); storeOpenPgpKeys(PgpUserStore.openpgpPublicKeys, publicKeysItem);

View file

@ -8,11 +8,12 @@
width: 1%; width: 1%;
} }
.open-pgp-key-user { .key-user {
margin: 0 0.5em;
white-space: nowrap; white-space: nowrap;
} }
.delete-open-pgp-key:not(:hover) { .delete-key:not(:hover) {
opacity: 0.7; opacity: 0.7;
} }
} }

View file

@ -65,7 +65,6 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
if (keyPair) { if (keyPair) {
keyPair.onServer = this.saveServer() ? 1 : 0; keyPair.onServer = this.saveServer() ? 1 : 0;
keyPair.inGnuPG = this.saveGnuPG() ? 1 : 0; keyPair.inGnuPG = this.saveGnuPG() ? 1 : 0;
keyPair.uid = userId;
PgpUserStore.storeKeyPair(keyPair, ()=>{ PgpUserStore.storeKeyPair(keyPair, ()=>{
this.submitRequest(false); this.submitRequest(false);
this.cancelCommand(); this.cancelCommand();

View file

@ -13,7 +13,7 @@ trait Pgp
*/ */
public function GnuPG() : ?\SnappyMail\PGP\GnuPG public function GnuPG() : ?\SnappyMail\PGP\GnuPG
{ {
$oAccount = $this->getAccountFromToken(); $oAccount = $this->getMainAccountFromToken();
if (!$oAccount) { if (!$oAccount) {
return null; return null;
} }
@ -95,9 +95,7 @@ trait Pgp
public function DoGnupgGetKeys() : array public function DoGnupgGetKeys() : array
{ {
$GPG = $this->GnuPG(); $GPG = $this->GnuPG();
return $GPG return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->keyInfo('') : false);
? $this->DefaultResponse(__FUNCTION__, $GPG->keyInfo(''))
: $this->FalseResponse(__FUNCTION__);
} }
public function DoGnupgGenerateKey() : array public function DoGnupgGenerateKey() : array
@ -112,9 +110,15 @@ trait Pgp
$this->GetActionParam('Passphrase', '') $this->GetActionParam('Passphrase', '')
); );
} }
return $fingerprint return $this->DefaultResponse(__FUNCTION__, $fingerprint);
? $this->DefaultResponse(__FUNCTION__, $fingerprint) }
: $this->FalseResponse(__FUNCTION__);
public function DoGnupgDeleteKey() : array
{
$GPG = $this->GnuPG();
$sKeyId = $this->GetActionParam('KeyId', '');
$bPrivate = !!$this->GetActionParam('isPrivate', 0);
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->deleteKey($sKeyId, $bPrivate) : false);
} }
public function DoGnupgImportKey() : array public function DoGnupgImportKey() : array
@ -155,9 +159,7 @@ trait Pgp
} }
$GPG = $sKey ? $this->GnuPG() : null; $GPG = $sKey ? $this->GnuPG() : null;
return $GPG return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->import($sKey) : false);
? $this->DefaultResponse(__FUNCTION__, $GPG->import($sKey))
: $this->FalseResponse(__FUNCTION__);
} }
/** /**
@ -195,21 +197,55 @@ trait Pgp
return $this->DefaultResponse(__FUNCTION__, $keys); return $this->DefaultResponse(__FUNCTION__, $keys);
} }
/**
* Used to store generated armored key pair from OpenPGP.js
* Handy when using multiple browsers
*/
public function DoPgpStoreKeyPair() : array
{
$result = [
'onServer' => [false, false, false],
'inGnuPG' => [false, false, false]
];
$publicKey = $this->GetActionParam('publicKey', '');
$privateKey = $this->GetActionParam('privateKey', '');
$revocationCertificate = $this->GetActionParam('revocationCertificate', '');
if ($this->GetActionParam('onServer', '')) {
$result['onServer'] = [
$this->StorePGPKey($publicKey),
$this->StorePGPKey($privateKey),
false // $this->StorePGPKey($revocationCertificate)
];
}
if ($this->GetActionParam('inGnuPG', '')) {
$GPG = $this->GnuPG();
if ($GPG) {
$result['inGnuPG'] = [
$publicKey && $GPG->import($publicKey),
$privateKey && $GPG->import($privateKey),
false // $revocationCertificate && $GPG->import($revocationCertificate)
];
}
}
return $this->DefaultResponse(__FUNCTION__, $result);
}
/** /**
* Used to store key from OpenPGP.js * Used to store key from OpenPGP.js
* Handy when using multiple browsers * Handy when using multiple browsers
*/ */
public function DoStorePGPKey() : array public function DoStorePGPKey() : array
{ {
$oAccount = $this->getMainAccountFromToken();
if (!$oAccount) {
return null;
}
$key = $this->GetActionParam('Key', ''); $key = $this->GetActionParam('Key', '');
$keyId = $this->GetActionParam('KeyId', ''); $keyId = $this->GetActionParam('KeyId', '');
$result = false; return $this->DefaultResponse(__FUNCTION__, ($key && $keyId && $this->StorePGPKey($key, $keyId)));
if ($key && $keyId) { }
private function StorePGPKey(string $key, string $keyId = '') : bool
{
$oAccount = $this->getMainAccountFromToken();
if ($oAccount) {
$keyId = $keyId ? "0x{$keyId}" : \sha1($key);
$dir = $this->StorageProvider()->GenerateFilePath( $dir = $this->StorageProvider()->GenerateFilePath(
$oAccount, $oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP \RainLoop\Providers\Storage\Enumerations\StorageType::PGP
@ -218,15 +254,13 @@ trait Pgp
$hash = $oAccount->CryptKey(); $hash = $oAccount->CryptKey();
$key = \SnappyMail\Crypt::Encrypt($key, $hash); $key = \SnappyMail\Crypt::Encrypt($key, $hash);
$key[] = \hash_hmac('sha1', $key[2], $hash); $key[] = \hash_hmac('sha1', $key[2], $hash);
$result = \file_put_contents("{$dir}/0x{$keyId}.key", \json_encode($key)); return !!\file_put_contents("{$dir}/{$keyId}.key", \json_encode($key));
} else if (\str_contains($key, 'PGP PUBLIC KEY')) { }
$result = \file_put_contents("{$dir}/0x{$keyId}_public.asc", $key); if (\str_contains($key, 'PGP PUBLIC KEY')) {
return !!\file_put_contents("{$dir}/{$keyId}_public.asc", $key);
} }
} }
return false;
return $result
? $this->TrueResponse(__FUNCTION__)
: $this->FalseResponse(__FUNCTION__);
} }
} }

View file

@ -166,6 +166,11 @@ class GnuPG
: $this->GPG->decryptverifyFile($filename, $plaintext); : $this->GPG->decryptverifyFile($filename, $plaintext);
} }
public function deleteKey(string $keyId, bool $private) : bool
{
return $this->getGPG()->deleteKey($keyId, $private);
}
/** /**
* Encrypts a given text * Encrypts a given text
*/ */
@ -285,64 +290,21 @@ class GnuPG
*/ */
public function keyInfo(string $pattern) : array public function keyInfo(string $pattern) : array
{ {
$keys = []; $keys = [
'public' => [],
'private' => []
];
// Public // Public
foreach ($this->handler()->keyinfo($pattern) as $info) { foreach ($this->handler()->keyinfo($pattern) as $key) {
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) { $key['can_verify'] = $key['can_sign'];
foreach ($info['uids'] as $uid) { unset($key['can_sign']);
$id = $uid['email']; $keys['public'][] = $key;
if (isset($keys[$id])) {
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
$keys[$id]['can_encrypt'] = $keys[$id]['can_encrypt'] || $info['can_encrypt'];
} else {
$keys[$id] = [
'name' => $uid['name'],
'email' => $uid['email'],
// Public Key tasks
'can_verify' => $info['can_sign'],
'can_encrypt' => $info['can_encrypt'],
// Private Key tasks
'can_sign' => false,
'can_decrypt' => false,
// The keys
'publicKeys' => [],
'privateKeys' => []
];
}
foreach ($info['subkeys'] as $key) {
$keys[$id]['publicKeys'][$key['fingerprint']] = $key;
}
}
}
} }
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5 // Private, read https://github.com/php-gnupg/php-gnupg/issues/5
foreach ($this->handler()->keyinfo($pattern, 1) as $info) { foreach ($this->handler()->keyinfo($pattern, 1) as $key) {
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) { $key['can_decrypt'] = $key['can_encrypt'];
foreach ($info['uids'] as $uid) { unset($key['can_encrypt']);
$id = $uid['email']; $keys['private'][] = $key;
if (isset($keys[$id])) {
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
$keys[$id]['can_decrypt'] = $keys[$id]['can_decrypt'] || $info['can_encrypt'];
} else {
$keys[$id] = [
'name' => $uid['name'],
'email' => $uid['email'],
// Public Key tasks
'can_verify' => false,
'can_encrypt' => false,
// Private Key tasks
'can_sign' => $info['can_sign'],
'can_decrypt' => $info['can_encrypt'],
// The keys
'publicKeys' => [],
'privateKeys' => []
];
}
foreach ($info['subkeys'] as $key) {
$keys[$id]['privateKeys'][$key['fingerprint']] = $key;
}
}
}
} }
return $keys; return $keys;
} }

View file

@ -87,8 +87,8 @@ class GPG
throw new \Exception("socket name for '{$homedir}/S.gpg-agent.extra' is too long"); throw new \Exception("socket name for '{$homedir}/S.gpg-agent.extra' is too long");
} }
if (!\is_dir($homedir)) { if ($homedir && !\is_dir($homedir) && !\mkdir($homedir, 0700, true)) {
\mkdir($homedir, 0700, true); throw new \Exception("mkdir({$homedir}) failed");
} }
$this->options['homedir'] = $homedir; $this->options['homedir'] = $homedir;
@ -519,11 +519,9 @@ class GPG
{ {
$key = $this->keyInfo($keyId, $private ? 1 : 0); $key = $this->keyInfo($keyId, $private ? 1 : 0);
if (!$key) { if (!$key) {
return false; throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
// throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
} }
if (!$private && $this->keyInfo($keyId, 1)) { if (!$private && $this->keyInfo($keyId, 1)) {
return false;
throw new \Exception('Delete private key first: ' . $keyId); throw new \Exception('Delete private key first: ' . $keyId);
} }

View file

@ -7,9 +7,11 @@
<a href="#" class="close" data-bind="click: function () { submitError('') }">×</a> <a href="#" class="close" data-bind="click: function () { submitError('') }">×</a>
<span data-bind="text: submitError"></span> <span data-bind="text: submitError"></span>
</div> </div>
<!-- Disable stupid browser password autofill -->
<input type="password" style="display:none"/>
<div class="control-group" data-bind="css: {'error': emailError}"> <div class="control-group" data-bind="css: {'error': emailError}">
<label data-i18n="GLOBAL/EMAIL"></label> <label data-i18n="GLOBAL/EMAIL"></label>
<input type="text" required="" class="input-xlarge" <input type="email" required="" class="input-xlarge"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
list="emailaddresses" data-bind="value: email" /> list="emailaddresses" data-bind="value: email" />
<datalist id="emailaddresses"> <datalist id="emailaddresses">

View file

@ -21,13 +21,43 @@
<!-- ko if: canGnuPG --> <!-- ko if: canGnuPG -->
<div class="legend">GnuPG</div> <div class="legend">GnuPG</div>
<table class="table table-hover list-table"> <table class="table table-hover list-table">
<tbody data-bind="foreach: gnupgkeys, i18nUpdate: gnupgkeys"> <tbody><tr><th colspan="4" data-i18n="SETTINGS_OPEN_PGP/TITLE_PRIVATE">Private keys</th></tr></tbody>
<tbody data-bind="foreach: gnupgPrivateKeys, i18nUpdate: gnupgPrivateKeys">
<tr> <tr>
<td> <td>
<span data-bind="visible: can_sign" class="fontastic"></span> <span data-bind="visible: can_sign" class="fontastic"></span>
<span data-bind="visible: can_encrypt" class="fontastic">🔒</span>
<span data-bind="visible: can_decrypt" class="fontastic">🔓</span> <span data-bind="visible: can_decrypt" class="fontastic">🔓</span>
<span class="open-pgp-key-user" data-bind="text: email"></span> <span class="key-id" data-bind="text: id"></span>
<!-- ko foreach: emails -->
<span class="key-user" data-bind="text: $data"></span>
<!-- /ko -->
</td>
<td>
<a class="btn btn-small btn-small-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete()}, click: remove"
data-i18n="GLOBAL/ARE_YOU_SURE"></a>
</td>
<td>
<span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span>
</td>
</tr>
</tbody>
<tbody><tr><th colspan="4" data-i18n="SETTINGS_OPEN_PGP/TITLE_PUBLIC">Public keys</th></tr></tbody>
<tbody data-bind="foreach: gnupgPublicKeys, i18nUpdate: gnupgPublicKeys">
<tr>
<td>
<span data-bind="visible: can_verify" class="fontastic"></span>
<span data-bind="visible: can_encrypt" class="fontastic">🔒</span>
<span class="key-id" data-bind="text: id"></span>
<!-- ko foreach: emails -->
<span class="key-user" data-bind="text: $data"></span>
<!-- /ko -->
</td>
<td>
<a class="btn btn-small btn-small-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete()}, click: remove"
data-i18n="GLOBAL/ARE_YOU_SURE"></a>
</td>
<td>
<span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -42,8 +72,9 @@
<tr> <tr>
<td data-bind="click: view"> <td data-bind="click: view">
<span class="fontastic" data-i18n="[title]SETTINGS_OPEN_PGP/TITLE_PRIVATE">🔒</span> <span class="fontastic" data-i18n="[title]SETTINGS_OPEN_PGP/TITLE_PRIVATE">🔒</span>
<span class="key-id" data-bind="text: id"></span>
<!-- ko foreach: emails --> <!-- ko foreach: emails -->
<span class="open-pgp-key-user" data-bind="text: $data"></span> <span class="key-user" data-bind="text: $data"></span>
<!-- /ko --> <!-- /ko -->
</td> </td>
<td> <td>
@ -51,7 +82,7 @@
data-i18n="GLOBAL/ARE_YOU_SURE"></a> data-i18n="GLOBAL/ARE_YOU_SURE"></a>
</td> </td>
<td> <td>
<span class="delete-open-pgp-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span> <span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -60,8 +91,9 @@
<tr> <tr>
<td data-bind="click: view"> <td data-bind="click: view">
<span class="fontastic" data-i18n="[title]SETTINGS_OPEN_PGP/TITLE_PUBLIC">🔑</span> <span class="fontastic" data-i18n="[title]SETTINGS_OPEN_PGP/TITLE_PUBLIC">🔑</span>
<span class="key-id" data-bind="text: id"></span>
<!-- ko foreach: emails --> <!-- ko foreach: emails -->
<span class="open-pgp-key-user" data-bind="text: $data"></span> <span class="key-user" data-bind="text: $data"></span>
<!-- /ko --> <!-- /ko -->
</td> </td>
<td> <td>
@ -69,7 +101,7 @@
data-i18n="GLOBAL/ARE_YOU_SURE"></a> data-i18n="GLOBAL/ARE_YOU_SURE"></a>
</td> </td>
<td> <td>
<span class="delete-open-pgp-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span> <span class="delete-key fontastic" data-bind="visible: !askDelete(), click: openForDeletion">🗑</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>