• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayBufferIsView,
5  ArrayBufferPrototypeSlice,
6  ArrayFrom,
7  ArrayPrototypeIncludes,
8  ArrayPrototypePush,
9  MathFloor,
10  SafeSet,
11  TypedArrayPrototypeSlice,
12} = primordials;
13
14const {
15  AESCipherJob,
16  KeyObjectHandle,
17  kCryptoJobAsync,
18  kKeyVariantAES_CTR_128,
19  kKeyVariantAES_CBC_128,
20  kKeyVariantAES_GCM_128,
21  kKeyVariantAES_KW_128,
22  kKeyVariantAES_CTR_192,
23  kKeyVariantAES_CBC_192,
24  kKeyVariantAES_GCM_192,
25  kKeyVariantAES_KW_192,
26  kKeyVariantAES_CTR_256,
27  kKeyVariantAES_CBC_256,
28  kKeyVariantAES_GCM_256,
29  kKeyVariantAES_KW_256,
30  kWebCryptoCipherDecrypt,
31  kWebCryptoCipherEncrypt,
32} = internalBinding('crypto');
33
34const {
35  hasAnyNotIn,
36  jobPromise,
37  validateByteLength,
38  validateKeyOps,
39  validateMaxBufferLength,
40  kAesKeyLengths,
41  kHandle,
42  kKeyObject,
43} = require('internal/crypto/util');
44
45const {
46  lazyDOMException,
47  promisify,
48} = require('internal/util');
49
50const { PromiseReject } = primordials;
51
52const {
53  InternalCryptoKey,
54  SecretKeyObject,
55  createSecretKey,
56} = require('internal/crypto/keys');
57
58const {
59  generateKey: _generateKey,
60} = require('internal/crypto/keygen');
61
62const kMaxCounterLength = 128;
63const kTagLengths = [32, 64, 96, 104, 112, 120, 128];
64const generateKey = promisify(_generateKey);
65
66function getAlgorithmName(name, length) {
67  switch (name) {
68    case 'AES-CBC': return `A${length}CBC`;
69    case 'AES-CTR': return `A${length}CTR`;
70    case 'AES-GCM': return `A${length}GCM`;
71    case 'AES-KW': return `A${length}KW`;
72  }
73}
74
75function validateKeyLength(length) {
76  if (length !== 128 && length !== 192 && length !== 256)
77    throw lazyDOMException('Invalid key length', 'DataError');
78}
79
80function getVariant(name, length) {
81  switch (name) {
82    case 'AES-CBC':
83      switch (length) {
84        case 128: return kKeyVariantAES_CBC_128;
85        case 192: return kKeyVariantAES_CBC_192;
86        case 256: return kKeyVariantAES_CBC_256;
87      }
88      break;
89    case 'AES-CTR':
90      switch (length) {
91        case 128: return kKeyVariantAES_CTR_128;
92        case 192: return kKeyVariantAES_CTR_192;
93        case 256: return kKeyVariantAES_CTR_256;
94      }
95      break;
96    case 'AES-GCM':
97      switch (length) {
98        case 128: return kKeyVariantAES_GCM_128;
99        case 192: return kKeyVariantAES_GCM_192;
100        case 256: return kKeyVariantAES_GCM_256;
101      }
102      break;
103    case 'AES-KW':
104      switch (length) {
105        case 128: return kKeyVariantAES_KW_128;
106        case 192: return kKeyVariantAES_KW_192;
107        case 256: return kKeyVariantAES_KW_256;
108      }
109      break;
110  }
111}
112
113function asyncAesCtrCipher(mode, key, data, { counter, length }) {
114  validateByteLength(counter, 'algorithm.counter', 16);
115  // The length must specify an integer between 1 and 128. While
116  // there is no default, this should typically be 64.
117  if (length === 0 || length > kMaxCounterLength) {
118    throw lazyDOMException(
119      'AES-CTR algorithm.length must be between 1 and 128',
120      'OperationError');
121  }
122
123  return jobPromise(() => new AESCipherJob(
124    kCryptoJobAsync,
125    mode,
126    key[kKeyObject][kHandle],
127    data,
128    getVariant('AES-CTR', key.algorithm.length),
129    counter,
130    length));
131}
132
133function asyncAesCbcCipher(mode, key, data, { iv }) {
134  validateByteLength(iv, 'algorithm.iv', 16);
135  return jobPromise(() => new AESCipherJob(
136    kCryptoJobAsync,
137    mode,
138    key[kKeyObject][kHandle],
139    data,
140    getVariant('AES-CBC', key.algorithm.length),
141    iv));
142}
143
144function asyncAesKwCipher(mode, key, data) {
145  return jobPromise(() => new AESCipherJob(
146    kCryptoJobAsync,
147    mode,
148    key[kKeyObject][kHandle],
149    data,
150    getVariant('AES-KW', key.algorithm.length)));
151}
152
153function asyncAesGcmCipher(
154  mode,
155  key,
156  data,
157  { iv, additionalData, tagLength = 128 }) {
158  if (!ArrayPrototypeIncludes(kTagLengths, tagLength)) {
159    return PromiseReject(lazyDOMException(
160      `${tagLength} is not a valid AES-GCM tag length`,
161      'OperationError'));
162  }
163
164  validateMaxBufferLength(iv, 'algorithm.iv');
165
166  if (additionalData !== undefined) {
167    validateMaxBufferLength(additionalData, 'algorithm.additionalData');
168  }
169
170  const tagByteLength = MathFloor(tagLength / 8);
171  let tag;
172  switch (mode) {
173    case kWebCryptoCipherDecrypt: {
174      const slice = ArrayBufferIsView(data) ?
175        TypedArrayPrototypeSlice : ArrayBufferPrototypeSlice;
176      tag = slice(data, -tagByteLength);
177
178      // Refs: https://www.w3.org/TR/WebCryptoAPI/#aes-gcm-operations
179      //
180      // > If *plaintext* has a length less than *tagLength* bits, then `throw`
181      // > an `OperationError`.
182      if (tagByteLength > tag.byteLength) {
183        return PromiseReject(lazyDOMException(
184          'The provided data is too small.',
185          'OperationError'));
186      }
187
188      data = slice(data, 0, -tagByteLength);
189      break;
190    }
191    case kWebCryptoCipherEncrypt:
192      tag = tagByteLength;
193      break;
194  }
195
196  return jobPromise(() => new AESCipherJob(
197    kCryptoJobAsync,
198    mode,
199    key[kKeyObject][kHandle],
200    data,
201    getVariant('AES-GCM', key.algorithm.length),
202    iv,
203    tag,
204    additionalData));
205}
206
207function aesCipher(mode, key, data, algorithm) {
208  switch (algorithm.name) {
209    case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm);
210    case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm);
211    case 'AES-GCM': return asyncAesGcmCipher(mode, key, data, algorithm);
212    case 'AES-KW': return asyncAesKwCipher(mode, key, data);
213  }
214}
215
216async function aesGenerateKey(algorithm, extractable, keyUsages) {
217  const { name, length } = algorithm;
218  if (!ArrayPrototypeIncludes(kAesKeyLengths, length)) {
219    throw lazyDOMException(
220      'AES key length must be 128, 192, or 256 bits',
221      'OperationError');
222  }
223
224  const checkUsages = ['wrapKey', 'unwrapKey'];
225  if (name !== 'AES-KW')
226    ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt');
227
228  const usagesSet = new SafeSet(keyUsages);
229  if (hasAnyNotIn(usagesSet, checkUsages)) {
230    throw lazyDOMException(
231      'Unsupported key usage for an AES key',
232      'SyntaxError');
233  }
234
235  const key = await generateKey('aes', { length }).catch((err) => {
236    throw lazyDOMException(
237      'The operation failed for an operation-specific reason' +
238      `[${err.message}]`,
239      { name: 'OperationError', cause: err });
240  });
241
242  return new InternalCryptoKey(
243    key,
244    { name, length },
245    ArrayFrom(usagesSet),
246    extractable);
247}
248
249async function aesImportKey(
250  algorithm,
251  format,
252  keyData,
253  extractable,
254  keyUsages) {
255  const { name } = algorithm;
256  const checkUsages = ['wrapKey', 'unwrapKey'];
257  if (name !== 'AES-KW')
258    ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt');
259
260  const usagesSet = new SafeSet(keyUsages);
261  if (hasAnyNotIn(usagesSet, checkUsages)) {
262    throw lazyDOMException(
263      'Unsupported key usage for an AES key',
264      'SyntaxError');
265  }
266
267  let keyObject;
268  let length;
269  switch (format) {
270    case 'raw': {
271      validateKeyLength(keyData.byteLength * 8);
272      keyObject = createSecretKey(keyData);
273      break;
274    }
275    case 'jwk': {
276      if (!keyData.kty)
277        throw lazyDOMException('Invalid keyData', 'DataError');
278
279      if (keyData.kty !== 'oct')
280        throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
281
282      if (usagesSet.size > 0 &&
283          keyData.use !== undefined &&
284          keyData.use !== 'enc') {
285        throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
286      }
287
288      validateKeyOps(keyData.key_ops, usagesSet);
289
290      if (keyData.ext !== undefined &&
291          keyData.ext === false &&
292          extractable === true) {
293        throw lazyDOMException(
294          'JWK "ext" Parameter and extractable mismatch',
295          'DataError');
296      }
297
298      const handle = new KeyObjectHandle();
299      handle.initJwk(keyData);
300
301      ({ length } = handle.keyDetail({ }));
302      validateKeyLength(length);
303
304      if (keyData.alg !== undefined) {
305        if (keyData.alg !== getAlgorithmName(algorithm.name, length))
306          throw lazyDOMException(
307            'JWK "alg" does not match the requested algorithm',
308            'DataError');
309      }
310
311      keyObject = new SecretKeyObject(handle);
312      break;
313    }
314    default:
315      throw lazyDOMException(
316        `Unable to import AES key with format ${format}`,
317        'NotSupportedError');
318  }
319
320  if (length === undefined) {
321    ({ length } = keyObject[kHandle].keyDetail({ }));
322    validateKeyLength(length);
323  }
324
325  return new InternalCryptoKey(
326    keyObject,
327    { name, length },
328    keyUsages,
329    extractable);
330}
331
332module.exports = {
333  aesCipher,
334  aesGenerateKey,
335  aesImportKey,
336  getAlgorithmName,
337};
338