mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-07-13 03:27:39 +03:00
#89 OpenPGP.js encrypt (+ verify)
This commit is contained in:
parent
6b2311663b
commit
73fa215e22
7 changed files with 126 additions and 53 deletions
|
|
@ -184,6 +184,7 @@ export const OpenPGPUserStore = new class {
|
||||||
const passphrase = prompt('OpenPGP.js Passphrase for ' + privateKey.id + ' ' + privateKey.emails[0]);
|
const passphrase = prompt('OpenPGP.js Passphrase for ' + privateKey.id + ' ' + privateKey.emails[0]);
|
||||||
if (null !== passphrase) {
|
if (null !== passphrase) {
|
||||||
const
|
const
|
||||||
|
publicKey = findOpenPGPKey(this.publicKeys, sender/*, sign*/),
|
||||||
decryptedKey = await openpgp.decryptKey({
|
decryptedKey = await openpgp.decryptKey({
|
||||||
privateKey: privateKey.key,
|
privateKey: privateKey.key,
|
||||||
passphrase
|
passphrase
|
||||||
|
|
@ -191,7 +192,7 @@ export const OpenPGPUserStore = new class {
|
||||||
|
|
||||||
return await openpgp.decrypt({
|
return await openpgp.decrypt({
|
||||||
message,
|
message,
|
||||||
verificationKeys: this.getPublicKeyFor(sender),
|
verificationKeys: publicKey && publicKey.key,
|
||||||
// expectSigned: true,
|
// expectSigned: true,
|
||||||
// signature: '', // Detached signature
|
// signature: '', // Detached signature
|
||||||
decryptionKeys: decryptedKey
|
decryptionKeys: decryptedKey
|
||||||
|
|
@ -238,21 +239,52 @@ export const OpenPGPUserStore = new class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signCleartext(text, privateKey) {
|
/**
|
||||||
|
* https://docs.openpgpjs.org/global.html#sign
|
||||||
|
*/
|
||||||
|
async sign(text, privateKey, detached) {
|
||||||
const passphrase = prompt('OpenPGP.js Passphrase for ' + privateKey.id + ' ' + privateKey.emails[0]);
|
const passphrase = prompt('OpenPGP.js Passphrase for ' + privateKey.id + ' ' + privateKey.emails[0]);
|
||||||
if (null !== passphrase) {
|
if (null !== passphrase) {
|
||||||
privateKey = await openpgp.decryptKey({
|
privateKey = await openpgp.decryptKey({
|
||||||
privateKey: privateKey.key,
|
privateKey: privateKey.key,
|
||||||
passphrase
|
passphrase
|
||||||
});
|
});
|
||||||
const unsignedMessage = await openpgp.createCleartextMessage({ text: text });
|
const message = detached
|
||||||
|
? await openpgp.createMessage({ text: text })
|
||||||
|
: await openpgp.createCleartextMessage({ text: text });
|
||||||
return await openpgp.sign({
|
return await openpgp.sign({
|
||||||
message: unsignedMessage, // CleartextMessage or Message object
|
message: message,
|
||||||
signingKeys: privateKey
|
signingKeys: privateKey,
|
||||||
// detached: false
|
detached: !!detached
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://docs.openpgpjs.org/global.html#encrypt
|
||||||
|
*/
|
||||||
|
async encrypt(text, recipients, signPrivateKey) {
|
||||||
|
const count = recipients.length;
|
||||||
|
recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
|
||||||
|
if (count === recipients.length) {
|
||||||
|
if (signPrivateKey) {
|
||||||
|
const passphrase = prompt('OpenPGP.js Passphrase for ' + signPrivateKey.id + ' ' + signPrivateKey.emails[0]);
|
||||||
|
if (null === passphrase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signPrivateKey = await openpgp.decryptKey({
|
||||||
|
privateKey: signPrivateKey.key,
|
||||||
|
passphrase
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await openpgp.encrypt({
|
||||||
|
message: await openpgp.createMessage({ text: text }),
|
||||||
|
encryptionKeys: recipients.map(pkey => pkey.key),
|
||||||
|
signingKeys: signPrivateKey
|
||||||
|
// signature
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,14 @@ export const PgpUserStore = new class {
|
||||||
async hasPublicKeyForEmails(recipients, all) {
|
async hasPublicKeyForEmails(recipients, all) {
|
||||||
const count = recipients.length;
|
const count = recipients.length;
|
||||||
if (count) {
|
if (count) {
|
||||||
if (GnuPGUserStore.hasPublicKeyForEmails(recipients, all)) {
|
|
||||||
return 'gnupg';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OpenPGPUserStore.hasPublicKeyForEmails(recipients, all)) {
|
if (OpenPGPUserStore.hasPublicKeyForEmails(recipients, all)) {
|
||||||
return 'openpgp';
|
return 'openpgp';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GnuPGUserStore.hasPublicKeyForEmails(recipients, all)) {
|
||||||
|
return 'gnupg';
|
||||||
|
}
|
||||||
|
|
||||||
let keyring = this.mailvelopeKeyring,
|
let keyring = this.mailvelopeKeyring,
|
||||||
mailvelope = keyring && await keyring.validKeyForAddress(recipients)
|
mailvelope = keyring && await keyring.validKeyForAddress(recipients)
|
||||||
/*.then(LookupResult => Object.entries(LookupResult))*/;
|
/*.then(LookupResult => Object.entries(LookupResult))*/;
|
||||||
|
|
|
||||||
|
|
@ -360,8 +360,8 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
Bcc: this.bcc(),
|
Bcc: this.bcc(),
|
||||||
ReplyTo: this.replyTo(),
|
ReplyTo: this.replyTo(),
|
||||||
Subject: this.subject(),
|
Subject: this.subject(),
|
||||||
TextIsHtml: TextIsHtml ? 1 : 0,
|
Html: TextIsHtml ? Text : '',
|
||||||
Text: Text,
|
Text: TextIsHtml ? '' : Text,
|
||||||
DraftInfo: this.aDraftInfo,
|
DraftInfo: this.aDraftInfo,
|
||||||
InReplyTo: this.sInReplyTo,
|
InReplyTo: this.sInReplyTo,
|
||||||
References: this.sReferences,
|
References: this.sReferences,
|
||||||
|
|
@ -453,16 +453,16 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (encrypt) {
|
if (encrypt) {
|
||||||
throw 'Encryption not yet implemented';
|
if (params.Html) {
|
||||||
|
throw 'Encrypt HTML with ' + encrypt + ' not yet implemented';
|
||||||
|
}
|
||||||
|
if ('openpgp' != encrypt) {
|
||||||
|
throw 'Encryption with ' + encrypt + ' not yet implemented';
|
||||||
}
|
}
|
||||||
if (sign && 'openpgp' != sign[0]) {
|
if (sign && 'openpgp' != sign[0]) {
|
||||||
throw 'Signing with ' + sign[0] + ' not yet implemented';
|
throw 'Signing with ' + sign[0] + ' not yet implemented';
|
||||||
}
|
}
|
||||||
if (sign && 'openpgp' == sign[0]) {
|
OpenPGPUserStore.encrypt(params.Text, this.allRecipients(), sign && sign[1]).then(text => {
|
||||||
if (params.TextIsHtml) {
|
|
||||||
throw i18n('PGP_NOTIFICATIONS/PGP_ERROR', { ERROR: "Can't sign HTML" });
|
|
||||||
}
|
|
||||||
OpenPGPUserStore.signCleartext(params.Text, sign[1]).then(text => {
|
|
||||||
if (text) {
|
if (text) {
|
||||||
params.Text = text;
|
params.Text = text;
|
||||||
send();
|
send();
|
||||||
|
|
@ -472,6 +472,42 @@ class ComposePopupView extends AbstractViewPopup {
|
||||||
this.sending(false);
|
this.sending(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (sign) {
|
||||||
|
if (params.Html) {
|
||||||
|
throw 'Signing HTML with ' + sign[0] + ' not yet implemented';
|
||||||
|
}
|
||||||
|
if ('openpgp' != sign[0]) {
|
||||||
|
throw 'Signing with ' + sign[0] + ' not yet implemented';
|
||||||
|
}
|
||||||
|
const detached = false;
|
||||||
|
if (detached) {
|
||||||
|
// Append headers
|
||||||
|
params.Text = [
|
||||||
|
'Content-Transfer-Encoding: base64',
|
||||||
|
'Content-Type: text/plain; charset="utf-8"; protected-headers="v1"',
|
||||||
|
// 'From: Demo <demo@snappymail.eu>',
|
||||||
|
// 'To: Demo <demo@snappymail.eu>',
|
||||||
|
// 'Subject: text detached signed'
|
||||||
|
''
|
||||||
|
]
|
||||||
|
// Now the body in base64
|
||||||
|
.concat(btoa(params.Text).match(/.{1,76}/g))
|
||||||
|
.join("\r\n");
|
||||||
|
}
|
||||||
|
OpenPGPUserStore.sign(params.Text, sign[1]).then(text => {
|
||||||
|
if (text) {
|
||||||
|
if (detached) {
|
||||||
|
params.Signature = text;
|
||||||
|
} else {
|
||||||
|
params.Text = text;
|
||||||
|
}
|
||||||
|
send();
|
||||||
|
} else {
|
||||||
|
this.sendError(true);
|
||||||
|
this.sendErrorDesc(i18n('PGP_NOTIFICATIONS/PGP_ERROR', { ERROR: 'Signing failed' }));
|
||||||
|
this.sending(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
send();
|
send();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -634,6 +634,13 @@ export class MailMessageView extends AbstractViewRight {
|
||||||
PgpUserStore.decrypt(oMessage).then(result => {
|
PgpUserStore.decrypt(oMessage).then(result => {
|
||||||
if (result && result.data) {
|
if (result && result.data) {
|
||||||
mimeToMessage(result.data, oMessage);
|
mimeToMessage(result.data, oMessage);
|
||||||
|
if (result.signatures) {
|
||||||
|
oMessage.pgpSigned(true);
|
||||||
|
oMessage.pgpVerified({
|
||||||
|
signatures: result.signatures,
|
||||||
|
success: !!result.signatures.length
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,13 @@ abstract class MimeType
|
||||||
const MULTIPART_ALTERNATIVE = 'multipart/alternative';
|
const MULTIPART_ALTERNATIVE = 'multipart/alternative';
|
||||||
const MULTIPART_RELATED = 'multipart/related';
|
const MULTIPART_RELATED = 'multipart/related';
|
||||||
const MULTIPART_MIXED = 'multipart/mixed';
|
const MULTIPART_MIXED = 'multipart/mixed';
|
||||||
|
|
||||||
const MULTIPART_SIGNED = 'multipart/signed';
|
const MULTIPART_SIGNED = 'multipart/signed';
|
||||||
|
const APPLICATION_PGP_SIGNATURE = 'application/pgp-signature';
|
||||||
|
|
||||||
|
const MULTIPART_ENCRYPTED = 'multipart/encrypted';
|
||||||
|
const APPLICATION_PGP_ENCRYPTED = 'application/pgp-encrypted';
|
||||||
|
const APPLICATION_OCTET_STREAM = 'application/octet-stream';
|
||||||
|
|
||||||
const MESSAGE_RFC822 = 'message/rfc822';
|
const MESSAGE_RFC822 = 'message/rfc822';
|
||||||
const MESSAGE_PARTIAL = 'message/partial';
|
const MESSAGE_PARTIAL = 'message/partial';
|
||||||
|
|
|
||||||
|
|
@ -47,28 +47,15 @@ class Message
|
||||||
$this->oAttachmentCollection = new AttachmentCollection;
|
$this->oAttachmentCollection = new AttachmentCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function DoesNotCreateEmptyTextPart() : self
|
public function DoesNotAddDefaultXMailer() : void
|
||||||
{
|
|
||||||
$this->bAddEmptyTextPart = false;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function DoesNotAddDefaultXMailer() : self
|
|
||||||
{
|
{
|
||||||
$this->bAddDefaultXMailer = false;
|
$this->bAddDefaultXMailer = false;
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function MessageId() : string
|
public function MessageId() : string
|
||||||
{
|
{
|
||||||
$sResult = '';
|
return empty($this->aHeadersValue[Enumerations\Header::MESSAGE_ID]) ? ''
|
||||||
if (!empty($this->aHeadersValue[Enumerations\Header::MESSAGE_ID]))
|
: $this->aHeadersValue[Enumerations\Header::MESSAGE_ID];
|
||||||
{
|
|
||||||
$sResult = $this->aHeadersValue[Enumerations\Header::MESSAGE_ID];
|
|
||||||
}
|
|
||||||
return $sResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SetMessageId(string $sMessageId) : void
|
public function SetMessageId(string $sMessageId) : void
|
||||||
|
|
@ -316,11 +303,6 @@ class Message
|
||||||
return $this->AddAlternative(Enumerations\MimeType::TEXT_HTML, $sHtml);
|
return $this->AddAlternative(Enumerations\MimeType::TEXT_HTML, $sHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function AddText(string $sHtmlOrPlainText, bool $bIsHtml = false) : self
|
|
||||||
{
|
|
||||||
return $bIsHtml ? $this->AddHtml($sHtmlOrPlainText) : $this->AddPlain($sHtmlOrPlainText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function AddAlternative(string $sContentType, string $sData) : self
|
public function AddAlternative(string $sContentType, string $sData) : self
|
||||||
{
|
{
|
||||||
$this->aAlternativeParts[] = array(
|
$this->aAlternativeParts[] = array(
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,10 @@ trait Messages
|
||||||
|
|
||||||
$this->Plugins()->RunHook('filter.send-message', array($oMessage));
|
$this->Plugins()->RunHook('filter.send-message', array($oMessage));
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: PGP encrypt/sign
|
||||||
|
*/
|
||||||
|
|
||||||
$mResult = false;
|
$mResult = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -995,7 +999,7 @@ trait Messages
|
||||||
|
|
||||||
$this->Plugins()->RunHook('filter.read-receipt-message-plain', array($oAccount, $oMessage, &$sText));
|
$this->Plugins()->RunHook('filter.read-receipt-message-plain', array($oAccount, $oMessage, &$sText));
|
||||||
|
|
||||||
$oMessage->AddText($sText, false);
|
$oMessage->AddPlain($sText);
|
||||||
|
|
||||||
$this->Plugins()->RunHook('filter.build-read-receipt-message', array($oMessage, $oAccount));
|
$this->Plugins()->RunHook('filter.build-read-receipt-message', array($oMessage, $oAccount));
|
||||||
|
|
||||||
|
|
@ -1004,8 +1008,6 @@ trait Messages
|
||||||
|
|
||||||
private function buildMessage(Account $oAccount, bool $bWithDraftInfo = true) : \MailSo\Mime\Message
|
private function buildMessage(Account $oAccount, bool $bWithDraftInfo = true) : \MailSo\Mime\Message
|
||||||
{
|
{
|
||||||
$bTextIsHtml = '1' === $this->GetActionParam('TextIsHtml', '0');
|
|
||||||
|
|
||||||
$oMessage = new \MailSo\Mime\Message();
|
$oMessage = new \MailSo\Mime\Message();
|
||||||
|
|
||||||
if (!$this->Config()->Get('security', 'hide_x_mailer_header', true))
|
if (!$this->Config()->Get('security', 'hide_x_mailer_header', true))
|
||||||
|
|
@ -1084,18 +1086,26 @@ trait Messages
|
||||||
$aFoundDataURL = array();
|
$aFoundDataURL = array();
|
||||||
$aFoundContentLocationUrls = array();
|
$aFoundContentLocationUrls = array();
|
||||||
|
|
||||||
|
$sHtml = $this->GetActionParam('Html', '');
|
||||||
$sText = $this->GetActionParam('Text', '');
|
$sText = $this->GetActionParam('Text', '');
|
||||||
if ($bTextIsHtml) {
|
if ($sHtml) {
|
||||||
$sText = \MailSo\Base\HtmlUtils::BuildHtml($sText, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls);
|
$sHtml = \MailSo\Base\HtmlUtils::BuildHtml($sHtml, $aFoundCids, $aFoundDataURL, $aFoundContentLocationUrls);
|
||||||
}
|
$this->Plugins()->RunHook('filter.message-html', array($oAccount, $oMessage, &$sHtml));
|
||||||
$this->Plugins()->RunHook($bTextIsHtml ? 'filter.message-html' : 'filter.message-plain',
|
|
||||||
array($oAccount, $oMessage, &$sText));
|
|
||||||
if ($bTextIsHtml && \strlen($sText)) {
|
|
||||||
$sTextConverted = \MailSo\Base\HtmlUtils::ConvertHtmlToPlain($sText);
|
$sTextConverted = \MailSo\Base\HtmlUtils::ConvertHtmlToPlain($sText);
|
||||||
$this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sTextConverted));
|
$this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sTextConverted));
|
||||||
$oMessage->AddText($sTextConverted, false);
|
$oMessage->AddPlain($sTextConverted);
|
||||||
|
$oMessage->AddHtml($sText);
|
||||||
|
} else {
|
||||||
|
$sSignature = $this->GetActionParam('Signature', null);
|
||||||
|
if ($sSignature) {
|
||||||
|
// MimeType::MULTIPART_SIGNED
|
||||||
|
// MimeType::APPLICATION_PGP_SIGNATURE
|
||||||
|
$oMessage->AddPlain($sText);
|
||||||
|
} else {
|
||||||
|
$this->Plugins()->RunHook('filter.message-plain', array($oAccount, $oMessage, &$sText));
|
||||||
|
$oMessage->AddPlain($sText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$oMessage->AddText($sText, $bTextIsHtml);
|
|
||||||
|
|
||||||
$aAttachments = $this->GetActionParam('Attachments', null);
|
$aAttachments = $this->GetActionParam('Attachments', null);
|
||||||
if (\is_array($aAttachments))
|
if (\is_array($aAttachments))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue