diff --git a/src/message.js b/src/message.js index 74d1fe66..6c868e6a 100644 --- a/src/message.js +++ b/src/message.js @@ -85,10 +85,38 @@ Message.prototype.getSigningKeyIds = function() { /** * Decrypt the message - * @param {module:key~Key|String} privateKey private key with decrypted secret data or password + * @param {module:key~Key|String} privateKey private key with decrypted secret data, password or session key + * @param {String} sessionKeyAlgorithm if privateKey is a session key, this must be set to the session key algorithm (i.e. 'aes256'). + * Do not set if privateKey is not a session key. * @return {Array} new message with decrypted content */ -Message.prototype.decrypt = function(privateKey) { +Message.prototype.decrypt = function(privateKey, sessionKeyAlgorithm) { + var keyObj; + if(sessionKeyAlgorithm) { + keyObj = {key: privateKey, algo: sessionKeyAlgorithm}; + } + else { + keyObj = this.decryptSessionKey(privateKey); + } + if(keyObj) { + var symEncryptedPacketlist = this.packets.filterByTag(enums.packet.symmetricallyEncrypted, enums.packet.symEncryptedIntegrityProtected); + if (symEncryptedPacketlist.length !== 0) { + var symEncryptedPacket = symEncryptedPacketlist[0]; + symEncryptedPacket.decrypt(keyObj.algo, keyObj.key); + var resultMsg = new Message(symEncryptedPacket.packets); + // remove packets after decryption + symEncryptedPacket.packets = new packet.List(); + return resultMsg; + } + } +}; + +/** + * Decrypt session key + * @param {module:key~Key|String} privateKey private key with decrypted secret data or password + * @return {Object} object with sessionKey, algo + */ +Message.prototype.decryptSessionKey = function(privateKey) { var keyPacket; if(String.prototype.isPrototypeOf(privateKey) || typeof privateKey === 'string') { var symEncryptedSessionKeyPacketlist = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); @@ -129,15 +157,7 @@ Message.prototype.decrypt = function(privateKey) { } if (keyPacket) { - var symEncryptedPacketlist = this.packets.filterByTag(enums.packet.symmetricallyEncrypted, enums.packet.symEncryptedIntegrityProtected); - if (symEncryptedPacketlist.length !== 0) { - var symEncryptedPacket = symEncryptedPacketlist[0]; - symEncryptedPacket.decrypt(keyPacket.sessionKeyAlgorithm, keyPacket.sessionKey); - var resultMsg = new Message(symEncryptedPacket.packets); - // remove packets after decryption - symEncryptedPacket.packets = new packet.List(); - return resultMsg; - } + return {key: keyPacket.sessionKey, algo: keyPacket.sessionKeyAlgorithm}; } }; @@ -171,14 +191,6 @@ Message.prototype.getText = function() { */ Message.prototype.encrypt = function(keys, passwords) { - /** Convert to arrays if necessary */ - if(keys && !Array.prototype.isPrototypeOf(keys)) { - keys = [keys] - } - if(passwords && !Array.prototype.isPrototypeOf(passwords)) { - passwords = [passwords] - } - /** Choose symAlgo */ var symAlgo; if(keys) { @@ -192,32 +204,10 @@ Message.prototype.encrypt = function(keys, passwords) { } var sessionKey = crypto.generateSessionKey(enums.read(enums.symmetric, symAlgo)); - var packetlist = new packet.List(); - if(keys) { - keys.forEach(function(key) { - var encryptionKeyPacket = key.getEncryptionKeyPacket(); - if (encryptionKeyPacket) { - var pkESKeyPacket = new packet.PublicKeyEncryptedSessionKey(); - pkESKeyPacket.publicKeyId = encryptionKeyPacket.getKeyId(); - pkESKeyPacket.publicKeyAlgorithm = encryptionKeyPacket.algorithm; - pkESKeyPacket.sessionKey = sessionKey; - pkESKeyPacket.sessionKeyAlgorithm = enums.read(enums.symmetric, symAlgo); - pkESKeyPacket.encrypt(encryptionKeyPacket); - packetlist.push(pkESKeyPacket); - } else { - throw new Error('Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex()); - } - }); - } - if(passwords) { - passwords.forEach(function(password) { - var symEncryptedSessionKeyPacket = new packet.SymEncryptedSessionKey(); - symEncryptedSessionKeyPacket.sessionKey = sessionKey; - symEncryptedSessionKeyPacket.sessionKeyAlgorithm = enums.read(enums.symmetric, symAlgo); - symEncryptedSessionKeyPacket.encrypt(password); - packetlist.push(symEncryptedSessionKeyPacket); - }); - } + + var msg = encryptSessionKey(sessionKey, enums.read(enums.symmetric, symAlgo), keys, passwords); + var packetlist = msg.packets; + var symEncryptedPacket; if (config.integrity_protect) { symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); @@ -229,6 +219,54 @@ Message.prototype.encrypt = function(keys, passwords) { packetlist.push(symEncryptedPacket); // remove packets after encryption symEncryptedPacket.packets = new packet.List(); + return msg; +}; + +/** + * Encrypt a session key + * @param {String} session key for encryption + * @param {String} session key algorithm + * @param {(Array|module:key~Key)} public key(s) for message encryption + * @param {(Array|String)} password(s) for message encryption + * @return {Array} new message with encrypted content + */ +function encryptSessionKey(sessionKey, symAlgo, keys, passwords) { + + /** Convert to arrays if necessary */ + if(keys && !Array.prototype.isPrototypeOf(keys)) { + keys = [keys] + } + if(passwords && !Array.prototype.isPrototypeOf(passwords)) { + passwords = [passwords] + } + + var packetlist = new packet.List(); + if(keys) { + keys.forEach(function(key) { + var encryptionKeyPacket = key.getEncryptionKeyPacket(); + if (encryptionKeyPacket) { + var pkESKeyPacket = new packet.PublicKeyEncryptedSessionKey(); + pkESKeyPacket.publicKeyId = encryptionKeyPacket.getKeyId(); + pkESKeyPacket.publicKeyAlgorithm = encryptionKeyPacket.algorithm; + pkESKeyPacket.sessionKey = sessionKey; + pkESKeyPacket.sessionKeyAlgorithm = symAlgo; + pkESKeyPacket.encrypt(encryptionKeyPacket); + packetlist.push(pkESKeyPacket); + } else { + throw new Error('Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex()); + } + }); + } + if(passwords) { + passwords.forEach(function(password) { + var symEncryptedSessionKeyPacket = new packet.SymEncryptedSessionKey(); + symEncryptedSessionKeyPacket.sessionKey = sessionKey; + symEncryptedSessionKeyPacket.sessionKeyAlgorithm = symAlgo; + symEncryptedSessionKeyPacket.encrypt(password); + packetlist.push(symEncryptedSessionKeyPacket); + }); + } + return new Message(packetlist); }; @@ -398,6 +436,16 @@ function readArmored(armoredText) { //TODO how do we want to handle bad text? Exception throwing //TODO don't accept non-message armored texts var input = armor.decode(armoredText).data; + return read(input); +} + +/** + * reads an OpenPGP binary string message and returns a message object + * @param {String} binary string message + * @return {module:message~Message} new message object + * @static + */ +function read(input) { var packetlist = new packet.List(); packetlist.read(input); return new Message(packetlist); @@ -459,7 +507,9 @@ function fromBinary(bytes, filename) { } exports.Message = Message; +exports.read = read; exports.readArmored = readArmored; exports.readSignedContent = readSignedContent; exports.fromText = fromText; exports.fromBinary = fromBinary; +exports.encryptSessionKey = encryptSessionKey; diff --git a/src/openpgp.js b/src/openpgp.js index 022b1ef2..f515f1e0 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -105,7 +105,11 @@ function encryptMessage(keys, data, passwords, params) { msg = msg.encrypt(keys, passwords); if(packets) { - return msg.packets; + var arr = []; + var dataIndex = msg.packets.indexOfTag(enums.packet.symmetricallyEncrypted, enums.packet.symEncryptedIntegrityProtected)[0]; + arr.push(msg.packets.slice(0,dataIndex).write()); // Keys + arr.push(msg.packets.slice(dataIndex,msg.packets.length).write()); // Data + return arr; } else { return armor.encode(enums.armor.message, msg.packets.write()); @@ -114,6 +118,29 @@ function encryptMessage(keys, data, passwords, params) { }, 'Error encrypting message!'); } +/** + * Encrypts session key with keys or passwords + * @param {String} sessionKey sessionKey as a binary string + * @param {String} algo algorithm of sessionKey + * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the key + * @param {(Array|String)} passwords passwords for the message + * @return {Promise} Binary string of key packets + * @static + */ +function encryptSessionKey(sessionKey, algo, keys, passwords) { + + if (asyncProxy) { + return asyncProxy.encryptSessionKey(sessionKey, algo, keys, passwords); + } + + return execute(function() { + + var msg = message.encryptSessionKey(sessionKey, algo, keys, passwords); + return msg.packets.write(); + + }, 'Error encrypting session key!'); +} + /** * Signs message text and encrypts it * @param {(Array|module:key~Key)} publicKeys array of keys or single key, used to encrypt the message @@ -144,20 +171,27 @@ function signAndEncryptMessage(publicKeys, privateKey, text) { /** * Decrypts message - * @param {module:key~Key|String} privateKey private key with decrypted secret key data or string password + * @param {module:key~Key|String} privateKey private key with decrypted secret key data, string password, or session key * @param {module:message~Message} msg the message object with the encrypted data - * @param {Boolean} binary if true, return literal data binaryString instead of converting from UTF-8 + * @param {Object} params parameter object with optional properties binary {Boolean} + * and sessionKeyAlgorithm {String} which must only be set when privateKey is a session key * @return {Promise<(String|null)>} decrypted message as as native JavaScript string * or null if no literal data found * @static */ -function decryptMessage(privateKey, msg, binary) { +function decryptMessage(privateKey, msg, params) { if (asyncProxy) { - return asyncProxy.decryptMessage(privateKey, msg, binary); + return asyncProxy.decryptMessage(privateKey, msg, params); + } + + var binary, sessionKeyAlgorithm; + if(params) { + binary = params.binary; + sessionKeyAlgorithm = params.sessionKeyAlgorithm; } return execute(function() { - msg = msg.decrypt(privateKey); + msg = msg.decrypt(privateKey, sessionKeyAlgorithm); if(binary) { return msg.getLiteralData(); } @@ -168,6 +202,26 @@ function decryptMessage(privateKey, msg, binary) { }, 'Error decrypting message!'); } +/** + * Decrypts message + * @param {module:key~Key|String} privateKey private key with decrypted secret key data or string password + * @param {module:message~Message} msg the message object with the encrypted session key packets + * @return {Promise} decrypted session key and algorithm in object form + * or null if no key packets found + * @static + */ +function decryptSessionKey(privateKey, msg) { + if (asyncProxy) { + return asyncProxy.decryptSessionKey(privateKey, msg); + } + + return execute(function() { + var obj = msg.decryptSessionKey(privateKey); + return obj; + + }, 'Error decrypting session key!'); +} + /** * Decrypts message and verifies signatures * @param {module:key~Key} privateKey private key with decrypted secret key data @@ -331,8 +385,10 @@ function onError(message, error) { exports.initWorker = initWorker; exports.getWorker = getWorker; exports.encryptMessage = encryptMessage; +exports.encryptSessionKey = encryptSessionKey; exports.signAndEncryptMessage = signAndEncryptMessage; exports.decryptMessage = decryptMessage; +exports.decryptSessionKey = decryptSessionKey; exports.decryptAndVerifyMessage = decryptAndVerifyMessage; exports.signClearMessage = signClearMessage; exports.verifyClearSignedMessage = verifyClearSignedMessage; diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index 5749d5f0..eedb787e 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -159,6 +159,35 @@ AsyncProxy.prototype.encryptMessage = function(keys, data, passwords, params) { }); }; +/** + * Encrypts session key with keys or passwords + * @param {String} sessionKey sessionKey as a binary string + * @param {String} algo algorithm of sessionKey + * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the key + * @param {(Array|String)} passwords passwords for the message + */ +AsyncProxy.prototype.encryptSessionKey = function(sessionKey, algo, keys, passwords) { + var self = this; + + return self.execute(function() { + if(keys) { + if (!Array.prototype.isPrototypeOf(keys)) { + keys = [keys]; + } + keys = keys.map(function(key) { + return key.toPacketlist(); + }); + } + self.worker.postMessage({ + event: 'encrypt-session-key', + sessionKey: sessionKey, + algo: algo, + keys: keys, + passwords: passwords + }); + }); +}; + /** * Signs message text and encrypts it * @param {(Array|module:key~Key)} publicKeys array of keys or single key, used to encrypt the message @@ -189,9 +218,10 @@ AsyncProxy.prototype.signAndEncryptMessage = function(publicKeys, privateKey, te * Decrypts message * @param {module:key~Key|String} privateKey private key with decrypted secret key data or string password * @param {module:message~Message} msg the message object with the encrypted data - * @param {Boolean} binary if true, return literal data binaryString instead of converting from UTF-8 + * @param {Object} params parameter object with optional properties binary {Boolean} + * and sessionKeyAlgorithm {String} which must only be set when privateKey is a session key */ -AsyncProxy.prototype.decryptMessage = function(privateKey, message, binary) { +AsyncProxy.prototype.decryptMessage = function(privateKey, message, params) { var self = this; return self.execute(function() { @@ -203,7 +233,29 @@ AsyncProxy.prototype.decryptMessage = function(privateKey, message, binary) { event: 'decrypt-message', privateKey: privateKey, message: message, - binary: binary + params: params + }); + }); +}; + +/** + * @param {module:key~Key|String} privateKey private key with decrypted secret key data or string password + * @param {module:message~Message} msg the message object with the encrypted session key packets + * @return {Promise} decrypted session key and algorithm in object form + * or null if no key packets found + */ +AsyncProxy.prototype.decryptSessionKey = function(privateKey, message) { + var self = this; + + return self.execute(function() { + if(!(String.prototype.isPrototypeOf(privateKey) || typeof privateKey === 'string')) { + privateKey = privateKey.toPacketlist(); + } + + self.worker.postMessage({ + event: 'decrypt-session-key', + privateKey: privateKey, + message: message }); }); }; diff --git a/src/worker/worker.js b/src/worker/worker.js index ca3fa27c..08dc4455 100644 --- a/src/worker/worker.js +++ b/src/worker/worker.js @@ -74,6 +74,16 @@ self.onmessage = function (event) { response({event: 'method-return', err: e.message}); }); break; + case 'encrypt-session-key': + if(msg.keys) { + msg.keys = msg.keys.map(packetlistCloneToKey); + } + window.openpgp.encryptSessionKey(msg.sessionKey, msg.algo, msg.keys, msg.passwords).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); + break; case 'sign-and-encrypt-message': if (!msg.publicKeys.length) { msg.publicKeys = [msg.publicKeys]; @@ -91,7 +101,18 @@ self.onmessage = function (event) { msg.privateKey = packetlistCloneToKey(msg.privateKey); } msg.message = packetlistCloneToMessage(msg.message.packets); - window.openpgp.decryptMessage(msg.privateKey, msg.message, msg.binary).then(function(data) { + window.openpgp.decryptMessage(msg.privateKey, msg.message, msg.params).then(function(data) { + response({event: 'method-return', data: data}); + }).catch(function(e) { + response({event: 'method-return', err: e.message}); + }); + break; + case 'decrypt-session-key': + if(!(String.prototype.isPrototypeOf(msg.privateKey) || typeof msg.privateKey === 'string')) { + msg.privateKey = packetlistCloneToKey(msg.privateKey); + } + msg.message = packetlistCloneToMessage(msg.message.packets); + window.openpgp.decryptSessionKey(msg.privateKey, msg.message).then(function(data) { response({event: 'method-return', data: data}); }).catch(function(e) { response({event: 'method-return', err: e.message}); diff --git a/test/general/basic.js b/test/general/basic.js index 46f4bb48..3fdcffe7 100644 --- a/test/general/basic.js +++ b/test/general/basic.js @@ -259,10 +259,15 @@ describe('Basic', function() { expect(pubKey).to.exist; - openpgp.encryptMessage([pubKey], plaintext, [password1, password2]).then(function(encrypted) { + var params = { + packets: true + }; + + openpgp.encryptMessage([pubKey], plaintext, [password1, password2], params).then(function(encrypted) { expect(encrypted).to.exist; - + encrypted = encrypted.join(''); + encrypted = openpgp.armor.encode(openpgp.enums.armor.message, encrypted); message = openpgp.message.readArmored(encrypted); expect(message).to.exist; diff --git a/test/worker/api.js b/test/worker/api.js index 100bc81b..e12f9ef2 100644 --- a/test/worker/api.js +++ b/test/worker/api.js @@ -272,13 +272,16 @@ describe('High level API', function() { describe('Decryption', function() { - var msgRSA, msgDE; + var msgRSA, msgDE, msgAES, keys, data; before(function() { privKeyRSA.decrypt('hello world'); privKeyDE.decrypt('hello world'); msgRSA = openpgp.message.fromText(plaintext).encrypt([pubKeyRSA],[password1, password2]); msgDE = openpgp.message.fromText(plaintext).encrypt([pubKeyDE]); + msgAES = openpgp.message.fromText(plaintext).encrypt([],[password1]); + var dataIndex = msgAES.packets.indexOfTag(openpgp.enums.packet.symmetricallyEncrypted, openpgp.enums.packet.symEncryptedIntegrityProtected)[0]; + data = msgAES.packets.slice(dataIndex,msgAES.packets.length).write(); }); it('RSA: decryptMessage async', function (done) { @@ -312,6 +315,43 @@ describe('High level API', function() { done(); }); }); + + it('RSA: decryptSessionKey/encryptSessionKey/decryptMessage async', function (done) { + openpgp.decryptSessionKey(password1, msgAES).then(function(sk) { + return openpgp.encryptSessionKey(sk.key, sk.algo, pubKeyRSA); + }).then(function(keypacket) { + var msg = openpgp.message.read([keypacket, data].join('')); + return openpgp.decryptMessage(privKeyRSA, msg); + }).then(function(data) { + expect(data).to.exist; + expect(data).to.equal(plaintext); + done(); + }); + }); + + it('AES: decryptSessionKey/encryptSessionKey/decryptMessage async', function (done) { + openpgp.decryptSessionKey(password1, msgAES).then(function(sk) { + return openpgp.encryptSessionKey(sk.key, sk.algo, [], password2); + }).then(function(keypacket) { + var msg = openpgp.message.read([keypacket, data].join('')); + return openpgp.decryptMessage(password2, msg); + }).then(function(data) { + expect(data).to.exist; + expect(data).to.equal(plaintext); + done(); + }); + }); + + it('AES: decryptSessionKey/decryptMessage with session key async', function (done) { + openpgp.decryptSessionKey(password1, msgAES).then(function(sk) { + var msg = openpgp.message.read(data); + return openpgp.decryptMessage(sk.key, msg, {sessionKeyAlgorithm: sk.algo}); + }).then(function(data) { + expect(data).to.exist; + expect(data).to.equal(plaintext); + done(); + }); + }); }); function verifySignature(data, privKey) {