mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-02 05:59:21 +03:00
#89 Improved public/private key handling
This commit is contained in:
parent
0560e22674
commit
f8cbd5d129
9 changed files with 160 additions and 109 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue