Added Import S/MIME certificate popup

And much better handling of the sign and encrypt options
This commit is contained in:
the-djmaze
2024-02-23 03:14:36 +01:00
parent f94fc34d4f
commit 6f33bc23d3
47 changed files with 375 additions and 227 deletions

View File

@@ -16,6 +16,7 @@ import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
import { SMimeUserStore } from 'Stores/User/SMime';
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
import Remote from 'Remote/User/Fetch';
@@ -70,6 +71,10 @@ export class UserSettingsSecurity extends AbstractViewSettings {
);
}
importToSMime() {
showScreenPopup(SMimeImportPopupView);
}
onBuild() {
/**
* Create an iframe to display the Mailvelope keyring settings.

View File

@@ -35,7 +35,8 @@ export const GnuPGUserStore = new class {
this.keyring = null;
this.publicKeys([]);
this.privateKeys([]);
Remote.request('GnupgGetKeys',
SettingsCapa('GnuPG')
&& Remote.request('GnupgGetKeys',
(iError, oData) => {
if (oData?.Result) {
this.keyring = oData.Result;

View File

@@ -0,0 +1,124 @@
import { SettingsGet } from 'Common/Globals';
// https://mailvelope.github.io/mailvelope/Keyring.html
let mailvelopeKeyring = null;
export const MailvelopeUserStore = {
keyring: null,
loadKeyring(identifier) {
identifier = identifier || SettingsGet('Email');
if (window.mailvelope) {
const fn = keyring => {
mailvelopeKeyring = keyring;
console.log('mailvelope ready');
};
mailvelope.getKeyring().then(fn, err => {
if (identifier) {
// attempt to create a new keyring for this app/user
mailvelope.createKeyring(identifier).then(fn, err => console.error(err));
} else {
console.error(err);
}
});
addEventListener('mailvelope-disconnect', event => {
alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
}, false);
} else {
addEventListener('mailvelope', () => this.loadKeyring(identifier));
}
},
async hasPublicKeyForEmails(recipients) {
const
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
/*.then(LookupResult => Object.entries(LookupResult))*/,
entries = mailvelope && Object.entries(mailvelope);
return entries && entries.filter(value => value[1]).length === recipients.length;
},
async getPrivateKeyFor(email/*, sign*/) {
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
return ['mailvelope', email];
}
return false;
},
async decrypt(message) {
// Try Mailvelope (does not support inline images)
if (mailvelopeKeyring) {
const sender = message.from[0].email,
armoredText = message.plain();
try {
let emails = [...message.from,...message.to,...message.cc].validUnique(),
i = emails.length;
while (i--) {
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
/**
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
* Creates an iframe to display an encrypted form
*/
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
/**
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
* Creates an iframe to display the decrypted content of the encrypted mail.
*/
const body = message.body;
body.textContent = '';
let result = await mailvelope.createDisplayContainer(
'#'+body.id,
armoredText,
mailvelopeKeyring,
{
senderAddress: sender
// emails[i].email
}
);
if (result) {
if (result.error?.message) {
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
alert(result.error.code + ': ' + result.error.message);
}
} else {
body.classList.add('mailvelope');
return true;
}
}
break;
}
}
} catch (err) {
console.error(err);
}
}
}
/**
* Returns headers that should be added to an outgoing email.
* So far this is only the autocrypt header.
*/
/*
mailvelopeKeyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de')
.then(function(additional) {
console.log('additionalHeadersForOutgoingEmail', additional);
// logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."}
});
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
mailvelopeKeyring.createKeyBackupContainer(selector, options)
mailvelopeKeyring.createKeyGenContainer(selector, {
// userIds: [],
keySize: 4096
})
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
mailvelopeKeyring.importPublicKey(armored)
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
mailvelopeKeyring.addSyncHandler({
uploadSync
downloadSync
backup
restore
});
*/
};

View File

