#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*/ {
constructor() {
this.gnupgkeys = PgpUserStore.gnupgKeys;
this.gnupgPublicKeys = PgpUserStore.gnupgPublicKeys;
this.gnupgPrivateKeys = PgpUserStore.gnupgPrivateKeys;
this.openpgpkeysPublic = PgpUserStore.openpgpPublicKeys;
this.openpgpkeysPrivate = PgpUserStore.openpgpPrivateKeys;

View file

@ -58,11 +58,7 @@ class OpenPgpKeyModel {
this.key = key;
const aEmails = [];
if (key.users) {
key.users.forEach(user => {
if (user.userID.email) {
aEmails.push(user.userID.email);
}
});
key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
}
this.id = key.getKeyID().toHex();
this.fingerprint = key.getFingerprint();
@ -101,7 +97,8 @@ export const PgpUserStore = new class {
* [ {email, can_encrypt, can_sign}, ... ]
*/
this.gnupgKeyring;
this.gnupgKeys = ko.observableArray();
this.gnupgPublicKeys = ko.observableArray();
this.gnupgPrivateKeys = ko.observableArray();
// OpenPGP.js
this.openpgpPublicKeys = ko.observableArray();
@ -159,12 +156,42 @@ export const PgpUserStore = new class {
if (Settings.capa(Capa.GnuPG)) {
this.gnupgKeyring = null;
this.gnupgKeys([]);
this.gnupgPublicKeys([]);
this.gnupgPrivateKeys([]);
Remote.request('GnupgGetKeys',
(iError, oData) => {
if (oData && 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');
}
}
@ -200,8 +227,6 @@ export const PgpUserStore = new class {
keyPair.revocationCertificate
keyPair.onServer
keyPair.inGnuPG
keyPair.uid.name
keyPair.uid.email
*/
storeKeyPair(keyPair, callback) {
// if (Settings.capa(Capa.GnuPG)) {
@ -213,9 +238,6 @@ export const PgpUserStore = new class {
callback && callback(iError, oData);
}, keyPair
);
// storeKeys(publicKeysItem);
// storeKeys(privateKeysItem);
openpgp.readKey({armoredKey:keyPair.publicKey}).then(key => {
PgpUserStore.openpgpPublicKeys.push(new OpenPgpKeyModel(keyPair.publicKey, key));
storeOpenPgpKeys(PgpUserStore.openpgpPublicKeys, publicKeysItem);

View file

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

View file

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

View file

@ -13,7 +13,7 @@ trait Pgp
*/
public function GnuPG() : ?\SnappyMail\PGP\GnuPG
{
$oAccount = $this->getAccountFromToken();
$oAccount = $this->getMainAccountFromToken();
if (!$oAccount) {
return null;
}
@ -95,9 +95,7 @@ trait Pgp
public function DoGnupgGetKeys() : array
{
$GPG = $this->GnuPG();
return $GPG
? $this->DefaultResponse(__FUNCTION__, $GPG->keyInfo(''))
: $this->FalseResponse(__FUNCTION__);
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->keyInfo('') : false);
}
public function DoGnupgGenerateKey() : array
@ -112,9 +110,15 @@ trait Pgp
$this->GetActionParam('Passphrase', '')
);
}
return $fingerprint
? $this->DefaultResponse(__FUNCTION__, $fingerprint)
: $this->FalseResponse(__FUNCTION__);
return $this->DefaultResponse(__FUNCTION__, $fingerprint);
}
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
@ -155,9 +159,7 @@ trait Pgp
}
$GPG = $sKey ? $this->GnuPG() : null;
return $GPG
? $this->DefaultResponse(__FUNCTION__, $GPG->import($sKey))
: $this->FalseResponse(__FUNCTION__);
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->import($sKey) : false);
}
/**
@ -195,21 +197,55 @@ trait Pgp
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
* Handy when using multiple browsers
*/
public function DoStorePGPKey() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$oAccount) {
return null;
}
$key = $this->GetActionParam('Key', '');
$keyId = $this->GetActionParam('KeyId', '');
$result = false;
if ($key && $keyId) {
return $this->DefaultResponse(__FUNCTION__, ($key && $keyId && $this->StorePGPKey($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(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP
@ -218,15 +254,13 @@ trait Pgp
$hash = $oAccount->CryptKey();
$key = \SnappyMail\Crypt::Encrypt($key, $hash);
$key[] = \hash_hmac('sha1', $key[2], $hash);
$result = \file_put_contents("{$dir}/0x{$keyId}.key", \json_encode($key));
} else if (\str_contains($key, 'PGP PUBLIC KEY')) {
$result = \file_put_contents("{$dir}/0x{$keyId}_public.asc", $key);
return !!\file_put_contents("{$dir}/{$keyId}.key", \json_encode($key));
}
if (\str_contains($key, 'PGP PUBLIC KEY')) {
return !!\file_put_contents("{$dir}/{$keyId}_public.asc", $key);
}
}
return $result
? $this->TrueResponse(__FUNCTION__)
: $this->FalseResponse(__FUNCTION__);
return false;
}
}

View file

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

View file

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

View file

@ -7,9 +7,11 @@
<a href="#" class="close" data-bind="click: function () { submitError('') }">×</a>
<span data-bind="text: submitError"></span>
</div>
<!-- Disable stupid browser password autofill -->
<input type="password" style="display:none"/>
<div class="control-group" data-bind="css: {'error': emailError}">
<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"
list="emailaddresses" data-bind="value: email" />
<datalist id="emailaddresses">

View file

@ -21,13 +21,43 @@
<!-- ko if: canGnuPG -->
<div class="legend">GnuPG</div>
<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>
<td>
<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 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>
</tr>
</tbody>
@ -42,8 +72,9 @@
<tr>
<td data-bind="click: view">
<span class="fontastic" data-i18n="[title]SETTINGS_OPEN_PGP/TITLE_PRIVATE">🔒</span>
<span class="key-id" data-bind="text: id"></span>
<!-- ko foreach: emails -->
<span class="open-pgp-key-user" data-bind="text: $data"></span>
<span class="key-user" data-bind="text: $data"></span>
<!-- /ko -->
</td>
<td>
@ -51,7 +82,7 @@
data-i18n="GLOBAL/ARE_YOU_SURE"></a>
</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>
</tr>
</tbody>
@ -60,8 +91,9 @@
<tr>
<td data-bind="click: view">
<span class="fontastic" data-i18n="[title]SETTINGS_OPEN_PGP/TITLE_PUBLIC">🔑</span>
<span class="key-id" data-bind="text: id"></span>
<!-- ko foreach: emails -->
<span class="open-pgp-key-user" data-bind="text: $data"></span>
<span class="key-user" data-bind="text: $data"></span>
<!-- /ko -->
</td>
<td>
@ -69,7 +101,7 @@
data-i18n="GLOBAL/ARE_YOU_SURE"></a>
</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>
</tr>
</tbody>