diff --git a/Gruntfile.js b/Gruntfile.js index 0a7505b5..6e61f5de 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -107,9 +107,12 @@ module.exports = function(grunt) { transform: [ ["babelify", { global: true, + // Only babelify chai-as-promised in node_modules + only: /^(?:.*\/node_modules\/chai-as-promised\/|(?!.*\/node_modules\/)).*$/, plugins: ["transform-async-to-generator", "syntax-async-functions", "transform-regenerator", + "transform-runtime", "transform-remove-strict-mode"], ignore: ['*.min.js'], presets: ["env"] diff --git a/src/crypto/pkcs1.js b/src/crypto/pkcs1.js index 38dc0671..c0cc29cf 100644 --- a/src/crypto/pkcs1.js +++ b/src/crypto/pkcs1.js @@ -50,13 +50,15 @@ hash_headers[11] = [0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, * @param {Integer} length Length of the padding in bytes * @return {String} Padding as string */ -function getPkcs1Padding(length) { +async function getPkcs1Padding(length) { let result = ''; - let randomByte; while (result.length < length) { - randomByte = random.getSecureRandomOctet(); - if (randomByte !== 0) { - result += String.fromCharCode(randomByte); + // eslint-disable-next-line no-await-in-loop + const randomBytes = await random.getRandomBytes(length - result.length); + for (let i = 0; i < randomBytes.length; i++) { + if (randomBytes[i] !== 0) { + result += String.fromCharCode(randomBytes[i]); + } } } return result; @@ -69,9 +71,9 @@ export default { * create a EME-PKCS1-v1_5 padding (See {@link https://tools.ietf.org/html/rfc4880#section-13.1.1|RFC 4880 13.1.1}) * @param {String} M message to be encoded * @param {Integer} k the length in octets of the key modulus - * @return {String} EME-PKCS1 padded message + * @return {Promise} EME-PKCS1 padded message */ - encode: function(M, k) { + encode: async function(M, k) { const mLen = M.length; // length checking if (mLen > k - 11) { @@ -79,15 +81,14 @@ export default { } // Generate an octet string PS of length k - mLen - 3 consisting of // pseudo-randomly generated nonzero octets - const PS = getPkcs1Padding(k - mLen - 3); + const PS = await getPkcs1Padding(k - mLen - 3); // Concatenate PS, the message M, and other padding to form an // encoded message EM of length k octets as EM = 0x00 || 0x02 || PS || 0x00 || M. - const EM = String.fromCharCode(0) + - String.fromCharCode(2) + - PS + - String.fromCharCode(0) + - M; - return EM; + return String.fromCharCode(0) + + String.fromCharCode(2) + + PS + + String.fromCharCode(0) + + M; }, /** * decodes a EME-PKCS1-v1_5 padding (See {@link https://tools.ietf.org/html/rfc4880#section-13.1.2|RFC 4880 13.1.2}) diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index ee1c5e16..f30fc9d8 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -48,7 +48,7 @@ export default { * g, p, q, x are all BN * returns { r: BN, s: BN } */ - sign: function(hash_algo, m, g, p, q, x) { + sign: async function(hash_algo, m, g, p, q, x) { let k; let r; let s; @@ -73,7 +73,8 @@ export default { // or s = 0 if signatures are generated properly. while (true) { // See Appendix B here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - k = random.getRandomBN(one, q); // returns in [1, q-1] + // eslint-disable-next-line no-await-in-loop + k = await random.getRandomBN(one, q); // returns in [1, q-1] r = gred.redPow(k).fromRed().toRed(redq); // (g**k mod p) mod q if (zero.cmp(r) === 0) { continue; @@ -96,7 +97,7 @@ export default { * p, q, g, y are all BN * returns BN */ - verify: function(hash_algo, r, s, m, p, q, g, y) { + verify: async function(hash_algo, r, s, m, p, q, g, y) { if (zero.ucmp(r) >= 0 || r.ucmp(q) >= 0 || zero.ucmp(s) >= 0 || s.ucmp(q) >= 0) { util.print_debug("invalid DSA Signature"); diff --git a/src/crypto/public_key/elgamal.js b/src/crypto/public_key/elgamal.js index b48402e6..678b0561 100644 --- a/src/crypto/public_key/elgamal.js +++ b/src/crypto/public_key/elgamal.js @@ -33,13 +33,13 @@ export default { * m, p, g, y are all BN * returns { c1: BN, c2: BN } */ - encrypt: function(m, p, g, y) { + encrypt: async function(m, p, g, y) { const redp = new BN.red(p); const mred = m.toRed(redp); const gred = g.toRed(redp); const yred = y.toRed(redp); // See Section 11.5 here: https://crypto.stanford.edu/~dabo/cryptobook/BonehShoup_0_4.pdf - const k = random.getRandomBN(zero, p); // returns in [0, p-1] + const k = await random.getRandomBN(zero, p); // returns in [0, p-1] return { c1: gred.redPow(k).fromRed(), c2: yred.redPow(k).redMul(mred).fromRed() @@ -50,7 +50,7 @@ export default { * c1, c2, p, x are all BN * returns BN */ - decrypt: function(c1, c2, p, x) { + decrypt: async function(c1, c2, p, x) { const redp = new BN.red(p); const c1red = c1.toRed(redp); const c2red = c2.toRed(redp); diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index cd1dd8ed..3508002c 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -178,7 +178,7 @@ Curve.prototype.genKeyPair = async function () { if (!keyPair || !keyPair.priv) { // elliptic fallback const r = await this.curve.genKeyPair({ - entropy: util.Uint8Array_to_str(random.getRandomBytes(32)) + entropy: util.Uint8Array_to_str(await random.getRandomBytes(32)) }); const compact = this.curve.curve.type === 'edwards' || this.curve.curve.type === 'mont'; if (this.keyType === enums.publicKey.eddsa) { diff --git a/src/crypto/public_key/prime.js b/src/crypto/public_key/prime.js index 53339f1a..95f232c9 100644 --- a/src/crypto/public_key/prime.js +++ b/src/crypto/public_key/prime.js @@ -37,15 +37,16 @@ export default { * @param {Integer} k Optional number of iterations of Miller-Rabin test * @return BN */ -function randomProbablePrime(bits, e, k) { +async function randomProbablePrime(bits, e, k) { const min = new BN(1).shln(bits - 1); - let n = random.getRandomBN(min, min.shln(1)); + let n = await random.getRandomBN(min, min.shln(1)); if (n.isEven()) { n.iaddn(1); // force odd } - while (!isProbablePrime(n, e, k)) { + // eslint-disable-next-line no-await-in-loop + while (!await isProbablePrime(n, e, k)) { n.iaddn(2); // If reached the maximum, go back to the minimum. if (n.bitLength() > bits) { @@ -62,17 +63,17 @@ function randomProbablePrime(bits, e, k) { * @param {Integer} k Optional number of iterations of Miller-Rabin test * @return {boolean} */ -function isProbablePrime(n, e, k) { +async function isProbablePrime(n, e, k) { if (e && !n.subn(1).gcd(e).eqn(1)) { return false; } if (!fermat(n)) { return false; } - if (!millerRabin(n, k, () => new BN(lowprimes[Math.random() * lowprimes.length | 0]))) { + if (!await millerRabin(n, k, () => new BN(lowprimes[Math.random() * lowprimes.length | 0]))) { return false; } - if (!millerRabin(n, k)) { + if (!await millerRabin(n, k)) { return false; } // TODO implement the Lucas test @@ -138,7 +139,7 @@ const lowprimes = [ * @param {Function} rand Optional function to generate potential witnesses * @return {boolean} */ -function millerRabin(n, k, rand) { +async function millerRabin(n, k, rand) { const len = n.bitLength(); const red = BN.mont(n); const rone = new BN(1).toRed(red); @@ -155,7 +156,8 @@ function millerRabin(n, k, rand) { const d = n.shrn(s); for (; k > 0; k--) { - let a = rand ? rand() : random.getRandomBN(new BN(2), n1); + // eslint-disable-next-line no-await-in-loop + let a = rand ? rand() : await random.getRandomBN(new BN(2), n1); let x = a.toRed(red).redPow(d); if (x.eq(rone) || x.eq(rn1)) diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index baae76ca..ac5c9e2f 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -55,7 +55,7 @@ export default { * @param d private MPI part as BN * @return BN */ - sign: function(m, n, e, d) { + sign: async function(m, n, e, d) { if (n.cmp(m) <= 0) { throw new Error('Data too large.'); } @@ -70,7 +70,7 @@ export default { * @param e public MPI part as BN * @return BN */ - verify: function(s, n, e) { + verify: async function(s, n, e) { if (n.cmp(s) <= 0) { throw new Error('Data too large.'); } @@ -85,7 +85,7 @@ export default { * @param e public MPI part as BN * @return BN */ - encrypt: function(m, n, e) { + encrypt: async function(m, n, e) { if (n.cmp(m) <= 0) { throw new Error('Data too large.'); } @@ -104,7 +104,7 @@ export default { * @param u RSA u as BN * @return {BN} The decrypted value of the message */ - decrypt: function(m, n, e, d, p, q, u) { + decrypt: async function(m, n, e, d, p, q, u) { if (n.cmp(m) <= 0) { throw new Error('Data too large.'); } @@ -117,7 +117,7 @@ export default { let blinder; let unblinder; if (config.rsa_blinding) { - unblinder = random.getRandomBN(new BN(2), n).toRed(nred); + unblinder = (await random.getRandomBN(new BN(2), n)).toRed(nred); blinder = unblinder.redInvm().redPow(e); m = m.toRed(nred).redMul(blinder).fromRed(); } @@ -204,8 +204,8 @@ export default { // RSA keygen fallback using 40 iterations of the Miller-Rabin test // See https://stackoverflow.com/a/6330138 for justification // Also see section C.3 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST - let p = prime.randomProbablePrime(B - (B >> 1), E, 40); - let q = prime.randomProbablePrime(B >> 1, E, 40); + let p = await prime.randomProbablePrime(B - (B >> 1), E, 40); + let q = await prime.randomProbablePrime(B >> 1, E, 40); if (p.cmp(q) < 0) { [p, q] = [q, p]; diff --git a/src/crypto/random.js b/src/crypto/random.js index 320c70dd..e714df31 100644 --- a/src/crypto/random.js +++ b/src/crypto/random.js @@ -37,50 +37,8 @@ export default { * @param {Integer} length Length in bytes to generate * @return {Uint8Array} Random byte array */ - getRandomBytes: function(length) { - const result = new Uint8Array(length); - for (let i = 0; i < length; i++) { - result[i] = this.getSecureRandomOctet(); - } - return result; - }, - - /** - * Return a secure random number in the specified range - * @param {Integer} from Min of the random number - * @param {Integer} to Max of the random number (max 32bit) - * @return {Integer} A secure random number - */ - getSecureRandom: function(from, to) { - let randUint = this.getSecureRandomUint(); - const bits = ((to - from)).toString(2).length; - while ((randUint & ((2 ** bits) - 1)) > (to - from)) { - randUint = this.getSecureRandomUint(); - } - return from + (Math.abs(randUint & ((2 ** bits) - 1))); - }, - - getSecureRandomOctet: function() { - const buf = new Uint8Array(1); - this.getRandomValues(buf); - return buf[0]; - }, - - getSecureRandomUint: function() { - const buf = new Uint8Array(4); - const dv = new DataView(buf.buffer); - this.getRandomValues(buf); - return dv.getUint32(0); - }, - - /** - * Helper routine which calls platform specific crypto random generator - * @param {Uint8Array} buf - */ - getRandomValues: function(buf) { - if (!(buf instanceof Uint8Array)) { - throw new Error('Invalid type: buf not an Uint8Array'); - } + getRandomBytes: async function(length) { + const buf = new Uint8Array(length); if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) { window.crypto.getRandomValues(buf); } else if (typeof window !== 'undefined' && typeof window.msCrypto === 'object' && typeof window.msCrypto.getRandomValues === 'function') { @@ -89,7 +47,7 @@ export default { const bytes = nodeCrypto.randomBytes(buf.length); buf.set(bytes); } else if (this.randomBuffer.buffer) { - this.randomBuffer.get(buf); + await this.randomBuffer.get(buf); } else { throw new Error('No secure random number generator available.'); } @@ -102,7 +60,7 @@ export default { * @param {module:type/mpi} max Upper bound, excluded * @return {module:BN} Random MPI */ - getRandomBN: function(min, max) { + getRandomBN: async function(min, max) { if (max.cmp(min) <= 0) { throw new Error('Illegal parameter value: max <= min'); } @@ -113,7 +71,7 @@ export default { // Using a while loop is necessary to avoid bias introduced by the mod operation. // However, we request 64 extra random bits so that the bias is negligible. // Section B.1.1 here: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf - const r = new BN(this.getRandomBytes(bytes + 8)); + const r = new BN(await this.getRandomBytes(bytes + 8)); return r.mod(modulus).add(min); }, @@ -126,15 +84,17 @@ export default { function RandomBuffer() { this.buffer = null; this.size = null; + this.callback = null; } /** * Initialize buffer * @param {Integer} size size of buffer */ -RandomBuffer.prototype.init = function(size) { +RandomBuffer.prototype.init = function(size, callback) { this.buffer = new Uint8Array(size); this.size = 0; + this.callback = callback; }; /** @@ -161,7 +121,7 @@ RandomBuffer.prototype.set = function(buf) { * Take numbers out of buffer and copy to array * @param {Uint8Array} buf the destination array */ -RandomBuffer.prototype.get = function(buf) { +RandomBuffer.prototype.get = async function(buf) { if (!this.buffer) { throw new Error('RandomBuffer is not initialized'); } @@ -169,7 +129,12 @@ RandomBuffer.prototype.get = function(buf) { throw new Error('Invalid type: buf not an Uint8Array'); } if (this.size < buf.length) { - throw new Error('Random number buffer depleted'); + if (!this.callback) { + throw new Error('Random number buffer depleted'); + } + // Wait for random bytes from main context, then try again + await this.callback(); + return this.get(buf); } for (let i = 0; i < buf.length; i++) { buf[i] = this.buffer[--this.size]; diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 28ed2d92..8b77c6d2 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -34,7 +34,7 @@ export default { const m = msg_MPIs[0].toBN(); const n = pub_MPIs[0].toBN(); const e = pub_MPIs[1].toBN(); - const EM = publicKey.rsa.verify(m, n, e); + const EM = await publicKey.rsa.verify(m, n, e); const EM2 = pkcs1.emsa.encode(hash_algo, util.Uint8Array_to_str(data), n.byteLength()); return util.Uint8Array_to_hex(EM) === EM2; } @@ -88,7 +88,7 @@ export default { const d = key_params[2].toBN(); data = util.Uint8Array_to_str(data); const m = new BN(pkcs1.emsa.encode(hash_algo, data, n.byteLength()), 16); - const signature = publicKey.rsa.sign(m, n, e, d); + const signature = await publicKey.rsa.sign(m, n, e, d); return util.Uint8Array_to_MPI(signature); } case enums.publicKey.dsa: { @@ -96,7 +96,7 @@ export default { const q = key_params[1].toBN(); const g = key_params[2].toBN(); const x = key_params[4].toBN(); - const signature = publicKey.dsa.sign(hash_algo, data, g, p, q, x); + const signature = await publicKey.dsa.sign(hash_algo, data, g, p, q, x); return util.concatUint8Array([ util.Uint8Array_to_MPI(signature.r), util.Uint8Array_to_MPI(signature.s) diff --git a/src/key.js b/src/key.js index cbf7d99c..8c989b96 100644 --- a/src/key.js +++ b/src/key.js @@ -153,7 +153,7 @@ Key.prototype.packetlist2structure = function(packetlist) { /** * Transforms structured key data to packetlist - * @return {module:packet/packetlist} The packets that form a key + * @returns {module:packet/packetlist} The packets that form a key */ Key.prototype.toPacketlist = function() { const packetlist = new packet.List(); @@ -210,7 +210,7 @@ Key.prototype.getKeyIds = function() { /** * Returns array containing first key packet for given key ID or all key packets in the case of a wildcard ID * @param {type/keyid} keyId - * @return {(module:packet/public_subkey|module:packet/public_key| + * @returns {(module:packet/public_subkey|module:packet/public_key| * module:packet/secret_subkey|module:packet/secret_key|null)} */ Key.prototype.getKeyPackets = function(packetKeyId) { @@ -229,7 +229,7 @@ Key.prototype.getKeyPackets = function(packetKeyId) { /** * Returns userids - * @return {Array} array of userids + * @returns {Array} array of userids */ Key.prototype.getUserIds = function() { const userids = []; @@ -243,7 +243,7 @@ Key.prototype.getUserIds = function() { /** * Returns true if this is a public key - * @return {Boolean} + * @returns {Boolean} */ Key.prototype.isPublic = function() { return this.primaryKey.tag === enums.packet.publicKey; @@ -251,7 +251,7 @@ Key.prototype.isPublic = function() { /** * Returns true if this is a private key - * @return {Boolean} + * @returns {Boolean} */ Key.prototype.isPrivate = function() { return this.primaryKey.tag === enums.packet.secretKey; @@ -259,7 +259,7 @@ Key.prototype.isPrivate = function() { /** * Returns key as public key (shallow copy) - * @return {module:key~Key} new public Key + * @returns {module:key~Key} new public Key */ Key.prototype.toPublic = function() { const packetlist = new packet.List(); @@ -290,7 +290,7 @@ Key.prototype.toPublic = function() { /** * Returns ASCII armored text of key - * @return {String} ASCII armor + * @returns {String} ASCII armor */ Key.prototype.armor = function() { const type = this.isPublic() ? enums.armor.public_key : enums.armor.private_key; @@ -301,7 +301,7 @@ Key.prototype.armor = function() { * Returns first key packet or key packet by given keyId that is available for signing or signature verification * @param {module:type/keyid} keyId, optional * @param {Date} date use the given date for verification instead of the current time - * @return {(module:packet/secret_subkey|module:packet/secret_key|null)} key packet or null if no signing key has been found + * @returns {(module:packet/secret_subkey|module:packet/secret_key|null)} key packet or null if no signing key has been found */ Key.prototype.getSigningKeyPacket = function (keyId=null, date=new Date()) { const primaryUser = this.getPrimaryUser(date); @@ -379,36 +379,34 @@ Key.prototype.getEncryptionKeyPacket = function(keyId, date=new Date()) { /** * Encrypts all secret key and subkey packets * @param {String} passphrase + * @returns {Promise} */ -Key.prototype.encrypt = function(passphrase) { +Key.prototype.encrypt = async function(passphrase) { if (!this.isPrivate()) { throw new Error("Nothing to encrypt in a public key"); } const keys = this.getAllKeyPackets(); - for (let i = 0; i < keys.length; i++) { - keys[i].encrypt(passphrase); - keys[i].clearPrivateParams(); - } + await Promise.all(keys.map(async function(packet) { + await packet.encrypt(passphrase); + await packet.clearPrivateParams(); + return packet; + })); + return true; }; /** * Decrypts all secret key and subkey packets * @param {String} passphrase - * @return {Boolean} true if all key and subkey packets decrypted successfully + * @returns {Promise} true if all key and subkey packets decrypted successfully */ -Key.prototype.decrypt = function(passphrase) { - if (this.isPrivate()) { - const keys = this.getAllKeyPackets(); - for (let i = 0; i < keys.length; i++) { - const success = keys[i].decrypt(passphrase); - if (!success) { - return false; - } - } - } else { +Key.prototype.decrypt = async function(passphrase) { + if (!this.isPrivate()) { throw new Error("Nothing to decrypt in a public key"); } + + const keys = this.getAllKeyPackets(); + await Promise.all(keys.map(packet => packet.decrypt(passphrase))); return true; }; @@ -416,7 +414,7 @@ Key.prototype.decrypt = function(passphrase) { * Decrypts specific key packets by key ID * @param {Array} keyIds * @param {String} passphrase - * @return {Boolean} true if all key packets decrypted successfully + * @returns {Boolean} true if all key packets decrypted successfully */ Key.prototype.decryptKeyPacket = function(keyIds, passphrase) { if (this.isPrivate()) { @@ -442,7 +440,7 @@ Key.prototype.decryptKeyPacket = function(keyIds, passphrase) { * Verify primary key. Checks for revocation signatures, expiration time * and valid self signature * @param {Date} date (optional) use the given date for verification instead of the current time - * @return {module:enums.keyStatus} The status of the primary key + * @returns {Promise{module:enums.keyStatus}} The status of the primary key */ Key.prototype.verifyPrimaryKey = async function(date=new Date()) { // TODO clarify OpenPGP's behavior given an expired revocation signature @@ -484,7 +482,7 @@ Key.prototype.verifyPrimaryKey = async function(date=new Date()) { /** * Returns the expiration time of the primary key or null if key does not expire - * @return {Date|null} + * @returns {Date|null} */ Key.prototype.getExpirationTime = function() { if (this.primaryKey.version === 3) { @@ -519,7 +517,7 @@ function getExpirationTime(keyPacket, selfCertificate, defaultValue=null) { * * NOTE: call verifyPrimaryUser before calling this function. * @param {Date} date use the given date for verification instead of the current time - * @return {{user: Array, selfCertificate: Array}|null} The primary user and the self signature + * @returns {{user: Array, selfCertificate: Array}|null} The primary user and the self signature */ Key.prototype.getPrimaryUser = function(date=new Date()) { let primaryUsers = []; @@ -652,7 +650,7 @@ Key.prototype.revoke = function() { /** * Signs primary user of key * @param {Array} privateKey decrypted private keys for signing - * @return {module:key~Key} new public key with new certificate signature + * @returns {Promise} new public key with new certificate signature */ Key.prototype.signPrimaryUser = async function(privateKeys) { await this.verifyPrimaryUser(); @@ -669,7 +667,7 @@ Key.prototype.signPrimaryUser = async function(privateKeys) { /** * Signs all users of key * @param {Array} privateKeys decrypted private keys for signing - * @return {module:key~Key} new public key with new certificate signature + * @returns {Promise} new public key with new certificate signature */ Key.prototype.signAllUsers = async function(privateKeys) { const that = this; @@ -685,7 +683,7 @@ Key.prototype.signAllUsers = async function(privateKeys) { * - if no arguments are given, verifies the self certificates; * - otherwise, verifies all certificates signed with given keys. * @param {Array} keys array of keys to verify certificate signatures - * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + * @returns {Promise>} list of signer's keyid and validity of signature */ Key.prototype.verifyPrimaryUser = async function(keys) { const { primaryKey } = this; @@ -735,7 +733,7 @@ Key.prototype.verifyPrimaryUser = async function(keys) { * - if no arguments are given, verifies the self certificates; * - otherwise, verifies all certificates signed with given keys. * @param {Array} keys array of keys to verify certificate signatures - * @return {Array<({userid: String, keyid: module:type/keyid, valid: Boolean})>} list of userid, signer's keyid and validity of signature + * @returns {Promise>} list of userid, signer's keyid and validity of signature */ Key.prototype.verifyAllUsers = async function(keys) { const results = []; @@ -771,7 +769,7 @@ function User(userPacket) { /** * Transforms structured user data to packetlist - * @return {module:packet/packetlist} + * @returns {module:packet/packetlist} */ User.prototype.toPacketlist = function() { const packetlist = new packet.List(); @@ -788,7 +786,7 @@ User.prototype.toPacketlist = function() { * @param {module:packet/signature} certificate The certificate to verify * @param {module:packet/public_subkey|module:packet/public_key| * module:packet/secret_subkey|module:packet/secret_key} key, optional The key to verify the signature - * @return {Boolean} True if the certificate is revoked + * @returns {Promise} True if the certificate is revoked */ User.prototype.isRevoked = async function(primaryKey, certificate, key) { certificate.revoked = null; @@ -810,7 +808,7 @@ User.prototype.isRevoked = async function(primaryKey, certificate, key) { * Signs user * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet * @param {Array} privateKeys decrypted private keys for signing - * @return {module:key~Key} new user with new certificate signatures + * @returns {Promise} new user with new certificate signatures */ User.prototype.sign = async function(primaryKey, privateKeys) { const dataToSign = { userid: this.userId || this.userAttribute, key: primaryKey }; @@ -851,7 +849,7 @@ User.prototype.sign = async function(primaryKey, privateKeys) { * @param {module:packet/signature} certificate A certificate of this user * @param {Array} keys array of keys to verify certificate signatures * @param {Date} date use the given date for verification instead of the current time - * @return {module:enums.keyStatus} status of the certificate + * @returns {Promise} status of the certificate */ User.prototype.verifyCertificate = async function(primaryKey, certificate, keys, date=new Date()) { const that = this; @@ -879,7 +877,7 @@ User.prototype.verifyCertificate = async function(primaryKey, certificate, keys, * Verifies all user certificates * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet * @param {Array} keys array of keys to verify certificate signatures - * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + * @returns {Promise>} list of signer's keyid and validity of signature */ User.prototype.verifyAllCertifications = async function(primaryKey, keys) { const that = this; @@ -897,7 +895,7 @@ User.prototype.verifyAllCertifications = async function(primaryKey, keys) { * Verify User. Checks for existence of self signatures, revocation signatures * and validity of self signature * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet - * @return {module:enums.keyStatus} status of user + * @returns {Promise} status of user */ User.prototype.verify = async function(primaryKey) { if (!this.selfCertifications) { @@ -925,7 +923,7 @@ User.prototype.verify = async function(primaryKey) { /** * Update user with new components from specified user * @param {module:key~User} user source user to merge - * @param {module:packet/signature} primaryKey primary key used for validation + * @param {Promise} primaryKey primary key used for validation */ User.prototype.update = async function(user, primaryKey) { const dataToVerify = { userid: this.userId || this.userAttribute, key: primaryKey }; @@ -954,7 +952,7 @@ function SubKey(subKeyPacket) { /** * Transforms structured subkey data to packetlist - * @return {module:packet/packetlist} + * @returns {module:packet/packetlist} */ SubKey.prototype.toPacketlist = function() { const packetlist = new packet.List(); @@ -970,7 +968,7 @@ SubKey.prototype.toPacketlist = function() { * Returns true if the subkey can be used for encryption * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet * @param {Date} date use the given date for verification instead of the current time - * @return {Boolean} + * @returns {Promise} */ SubKey.prototype.isValidEncryptionKey = async function(primaryKey, date=new Date()) { if (await this.verify(primaryKey, date) !== enums.keyStatus.valid) { @@ -988,7 +986,7 @@ SubKey.prototype.isValidEncryptionKey = async function(primaryKey, date=new Date * Returns true if the subkey can be used for signing of data * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet * @param {Date} date use the given date for verification instead of the current time - * @return {Boolean} + * @returns {Promise} */ SubKey.prototype.isValidSigningKey = async function(primaryKey, date=new Date()) { if (await this.verify(primaryKey, date) !== enums.keyStatus.valid) { @@ -1007,7 +1005,7 @@ SubKey.prototype.isValidSigningKey = async function(primaryKey, date=new Date()) * and valid binding signature * @param {module:packet/secret_key|module:packet/public_key} primaryKey The primary key packet * @param {Date} date use the given date for verification instead of the current time - * @return {module:enums.keyStatus} The status of the subkey + * @returns {Promise} The status of the subkey */ SubKey.prototype.verify = async function(primaryKey, date=new Date()) { const that = this; @@ -1054,7 +1052,7 @@ SubKey.prototype.verify = async function(primaryKey, date=new Date()) { /** * Returns the expiration time of the subkey or null if key does not expire - * @return {Date|null} + * @returns {Date|null} */ SubKey.prototype.getExpirationTime = function() { let highest; @@ -1073,7 +1071,7 @@ SubKey.prototype.getExpirationTime = function() { /** * Update subkey with new components from specified subkey * @param {module:key~SubKey} subKey source subkey to merge - * @param {module:packet/signature} primaryKey primary key used for validation + * @param {Promise} primaryKey primary key used for validation */ SubKey.prototype.update = async function(subKey, primaryKey) { if (await subKey.verify(primaryKey) === enums.keyStatus.invalid) { @@ -1115,7 +1113,7 @@ SubKey.prototype.update = async function(subKey, primaryKey) { /** * Reads an unarmored OpenPGP key list and returns one or multiple key objects * @param {Uint8Array} data to be parsed - * @return {{keys: Array, err: (Array|null)}} result object with key and error arrays + * @returns {{keys: Array, err: (Array|null)}} result object with key and error arrays * @static */ export function read(data) { @@ -1148,7 +1146,7 @@ export function read(data) { /** * Reads an OpenPGP armored text and returns one or multiple key objects * @param {String} armoredText text to be parsed - * @return {{keys: Array, err: (Array|null)}} result object with key and error arrays + * @returns {{keys: Array, err: (Array|null)}} result object with key and error arrays * @static */ export function readArmored(armoredText) { @@ -1176,7 +1174,7 @@ export function readArmored(armoredText) { * @param {String} options.passphrase The passphrase used to encrypt the resulting private key * @param {Boolean} [options.unlocked=false] The secret part of the generated key is unlocked * @param {Number} [options.keyExpirationTime=0] The number of seconds after the key creation time that the key expires - * @return {module:key~Key} + * @returns {module:key~Key} * @static */ export function generate(options) { @@ -1251,49 +1249,51 @@ export function generate(options) { * @param {String} options.passphrase The passphrase used to encrypt the resulting private key * @param {Boolean} [options.unlocked=false] The secret part of the generated key is unlocked * @param {Number} [options.keyExpirationTime=0] The number of seconds after the key creation time that the key expires - * @return {module:key~Key} + * @returns {Promise} * @static */ -export function reformat(options) { +export async function reformat(options) { let secretKeyPacket; let secretSubkeyPacket; - return Promise.resolve().then(() => { - options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; - if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated - throw new Error('Only RSA Encrypt or Sign supported'); - } - if (!options.privateKey.decrypt()) { - throw new Error('Key not decrypted'); - } + options.keyType = options.keyType || enums.publicKey.rsa_encrypt_sign; + if (options.keyType !== enums.publicKey.rsa_encrypt_sign) { // RSA Encrypt-Only and RSA Sign-Only are deprecated and SHOULD NOT be generated + throw new Error('Only RSA Encrypt or Sign supported'); + } - if (!options.passphrase) { // Key without passphrase is unlocked by definition - options.unlocked = true; + try { + await options.privateKey.decrypt(); + } + catch(err) { + throw new Error('Key not decrypted'); + } + + if (!options.passphrase) { // Key without passphrase is unlocked by definition + options.unlocked = true; + } + if (util.isString(options.userIds)) { + options.userIds = [options.userIds]; + } + const packetlist = options.privateKey.toPacketlist(); + for (let i = 0; i < packetlist.length; i++) { + if (packetlist[i].tag === enums.packet.secretKey) { + secretKeyPacket = packetlist[i]; + options.keyType = secretKeyPacket.algorithm; + } else if (packetlist[i].tag === enums.packet.secretSubkey) { + secretSubkeyPacket = packetlist[i]; + options.subkeyType = secretSubkeyPacket.algorithm; } - if (util.isString(options.userIds)) { - options.userIds = [options.userIds]; - } - const packetlist = options.privateKey.toPacketlist(); - for (let i = 0; i < packetlist.length; i++) { - if (packetlist[i].tag === enums.packet.secretKey) { - secretKeyPacket = packetlist[i]; - options.keyType = secretKeyPacket.algorithm; - } else if (packetlist[i].tag === enums.packet.secretSubkey) { - secretSubkeyPacket = packetlist[i]; - options.subkeyType = secretSubkeyPacket.algorithm; - } - } - if (!secretKeyPacket) { - throw new Error('Key does not contain a secret key packet'); - } - return wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options); - }); + } + if (!secretKeyPacket) { + throw new Error('Key does not contain a secret key packet'); + } + return wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options); } async function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) { // set passphrase protection if (options.passphrase) { - secretKeyPacket.encrypt(options.passphrase); + await secretKeyPacket.encrypt(options.passphrase); if (secretSubkeyPacket) { secretSubkeyPacket.encrypt(options.passphrase); } @@ -1383,7 +1383,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPacket, options) { /** * Returns the preferred signature hash algorithm of a key * @param {object} key - * @return {String} + * @returns {String} */ export function getPreferredHashAlgo(key) { let hash_algo = config.prefer_hash_algorithm; @@ -1417,7 +1417,7 @@ export function getPreferredHashAlgo(key) { /** * Returns the preferred symmetric algorithm for a set of keys * @param {Array} keys Set of keys - * @return {enums.symmetric} Preferred symmetric algorithm + * @returns {enums.symmetric} Preferred symmetric algorithm */ export function getPreferredSymAlgo(keys) { const prioMap = {}; diff --git a/src/message.js b/src/message.js index 9109b804..2c39b1e1 100644 --- a/src/message.js +++ b/src/message.js @@ -55,7 +55,7 @@ export function Message(packetlist) { /** * Returns the key IDs of the keys to which the session key is encrypted - * @return {Array} array of keyid objects + * @returns {Array} array of keyid objects */ Message.prototype.getEncryptionKeyIds = function() { const keyIds = []; @@ -68,7 +68,7 @@ Message.prototype.getEncryptionKeyIds = function() { /** * Returns the key IDs of the keys that signed the message - * @return {Array} array of keyid objects + * @returns {Array} array of keyid objects */ Message.prototype.getSigningKeyIds = function() { const keyIds = []; @@ -93,7 +93,7 @@ Message.prototype.getSigningKeyIds = function() { * @param {Array} privateKeys (optional) private keys with decrypted secret data * @param {Array} passwords (optional) passwords used to decrypt * @param {Array} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String } - * @return {Message} new message with decrypted content + * @returns {Promise} new message with decrypted content */ Message.prototype.decrypt = async function(privateKeys, passwords, sessionKeys) { const keyObjs = sessionKeys || await this.decryptSessionKeys(privateKeys, passwords); @@ -105,7 +105,7 @@ Message.prototype.decrypt = async function(privateKeys, passwords, sessionKeys) ); if (symEncryptedPacketlist.length === 0) { - return; + return this; } const symEncryptedPacket = symEncryptedPacketlist[0]; @@ -138,74 +138,73 @@ Message.prototype.decrypt = async function(privateKeys, passwords, sessionKeys) * Decrypt encrypted session keys either with private keys or passwords. * @param {Array} privateKeys (optional) private keys with decrypted secret data * @param {Array} passwords (optional) passwords used to decrypt - * @return {Array<{ data:Uint8Array, algorithm:String }>} array of object with potential sessionKey, algorithm pairs + * @returns {Promise{Array<{ data:Uint8Array, algorithm:String }>}} array of object with potential sessionKey, algorithm pairs */ -Message.prototype.decryptSessionKeys = function(privateKeys, passwords) { +Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) { let keyPackets = []; - return Promise.resolve().then(async () => { - if (passwords) { - const symESKeyPacketlist = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); - if (!symESKeyPacketlist) { - throw new Error('No symmetrically encrypted session key packet found.'); - } - await Promise.all(symESKeyPacketlist.map(async function(packet) { - await Promise.all(passwords.map(async function(password) { - try { - await packet.decrypt(password); - keyPackets.push(packet); - } catch (err) {} - })); - })); - } else if (privateKeys) { - const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); - if (!pkESKeyPacketlist) { - throw new Error('No public key encrypted session key packet found.'); - } - await Promise.all(pkESKeyPacketlist.map(async function(packet) { - const privateKeyPackets = privateKeys.reduce(function(acc, privateKey) { - return acc.concat(privateKey.getKeyPackets(packet.publicKeyId)); - }, []); - await Promise.all(privateKeyPackets.map(async function(privateKeyPacket) { - if (!privateKeyPacket) { - return; - } - if (!privateKeyPacket.isDecrypted) { - throw new Error('Private key is not decrypted.'); - } - try { - await packet.decrypt(privateKeyPacket); - keyPackets.push(packet); - } catch (err) {} - })); + if (passwords) { + const symESKeyPacketlist = this.packets.filterByTag(enums.packet.symEncryptedSessionKey); + if (!symESKeyPacketlist) { + throw new Error('No symmetrically encrypted session key packet found.'); + } + await Promise.all(symESKeyPacketlist.map(async function(packet) { + await Promise.all(passwords.map(async function(password) { + try { + await packet.decrypt(password); + keyPackets.push(packet); + } catch (err) {} })); - } else { - throw new Error('No key or password specified.'); - } - }).then(() => { - if (keyPackets.length) { - // Return only unique session keys - if (keyPackets.length > 1) { - const seen = {}; - keyPackets = keyPackets.filter(function(item) { - const k = item.sessionKeyAlgorithm + util.Uint8Array_to_str(item.sessionKey); - if (seen.hasOwnProperty(k)) { - return false; - } - seen[k] = true; - return true; - }); - } + })); - return keyPackets.map(packet => ({ data: packet.sessionKey, algorithm: packet.sessionKeyAlgorithm })); + } else if (privateKeys) { + const pkESKeyPacketlist = this.packets.filterByTag(enums.packet.publicKeyEncryptedSessionKey); + if (!pkESKeyPacketlist) { + throw new Error('No public key encrypted session key packet found.'); } - throw new Error('Session key decryption failed.'); - }); + await Promise.all(pkESKeyPacketlist.map(async function(packet) { + const privateKeyPackets = privateKeys.reduce(function(acc, privateKey) { + return acc.concat(privateKey.getKeyPackets(packet.publicKeyId)); + }, []); + await Promise.all(privateKeyPackets.map(async function(privateKeyPacket) { + if (!privateKeyPacket) { + return; + } + if (!privateKeyPacket.isDecrypted) { + throw new Error('Private key is not decrypted.'); + } + try { + await packet.decrypt(privateKeyPacket); + keyPackets.push(packet); + } catch (err) {} + })); + })); + } else { + throw new Error('No key or password specified.'); + } + + if (keyPackets.length) { + // Return only unique session keys + if (keyPackets.length > 1) { + const seen = {}; + keyPackets = keyPackets.filter(function(item) { + const k = item.sessionKeyAlgorithm + util.Uint8Array_to_str(item.sessionKey); + if (seen.hasOwnProperty(k)) { + return false; + } + seen[k] = true; + return true; + }); + } + + return keyPackets.map(packet => ({ data: packet.sessionKey, algorithm: packet.sessionKeyAlgorithm })); + } + throw new Error('Session key decryption failed.'); }; /** * Get literal data that is the body of the message - * @return {(Uint8Array|null)} literal body of the message as Uint8Array + * @returns {(Uint8Array|null)} literal body of the message as Uint8Array */ Message.prototype.getLiteralData = function() { const literal = this.packets.findPacket(enums.packet.literal); @@ -214,7 +213,7 @@ Message.prototype.getLiteralData = function() { /** * Get filename from literal data packet - * @return {(String|null)} filename of literal data packet as string + * @returns {(String|null)} filename of literal data packet as string */ Message.prototype.getFilename = function() { const literal = this.packets.findPacket(enums.packet.literal); @@ -223,7 +222,7 @@ Message.prototype.getFilename = function() { /** * Get literal data as text - * @return {(String|null)} literal body of the message interpreted as text + * @returns {(String|null)} literal body of the message interpreted as text */ Message.prototype.getText = function() { const literal = this.packets.findPacket(enums.packet.literal); @@ -240,54 +239,52 @@ Message.prototype.getText = function() { * @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String } * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs * @param {Date} date (optional) override the creation date of the literal package - * @return {Message} new message with encrypted content + * @returns {Promise} new message with encrypted content */ -Message.prototype.encrypt = function(keys, passwords, sessionKey, wildcard=false, date=new Date()) { +Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard=false, date=new Date()) { let symAlgo; - let msg; let symEncryptedPacket; - return Promise.resolve().then(async () => { - if (sessionKey) { - if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { - throw new Error('Invalid session key for encryption.'); - } - symAlgo = sessionKey.algorithm; - sessionKey = sessionKey.data; - } else if (keys && keys.length) { - symAlgo = enums.read(enums.symmetric, getPreferredSymAlgo(keys)); - } else if (passwords && passwords.length) { - symAlgo = enums.read(enums.symmetric, config.encryption_cipher); - } else { - throw new Error('No keys, passwords, or session key provided.'); + + if (sessionKey) { + if (!util.isUint8Array(sessionKey.data) || !util.isString(sessionKey.algorithm)) { + throw new Error('Invalid session key for encryption.'); } + symAlgo = sessionKey.algorithm; + sessionKey = sessionKey.data; + } else if (keys && keys.length) { + symAlgo = enums.read(enums.symmetric, getPreferredSymAlgo(keys)); + } else if (passwords && passwords.length) { + symAlgo = enums.read(enums.symmetric, config.encryption_cipher); + } else { + throw new Error('No keys, passwords, or session key provided.'); + } - if (!sessionKey) { - sessionKey = crypto.generateSessionKey(symAlgo); + if (!sessionKey) { + sessionKey = await crypto.generateSessionKey(symAlgo); + } + + const msg = await encryptSessionKey(sessionKey, symAlgo, keys, passwords, wildcard, date); + + if (config.aead_protect) { + symEncryptedPacket = new packet.SymEncryptedAEADProtected(); + } else if (config.integrity_protect) { + symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); + } else { + symEncryptedPacket = new packet.SymmetricallyEncrypted(); + } + symEncryptedPacket.packets = this.packets; + + await symEncryptedPacket.encrypt(symAlgo, sessionKey); + + msg.packets.push(symEncryptedPacket); + symEncryptedPacket.packets = new packet.List(); // remove packets after encryption + return { + message: msg, + sessionKey: { + data: sessionKey, + algorithm: symAlgo } - - msg = await encryptSessionKey(sessionKey, symAlgo, keys, passwords, wildcard, date); - - if (config.aead_protect) { - symEncryptedPacket = new packet.SymEncryptedAEADProtected(); - } else if (config.integrity_protect) { - symEncryptedPacket = new packet.SymEncryptedIntegrityProtected(); - } else { - symEncryptedPacket = new packet.SymmetricallyEncrypted(); - } - symEncryptedPacket.packets = this.packets; - - return symEncryptedPacket.encrypt(symAlgo, sessionKey); - }).then(() => { - msg.packets.push(symEncryptedPacket); - symEncryptedPacket.packets = new packet.List(); // remove packets after encryption - return { - message: msg, - sessionKey: { - data: sessionKey, - algorithm: symAlgo - } - }; - }); + }; }; /** @@ -298,64 +295,64 @@ Message.prototype.encrypt = function(keys, passwords, sessionKey, wildcard=false * @param {Array} passwords (optional) for message encryption * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs * @param {Date} date (optional) override the creation date signature - * @return {Message} new message with encrypted content + * @returns {Promise} new message with encrypted content */ -export function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwords, wildcard=false, date=new Date()) { +export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwords, wildcard=false, date=new Date()) { const packetlist = new packet.List(); - return Promise.resolve().then(async () => { - if (publicKeys) { - const results = await Promise.all(publicKeys.map(async function(key) { - await key.verifyPrimaryUser(); - const encryptionKeyPacket = key.getEncryptionKeyPacket(undefined, date); - if (!encryptionKeyPacket) { - throw new Error('Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex()); + if (publicKeys) { + const results = await Promise.all(publicKeys.map(async function(key) { + await key.verifyPrimaryUser(); + const encryptionKeyPacket = key.getEncryptionKeyPacket(undefined, date); + if (!encryptionKeyPacket) { + throw new Error('Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex()); + } + const pkESKeyPacket = new packet.PublicKeyEncryptedSessionKey(); + pkESKeyPacket.publicKeyId = wildcard ? type_keyid.wildcard() : encryptionKeyPacket.getKeyId(); + pkESKeyPacket.publicKeyAlgorithm = encryptionKeyPacket.algorithm; + pkESKeyPacket.sessionKey = sessionKey; + pkESKeyPacket.sessionKeyAlgorithm = symAlgo; + await pkESKeyPacket.encrypt(encryptionKeyPacket); + delete pkESKeyPacket.sessionKey; // delete plaintext session key after encryption + return pkESKeyPacket; + })); + packetlist.concat(results); + } + + if (passwords) { + const testDecrypt = async function(keyPacket, password) { + try { + await keyPacket.decrypt(password); + return 1; + } catch (e) { + return 0; + } + }; + + const sum = (accumulator, currentValue) => accumulator + currentValue; + + const encryptPassword = async function(sessionKey, symAlgo, password) { + const symEncryptedSessionKeyPacket = new packet.SymEncryptedSessionKey(); + symEncryptedSessionKeyPacket.sessionKey = sessionKey; + symEncryptedSessionKeyPacket.sessionKeyAlgorithm = symAlgo; + await symEncryptedSessionKeyPacket.encrypt(password); + + if (config.password_collision_check) { + const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd))); + if (results.reduce(sum) !== 1) { + return encryptPassword(sessionKey, symAlgo, password); } - const pkESKeyPacket = new packet.PublicKeyEncryptedSessionKey(); - pkESKeyPacket.publicKeyId = wildcard ? type_keyid.wildcard() : encryptionKeyPacket.getKeyId(); - pkESKeyPacket.publicKeyAlgorithm = encryptionKeyPacket.algorithm; - pkESKeyPacket.sessionKey = sessionKey; - pkESKeyPacket.sessionKeyAlgorithm = symAlgo; - await pkESKeyPacket.encrypt(encryptionKeyPacket); - delete pkESKeyPacket.sessionKey; // delete plaintext session key after encryption - return pkESKeyPacket; - })); - packetlist.concat(results); - } + } - if (passwords) { - const testDecrypt = async function(keyPacket, password) { - try { - await keyPacket.decrypt(password); - return 1; - } catch (e) { - return 0; - } - }; + delete symEncryptedSessionKeyPacket.sessionKey; // delete plaintext session key after encryption + return symEncryptedSessionKeyPacket; + }; - const sum = (accumulator, currentValue) => accumulator + currentValue; + const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symAlgo, pwd))); + packetlist.concat(results); + } - const encryptPassword = async function(sessionKey, symAlgo, password) { - const symEncryptedSessionKeyPacket = new packet.SymEncryptedSessionKey(); - symEncryptedSessionKeyPacket.sessionKey = sessionKey; - symEncryptedSessionKeyPacket.sessionKeyAlgorithm = symAlgo; - await symEncryptedSessionKeyPacket.encrypt(password); - - if (config.password_collision_check) { - const results = await Promise.all(passwords.map(pwd => testDecrypt(symEncryptedSessionKeyPacket, pwd))); - if (results.reduce(sum) !== 1) { - return encryptPassword(sessionKey, symAlgo, password); - } - } - - delete symEncryptedSessionKeyPacket.sessionKey; // delete plaintext session key after encryption - return symEncryptedSessionKeyPacket; - }; - - const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symAlgo, pwd))); - packetlist.concat(results); - } - }).then(() => new Message(packetlist)); + return new Message(packetlist); } /** @@ -363,7 +360,7 @@ export function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwords, wi * @param {Array} privateKeys private keys with decrypted secret key data for signing * @param {Signature} signature (optional) any existing detached signature to add to the message * @param {Date} date} (optional) override the creation time of the signature - * @return {module:message~Message} new message with signed content + * @returns {Promise} new message with signed content */ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new Date()) { const packetlist = new packet.List(); @@ -427,7 +424,7 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new /** * Compresses the message (the literal and -if signed- signature data packets of the message) * @param {module:enums.compression} compression compression algorithm to be used - * @return {module:message~Message} new message with compressed content + * @returns {module:message~Message} new message with compressed content */ Message.prototype.compress = function(compression) { if (compression === enums.compression.uncompressed) { @@ -446,10 +443,10 @@ Message.prototype.compress = function(compression) { /** * Create a detached signature for the message (the literal data packet of the message) - * @param {Array} privateKeys private keys with decrypted secret key data for signing - * @param {Signature} signature (optional) any existing detached signature - * @param {Date} date (optional) override the creation time of the signature - * @return {module:signature~Signature} new detached signature of message content + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @param {Signature} signature (optional) any existing detached signature + * @param {Date} date (optional) override the creation time of the signature + * @returns {Promise} new detached signature of message content */ Message.prototype.signDetached = async function(privateKeys=[], signature=null, date=new Date()) { const literalDataPacket = this.packets.findPacket(enums.packet.literal); @@ -461,11 +458,11 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null, /** * Create signature packets for the message - * @param {module:packet/literal} literalDataPacket the literal data packet to sign - * @param {Array} privateKeys private keys with decrypted secret key data for signing - * @param {Signature} signature (optional) any existing detached signature to append - * @param {Date} date (optional) override the creationtime of the signature - * @return {module:packet/packetlist} list of signature packets + * @param {module:packet/literal} literalDataPacket the literal data packet to sign + * @param {Array} privateKeys private keys with decrypted secret key data for signing + * @param {Signature} signature (optional) any existing detached signature to append + * @param {Date} date (optional) override the creationtime of the signature + * @returns {Promise} list of signature packets */ export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date()) { const packetlist = new packet.List(); @@ -507,7 +504,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig * Verify message signatures * @param {Array} keys array of keys to verify signatures * @param {Date} date (optional) Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + * @returns {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature */ Message.prototype.verify = function(keys, date=new Date()) { const msg = this.unwrapCompressed(); @@ -524,7 +521,7 @@ Message.prototype.verify = function(keys, date=new Date()) { * @param {Array} keys array of keys to verify signatures * @param {Signature} signature * @param {Date} date Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + * @returns {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature */ Message.prototype.verifyDetached = function(signature, keys, date=new Date()) { const msg = this.unwrapCompressed(); @@ -542,7 +539,7 @@ Message.prototype.verifyDetached = function(signature, keys, date=new Date()) { * @param {Array} literalDataList array of literal data packets * @param {Array} keys array of keys to verify signatures * @param {Date} date Verify the signature against the given date, i.e. check signature creation time < date < expiration time - * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + * @returns {Promise{Array<({keyid: module:type/keyid, valid: Boolean})>}} list of signer's keyid and validity of signature */ export async function createVerificationObjects(signatureList, literalDataList, keys, date=new Date()) { return Promise.all(signatureList.map(async function(signature) { @@ -572,7 +569,7 @@ export async function createVerificationObjects(signatureList, literalDataList, /** * Unwrap compressed message - * @return {module:message~Message} message Content of compressed message + * @returns {module:message~Message} message Content of compressed message */ Message.prototype.unwrapCompressed = function() { const compressed = this.packets.filterByTag(enums.packet.compressed); @@ -592,7 +589,7 @@ Message.prototype.appendSignature = function(detachedSignature) { /** * Returns ASCII armored text of message - * @return {String} ASCII armor + * @returns {String} ASCII armor */ Message.prototype.armor = function() { return armor.encode(enums.armor.message, this.packets.write()); @@ -601,7 +598,7 @@ Message.prototype.armor = function() { /** * reads an OpenPGP armored message and returns a message object * @param {String} armoredText text to be parsed - * @return {module:message~Message} new message object + * @returns {module:message~Message} new message object * @static */ export function readArmored(armoredText) { @@ -614,7 +611,7 @@ export function readArmored(armoredText) { /** * reads an OpenPGP message as byte array and returns a message object * @param {Uint8Array} input binary message - * @return {Message} new message object + * @returns {Message} new message object * @static */ export function read(input) { @@ -628,7 +625,7 @@ export function read(input) { * @param {String} text * @param {String} filename (optional) * @param {Date} date (optional) - * @return {module:message~Message} new message object + * @returns {module:message~Message} new message object * @static */ export function fromText(text, filename, date=new Date()) { @@ -648,7 +645,7 @@ export function fromText(text, filename, date=new Date()) { * @param {Uint8Array} bytes * @param {String} filename (optional) * @param {Date} date (optional) - * @return {module:message~Message} new message object + * @returns {module:message~Message} new message object * @static */ export function fromBinary(bytes, filename, date=new Date()) { diff --git a/src/openpgp.js b/src/openpgp.js index bbe9f658..4f73229a 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -55,19 +55,20 @@ let asyncProxy; // instance of the asyncproxy /** * Set the path for the web worker script and create an instance of the async proxy - * @param {String} path relative path to the worker scripts, default: 'openpgp.worker.js' - * @param {Object} worker alternative to path parameter: web worker initialized with 'openpgp.worker.js' + * @param {String} path relative path to the worker scripts, default: 'openpgp.worker.js' + * @param {Number} n number of workers to initialize + * @param {Array} workers alternative to path parameter: web workers initialized with 'openpgp.worker.js' */ -export function initWorker({ path='openpgp.worker.js', worker } = {}) { - if (worker || (typeof window !== 'undefined' && window.Worker)) { - asyncProxy = new AsyncProxy({ path, worker, config }); +export function initWorker({ path='openpgp.worker.js', n = 1, workers = [] } = {}) { + if (workers.length || (typeof window !== 'undefined' && window.Worker)) { + asyncProxy = new AsyncProxy({ path, n, workers, config }); return true; } } /** * Returns a reference to the async proxy if the worker was initialized with openpgp.initWorker() - * @return {module:worker/async_proxy~AsyncProxy|null} the async proxy or null if not initialized + * @returns {module:worker/async_proxy~AsyncProxy|null} the async proxy or null if not initialized */ export function getWorker() { return asyncProxy; @@ -96,7 +97,7 @@ export function destroyWorker() { * @param {String} curve (optional) elliptic curve for ECC keys: curve25519, p256, p384, p521, or secp256k1 * @param {Boolean} unlocked (optional) If the returned secret part of the generated key is unlocked * @param {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires - * @return {Promise} The generated key object in the form: + * @returns {Promise} The generated key object in the form: * { key:Key, privateKeyArmored:String, publicKeyArmored:String } * @static */ @@ -133,7 +134,7 @@ export function generateKey({ * @param {String} passphrase (optional) The passphrase used to encrypt the resulting private key * @param {Boolean} unlocked (optional) If the returned secret part of the generated key is unlocked * @param {Number} keyExpirationTime (optional) The number of seconds after the key creation time that the key expires - * @return {Promise} The generated key object in the form: + * @returns {Promise} The generated key object in the form: * { key:Key, privateKeyArmored:String, publicKeyArmored:String } * @static */ @@ -163,7 +164,7 @@ export function reformatKey({ * Unlock a private key with your passphrase. * @param {Key} privateKey the private key that is to be decrypted * @param {String} passphrase the user's passphrase chosen during key generation - * @return {Key} the unlocked private key + * @returns {Promise} the unlocked key object in the form: { key:Key } */ export function decryptKey({ privateKey, passphrase }) { if (asyncProxy) { // use web worker if available @@ -179,6 +180,26 @@ export function decryptKey({ privateKey, passphrase }) { }).catch(onError.bind(null, 'Error decrypting private key')); } +/** + * Lock a private key with your passphrase. + * @param {Key} privateKey the private key that is to be decrypted + * @param {String} passphrase the user's passphrase chosen during key generation + * @returns {Promise} the locked key object in the form: { key:Key } + */ +export function encryptKey({ privateKey, passphrase }) { + if (asyncProxy) { // use web worker if available + return asyncProxy.delegate('encryptKey', { privateKey, passphrase }); + } + + return Promise.resolve().then(async function() { + await privateKey.encrypt(passphrase); + + return { + key: privateKey + }; + }).catch(onError.bind(null, 'Error decrypting private key')); +} + /////////////////////////////////////////// // // @@ -203,7 +224,7 @@ export function decryptKey({ privateKey, passphrase }) { * @param {Boolean} returnSessionKey (optional) if the unencrypted session key should be added to returned object * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs * @param {Date} date (optional) override the creation date of the message and the message signature - * @return {Promise} encrypted (and optionally signed message) in the form: + * @returns {Promise} encrypted (and optionally signed message) in the form: * {data: ASCII armored message if 'armor' is true, * message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true} * @static @@ -255,7 +276,7 @@ export function encrypt({ data, publicKeys, privateKeys, passwords, sessionKey, * @param {String} format (optional) return data format either as 'utf8' or 'binary' * @param {Signature} signature (optional) detached signature for verification * @param {Date} date (optional) use the given date for verification instead of the current time - * @return {Promise} decrypted and verified message in the form: + * @returns {Promise} decrypted and verified message in the form: * { data:Uint8Array|String, filename:String, signatures:[{ keyid:String, valid:Boolean }] } * @static */ @@ -294,7 +315,7 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe * @param {Boolean} armor (optional) if the return value should be ascii armored or the message object * @param {Boolean} detached (optional) if the return value should contain a detached signature * @param {Date} date (optional) override the creation date signature - * @return {Promise} signed cleartext in the form: + * @returns {Promise} signed cleartext in the form: * {data: ASCII armored message if 'armor' is true, * message: full Message object if 'armor' is false, signature: detached signature if 'detached' is true} * @static @@ -336,7 +357,7 @@ export function sign({ * @param {CleartextMessage} message cleartext message object with signatures * @param {Signature} signature (optional) detached signature for verification * @param {Date} date (optional) use the given date for verification instead of the current time - * @return {Promise} cleartext with status of verified signatures in the form of: + * @returns {Promise} cleartext with status of verified signatures in the form of: * { data:String, signatures: [{ keyid:String, valid:Boolean }] } * @static */ @@ -372,7 +393,7 @@ export function verify({ message, publicKeys, signature=null, date=new Date() }) * @param {Key|Array} publicKeys (optional) array of public keys or single key, used to encrypt the key * @param {String|Array} passwords (optional) passwords for the message * @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs - * @return {Promise} the encrypted session key packets contained in a message object + * @returns {Promise} the encrypted session key packets contained in a message object * @static */ export function encryptSessionKey({ data, algorithm, publicKeys, passwords, wildcard=false }) { @@ -395,7 +416,7 @@ export function encryptSessionKey({ data, algorithm, publicKeys, passwords, wild * @param {Message} message a message object containing the encrypted session key packets * @param {Key|Array} privateKeys (optional) private keys with decrypted secret key data * @param {String|Array} passwords (optional) passwords to decrypt the session key - * @return {Promise} Array of decrypted session key, algorithm pairs in form: + * @returns {Promise} Array of decrypted session key, algorithm pairs in form: * { data:Uint8Array, algorithm:String } * or 'undefined' if no key packets found * @static @@ -484,7 +505,7 @@ function formatUserIds(userIds) { /** * Normalize parameter to an array if it is not undefined. * @param {Object} param the parameter to be normalized - * @return {Array|undefined} the resulting array or undefined + * @returns {Array|undefined} the resulting array or undefined */ function toArray(param) { if (param && !util.isArray(param)) { @@ -498,7 +519,7 @@ function toArray(param) { * @param {String|Uint8Array} data the payload for the message * @param {String} filename the literal data packet's filename * @param {Date} date the creation date of the package - * @return {Message} a message object + * @returns {Message} a message object */ function createMessage(data, filename, date=new Date()) { let msg; @@ -516,7 +537,7 @@ function createMessage(data, filename, date=new Date()) { * Parse the message given a certain format. * @param {Message} message the message object to be parse * @param {String} format the output format e.g. 'utf8' or 'binary' - * @return {Object} the parse data in the respective format + * @returns {Object} the parse data in the respective format */ function parseMessage(message, format) { if (format === 'binary') { @@ -551,7 +572,7 @@ function onError(message, error) { /** * Check for AES-GCM support and configuration by the user. Only browsers that * implement the current WebCrypto specification support native AES-GCM. - * @return {Boolean} If authenticated encryption should be used + * @returns {Boolean} If authenticated encryption should be used */ function nativeAEAD() { return util.getWebCrypto() && config.aead_protect; diff --git a/src/packet/packetlist.js b/src/packet/packetlist.js index 41946213..35c561c7 100644 --- a/src/packet/packetlist.js +++ b/src/packet/packetlist.js @@ -91,7 +91,7 @@ Packetlist.prototype.push = function (packet) { /** * Remove a packet from the list and return it. - * @return {Object} The packet that was removed + * @returns {Object} The packet that was removed */ Packetlist.prototype.pop = function() { if (this.length === 0) { @@ -164,6 +164,8 @@ Packetlist.prototype.map = function (callback) { /** * Executes the callback function once for each element * until it finds one where callback returns a truthy value +* @param {Function} callback +* @returns {Promise} */ Packetlist.prototype.some = async function (callback) { for (let i = 0; i < this.length; i++) { @@ -178,7 +180,7 @@ Packetlist.prototype.some = async function (callback) { /** * Traverses packet tree and returns first matching packet * @param {module:enums.packet} type The packet type - * @return {module:packet/packet|null} + * @returns {module:packet/packet|null} */ Packetlist.prototype.findPacket = function (type) { const packetlist = this.filterByTag(type); diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 5342b649..23a90fdb 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -67,7 +67,7 @@ export default function PublicKeyEncryptedSessionKey() { * @param {Integer} position Position to start reading from the input string * @param {Integer} len Length of the packet or the remaining length of * input at position - * @return {module:packet/public_key_encrypted_session_key} Object representation + * @returns {module:packet/public_key_encrypted_session_key} Object representation */ PublicKeyEncryptedSessionKey.prototype.read = function (bytes) { this.version = bytes[0]; @@ -88,7 +88,7 @@ PublicKeyEncryptedSessionKey.prototype.read = function (bytes) { /** * Create a string representation of a tag 1 packet * - * @return {Uint8Array} The Uint8Array representation + * @returns {Uint8Array} The Uint8Array representation */ PublicKeyEncryptedSessionKey.prototype.write = function () { const arr = [new Uint8Array([this.version]), this.publicKeyId.write(), new Uint8Array([enums.write(enums.publicKey, this.publicKeyAlgorithm)])]; @@ -100,6 +100,11 @@ PublicKeyEncryptedSessionKey.prototype.write = function () { return util.concatUint8Array(arr); }; +/** + * Encrypt session key packet + * @param {module:packet/public_key} key Public key + * @returns {Promise} + */ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { let data = String.fromCharCode(enums.write(enums.symmetric, this.sessionKeyAlgorithm)); @@ -112,11 +117,12 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { if (algo === enums.publicKey.ecdh) { toEncrypt = new type_mpi(crypto.pkcs5.encode(data)); } else { - toEncrypt = new type_mpi(crypto.pkcs1.eme.encode(data, key.params[0].byteLength())); + toEncrypt = new type_mpi(await crypto.pkcs1.eme.encode(data, key.params[0].byteLength())); } this.encrypted = await crypto.publicKeyEncrypt( algo, key.params, toEncrypt, key.fingerprint); + return true; }; /** @@ -125,7 +131,7 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) { * * @param {module:packet/secret_key} key * Private key with secret params unlocked - * @return {String} The unencrypted session key + * @returns {Promise} */ PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) { const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); @@ -150,6 +156,7 @@ PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) { this.sessionKey = key; this.sessionKeyAlgorithm = enums.read(enums.symmetric, decoded.charCodeAt(0)); } + return true; }; /** diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index dc9d7a50..13fd624d 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -81,7 +81,7 @@ function parse_cleartext_params(hash_algorithm, cleartext, algorithm) { const hash = util.Uint8Array_to_str(hashfn(cleartext)); if (hash !== hashtext) { - return new Error("Hash mismatch."); + return new Error("Incorrect key passphrase"); } const algo = enums.write(enums.publicKey, algorithm); @@ -151,7 +151,7 @@ SecretKey.prototype.read = function (bytes) { }; /** Creates an OpenPGP key packet for the given key. - * @return {String} A string of bytes containing the secret key OpenPGP packet + * @returns {String} A string of bytes containing the secret key OpenPGP packet */ SecretKey.prototype.write = function () { const arr = [this.writePublicKey()]; @@ -172,21 +172,23 @@ SecretKey.prototype.write = function () { * and the passphrase is empty or undefined, the key will be set as not encrypted. * This can be used to remove passphrase protection after calling decrypt(). * @param {String} passphrase + * @returns {Promise} */ -SecretKey.prototype.encrypt = function (passphrase) { +SecretKey.prototype.encrypt = async function (passphrase) { if (this.isDecrypted && !passphrase) { this.encrypted = null; - return; + return false; } else if (!passphrase) { throw new Error('The key must be decrypted before removing passphrase protection.'); } const s2k = new type_s2k(); + s2k.salt = await crypto.random.getRandomBytes(8); const symmetric = 'aes256'; const cleartext = write_cleartext_params('sha1', this.algorithm, this.params); const key = produceEncryptionKey(s2k, passphrase, symmetric); const blockLen = crypto.cipher[symmetric].blockSize; - const iv = crypto.random.getRandomBytes(blockLen); + const iv = await crypto.random.getRandomBytes(blockLen); const arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])]; arr.push(s2k.write()); @@ -194,6 +196,7 @@ SecretKey.prototype.encrypt = function (passphrase) { arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv)); this.encrypted = util.concatUint8Array(arr); + return true; }; function produceEncryptionKey(s2k, passphrase, algorithm) { @@ -208,12 +211,10 @@ function produceEncryptionKey(s2k, passphrase, algorithm) { * @link module:packet/secret_key.isDecrypted should be * false otherwise a call to this function is not needed * - * @param {String} str_passphrase The passphrase for this private key - * as string - * @return {Boolean} True if the passphrase was correct or param already - * decrypted; false if not + * @param {String} passphrase The passphrase for this private key as string + * @returns {Promise} */ -SecretKey.prototype.decrypt = function (passphrase) { +SecretKey.prototype.decrypt = async function (passphrase) { if (this.isDecrypted) { return true; } @@ -261,11 +262,12 @@ SecretKey.prototype.decrypt = function (passphrase) { const privParams = parse_cleartext_params(hash, cleartext, this.algorithm); if (privParams instanceof Error) { - return false; + throw privParams; } this.params = this.params.concat(privParams); this.isDecrypted = true; this.encrypted = null; + return true; }; diff --git a/src/packet/signature.js b/src/packet/signature.js index b1449a17..4e0fd88d 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -211,6 +211,7 @@ Signature.prototype.write = function () { * Signs provided data. This needs to be done prior to serialization. * @param {module:packet/secret_key} key private key used to sign the message. * @param {Object} data Contains packets to be signed. + * @returns {Promise} */ Signature.prototype.sign = async function (key, data) { const signatureType = enums.write(enums.signature, this.signatureType); @@ -247,6 +248,7 @@ Signature.prototype.sign = async function (key, data) { this.signature = await crypto.signature.sign( publicKeyAlgorithm, hashAlgorithm, key.params, toHash ); + return true; }; /** @@ -615,7 +617,7 @@ Signature.prototype.calculateTrailer = function () { * @param {String|Object} data data which on the signature applies * @param {module:packet/public_subkey|module:packet/public_key| * module:packet/secret_subkey|module:packet/secret_key} key the public key to verify the signature - * @return {boolean} True if message is verified, else false. + * @return {Promise} True if message is verified, else false. */ Signature.prototype.verify = async function (key, data) { const signatureType = enums.write(enums.signature, this.signatureType); diff --git a/src/packet/sym_encrypted_aead_protected.js b/src/packet/sym_encrypted_aead_protected.js index 631ea56a..af469913 100644 --- a/src/packet/sym_encrypted_aead_protected.js +++ b/src/packet/sym_encrypted_aead_protected.js @@ -64,23 +64,21 @@ SymEncryptedAEADProtected.prototype.write = function () { * Decrypt the encrypted payload. * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' * @param {Uint8Array} key The session key used to encrypt the payload - * @return {Promise} Nothing is returned + * @return {Promise} */ -SymEncryptedAEADProtected.prototype.decrypt = function (sessionKeyAlgorithm, key) { - return crypto.gcm.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv).then(decrypted => { - this.packets.read(decrypted); - }); +SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) { + this.packets.read(await crypto.gcm.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv)); + return true; }; /** * Encrypt the packet list payload. * @param {String} sessionKeyAlgorithm The session key's cipher algorithm e.g. 'aes128' * @param {Uint8Array} key The session key used to encrypt the payload - * @return {Promise} Nothing is returned + * @return {Promise} */ -SymEncryptedAEADProtected.prototype.encrypt = function (sessionKeyAlgorithm, key) { - this.iv = crypto.random.getRandomValues(new Uint8Array(IV_LEN)); // generate new random IV - return crypto.gcm.encrypt(sessionKeyAlgorithm, this.packets.write(), key, this.iv).then(encrypted => { - this.encrypted = encrypted; - }); +SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { + this.iv = await crypto.random.getRandomBytes(IV_LEN); // generate new random IV + this.encrypted = await crypto.gcm.encrypt(sessionKeyAlgorithm, this.packets.write(), key, this.iv); + return true; }; diff --git a/src/packet/sym_encrypted_integrity_protected.js b/src/packet/sym_encrypted_integrity_protected.js index 25a9075d..335dfd5b 100644 --- a/src/packet/sym_encrypted_integrity_protected.js +++ b/src/packet/sym_encrypted_integrity_protected.js @@ -79,11 +79,11 @@ SymEncryptedIntegrityProtected.prototype.write = function () { * Encrypt the payload in the packet. * @param {String} sessionKeyAlgorithm The selected symmetric encryption algorithm to be used e.g. 'aes128' * @param {Uint8Array} key The key of cipher blocksize length to be used - * @return {Promise} + * @return {Promise} */ -SymEncryptedIntegrityProtected.prototype.encrypt = function (sessionKeyAlgorithm, key) { +SymEncryptedIntegrityProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) { const bytes = this.packets.write(); - const prefixrandom = crypto.getPrefixRandom(sessionKeyAlgorithm); + const prefixrandom = await crypto.getPrefixRandom(sessionKeyAlgorithm); const repeat = new Uint8Array([prefixrandom[prefixrandom.length - 2], prefixrandom[prefixrandom.length - 1]]); const prefix = util.concatUint8Array([prefixrandom, repeat]); const mdc = new Uint8Array([0xD3, 0x14]); // modification detection code packet @@ -98,17 +98,16 @@ SymEncryptedIntegrityProtected.prototype.encrypt = function (sessionKeyAlgorithm this.encrypted = crypto.cfb.encrypt(prefixrandom, sessionKeyAlgorithm, tohash, key, false); this.encrypted = this.encrypted.subarray(0, prefix.length + tohash.length); } - - return Promise.resolve(); + return true; }; /** * Decrypts the encrypted data contained in the packet. * @param {String} sessionKeyAlgorithm The selected symmetric encryption algorithm to be used e.g. 'aes128' * @param {Uint8Array} key The key of cipher blocksize length to be used - * @return {Promise} + * @return {Promise} */ -SymEncryptedIntegrityProtected.prototype.decrypt = function (sessionKeyAlgorithm, key) { +SymEncryptedIntegrityProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) { let decrypted; if (sessionKeyAlgorithm.substr(0, 3) === 'aes') { // AES optimizations. Native code for node, asmCrypto for browser. decrypted = aesDecrypt(sessionKeyAlgorithm, this.encrypted, key); diff --git a/src/packet/sym_encrypted_session_key.js b/src/packet/sym_encrypted_session_key.js index fa6d25ab..3884f072 100644 --- a/src/packet/sym_encrypted_session_key.js +++ b/src/packet/sym_encrypted_session_key.js @@ -52,7 +52,7 @@ export default function SymEncryptedSessionKey() { this.sessionKeyEncryptionAlgorithm = null; this.sessionKeyAlgorithm = 'aes256'; this.encrypted = null; - this.s2k = new type_s2k(); + this.s2k = null; } /** @@ -73,6 +73,7 @@ SymEncryptedSessionKey.prototype.read = function(bytes) { const algo = enums.read(enums.symmetric, bytes[1]); // A string-to-key (S2K) specifier, length as defined above. + this.s2k = new type_s2k(); const s2klength = this.s2k.read(bytes.subarray(2, bytes.length)); // Optionally, the encrypted session key itself, which is decrypted @@ -101,12 +102,11 @@ SymEncryptedSessionKey.prototype.write = function() { }; /** - * Decrypts the session key (only for public key encrypted session key - * packets (tag 1) - * - * @return {Uint8Array} The unencrypted session key + * Decrypts the session key + * @param {String} passphrase The passphrase in string form + * @return {Promise} */ -SymmetricallyEncrypted.prototype.decrypt = function (sessionKeyAlgorithm, key) { +SymmetricallyEncrypted.prototype.decrypt = async function (sessionKeyAlgorithm, key) { const decrypted = crypto.cfb.decrypt(sessionKeyAlgorithm, key, this.encrypted, true); // for modern cipher (blocklength != 64 bit, except for Twofish) MDC is required if (!this.ignore_mdc_error && @@ -73,13 +71,20 @@ SymmetricallyEncrypted.prototype.decrypt = function (sessionKeyAlgorithm, key) { } this.packets.read(decrypted); - return Promise.resolve(); + return true; }; -SymmetricallyEncrypted.prototype.encrypt = function (algo, key) { +/** + * Encrypt the symmetrically-encrypted packet data + * @param {module:enums.symmetric} sessionKeyAlgorithm + * Symmetric key algorithm to use // See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880 9.2} + * @param {Uint8Array} key The key of cipher blocksize length to be used + * @returns {Promise} + */ +SymmetricallyEncrypted.prototype.encrypt = async function (algo, key) { const data = this.packets.write(); - this.encrypted = crypto.cfb.encrypt(crypto.getPrefixRandom(algo), algo, data, key, true); + this.encrypted = crypto.cfb.encrypt(await crypto.getPrefixRandom(algo), algo, data, key, true); - return Promise.resolve(); + return true; }; diff --git a/src/type/s2k.js b/src/type/s2k.js index 1493b18f..d0294dc1 100644 --- a/src/type/s2k.js +++ b/src/type/s2k.js @@ -46,7 +46,7 @@ export default function S2K() { /** Eight bytes of salt in a binary string. * @type {String} */ - this.salt = crypto.random.getRandomBytes(8); + this.salt = null; } S2K.prototype.get_count = function () { diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index 4e68e25c..2d8469bb 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -19,28 +19,68 @@ import util from '../util.js'; import crypto from '../crypto'; import packet from '../packet'; -const INITIAL_RANDOM_SEED = 50000; // random bytes seeded to worker -const RANDOM_SEED_REQUEST = 20000; // random bytes seeded after worker request +/** + * Message handling + */ +function handleMessage(workerId) { + return function(event) { + const msg = event.data; + switch (msg.event) { + case 'method-return': + if (msg.err) { + // fail + const err = new Error(msg.err); + // add worker stack + err.workerStack = msg.stack; + this.tasks[msg.id].reject(err); + } else { + // success + this.tasks[msg.id].resolve(msg.data); + } + delete this.tasks[msg.id]; + this.workers[workerId].requests--; + break; + case 'request-seed': + this.seedRandom(workerId, msg.amount); + break; + default: + throw new Error('Unknown Worker Event.'); + } + }; +} /** * Initializes a new proxy and loads the web worker * @constructor - * @param {String} path The path to the worker or 'openpgp.worker.js' by default - * @param {Object} config config The worker configuration - * @param {Object} worker alternative to path parameter: web worker initialized with 'openpgp.worker.js' - * @return {Promise} + * @param {String} path The path to the worker or 'openpgp.worker.js' by default + * @param {Number} n number of workers to initialize if path given + * @param {Object} config config The worker configuration + * @param {Array} worker alternative to path parameter: web worker initialized with 'openpgp.worker.js' */ -export default function AsyncProxy({ path='openpgp.worker.js', worker, config } = {}) { - this.worker = worker || new Worker(path); - this.worker.onmessage = this.onMessage.bind(this); - this.worker.onerror = e => { - throw new Error('Unhandled error in openpgp worker: ' + e.message + ' (' + e.filename + ':' + e.lineno + ')'); - }; - this.seedRandom(INITIAL_RANDOM_SEED); +export default function AsyncProxy({ path='openpgp.worker.js', n = 1, workers = [], config } = {}) { - if (config) { - this.worker.postMessage({ event:'configure', config }); + if (workers.length) { + this.workers = workers; } + else { + this.workers = []; + while (this.workers.length < n) { + this.workers.push(new Worker(path)); + } + } + + let workerId = 0; + this.workers.forEach(worker => { + worker.requests = 0; + worker.onmessage = handleMessage(workerId++).bind(this); + worker.onerror = e => { + throw new Error('Unhandled error in openpgp worker: ' + e.message + ' (' + e.filename + ':' + e.lineno + ')'); + }; + + if (config) { + worker.postMessage({ event:'configure', config }); + } + }); // Cannot rely on task order being maintained, use object keyed by request ID to track tasks this.tasks = {}; @@ -55,61 +95,22 @@ AsyncProxy.prototype.getID = function() { return this.currentID++; }; -/** - * Message handling - */ -AsyncProxy.prototype.onMessage = function(event) { - const msg = event.data; - switch (msg.event) { - case 'method-return': - if (msg.err) { - // fail - const err = new Error(msg.err); - // add worker stack - err.workerStack = msg.stack; - this.tasks[msg.id].reject(err); - } else { - // success - this.tasks[msg.id].resolve(msg.data); - } - delete this.tasks[msg.id]; - break; - case 'request-seed': - this.seedRandom(RANDOM_SEED_REQUEST); - break; - default: - throw new Error('Unknown Worker Event.'); - } -}; - /** * Send message to worker with random data * @param {Integer} size Number of bytes to send */ -AsyncProxy.prototype.seedRandom = function(size) { - const buf = this.getRandomBuffer(size); - this.worker.postMessage({ event:'seed-random', buf }, util.getTransferables(buf)); +AsyncProxy.prototype.seedRandom = async function(workerId, size) { + const buf = await crypto.random.getRandomBytes(size); + this.workers[workerId].postMessage({ event:'seed-random', buf }, util.getTransferables(buf)); }; /** - * Get Uint8Array with random numbers - * @param {Integer} size Length of buffer - * @return {Uint8Array} - */ -AsyncProxy.prototype.getRandomBuffer = function(size) { - if (!size) { - return null; - } - const buf = new Uint8Array(size); - crypto.random.getRandomValues(buf); - return buf; -}; - -/** - * Terminates the worker + * Terminates the workers */ AsyncProxy.prototype.terminate = function() { - this.worker.terminate(); + this.workers.forEach(worker => { + worker.terminate(); + }); }; /** @@ -119,11 +120,21 @@ AsyncProxy.prototype.terminate = function() { * @return {Promise} see the corresponding public api functions for their return types */ AsyncProxy.prototype.delegate = function(method, options) { + const id = this.getID(); + const requests = this.workers.map(worker => worker.requests); + const minRequests = Math.min(requests); + let workerId = 0; + for(; workerId < this.workers.length; workerId++) { + if (this.workers[workerId].requests === minRequests) { + break; + } + } return new Promise((resolve, reject) => { // clone packets (for web worker structured cloning algorithm) - this.worker.postMessage({ id:id, event:method, options:packet.clone.clonePackets(options) }, util.getTransferables(options)); + this.workers[workerId].postMessage({ id:id, event:method, options:packet.clone.clonePackets(options) }, util.getTransferables(options)); + this.workers[workerId].requests++; // remember to handle parsing cloned packets from worker this.tasks[id] = { resolve: data => resolve(packet.clone.parseClonedPackets(data, method)), reject }; diff --git a/src/worker/worker.js b/src/worker/worker.js index b0354aac..7b934376 100644 --- a/src/worker/worker.js +++ b/src/worker/worker.js @@ -24,10 +24,29 @@ self.window = {}; // to make UMD bundles work importScripts('openpgp.js'); var openpgp = window.openpgp; +var randomQueue = []; +var randomRequested = false; var MIN_SIZE_RANDOM_BUFFER = 40000; var MAX_SIZE_RANDOM_BUFFER = 60000; +var MIN_SIZE_RANDOM_REQUEST = 20000; -openpgp.crypto.random.randomBuffer.init(MAX_SIZE_RANDOM_BUFFER); +/** + * Handle random buffer exhaustion by requesting more random bytes from the main window + * @return {Promise} Empty promise whose resolution indicates that the buffer has been refilled + */ +function randomCallback() { + + if (!randomRequested) { + self.postMessage({ event: 'request-seed', amount: MAX_SIZE_RANDOM_BUFFER }); + } + randomRequested = true; + + return new Promise(function(resolve, reject) { + randomQueue.push(resolve); + }); +} + +openpgp.crypto.random.randomBuffer.init(MAX_SIZE_RANDOM_BUFFER, randomCallback); /** * Handle messages from the main window. @@ -43,6 +62,13 @@ self.onmessage = function(event) { case 'seed-random': seedRandom(msg.buf); + + var queueCopy = randomQueue; + randomQueue = []; + for (var i = 0; i < queueCopy.length; i++) { + queueCopy[i](); + } + break; default: @@ -99,8 +125,8 @@ function delegate(id, method, options) { * @param {Object} event Contains event type and data */ function response(event) { - if (openpgp.crypto.random.randomBuffer.size < MIN_SIZE_RANDOM_BUFFER) { - self.postMessage({ event: 'request-seed' }); + if (!randomRequested && openpgp.crypto.random.randomBuffer.size < MIN_SIZE_RANDOM_BUFFER) { + self.postMessage({ event: 'request-seed', amount: MIN_SIZE_RANDOM_REQUEST }); } self.postMessage(event, openpgp.util.getTransferables(event.data)); } diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 0a59504d..212a38a3 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -278,19 +278,19 @@ describe('API functional testing', function() { }); function testCFB(plaintext, resync) { - symmAlgos.forEach(function(algo) { - const symmKey = crypto.generateSessionKey(algo); - const symmencData = crypto.cfb.encrypt(crypto.getPrefixRandom(algo), algo, util.str_to_Uint8Array(plaintext), symmKey, resync); + symmAlgos.forEach(async function(algo) { + const symmKey = await crypto.generateSessionKey(algo); + const symmencData = crypto.cfb.encrypt(await crypto.getPrefixRandom(algo), algo, util.str_to_Uint8Array(plaintext), symmKey, resync); const text = util.Uint8Array_to_str(crypto.cfb.decrypt(algo, symmKey, symmencData, resync)); expect(text).to.equal(plaintext); }); } function testAESCFB(plaintext) { - symmAlgos.forEach(function(algo) { + symmAlgos.forEach(async function(algo) { if(algo.substr(0,3) === 'aes') { - const symmKey = crypto.generateSessionKey(algo); - const rndm = crypto.getPrefixRandom(algo); + const symmKey = await crypto.generateSessionKey(algo); + const rndm = await crypto.getPrefixRandom(algo); const repeat = new Uint8Array([rndm[rndm.length - 2], rndm[rndm.length - 1]]); const prefix = util.concatUint8Array([rndm, repeat]); @@ -307,9 +307,9 @@ describe('API functional testing', function() { function testAESGCM(plaintext) { symmAlgos.forEach(function(algo) { if(algo.substr(0,3) === 'aes') { - it(algo, function() { - const key = crypto.generateSessionKey(algo); - const iv = crypto.random.getRandomValues(new Uint8Array(crypto.gcm.ivLength)); + it(algo, async function() { + const key = await crypto.generateSessionKey(algo); + const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength); return crypto.gcm.encrypt( algo, util.str_to_Uint8Array(plaintext), key, iv @@ -373,11 +373,10 @@ describe('API functional testing', function() { it('Asymmetric using RSA with eme_pkcs1 padding', function () { const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256')); - const RSAUnencryptedData = crypto.pkcs1.eme.encode(symmKey, RSApubMPIs[0].byteLength()) - const RSAUnencryptedMPI = new openpgp.MPI(RSAUnencryptedData); - return crypto.publicKeyEncrypt( - 1, RSApubMPIs, RSAUnencryptedMPI - ).then(RSAEncryptedData => { + return crypto.pkcs1.eme.encode(symmKey, RSApubMPIs[0].byteLength()).then(RSAUnencryptedData => { + const RSAUnencryptedMPI = new openpgp.MPI(RSAUnencryptedData); + return crypto.publicKeyEncrypt(1, RSApubMPIs, RSAUnencryptedMPI); + }).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( 1, RSApubMPIs.concat(RSAsecMPIs), RSAEncryptedData @@ -393,12 +392,10 @@ describe('API functional testing', function() { it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { const symmKey = util.Uint8Array_to_str(crypto.generateSessionKey('aes256')); - const ElgamalUnencryptedData = crypto.pkcs1.eme.encode(symmKey, ElgamalpubMPIs[0].byteLength()); - const ElgamalUnencryptedMPI = new openpgp.MPI(ElgamalUnencryptedData); - - return crypto.publicKeyEncrypt( - 16, ElgamalpubMPIs, ElgamalUnencryptedMPI - ).then(ElgamalEncryptedData => { + return crypto.pkcs1.eme.encode(symmKey, ElgamalpubMPIs[0].byteLength()).then(ElgamalUnencryptedData => { + const ElgamalUnencryptedMPI = new openpgp.MPI(ElgamalUnencryptedData); + return crypto.publicKeyEncrypt(16, ElgamalpubMPIs, ElgamalUnencryptedMPI); + }).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( 16, ElgamalpubMPIs.concat(ElgamalsecMPIs), ElgamalEncryptedData diff --git a/test/crypto/random.js b/test/crypto/random.js index 3e53fa12..4c8056d7 100644 --- a/test/crypto/random.js +++ b/test/crypto/random.js @@ -12,9 +12,9 @@ describe('Random Buffer', function() { expect(randomBuffer).to.exist; }); - it('Throw error if not initialized', function () { + it('Throw error if not initialized', async function () { expect(randomBuffer.set.bind(randomBuffer)).to.throw('RandomBuffer is not initialized'); - expect(randomBuffer.get.bind(randomBuffer)).to.throw('RandomBuffer is not initialized'); + await expect(randomBuffer.get(new Uint8Array(1))).to.eventually.be.rejectedWith('RandomBuffer is not initialized'); }); it('Initialization', function () { @@ -56,13 +56,13 @@ describe('Random Buffer', function() { expect(randomBuffer.size).to.equal(1); }); - it('Get Method', function () { + it('Get Method', async function () { randomBuffer.init(5); let buf = new Uint8Array(5); buf[0] = 1; buf[1] = 2; buf[2] = 5; buf[3] = 7; buf[4] = 8; randomBuffer.set(buf); buf = new Uint32Array(2); - expect(randomBuffer.get.bind(randomBuffer, buf)).to.throw('Invalid type: buf not an Uint8Array'); + await expect(randomBuffer.get(buf)).to.eventually.be.rejectedWith('Invalid type: buf not an Uint8Array'); buf = new Uint8Array(2); randomBuffer.get(buf); expect(equal(randomBuffer.buffer, [1,2,5,0,0])).to.be.true; @@ -74,6 +74,6 @@ describe('Random Buffer', function() { expect(buf).to.to.have.property('1', 2); expect(equal(randomBuffer.buffer, [1,0,0,0,0])).to.be.true; expect(randomBuffer.size).to.equal(1); - expect(function() { randomBuffer.get(buf); }).to.throw('Random number buffer depleted'); + await expect(randomBuffer.get(buf)).to.eventually.be.rejectedWith('Random number buffer depleted'); }); }); diff --git a/test/general/ecc_nist.js b/test/general/ecc_nist.js index df5339b9..f88708d8 100644 --- a/test/general/ecc_nist.js +++ b/test/general/ecc_nist.js @@ -142,7 +142,7 @@ describe('Elliptic Curve Cryptography', function () { data[name].pub_key = pub.keys[0]; return data[name].pub_key; } - function load_priv_key(name) { + async function load_priv_key(name) { if (data[name].priv_key) { return data[name].priv_key; } @@ -151,7 +151,7 @@ describe('Elliptic Curve Cryptography', function () { expect(pk.err).to.not.exist; expect(pk.keys).to.have.length(1); expect(pk.keys[0].primaryKey.getKeyId().toHex()).to.equal(data[name].id); - expect(pk.keys[0].decrypt(data[name].pass)).to.be.true; + expect(await pk.keys[0].decrypt(data[name].pass)).to.be.true; data[name].priv_key = pk.keys[0]; return data[name].priv_key; } @@ -160,10 +160,10 @@ describe('Elliptic Curve Cryptography', function () { load_pub_key('juliet'); done(); }); - it('Load private key', function (done) { - load_priv_key('romeo'); - load_priv_key('juliet'); - done(); + it('Load private key', async function () { + await load_priv_key('romeo'); + await load_priv_key('juliet'); + return true; }); it('Verify clear signed message', function () { const pub = load_pub_key('juliet'); @@ -175,52 +175,45 @@ describe('Elliptic Curve Cryptography', function () { expect(result.signatures[0].valid).to.be.true; }); }); - it('Sign message', function () { - const romeo = load_priv_key('romeo'); - return openpgp.sign({privateKeys: [romeo], data: data.romeo.message + "\n"}).then(function (signed) { - const romeo = load_pub_key('romeo'); - const msg = openpgp.cleartext.readArmored(signed.data); - return openpgp.verify({publicKeys: [romeo], message: msg}).then(function (result) { - expect(result).to.exist; - expect(result.data.trim()).to.equal(data.romeo.message); - expect(result.signatures).to.have.length(1); - expect(result.signatures[0].valid).to.be.true; - }); - }); + it('Sign message', async function () { + const romeoPrivate = await load_priv_key('romeo'); + const signed = await openpgp.sign({privateKeys: [romeoPrivate], data: data.romeo.message + "\n"}); + const romeoPublic = load_pub_key('romeo'); + const msg = openpgp.cleartext.readArmored(signed.data); + const result = await openpgp.verify({publicKeys: [romeoPublic], message: msg}); + + expect(result).to.exist; + expect(result.data.trim()).to.equal(data.romeo.message); + expect(result.signatures).to.have.length(1); + expect(result.signatures[0].valid).to.be.true; }); - it('Decrypt and verify message', function () { + it('Decrypt and verify message', async function () { const juliet = load_pub_key('juliet'); - const romeo = load_priv_key('romeo'); + const romeo = await load_priv_key('romeo'); const msg = openpgp.message.readArmored(data.juliet.message_encrypted); - return openpgp.decrypt( - {privateKeys: romeo, publicKeys: [juliet], message: msg} - ).then(function (result) { - expect(result).to.exist; - // trim required because https://github.com/openpgpjs/openpgpjs/issues/311 - expect(result.data.trim()).to.equal(data.juliet.message); - expect(result.signatures).to.have.length(1); - expect(result.signatures[0].valid).to.be.true; - }); + const result = await openpgp.decrypt({privateKeys: romeo, publicKeys: [juliet], message: msg}); + + expect(result).to.exist; + // trim required because https://github.com/openpgpjs/openpgpjs/issues/311 + expect(result.data.trim()).to.equal(data.juliet.message); + expect(result.signatures).to.have.length(1); + expect(result.signatures[0].valid).to.be.true; }); - it('Encrypt and sign message', function () { - const romeo = load_priv_key('romeo'); - const juliet = load_pub_key('juliet'); - expect(romeo.decrypt(data.romeo.pass)).to.be.true; - return openpgp.encrypt( - {publicKeys: [juliet], privateKeys: [romeo], data: data.romeo.message + "\n"} - ).then(function (encrypted) { - const message = openpgp.message.readArmored(encrypted.data); - const romeo = load_pub_key('romeo'); - const juliet = load_priv_key('juliet'); - return openpgp.decrypt( - {privateKeys: juliet, publicKeys: [romeo], message: message} - ).then(function (result) { - expect(result).to.exist; - expect(result.data.trim()).to.equal(data.romeo.message); - expect(result.signatures).to.have.length(1); - expect(result.signatures[0].valid).to.be.true; - }); - }); + it('Encrypt and sign message', async function () { + const romeoPrivate = await load_priv_key('romeo'); + const julietPublic = load_pub_key('juliet'); + expect(await romeoPrivate.decrypt(data.romeo.pass)).to.be.true; + const encrypted = await openpgp.encrypt({publicKeys: [julietPublic], privateKeys: [romeoPrivate], data: data.romeo.message + "\n"}); + + const message = openpgp.message.readArmored(encrypted.data); + const romeoPublic = load_pub_key('romeo'); + const julietPrivate = await load_priv_key('juliet'); + const result = await openpgp.decrypt({privateKeys: julietPrivate, publicKeys: [romeoPublic], message: message}); + + expect(result).to.exist; + expect(result.data.trim()).to.equal(data.romeo.message); + expect(result.signatures).to.have.length(1); + expect(result.signatures[0].valid).to.be.true; }); it('Generate key', function () { const options = { diff --git a/test/general/key.js b/test/general/key.js index ea7a1147..87d53190 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -1037,26 +1037,24 @@ describe('Key', function() { }); }); - it('Encrypt key with new passphrase', function() { + it('Encrypt key with new passphrase', async function() { const userId = 'test '; const opt = {numBits: 512, userIds: userId, passphrase: 'passphrase'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys - return openpgp.generateKey(opt).then(function(key) { - key = key.key; - const armor1 = key.armor(); - const armor2 = key.armor(); - expect(armor1).to.equal(armor2); - expect(key.decrypt('passphrase')).to.be.true; - expect(key.primaryKey.isDecrypted).to.be.true; - key.encrypt('new_passphrase'); - expect(key.primaryKey.isDecrypted).to.be.false; - expect(key.decrypt('passphrase')).to.be.false; - expect(key.primaryKey.isDecrypted).to.be.false; - expect(key.decrypt('new_passphrase')).to.be.true; - expect(key.primaryKey.isDecrypted).to.be.true; - const armor3 = key.armor(); - expect(armor3).to.not.equal(armor1); - }); + const key = (await openpgp.generateKey(opt)).key; + const armor1 = key.armor(); + const armor2 = key.armor(); + expect(armor1).to.equal(armor2); + expect(await key.decrypt('passphrase')).to.be.true; + expect(key.primaryKey.isDecrypted).to.be.true; + await key.encrypt('new_passphrase'); + expect(key.primaryKey.isDecrypted).to.be.false; + await expect(key.decrypt('passphrase')).to.eventually.be.rejectedWith('Incorrect key passphrase'); + expect(key.primaryKey.isDecrypted).to.be.false; + expect(await key.decrypt('new_passphrase')).to.be.true; + expect(key.primaryKey.isDecrypted).to.be.true; + const armor3 = key.armor(); + expect(armor3).to.not.equal(armor1); }); it('Generate key - ensure keyExpirationTime works', function() { @@ -1268,7 +1266,7 @@ describe('Key', function() { }); }); - it('Throw user friendly error when reformatting encrypted key', function() { + it('Reject with user-friendly error when reformatting encrypted key', function() { const opt = {numBits: 512, userIds: 'test1 ', passphrase: '1234'}; if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys return openpgp.generateKey(opt).then(function(original) { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 6a71eec6..e678d292 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -363,7 +363,7 @@ describe('OpenPGP.js public api tests', function() { postMessage: function() {} }; openpgp.initWorker({ - worker: workerStub + workers: [workerStub] }); expect(openpgp.getWorker()).to.exist; openpgp.destroyWorker(); @@ -522,7 +522,7 @@ describe('OpenPGP.js public api tests', function() { postMessage: function() {} }; openpgp.initWorker({ - worker: workerStub + workers: [workerStub] }); const proxyGenStub = stub(openpgp.getWorker(), 'delegate'); getWebCryptoAllStub.returns(); @@ -635,12 +635,12 @@ describe('OpenPGP.js public api tests', function() { openpgp.config.aead_protect = aead_protectVal; }); - it('Decrypting key with wrong passphrase returns false', function () { - expect(privateKey.keys[0].decrypt('wrong passphrase')).to.be.false; + it('Decrypting key with wrong passphrase rejected', function () { + expect(privateKey.keys[0].decrypt('wrong passphrase')).to.eventually.be.rejectedWith('Incorrect key passphrase'); }); - it('Decrypting key with correct passphrase returns true', function () { - expect(privateKey.keys[0].decrypt(passphrase)).to.be.true; + it('Decrypting key with correct passphrase returns true', async function () { + expect(await privateKey.keys[0].decrypt(passphrase)).to.be.true; }); tryTests('CFB mode (asm.js)', tests, { @@ -719,7 +719,7 @@ describe('OpenPGP.js public api tests', function() { privateKey: privateKey.keys[0], passphrase: 'incorrect' }).catch(function(error){ - expect(error.message).to.match(/Invalid passphrase/); + expect(error.message).to.match(/Incorrect key passphrase/); }); }); }); @@ -727,9 +727,10 @@ describe('OpenPGP.js public api tests', function() { describe('encryptSessionKey, decryptSessionKeys', function() { const sk = new Uint8Array([0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01]); - beforeEach(function(done) { - expect(privateKey.keys[0].decrypt(passphrase)).to.be.true; - privateKey.keys[0].verifyPrimaryUser().then(() => done()); + beforeEach(async function() { + expect(await privateKey.keys[0].decrypt(passphrase)).to.be.true; + await privateKey.keys[0].verifyPrimaryUser(); + return true; }); it('should encrypt with public key', function() { @@ -867,33 +868,13 @@ describe('OpenPGP.js public api tests', function() { '=6XMW\r\n' + '-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n'; - beforeEach(function (done) { - expect(privateKey.keys[0].decrypt(passphrase)).to.be.true; - Promise.all([ - privateKey.keys[0].verifyPrimaryUser(), - privateKey_2000_2008.keys[0].verifyPrimaryUser(), - privateKey_1337.keys[0].verifyPrimaryUser(), - privateKey_2038_2045.keys[0].verifyPrimaryUser() - ]).then(() => done()); - }); - - it('should encrypt then decrypt', function () { - const encOpt = { - data: plaintext, - publicKeys: publicKey.keys - }; - const decOpt = { - privateKeys: privateKey.keys - }; - return openpgp.encrypt(encOpt).then(function (encrypted) { - expect(encrypted.data).to.match(/^-----BEGIN PGP MESSAGE/); - decOpt.message = openpgp.message.readArmored(encrypted.data); - return openpgp.decrypt(decOpt); - }).then(function (decrypted) { - expect(decrypted.data).to.equal(plaintext); - expect(decrypted.signatures).to.exist; - expect(decrypted.signatures.length).to.equal(0); - }); + beforeEach( async function () { + expect(await privateKey.keys[0].decrypt(passphrase)).to.be.true; + await privateKey.keys[0].verifyPrimaryUser(); + await privateKey_2000_2008.keys[0].verifyPrimaryUser(); + await privateKey_1337.keys[0].verifyPrimaryUser(); + await privateKey_2038_2045.keys[0].verifyPrimaryUser(); + return true; }); it('should encrypt then decrypt', function () { @@ -1001,9 +982,9 @@ describe('OpenPGP.js public api tests', function() { }); }); - it('should encrypt using custom session key and decrypt using session key', function () { + it('should encrypt using custom session key and decrypt using session key', async function () { const sessionKey = { - data: openpgp.crypto.generateSessionKey('aes256'), + data: await openpgp.crypto.generateSessionKey('aes256'), algorithm: 'aes256' }; const encOpt = { diff --git a/test/general/packet.js b/test/general/packet.js index 8e924fff..c5fe98d4 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -55,7 +55,7 @@ describe("Packet", function() { '=KXkj\n' + '-----END PGP PRIVATE KEY BLOCK-----'; - it('Symmetrically encrypted packet', function(done) { + it('Symmetrically encrypted packet', async function() { const message = new openpgp.packet.List(); const literal = new openpgp.packet.Literal(); @@ -68,18 +68,17 @@ describe("Packet", function() { const key = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); const algo = 'aes256'; - enc.encrypt(algo, key); + await enc.encrypt(algo, key); const msg2 = new openpgp.packet.List(); msg2.read(message.write()); msg2[0].ignore_mdc_error = true; - msg2[0].decrypt(algo, key); + await msg2[0].decrypt(algo, key); expect(stringify(msg2[0].packets[0].data)).to.equal(stringify(literal.data)); - done(); }); - it('Symmetrically encrypted packet - MDC error for modern cipher', function() { + it('Symmetrically encrypted packet - MDC error for modern cipher', async function() { const message = new openpgp.packet.List(); const literal = new openpgp.packet.Literal(); @@ -87,19 +86,19 @@ describe("Packet", function() { const enc = new openpgp.packet.SymmetricallyEncrypted(); message.push(enc); - enc.packets.push(literal); + await enc.packets.push(literal); const key = '12345678901234567890123456789012'; const algo = 'aes256'; - enc.encrypt(algo, key); + await enc.encrypt(algo, key); const msg2 = new openpgp.packet.List(); msg2.read(message.write()); - expect(msg2[0].decrypt.bind(msg2[0], algo, key)).to.throw('Decryption failed due to missing MDC in combination with modern cipher.'); + expect(msg2[0].decrypt(algo, key)).to.eventually.be.rejectedWith('Decryption failed due to missing MDC in combination with modern cipher.'); }); - it('Sym. encrypted integrity protected packet', function(done) { + it('Sym. encrypted integrity protected packet', async function() { const key = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); const algo = 'aes256'; @@ -110,15 +109,14 @@ describe("Packet", function() { msg.push(enc); literal.setText('Hello world!'); enc.packets.push(literal); - enc.encrypt(algo, key); + await enc.encrypt(algo, key); const msg2 = new openpgp.packet.List(); msg2.read(msg.write()); - msg2[0].decrypt(algo, key); + await msg2[0].decrypt(algo, key); expect(stringify(msg2[0].packets[0].data)).to.equal(stringify(literal.data)); - done(); }); it('Sym. encrypted AEAD protected packet', function() { @@ -310,7 +308,7 @@ describe("Packet", function() { }); }); - it('Sym. encrypted session key reading/writing', function(done) { + it('Sym. encrypted session key reading/writing', async function() { const passphrase = 'hello'; const algo = 'aes256'; @@ -323,23 +321,22 @@ describe("Packet", function() { msg.push(enc); key_enc.sessionKeyAlgorithm = algo; - key_enc.decrypt(passphrase); + await key_enc.encrypt(passphrase); const key = key_enc.sessionKey; literal.setText('Hello world!'); enc.packets.push(literal); - enc.encrypt(algo, key); + await enc.encrypt(algo, key); const msg2 = new openpgp.packet.List(); msg2.read(msg.write()); - msg2[0].decrypt(passphrase); + await msg2[0].decrypt(passphrase); const key2 = msg2[0].sessionKey; - msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); + await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2); expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data)); - done(); }); it('Secret key encryption/decryption test', function() { diff --git a/test/general/x25519.js b/test/general/x25519.js index cd21bca1..b7849304 100644 --- a/test/general/x25519.js +++ b/test/general/x25519.js @@ -127,7 +127,7 @@ describe('X25519 Cryptography', function () { return data[name].pub_key; } - function load_priv_key(name) { + async function load_priv_key(name) { if (data[name].priv_key) { return data[name].priv_key; } @@ -136,7 +136,7 @@ describe('X25519 Cryptography', function () { expect(pk.err).to.not.exist; expect(pk.keys).to.have.length(1); expect(pk.keys[0].primaryKey.getKeyId().toHex()).to.equal(data[name].id); - expect(pk.keys[0].decrypt(data[name].pass)).to.be.true; + expect(await pk.keys[0].decrypt(data[name].pass)).to.be.true; data[name].priv_key = pk.keys[0]; return data[name].priv_key; } @@ -149,10 +149,10 @@ describe('X25519 Cryptography', function () { // This test is slow because the keys are generated by GPG2, which // by default chooses a larger number for S2K iterations than we do. - it('Load private key', function (done) { - load_priv_key('light'); - load_priv_key('night'); - done(); + it('Load private key', async function () { + await load_priv_key('light'); + await load_priv_key('night'); + return true; }); it('Verify clear signed message', function () { @@ -167,56 +167,49 @@ describe('X25519 Cryptography', function () { }); }); - it('Sign message', function () { + it('Sign message', async function () { const name = 'light'; - const priv = load_priv_key(name); - return openpgp.sign({ privateKeys: [priv], data: data[name].message + "\n" }).then(function (signed) { - const pub = load_pub_key(name); - const msg = openpgp.cleartext.readArmored(signed.data); - return openpgp.verify({ publicKeys: [pub], message: msg}).then(function (result) { - expect(result).to.exist; - expect(result.data.trim()).to.equal(data[name].message); - expect(result.signatures).to.have.length(1); - expect(result.signatures[0].valid).to.be.true; - }); - }); + const priv = await load_priv_key(name); + const signed = await openpgp.sign({ privateKeys: [priv], data: data[name].message + "\n" }); + const pub = load_pub_key(name); + const msg = openpgp.cleartext.readArmored(signed.data); + const result = await openpgp.verify({ publicKeys: [pub], message: msg}); + + expect(result).to.exist; + expect(result.data.trim()).to.equal(data[name].message); + expect(result.signatures).to.have.length(1); + expect(result.signatures[0].valid).to.be.true; }); - it('Decrypt and verify message', function () { + it('Decrypt and verify message', async function () { const light = load_pub_key('light'); - const night = load_priv_key('night'); - expect(night.decrypt(data.night.pass)).to.be.true; + const night = await load_priv_key('night'); + expect(await night.decrypt(data.night.pass)).to.be.true; const msg = openpgp.message.readArmored(data.night.message_encrypted); - return openpgp.decrypt( - { privateKeys: night, publicKeys: [light], message: msg } - ).then(function (result) { - expect(result).to.exist; - // trim required because https://github.com/openpgpjs/openpgpjs/issues/311 - expect(result.data.trim()).to.equal(data.night.message); - expect(result.signatures).to.have.length(1); - expect(result.signatures[0].valid).to.be.true; - }); + const result = await openpgp.decrypt({ privateKeys: night, publicKeys: [light], message: msg }); + + expect(result).to.exist; + // trim required because https://github.com/openpgpjs/openpgpjs/issues/311 + expect(result.data.trim()).to.equal(data.night.message); + expect(result.signatures).to.have.length(1); + expect(result.signatures[0].valid).to.be.true; }); - it('Encrypt and sign message', function () { - const night = load_pub_key('night'); - const light = load_priv_key('light'); - expect(light.decrypt(data.light.pass)).to.be.true; - openpgp.encrypt( - { publicKeys: [night], privateKeys: [light], data: data.light.message + "\n" } - ).then(function (encrypted) { - const message = openpgp.message.readArmored(encrypted.data); - const light = load_pub_key('light'); - const night = load_priv_key('night'); - return openpgp.decrypt( - { privateKeys: night, publicKeys: [light], message: message } - ).then(function (result) { - expect(result).to.exist; - expect(result.data.trim()).to.equal(data.light.message); - expect(result.signatures).to.have.length(1); - expect(result.signatures[0].valid).to.be.true; - }); - }); + it('Encrypt and sign message', async function () { + const nightPublic = load_pub_key('night'); + const lightPrivate = await load_priv_key('light'); + expect(await lightPrivate.decrypt(data.light.pass)).to.be.true; + const encrypted = await openpgp.encrypt({ publicKeys: [nightPublic], privateKeys: [lightPrivate], data: data.light.message + "\n" }); + + const message = openpgp.message.readArmored(encrypted.data); + const lightPublic = load_pub_key('light'); + const nightPrivate = await load_priv_key('night'); + const result = await openpgp.decrypt({ privateKeys: nightPrivate, publicKeys: [lightPublic], message: message }); + + expect(result).to.exist; + expect(result.data.trim()).to.equal(data.light.message); + expect(result.signatures).to.have.length(1); + expect(result.signatures[0].valid).to.be.true; }); // TODO export, then reimport key and validate diff --git a/test/worker/async_proxy.js b/test/worker/async_proxy.js index f7bcd4de..6c6d1c0b 100644 --- a/test/worker/async_proxy.js +++ b/test/worker/async_proxy.js @@ -47,15 +47,12 @@ tryTests('Async Proxy', tests, { function tests() { - describe('Error handling', function() { - it('Depleted random buffer in worker gives error', function() { - const wProxy = new openpgp.AsyncProxy({ path:'../dist/openpgp.worker.js' }); - wProxy.worker = new Worker('../dist/openpgp.worker.js'); - wProxy.worker.onmessage = wProxy.onMessage.bind(wProxy); - wProxy.seedRandom(10); - return wProxy.delegate('encrypt', { publicKeys:[pubKey], data:plaintext }).catch(function(err) { - expect(err.message).to.match(/Random number buffer depleted/); - }); + describe('Random number pipeline', function() { + it('Random number buffer automatically reseeded', function() { + const worker = new Worker('../dist/openpgp.worker.js'); + const wProxy = new openpgp.AsyncProxy({ path:'../dist/openpgp.worker.js', workers: [worker] }); + + return wProxy.delegate('encrypt', { publicKeys:[pubKey], data:plaintext }); }); });