@@ -8,12 +8,10 @@ import { SettingsCapa, SettingsGet } from 'Common/Globals';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import { MailvelopeUserStore } from 'Stores/User/Mailvelope';
import Remote from 'Remote/User/Fetch';
// https://mailvelope.github.io/mailvelope/Keyring.html
let mailvelopeKeyring = null;
export const
BEGIN_PGP_MESSAGE = '-----BEGIN PGP MESSAGE-----',
// BEGIN_PGP_SIGNATURE = '-----BEGIN PGP SIGNATURE-----',
@@ -37,32 +35,9 @@ export const
}
loadKeyrings(identifier) {
identifier = identifier || SettingsGet('Email');
if (window.mailvelope) {
const fn = keyring => {
mailvelopeKeyring = keyring;
console.log('mailvelope ready');
};
mailvelope.getKeyring().then(fn, err => {
if (identifier) {
// attempt to create a new keyring for this app/user
mailvelope.createKeyring(identifier).then(fn, err => console.error(err));
} else {
console.error(err);
}
});
addEventListener('mailvelope-disconnect', event => {
alert('Mailvelope is updated to version ' + event.detail.version + '. Reload page');
}, false);
} else {
addEventListener('mailvelope', () => this.loadKeyrings(identifier));
}
MailvelopeUserStore.loadKeyring(identifier);
OpenPGPUserStore.loadKeyrings();
if (SettingsCapa('GnuPG')) {
GnuPGUserStore.loadKeyrings();
}
GnuPGUserStore.loadKeyrings();
}
/**
@@ -92,22 +67,14 @@ export const
}
);
}
OpenPGPUserStore.isSupported() && OpenPGPUserStore.importKey(key);
}
async mailvelopeHasPublicKeyForEmails(recipients) {
const
mailvelope = mailvelopeKeyring && await mailvelopeKeyring.validKeyForAddress(recipients)
/*.then(LookupResult => Object.entries(LookupResult))*/,
entries = mailvelope && Object.entries(mailvelope);
return entries && entries.filter(value => value[1]).length === recipients.length;
OpenPGPUserStore.importKey(key);
}
/**
* Checks if verifying/encrypting a message is possible with given email addresses.
* Returns the first library that can.
*/
async hasPublicKeyForEmails(recipients) {
hasPublicKeyForEmails(recipients) {
if (recipients.length) {
if (GnuPGUserStore.hasPublicKeyForEmails(recipients)) {
return 'gnupg';
@@ -119,40 +86,15 @@ export const
return false;
}
async getMailvelopePrivateKeyFor(email/*, sign*/) {
if (mailvelopeKeyring && await mailvelopeKeyring.hasPrivateKey({email:email})) {
return ['mailvelope', email];
}
return false;
}
/**
* Checks if signing a message is possible with given email address.
* Returns the first library that can.
*/
async getKeyForSigning(email) {
let key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
if (key) {
return ['openpgp', key];
}
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
if (key) {
return ['gnupg', key];
}
// return await this.getMailvelopePrivateKeyFor(email, 1);
}
async decrypt(message) {
const sender = message.from[0].email,
armoredText = message.plain();
const armoredText = message.plain();
if (!this.isEncrypted(armoredText)) {
throw Error('Not armored text');
}
// Try OpenPGP.js
if (OpenPGPUserStore.isSupported()) {
const sender = message.from[0].email;
let result = await OpenPGPUserStore.decrypt(armoredText, sender);
if (result) {
return result;
@@ -160,52 +102,9 @@ export const
}
// Try Mailvelope (does not support inline images)
if (mailvelopeKeyring) {
try {
let emails = [...message.from,...message.to,...message.cc].validUnique(),
i = emails.length;
while (i--) {
if (await this.getMailvelopePrivateKeyFor(emails[i].email)) {
/**
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createEncryptedFormContainer
* Creates an iframe to display an encrypted form
*/
// mailvelope.createEncryptedFormContainer('#mailvelope-form');
/**
* https://mailvelope.github.io/mailvelope/Mailvelope.html#createDisplayContainer
* Creates an iframe to display the decrypted content of the encrypted mail.
*/
const body = message.body;
body.textContent = '';
let result = await mailvelope.createDisplayContainer(
'#'+body.id,
armoredText,
mailvelopeKeyring,
{
senderAddress: sender
// emails[i].email
}
);
if (result) {
if (result.error?.message) {
if ('PWD_DIALOG_CANCEL' !== result.error.code) {
alert(result.error.code + ': ' + result.error.message);
}
} else {
body.classList.add('mailvelope');
return true;
}
}
break;
}
}
} catch (err) {
console.error(err);
}
}
// Now try GnuPG
return GnuPGUserStore.decrypt(message);
return (await MailvelopeUserStore.decrypt(message))
// Or try GnuPG
|| GnuPGUserStore.decrypt(message);
}
async verify(message) {
@@ -248,35 +147,4 @@ export const
}
return false;
}
/**
* Returns headers that should be added to an outgoing email.
* So far this is only the autocrypt header.
*/
/*
mailvelopeKeyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de')
.then(function(additional) {
console.log('additionalHeadersForOutgoingEmail', additional);
// logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."}
});
mailvelopeKeyring.addSyncHandler(syncHandlerObj)
mailvelopeKeyring.createKeyBackupContainer(selector, options)
mailvelopeKeyring.createKeyGenContainer(selector, {
// userIds: [],
keySize: 4096
})
mailvelopeKeyring.exportOwnPublicKey(emailAddr).then(<AsciiArmored, Error>)
mailvelopeKeyring.importPublicKey(armored)
// https://mailvelope.github.io/mailvelope/global.html#SyncHandlerObject
mailvelopeKeyring.addSyncHandler({
uploadSync
downloadSync
backup
restore
});
*/
};

View File

@@ -32,6 +32,7 @@ import { FolderUserStore } from 'Stores/User/Folder';
import { PgpUserStore } from 'Stores/User/Pgp';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { MailvelopeUserStore } from 'Stores/User/Mailvelope';
//import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { SMimeUserStore } from 'Stores/User/SMime';
import { Passphrases } from 'Storage/Passphrases';
@@ -255,13 +256,6 @@ export class ComposePopupView extends AbstractViewPopup {
doSign: false,
doEncrypt: false,
pgpSignKey: false,
canPgpEncrypt: false,
canMailvelope: false,
canSMimeSign: false,
canSMimeEncrypt: false,
draftsFolder: '',
draftUid: 0,
sending: false,
@@ -284,6 +278,8 @@ export class ComposePopupView extends AbstractViewPopup {
});
this.attachments = koArrayWithDestroy();
this.encryptOptions = koArrayWithDestroy();
this.signOptions = koArrayWithDestroy();
this.dragAndDropOver = ko.observable(false).extend({ debounce: 1 });
this.dragAndDropVisible = ko.observable(false).extend({ debounce: 1 });
@@ -336,11 +332,9 @@ export class ComposePopupView extends AbstractViewPopup {
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
isDraft: () => this.draftsFolder() && this.draftUid(),
canSign: () => {
let s = this.canSMimeSign();
return this.pgpSignKey() || s;
},
canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(),
canEncrypt: () => this.encryptOptions().length,
canMailvelope: () => this.encryptOptions.includes('Mailvelope'),
canSign: () => this.signOptions().length,
identitiesOptions: () =>
IdentityUserStore.map(item => ({
@@ -361,24 +355,14 @@ export class ComposePopupView extends AbstractViewPopup {
currentIdentity: value => {
if (value) {
const smime = !!(value.smimeKey() && value.smimeCertificate());
this.from(value.formattedName());
this.doEncrypt(value.pgpEncrypt()/* || SettingsUserStore.pgpEncrypt()*/);
this.doSign(smime || value.pgpSign()/* || SettingsUserStore.pgpSign()*/);
this.canSMimeSign(smime);
this.doSign(value.pgpSign()/* || SettingsUserStore.pgpSign()*/);
}
},
from: value => {
this.pgpSignKey(false);
value = getEmail(value);
value && PgpUserStore.getKeyForSigning(value).then(result => {
console.log({
email: value,
pgpSignKey:result
});
this.pgpSignKey(result)
});
from: () => {
this.initSign();
this.initEncrypt();
},
@@ -1381,29 +1365,57 @@ export class ComposePopupView extends AbstractViewPopup {
].join(',').split(',').map(value => getEmail(value.trim())).validUnique();
}
initEncrypt() {
const recipients = this.allRecipients();
PgpUserStore.hasPublicKeyForEmails(recipients).then(result => {
console.log({canPgpEncrypt:result});
this.canPgpEncrypt(result);
});
PgpUserStore.mailvelopeHasPublicKeyForEmails(recipients).then(result => {
console.log({canMailvelope:result});
this.canMailvelope(result);
if (!result) {
'mailvelope' === this.viewArea() && this.bodyArea();
// this.dropMailvelope();
}
});
const count = recipients.length,
/**
* Checks if signing a message is possible with from email address.
* And sets all that can.
*/
initSign() {
let options = [],
identity = this.currentIdentity(),
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null,
length = count ? recipients.filter(email =>
email == from
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
).length : 0;
this.canSMimeEncrypt(length && length === count);
console.log({canSMimeEncrypt:this.canSMimeEncrypt()});
email = getEmail(this.from()),
key = OpenPGPUserStore.getPrivateKeyFor(email, 1);
key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]);
identity.smimeKey() && identity.smimeCertificate() && identity.email() === email
&& options.push(['S/MIME']);
console.dir({signOptions: options});
this.signOptions(options);
}
initEncrypt() {
const recipients = this.allRecipients(),
options = [];
if (recipients.length) {
GnuPGUserStore.hasPublicKeyForEmails(recipients)
&& options.push('GnuPG');
OpenPGPUserStore.hasPublicKeyForEmails(recipients)
&& options.push('OpenPGP');
MailvelopeUserStore.hasPublicKeyForEmails(recipients).then(result => {
if (result) {
options.push('Mailvelope');
} else {
'mailvelope' === this.viewArea() && this.bodyArea();
// this.dropMailvelope();
}
});
const count = recipients.length,
identity = this.currentIdentity(),
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
count
&& count === recipients.filter(email =>
email == from
|| SMimeUserStore.find(certificate => email == certificate.emailAddress && certificate.smimeencrypt)
).length
&& options.push('S/MIME');
}
console.dir({encryptOptions:options});
this.encryptOptions(options);
}
async getMessageRequestParams(sSaveFolder, draft)
@@ -1464,8 +1476,8 @@ export class ComposePopupView extends AbstractViewPopup {
linkedData: []
},
recipients = draft ? [identity.email()] : this.allRecipients(),
sign = !draft && this.doSign() && (this.pgpSignKey() || this.canSMimeSign()),
encrypt = this.doEncrypt() && (this.canPgpEncrypt() || this.canSMimeEncrypt()),
signOptions = !draft && this.doSign() && this.signOptions(),
encryptOptions = this.doEncrypt() && this.encryptOptions(),
isHtml = this.oEditor.isHtml();
if (isHtml) {
@@ -1492,7 +1504,7 @@ export class ComposePopupView extends AbstractViewPopup {
params.autocrypt.push({addr:k, keydata:v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g).trim()})
);
*/
} else if (sign || encrypt) {
} else if (signOptions.length || encryptOptions.length) {
if (!draft && !hasAttachments && !Text.length) {
throw i18n('COMPOSE/ERROR_EMPTY_BODY');
}
@@ -1512,9 +1524,10 @@ export class ComposePopupView extends AbstractViewPopup {
alternative.children.push(data);
data = alternative;
}
if (sign) {
if (sign?.[1]) {
if ('openpgp' == sign[0]) {
let sign = true;
for (let i = 0; i < signOptions.length; ++i) {
if ('OpenPGP' == signOptions[i][0]) {
try {
// Doesn't sign attachments
params.html = params.plain = '';
let signed = new MimePart;
@@ -1525,34 +1538,53 @@ export class ComposePopupView extends AbstractViewPopup {
let signature = new MimePart;
signature.headers['Content-Type'] = 'application/pgp-signature; name="signature.asc"';
signature.headers['Content-Transfer-Encoding'] = '7Bit';
signature.body = await OpenPGPUserStore.sign(data.toString(), sign[1], 1);
signature.body = await OpenPGPUserStore.sign(data.toString(), signOptions[i][1], 1);
signed.children.push(signature);
params.signed = signed.toString();
params.boundary = signed.boundary;
data = signed;
} else if ('gnupg' == sign[0]) {
// TODO: sign in PHP fails
// params.signData = data.toString();
params.signFingerprint = sign[1].fingerprint;
params.signPassphrase = await GnuPGUserStore.sign(sign[1]);
} else {
throw 'Signing with ' + sign[0] + ' not yet implemented';
} catch (e) {
sign = false;
console.error(e);
}
} else if (this.canSMimeSign()) {
break;
}
if ('GnuPG' == signOptions[i][0]) {
// TODO: sign in PHP fails
let pass = await GnuPGUserStore.sign(signOptions[i][1]);
if (null != pass) {
// params.signData = data.toString();
params.signFingerprint = signOptions[i][1].fingerprint;
params.signPassphrase = pass;
} else {
sign = false;
}
break;
}
if ('S/MIME' == signOptions[i][0]) {
// TODO: sign in PHP fails
params.signCertificate = identity.smimeCertificate();
params.signPrivateKey = identity.smimeKey();
if (identity.smimeKeyEncrypted()) {
const pass = await Passphrases.ask(
params.signPrivateKey,
identity.smimeKey(),
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
'CRYPTO/DECRYPT'
);
params.signPassphrase = pass?.password;
// pass && pass.remember && Passphrases.set(identity, pass.password);
if (null != pass) {
params.signPassphrase = pass.password;
// pass.remember && Passphrases.set(identity, pass.password);
} else {
sign = false;
}
}
}
}
if (encrypt) {
if (signOptions.length && !sign) {
throw 'Signing failed';
}
if (encryptOptions.length) {
const autocrypt = () =>
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
params.autocrypt.push({
@@ -1560,24 +1592,30 @@ export class ComposePopupView extends AbstractViewPopup {
keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
})
);
if ('openpgp' == encrypt) {
// Doesn't encrypt attachments
params.encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
params.signed = '';
autocrypt();
} else if ('gnupg' == encrypt) {
// Does encrypt attachments
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
autocrypt();
} else if (this.canSMimeEncrypt() && identity && identity.smimeKey() && identity.smimeCertificate()) {
params.encryptCertificates = [identity.smimeCertificate()];
SMimeUserStore.forEach(certificate => {
certificate.emailAddress != identity.email()
&& recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id)
});
} else {
throw 'Encryption with ' + encrypt + ' not yet implemented';
for (let i = 0; i < encryptOptions.length; ++i) {
if ('OpenPGP' == encryptOptions[i]) {
// Doesn't encrypt attachments
params.encrypted = await OpenPGPUserStore.encrypt(data.toString(), recipients);
params.signed = '';
autocrypt();
break;
}
if ('GnuPG' == encryptOptions[i]) {
// Does encrypt attachments
params.encryptFingerprints = JSON.stringify(GnuPGUserStore.getPublicKeyFingerprints(recipients));
autocrypt();
break;
}
if ('S/MIME' == encryptOptions[i]) {
params.encryptCertificates = [identity.smimeCertificate()];
SMimeUserStore.forEach(certificate => {
certificate.emailAddress != identity.email()
&& recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id)
});
break;
}
// We skip Mailvelope as it has its own window
}
}
}

View File

@@ -0,0 +1,50 @@
import { addObservablesTo } from 'External/ko';
import { getNotification } from 'Common/Translator';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
import Remote from 'Remote/User/Fetch';
export class SMimeImportPopupView extends AbstractViewPopup {
constructor() {
super('SMimeImport');
addObservablesTo(this, {
pem: '',
pemError: false,
pemErrorMessage: '',
pemValid: false
});
this.pem.subscribe(value => {
this.pemError(false);
this.pemErrorMessage('');
this.pemValid(value && value.includes('-----BEGIN CERTIFICATE-----'));
});
}
submitForm() {
if (this.pemValid()) {
Remote.request('SMimeImportCertificate',
(iError, oData) => {
if (iError) {
this.pemError(true);
this.pemErrorMessage(getNotification(iError, oData?.ErrorMessage));
// oData?.ErrorMessageAdditional;
} else {
this.close();
}
},
{pem:this.pem()}
);
} else {
this.pemError(true);
}
}
onShow() {
this.pem('');
this.pemError(false);
this.pemErrorMessage('');
}
}

View File

@@ -173,6 +173,15 @@ trait SMime
return $this->DefaultResponse($result);
}
public function DoSMimeImportCertificate() : array
{
return $this->DefaultResponse(
$this->SMIME()->storeCertificate(
$this->GetActionParam('pem', '')
)
);
}
public function DoSMimeImportCertificatesFromMessage() : array
{
/*

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Подписано с S\/MIME",
"ENCRYPTED_MESSAGE": "Шифровано с S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Zpráva podepsaná S\/MIME",
"ENCRYPTED_MESSAGE": "Zpráva šifrovaná S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signeret meddelelse",
"ENCRYPTED_MESSAGE": "S\/MIME krypteret meddelelse",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signierte Nachricht",
"ENCRYPTED_MESSAGE": "S\/MIME-verschlüsselte Nachricht",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Μήνυμα υπογεγραμμένο με S\/MIME",
"ENCRYPTED_MESSAGE": "Μήνυμα κωδικοποιημένο με S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S/MIME certificate",
"CERTIFICATES": "S/MIME Certificates",
"SIGNED_MESSAGE": "S/MIME signed message",
"ENCRYPTED_MESSAGE": "S/MIME encrypted message",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Mensaje firmado mediante S\/MIME",
"ENCRYPTED_MESSAGE": "Mensaje cifrado mediante S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME abil signeeritud kiri",
"ENCRYPTED_MESSAGE": "S\/MIME abil krüpteeritud kiri",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME sinatutako mezua",
"ENCRYPTED_MESSAGE": "S\/MIME zifratutako mezua",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "پیام توسط S\/MIME امضاء شد",
"ENCRYPTED_MESSAGE": "پیام توسط S\/MIME رمزنگاری شد",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME allekirjoitettu viesti",
"ENCRYPTED_MESSAGE": "S\/MIME salattu visti",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Message signé par S\/MIME",
"ENCRYPTED_MESSAGE": "Message chiffré par S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME aláírt üzenet",
"ENCRYPTED_MESSAGE": "S\/MIME kódolt üzenet",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Pesan bertanda-tangan S\/MIME",
"ENCRYPTED_MESSAGE": "Pesan terenkripsi S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Skeyti undirritað með S\/MIME",
"ENCRYPTED_MESSAGE": "Skeyti dulritað með S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Messaggio firmato con S\/MIME",
"ENCRYPTED_MESSAGE": "Messaggio cifrato con S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME署名済みメッセージ",
"ENCRYPTED_MESSAGE": "S\/MIME暗号化メッセージ",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME로 서명된 메세지입니다.",
"ENCRYPTED_MESSAGE": "S\/MIME로 암호화된 메세지입니다.",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME pasirašytas pranešimas",
"ENCRYPTED_MESSAGE": "S\/MIME šifruotas pranešimas",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signert melding",
"ENCRYPTED_MESSAGE": "S\/MIME-kryptert melding",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Maak een back-up van de privésleutel op de server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Importeer S\/MIME certificaat",
"CERTIFICATES": "S\/MIME Certificaten",
"SIGNED_MESSAGE": "S\/MIME ondertekend bericht",
"ENCRYPTED_MESSAGE": "S\/MIME versleuteld bericht",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Utwórz kopię zapasową klucza prywatnego na serwerze"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Wiadomość podpisana S\/MIME",
"ENCRYPTED_MESSAGE": "Wiadomość zaszyfrowana S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
"ENCRYPTED_MESSAGE": "Mensagem criptografada com S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
"ENCRYPTED_MESSAGE": "Mensagem encriptada com S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Mensagem assinada com S\/MIME",
"ENCRYPTED_MESSAGE": "Mensagem encriptada com S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME signed message",
"ENCRYPTED_MESSAGE": "S\/MIME encrypted message",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME подписанное сообщение",
"ENCRYPTED_MESSAGE": "S\/MIME шифрованное сообщение",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Správa podpísaná s S\/MIME",
"ENCRYPTED_MESSAGE": "Správa šifrovaná s S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Sporočilo, podpisano z S\/MIME",
"ENCRYPTED_MESSAGE": "Sporočilo, šifrirano z S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME-signerat meddelande",
"ENCRYPTED_MESSAGE": "S\/MIME-krypterat meddelande",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME imzalı mesaj",
"ENCRYPTED_MESSAGE": "S\/MIME şifreli mesaj",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "S\/MIME підписане повідомлення",
"ENCRYPTED_MESSAGE": "S\/MIME шифроване повідомлення",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "Thư đã được ký xác nhận bằng mã S\/MIME",
"ENCRYPTED_MESSAGE": "Thư đã được mã hóa bởi S\/MIME",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "由 S\/MIME 签名",
"ENCRYPTED_MESSAGE": "由 S\/MIME 加密",

View File

@@ -302,6 +302,7 @@
"BACKUP_PRIVATE_KEY_ON_SERVER": "Backup private key on server"
},
"SMIME": {
"POPUP_IMPORT_TITLE": "Import S\/MIME certificate",
"CERTIFICATES": "S\/MIME Certificates",
"SIGNED_MESSAGE": "已透過 S\/MIME 簽署郵件",
"ENCRYPTED_MESSAGE": "已透過 S\/MIME 加密郵件",

View File

@@ -0,0 +1,13 @@
<header>
<a href="#" class="close" data-bind="click: close">×</a>
<h3 data-i18n="SMIME/POPUP_IMPORT_TITLE"></h3>
</header>
<form id="smime-import" class="modal-body" autocomplete="off" data-bind="submit: submitForm">
<div class="alert" data-bind="visible: pemError() && pemErrorMessage(), text: pemErrorMessage"></div>
<div class="control-group" data-bind="css: {'error': pemError}">
<textarea class="input-xxlarge" rows="14" autofocus="" autocomplete="off" data-bind="value: pem" required=""></textarea>
</div>
</form>
<footer>
<button class="btn" form="smime-import" data-icon="✚" data-i18n="OPENPGP/POPUP_IMPORT_BUTTON"></button>
</footer>

View File

@@ -125,6 +125,8 @@
<details>
<summary class="legend" data-i18n="SMIME/CERTIFICATES"></summary>
<button class="btn" data-bind="click: importToSMime" data-icon="✚" data-i18n="OPENPGP/POPUP_IMPORT_BUTTON"></button>
<table class="table table-hover list-table">
<tbody data-bind="foreach: smimeCertificates, i18nUpdate: smimeCertificates">
<tr>