Re-enable using WebCrypto for X25519 when available

Reverting commit ccb040ae96acd127a29161ffaf3b82b5b18c062f .
Firefox has fixed support in v132 (https://bugzilla.mozilla.org/show_bug.cgi?id=1918354)
usage of v130 and 131, which have a broken implementation, is now below 1%.

Also, Chrome has released support in v133.
This commit is contained in:
larabr 2025-02-24 14:45:42 +01:00
parent 6d4a86295e
commit d5689894f6
2 changed files with 100 additions and 20 deletions

View File

@ -11,6 +11,7 @@ import enums from '../../../enums';
import util from '../../../util'; import util from '../../../util';
import computeHKDF from '../../hkdf'; import computeHKDF from '../../hkdf';
import { getCipherParams } from '../../cipher'; import { getCipherParams } from '../../cipher';
import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64';
const HKDF_INFO = { const HKDF_INFO = {
x25519: util.encodeUTF8('OpenPGP X25519'), x25519: util.encodeUTF8('OpenPGP X25519'),
@ -24,7 +25,22 @@ const HKDF_INFO = {
*/ */
export async function generate(algo) { export async function generate(algo) {
switch (algo) { switch (algo) {
case enums.publicKey.x25519: { case enums.publicKey.x25519:
try {
const webCrypto = util.getWebCrypto();
const webCryptoKey = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']);
const privateKey = await webCrypto.exportKey('jwk', webCryptoKey.privateKey);
const publicKey = await webCrypto.exportKey('jwk', webCryptoKey.publicKey);
return {
A: new Uint8Array(b64ToUint8Array(publicKey.x)),
k: b64ToUint8Array(privateKey.d)
};
} catch (err) {
if (err.name !== 'NotSupportedError') {
throw err;
}
// k stays in little-endian, unlike legacy ECDH over curve25519 // k stays in little-endian, unlike legacy ECDH over curve25519
const k = getRandomBytes(32); const k = getRandomBytes(32);
const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k);
@ -171,7 +187,26 @@ export function getPayloadSize(algo) {
*/ */
export async function generateEphemeralEncryptionMaterial(algo, recipientA) { export async function generateEphemeralEncryptionMaterial(algo, recipientA) {
switch (algo) { switch (algo) {
case enums.publicKey.x25519: { case enums.publicKey.x25519:
try {
const webCrypto = util.getWebCrypto();
const jwk = publicKeyToJWK(algo, recipientA);
const ephemeralKeyPair = await webCrypto.generateKey('X25519', true, ['deriveKey', 'deriveBits']);
const recipientPublicKey = await webCrypto.importKey('jwk', jwk, 'X25519', false, []);
const sharedSecretBuffer = await webCrypto.deriveBits(
{ name: 'X25519', public: recipientPublicKey },
ephemeralKeyPair.privateKey,
getPayloadSize(algo) * 8 // in bits
);
const ephemeralPublicKeyJwt = await webCrypto.exportKey('jwk', ephemeralKeyPair.publicKey);
return {
sharedSecret: new Uint8Array(sharedSecretBuffer),
ephemeralPublicKey: new Uint8Array(b64ToUint8Array(ephemeralPublicKeyJwt.x))
};
} catch (err) {
if (err.name !== 'NotSupportedError') {
throw err;
}
const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo)); const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo));
const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA);
assertNonZeroArray(sharedSecret); assertNonZeroArray(sharedSecret);
@ -193,7 +228,23 @@ export async function generateEphemeralEncryptionMaterial(algo, recipientA) {
export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) { export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) {
switch (algo) { switch (algo) {
case enums.publicKey.x25519: { case enums.publicKey.x25519:
try {
const webCrypto = util.getWebCrypto();
const privateKeyJWK = privateKeyToJWK(algo, A, k);
const ephemeralPublicKeyJWK = publicKeyToJWK(algo, ephemeralPublicKey);
const privateKey = await webCrypto.importKey('jwk', privateKeyJWK, 'X25519', false, ['deriveKey', 'deriveBits']);
const ephemeralPublicKeyReference = await webCrypto.importKey('jwk', ephemeralPublicKeyJWK, 'X25519', false, []);
const sharedSecretBuffer = await webCrypto.deriveBits(
{ name: 'X25519', public: ephemeralPublicKeyReference },
privateKey,
getPayloadSize(algo) * 8 // in bits
);
return new Uint8Array(sharedSecretBuffer);
} catch (err) {
if (err.name !== 'NotSupportedError') {
throw err;
}
const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey); const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey);
assertNonZeroArray(sharedSecret); assertNonZeroArray(sharedSecret);
return sharedSecret; return sharedSecret;
@ -224,3 +275,32 @@ function assertNonZeroArray(sharedSecret) {
throw new Error('Unexpected low order point'); throw new Error('Unexpected low order point');
} }
} }
function publicKeyToJWK(algo, publicKey) {
switch (algo) {
case enums.publicKey.x25519: {
const jwk = {
kty: 'OKP',
crv: 'X25519',
x: uint8ArrayToB64(publicKey, true),
ext: true
};
return jwk;
}
default:
throw new Error('Unsupported ECDH algorithm');
}
}
function privateKeyToJWK(algo, publicKey, privateKey) {
switch (algo) {
case enums.publicKey.x25519: {
const jwk = publicKeyToJWK(algo, publicKey);
jwk.d = uint8ArrayToB64(privateKey, true);
return jwk;
}
default:
throw new Error('Unsupported ECDH algorithm');
}
}

View File

@ -234,9 +234,9 @@ export default () => describe('ECDH key exchange @lightweight', function () {
for (const { vector } of vectors) { for (const { vector } of vectors) {
const lowOrderPoint = util.hexToUint8Array(vector); const lowOrderPoint = util.hexToUint8Array(vector);
const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x25519); const { A: K_A, k: a } = await elliptic_curves.ecdhX.generate(openpgp.enums.publicKey.x25519);
await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x25519, data, lowOrderPoint)).to.be.rejectedWith(/low order point/); await expect(elliptic_curves.ecdhX.encrypt(openpgp.enums.publicKey.x25519, data, lowOrderPoint)).to.be.rejected; // OperationError, DataError or 'low order point', depending on platform
const dummyWrappedKey = new Uint8Array(32); // expected to be unused const dummyWrappedKey = new Uint8Array(32); // expected to be unused
await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x25519, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejectedWith(/low order point/); await expect(elliptic_curves.ecdhX.decrypt(openpgp.enums.publicKey.x25519, lowOrderPoint, dummyWrappedKey, K_A, a)).to.be.rejected; // OperationError, DataError or 'low order point', depending on platform
} }
}); });