From 2e4e9387a0f7537d5e6de288d9ba9a62ffcdb71d Mon Sep 17 00:00:00 2001 From: Bart Butler Date: Sun, 15 Mar 2015 17:17:06 -0700 Subject: [PATCH] Fixes for symmetrically encrypted session keys --- src/crypto/cfb.js | 7 +- src/message.js | 136 +++++++++++++++++------- src/openpgp.js | 31 +++--- src/packet/sym_encrypted_session_key.js | 34 +++--- src/type/s2k.js | 8 +- src/worker/async_proxy.js | 34 +++--- src/worker/worker.js | 11 +- test/general/basic.js | 21 +++- test/worker/api.js | 68 +++++++++--- 9 files changed, 243 insertions(+), 107 deletions(-) diff --git a/src/crypto/cfb.js b/src/crypto/cfb.js index 14a5bb5b..50b421fe 100644 --- a/src/crypto/cfb.js +++ b/src/crypto/cfb.js @@ -247,7 +247,12 @@ module.exports = { var pos = 0; var cyphertext = ''; var tempBlock = ''; - blockc = iv.substring(0, block_size); + if (iv === null) + for (i = 0; i < block_size; i++) { + blockc += String.fromCharCode(0); + } + else + blockc = iv.substring(0, block_size); while (plaintext.length > block_size * pos) { var encblock = cipherfn.encrypt(util.str2bin(blockc)); blocki = plaintext.substring((pos * block_size), (pos * block_size) + block_size); diff --git a/src/message.js b/src/message.js index 39ba2547..74d1fe66 100644 --- a/src/message.js +++ b/src/message.js @@ -85,31 +85,54 @@ Message.prototype.getSigningKeyIds = function() { /** * Decrypt the message - * @param {module:key~Key} privateKey private key with decrypted secret data + * @param {module:key~Key|String} privateKey private key with decrypted secret data or password * @return {Array} new message with decrypted content */ Message.prototype.decrypt = function(privateKey) { - var encryptionKeyIds = this.getEncryptionKeyIds(); - if (!encryptionKeyIds.length) { - // nothing to decrypt return unmodified message - return this; - } - var privateKeyPacket = privateKey.getKeyPacket(encryptionKeyIds); - if (!privateKeyPacket.isDecrypted) throw new Error('Private key is not decrypted.'); - var pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); - var pkESKeyPacket; - for (var i = 0; i < pkESKeyPacketlist.length; i++) { - if (pkESKeyPacketlist[i].publicKeyId.equals(privateKeyPacket.getKeyId())) { - pkESKeyPacket = pkESKeyPacketlist[i]; - pkESKeyPacket.decrypt(privateKeyPacket); - break; + var keyPacket; + if(String.prototype.isPrototypeOf(privateKey) || typeof privateKey === 'string') { + var symEncryptedSessionKeyPacketlist = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); + var symLength = symEncryptedSessionKeyPacketlist.length; + for (var i = 0; i < symLength; i++) { + keyPacket = symEncryptedSessionKeyPacketlist[i]; + try { + keyPacket.decrypt(privateKey); + break; + } + catch(err) { + if(i === (symLength-1)) { + throw err; + } + } + } + + if(!keyPacket) { + throw new Error('No symmetrically encrypted session key packet found.'); } } - if (pkESKeyPacket) { + else { + var encryptionKeyIds = this.getEncryptionKeyIds(); + if (!encryptionKeyIds.length) { + // nothing to decrypt return unmodified message + return this; + } + var privateKeyPacket = privateKey.getKeyPacket(encryptionKeyIds); + if (!privateKeyPacket.isDecrypted) throw new Error('Private key is not decrypted.'); + var pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); + for (var i = 0; i < pkESKeyPacketlist.length; i++) { + if (pkESKeyPacketlist[i].publicKeyId.equals(privateKeyPacket.getKeyId())) { + keyPacket = pkESKeyPacketlist[i]; + keyPacket.decrypt(privateKeyPacket); + break; + } + } + } + + if (keyPacket) { var symEncryptedPacketlist = this.packets.filterByTag(enums.packet.symmetricallyEncrypted, enums.packet.symEncryptedIntegrityProtected); if (symEncryptedPacketlist.length !== 0) { var symEncryptedPacket = symEncryptedPacketlist[0]; - symEncryptedPacket.decrypt(pkESKeyPacket.sessionKeyAlgorithm, pkESKeyPacket.sessionKey); + symEncryptedPacket.decrypt(keyPacket.sessionKeyAlgorithm, keyPacket.sessionKey); var resultMsg = new Message(symEncryptedPacket.packets); // remove packets after decryption symEncryptedPacket.packets = new packet.List(); @@ -142,27 +165,59 @@ Message.prototype.getText = function() { /** * Encrypt the message - * @param {Array} keys array of keys, used to encrypt the message + * @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 */ -Message.prototype.encrypt = function(keys) { - var packetlist = new packet.List(); - var symAlgo = keyModule.getPreferredSymAlgo(keys); +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) { + symAlgo = keyModule.getPreferredSymAlgo(keys); + } + else if(passwords) { + symAlgo = config.encryption_cipher; + } + else { + throw new Error('No keys or passwords'); + } + var sessionKey = crypto.generateSessionKey(enums.read(enums.symmetric, symAlgo)); - 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()); - } - }); + 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 symEncryptedPacket; if (config.integrity_protect) { symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); @@ -366,13 +421,17 @@ function readSignedContent(content, detachedSignature) { /** * creates new message object from text * @param {String} text + * @param {String} filename (optional) * @return {module:message~Message} new message object * @static */ -function fromText(text) { +function fromText(text, filename) { var literalDataPacket = new packet.Literal(); // text will be converted to UTF8 literalDataPacket.setText(text); + if(filename !== undefined) { + literalDataPacket.setFilename(filename); + } var literalDataPacketlist = new packet.List(); literalDataPacketlist.push(literalDataPacket); return new Message(literalDataPacketlist); @@ -381,7 +440,7 @@ function fromText(text) { /** * creates new message object from binary data * @param {String} bytes - * @param {String} filename + * @param {String} filename (optional) * @return {module:message~Message} new message object * @static */ @@ -391,6 +450,9 @@ function fromBinary(bytes, filename) { literalDataPacket.setFilename(filename); } literalDataPacket.setBytes(bytes, enums.read(enums.literal, enums.literal.binary)); + if(filename !== undefined) { + literalDataPacket.setFilename(filename); + } var literalDataPacketlist = new packet.List(); literalDataPacketlist.push(literalDataPacket); return new Message(literalDataPacketlist); diff --git a/src/openpgp.js b/src/openpgp.js index b330d428..e4ce81f9 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -72,28 +72,24 @@ function getWorker() { } /** - * Encrypts message text with keys - * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the message - * @param {String} text message as native JavaScript string - * @return {Promise} encrypted ASCII armored message + * Encrypts message text/data with keys or passwords + * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the message + * @param {String} text text message as native JavaScript string + * @param {(Array|String)} passwords passwords for the message + * @return {Promise} encrypted ASCII armored message * @static */ -function encryptMessage(keys, text) { - if (!keys.length) { - keys = [keys]; - } +function encryptMessage(keys, text, passwords) { if (asyncProxy) { - return asyncProxy.encryptMessage(keys, text); + return asyncProxy.encryptMessage(keys, text, passwords); } return execute(function() { var msg, armored; msg = message.fromText(text); - msg = msg.encrypt(keys); - armored = armor.encode(enums.armor.message, msg.packets.write()); - return armored; - + msg = msg.encrypt(keys, passwords); + return armor.encode(enums.armor.message, msg.packets.write()); }, 'Error encrypting message!'); } @@ -127,10 +123,10 @@ function signAndEncryptMessage(publicKeys, privateKey, text) { /** * Decrypts message - * @param {module:key~Key} privateKey private key with decrypted secret key data - * @param {module:message~Message} msg the message object with the encrypted data - * @return {Promise<(String|null)>} decrypted message as as native JavaScript string - * or null if no literal data found + * @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 + * @return {Promise<(String|null)>} decrypted message as as native JavaScript string + * or null if no literal data found * @static */ function decryptMessage(privateKey, msg) { @@ -141,7 +137,6 @@ function decryptMessage(privateKey, msg) { return execute(function() { msg = msg.decrypt(privateKey); return msg.getText(); - }, 'Error decrypting message!'); } diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index b5e5ae14..81475214 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -47,6 +47,7 @@ module.exports = SymEncryptedSessionKey; function SymEncryptedSessionKey() { this.tag = enums.packet.symEncryptedSessionKey; this.version = 4; + this.sessionKey = null; this.sessionKeyEncryptionAlgorithm = null; this.sessionKeyAlgorithm = 'aes256'; this.encrypted = null; @@ -109,7 +110,6 @@ SymEncryptedSessionKey.prototype.decrypt = function(passphrase) { this.sessionKeyEncryptionAlgorithm : this.sessionKeyAlgorithm; - var length = crypto.cipher[algo].keySize; var key = this.s2k.produce_key(passphrase, length); @@ -117,30 +117,38 @@ SymEncryptedSessionKey.prototype.decrypt = function(passphrase) { this.sessionKey = key; } else { - var decrypted = crypto.cfb.decrypt( - this.sessionKeyEncryptionAlgorithm, key, this.encrypted, true); - decrypted = decrypted.join(''); + + var decrypted = crypto.cfb.normalDecrypt( + algo, key, this.encrypted, null); this.sessionKeyAlgorithm = enums.read(enums.symmetric, - decrypted[0].keyCodeAt()); + decrypted.charCodeAt(0)); this.sessionKey = decrypted.substr(1); } }; SymEncryptedSessionKey.prototype.encrypt = function(passphrase) { - var length = crypto.getKeyLength(this.sessionKeyEncryptionAlgorithm); + var algo = this.sessionKeyEncryptionAlgorithm !== null ? + this.sessionKeyEncryptionAlgorithm : + this.sessionKeyAlgorithm; + + this.sessionKeyEncryptionAlgorithm = algo; + + var length = crypto.cipher[algo].keySize; var key = this.s2k.produce_key(passphrase, length); - var private_key = String.fromCharCode( - enums.write(enums.symmetric, this.sessionKeyAlgorithm)) + + var algo_enum = String.fromCharCode( + enums.write(enums.symmetric, this.sessionKeyAlgorithm)); - crypto.getRandomBytes( - crypto.getKeyLength(this.sessionKeyAlgorithm)); + var private_key; + if(this.sessionKey === null) { + this.sessionKey = crypto.getRandomBytes(crypto.cipher[this.sessionKeyAlgorithm].keySize); + } + private_key = algo_enum + this.sessionKey; - this.encrypted = crypto.cfb.encrypt( - crypto.getPrefixRandom(this.sessionKeyEncryptionAlgorithm), - this.sessionKeyEncryptionAlgorithm, key, private_key, true); + this.encrypted = crypto.cfb.normalEncrypt( + algo, key, private_key, null); }; /** diff --git a/src/type/s2k.js b/src/type/s2k.js index e4d0fab7..e51b2887 100644 --- a/src/type/s2k.js +++ b/src/type/s2k.js @@ -191,9 +191,9 @@ S2K.prototype.produce_key = function (passphrase, numBytes) { module.exports.fromClone = function (clone) { var s2k = new S2K(); - this.algorithm = clone.algorithm; - this.type = clone.type; - this.c = clone.c; - this.salt = clone.salt; + s2k.algorithm = clone.algorithm; + s2k.type = clone.type; + s2k.c = clone.c; + s2k.salt = clone.salt; return s2k; }; diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index 66c36264..3fb63081 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -130,24 +130,28 @@ AsyncProxy.prototype.terminate = function() { }; /** - * Encrypts message text with keys - * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the message - * @param {String} text message as native JavaScript string + * Encrypts message text/data with keys or passwords + * @param {(Array|module:key~Key)} keys array of keys or single key, used to encrypt the message + * @param {String} text text message as native JavaScript string/binary string + * @param {(Array|String)} passwords passwords for the message */ -AsyncProxy.prototype.encryptMessage = function(keys, text) { +AsyncProxy.prototype.encryptMessage = function(keys, text, passwords) { var self = this; return self.execute(function() { - if (!keys.length) { - keys = [keys]; + if(keys) { + if (!Array.prototype.isPrototypeOf(keys)) { + keys = [keys]; + } + keys = keys.map(function(key) { + return key.toPacketlist(); + }); } - keys = keys.map(function(key) { - return key.toPacketlist(); - }); self.worker.postMessage({ event: 'encrypt-message', keys: keys, - text: text + text: text, + passwords: passwords }); }); }; @@ -180,14 +184,18 @@ AsyncProxy.prototype.signAndEncryptMessage = function(publicKeys, privateKey, te /** * Decrypts message - * @param {module:key~Key} privateKey private key with decrypted secret key data - * @param {module:message~Message} message the message object with the encrypted data + * @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 */ AsyncProxy.prototype.decryptMessage = function(privateKey, message) { var self = this; return self.execute(function() { - privateKey = privateKey.toPacketlist(); + if(!(String.prototype.isPrototypeOf(privateKey) || typeof privateKey === 'string')) { + privateKey = privateKey.toPacketlist(); + } + self.worker.postMessage({ event: 'decrypt-message', privateKey: privateKey, diff --git a/src/worker/worker.js b/src/worker/worker.js index beb81aec..a7fc8ab4 100644 --- a/src/worker/worker.js +++ b/src/worker/worker.js @@ -65,11 +65,10 @@ self.onmessage = function (event) { window.openpgp.crypto.random.randomBuffer.set(msg.buf); break; case 'encrypt-message': - if (!msg.keys.length) { - msg.keys = [msg.keys]; + if(msg.keys) { + msg.keys = msg.keys.map(packetlistCloneToKey); } - msg.keys = msg.keys.map(packetlistCloneToKey); - window.openpgp.encryptMessage(msg.keys, msg.text).then(function(data) { + window.openpgp.encryptMessage(msg.keys, msg.text, msg.passwords).then(function(data) { response({event: 'method-return', data: data}); }).catch(function(e) { response({event: 'method-return', err: e.message}); @@ -88,7 +87,9 @@ self.onmessage = function (event) { }); break; case 'decrypt-message': - msg.privateKey = packetlistCloneToKey(msg.privateKey); + if(!(String.prototype.isPrototypeOf(msg.privateKey) || typeof msg.privateKey === 'string')) { + msg.privateKey = packetlistCloneToKey(msg.privateKey); + } msg.message = packetlistCloneToMessage(msg.message.packets); window.openpgp.decryptMessage(msg.privateKey, msg.message).then(function(data) { response({event: 'method-return', data: data}); diff --git a/test/general/basic.js b/test/general/basic.js index 07977adc..46f4bb48 100644 --- a/test/general/basic.js +++ b/test/general/basic.js @@ -243,6 +243,9 @@ describe('Basic', function() { var plaintext = 'short message\nnext line\n한국어/조선말'; + var password1 = 'I am a password'; + var password2 = 'I am another password'; + var privKey, message, keyids; it('Test initialization', function (done) { @@ -256,7 +259,7 @@ describe('Basic', function() { expect(pubKey).to.exist; - openpgp.encryptMessage([pubKey], plaintext).then(function(encrypted) { + openpgp.encryptMessage([pubKey], plaintext, [password1, password2]).then(function(encrypted) { expect(encrypted).to.exist; @@ -315,6 +318,22 @@ describe('Basic', function() { }); }); + it('Decrypt with password1 leads to the same result', function (done) { + openpgp.decryptMessage(password1, message).then(function(decrypted) { + expect(decrypted).to.exist; + expect(decrypted).to.equal(plaintext); + done(); + }); + }); + + it('Decrypt with password2 leads to the same result', function (done) { + openpgp.decryptMessage(password2, message).then(function(decrypted) { + expect(decrypted).to.exist; + expect(decrypted).to.equal(plaintext); + done(); + }); + }); + it('Decrypt message 2x', function(done) { var decrypted1; diff --git a/test/worker/api.js b/test/worker/api.js index 07bbcc73..100bc81b 100644 --- a/test/worker/api.js +++ b/test/worker/api.js @@ -157,20 +157,23 @@ var priv_key_de = '-----END PGP PRIVATE KEY BLOCK-----'].join('\n'); - var plaintext = 'short message\nnext line\n한국어/조선말'; +var plaintext = 'short message\nnext line\n한국어/조선말'; - var pubKeyRSA, privKeyRSA, pubKeyDE, privKeyDE; +var password1 = 'I am a password'; +var password2 = 'I am another password'; - function initKeys() { - pubKeyRSA = openpgp.key.readArmored(pub_key_rsa).keys[0]; - expect(pubKeyRSA).to.exist; - privKeyRSA = openpgp.key.readArmored(priv_key_rsa).keys[0]; - expect(privKeyRSA).to.exist; - pubKeyDE = openpgp.key.readArmored(pub_key_de).keys[0]; - expect(pubKeyDE).to.exist; - privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0]; - expect(privKeyDE).to.exist; - } +var pubKeyRSA, privKeyRSA, pubKeyDE, privKeyDE; + +function initKeys() { + pubKeyRSA = openpgp.key.readArmored(pub_key_rsa).keys[0]; + expect(pubKeyRSA).to.exist; + privKeyRSA = openpgp.key.readArmored(priv_key_rsa).keys[0]; + expect(privKeyRSA).to.exist; + pubKeyDE = openpgp.key.readArmored(pub_key_de).keys[0]; + expect(pubKeyDE).to.exist; + privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0]; + expect(privKeyDE).to.exist; +} describe('Init Worker', function() { @@ -215,8 +218,8 @@ describe('High level API', function() { }); describe('Encryption', function() { - it('RSA: encryptMessage async', function (done) { - openpgp.encryptMessage([pubKeyRSA], plaintext).then(function(data) { + it('AES: encryptMessage one password async', function (done) { + openpgp.encryptMessage([], plaintext, password1).then(function(data) { expect(data).to.exist; expect(data).to.match(/^-----BEGIN PGP MESSAGE/); var msg = openpgp.message.readArmored(data); @@ -235,6 +238,26 @@ describe('High level API', function() { }); }); + it('RSA: encryptMessage one key one password async', function (done) { + openpgp.encryptMessage(pubKeyRSA, plaintext, password1).then(function(data) { + expect(data).to.exist; + expect(data).to.match(/^-----BEGIN PGP MESSAGE/); + var msg = openpgp.message.readArmored(data); + expect(msg).to.be.an.instanceof(openpgp.message.Message); + done(); + }); + }); + + it('RSA: encryptMessage one key two passwords async', function (done) { + openpgp.encryptMessage(pubKeyRSA, plaintext, [password1, password2]).then(function(data) { + expect(data).to.exist; + expect(data).to.match(/^-----BEGIN PGP MESSAGE/); + var msg = openpgp.message.readArmored(data); + expect(msg).to.be.an.instanceof(openpgp.message.Message); + done(); + }); + }); + it('ELG: encryptMessage async', function (done) { openpgp.encryptMessage([pubKeyDE], plaintext).then(function(data) { expect(data).to.exist; @@ -254,7 +277,7 @@ describe('High level API', function() { before(function() { privKeyRSA.decrypt('hello world'); privKeyDE.decrypt('hello world'); - msgRSA = openpgp.message.fromText(plaintext).encrypt([pubKeyRSA]); + msgRSA = openpgp.message.fromText(plaintext).encrypt([pubKeyRSA],[password1, password2]); msgDE = openpgp.message.fromText(plaintext).encrypt([pubKeyDE]); }); @@ -274,6 +297,21 @@ describe('High level API', function() { }); }); + it('AES: decryptMessage password1 async', function (done) { + openpgp.decryptMessage(password1, msgRSA).then(function(data) { + expect(data).to.exist; + expect(data).to.equal(plaintext); + done(); + }); + }); + + it('AES: decryptMessage password2 async', function (done) { + openpgp.decryptMessage(password2, msgRSA).then(function(data) { + expect(data).to.exist; + expect(data).to.equal(plaintext); + done(); + }); + }); }); function verifySignature(data, privKey) {