diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index bdb3fd28..a9293e75 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -64,10 +64,11 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, dat oid, kdfParams, data, Q, fingerprint); return { V, C: new ECDHSymkey(C) }; } - case enums.publicKey.x25519: { + case enums.publicKey.x25519: + case enums.publicKey.x448: { if (!util.isAES(symmetricAlgo)) { // see https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/276 - throw new Error('X25519 keys can only encrypt AES session keys'); + throw new Error('X25519 and X448 keys can only encrypt AES session keys'); } const { A } = publicParams; const { ephemeralPublicKey, wrappedKey } = await publicKey.elliptic.ecdhX.encrypt( @@ -116,7 +117,8 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, return publicKey.elliptic.ecdh.decrypt( oid, kdfParams, V, C.data, Q, d, fingerprint); } - case enums.publicKey.x25519: { + case enums.publicKey.x25519: + case enums.publicKey.x448: { const { A } = publicKeyParams; const { k } = privateKeyParams; const { ephemeralPublicKey, C } = sessionKeyParams; @@ -182,7 +184,8 @@ export function parsePublicKeyParams(algo, bytes) { } case enums.publicKey.ed25519: case enums.publicKey.ed448: - case enums.publicKey.x25519: { + case enums.publicKey.x25519: + case enums.publicKey.x448: { const A = bytes.subarray(read, read + getCurvePayloadSize(algo)); read += A.length; return { read, publicParams: { A } }; } @@ -234,8 +237,10 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) { const seed = bytes.subarray(read, read + payloadSize); read += seed.length; return { read, privateParams: { seed } }; } - case enums.publicKey.x25519: { - const k = bytes.subarray(read, read + 32); read += k.length; + case enums.publicKey.x25519: + case enums.publicKey.x448: { + const payloadSize = getCurvePayloadSize(algo); + const k = bytes.subarray(read, read + payloadSize); read += k.length; return { read, privateParams: { k } }; } default: @@ -275,13 +280,15 @@ 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. + // Algorithm-Specific Fields for X25519 or X448 encrypted session keys: + // - 32 octets representing an ephemeral X25519 public key (or 57 octets for X448). // - 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; + case enums.publicKey.x25519: + case enums.publicKey.x448: { + const pointSize = getCurvePayloadSize(algo); + const ephemeralPublicKey = bytes.subarray(read, read + pointSize); read += ephemeralPublicKey.length; const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); return { ephemeralPublicKey, C }; } @@ -298,7 +305,12 @@ 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, enums.publicKey.x25519, enums.publicKey.ed448]); + const algosWithNativeRepresentation = new Set([ + enums.publicKey.ed25519, + enums.publicKey.x25519, + enums.publicKey.ed448, + enums.publicKey.x448 + ]); const orderedParams = Object.keys(params).map(name => { const param = params[name]; if (!util.isUint8Array(param)) return param.write(); @@ -351,6 +363,7 @@ export function generateParams(algo, bits, oid) { publicParams: { A } })); case enums.publicKey.x25519: + case enums.publicKey.x448: return publicKey.elliptic.ecdhX.generate(algo).then(({ A, k }) => ({ privateParams: { k }, publicParams: { A } @@ -411,7 +424,8 @@ export async function validateParams(algo, publicParams, privateParams) { const { seed } = privateParams; return publicKey.elliptic.eddsa.validateParams(algo, A, seed); } - case enums.publicKey.x25519: { + case enums.publicKey.x25519: + case enums.publicKey.x448: { const { A } = publicParams; const { k } = privateParams; return publicKey.elliptic.ecdhX.validateParams(algo, A, k); diff --git a/src/crypto/public_key/elliptic/ecdh_x.js b/src/crypto/public_key/elliptic/ecdh_x.js index d5d3f10e..decdeaab 100644 --- a/src/crypto/public_key/elliptic/ecdh_x.js +++ b/src/crypto/public_key/elliptic/ecdh_x.js @@ -3,7 +3,8 @@ * @module crypto/public_key/elliptic/ecdh */ -import nacl from '@openpgp/tweetnacl/nacl-fast-light'; +import x25519 from '@openpgp/tweetnacl/nacl-fast-light'; +import { x448 } from '@openpgp/noble-curves/ed448'; import * as aesKW from '../../aes_kw'; import { getRandomBytes } from '../../random'; @@ -13,7 +14,8 @@ import getCipher from '../../cipher/getCipher'; import computeHKDF from '../../hkdf'; const HKDF_INFO = { - x25519: util.encodeUTF8('OpenPGP X25519') + x25519: util.encodeUTF8('OpenPGP X25519'), + x448: util.encodeUTF8('OpenPGP X448') }; /** @@ -26,7 +28,12 @@ export async function generate(algo) { case enums.publicKey.x25519: { // k stays in little-endian, unlike legacy ECDH over curve25519 const k = getRandomBytes(32); - const { publicKey: A } = nacl.box.keyPair.fromSecretKey(k); + const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); + return { A, k }; + } + case enums.publicKey.x448: { + const k = x448.utils.randomPrivateKey(); + const A = x448.getPublicKey(k); return { A, k }; } default: @@ -49,7 +56,15 @@ export async function validateParams(algo, A, k) { * Derive public point A' from private key * and expect A == A' */ - const { publicKey } = nacl.box.keyPair.fromSecretKey(k); + const { publicKey } = x25519.box.keyPair.fromSecretKey(k); + return util.equalsUint8Array(A, publicKey); + } + case enums.publicKey.x448: { + /** + * Derive public point A' from private key + * and expect A == A' + */ + const publicKey = x448.getPublicKey(k); return util.equalsUint8Array(A, publicKey); } @@ -74,8 +89,8 @@ 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 sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); + const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey); const hkdfInput = util.concatUint8Array([ ephemeralPublicKey, recipientA, @@ -86,6 +101,20 @@ export async function encrypt(algo, data, recipientA) { const wrappedKey = aesKW.wrap(encryptionKey, data); return { ephemeralPublicKey, wrappedKey }; } + case enums.publicKey.x448: { + const ephemeralSecretKey = x448.utils.randomPrivateKey(); + const sharedSecret = x448.getSharedSecret(ephemeralSecretKey, recipientA); + const ephemeralPublicKey = x448.getPublicKey(ephemeralSecretKey); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + recipientA, + sharedSecret + ]); + const { keySize } = getCipher(enums.symmetric.aes256); + const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize); + const wrappedKey = aesKW.wrap(encryptionKey, data); + return { ephemeralPublicKey, wrappedKey }; + } default: throw new Error('Unsupported ECDH algorithm'); @@ -106,7 +135,7 @@ export async function encrypt(algo, data, recipientA) { export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) { switch (algo) { case enums.publicKey.x25519: { - const sharedSecret = nacl.scalarMult(k, ephemeralPublicKey); + const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey); const hkdfInput = util.concatUint8Array([ ephemeralPublicKey, A, @@ -116,6 +145,17 @@ export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) { const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize); return aesKW.unwrap(encryptionKey, wrappedKey); } + case enums.publicKey.x448: { + const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey); + const hkdfInput = util.concatUint8Array([ + ephemeralPublicKey, + A, + sharedSecret + ]); + const { keySize } = getCipher(enums.symmetric.aes256); + const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize); + return aesKW.unwrap(encryptionKey, wrappedKey); + } default: throw new Error('Unsupported ECDH algorithm'); } diff --git a/src/key/helper.js b/src/key/helper.js index d2795a89..e887ed19 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -356,6 +356,7 @@ export function isValidSigningKeyPacket(keyPacket, signature) { keyAlgo !== enums.publicKey.elgamal && keyAlgo !== enums.publicKey.ecdh && keyAlgo !== enums.publicKey.x25519 && + keyAlgo !== enums.publicKey.x448 && (!signature.keyFlags || (signature.keyFlags[0] & enums.keyFlags.signData) !== 0); } diff --git a/src/message.js b/src/message.js index 8e5c725f..159c003e 100644 --- a/src/message.js +++ b/src/message.js @@ -357,8 +357,9 @@ export class Message { await Promise.all(encryptionKeys.map(key => key.getEncryptionKey() .catch(() => null) // ignore key strength requirements .then(maybeKey => { - if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519) && !aeadAlgoName && !util.isAES(symmetricAlgo)) { // if AEAD is defined, then PKESK v6 are used, and the algo info is encrypted - throw new Error('Could not generate a session key compatible with the given `encryptionKeys`: X22519 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.'); + if (maybeKey && (maybeKey.keyPacket.algorithm === enums.publicKey.x25519 || maybeKey.keyPacket.algorithm === enums.publicKey.x448) && + !aeadAlgoName && !util.isAES(symmetricAlgo)) { // if AEAD is defined, then PKESK v6 are used, and the algo info is encrypted + throw new Error('Could not generate a session key compatible with the given `encryptionKeys`: X22519 and X448 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.'); } }) )); diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index 8121e32c..2618ce32 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -128,7 +128,8 @@ class PublicKeyEncryptedSessionKeyPacket { } this.publicKeyAlgorithm = bytes[offset++]; this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset)); - if (this.version === 3 && this.publicKeyAlgorithm === enums.publicKey.x25519) { + if (this.version === 3 && ( + this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448)) { this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); } } @@ -200,7 +201,9 @@ class PublicKeyEncryptedSessionKeyPacket { const { sessionKey, sessionKeyAlgorithm } = decodeSessionKey(this.version, this.publicKeyAlgorithm, decryptedData, randomSessionKey); // v3 Montgomery curves have cleartext cipher algo - if (this.version === 3 && this.publicKeyAlgorithm !== enums.publicKey.x25519) { + if (this.version === 3 && ( + this.publicKeyAlgorithm !== enums.publicKey.x25519 && this.publicKeyAlgorithm !== enums.publicKey.x448) + ) { this.sessionKeyAlgorithm = sessionKeyAlgorithm; } this.sessionKey = sessionKey; @@ -224,6 +227,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { ]); } case enums.publicKey.x25519: + case enums.publicKey.x448: return sessionKeyData; default: throw new Error('Unsupported public key algorithm'); @@ -270,6 +274,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { } } case enums.publicKey.x25519: + case enums.publicKey.x448: return { sessionKey: decryptedData }; diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 501f0481..4f211386 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -189,6 +189,16 @@ export default () => describe('ECDH key exchange @lightweight', function () { expect(await ecdhX.decrypt(openpgp.enums.publicKey.x25519, ephemeralPublicKey, wrappedKey, K_B, b)).to.deep.equal(data); }); + it('Successful exchange x448', async function () { + const { ecdhX } = elliptic_curves; + const data = random.getRandomBytes(); + // Bob's keys from https://www.rfc-editor.org/rfc/rfc7748#section-6.2 + const b = util.hexToUint8Array('1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d'); + const K_B = util.hexToUint8Array('3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609'); + const { ephemeralPublicKey, wrappedKey } = await ecdhX.encrypt(openpgp.enums.publicKey.x448, data, K_B); + expect(await ecdhX.decrypt(openpgp.enums.publicKey.x448, 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.CurveWithOID(curveName); diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 54d79ca3..3388f26b 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -247,6 +247,61 @@ export default () => { }); }); + describe('Ed448/X448 parameter validation', function() { + let eddsaKey; + let ecdhXKey; + before(async () => { + eddsaKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xXsEZCWHXBwwtqciq6ZFU13s+dyhkWR5tOEmF1oX8OiP1B5ypfqyGVM8DkQh +5eTIMwB1oqJCROANoyA0q2dSigAAbDA5xr74DeClPPXC4ZXJ9uzuJWKvQvE8 +x3EflhgoQCGBM7JfvH5zwdrJvPt8RKDvm0QkZzhPvnFoHnzNBHRlc3TCugQQ +HAgAPgWCZCWHXAQLCQcICZDsN6h/ys3ppwMVCAoEFgACAQIZAQKbAwIeARYh +BOJyE9P2eIcU2N2Ne+w3qH/KzemnAAAh1hTFCcEU77bU3YelrJTCNIOQnvt7 +Hs6yZz2053CQTOC+wHkUQLaYYBEXSNyLZxoyv+NuGTiwbuYtAOlbE2erM7Cx +8B2Qz7M29UkFLMBUfb+yi+gTYYUWCXVQ7Um7MGjjgUG8+9p452i6f28mhRD8 +tTgNAMd5BGQlh1wavTIFgILtbzrqQCiwDGx0YcFNzu9+FZ8vK5Mmm7UEZj0a +y7FWQtZw8tTaU6mY+RrSa52RjzkGLtQAQO++tgYqc+BnCFdCZ3ZYPRvD3mof +ffoo3l4xmto+iyvJZbQ4wQPXttg7VjCpEfOsL9TW9Xs09aIbysKmBBgcCAAq +BYJkJYdcCZDsN6h/ys3ppwKbDBYhBOJyE9P2eIcU2N2Ne+w3qH/KzemnAAC0 +6/eZhh/Oj2gRdab2JeFGWACGIRDKxPXsWRCXR4YrSxcvCKK6rOvsyxQsgIsJ +JyPYkRPfmbKcseUDAEkSBLAfeizDGh7ea0GOdIMhwE/CW4f/H8ULbwi36y13 +x3oMNVaYsI9dZ588Gpi8XYy2jOtqIPQ1AA== +-----END PGP PRIVATE KEY BLOCK-----` }); + ecdhXKey = eddsaKey.subkeys[0]; + }); + + it('Ed448 params should be valid', async function() { + await expect(eddsaKey.keyPacket.validate()).to.not.be.rejected; + }); + + it('detect invalid Ed448 public point', async function() { + const eddsaKeyPacket = await cloneKeyPacket(eddsaKey); + const A = eddsaKeyPacket.publicParams.A; + A[0]++; + await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + + const infA = new Uint8Array(A.length); + eddsaKeyPacket.publicParams.A = infA; + await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + + it('X448 params should be valid', async function() { + await expect(ecdhXKey.keyPacket.validate()).to.not.be.rejected; + }); + + it('detect invalid x448 public point', async function() { + const ecdhXKeyPacket = await cloneKeyPacket(ecdhXKey); + const A = ecdhXKeyPacket.publicParams.A; + A[0]++; + await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + + const infA = new Uint8Array(A.length); + ecdhXKeyPacket.publicParams.A = infA; + await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); + }); + describe('RSA parameter validation', function() { let rsaKey; before(async () => { diff --git a/test/general/key.js b/test/general/key.js index 0a2fb66a..0e5dc5ae 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -3223,29 +3223,38 @@ aU71tdtNBQ== it('Parsing V4 key using curve448 format', async function() { const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- -xX0GZRqLYhwAAAA5U/IaIOge/FoLzCetXKx029bdJHCz2hMFBRMuzq4msjaT -+hLeV6puyC/PeSEfaanqTuo31vvsti2AAIttr4GDGXF4vfPzbzkWV9dT4VVs -IU7QqLv1hzwZ+k7pHroRyXnUiYxRYHuzlg7Vw4CrAtN/8T65OMLAHgYfHAoA -AAA9BQJlGotiIqEGAxidsHRHDsyFTw1Q7OoGEAEnRnxthKMwVBqhIL2o+HUC -GwMCHgkCCwcDFQoIAhYAAycHAgAAAAA2KiC+Y+fhQ/48CkT9WrXTX9SCn3vH -z43Wb++AkmpWL1HQmrJE3S4gGltezZK2E9ovagzxKxVrL14uC6hs6kJ0JIiW -QSeMeexCTy+Gdr6j0wb4FhFNnoIu3yu2ABmZpFX/5/191YeWUryKFDAoUZmK -gQTSOzJEvyO0ACR5L4vV3ADceOAdG8/sqhE89rTSevFXng4JAM0XVXNlckEg -PFVzZXJBQHRlc3QudGVzdD7CwA0GExwKAAAALAUCZRqLYiKhBgMYnbB0Rw7M -hU8NUOzqBhABJ0Z8bYSjMFQaoSC9qPh1AhkBAAAAAFw/IH72M1iyzMWhbgtw -v0SR/XxvOIW/ZrT4Ix9236lvoOE4taL/D46CbZOjm7VAeOSfSdxt1xSKnoAL -RsCNQ8tVPjPXclzqr6R8MbPIgBWxKcMS2eStYpBbG5qAmc+K5jdA2xcl9iW5 -bWleZ1LTah4lF6qCiD73IffADXtzw8iAMTX+0wM5N1tJUEGvgqe00ohRKiQA ------END PGP PRIVATE KEY BLOCK-----` }); +xX0GZRqLYhwAAAA52IEq/TpKiPp6RofQaq4uhCruTtiG+qiVFnwsQgeh0ui34kHD +Y1E04mBai0pCoDiFVokwsKt3F5sAAC8lDYfVP/p3atbXJDTJB2W9WmZxIS7pUGhS +bjlWpZB/OVTBsoIfP/2J+Hi4ESwBRfDUDgwK4aJVKsLAIAYfHAoAAAA/BQJlGoti +IqEGobsxt8WKsMuJWANyTXpWMdC1QN/7EyJClfcs+nBgqdUCGwMCHgkCCwcDFQoI +AhYABScHAwcCAAAAAPiGIG2qmhCULQ/+H4rKV0XEM1x0uVY3l878Pa6ijZLouZU/ +VRd5PnbGyLPL++q3LDViUUdZ1uusRc01f677Q6wpUU90k8MH/oULwI0+KPtqe1N4 +6nr1NTERsAmAaPjUdf4ZUXX/GWiTd/AlsS5JqGnAQxKRJkzCJacOTOElRMjzGUX7 +CGaAnhSC86YRZ68ocTPfZysAzRdVc2VyQiA8VXNlckJAdGVzdC50ZXN0PsLADQYT +HAoAAAAsBQJlGotiIqEGobsxt8WKsMuJWANyTXpWMdC1QN/7EyJClfcs+nBgqdUC +GQEAAAAASKwgVzMoPb2Hbr3lbNI1CRWECokYLokL7F8MbYiMnlg+v6QXLdStvT13 +ZjxdrWQAx3MbihSOUSXbdAys90yMOAdtognj+x418J/TaYFMtIGBHwoHv8gQVnx9 +9ICv8ezx1T5VvGBYNuKZ5Ww0WPEpYMf1VA+Y9JxpohdcRenNBdSug4tLWla2y8NH +aO28Fltpb4AuGQDHewZlGotiGgAAADjdabr1ohAOnbSUUkVhtUM/LVdnYgDLhmaj +YZ1N7TWY0fqEpMk2LLo2165HOmhddRPeTB1TWbuwBwB8lKc3czFUzYcAgvZ08T5S +UUHjfIhjeJeY4yd0OZDfzPw1vbegCc7t94bT+XGoIQbC/Bl7HCyAiMLADQYYHAoA +AAAsBQJlGotiIqEGobsxt8WKsMuJWANyTXpWMdC1QN/7EyJClfcs+nBgqdUCGwwA +AAAAHh0gf2kdqLoXdFX+aNVORr5VCVgcm2gBw8l68lDJ1ftA71bMllFi6Q5sLPUr +6UCpJYYk3o3hYUYzIHmCIZGVYe1pxRgIUNqxydXEfzylJmN5NbiJNkjJizrI7oAR +1mIcEEb/hmRMOUs1V2mcGuoeALBI/r/SyqDE2GRjH6d6g1RS7ZARPPHlZlY4CTqC +4a7L+8odDwA= +=chx0 +-----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.ed448); expect(signingKey.getAlgorithmInfo()).to.deep.equal({ algorithm: 'ed448' }); - // const encryptionKey = await privateKey.getEncryptionKey(); - // expect(encryptionKey.keyPacket.algorithm).to.equal(openpgp.enums.publicKey.x25519); - // expect(encryptionKey.getAlgorithmInfo()).to.deep.equal({ algorithm: 'x25519' }); + const encryptionKey = await privateKey.getEncryptionKey(); + expect(encryptionKey.keyPacket.algorithm).to.equal(openpgp.enums.publicKey.x448); + expect(encryptionKey.getAlgorithmInfo()).to.deep.equal({ algorithm: 'x448' }); }); it('Testing key ID and fingerprint for V4 keys', async function() { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index e159f30b..c84ccd54 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -2131,7 +2131,7 @@ mNwbfFbSNhZYWjFada77EKBn60j8QT/xCQzLR1clci7ieW2knw== expect(data).to.equal('Hello World!'); }); - it('supports encrypting new x25519 format', async function () { + it('supports encrypting/decrypting new x25519 format', async function () { // v4 key const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- @@ -2160,6 +2160,40 @@ yxsNL0GomZ+hxiE0MOZwRr10DxfVaRabF1fcf9PHSHX2SwEFXUKMIHgbMQs= expect(data).to.equal(plaintext); }); + it('supports encrypting/decrypting with x448', async function () { + // v4 key + const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xXsEZRqJ5BwHESfKnw5YJly5WobjigVm0kKY84NxrP6JKeIvIWiFqqSlozGpKZyR +50YbVTHmxpUCuJ7YNwX0UoAAoSO8IXmMM/XMd4ph00ju+fbSHdtQfyNhfFTi3UoM +V5DiFT+uOYDP+zwAwLWCR86csxmCWn6O10DNHcDNF1VzZXJBIDxVc2VyQUB0ZXN0 +LnRlc3Q+wroEExwKAD4FAmUaieQJEC8lwIrxSM+5FiEEGR2s5Bj5WVDN0Px6LyXA +ivFIz7kCGwMCHgkCGQECCwcDFQoIAhYAAycHAgAA21/PqAuGDL5+3qrf3YoVOP+5 +0BoJ+ZMhzcgax+cQTyndmOZYBfOqV/SJ8mf6CRhbB76JhGIvmRMtyYDQgDMVvcoA +yojVNs6e/Jco16bVJxM85wKDXJuq6AhtPQ8w/0WaCJtEf1uxqeQPEbOM+KtT/xY2 +KgDHeQRlGonkGuOtAhogSIU3z/+gFzF8U7JQe7QDRYr9VWfi2WXFFarzg/3DMRur +oIB7mqkaaSatrvVuud1ZmRCWAMM4f57dvSdCKsVqSe+tlS225OmdWmnGLqyErBb6 +44E2oENhDUom9OUGUPm8dXUjQbrmw6ec9hNLHWXCpgQYHAoAKgUCZRqJ5AkQLyXA +ivFIz7kWIQQZHazkGPlZUM3Q/HovJcCK8UjPuQIbDAAAZka10c8KlmwftJuboIV5 +DalGWrZhbywJpEZRfoikcebSYi5++w1SbpXZGu27sl+BznGyyyqAfxyJjoCZaqCs +ewbKh04DNAg4v4v0W0a8UvD3j/CuciEMXjK9nUErt91zEwxNZy43yrQY2aAayDs8 +94FqMAA= +=GBh1 +-----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----- @@ -4370,7 +4404,7 @@ kl0L message: await openpgp.createMessage({ text: plaintext }), encryptionKeys: privateKeyCast5, sessionKey: { data: new Uint8Array(16).fill(1), algorithm: 'cast5' } - })).to.be.rejectedWith(/X25519 keys can only encrypt AES session keys/); + })).to.be.rejectedWith(/X25519 and X448 keys can only encrypt AES session keys/); await expect(openpgp.decryptSessionKeys({ message: await openpgp.readMessage({ armoredMessage: `-----BEGIN PGP MESSAGE----- @@ -4383,6 +4417,48 @@ A7sB7uYCTVCLIMfPFwVZH+c29gpCzPxSXQ== })).to.be.rejectedWith(/AES session key expected/); }); + it('should enforce using AES session keys with x448 keys', async function () { + // X448 key (v4) with cast5 as preferred cipher + const privateKeyCast5 = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xXsEZRrtaRyScvyNjK0o5ccICztnWhA1MSij7WdzPfuNy7ryUzB+kqzpziBR +IIKp5PN0NW3mOYRDnUyo7QHBl4AA30tR5ED8u5v/rNIzKz/mKsD6XeYy+d0Q +5utwuR8BUxx9mcIUGdS65z9H6PUMGnfCwqAGVCTzBrSCHgTNAMK3BBAcCgA7 +BYJlGu1pAwsDBwmQkFi4G9HqQDwDFQgKAhYAAhkBApsDAh4BFiEE7kZAI1Dd +SVlLtf4QkFi4G9HqQDwAAPA7E+p0vwVLtUCfT0aBFzapFn8xjoow6jrUNTo3 +8EtaN0fqP2vaeQwW/vv26wobD+hbL2RwyFtAEV6AeeDsPVhbx7WA7yKHPzvl +GOYEGw0h57DuhvSxGciuyt0Y5PR2Vrz/2/wHGcEHzsrhTNysUetluxEAx3kE +ZRrtaRrySCLAqKQSATJOXdoRoNKVasJHlKrG3qgMbt1U6uSdctHBitTiHHTf +GU/Jg0ADA3Eg0bCyDupWNACmHJGu7q0o7O7BTAm0AsMbHxoIkNN9JsijwAp5 +FLtdXK9cAOkNaXPMkEGQkt1hmoW50lUq0iWcGBpzwqYEGBwKACoFgmUa7WkJ +kJBYuBvR6kA8ApsMFiEE7kZAI1DdSVlLtf4QkFi4G9HqQDwAAD3uf3qdwHY8 +65W22GR17PbqF+9uvkPpXLBi32FVPFkxJqYvIN5/LAQ33xdEE0mzO4As4+Oi +x8fsFb2AEXLEwlSnL+Eo0O+iUQd3/94yMbMFRlNxrdaqZ3+7CehbnieI/vby +LIEnN38XBi0HE70uoU5prxUA +-----END PGP PRIVATE KEY BLOCK-----` }); + + await expect(openpgp.generateSessionKey({ + encryptionKeys: privateKeyCast5, + config: { preferredSymmetricAlgorithm: openpgp.enums.symmetric.cast5 } + })).to.be.rejectedWith(/Could not generate a session key compatible with the given `encryptionKeys`/); + + await expect(openpgp.encrypt({ + message: await openpgp.createMessage({ text: plaintext }), + encryptionKeys: privateKeyCast5, + sessionKey: { data: new Uint8Array(16).fill(1), algorithm: 'cast5' } + })).to.be.rejectedWith(/X25519 and X448 keys can only encrypt AES session keys/); + + await expect(openpgp.decryptSessionKeys({ + message: await openpgp.readMessage({ armoredMessage: `-----BEGIN PGP MESSAGE----- + +wVwD2k7TUuqJwZkaXvEGk7B3pklJ5uRcRdKwwDJ40yKT0m5ic1e/2F+Se3xQ +zDE+N2DZ0B37pu4NUzTGBRo0oLD9EwwZA9+oJpBBOOry3cGmBYWvQHbvBpNE +5X5l8A== +-----END PGP MESSAGE-----` }), + decryptionKeys: privateKeyCast5 + })).to.be.rejectedWith(/AES session key expected/); + }); + describe('Sign and verify with each curve', function() { const curves = ['secp256k1' , 'p256', 'p384', 'p521', 'curve25519', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1']; curves.forEach(curve => {