/** * The MIT License (MIT) * * Copyright (c) 2014-2019 Mailvelope GmbH * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * @type {Mailvelope} */ class Mailvelope { /** * Gives access to the mailvelope extension version * @returns {Promise.<String, Error>} */ getVersion() { return send('get-version'); } /** * Retrieves the Keyring for the given identifier * @param {String} identifier - the identifier of the keyring, if empty the main keyring is returned * @returns {Promise.<Keyring, Error>} * @throws {Error} error.code = 'NO_KEYRING_FOR_ID' */ getKeyring(identifier) { return send('get-keyring', {identifier}).then(options => new Keyring(identifier, options)); } /** * Creates a Keyring for the given identifier * @param {String} identifier - the identifier of the new keyring * @returns {Promise.<Keyring, Error>} * @throws {Error} error.code = 'KEYRING_ALREADY_EXISTS' * @example * mailvelope.createKeyring('Account-ID-4711').then(function(keyring) { * // continue to display the settings container and start the setup wizard * mailvelope.createSettingsContainer('#mailvelope-settings', keyring); * }); */ createKeyring(identifier) { return send('create-keyring', {identifier}).then(options => new Keyring(identifier, options)); } /** * Ascii Armored PGP Text Block * @typedef {String} AsciiArmored */ /** * CSS Selector String * @typedef {String} CssSelector */ /** * @typedef {Object} DisplayContainerOptions * @property {String} senderAddress - email address of sender, used to indentify key for signature verification */ /** * @typedef {Object} DisplayContainer * @property {Error} error - Error object with code and message attribute * error.code = 'DECRYPT_ERROR' - generic decrypt error * error.code = 'ARMOR_PARSE_ERROR' - error while parsing the armored message * error.code = 'PWD_DIALOG_CANCEL' - user canceled password dialog * error.code = 'NO_KEY_FOUND' - no private key found to decrypt this message */ /** * Creates an iframe to display the decrypted content of the encrypted mail. * The iframe will be injected into the container identified by selector. * @param {CssSelector} selector - target container * @param {AsciiArmored} armored - the encrypted mail to display * @param {Keyring} [keyring] - the keyring to use for this operation * @param {DisplayContainerOptions} options * @returns {Promise.<DisplayContainer, Error>} */ createDisplayContainer(selector, armored, keyring, options) { try { checkTypeKeyring(keyring); } catch (e) { return Promise.reject(e); } return send('display-container', {selector, armored, identifier: keyring && keyring.identifier, options}).then(display => { if (display && display.error) { display.error = mapError(display.error); } return display; }); } /** * @typedef {Object} EditorContainerOptions * @property {number} quota - mail content (text + attachments) limit in kilobytes (default: 20480) * @property {boolean} signMsg - if true then the mail will be signed (default: false) * @property {AsciiArmored} armoredDraft - a PGP message, signed and encrypted with the default key of the user, will be used to restore a draft in the editor * The armoredDraft parameter can't be combined with the parameters: predefinedText, quotedMail... parameters, keepAttachments * @property {String} predefinedText - text that will be added to the editor * @property {AsciiArmored} quotedMail - mail that should be quoted * @property {boolean} quotedMailIndent - if true the quoted mail will be indented (default: true) * @property {String} quotedMailHeader - header to be added before the quoted mail * @property {boolean} keepAttachments - add attachments of quotedMail to editor (default: false) */ /** * Creates an iframe with an editor for a new encrypted mail. * The iframe will be injected into the container identified by selector. * @param {CssSelector} selector - target container * @param {Keyring} [keyring] - the keyring to use for this operation * @param {EditorContainerOptions} options * @returns {Promise.<Editor, Error>} * @throws {Error} error.code = 'WRONG_ARMORED_TYPE' - parameters of type AsciiArmored do not have the correct armor type <br> error.code = 'INVALID_OPTIONS' - invalid combination of options parameter * @example * mailvelope.createEditorContainer('#editor-element', keyring).then(function(editor) { * // register event handler for mail client send button * $('#mailer-send').click(function() { * // encrypt current content of editor for array of recipients * editor.encrypt(['info@mailvelope.com', 'abc@web.de']).then(function(armored) { * console.log('encrypted message', armored); * }); * }); * }); */ createEditorContainer(selector, keyring, options) { try { checkTypeKeyring(keyring); } catch (e) { return Promise.reject(e); } return send('editor-container', {selector, identifier: keyring && keyring.identifier, options}).then(editorId => new Editor(editorId)); } /** * @typedef {Object} SettingsContainerOptions * @property {String} email - the email address of the current user * @property {String} fullName - the full name of the current user */ /** * Creates an iframe to display the keyring settings. * The iframe will be injected into the container identified by selector. * @param {CssSelector} selector - target container * @param {Keyring} [keyring] - the keyring to use for the setup * @param {SettingsContainerOptions} options * @returns {Promise.<undefined, Error>} */ createSettingsContainer(selector, keyring, options) { try { checkTypeKeyring(keyring); } catch (e) { return Promise.reject(e); } return send('settings-container', {selector, identifier: keyring && keyring.identifier, options}); } /** * Creates an iframe to display an encrypted form * The iframe will be injected into the container identified by selector. * @param @param {String} selector - the id of target container * @param @param {String} formHtml - the form definition * @param @param {String} signature - the OpenPGP signature * @returns {Promise.<Object, Error>} an object that includes armoredData * @throws {Error} error.code = 'INVALID_FORM' the form definition is not valid */ createEncryptedFormContainer(selector, formHtml, signature) { return send('encrypted-form-container', {selector, formHtml, signature}); } } // connection to content script is alive let connected = true; let syncHandler = null; function checkTypeKeyring(keyring) { if (keyring && !(keyring instanceof Keyring)) { const error = new Error('Type mismatch: keyring should be instance of Keyring.'); error.code = 'TYPE_MISMATCH'; throw error; } } /** * Not accessible, instance can be obtained using {@link Mailvelope#getKeyring} * or {@link Mailvelope#createKeyring}. * @param {String} identifier - the keyring identifier * @param {object} options - the options * @property {number} logoRev - revision number of the keyring logo, initial value: 0 */ class Keyring { constructor(identifier, options) { this.identifier = identifier; this.logoRev = options.revision || 0; } /** * @typedef {Object} LookupResult * @property {String} fingerprint - Fingerprint of the key * @property {Date} lastModified - last time the key was modified * @property {String} source - Source the key was found at <br> * Currently available: <br> * * 'LOC' - local key ring <br> * * 'WKD' - web key directory <br> * * 'MKS' - mailvelope key server <br> * * 'AC' - autocrypt * @property {Date} [lastSeen] - last time the key was seen <br> * (not set for local keys) * @property {String} [armored] - Armored key that can be imported <br> * (not set for local keys) */ /** * Checks for valid key in the keyring for provided email addresses * If none is found also checks in other sources (see LookupResult). * @param {Array} recipients - list of email addresses for key lookup * @returns {Promise.<Object, Error>} The object maps email addresses to: <br> * false: no valid key <br> * {keys: [LookupResult]}: valid keys * * @example * keyring.validKeyForAddress(['abc@web.de', 'info@mailvelope.com']).then(function(result) { * console.log(result); * // { * // 'abc@web.de': false, * // 'info@mailvelope.com': { * // keys: [ * // { * // fingerprint: 'f37377c39898d05ffd39157a98bbec557ce08def', * // lastModified: Tue May 19 2015 10:36:53 GMT+0200 (CEST), * // source: 'LOC' * // } * // ] * // } * // } * }); */ validKeyForAddress(recipients) { return send('query-valid-key', {identifier: this.identifier, recipients}).then(keyMap => { for (const address in keyMap) { if (keyMap[address]) { keyMap[address].keys.forEach(key => { key.lastModified = new Date(key.lastModified); }); } } return keyMap; }); } /** * Exports the public key as an ascii armored string. * Only keys belonging to the user (corresponding private key exists) can be exported. * @param {String} emailAddr - email address to identify the public+private key * @returns {Promise.<AsciiArmored, Error>} * @throws {Error} error.code = 'NO_KEY_FOR_ADDRESS' * @example * keyring.exportOwnPublicKey('abc@web.de').then(function(armoredPublicKey) { * console.log('exportOwnPublicKey', armoredPublicKey); * // prints: "-----BEGIN PGP PUBLIC KEY BLOCK..." * }); */ exportOwnPublicKey(emailAddr) { return send('export-own-pub-key', {identifier: this.identifier, emailAddr}); } /** * @typedef {Object} additionalMailHeaders * @property {String} autocrypt - the Autocrypt header that should be <br> * added to the outgoing mail<br> */ /** * @typedef {Object} outgoingMailHeaders * @property {String} from - the From header */ /** * Returns headers that should be added to an outgoing email. * So far this is only the `autocrypt` header. * @param {outgoingMailHeaders} headers - headers of the outgoing mail. <br> * In particular `from` to select the key * @returns {Promise.<additionalMailHeaders, Error>} * @throws {Error} error.code = 'NO_KEY_FOR_ADDRESS' * @example * keyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de').then(function(additional) { * console.log('additionalHeadersForOutgoingEmail', additional); * // logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."} * }); */ additionalHeadersForOutgoingEmail(headers) { return send('additional-headers-for-outgoing', {identifier: this.identifier, headers}); } /** * Asks the user if they want to import the public key. * @param {AsciiArmored} armored - public key to import * @returns {Promise.<String, Error>} 'IMPORTED' - key has been imported <br> 'UPDATED' - key already in keyring, new key merged with existing key <br> 'INVALIDATED' - key has been updated, new status of key is 'invalid' (e.g. revoked) <br> 'REJECTED' - key import rejected by user * @throws {Error} error.code = 'IMPORT_ERROR' <br> error.code = 'WRONG_ARMORED_TYPE' */ importPublicKey(armored) { return send('import-pub-key', {identifier: this.identifier, armored}); } /** * @typedef {Object} AutocryptMailHeaders * @property {String} autocrypt - the Autocrypt header to process * @property {String} from - the From header * @property {String} date - the Date header */ /** * Process Autocrypt header from message being read. * @param {AutocryptMailHeaders} headers - the relevant mail headers * @returns {Promise.<undefined, Error>} * @throws {Error} error.code = 'INVALID_HEADER' <br> error.code = 'STORAGE_ERROR' */ processAutocryptHeader(headers) { return send('process-autocrypt-header', {identifier: this.identifier, headers}); } /** * Set logo for keyring. The image is persisted in Mailvelope with a revision number, * therefore the method is only required after new keyring generation or if logo and revision number changes. * @param {String} dataURL - data-URL representing the logo, max. file size: ~10KB, max. image size: 192x96px, content-type: image/png * @param {number} revision - revision number * @returns {Promise.<undefined, Error>} * @throws {Error} error.code = 'LOGO_INVALID' <br> * error.code = 'REVISION_INVALID' * @example * keyring.setLogo('data:image/png;base64,iVBORS==', 3).then(function() { * // keyring.logoRev == 3 * }).catch(function(error) { * // logo update failed * }); * */ setLogo(dataURL, revision) { return send('set-logo', {identifier: this.identifier, dataURL, revision}).then(() => { this.logoRev = revision; }); } /** * @typedef {Object} UserId * @property {String} email - the email address of the current user * @property {String} fullName - the full name of the current user */ /** * @typedef {Object} KeyGenContainerOptions * @property {Array.<UserId>} userIds - array of user IDs. The first entry in the array is set as the primary user ID. * @property {number} keySize - key size in bit, optional, default: 2048, valid values: 2048, 4096. */ /** * Creates an iframe to display the key generation container. * The iframe will be injected into the container identified by selector. * @param {CssSelector} selector - target container * @param {KeyGenContainerOptions} options * @returns {Promise.<Generator, Error>} * @throws {Error} error.code = 'INPUT_NOT_VALID' */ createKeyGenContainer(selector, options) { return send('key-gen-container', {selector, identifier: this.identifier, options}).then(generatorId => new Generator(generatorId)); } /** * @typedef {Object} KeyBackupContainerOptions * @param {Boolean} initialSetup (default: true) */ /** * Creates an iframe to initiate the key backup process. * @param {CssSelector} selector - target container * @param {KeyBackupContainerOptions} options * @returns {Promise.<KeyBackupPopup, Error>} */ createKeyBackupContainer(selector, options) { return send('key-backup-container', {selector, identifier: this.identifier, options}).then(popupId => new KeyBackupPopup(popupId)); } /** * @typedef {Object} PrivateKeyContainerOptions * @property {boolean} restorePassword (default: false) */ /** * Creates an iframe to restore the backup. * @param {CssSelector} selector - target container * @param {PrivateKeyContainerOptions} options * @returns {Promise.<undefined, Error>} */ restoreBackupContainer(selector, options) { return send('restore-backup-container', {selector, identifier: this.identifier, options}).then(restoreId => new RestoreBackup(restoreId)); } /** * Check if keyring contains valid private key with given fingerprint * @param {String|{fingerprint: String, email: String}} fingerprint or Object with fingerprint or email property * @returns {Promise.<boolean, Error>} */ hasPrivateKey(query = {}) { const fingerprint = typeof query === 'string' ? query : query.fingerprint; const {email} = query; return send('has-private-key', {identifier: this.identifier, fingerprint, email}).then(result => result); } /** * @typedef {Object} UploadSyncReply * @property {String} eTag - entity tag for the uploaded encrypted keyring */ /** * @typedef {Function} UploadSyncHandler * @param {Object} uploadObj - object with upload data * @param {String} uploadObj.eTag - entity tag for the uploaded encrypted keyring, or null if initial upload * @param {AsciiArmored} uploadObj.keyringMsg - encrypted keyring as PGP armored message * @returns {Promise.<UploadSyncReply, Error>} - if version on server has different eTag, then the promise is rejected * if server is initial and uploadObj.eTag is not null, then the promise is rejected */ /** * @typedef {Object} DownloadSyncReply * @property {AsciiArmored} keyringMsg - encrypted keyring as PGP armored message, or null if no newer version available * @property {String} eTag - entity tag for the current encrypted keyring message, or null if server is intial */ /** * @typedef {Function} DownloadSyncHandler * @param {Object} downloadObj - meta info for download * @param {String} downloadObj.eTag - entity tag for the current local keyring, or null if no local eTag * @returns {Promise.<DownloadSyncReply, Error>} - if version on server has same eTag, then keyringMsg property of reply is empty, but eTag in reply has to be set * if server is initial and downloadObj.eTag is not null, then the promise is resolved with empty eTag */ /** * @typedef {Object} BackupSyncPacket * @property {AsciiArmored} backup - encrypted key backup as PGP armored message */ /** * @typedef {Function} BackupSyncHandler * @param {BackupSyncPacket} - object with backup data * @returns {Promise.<undefined, Error>} */ /** * @typedef {Function} RestoreSyncHandler * @returns {Promise.<BackupSyncPacket, Error>} */ /** * @typedef {Object} SyncHandlerObject * @property {UploadSyncHandler} uploadSync - function called by Mailvelope to upload the keyring (public keys), the message is encrypted with the default private key * @property {DownloadSyncHandler} downloadSync - function called by Mailvelope to download the encrypted keyring (public keys) * @property {BackupSyncHandler} backup - function called by Mailvelope to upload a symmetrically encrypted private key backup * @property {RestoreSyncHandler} restore - function called by Mailvelope to restore a private key backup */ /** * Add various functions for keyring synchronization * @param {SyncHandlerObject} syncHandlerObj * @returns {Promise.<undefined, Error>} */ addSyncHandler(syncHandlerObj) { if (typeof syncHandlerObj.uploadSync !== typeof syncHandlerObj.downloadSync) { return Promise.reject(new Error('uploadSync and downloadSync Handler cannot be set exclusively.')); } return send('add-sync-handler', {identifier: this.identifier}).then(syncHandlerId => { if (syncHandler) { syncHandler.update(syncHandlerObj); } else { syncHandler = new SyncHandler(syncHandlerId, syncHandlerObj); } }); } /** * @typedef {Object} OpenSettingsOptions * @param {Boolean} showDefaultKey (default: false) */ /** * Open the extension settings in a new browser tab * @param {OpenSettingsOptions} [options] * @returns {Promise.<undefined, Error>} */ openSettings(options) { return send('open-settings', {identifier: this.identifier, options}); } } /** * Not accessible, instance can be obtained using {@link Keyring#createKeyBackupContainer} * @param {String} popupId */ class KeyBackupPopup { constructor(popupId) { this.popupId = popupId; } /** * @returns {Promise.<undefined, Error>} - key backup ready or error * @throws {Error} */ isReady() { return send('keybackup-popup-isready', {popupId: this.popupId}); } } /** * Not accessible, instance can be obtained using {@link Keyring#createKeyGenContainer}. * @param {String} generatorId - the internal id of the generator */ class Generator { constructor(generatorId) { this.generatorId = generatorId; } /** * Generate a private key * @param {Promise.<undefined, Error>} [confirm] - newly generate key is only persisted if Promise resolves, * in the reject or timeout case the generated key is rejected * @returns {Promise.<AsciiArmored, Error>} - the newly generated key (public part) * @throws {Error} */ generate(confirm) { return send('generator-generate', {generatorId: this.generatorId, confirmRequired: Boolean(confirm)}).then(armored => { if (confirm) { confirm.then(() => { emit('generator-generate-confirm', {generatorId: this.generatorId}); }).catch(e => { emit('generator-generate-reject', {generatorId: this.generatorId, error: objError(e)}); }); } return armored; }); } } /** * Not accessible, instance can be obtained using {@link Keyring#restoreBackupContainer}. * @param {String} restoreId - the internal id of the restore backup */ class RestoreBackup { constructor(restoreId) { this.restoreId = restoreId; } /** * @returns {Promise.<undefined, Error>} - key restore ready or error * @throws {Error} */ isReady() { return send('restore-backup-isready', {restoreId: this.restoreId}); } } /** * Not accessible, instance can be obtained using {@link Mailvelope#createEditorContainer}. * @param {String} editorId - the internal id of the editor */ class Editor { constructor(editorId) { this.editorId = editorId; } /** * Requests the encryption of the editor content for the given recipients. * @param {Array.<String>} recipients - list of email addresses for public key lookup and encryption * @returns {Promise.<AsciiArmored, Error>} * @throws {Error} error.code = 'ENCRYPT_IN_PROGRESS' <br> * error.code = 'NO_KEY_FOR_RECIPIENT' <br> * error.code = 'ENCRYPT_QUOTA_SIZE' * @example * editor.encrypt(['abc@web.de', 'info@com']).then(function (armoredMessage) { * console.log('encrypt', armoredMessage); // prints: "-----BEGIN PGP MESSAGE..." * } */ encrypt(recipients) { return send('editor-encrypt', {recipients, editorId: this.editorId}); } /** * Encrypt and sign the content of the editor with the default key of the user. * To be used to save drafts. To restore drafts use the options.armoredDraft parameter of the createEditorContainer method. * @returns {Promise.<AsciiArmored, Error>} * @throws {Error} error.code = 'ENCRYPT_IN_PROGRESS' <br> * error.code = 'NO_KEY_FOR_ENCRYPTION' <br> * error.code = 'ENCRYPT_QUOTA_SIZE' */ createDraft() { return send('editor-create-draft', {editorId: this.editorId}); } } const callbacks = Object.create(null); class SyncHandler { constructor(syncHandlerId, handlers) { this.syncHandlerId = syncHandlerId; this.handlers = handlers; } update(handlers) { for (const handle in handlers) { this.handlers[handle] = handlers[handle]; } } } function handleSyncEvent({type, id, data}) { let handler = null; switch (type) { case 'upload': handler = syncHandler.handlers.uploadSync; break; case 'download': handler = syncHandler.handlers.downloadSync; break; case 'backup': handler = syncHandler.handlers.backup; break; case 'restore': handler = syncHandler.handlers.restore; break; default: console.log('mailvelope-client-api unknown sync event', type); } if (!handler) { emit('sync-handler-done', {syncHandlerId: syncHandler.syncHandlerId, syncType: type, error: {message: 'Sync handler not available'}, id}); return; } handler(data) .then(result => { emit('sync-handler-done', {syncHandlerId: syncHandler.syncHandlerId, syncType: type, syncData: result, id}); }) .catch(error => { if (!error) { error = new Error('Unknown Error'); } emit('sync-handler-done', {syncHandlerId: syncHandler.syncHandlerId, syncType: type, error: objError(error), id}); }); } function eventListener(msg) { if (msg.origin !== window.location.origin || msg.data.mvelo_client || !msg.data.mvelo_extension) { return; } //console.log('clientAPI eventListener', event.data); switch (msg.data.event) { case 'sync-event': handleSyncEvent(msg.data); break; case '_reply': { let error; if (msg.data.error) { error = mapError(msg.data.error); if (!callbacks[msg.data._reply]) { throw error; } } callbacks[msg.data._reply](error, msg.data.result); delete callbacks[msg.data._reply]; break; } default: console.log('mailvelope-client-api unknown event', msg.data.event); } } function disconnectListener() { window.removeEventListener('message', eventListener); connected = false; } function getHash() { let result = ''; const buf = new Uint16Array(6); window.crypto.getRandomValues(buf); for (let i = 0; i < buf.length; i++) { result += buf[i].toString(16); } return result; } function mapError(obj) { const error = new Error(obj.message); error.code = obj.code; return error; } function objError(error) { if (error instanceof Error || typeof error === 'string') { error = {message: error.message || String(error)}; } return error; } function checkConnection() { if (!connected) { const error = new Error('Connection to Mailvelope extension is no longer alive.'); error.code = 'NO_CONNECTION'; throw error; } } function emit(event, data) { checkConnection(); const message = {...data, event, mvelo_client: true}; window.postMessage(message, window.location.origin); } function send(event, data) { checkConnection(); return new Promise((resolve, reject) => { const message = {...data, event, mvelo_client: true, _reply: getHash()}; callbacks[message._reply] = (err, data) => err ? reject(err) : resolve(data); window.postMessage(message, window.location.origin); }); } export function init() { window.mailvelope = new Mailvelope(); window.addEventListener('message', eventListener); window.addEventListener('mailvelope-disconnect', disconnectListener); window.setTimeout(() => { window.dispatchEvent(new CustomEvent('mailvelope', {detail: window.mailvelope})); }, 1); }