diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index 21df6a98..1d780356 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -35,19 +35,21 @@ import util from '../util'; import OID from '../type/oid'; import { Curve } from './public_key/elliptic/curves'; import { UnsupportedError } from '../packet/packet'; +import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; /** * Encrypts data using specified algorithm and public key parameters. * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms. - * @param {module:enums.publicKey} algo - Public key algorithm + * @param {module:enums.publicKey} keyAlgo - Public key algorithm + * @param {module:enums.symmetric} symmetricAlgo - Cipher algorithm * @param {Object} publicParams - Algorithm-specific public key parameters - * @param {Uint8Array} data - Data to be encrypted + * @param {Uint8Array} data - Session key data to be encrypted * @param {Uint8Array} fingerprint - Recipient fingerprint * @returns {Promise} Encrypted session key parameters. * @async */ -export async function publicKeyEncrypt(algo, publicParams, data, fingerprint) { - switch (algo) { +export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, data, fingerprint) { + switch (keyAlgo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: { const { n, e } = publicParams; @@ -64,6 +66,14 @@ export async function publicKeyEncrypt(algo, publicParams, data, fingerprint) { oid, kdfParams, data, Q, fingerprint); return { V, C: new ECDHSymkey(C) }; } + case enums.publicKey.x25519: { + const { A } = publicParams; + const { ephemeralPublicKey, wrappedKey } = await publicKey.elliptic.ecdhX.encrypt( + keyAlgo, data, A); + const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); + return { ephemeralPublicKey, C }; + + } default: return []; } @@ -105,6 +115,13 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, return publicKey.elliptic.ecdh.decrypt( oid, kdfParams, V, C.data, Q, d, fingerprint); } + case enums.publicKey.x25519: { + const { A } = publicKeyParams; + const { k } = privateKeyParams; + const { ephemeralPublicKey, C } = sessionKeyParams; + return publicKey.elliptic.ecdhX.decrypt( + algo, ephemeralPublicKey, C.wrappedKey, A, k); + } default: throw new Error('Unknown public key encryption algorithm.'); } @@ -160,7 +177,8 @@ export function parsePublicKeyParams(algo, bytes) { const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read)); return { read: read, publicParams: { oid, Q, kdfParams } }; } - case enums.publicKey.ed25519: { + case enums.publicKey.ed25519: + case enums.publicKey.x25519: { const A = bytes.subarray(read, read + 32); read += A.length; return { read, publicParams: { A } }; } @@ -211,6 +229,10 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) { const seed = bytes.subarray(read, read + 32); read += seed.length; return { read, privateParams: { seed } }; } + case enums.publicKey.x25519: { + const k = bytes.subarray(read, read + 32); read += k.length; + return { read, privateParams: { k } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -248,6 +270,16 @@ export function parseEncSessionKeyParams(algo, bytes) { const C = new ECDHSymkey(); C.read(bytes.subarray(read)); return { V, C }; } + // Algorithm-Specific Fields for X25519 encrypted session keys: + // - 32 octets representing an ephemeral X25519 public key. + // - A one-octet size of the following fields. + // - The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet). + // - The encrypted session key. + case enums.publicKey.x25519: { + const ephemeralPublicKey = bytes.subarray(read, read + 32); read += ephemeralPublicKey.length; + const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); + return { ephemeralPublicKey, C }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -261,7 +293,7 @@ export function parseEncSessionKeyParams(algo, bytes) { */ export function serializeParams(algo, params) { // Some algorithms do not rely on MPIs to store the binary params - const algosWithNativeRepresentation = new Set([enums.publicKey.ed25519]); + const algosWithNativeRepresentation = new Set([enums.publicKey.ed25519, enums.publicKey.x25519]); const orderedParams = Object.keys(params).map(name => { const param = params[name]; if (!util.isUint8Array(param)) return param.write(); @@ -313,6 +345,11 @@ export function generateParams(algo, bits, oid) { privateParams: { seed }, publicParams: { A } })); + case enums.publicKey.x25519: + return publicKey.elliptic.ecdhX.generate(algo).then(({ A, k }) => ({ + privateParams: { k }, + publicParams: { A } + })); case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -369,6 +406,11 @@ export async function validateParams(algo, publicParams, privateParams) { const { seed } = privateParams; return publicKey.elliptic.eddsa.validateParams(algo, A, seed); } + case enums.publicKey.x25519: { + const { A } = publicParams; + const { k } = privateParams; + return publicKey.elliptic.ecdhX.validateParams(algo, A, k); + } default: throw new Error('Unknown public key algorithm.'); } diff --git a/src/crypto/hkdf.js b/src/crypto/hkdf.js new file mode 100644 index 00000000..d7d6d969 --- /dev/null +++ b/src/crypto/hkdf.js @@ -0,0 +1,21 @@ +/** + * @fileoverview This module implements HKDF using either the WebCrypto API or Node.js' crypto API. + * @module crypto/hkdf + * @private + */ + +import enums from '../enums'; +import util from '../util'; + +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); + +export default async function HKDF(hashAlgo, key, salt, info, length) { + const hash = enums.read(enums.webHash, hashAlgo); + if (!hash) throw new Error('Hash algo not supported with HKDF'); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey('raw', key, 'HKDF', false, ['deriveBits']); + const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, length * 8); + return new Uint8Array(bits); +} diff --git a/src/crypto/public_key/elliptic/ecdh_x.js b/src/crypto/public_key/elliptic/ecdh_x.js new file mode 100644 index 00000000..4e367ee6 --- /dev/null +++ b/src/crypto/public_key/elliptic/ecdh_x.js @@ -0,0 +1,125 @@ +/** + * @fileoverview Key encryption and decryption for RFC 6637 ECDH + * @module crypto/public_key/elliptic/ecdh + * @private + */ + +import nacl from '@openpgp/tweetnacl/nacl-fast-light'; +import * as aesKW from '../../aes_kw'; +import { getRandomBytes } from '../../random'; + +import enums from '../../../enums'; +import util from '../../../util'; +import getCipher from '../../cipher/getCipher'; +import computeHKDF from '../../hkdf'; + +const HKDF_INFO = { + x25519: util.encodeUTF8('OpenPGP X25519') +}; + +/** + * Generate ECDH key for Montgomery curves + * @param {module:enums.publicKey} algo - Algorithm identifier + * @returns Promise<{ A, k }> + */ +export async function generate(algo) { + switch (algo) { + case enums.publicKey.x25519: { + // k stays in little-endian, unlike legacy ECDH over curve25519 + const k = getRandomBytes(32); + k[0] &= 248; + k[31] = (k[31] & 127) | 64; + const { publicKey: A } = nacl.box.keyPair.fromSecretKey(k); + return { A, k }; + } + default: + throw new Error('Unsupported ECDH algorithm'); + } +} + +/** +* Validate ECDH parameters +* @param {module:enums.publicKey} algo - Algorithm identifier +* @param {Uint8Array} A - ECDH public point +* @param {Uint8Array} k - ECDH secret scalar +* @returns {Promise} Whether params are valid. +* @async +*/ +export async function validateParams(algo, A, k) { + switch (algo) { + case enums.publicKey.x25519: { + /** + * Derive public point A' from private key + * and expect A == A' + */ + const { publicKey } = nacl.box.keyPair.fromSecretKey(k); + return util.equalsUint8Array(A, publicKey); + } + + default: + return false; + } +} + +/** + * Wrap and encrypt a session key + * + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {Uint8Array} data - session key data to be encrypted + * @param {Uint8Array} recipientA - Recipient public key (K_B) + * @returns {Promise<{ + * ephemeralPublicKey: Uint8Array, + * wrappedKey: Uint8Array + * }>} ephemeral public key (K_A) and encrypted key + * @async + */ +export async function encrypt(algo, data, recipientA) { + switch (algo) { + case enums.publicKey.x25519: { + const ephemeralSecretKey = getRandomBytes(32); + const sharedSecret = nacl.scalarMult(ephemeralSecretKey, recipientA); + const { publicKey: ephemeralPublicKey } = nacl.box.keyPair.fromSecretKey(ephemeralSecretKey); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + recipientA, + sharedSecret + ]); + const { keySize } = getCipher(enums.symmetric.aes128); + const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); + const wrappedKey = aesKW.wrap(encryptionKey, data); + return { ephemeralPublicKey, wrappedKey }; + } + + default: + throw new Error('Unsupported ECDH algorithm'); + } +} + +/** + * Decrypt and unwrap the session key + * + * @param {module:enums.publicKey} algo - Algorithm identifier + * @param {Uint8Array} ephemeralPublicKey - (K_A) + * @param {Uint8Array} wrappedKey, + * @param {Uint8Array} A - Recipient public key (K_b), needed for KDF + * @param {Uint8Array} k - Recipient secret key (b) + * @returns {Promise} decrypted session key data + * @async + */ +export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) { + switch (algo) { + case enums.publicKey.x25519: { + const sharedSecret = nacl.scalarMult(k, ephemeralPublicKey); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + A, + sharedSecret + ]); + const { keySize } = getCipher(enums.symmetric.aes128); + const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); + return aesKW.unwrap(encryptionKey, wrappedKey); + } + default: + throw new Error('Unsupported ECDH algorithm'); + } +} diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 474ebb07..cc89241c 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -104,19 +104,19 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { * Validate (non-legacy) EdDSA parameters * @param {module:enums.publicKey} algo - Algorithm identifier * @param {Uint8Array} A - EdDSA public point - * @param {Uint8Array} k - EdDSA secret seed + * @param {Uint8Array} seed - EdDSA secret seed * @param {Uint8Array} oid - (legacy only) EdDSA OID * @returns {Promise} Whether params are valid. * @async */ -export async function validateParams(algo, A, k) { +export async function validateParams(algo, A, seed) { switch (algo) { case enums.publicKey.ed25519: { /** * Derive public point A' from private key * and expect A == A' */ - const { publicKey } = nacl.sign.keyPair.fromSeed(k); + const { publicKey } = nacl.sign.keyPair.fromSeed(seed); return util.equalsUint8Array(A, publicKey); } diff --git a/src/crypto/public_key/elliptic/index.js b/src/crypto/public_key/elliptic/index.js index 1c338acd..4af14817 100644 --- a/src/crypto/public_key/elliptic/index.js +++ b/src/crypto/public_key/elliptic/index.js @@ -30,7 +30,8 @@ import * as ecdsa from './ecdsa'; import * as eddsaLegacy from './eddsa_legacy'; import * as eddsa from './eddsa'; import * as ecdh from './ecdh'; +import * as ecdhX from './ecdh_x'; export { - Curve, ecdh, ecdsa, eddsaLegacy, eddsa, generate, getPreferredHashAlgo + Curve, ecdh, ecdhX, ecdsa, eddsaLegacy, eddsa, generate, getPreferredHashAlgo }; diff --git a/src/enums.js b/src/enums.js index 84bf9ccd..1ac86d53 100644 --- a/src/enums.js +++ b/src/enums.js @@ -117,14 +117,14 @@ export default { aedh: 23, /** Reserved for AEDSA */ aedsa: 24, - /** ECDH 25519 (encrypt only) */ + /** X25519 (Encrypt only) */ x25519: 25, - /** ECDH 448 (encrypt only) */ + /** X448 (Encrypt only) */ x448: 26, - /** EdDSA 25519 (sign only) */ + /** Ed25519 (Sign only) */ ed25519: 27, - /** EdDSA 448 (sign only) */ - eddsa448: 28 + /** Ed448 (Sign only) */ + ed448: 28 }, /** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2} diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 09552fea..231c62c7 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -67,13 +67,17 @@ class PublicKeyEncryptedSessionKeyPacket { * @param {Uint8Array} bytes - Payload of a tag 1 packet */ read(bytes) { - this.version = bytes[0]; + let i = 0; + this.version = bytes[i++]; if (this.version !== VERSION) { throw new UnsupportedError(`Version ${this.version} of the PKESK packet is unsupported.`); } - this.publicKeyID.read(bytes.subarray(1, bytes.length)); - this.publicKeyAlgorithm = bytes[9]; - this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(10)); + i += this.publicKeyID.read(bytes.subarray(i)); + this.publicKeyAlgorithm = bytes[i++]; + this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(i), this.version); + if (this.publicKeyAlgorithm === enums.publicKey.x25519) { + this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); + } } /** @@ -99,14 +103,10 @@ class PublicKeyEncryptedSessionKeyPacket { * @async */ async encrypt(key) { - const data = util.concatUint8Array([ - new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]), - this.sessionKey, - util.writeChecksum(this.sessionKey) - ]); const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); + const encoded = encodeSessionKey(this.version, algo, this.sessionKeyAlgorithm, this.sessionKey); this.encrypted = await crypto.publicKeyEncrypt( - algo, key.publicParams, data, key.getFingerprintBytes()); + algo, this.sessionKeyAlgorithm, key.publicParams, encoded, key.getFingerprintBytes()); } /** @@ -123,35 +123,85 @@ class PublicKeyEncryptedSessionKeyPacket { throw new Error('Decryption error'); } - const randomPayload = randomSessionKey ? util.concatUint8Array([ - new Uint8Array([randomSessionKey.sessionKeyAlgorithm]), - randomSessionKey.sessionKey, - util.writeChecksum(randomSessionKey.sessionKey) - ]) : null; - const decoded = await crypto.publicKeyDecrypt(this.publicKeyAlgorithm, key.publicParams, key.privateParams, this.encrypted, key.getFingerprintBytes(), randomPayload); - const symmetricAlgoByte = decoded[0]; - const sessionKey = decoded.subarray(1, decoded.length - 2); - const checksum = decoded.subarray(decoded.length - 2); - const computedChecksum = util.writeChecksum(sessionKey); - const isValidChecksum = computedChecksum[0] === checksum[0] & computedChecksum[1] === checksum[1]; + const randomPayload = randomSessionKey ? + encodeSessionKey(this.version, this.publicKeyAlgorithm, randomSessionKey.sessionKeyAlgorithm, randomSessionKey.sessionKey) : + null; + const decryptedData = await crypto.publicKeyDecrypt(this.publicKeyAlgorithm, key.publicParams, key.privateParams, this.encrypted, key.getFingerprintBytes(), randomPayload); - if (randomSessionKey) { - // We must not leak info about the validity of the decrypted checksum or cipher algo. - // The decrypted session key must be of the same algo and size as the random session key, otherwise we discard it and use the random data. - const isValidPayload = isValidChecksum & symmetricAlgoByte === randomSessionKey.sessionKeyAlgorithm & sessionKey.length === randomSessionKey.sessionKey.length; - this.sessionKeyAlgorithm = util.selectUint8(isValidPayload, symmetricAlgoByte, randomSessionKey.sessionKeyAlgorithm); - this.sessionKey = util.selectUint8Array(isValidPayload, sessionKey, randomSessionKey.sessionKey); + const { sessionKey, sessionKeyAlgorithm } = decodeSessionKey(this.version, this.publicKeyAlgorithm, decryptedData, randomSessionKey); - } else { - const isValidPayload = isValidChecksum && enums.read(enums.symmetric, symmetricAlgoByte); - if (isValidPayload) { - this.sessionKey = sessionKey; - this.sessionKeyAlgorithm = symmetricAlgoByte; - } else { - throw new Error('Decryption error'); - } + // v3 Montgomery curves have cleartext cipher algo + if (this.publicKeyAlgorithm !== enums.publicKey.x25519) { + this.sessionKeyAlgorithm = sessionKeyAlgorithm; } + this.sessionKey = sessionKey; } } export default PublicKeyEncryptedSessionKeyPacket; + + +function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { + switch (keyAlgo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.elgamal: + case enums.publicKey.ecdh: { + // add checksum + return util.concatUint8Array([ + new Uint8Array([cipherAlgo]), + sessionKeyData, + util.writeChecksum(sessionKeyData.subarray(sessionKeyData.length % 8)) + ]); + } + case enums.publicKey.x25519: + return sessionKeyData; + default: + throw new Error('Unsupported public key algorithm'); + } +} + + +function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { + switch (keyAlgo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.elgamal: + case enums.publicKey.ecdh: { + // verify checksum in constant time + const result = decryptedData.subarray(0, decryptedData.length - 2); + const checksum = decryptedData.subarray(decryptedData.length - 2); + const computedChecksum = util.writeChecksum(result.subarray(result.length % 8)); + const isValidChecksum = computedChecksum[0] === checksum[0] & computedChecksum[1] === checksum[1]; + const decryptedSessionKey = { sessionKeyAlgorithm: result[0], sessionKey: result.subarray(1) }; + if (randomSessionKey) { + // We must not leak info about the validity of the decrypted checksum or cipher algo. + // The decrypted session key must be of the same algo and size as the random session key, otherwise we discard it and use the random data. + const isValidPayload = isValidChecksum & + decryptedSessionKey.sessionKeyAlgorithm === randomSessionKey.sessionKeyAlgorithm & + decryptedSessionKey.sessionKey.length === randomSessionKey.sessionKey.length; + return { + sessionKey: util.selectUint8Array(isValidPayload, decryptedSessionKey.sessionKey, randomSessionKey.sessionKey), + sessionKeyAlgorithm: util.selectUint8( + isValidPayload, + decryptedSessionKey.sessionKeyAlgorithm, + randomSessionKey.sessionKeyAlgorithm + ) + }; + } else { + const isValidPayload = isValidChecksum && enums.read(enums.symmetric, decryptedSessionKey.sessionKeyAlgorithm); + if (isValidPayload) { + return decryptedSessionKey; + } else { + throw new Error('Decryption error'); + } + } + } + case enums.publicKey.x25519: + return { + sessionKey: decryptedData + }; + default: + throw new Error('Unsupported public key algorithm'); + } +} diff --git a/src/type/ecdh_symkey.js b/src/type/ecdh_symkey.js index f3e17879..71d05983 100644 --- a/src/type/ecdh_symkey.js +++ b/src/type/ecdh_symkey.js @@ -16,7 +16,7 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA /** - * Encoded symmetric key for ECDH + * Encoded symmetric key for ECDH (incl. legacy x25519) * * @module type/ecdh_symkey * @private @@ -26,26 +26,23 @@ import util from '../util'; class ECDHSymmetricKey { constructor(data) { - if (typeof data === 'undefined') { - data = new Uint8Array([]); - } else if (util.isString(data)) { - data = util.stringToUint8Array(data); - } else { - data = new Uint8Array(data); + if (data) { + this.data = data; } - this.data = data; } /** - * Read an ECDHSymmetricKey from an Uint8Array - * @param {Uint8Array} input - Where to read the encoded symmetric key from + * Read an ECDHSymmetricKey from an Uint8Array: + * - 1 octect for the length `l` + * - `l` octects of encoded session key data + * @param {Uint8Array} bytes * @returns {Number} Number of read bytes. */ - read(input) { - if (input.length >= 1) { - const length = input[0]; - if (input.length >= 1 + length) { - this.data = input.subarray(1, 1 + length); + read(bytes) { + if (bytes.length >= 1) { + const length = bytes[0]; + if (bytes.length >= 1 + length) { + this.data = bytes.subarray(1, 1 + length); return 1 + this.data.length; } } @@ -54,7 +51,7 @@ class ECDHSymmetricKey { /** * Write an ECDHSymmetricKey as an Uint8Array - * @returns {Uint8Array} An array containing the value + * @returns {Uint8Array} Serialised data */ write() { return util.concatUint8Array([new Uint8Array([this.data.length]), this.data]); diff --git a/src/type/ecdh_x_symkey.js b/src/type/ecdh_x_symkey.js new file mode 100644 index 00000000..ecef5715 --- /dev/null +++ b/src/type/ecdh_x_symkey.js @@ -0,0 +1,47 @@ +/** + * Encoded symmetric key for x25519 and x448 + * The payload format varies for v3 and v6 PKESK: + * the former includes an algorithm byte preceeding the encrypted session key. + * + * @module type/x25519x448_symkey + */ + +import util from '../util'; + +class ECDHXSymmetricKey { + static fromObject({ wrappedKey, algorithm }) { + const instance = new ECDHXSymmetricKey(); + instance.wrappedKey = wrappedKey; + instance.algorithm = algorithm; + return instance; + } + + /** + * - 1 octect for the length `l` + * - `l` octects of encoded session key data (with optional leading algorithm byte) + * @param {Uint8Array} bytes + * @returns {Number} Number of read bytes. + */ + read(bytes) { + let read = 0; + let followLength = bytes[read++]; + this.algorithm = followLength % 2 ? bytes[read++] : null; // session key size is always even + followLength -= followLength % 2; + this.wrappedKey = bytes.subarray(read, read + followLength); read += followLength; + } + + /** + * Write an MontgomerySymmetricKey as an Uint8Array + * @returns {Uint8Array} Serialised data + */ + write() { + return util.concatUint8Array([ + this.algorithm ? + new Uint8Array([this.wrappedKey.length + 1, this.algorithm]) : + new Uint8Array([this.wrappedKey.length]), + this.wrappedKey + ]); + } +} + +export default ECDHXSymmetricKey; diff --git a/src/type/keyid.js b/src/type/keyid.js index b9c88ac6..2abf6ae0 100644 --- a/src/type/keyid.js +++ b/src/type/keyid.js @@ -42,6 +42,7 @@ class KeyID { */ read(bytes) { this.bytes = util.uint8ArrayToString(bytes.subarray(0, 8)); + return this.bytes.length; } /** diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index 7c8069e3..68009ec3 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -269,7 +269,7 @@ module.exports = () => describe('API functional testing', function() { it('Asymmetric using RSA with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoRSA, RSAPublicParams, symmKey).then(RSAEncryptedData => { + return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, symmKey).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( algoRSA, RSAPublicParams, RSAPrivateParams, RSAEncryptedData ).then(data => { @@ -280,7 +280,7 @@ module.exports = () => describe('API functional testing', function() { it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoElGamal, elGamalPublicParams, symmKey).then(ElgamalEncryptedData => { + return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, symmKey).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( algoElGamal, elGamalPublicParams, elGamalPrivateParams, ElgamalEncryptedData ).then(data => { diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index fe43a653..193cda7b 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -8,6 +8,7 @@ const KDFParams = require('../../src/type/kdf_params'); const elliptic_curves = require('../../src/crypto/public_key/elliptic'); const util = require('../../src/util'); const elliptic_data = require('./elliptic_data'); +const random = require('../../src/crypto/random'); const key_data = elliptic_data.key_data; /* eslint-disable no-invalid-this */ @@ -131,52 +132,107 @@ module.exports = () => describe('ECDH key exchange @lightweight', function () { 71, 245, 86, 3, 168, 101, 74, 209, 105 ]); - describe('ECDHE key generation', function () { - const ecdh = elliptic_curves.ecdh; + const ecdh = elliptic_curves.ecdh; - it('Invalid curve', async function () { - if (!openpgp.config.useIndutnyElliptic && !util.getNodeCrypto()) { - this.skip(); - } - const curve = new elliptic_curves.Curve('secp256k1'); + it('Invalid curve', async function () { + if (!openpgp.config.useIndutnyElliptic && !util.getNodeCrypto()) { + this.skip(); + } + const curve = new elliptic_curves.Curve('secp256k1'); + const oid = new OID(curve.oid); + const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); + const data = util.stringToUint8Array('test'); + expect( + ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1) + ).to.be.rejectedWith(Error, /Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|Unknown point format/); + }); + + it('Different keys', async function () { + const curve = new elliptic_curves.Curve('curve25519'); + const oid = new OID(curve.oid); + const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); + const data = util.stringToUint8Array('test'); + const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1); + await expect( + ecdh.decrypt(oid, kdfParams, V, C, Q2, d2, fingerprint1) + ).to.be.rejectedWith(/Key Data Integrity failed/); + }); + + it('Invalid fingerprint', async function () { + const curve = new elliptic_curves.Curve('curve25519'); + const oid = new OID(curve.oid); + const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); + const data = util.stringToUint8Array('test'); + const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q2, fingerprint1); + await expect( + ecdh.decrypt(oid, kdfParams, V, C, Q2, d2, fingerprint2) + ).to.be.rejectedWith(/Key Data Integrity failed/); + }); + + it('Successful exchange x25519 (legacy)', async function () { + const curve = new elliptic_curves.Curve('curve25519'); + const oid = new OID(curve.oid); + const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); + const data = util.stringToUint8Array('test'); + const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1); + expect(await ecdh.decrypt(oid, kdfParams, V, C, Q1, d1, fingerprint1)).to.deep.equal(data); + }); + + it('Successful exchange x25519', async function () { + const { ecdhX } = elliptic_curves; + const data = random.getRandomBytes(32); + // Bob's keys from https://www.rfc-editor.org/rfc/rfc7748#section-6.1 + const b = util.hexToUint8Array('5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb'); + const K_B = util.hexToUint8Array('de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f'); + const { ephemeralPublicKey, wrappedKey } = await ecdhX.encrypt(openpgp.enums.publicKey.x25519, data, K_B); + expect(await ecdhX.decrypt(openpgp.enums.publicKey.x25519, ephemeralPublicKey, wrappedKey, K_B, b)).to.deep.equal(data); + }); + + ['p256', 'p384', 'p521'].forEach(curveName => { + it(`NIST ${curveName} - Successful exchange`, async function () { + const curve = new elliptic_curves.Curve(curveName); const oid = new OID(curve.oid); const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); const data = util.stringToUint8Array('test'); - expect( - ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1) - ).to.be.rejectedWith(Error, /Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|Unknown point format/); + const Q = key_data[curveName].pub; + const d = key_data[curveName].priv; + const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1); + expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); }); - it('Different keys', async function () { - const curve = new elliptic_curves.Curve('curve25519'); - const oid = new OID(curve.oid); - const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); - const data = util.stringToUint8Array('test'); - const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1); - await expect( - ecdh.decrypt(oid, kdfParams, V, C, Q2, d2, fingerprint1) - ).to.be.rejectedWith(/Key Data Integrity failed/); + }); + + describe('Comparing decrypting with and without native crypto', () => { + let sinonSandbox; + let getWebCryptoStub; + let getNodeCryptoStub; + + beforeEach(function () { + sinonSandbox = sandbox.create(); }); - it('Invalid fingerprint', async function () { - const curve = new elliptic_curves.Curve('curve25519'); - const oid = new OID(curve.oid); - const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); - const data = util.stringToUint8Array('test'); - const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q2, fingerprint1); - await expect( - ecdh.decrypt(oid, kdfParams, V, C, Q2, d2, fingerprint2) - ).to.be.rejectedWith(/Key Data Integrity failed/); - }); - it('Successful exchange curve25519', async function () { - const curve = new elliptic_curves.Curve('curve25519'); - const oid = new OID(curve.oid); - const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); - const data = util.stringToUint8Array('test'); - const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q1, d1, fingerprint1)).to.deep.equal(data); + + afterEach(function () { + sinonSandbox.restore(); }); + const disableNative = () => { + enableNative(); + // stubbed functions return undefined + getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto'); + getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); + }; + const enableNative = () => { + getWebCryptoStub && getWebCryptoStub.restore(); + getNodeCryptoStub && getNodeCryptoStub.restore(); + }; + ['p256', 'p384', 'p521'].forEach(curveName => { - it(`NIST ${curveName} - Successful exchange`, async function () { + it(`NIST ${curveName}`, async function () { + const nodeCrypto = util.getNodeCrypto(); + const webCrypto = util.getWebCrypto(); + if (!nodeCrypto && !webCrypto) { + this.skip(); + } + const curve = new elliptic_curves.Curve(curveName); const oid = new OID(curve.oid); const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); @@ -184,58 +240,14 @@ module.exports = () => describe('ECDH key exchange @lightweight', function () { const Q = key_data[curveName].pub; const d = key_data[curveName].priv; const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1); + + const nativeDecryptSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH'); expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - }); - }); - - describe('Comparing decrypting with and without native crypto', () => { - let sinonSandbox; - let getWebCryptoStub; - let getNodeCryptoStub; - - beforeEach(function () { - sinonSandbox = sandbox.create(); - }); - - afterEach(function () { - sinonSandbox.restore(); - }); - - const disableNative = () => { - enableNative(); - // stubbed functions return undefined - getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto'); - getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); - }; - const enableNative = () => { - getWebCryptoStub && getWebCryptoStub.restore(); - getNodeCryptoStub && getNodeCryptoStub.restore(); - }; - - ['p256', 'p384', 'p521'].forEach(curveName => { - it(`NIST ${curveName}`, async function () { - const nodeCrypto = util.getNodeCrypto(); - const webCrypto = util.getWebCrypto(); - if (!nodeCrypto && !webCrypto) { - this.skip(); - } - - const curve = new elliptic_curves.Curve(curveName); - const oid = new OID(curve.oid); - const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); - const data = util.stringToUint8Array('test'); - const Q = key_data[curveName].pub; - const d = key_data[curveName].priv; - const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1); - - const nativeDecryptSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH'); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - disableNative(); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - if (curveName !== 'p521') { // safari does not implement p521 in webcrypto - expect(nativeDecryptSpy.calledOnce).to.be.true; - } - }); + disableNative(); + expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); + if (curveName !== 'p521') { // safari does not implement p521 in webcrypto + expect(nativeDecryptSpy.calledOnce).to.be.true; + } }); }); }); diff --git a/test/general/key.js b/test/general/key.js index 6288bd8c..2b416def 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -3015,17 +3015,27 @@ zWBsBR8VnoOVfEE+VQk6YAi7cTSjcMjfsIez9FYtAQDKo9aCMhUohYyqvhZjn8aS it('Parsing V4 key using new curve25519 format', async function() { const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- -xUkEZBw5PBscroGar9fsilA0q9AX979pBhTNkGQ69vQGGW7kxRxNuABB+eAw -JrQ9A3o1gUJg28ORTQd72+kFo87184qR97a6rRGFzQR0ZXN0wogEEBsIAD4F -gmQcOTwECwkHCAmQT/m+Rl22Ps8DFQgKBBYAAgECGQECmwMCHgEWIQSUlOfm -G7MWJd2909ZP+b5GXbY+zwAAVs/4pWH4l7pWcTATBavVqSATMKi4A+usp89G -J/qaHc+qmcEpIMmPNvLQ7n4F4kEXk8Zwz+OXovVWLQ+Njl5gzooF -=wYg1 +xUkEZB3qzRto01j2k2pwN5ux9w70stPinAdXULLr20CRW7U7h2GSeACch0M+ +qzQg8yjFQ8VBvu3uwgKH9senoHmj72lLSCLTmhFKzQR0ZXN0wogEEBsIAD4F +gmQd6s0ECwkHCAmQIf45+TuC+xMDFQgKBBYAAgECGQECmwMCHgEWIQSWEzMi +jJUHvyIbVKIh/jn5O4L7EwAAUhaHNlgudvxARdPPETUzVgjuWi+YIz8w1xIb +lHQMvIrbe2sGCQIethpWofd0x7DHuv/ciHg+EoxJ/Td6h4pWtIoKx0kEZB3q +zRm4CyA7quliq7yx08AoOqHTuuCgvpkSdEhpp3pEyejQOgBo0p6ywIiLPllY +0t+jpNspHpAGfXID6oqjpYuJw3AfVRBlwnQEGBsIACoFgmQd6s0JkCH+Ofk7 +gvsTApsMFiEElhMzIoyVB78iG1SiIf45+TuC+xMAAGgQuN9G73446ykvJ/mL +sCZ7zGFId2gBd1EnG0FTC4npfOKpck0X8dngByrCxU8LDSfvjsEp/xDAiKsQ +aU71tdtNBQ== +=e7jT -----END PGP PRIVATE KEY BLOCK-----` }); // sanity checks await expect(privateKey.validate()).to.be.fulfilled; const signingKey = await privateKey.getSigningKey(); expect(signingKey.keyPacket.algorithm).to.equal(openpgp.enums.publicKey.ed25519); + expect(signingKey.getAlgorithmInfo()).to.deep.equal({ algorithm: 'ed25519' }); + + const encryptionKey = await privateKey.getEncryptionKey(); + expect(encryptionKey.keyPacket.algorithm).to.equal(openpgp.enums.publicKey.x25519); + expect(encryptionKey.getAlgorithmInfo()).to.deep.equal({ algorithm: 'x25519' }); }); it('Testing key ID and fingerprint for V4 keys', async function() { @@ -4120,7 +4130,7 @@ XvmoLueOOShu01X/kaylMqaT8w== expect(await signatures[0].verified).to.be.true; }); - it('encrypt/decrypt data with the new subkey correctly using curve25519', async function() { + it('encrypt/decrypt data with the new subkey correctly using curve25519 (legacy format)', async function() { const userID = { name: 'test', email: 'a@b.com' }; const vData = 'the data to encrypted!'; const opt = { curve: 'curve25519', userIDs: [userID], format: 'object', subkeys:[] }; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 0378b3ad..82e16136 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -2018,6 +2018,65 @@ aOU= expect(await stream.readToEnd(streamedData)).to.equal(text); }); + it('supports decrypting new x25519 format', async function () { + // v4 key + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUkEZIbSkxsHknQrXGfb+kM2iOsOvin8yE05ff5hF8KE6k+saspAZQCy/kfFUYc2 +GkpOHc42BI+MsysKzk4ofjBAfqM+bb7goQ3hzRV1c2VyIDx1c2VyQHRlc3QudGVz +dD7ChwQTGwgAPQUCZIbSkwmQQezK2iB2tIkWIQRqZza9wQZcwxpjGYNB7MraIHa0 +iQIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAFOeZ7jrKZsCzRfu1ffFa77074st0zRo +BTJXoXBQ1ZzLjsh+ZO6fB2odnYJtQYstv45H/3JyLVogcMnFeYmHeSP3AMdJBGSG +0pMZfpd7TiOQv7uKSK+k4HT9lKr5+dmvb7vox/8ids6unEkAF1v8fCKogIrtBWVT +nVbwnovjM3LLexpXFZSgTKRcNMgPRMJ0BBgbCAAqBQJkhtKTCZBB7MraIHa0iRYh +BGpnNr3BBlzDGmMZg0HsytogdrSJAhsMAADCYs2I9wBakIu9Hhxs4R3Jq9F8J7AH +yxsNL0GomZ+hxiE0MOZwRr10DxfVaRabF1fcf9PHSHX2SwEFXUKMIHgbMQs= +=bJqd +-----END PGP PRIVATE KEY BLOCK-----` }); + + const messageToDecrypt = `-----BEGIN PGP MESSAGE----- + +wUQDYc6clYlCdtoZ3rAsvBDIwvoLmvM0zwViG8Ec0PgFfN5R6C4BqEZD53UZB1WM +J68hXSj1Sa235XAUYE1pZerTKhglvdI9Aeve8+L0w5RDMjmBBA50Yv/YT8liqhNi +mNwbfFbSNhZYWjFada77EKBn60j8QT/xCQzLR1clci7ieW2knw== +=NKye +-----END PGP MESSAGE-----`; + const { data } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: messageToDecrypt }), + decryptionKeys: privateKey + }); + expect(data).to.equal('Hello World!'); + }); + + it('supports encrypting new x25519 format', async function () { + // v4 key + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xUkEZIbSkxsHknQrXGfb+kM2iOsOvin8yE05ff5hF8KE6k+saspAZQCy/kfFUYc2 +GkpOHc42BI+MsysKzk4ofjBAfqM+bb7goQ3hzRV1c2VyIDx1c2VyQHRlc3QudGVz +dD7ChwQTGwgAPQUCZIbSkwmQQezK2iB2tIkWIQRqZza9wQZcwxpjGYNB7MraIHa0 +iQIbAwIeAQIZAQILBwIVCAIWAAMnBwIAAFOeZ7jrKZsCzRfu1ffFa77074st0zRo +BTJXoXBQ1ZzLjsh+ZO6fB2odnYJtQYstv45H/3JyLVogcMnFeYmHeSP3AMdJBGSG +0pMZfpd7TiOQv7uKSK+k4HT9lKr5+dmvb7vox/8ids6unEkAF1v8fCKogIrtBWVT +nVbwnovjM3LLexpXFZSgTKRcNMgPRMJ0BBgbCAAqBQJkhtKTCZBB7MraIHa0iRYh +BGpnNr3BBlzDGmMZg0HsytogdrSJAhsMAADCYs2I9wBakIu9Hhxs4R3Jq9F8J7AH +yxsNL0GomZ+hxiE0MOZwRr10DxfVaRabF1fcf9PHSHX2SwEFXUKMIHgbMQs= +=bJqd +-----END PGP PRIVATE KEY BLOCK-----` }); + const plaintext = 'plaintext'; + + const signed = await openpgp.encrypt({ + message: await openpgp.createMessage({ text: plaintext }), + encryptionKeys: privateKey + }); + + const { data } = await openpgp.decrypt({ + message: await openpgp.readMessage({ armoredMessage: signed }), + decryptionKeys: privateKey + }); + expect(data).to.equal(plaintext); + }); + it('should support encrypting with encrypted key with unknown s2k (unparseableKeyMaterial)', async function() { const originalDecryptedKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- @@ -2063,7 +2122,6 @@ VFBLG8uc9IiaKann/DYBAJcZNZHRSfpDoV2pUA5EAEi2MdjxkRysFQnYPRAu decryptionKeys: originalDecryptedKey }); expect(decrypted.data).to.equal('test'); - }); }); describe('encryptSessionKey - unit tests', function() { diff --git a/test/general/x25519.js b/test/general/x25519.js index d70b1fe2..b58505be 100644 --- a/test/general/x25519.js +++ b/test/general/x25519.js @@ -11,7 +11,7 @@ const util = require('../../src/util'); const input = require('./testInputs'); -module.exports = () => (openpgp.config.ci ? describe.skip : describe)('X25519 Cryptography', function () { +module.exports = () => (openpgp.config.ci ? describe.skip : describe)('X25519 Cryptography (legacy format)', function () { const data = { light: { id: '1ecdf026c0245830',