Skip to content

Commit cc7f5ea

Browse files
committed
crypto: wire ChaCha20-Poly1305 in Web Cryptography when using BoringSSL
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 3e17845 commit cc7f5ea

10 files changed

Lines changed: 115 additions & 78 deletions

lib/internal/crypto/util.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ const conditionalAlgorithms = {
400400
'Argon2d': !!Argon2Job,
401401
'Argon2i': !!Argon2Job,
402402
'Argon2id': !!Argon2Job,
403-
'ChaCha20-Poly1305': !process.features.openssl_is_boringssl ||
403+
'ChaCha20-Poly1305': process.features.openssl_is_boringssl ||
404404
ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'),
405405
'cSHAKE128': !process.features.openssl_is_boringssl ||
406406
ArrayPrototypeIncludes(getHashes(), 'shake128'),

src/crypto/crypto_chacha20_poly1305.cc

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
#include "v8.h"
1111

1212
#include <openssl/evp.h>
13+
#ifdef OPENSSL_IS_BORINGSSL
14+
#include <openssl/aead.h>
15+
#endif
1316

1417
namespace node {
1518

@@ -110,10 +113,15 @@ Maybe<void> ChaCha20Poly1305CipherTraits::AdditionalConfig(
110113
params->mode = mode;
111114
params->cipher = ncrypto::Cipher::CHACHA20_POLY1305;
112115

116+
#ifndef OPENSSL_IS_BORINGSSL
117+
// On BoringSSL, ChaCha20-Poly1305 is not exposed via the EVP_CIPHER registry
118+
// so FromNid() returns a null Cipher. We use EVP_AEAD directly in DoCipher
119+
// instead.
113120
if (!params->cipher) {
114121
THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env);
115122
return Nothing<void>();
116123
}
124+
#endif
117125

118126
// IV parameter (required)
119127
if (!ValidateIV(env, mode, args[offset], params)) {
@@ -144,6 +152,75 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher(
144152
return WebCryptoCipherStatus::INVALID_KEY_TYPE;
145153
}
146154

155+
#ifdef OPENSSL_IS_BORINGSSL
156+
// BoringSSL does not expose ChaCha20-Poly1305 via the EVP_CIPHER registry;
157+
// it is only available through the EVP_AEAD API. Matches Chromium's
158+
// WebCrypto ChaCha20-Poly1305 implementation.
159+
const auto key_bytes =
160+
reinterpret_cast<const unsigned char*>(key_data.GetSymmetricKey());
161+
const auto ad_bytes = params.additional_data.data<unsigned char>();
162+
const auto ad_len = params.additional_data.size();
163+
const auto iv_bytes = params.iv.data<unsigned char>();
164+
const auto iv_len = params.iv.size();
165+
166+
bssl::ScopedEVP_AEAD_CTX ctx;
167+
if (!EVP_AEAD_CTX_init(ctx.get(),
168+
EVP_aead_chacha20_poly1305(),
169+
key_bytes,
170+
key_data.GetSymmetricKeySize(),
171+
kChaCha20Poly1305TagSize,
172+
nullptr)) {
173+
return WebCryptoCipherStatus::FAILED;
174+
}
175+
176+
if (cipher_mode == kWebCryptoCipherEncrypt) {
177+
size_t out_len = 0;
178+
const size_t max_out_len = in.size() + kChaCha20Poly1305TagSize;
179+
auto buf = DataPointer::Alloc(max_out_len);
180+
if (!EVP_AEAD_CTX_seal(ctx.get(),
181+
static_cast<unsigned char*>(buf.get()),
182+
&out_len,
183+
max_out_len,
184+
iv_bytes,
185+
iv_len,
186+
in.data<unsigned char>(),
187+
in.size(),
188+
ad_bytes,
189+
ad_len)) {
190+
return WebCryptoCipherStatus::FAILED;
191+
}
192+
buf = buf.resize(out_len);
193+
*out = ByteSource::Allocated(buf.release());
194+
return WebCryptoCipherStatus::OK;
195+
}
196+
197+
// Decrypt
198+
if (in.size() < kChaCha20Poly1305TagSize) {
199+
return WebCryptoCipherStatus::FAILED;
200+
}
201+
size_t out_len = 0;
202+
const size_t max_out_len = in.size(); // at most |in_len| bytes written
203+
auto buf = DataPointer::Alloc(max_out_len == 0 ? 1 : max_out_len);
204+
if (!EVP_AEAD_CTX_open(ctx.get(),
205+
static_cast<unsigned char*>(buf.get()),
206+
&out_len,
207+
max_out_len,
208+
iv_bytes,
209+
iv_len,
210+
in.data<unsigned char>(),
211+
in.size(),
212+
ad_bytes,
213+
ad_len)) {
214+
return WebCryptoCipherStatus::FAILED;
215+
}
216+
if (out_len == 0) {
217+
*out = ByteSource();
218+
} else {
219+
buf = buf.resize(out_len);
220+
*out = ByteSource::Allocated(buf.release());
221+
}
222+
return WebCryptoCipherStatus::OK;
223+
#else
147224
auto ctx = CipherCtxPointer::New();
148225
CHECK(ctx);
149226

@@ -242,6 +319,7 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher(
242319
*out = ByteSource::Allocated(buf.release());
243320

244321
return WebCryptoCipherStatus::OK;
322+
#endif // OPENSSL_IS_BORINGSSL
245323
}
246324

247325
void ChaCha20Poly1305::Initialize(Environment* env, Local<Object> target) {

test/fixtures/webcrypto/supports-modern-algorithms.mjs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ const pqc = hasOpenSSL(3, 5);
66
const argon2 = hasOpenSSL(3, 2);
77
const shake128 = crypto.getHashes().includes('shake128');
88
const shake256 = crypto.getHashes().includes('shake256');
9-
const chacha = crypto.getCiphers().includes('chacha20-poly1305');
109
const ocb = hasOpenSSL(3);
1110
const kmac = hasOpenSSL(3);
1211
const boringSSL = process.features.openssl_is_boringssl;
@@ -78,7 +77,7 @@ export const vectors = {
7877
[pqc, 'ML-KEM-512'],
7978
[pqc, 'ML-KEM-768'],
8079
[pqc, 'ML-KEM-1024'],
81-
[chacha, 'ChaCha20-Poly1305'],
80+
[true, 'ChaCha20-Poly1305'],
8281
[ocb, { name: 'AES-OCB', length: 128 }],
8382
[false, 'Argon2d'],
8483
[false, 'Argon2i'],
@@ -99,7 +98,7 @@ export const vectors = {
9998
[pqc, 'ML-KEM-512'],
10099
[pqc, 'ML-KEM-768'],
101100
[pqc, 'ML-KEM-1024'],
102-
[chacha, 'ChaCha20-Poly1305'],
101+
[true, 'ChaCha20-Poly1305'],
103102
[ocb, { name: 'AES-OCB', length: 128 }],
104103
[argon2, 'Argon2d'],
105104
[argon2, 'Argon2i'],
@@ -120,7 +119,7 @@ export const vectors = {
120119
[pqc, 'ML-KEM-512'],
121120
[pqc, 'ML-KEM-768'],
122121
[pqc, 'ML-KEM-1024'],
123-
[chacha, 'ChaCha20-Poly1305'],
122+
[true, 'ChaCha20-Poly1305'],
124123
[ocb, 'AES-OCB'],
125124
[false, 'Argon2d'],
126125
[false, 'Argon2i'],
@@ -186,9 +185,9 @@ export const vectors = {
186185
[false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 16777215, memory: 8, passes: 1 }, 32],
187186
],
188187
'encrypt': [
189-
[chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }],
188+
[true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }],
190189
[false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }],
191-
[chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }],
190+
[true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }],
192191
[false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }],
193192
[false, 'ChaCha20-Poly1305'],
194193
[ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }],

