Skip to content

Commit 3f47a2f

Browse files
panvatargos
authored andcommitted
crypto: add ChaCha20-Poly1305 Web Cryptography algorithm
PR-URL: #59365 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent 16afd10 commit 3f47a2f

25 files changed

+1239
-176
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,7 @@ const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm);
29272927
const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap);
29282928
const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap);
29292929
const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap);
2930+
const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305);
29302931

29312932
bool Cipher::isGcmMode() const {
29322933
if (!cipher_) return false;

deps/ncrypto/ncrypto.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ class Cipher final {
373373
static const Cipher AES_128_KW;
374374
static const Cipher AES_192_KW;
375375
static const Cipher AES_256_KW;
376+
static const Cipher CHACHA20_POLY1305;
376377

377378
struct CipherParams {
378379
int padding;

doc/api/webcrypto.md

Lines changed: 175 additions & 146 deletions
Large diffs are not rendered by default.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
'use strict';
2+
3+
const {
4+
ArrayBufferIsView,
5+
ArrayBufferPrototypeSlice,
6+
ArrayFrom,
7+
PromiseReject,
8+
SafeSet,
9+
TypedArrayPrototypeSlice,
10+
} = primordials;
11+
12+
const {
13+
ChaCha20Poly1305CipherJob,
14+
KeyObjectHandle,
15+
kCryptoJobAsync,
16+
kWebCryptoCipherDecrypt,
17+
kWebCryptoCipherEncrypt,
18+
} = internalBinding('crypto');
19+
20+
const {
21+
hasAnyNotIn,
22+
jobPromise,
23+
validateKeyOps,
24+
kHandle,
25+
kKeyObject,
26+
} = require('internal/crypto/util');
27+
28+
const {
29+
lazyDOMException,
30+
promisify,
31+
} = require('internal/util');
32+
33+
const {
34+
InternalCryptoKey,
35+
SecretKeyObject,
36+
createSecretKey,
37+
} = require('internal/crypto/keys');
38+
39+
const {
40+
randomBytes: _randomBytes,
41+
} = require('internal/crypto/random');
42+
43+
const randomBytes = promisify(_randomBytes);
44+
45+
function validateKeyLength(length) {
46+
if (length !== 256)
47+
throw lazyDOMException('Invalid key length', 'DataError');
48+
}
49+
50+
function c20pCipher(mode, key, data, algorithm) {
51+
let tag;
52+
switch (mode) {
53+
case kWebCryptoCipherDecrypt: {
54+
const slice = ArrayBufferIsView(data) ?
55+
TypedArrayPrototypeSlice : ArrayBufferPrototypeSlice;
56+
57+
if (data.byteLength < 16) {
58+
return PromiseReject(lazyDOMException(
59+
'The provided data is too small.',
60+
'OperationError'));
61+
}
62+
63+
tag = slice(data, -16);
64+
data = slice(data, 0, -16);
65+
break;
66+
}
67+
case kWebCryptoCipherEncrypt:
68+
tag = 16;
69+
break;
70+
}
71+
72+
return jobPromise(() => new ChaCha20Poly1305CipherJob(
73+
kCryptoJobAsync,
74+
mode,
75+
key[kKeyObject][kHandle],
76+
data,
77+
algorithm.iv,
78+
tag,
79+
algorithm.additionalData));
80+
}
81+
82+
async function c20pGenerateKey(algorithm, extractable, keyUsages) {
83+
const { name } = algorithm;
84+
85+
const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'];
86+
87+
const usagesSet = new SafeSet(keyUsages);
88+
if (hasAnyNotIn(usagesSet, checkUsages)) {
89+
throw lazyDOMException(
90+
`Unsupported key usage for a ${algorithm.name} key`,
91+
'SyntaxError');
92+
}
93+
94+
const keyData = await randomBytes(32).catch((err) => {
95+
throw lazyDOMException(
96+
'The operation failed for an operation-specific reason' +
97+
`[${err.message}]`,
98+
{ name: 'OperationError', cause: err });
99+
});
100+
101+
return new InternalCryptoKey(
102+
createSecretKey(keyData),
103+
{ name },
104+
ArrayFrom(usagesSet),
105+
extractable);
106+
}
107+
108+
function c20pImportKey(
109+
algorithm,
110+
format,
111+
keyData,
112+
extractable,
113+
keyUsages) {
114+
const { name } = algorithm;
115+
const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'];
116+
117+
const usagesSet = new SafeSet(keyUsages);
118+
if (hasAnyNotIn(usagesSet, checkUsages)) {
119+
throw lazyDOMException(
120+
`Unsupported key usage for a ${algorithm.name} key`,
121+
'SyntaxError');
122+
}
123+
124+
let keyObject;
125+
switch (format) {
126+
case 'KeyObject': {
127+
keyObject = keyData;
128+
break;
129+
}
130+
case 'raw-secret': {
131+
keyObject = createSecretKey(keyData);
132+
break;
133+
}
134+
case 'jwk': {
135+
if (!keyData.kty)
136+
throw lazyDOMException('Invalid keyData', 'DataError');
137+
138+
if (keyData.kty !== 'oct')
139+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
140+
141+
if (usagesSet.size > 0 &&
142+
keyData.use !== undefined &&
143+
keyData.use !== 'enc') {
144+
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
145+
}
146+
147+
validateKeyOps(keyData.key_ops, usagesSet);
148+
149+
if (keyData.ext !== undefined &&
150+
keyData.ext === false &&
151+
extractable === true) {
152+
throw lazyDOMException(
153+
'JWK "ext" Parameter and extractable mismatch',
154+
'DataError');
155+
}
156+
157+
const handle = new KeyObjectHandle();
158+
try {
159+
handle.initJwk(keyData);
160+
} catch (err) {
161+
throw lazyDOMException(
162+
'Invalid keyData', { name: 'DataError', cause: err });
163+
}
164+
165+
if (keyData.alg !== undefined && keyData.alg !== 'C20P') {
166+
throw lazyDOMException(
167+
'JWK "alg" does not match the requested algorithm',
168+
'DataError');
169+
}
170+
171+
keyObject = new SecretKeyObject(handle);
172+
break;
173+
}
174+
default:
175+
return undefined;
176+
}
177+
178+
validateKeyLength(keyObject.symmetricKeySize * 8);
179+
180+
return new InternalCryptoKey(
181+
keyObject,
182+
{ name },
183+
keyUsages,
184+
extractable);
185+
}
186+
187+
module.exports = {
188+
c20pCipher,
189+
c20pGenerateKey,
190+
c20pImportKey,
191+
};

lib/internal/crypto/keys.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ const {
199199
result = require('internal/crypto/aes')
200200
.aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages);
201201
break;
202+
case 'ChaCha20-Poly1305':
203+
result = require('internal/crypto/chacha20_poly1305')
204+
.c20pImportKey(algorithm, 'KeyObject', this, extractable, keyUsages);
205+
break;
202206
case 'HKDF':
203207
// Fall through
204208
case 'PBKDF2':

lib/internal/crypto/util.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,13 @@ const kSupportedAlgorithms = {
245245
'encrypt': {
246246
'RSA-OAEP': 'RsaOaepParams',
247247
'AES-CBC': 'AesCbcParams',
248-
'AES-GCM': 'AesGcmParams',
248+
'AES-GCM': 'AeadParams',
249249
'AES-CTR': 'AesCtrParams',
250250
},
251251
'decrypt': {
252252
'RSA-OAEP': 'RsaOaepParams',
253253
'AES-CBC': 'AesCbcParams',
254-
'AES-GCM': 'AesGcmParams',
254+
'AES-GCM': 'AeadParams',
255255
'AES-CTR': 'AesCtrParams',
256256
},
257257
'get key length': {
@@ -290,6 +290,14 @@ const experimentalAlgorithms = ObjectEntries({
290290
'SHA3-256': { digest: null },
291291
'SHA3-384': { digest: null },
292292
'SHA3-512': { digest: null },
293+
'ChaCha20-Poly1305': {
294+
'encrypt': 'AeadParams',
295+
'decrypt': 'AeadParams',
296+
'generateKey': null,
297+
'importKey': null,
298+
'exportKey': null,
299+
'get key length': null,
300+
},
293301
});
294302

295303
for (const { 0: algorithm, 1: nid } of [
@@ -325,7 +333,7 @@ for (let i = 0; i < experimentalAlgorithms.length; i++) {
325333
}
326334

327335
const simpleAlgorithmDictionaries = {
328-
AesGcmParams: { iv: 'BufferSource', additionalData: 'BufferSource' },
336+
AeadParams: { iv: 'BufferSource', additionalData: 'BufferSource' },
329337
RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier' },
330338
EcKeyGenParams: {},
331339
HmacKeyGenParams: { hash: 'HashAlgorithmIdentifier' },

lib/internal/crypto/webcrypto.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ async function generateKey(
155155
result = await require('internal/crypto/aes')
156156
.aesGenerateKey(algorithm, extractable, keyUsages);
157157
break;
158+
case 'ChaCha20-Poly1305':
159+
resultType = 'CryptoKey';
160+
result = await require('internal/crypto/chacha20_poly1305')
161+
.c20pGenerateKey(algorithm, extractable, keyUsages);
162+
break;
158163
case 'ML-DSA-44':
159164
// Fall through
160165
case 'ML-DSA-65':
@@ -252,6 +257,8 @@ function getKeyLength({ name, length, hash }) {
252257
case 'HKDF':
253258
case 'PBKDF2':
254259
return null;
260+
case 'ChaCha20-Poly1305':
261+
return 256;
255262
}
256263
}
257264

@@ -431,7 +438,7 @@ async function exportKeyRawSeed(key) {
431438
}
432439
}
433440

434-
async function exportKeyRawSecret(key) {
441+
async function exportKeyRawSecret(key, format) {
435442
switch (key.algorithm.name) {
436443
case 'AES-CTR':
437444
// Fall through
@@ -443,6 +450,11 @@ async function exportKeyRawSecret(key) {
443450
// Fall through
444451
case 'HMAC':
445452
return key[kKeyObject][kHandle].export().buffer;
453+
case 'ChaCha20-Poly1305':
454+
if (format === 'raw-secret') {
455+
return key[kKeyObject][kHandle].export().buffer;
456+
}
457+
return undefined;
446458
default:
447459
return undefined;
448460
}
@@ -504,6 +516,9 @@ async function exportKeyJWK(key) {
504516
parameters.alg = require('internal/crypto/aes')
505517
.getAlgorithmName(key.algorithm.name, key.algorithm.length);
506518
break;
519+
case 'ChaCha20-Poly1305':
520+
parameters.alg = 'C20P';
521+
break;
507522
case 'HMAC': {
508523
const alg = normalizeHashName(
509524
key.algorithm.hash.name,
@@ -563,7 +578,7 @@ async function exportKey(format, key) {
563578
}
564579
case 'raw-secret': {
565580
if (key.type === 'secret') {
566-
result = await exportKeyRawSecret(key);
581+
result = await exportKeyRawSecret(key, format);
567582
}
568583
break;
569584
}
@@ -581,7 +596,7 @@ async function exportKey(format, key) {
581596
}
582597
case 'raw': {
583598
if (key.type === 'secret') {
584-
result = await exportKeyRawSecret(key);
599+
result = await exportKeyRawSecret(key, format);
585600
} else if (key.type === 'public') {
586601
result = await exportKeyRawPublic(key, format);
587602
}
@@ -687,6 +702,10 @@ async function importKey(
687702
result = require('internal/crypto/aes')
688703
.aesImportKey(algorithm, format, keyData, extractable, keyUsages);
689704
break;
705+
case 'ChaCha20-Poly1305':
706+
result = require('internal/crypto/chacha20_poly1305')
707+
.c20pImportKey(algorithm, format, keyData, extractable, keyUsages);
708+
break;
690709
case 'HKDF':
691710
// Fall through
692711
case 'PBKDF2':
@@ -975,6 +994,9 @@ async function cipherOrWrap(mode, algorithm, key, data, op) {
975994
case 'AES-GCM':
976995
return require('internal/crypto/aes')
977996
.aesCipher(mode, key, data, algorithm);
997+
case 'ChaCha20-Poly1305':
998+
return require('internal/crypto/chacha20_poly1305')
999+
.c20pCipher(mode, key, data, algorithm);
9781000
case 'AES-KW':
9791001
if (op === 'wrapKey' || op === 'unwrapKey') {
9801002
return require('internal/crypto/aes')

0 commit comments

Comments
 (0)