Skip to content

Commit 1c07d26

Browse files
larabrlubux
andcommitted
crypto-refresh: add support for new X25519 key and PKESK format
As specified in openpgp-crypto-refresh-09. Instead of encoding the symmetric key algorithm in the PKESK ciphertext (requiring padding), the symmetric key algorithm is left unencrypted. Co-authored-by: Lukas Burkhalter <[email protected]>
1 parent 3f44082 commit 1c07d26

15 files changed

Lines changed: 531 additions & 167 deletions

File tree

src/crypto/crypto.js

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,21 @@ import util from '../util';
3535
import OID from '../type/oid';
3636
import { Curve } from './public_key/elliptic/curves';
3737
import { UnsupportedError } from '../packet/packet';
38+
import ECDHXSymmetricKey from '../type/ecdh_x_symkey';
3839

3940
/**
4041
* Encrypts data using specified algorithm and public key parameters.
4142
* See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms.
42-
* @param {module:enums.publicKey} algo - Public key algorithm
43+
* @param {module:enums.publicKey} keyAlgo - Public key algorithm
44+
* @param {module:enums.symmetric} symmetricAlgo - Cipher algorithm
4345
* @param {Object} publicParams - Algorithm-specific public key parameters
44-
* @param {Uint8Array} data - Data to be encrypted
46+
* @param {Uint8Array} data - Session key data to be encrypted
4547
* @param {Uint8Array} fingerprint - Recipient fingerprint
4648
* @returns {Promise<Object>} Encrypted session key parameters.
4749
* @async
4850
*/
49-
export async function publicKeyEncrypt(algo, publicParams, data, fingerprint) {
50-
switch (algo) {
51+
export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, data, fingerprint) {
52+
switch (keyAlgo) {
5153
case enums.publicKey.rsaEncrypt:
5254
case enums.publicKey.rsaEncryptSign: {
5355
const { n, e } = publicParams;
@@ -64,6 +66,14 @@ export async function publicKeyEncrypt(algo, publicParams, data, fingerprint) {
6466
oid, kdfParams, data, Q, fingerprint);
6567
return { V, C: new ECDHSymkey(C) };
6668
}
69+
case enums.publicKey.x25519: {
70+
const { A } = publicParams;
71+
const { ephemeralPublicKey, wrappedKey } = await publicKey.elliptic.ecdhX.encrypt(
72+
keyAlgo, data, A);
73+
const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey });
74+
return { ephemeralPublicKey, C };
75+
76+
}
6777
default:
6878
return [];
6979
}
@@ -105,6 +115,13 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
105115
return publicKey.elliptic.ecdh.decrypt(
106116
oid, kdfParams, V, C.data, Q, d, fingerprint);
107117
}
118+
case enums.publicKey.x25519: {
119+
const { A } = publicKeyParams;
120+
const { k } = privateKeyParams;
121+
const { ephemeralPublicKey, C } = sessionKeyParams;
122+
return publicKey.elliptic.ecdhX.decrypt(
123+
algo, ephemeralPublicKey, C.wrappedKey, A, k);
124+
}
108125
default:
109126
throw new Error('Unknown public key encryption algorithm.');
110127
}
@@ -160,7 +177,8 @@ export function parsePublicKeyParams(algo, bytes) {
160177
const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read));
161178
return { read: read, publicParams: { oid, Q, kdfParams } };
162179
}
163-
case enums.publicKey.ed25519: {
180+
case enums.publicKey.ed25519:
181+
case enums.publicKey.x25519: {
164182
const A = bytes.subarray(read, read + 32); read += A.length;
165183
return { read, publicParams: { A } };
166184
}
@@ -211,6 +229,10 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
211229
const seed = bytes.subarray(read, read + 32); read += seed.length;
212230
return { read, privateParams: { seed } };
213231
}
232+
case enums.publicKey.x25519: {
233+
const k = bytes.subarray(read, read + 32); read += k.length;
234+
return { read, privateParams: { k } };
235+
}
214236
default:
215237
throw new UnsupportedError('Unknown public key encryption algorithm.');
216238
}
@@ -248,6 +270,16 @@ export function parseEncSessionKeyParams(algo, bytes) {
248270
const C = new ECDHSymkey(); C.read(bytes.subarray(read));
249271
return { V, C };
250272
}
273+
// Algorithm-Specific Fields for X25519 encrypted session keys:
274+
// - 32 octets representing an ephemeral X25519 public key.
275+
// - A one-octet size of the following fields.
276+
// - The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet).
277+
// - The encrypted session key.
278+
case enums.publicKey.x25519: {
279+
const ephemeralPublicKey = bytes.subarray(read, read + 32); read += ephemeralPublicKey.length;
280+
const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read));
281+
return { ephemeralPublicKey, C };
282+
}
251283
default:
252284
throw new UnsupportedError('Unknown public key encryption algorithm.');
253285
}
@@ -261,7 +293,7 @@ export function parseEncSessionKeyParams(algo, bytes) {
261293
*/
262294
export function serializeParams(algo, params) {
263295
// Some algorithms do not rely on MPIs to store the binary params
264-
const algosWithNativeRepresentation = new Set([enums.publicKey.ed25519]);
296+
const algosWithNativeRepresentation = new Set([enums.publicKey.ed25519, enums.publicKey.x25519]);
265297
const orderedParams = Object.keys(params).map(name => {
266298
const param = params[name];
267299
if (!util.isUint8Array(param)) return param.write();
@@ -313,6 +345,11 @@ export function generateParams(algo, bits, oid) {
313345
privateParams: { seed },
314346
publicParams: { A }
315347
}));
348+
case enums.publicKey.x25519:
349+
return publicKey.elliptic.ecdhX.generate(algo).then(({ A, k }) => ({
350+
privateParams: { k },
351+
publicParams: { A }
352+
}));
316353
case enums.publicKey.dsa:
317354
case enums.publicKey.elgamal:
318355
throw new Error('Unsupported algorithm for key generation.');
@@ -369,6 +406,11 @@ export async function validateParams(algo, publicParams, privateParams) {
369406
const { seed } = privateParams;
370407
return publicKey.elliptic.eddsa.validateParams(algo, A, seed);
371408
}
409+
case enums.publicKey.x25519: {
410+
const { A } = publicParams;
411+
const { k } = privateParams;
412+
return publicKey.elliptic.ecdhX.validateParams(algo, A, k);
413+
}
372414
default:
373415
throw new Error('Unknown public key algorithm.');
374416
}

src/crypto/hkdf.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @fileoverview This module implements HKDF using either the WebCrypto API or Node.js' crypto API.
3+
* @module crypto/hkdf
4+
* @private
5+
*/
6+
7+
import enums from '../enums';
8+
import util from '../util';
9+
10+
const webCrypto = util.getWebCrypto();
11+
const nodeCrypto = util.getNodeCrypto();
12+
13+
export default async function HKDF(hashAlgo, key, salt, info, length) {
14+
const hash = enums.read(enums.webHash, hashAlgo);
15+
if (!hash) throw new Error('Hash algo not supported with HKDF');
16+
17+
const crypto = webCrypto || nodeCrypto.webcrypto.subtle;
18+
const importedKey = await crypto.importKey('raw', key, 'HKDF', false, ['deriveBits']);
19+
const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, length * 8);
20+
return new Uint8Array(bits);
21+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* @fileoverview Key encryption and decryption for RFC 6637 ECDH
3+
* @module crypto/public_key/elliptic/ecdh
4+
* @private
5+
*/
6+
7+
import nacl from '@openpgp/tweetnacl/nacl-fast-light';
8+
import * as aesKW from '../../aes_kw';
9+
import { getRandomBytes } from '../../random';
10+
11+
import enums from '../../../enums';
12+
import util from '../../../util';
13+
import getCipher from '../../cipher/getCipher';
14+
import computeHKDF from '../../hkdf';
15+
16+
const HKDF_INFO = {
17+
x25519: util.encodeUTF8('OpenPGP X25519')
18+
};
19+
20+
/**
21+
* Generate ECDH key for Montgomery curves
22+
* @param {module:enums.publicKey} algo - Algorithm identifier
23+
* @returns Promise<{ A, k }>
24+
*/
25+
export async function generate(algo) {
26+
switch (algo) {
27+
case enums.publicKey.x25519: {
28+
// k stays in little-endian, unlike legacy ECDH over curve25519
29+
const k = getRandomBytes(32);
30+
k[0] &= 248;
31+
k[31] = (k[31] & 127) | 64;
32+
const { publicKey: A } = nacl.box.keyPair.fromSecretKey(k);
33+
return { A, k };
34+
}
35+
default:
36+
throw new Error('Unsupported ECDH algorithm');
37+
}
38+
}
39+
40+
/**
41+
* Validate ECDH parameters
42+
* @param {module:enums.publicKey} algo - Algorithm identifier
43+
* @param {Uint8Array} A - ECDH public point
44+
* @param {Uint8Array} k - ECDH secret scalar
45+
* @returns {Promise<Boolean>} Whether params are valid.
46+
* @async
47+
*/
48+
export async function validateParams(algo, A, k) {
49+
switch (algo) {
50+
case enums.publicKey.x25519: {
51+
/**
52+
* Derive public point A' from private key
53+
* and expect A == A'
54+
*/
55+
const { publicKey } = nacl.box.keyPair.fromSecretKey(k);
56+
return util.equalsUint8Array(A, publicKey);
57+
}
58+
59+
default:
60+
return false;
61+
}
62+
}
63+
64+
/**
65+
* Wrap and encrypt a session key
66+
*
67+
* @param {module:enums.publicKey} algo - Algorithm identifier
68+
* @param {Uint8Array} data - session key data to be encrypted
69+
* @param {Uint8Array} recipientA - Recipient public key (K_B)
70+
* @returns {Promise<{
71+
* ephemeralPublicKey: Uint8Array,
72+
* wrappedKey: Uint8Array
73+
* }>} ephemeral public key (K_A) and encrypted key
74+
* @async
75+
*/
76+
export async function encrypt(algo, data, recipientA) {
77+
switch (algo) {
78+
case enums.publicKey.x25519: {
79+
const ephemeralSecretKey = getRandomBytes(32);
80+
const sharedSecret = nacl.scalarMult(ephemeralSecretKey, recipientA);
81+
const { publicKey: ephemeralPublicKey } = nacl.box.keyPair.fromSecretKey(ephemeralSecretKey);
82+
const hkdfInput = util.concatUint8Array([
83+
ephemeralPublicKey,
84+
recipientA,
85+
sharedSecret
86+
]);
87+
const { keySize } = getCipher(enums.symmetric.aes128);
88+
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
89+
const wrappedKey = aesKW.wrap(encryptionKey, data);
90+
return { ephemeralPublicKey, wrappedKey };
91+
}
92+
93+
default:
94+
throw new Error('Unsupported ECDH algorithm');
95+
}
96+
}
97+
98+
/**
99+
* Decrypt and unwrap the session key
100+
*
101+
* @param {module:enums.publicKey} algo - Algorithm identifier
102+
* @param {Uint8Array} ephemeralPublicKey - (K_A)
103+
* @param {Uint8Array} wrappedKey,
104+
* @param {Uint8Array} A - Recipient public key (K_b), needed for KDF
105+
* @param {Uint8Array} k - Recipient secret key (b)
106+
* @returns {Promise<Uint8Array>} decrypted session key data
107+
* @async
108+
*/
109+
export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) {
110+
switch (algo) {
111+
case enums.publicKey.x25519: {
112+
const sharedSecret = nacl.scalarMult(k, ephemeralPublicKey);
113+
const hkdfInput = util.concatUint8Array([
114+
ephemeralPublicKey,
115+
A,
116+
sharedSecret
117+
]);
118+
const { keySize } = getCipher(enums.symmetric.aes128);
119+
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
120+
return aesKW.unwrap(encryptionKey, wrappedKey);
121+
}
122+
default:
123+
throw new Error('Unsupported ECDH algorithm');
124+
}
125+
}