test/parallel/test-crypto-key-objects-to-crypto-key.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,10 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) {
2626
{
2727
for (const length of [128, 192, 256]) {
2828
const key = createSecretKey(randomBytes(length >> 3));
29-
let algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW'];
29+
const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW'];
3030
if (length === 256)
3131
algorithms.push('ChaCha20-Poly1305');
3232

33-
if (process.features.openssl_is_boringssl) {
34-
algorithms = algorithms.filter((a) => a !== 'ChaCha20-Poly1305');
35-
common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case');
36-
}
37-
3833
for (const algorithm of algorithms) {
3934
const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt'];
4035
for (const extractable of [true, false]) {

test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,11 @@ async function test(algorithmName, keyLength, ivLength, format = 'raw') {
2929

3030
const tests = [
3131
test('AES-GCM', 32, 12),
32+
test('ChaCha20-Poly1305', 32, 12, 'raw-secret'),
3233
];
3334

3435
if (hasOpenSSL(3)) {
3536
tests.push(test('AES-OCB', 32, 12, 'raw-secret'));
3637
}
3738

38-
if (!process.features.openssl_is_boringssl) {
39-
tests.push(test('ChaCha20-Poly1305', 32, 12, 'raw-secret'));
40-
}
41-
4239
Promise.all(tests).then(common.mustCall());

test/parallel/test-webcrypto-deduplicate-usages.js

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ function assertSameSet(actual, expected, msg) {
4545
{ algorithm: { name: 'AES-KW', length: 128 },
4646
usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'],
4747
expected: ['wrapKey', 'unwrapKey'] },
48+
{ algorithm: { name: 'ChaCha20-Poly1305' },
49+
usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'],
50+
expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] },
4851
];
4952

5053
if (hasOpenSSL(3)) {
@@ -62,16 +65,6 @@ function assertSameSet(actual, expected, msg) {
6265
common.printSkipMessage('AES-OCB and KMAC require OpenSSL >= 3');
6366
}
6467

65-
if (!process.features.openssl_is_boringssl) {
66-
symmetric.push({
67-
algorithm: { name: 'ChaCha20-Poly1305' },
68-
usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'],
69-
expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
70-
});
71-
} else {
72-
common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL');
73-
}
74-
7568
for (const { algorithm, usages, expected } of symmetric) {
7669
tests.push((async () => {
7770
const key = await subtle.generateKey(algorithm, true, usages);
@@ -342,20 +335,16 @@ function assertSameSet(actual, expected, msg) {
342335
})());
343336

344337
// ChaCha20-Poly1305 raw-secret import.
345-
if (!process.features.openssl_is_boringssl) {
346-
tests.push((async () => {
347-
const key = await subtle.importKey(
348-
'raw-secret',
349-
new Uint8Array(32),
350-
{ name: 'ChaCha20-Poly1305' },
351-
true,
352-
['decrypt', 'encrypt', 'decrypt', 'encrypt']);
353-
assertSameSet(key.usages, ['encrypt', 'decrypt']);
354-
assert.strictEqual(key.usages.length, 2);
355-
})());
356-
} else {
357-
common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL');
358-
}
338+
tests.push((async () => {
339+
const key = await subtle.importKey(
340+
'raw-secret',
341+
new Uint8Array(32),
342+
{ name: 'ChaCha20-Poly1305' },
343+
true,
344+
['decrypt', 'encrypt', 'decrypt', 'encrypt']);
345+
assertSameSet(key.usages, ['encrypt', 'decrypt']);
346+
assert.strictEqual(key.usages.length, 2);
347+
})());
359348

360349
// AES-OCB raw-secret import.
361350
if (hasOpenSSL(3)) {

test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ const common = require('../common');
55
if (!common.hasCrypto)
66
common.skip('missing crypto');
77

8-
if (process.features.openssl_is_boringssl)
9-
common.skip('Skipping unsupported ChaCha20-Poly1305 test case');
10-
118
const assert = require('assert');
129
const { subtle } = globalThis.crypto;
1310

test/parallel/test-webcrypto-keygen.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,16 @@ const vectors = {
142142
'wrapKey',
143143
'unwrapKey',
144144
],
145-
}
145+
},
146+
'ChaCha20-Poly1305': {
147+
result: 'CryptoKey',
148+
usages: [
149+
'encrypt',
150+
'decrypt',
151+
'wrapKey',
152+
'unwrapKey',
153+
],
154+
},
146155
};
147156

148157
if (!process.features.openssl_is_boringssl) {
@@ -160,15 +169,6 @@ if (!process.features.openssl_is_boringssl) {
160169
'deriveBits',
161170
],
162171
};
163-
vectors['ChaCha20-Poly1305'] = {
164-
result: 'CryptoKey',
165-
usages: [
166-
'encrypt',
167-
'decrypt',
168-
'wrapKey',
169-
'unwrapKey',
170-
],
171-
};
172172
} else {
173173
common.printSkipMessage('Skipping unsupported test cases');
174174
}

test/parallel/test-webcrypto-wrap-unwrap.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,15 @@ const kWrappingData = {
4444
wrap: { },
4545
pair: false
4646
},
47-
};
48-
49-
50-
if (!process.features.openssl_is_boringssl) {
51-
kWrappingData['ChaCha20-Poly1305'] = {
47+
'ChaCha20-Poly1305': {
5248
wrap: {
5349
iv: new Uint8Array(12),
5450
additionalData: new Uint8Array(16),
5551
tagLength: 128
5652
},
5753
pair: false
58-
};
59-
} else {
60-
common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case');
61-
}
54+
}
55+
};
6256

6357
if (hasOpenSSL(3)) {
6458
kWrappingData['AES-OCB'] = {
@@ -197,19 +191,14 @@ async function generateKeysToWrap() {
197191
usages: ['wrapKey', 'unwrapKey'],
198192
pair: false,
199193
},
200-
];
201-
202-
if (!process.features.openssl_is_boringssl) {
203-
parameters.push({
194+
{
204195
algorithm: {
205196
name: 'ChaCha20-Poly1305'
206197
},
207198
usages: ['encrypt', 'decrypt'],
208199
pair: false,
209-
});
210-
} else {
211-
common.printSkipMessage('Skipping unsupported ChaCha20-Poly1305 test case');
212-
}
200+
},
201+
];
213202

214203
if (hasOpenSSL(3, 5)) {
215204
for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) {

test/wpt/status/WebCryptoAPI.cjs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,16 @@ if (process.features.openssl_is_boringssl) {
6767
'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js',
6868
'digest/cshake.tentative.https.any.js',
6969
'digest/sha3.tentative.https.any.js',
70-
'encrypt_decrypt/chacha20_poly1305.tentative.https.any.js',
7170
'generateKey/failures_Ed448.tentative.https.any.js',
7271
'generateKey/failures_X448.tentative.https.any.js',
73-
'generateKey/failures_chacha20_poly1305.tentative.https.any.js',
7472
'generateKey/successes_Ed448.tentative.https.any.js',
7573
'generateKey/successes_X448.tentative.https.any.js',
76-
'generateKey/successes_chacha20_poly1305.tentative.https.any.js',
77-
'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js',
7874
'import_export/okp_importKey_Ed448.tentative.https.any.js',
7975
'import_export/okp_importKey_failures_Ed448.tentative.https.any.js',
8076
'import_export/okp_importKey_failures_X448.tentative.https.any.js',
8177
'import_export/okp_importKey_X448.tentative.https.any.js',
8278
'sign_verify/eddsa_curve448.tentative.https.any.js');
8379

84-
skipSubtests(
85-
['supports-modern.tentative.https.any.js', /ChaCha20-Poly1305/],
86-
['supports-modern.tentative.https.any.js', /^supports returns true for algorithm objects with valid parameters$/]);
8780
}
8881

8982
function assertNoOverlap(fileSkips, subtestSkips) {

0 commit comments

Comments
 (0)