Skip to content

Commit 8d4dd34

Browse files
authored
Merge pull request #1620
Add support for new Ed25519/X25519 keys, signatures and messages, as per crypto-refresh document.
2 parents b6170aa + 5ae2846 commit 8d4dd34

31 files changed

Lines changed: 978 additions & 239 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"prebrowsertest": "npm run build-test",
4646
"browsertest": "npm start -- -o test/unittests.html",
4747
"test-browser": "karma start test/karma.conf.js",
48-
"test-browserstack": "karma start test/karma.conf.js --browsers bs_safari_latest,bs_ios_15,bs_safari_13_1",
48+
"test-browserstack": "karma start test/karma.conf.js --browsers bs_safari_latest,bs_ios_14,bs_safari_13_1",
4949
"coverage": "nyc npm test",
5050
"lint": "eslint .",
5151
"docs": "jsdoc --configure .jsdocrc.js --destination docs --recurse README.md src && printf '%s' 'docs.openpgpjs.org' > docs/CNAME",

src/crypto/crypto.js

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,23 @@ import KDFParams from '../type/kdf_params';
3333
import enums from '../enums';
3434
import util from '../util';
3535
import OID from '../type/oid';
36-
import { Curve } from './public_key/elliptic/curves';
36+
import { CurveWithOID } from './public_key/elliptic/oid_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,17 @@ 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+
if (!util.isAES(symmetricAlgo)) {
71+
// see https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/276
72+
throw new Error('X25519 keys can only encrypt AES session keys');
73+
}
74+
const { A } = publicParams;
75+
const { ephemeralPublicKey, wrappedKey } = await publicKey.elliptic.ecdhX.encrypt(
76+
keyAlgo, data, A);
77+
const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey });
78+
return { ephemeralPublicKey, C };
79+
}
6780
default:
6881
return [];
6982
}
@@ -105,6 +118,16 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
105118
return publicKey.elliptic.ecdh.decrypt(
106119
oid, kdfParams, V, C.data, Q, d, fingerprint);
107120
}
121+
case enums.publicKey.x25519: {
122+
const { A } = publicKeyParams;
123+
const { k } = privateKeyParams;
124+
const { ephemeralPublicKey, C } = sessionKeyParams;
125+
if (!util.isAES(C.algorithm)) {
126+
throw new Error('AES session key expected');
127+
}
128+
return publicKey.elliptic.ecdhX.decrypt(
129+
algo, ephemeralPublicKey, C.wrappedKey, A, k);
130+
}
108131
default:
109132
throw new Error('Unknown public key encryption algorithm.');
110133
}
@@ -145,7 +168,8 @@ export function parsePublicKeyParams(algo, bytes) {
145168
const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2;
146169
return { read: read, publicParams: { oid, Q } };
147170
}
148-
case enums.publicKey.eddsa: {
171+
case enums.publicKey.eddsa:
172+
case enums.publicKey.ed25519Legacy: {
149173
const oid = new OID(); read += oid.read(bytes);
150174
checkSupportedCurve(oid);
151175
let Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2;
@@ -159,6 +183,11 @@ export function parsePublicKeyParams(algo, bytes) {
159183
const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read));
160184
return { read: read, publicParams: { oid, Q, kdfParams } };
161185
}
186+
case enums.publicKey.ed25519:
187+
case enums.publicKey.x25519: {
188+
const A = bytes.subarray(read, read + 32); read += A.length;
189+
return { read, publicParams: { A } };
190+
}
162191
default:
163192
throw new UnsupportedError('Unknown public key encryption algorithm.');
164193
}
@@ -190,17 +219,26 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
190219
}
191220
case enums.publicKey.ecdsa:
192221
case enums.publicKey.ecdh: {
193-
const curve = new Curve(publicParams.oid);
222+
const curve = new CurveWithOID(publicParams.oid);
194223
let d = util.readMPI(bytes.subarray(read)); read += d.length + 2;
195224
d = util.leftPad(d, curve.payloadSize);
196225
return { read, privateParams: { d } };
197226
}
198-
case enums.publicKey.eddsa: {
199-
const curve = new Curve(publicParams.oid);
227+
case enums.publicKey.eddsa:
228+
case enums.publicKey.ed25519Legacy: {
229+
const curve = new CurveWithOID(publicParams.oid);
200230
let seed = util.readMPI(bytes.subarray(read)); read += seed.length + 2;
201231
seed = util.leftPad(seed, curve.payloadSize);
202232
return { read, privateParams: { seed } };
203233
}
234+
case enums.publicKey.ed25519: {
235+
const seed = bytes.subarray(read, read + 32); read += seed.length;
236+
return { read, privateParams: { seed } };
237+
}
238+
case enums.publicKey.x25519: {
239+
const k = bytes.subarray(read, read + 32); read += k.length;
240+
return { read, privateParams: { k } };
241+
}
204242
default:
205243
throw new UnsupportedError('Unknown public key encryption algorithm.');
206244
}
@@ -238,6 +276,16 @@ export function parseEncSessionKeyParams(algo, bytes) {
238276
const C = new ECDHSymkey(); C.read(bytes.subarray(read));
239277
return { V, C };
240278
}
279+
// Algorithm-Specific Fields for X25519 encrypted session keys:
280+
// - 32 octets representing an ephemeral X25519 public key.
281+
// - A one-octet size of the following fields.
282+
// - The one-octet algorithm identifier, if it was passed (in the case of a v3 PKESK packet).
283+
// - The encrypted session key.
284+
case enums.publicKey.x25519: {
285+
const ephemeralPublicKey = bytes.subarray(read, read + 32); read += ephemeralPublicKey.length;
286+
const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read));
287+
return { ephemeralPublicKey, C };
288+
}
241289
default:
242290
throw new UnsupportedError('Unknown public key encryption algorithm.');
243291
}
@@ -250,9 +298,12 @@ export function parseEncSessionKeyParams(algo, bytes) {
250298
* @returns {Uint8Array} The array containing the MPIs.
251299
*/
252300
export function serializeParams(algo, params) {
301+
// Some algorithms do not rely on MPIs to store the binary params
302+
const algosWithNativeRepresentation = new Set([enums.publicKey.ed25519, enums.publicKey.x25519]);
253303
const orderedParams = Object.keys(params).map(name => {
254304
const param = params[name];
255-
return util.isUint8Array(param) ? util.uint8ArrayToMPI(param) : param.write();
305+
if (!util.isUint8Array(param)) return param.write();
306+
return algosWithNativeRepresentation.has(algo) ? param : util.uint8ArrayToMPI(param);
256307
});
257308
return util.concatUint8Array(orderedParams);
258309
}
@@ -281,6 +332,7 @@ export function generateParams(algo, bits, oid) {
281332
publicParams: { oid: new OID(oid), Q }
282333
}));
283334
case enums.publicKey.eddsa:
335+
case enums.publicKey.ed25519Legacy:
284336
return publicKey.elliptic.generate(oid).then(({ oid, Q, secret }) => ({
285337
privateParams: { seed: secret },
286338
publicParams: { oid: new OID(oid), Q }
@@ -294,6 +346,16 @@ export function generateParams(algo, bits, oid) {
294346
kdfParams: new KDFParams({ hash, cipher })
295347
}
296348
}));
349+
case enums.publicKey.ed25519:
350+
return publicKey.elliptic.eddsa.generate(algo).then(({ A, seed }) => ({
351+
privateParams: { seed },
352+
publicParams: { A }
353+
}));
354+
case enums.publicKey.x25519:
355+
return publicKey.elliptic.ecdhX.generate(algo).then(({ A, k }) => ({
356+
privateParams: { k },
357+
publicParams: { A }
358+
}));
297359
case enums.publicKey.dsa:
298360
case enums.publicKey.elgamal:
299361
throw new Error('Unsupported algorithm for key generation.');
@@ -339,10 +401,21 @@ export async function validateParams(algo, publicParams, privateParams) {
339401
const { d } = privateParams;
340402
return algoModule.validateParams(oid, Q, d);
341403
}
342-
case enums.publicKey.eddsa: {
343-
const { oid, Q } = publicParams;
404+
case enums.publicKey.eddsa:
405+
case enums.publicKey.ed25519Legacy: {
406+
const { Q, oid } = publicParams;
407+
const { seed } = privateParams;
408+
return publicKey.elliptic.eddsaLegacy.validateParams(oid, Q, seed);
409+
}
410+
case enums.publicKey.ed25519: {
411+
const { A } = publicParams;
344412
const { seed } = privateParams;
345-
return publicKey.elliptic.eddsa.validateParams(oid, Q, seed);
413+
return publicKey.elliptic.eddsa.validateParams(algo, A, seed);
414+
}
415+
case enums.publicKey.x25519: {
416+
const { A } = publicParams;
417+
const { k } = privateParams;
418+
return publicKey.elliptic.ecdhX.validateParams(algo, A, k);
346419
}
347420
default:
348421
throw new Error('Unknown public key algorithm.');

src/crypto/hkdf.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
const nodeSubtleCrypto = nodeCrypto && nodeCrypto.webcrypto && nodeCrypto.webcrypto.subtle;
13+
14+
export default async function HKDF(hashAlgo, inputKey, salt, info, outLen) {
15+
const hash = enums.read(enums.webHash, hashAlgo);
16+
if (!hash) throw new Error('Hash algo not supported with HKDF');
17+
18+
if (webCrypto || nodeSubtleCrypto) {
19+
const crypto = webCrypto || nodeSubtleCrypto;
20+
const importedKey = await crypto.importKey('raw', inputKey, 'HKDF', false, ['deriveBits']);
21+
const bits = await crypto.deriveBits({ name: 'HKDF', hash, salt, info }, importedKey, outLen * 8);
22+
return new Uint8Array(bits);
23+
}
24+
25+
if (nodeCrypto) {
26+
const hashAlgoName = enums.read(enums.hash, hashAlgo);
27+
// Node-only HKDF implementation based on https://www.rfc-editor.org/rfc/rfc5869
28+
29+
const computeHMAC = (hmacKey, hmacMessage) => nodeCrypto.createHmac(hashAlgoName, hmacKey).update(hmacMessage).digest();
30+
// Step 1: Extract
31+
// PRK = HMAC-Hash(salt, IKM)
32+
const pseudoRandomKey = computeHMAC(salt, inputKey);
33+
34+
const hashLen = pseudoRandomKey.length;
35+
36+
// Step 2: Expand
37+
// HKDF-Expand(PRK, info, L) -> OKM
38+
const n = Math.ceil(outLen / hashLen);
39+
const outputKeyingMaterial = new Uint8Array(n * hashLen);
40+
41+
// HMAC input buffer updated at each iteration
42+
const roundInput = new Uint8Array(hashLen + info.length + 1);
43+
// T_i and last byte are updated at each iteration, but `info` remains constant
44+
roundInput.set(info, hashLen);
45+
46+
for (let i = 0; i < n; i++) {
47+
// T(0) = empty string (zero length)
48+
// T(i) = HMAC-Hash(PRK, T(i-1) | info | i)
49+
roundInput[roundInput.length - 1] = i + 1;
50+
// t = T(i+1)
51+
const t = computeHMAC(pseudoRandomKey, i > 0 ? roundInput : roundInput.subarray(hashLen));
52+
roundInput.set(t, 0);
53+
54+
outputKeyingMaterial.set(t, i * hashLen);
55+
}
56+
57+
return outputKeyingMaterial.subarray(0, outLen);
58+
}
59+
60+
throw new Error('No HKDF implementation available');
61+
}

src/crypto/mode/cfb.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export async function encrypt(algo, key, plaintext, iv, config) {
5757
if (util.getNodeCrypto() && nodeAlgos[algoName]) { // Node crypto library.
5858
return nodeEncrypt(algo, key, plaintext, iv);
5959
}
60-
if (algoName.substr(0, 3) === 'aes') {
60+
if (util.isAES(algo)) {
6161
return aesEncrypt(algo, key, plaintext, iv, config);
6262
}
6363

@@ -100,7 +100,7 @@ export async function decrypt(algo, key, ciphertext, iv) {
100100
if (util.getNodeCrypto() && nodeAlgos[algoName]) { // Node crypto library.
101101
return nodeDecrypt(algo, key, ciphertext, iv);
102102
}
103-
if (algoName.substr(0, 3) === 'aes') {
103+
if (util.isAES(algo)) {
104104
return aesDecrypt(algo, key, ciphertext, iv);
105105
}
106106

src/crypto/public_key/elliptic/ecdh.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
*/
2323

2424
import nacl from '@openpgp/tweetnacl/nacl-fast-light';
25-
import { Curve, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams } from './curves';
25+
import { CurveWithOID, jwkToRawPublic, rawPublicToJWK, privateToJWK, validateStandardParams } from './oid_curves';
2626
import * as aesKW from '../../aes_kw';
2727
import { getRandomBytes } from '../../random';
2828
import hash from '../../hash';
@@ -86,7 +86,7 @@ async function kdf(hashAlgo, X, length, param, stripLeading = false, stripTraili
8686
/**
8787
* Generate ECDHE ephemeral key and secret from public key
8888
*
89-
* @param {Curve} curve - Elliptic curve object
89+
* @param {CurveWithOID} curve - Elliptic curve object
9090
* @param {Uint8Array} Q - Recipient public key
9191
* @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>}
9292
* @async
@@ -129,7 +129,7 @@ async function genPublicEphemeralKey(curve, Q) {
129129
export async function encrypt(oid, kdfParams, data, Q, fingerprint) {
130130
const m = pkcs5.encode(data);
131131

132-
const curve = new Curve(oid);
132+
const curve = new CurveWithOID(oid);
133133
const { publicKey, sharedKey } = await genPublicEphemeralKey(curve, Q);
134134
const param = buildEcdhParam(enums.publicKey.ecdh, oid, kdfParams, fingerprint);
135135
const { keySize } = getCipher(kdfParams.cipher);
@@ -141,7 +141,7 @@ export async function encrypt(oid, kdfParams, data, Q, fingerprint) {
141141
/**
142142
* Generate ECDHE secret from private key and public part of ephemeral key
143143
*
144-
* @param {Curve} curve - Elliptic curve object
144+
* @param {CurveWithOID} curve - Elliptic curve object
145145
* @param {Uint8Array} V - Public part of ephemeral key
146146
* @param {Uint8Array} Q - Recipient public key
147147
* @param {Uint8Array} d - Recipient private key
@@ -189,7 +189,7 @@ async function genPrivateEphemeralKey(curve, V, Q, d) {
189189
* @async
190190
*/
191191
export async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) {
192-
const curve = new Curve(oid);
192+
const curve = new CurveWithOID(oid);
193193
const { sharedKey } = await genPrivateEphemeralKey(curve, V, Q, d);
194194
const param = buildEcdhParam(enums.publicKey.ecdh, oid, kdfParams, fingerprint);
195195
const { keySize } = getCipher(kdfParams.cipher);
@@ -209,7 +209,7 @@ export async function decrypt(oid, kdfParams, V, C, Q, d, fingerprint) {
209209
/**
210210
* Generate ECDHE secret from private key and public part of ephemeral key using webCrypto
211211
*
212-
* @param {Curve} curve - Elliptic curve object
212+
* @param {CurveWithOID} curve - Elliptic curve object
213213
* @param {Uint8Array} V - Public part of ephemeral key
214214
* @param {Uint8Array} Q - Recipient public key
215215
* @param {Uint8Array} d - Recipient private key
@@ -262,7 +262,7 @@ async function webPrivateEphemeralKey(curve, V, Q, d) {
262262
/**
263263
* Generate ECDHE ephemeral key and secret from public key using webCrypto
264264
*
265-
* @param {Curve} curve - Elliptic curve object
265+
* @param {CurveWithOID} curve - Elliptic curve object
266266
* @param {Uint8Array} Q - Recipient public key
267267
* @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>}
268268
* @async
@@ -310,7 +310,7 @@ async function webPublicEphemeralKey(curve, Q) {
310310
/**
311311
* Generate ECDHE secret from private key and public part of ephemeral key using indutny/elliptic
312312
*
313-
* @param {Curve} curve - Elliptic curve object
313+
* @param {CurveWithOID} curve - Elliptic curve object
314314
* @param {Uint8Array} V - Public part of ephemeral key
315315
* @param {Uint8Array} d - Recipient private key
316316
* @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>}
@@ -330,7 +330,7 @@ async function ellipticPrivateEphemeralKey(curve, V, d) {
330330
/**
331331
* Generate ECDHE ephemeral key and secret from public key using indutny/elliptic
332332
*
333-
* @param {Curve} curve - Elliptic curve object
333+
* @param {CurveWithOID} curve - Elliptic curve object
334334
* @param {Uint8Array} Q - Recipient public key
335335
* @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>}
336336
* @async
@@ -350,7 +350,7 @@ async function ellipticPublicEphemeralKey(curve, Q) {
350350
/**
351351
* Generate ECDHE secret from private key and public part of ephemeral key using nodeCrypto
352352
*
353-
* @param {Curve} curve - Elliptic curve object
353+
* @param {CurveWithOID} curve - Elliptic curve object
354354
* @param {Uint8Array} V - Public part of ephemeral key
355355
* @param {Uint8Array} d - Recipient private key
356356
* @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>}
@@ -367,7 +367,7 @@ async function nodePrivateEphemeralKey(curve, V, d) {
367367
/**
368368
* Generate ECDHE ephemeral key and secret from public key using nodeCrypto
369369
*
370-
* @param {Curve} curve - Elliptic curve object
370+
* @param {CurveWithOID} curve - Elliptic curve object
371371
* @param {Uint8Array} Q - Recipient public key
372372
* @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>}
373373
* @async

0 commit comments

Comments
 (0)