src/crypto/public_key/elliptic/eddsa.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,19 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) {
104104
* Validate (non-legacy) EdDSA parameters
105105
* @param {module:enums.publicKey} algo - Algorithm identifier
106106
* @param {Uint8Array} A - EdDSA public point
107-
* @param {Uint8Array} k - EdDSA secret seed
107+
* @param {Uint8Array} seed - EdDSA secret seed
108108
* @param {Uint8Array} oid - (legacy only) EdDSA OID
109109
* @returns {Promise<Boolean>} Whether params are valid.
110110
* @async
111111
*/
112-
export async function validateParams(algo, A, k) {
112+
export async function validateParams(algo, A, seed) {
113113
switch (algo) {
114114
case enums.publicKey.ed25519: {
115115
/**
116116
* Derive public point A' from private key
117117
* and expect A == A'
118118
*/
119-
const { publicKey } = nacl.sign.keyPair.fromSeed(k);
119+
const { publicKey } = nacl.sign.keyPair.fromSeed(seed);
120120
return util.equalsUint8Array(A, publicKey);
121121
}
122122

src/crypto/public_key/elliptic/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import * as ecdsa from './ecdsa';
3030
import * as eddsaLegacy from './eddsa_legacy';
3131
import * as eddsa from './eddsa';
3232
import * as ecdh from './ecdh';
33+
import * as ecdhX from './ecdh_x';
3334

3435
export {
35-
Curve, ecdh, ecdsa, eddsaLegacy, eddsa, generate, getPreferredHashAlgo
36+
Curve, ecdh, ecdhX, ecdsa, eddsaLegacy, eddsa, generate, getPreferredHashAlgo
3637
};

src/enums.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@ export default {
117117
aedh: 23,
118118
/** Reserved for AEDSA */
119119
aedsa: 24,
120-
/** ECDH 25519 (encrypt only) */
120+
/** X25519 (Encrypt only) */
121121
x25519: 25,
122-
/** ECDH 448 (encrypt only) */
122+
/** X448 (Encrypt only) */
123123
x448: 26,
124-
/** EdDSA 25519 (sign only) */
124+
/** Ed25519 (Sign only) */
125125
ed25519: 27,
126-
/** EdDSA 448 (sign only) */
127-
eddsa448: 28
126+
/** Ed448 (Sign only) */
127+
ed448: 28
128128
},
129129

130130
/** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2}

0 commit comments

Comments
 (0)