Skip to main content

script/dom/webcrypto/subtlecrypto/
hmac_operation.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use aws_lc_rs::constant_time::verify_slices_are_equal;
6use aws_lc_rs::hmac;
7use js::context::JSContext;
8use rand::TryRng;
9use rand::rngs::SysRng;
10use script_bindings::codegen::GenericBindings::CryptoKeyBinding::CryptoKeyMethods;
11use script_bindings::domstring::DOMString;
12use zeroize::Zeroizing;
13
14use crate::dom::bindings::codegen::Bindings::CryptoKeyBinding::{KeyType, KeyUsage};
15use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{JsonWebKey, KeyFormat};
16use crate::dom::bindings::error::Error;
17use crate::dom::bindings::root::DomRoot;
18use crate::dom::cryptokey::{CryptoKey, Handle, KeyUsageVecHelper};
19use crate::dom::globalscope::GlobalScope;
20use crate::dom::subtlecrypto::{
21    CryptoAlgorithm, ExportedKey, HmacImportParams, HmacKeyAlgorithm, HmacKeyGenParams,
22    JsonWebKeyExt, JwkStringField, KeyAlgorithmAndDerivatives, NormalizedAlgorithm,
23};
24
25/// <https://w3c.github.io/webcrypto/#hmac-operations-sign>
26pub(crate) fn sign(key: &CryptoKey, message: &[u8]) -> Result<Vec<u8>, Error> {
27    // Step 1. Let mac be the result of performing the MAC Generation operation described in
28    // Section 4 of [FIPS-198-1] using the key represented by the [[handle]] internal slot of key,
29    // the hash function identified by the hash attribute of the [[algorithm]] internal slot of key
30    // and message as the input data text.
31    let hash_function = match key.algorithm() {
32        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algo) => match algo.hash.name() {
33            CryptoAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
34            CryptoAlgorithm::Sha256 => hmac::HMAC_SHA256,
35            CryptoAlgorithm::Sha384 => hmac::HMAC_SHA384,
36            CryptoAlgorithm::Sha512 => hmac::HMAC_SHA512,
37            _ => {
38                return Err(Error::NotSupported(Some(
39                    "Unsupported hash algorithm for HMAC".into(),
40                )));
41            },
42        },
43        _ => {
44            return Err(Error::NotSupported(Some(
45                "The key algorithm is not HMAC".into(),
46            )));
47        },
48    };
49    let sign_key = hmac::Key::new(hash_function, key.handle().as_bytes());
50    let mac = hmac::sign(&sign_key, message);
51
52    // Step 2. Return mac.
53    Ok(mac.as_ref().to_vec())
54}
55
56/// <https://w3c.github.io/webcrypto/#hmac-operations-verify>
57pub(crate) fn verify(key: &CryptoKey, message: &[u8], signature: &[u8]) -> Result<bool, Error> {
58    // Step 1. Let mac be the result of performing the MAC Generation operation described in
59    // Section 4 of [FIPS-198-1] using the key represented by the [[handle]] internal slot of key,
60    // the hash function identified by the hash attribute of the [[algorithm]] internal slot of key
61    // and message as the input data text.
62    let hash_function = match key.algorithm() {
63        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algo) => match algo.hash.name() {
64            CryptoAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
65            CryptoAlgorithm::Sha256 => hmac::HMAC_SHA256,
66            CryptoAlgorithm::Sha384 => hmac::HMAC_SHA384,
67            CryptoAlgorithm::Sha512 => hmac::HMAC_SHA512,
68            _ => {
69                return Err(Error::NotSupported(Some(
70                    "Unsupported hash algorithm for HMAC".into(),
71                )));
72            },
73        },
74        _ => {
75            return Err(Error::NotSupported(Some(
76                "The key algorithm is not HMAC".into(),
77            )));
78        },
79    };
80    let sign_key = hmac::Key::new(hash_function, key.handle().as_bytes());
81    let mac = hmac::sign(&sign_key, message);
82
83    // Step 2. Return true if mac is equal to signature and false otherwise. This comparison must
84    // be performed in constant-time.
85    Ok(verify_slices_are_equal(mac.as_ref(), signature).is_ok())
86}
87
88/// <https://w3c.github.io/webcrypto/#hmac-operations-generate-key>
89pub(crate) fn generate_key(
90    cx: &mut JSContext,
91    global: &GlobalScope,
92    normalized_algorithm: &HmacKeyGenParams,
93    extractable: bool,
94    usages: Vec<KeyUsage>,
95) -> Result<DomRoot<CryptoKey>, Error> {
96    // Step 1. If usages contains any entry which is not "sign" or "verify", then throw a SyntaxError.
97    if usages
98        .iter()
99        .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify))
100    {
101        return Err(Error::Syntax(Some(
102            "Usages contains an entry which is not \"sign\" or \"verify\"".into(),
103        )));
104    }
105
106    // Step 2.
107    let length = match normalized_algorithm.length {
108        // If the length member of normalizedAlgorithm is not present:
109        None => {
110            // Let length be the block size in bits of the hash function identified by the
111            // hash member of normalizedAlgorithm.
112            hash_function_block_size_in_bits(normalized_algorithm.hash.name())?
113        },
114        // Otherwise, if the length member of normalizedAlgorithm is non-zero:
115        Some(length) if length != 0 => {
116            // Let length be equal to the length member of normalizedAlgorithm.
117            length
118        },
119        // Otherwise:
120        _ => {
121            // throw an OperationError.
122            return Err(Error::Operation(Some(
123                "The length member of normalizedAlgorithm is zero".into(),
124            )));
125        },
126    };
127
128    // Step 3. Generate a key of length length bits.
129    // Step 4. If the key generation step fails, then throw an OperationError.
130    let mut key_data = vec![0; length as usize];
131    if SysRng.try_fill_bytes(&mut key_data).is_err() {
132        return Err(Error::JSFailed);
133    }
134
135    // Step 6. Let algorithm be a new HmacKeyAlgorithm.
136    // Step 7. Set the name attribute of algorithm to "HMAC".
137    // Step 8. Set the length attribute of algorithm to length.
138    // Step 9. Let hash be a new KeyAlgorithm.
139    // Step 10. Set the name attribute of hash to equal the name member of the hash member of
140    // normalizedAlgorithm.
141    // Step 11. Set the hash attribute of algorithm to hash.
142    let algorithm = HmacKeyAlgorithm {
143        name: CryptoAlgorithm::Hmac,
144        hash: normalized_algorithm.hash.clone(),
145        length,
146    };
147
148    // Step 5. Let key be a new CryptoKey object representing the generated key.
149    // Step 12. Set the [[type]] internal slot of key to "secret".
150    // Step 13. Set the [[algorithm]] internal slot of key to algorithm.
151    // Step 14. Set the [[extractable]] internal slot of key to be extractable.
152    // Step 15. Set the [[usages]] internal slot of key to be the normalized value of usages.
153    let key = CryptoKey::new(
154        cx,
155        global,
156        KeyType::Secret,
157        extractable,
158        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algorithm),
159        usages.normalized_value(),
160        Handle::Hmac(key_data.into()),
161    );
162
163    // Step 16. Return key.
164    Ok(key)
165}
166
167/// <https://w3c.github.io/webcrypto/#hmac-operations-import-key>
168pub(crate) fn import_key(
169    cx: &mut JSContext,
170    global: &GlobalScope,
171    normalized_algorithm: &HmacImportParams,
172    format: KeyFormat,
173    key_data: &[u8],
174    extractable: bool,
175    usages: Vec<KeyUsage>,
176) -> Result<DomRoot<CryptoKey>, Error> {
177    // Step 1. If the length member of normalizedAlgorithm is present and is zero, then throw a
178    // DataError.
179    if normalized_algorithm
180        .length
181        .is_some_and(|length| length == 0)
182    {
183        return Err(Error::Data(Some(
184            "The length member of normalizedAlgorithm is present and is zero".into(),
185        )));
186    }
187
188    // Step 2. Let keyData be the key data to be imported.
189
190    // Step 3. If usages contains an entry which is not "sign" or "verify", then throw a SyntaxError.
191    // Note: This is not explicitly spec'ed, but also throw a SyntaxError if usages is empty
192    if usages
193        .iter()
194        .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify)) ||
195        usages.is_empty()
196    {
197        return Err(Error::Syntax(Some(
198            "Usages contains an entry which is not \"sign\" or \"verify\", or is empty".into(),
199        )));
200    }
201
202    // Step 4. Let hash be a new KeyAlgorithm.
203    let hash;
204
205    // Step 5.
206    let data: Zeroizing<Vec<u8>>;
207    match format {
208        // If format is "raw":
209        KeyFormat::Raw | KeyFormat::Raw_secret => {
210            // Step 5.1. Let data be keyData.
211            data = key_data.to_vec().into();
212
213            // Step 5.2. Set hash to equal the hash member of normalizedAlgorithm.
214            hash = &normalized_algorithm.hash;
215        },
216        // If format is "jwk":
217        KeyFormat::Jwk => {
218            // Step 5.1. If keyData is a JsonWebKey dictionary: Let jwk equal keyData.
219            // Otherwise: Throw a DataError.
220            // NOTE: Deserialize keyData to JsonWebKey dictionary by running JsonWebKey::parse
221            let jwk = JsonWebKey::parse(cx, key_data)?;
222
223            // Step 5.2. If the kty field of jwk is not "oct", then throw a DataError.
224            if jwk.kty.as_ref().is_none_or(|kty| kty != "oct") {
225                return Err(Error::Data(Some(
226                    "The kty field of jwk is not \"oct\"".into(),
227                )));
228            }
229
230            // Step 5.3. If jwk does not meet the requirements of Section 6.4 of JSON Web
231            // Algorithms [JWA], then throw a DataError.
232            // NOTE: Done by Step 5.4 and 5.6.
233
234            // Step 5.4. Let data be the byte sequence obtained by decoding the k field of jwk.
235            data = jwk.decode_required_string_field(JwkStringField::K)?;
236
237            // Step 5.5. Set the hash to equal the hash member of normalizedAlgorithm.
238            hash = &normalized_algorithm.hash;
239
240            // Step 5.6.
241            match hash.name() {
242                // If the name attribute of hash is "SHA-1":
243                CryptoAlgorithm::Sha1 => {
244                    // If the alg field of jwk is present and is not "HS1", then throw a DataError.
245                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS1") {
246                        return Err(Error::Data(Some(
247                            "The alg field of jwk is present, and is not \"HS1\"".into(),
248                        )));
249                    }
250                },
251                // If the name attribute of hash is "SHA-256":
252                CryptoAlgorithm::Sha256 => {
253                    // If the alg field of jwk is present and is not "HS256", then throw a DataError.
254                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS256") {
255                        return Err(Error::Data(Some(
256                            "The alg field of jwk is present, and is not \"HS256\"".into(),
257                        )));
258                    }
259                },
260                // If the name attribute of hash is "SHA-384":
261                CryptoAlgorithm::Sha384 => {
262                    // If the alg field of jwk is present and is not "HS384", then throw a DataError.
263                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS384") {
264                        return Err(Error::Data(Some(
265                            "The alg field of jwk is present, and is not \"HS384\"".into(),
266                        )));
267                    }
268                },
269                // If the name attribute of hash is "SHA-512":
270                CryptoAlgorithm::Sha512 => {
271                    // If the alg field of jwk is present and is not "HS512", then throw a DataError.
272                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS512") {
273                        return Err(Error::Data(Some(
274                            "The alg field of jwk is present, and is not \"HS512\"".into(),
275                        )));
276                    }
277                },
278                // Otherwise,
279                _name => {
280                    // if the name attribute of hash is defined in another applicable specification:
281                    // Perform any key import steps defined by other applicable specifications,
282                    // passing format, jwk and hash and obtaining hash
283                    // NOTE: Currently not support applicable specification.
284                    return Err(Error::NotSupported(Some(
285                        "Unsupported hash algorithm".into(),
286                    )));
287                },
288            }
289
290            // Step 5.7. If usages is non-empty and the use field of jwk is present and is not
291            // "sig", then throw a DataError.
292            if !usages.is_empty() && jwk.use_.as_ref().is_some_and(|use_| use_ != "sig") {
293                return Err(Error::Data(Some(
294                    "Usages is non-empty and the use field of jwk is present and is not \"sig\""
295                        .into(),
296                )));
297            }
298
299            // Step 5.8. If the key_ops field of jwk is present, and is invalid according to
300            // the requirements of JSON Web Key [JWK] or does not contain all of the specified
301            // usages values, then throw a DataError.
302            jwk.check_key_ops(&usages)?;
303
304            // Step 5.9. If the ext field of jwk is present and has the value false and
305            // extractable is true, then throw a DataError.
306            if jwk.ext.is_some_and(|ext| !ext) && extractable {
307                return Err(Error::Data(Some(
308                    "The ext field of jwk is present and has the value false and extractable is true"
309                        .into(),
310                )));
311            }
312        },
313        // Otherwise:
314        _ => {
315            // throw a NotSupportedError.
316            return Err(Error::NotSupported(Some(
317                "Unsupported import key format for HMAC key".into(),
318            )));
319        },
320    }
321
322    // Step 6. Let length be the length in bits of data.
323    let mut length = data.len() as u32 * 8;
324
325    // Step 7. If length is zero then throw a DataError.
326    if length == 0 {
327        return Err(Error::Data(Some(
328            "The length in bits of data is zero".into(),
329        )));
330    }
331
332    // Step 8. If the length member of normalizedAlgorithm is present:
333    if let Some(given_length) = normalized_algorithm.length {
334        //  If the length member of normalizedAlgorithm is greater than length:
335        if given_length > length {
336            // throw a DataError.
337            return Err(Error::Data(Some(
338                "The length member of normalizedAlgorithm is greater than the length in bits of data"
339                    .into(),
340            )));
341        }
342        // Otherwise:
343        else {
344            // Set length equal to the length member of normalizedAlgorithm.
345            length = given_length;
346        }
347    }
348
349    // Step 9. Let key be a new CryptoKey object representing an HMAC key with the first length
350    // bits of data.
351    // Step 10. Set the [[type]] internal slot of key to "secret".
352    // Step 11. Let algorithm be a new HmacKeyAlgorithm.
353    // Step 12. Set the name attribute of algorithm to "HMAC".
354    // Step 13. Set the length attribute of algorithm to length.
355    // Step 14. Set the hash attribute of algorithm to hash.
356    // Step 15. Set the [[algorithm]] internal slot of key to algorithm.
357    let algorithm = HmacKeyAlgorithm {
358        name: CryptoAlgorithm::Hmac,
359        hash: hash.clone(),
360        length,
361    };
362    let truncated_data = data[..length as usize / 8].to_vec();
363    let key = CryptoKey::new(
364        cx,
365        global,
366        KeyType::Secret,
367        extractable,
368        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algorithm),
369        usages.normalized_value(),
370        Handle::Hmac(truncated_data.into()),
371    );
372
373    // Step 16. Return key.
374    Ok(key)
375}
376
377/// <https://w3c.github.io/webcrypto/#hmac-operations-export-key>
378pub(crate) fn export_key(format: KeyFormat, key: &CryptoKey) -> Result<ExportedKey, Error> {
379    match format {
380        KeyFormat::Raw | KeyFormat::Raw_secret => match key.handle() {
381            Handle::Hmac(key_data) => Ok(ExportedKey::new_bytes(key_data.as_slice().to_vec())),
382            _ => Err(Error::Operation(Some(
383                "The key handle is not representing an HMAC key".into(),
384            ))),
385        },
386        KeyFormat::Jwk => {
387            // Step 4.1. Let jwk be a new JsonWebKey dictionary.
388            let mut jwk = JsonWebKey::default();
389
390            // Step 4.2. Set the kty attribute of jwk to the string "oct".
391            jwk.kty = Some(DOMString::from_static("oct"));
392
393            // Step 4.3. Set the k attribute of jwk to be a string containing data, encoded according
394            // to Section 6.4 of JSON Web Algorithms [JWA].
395            let key_data = key.handle().as_bytes();
396            jwk.encode_string_field(JwkStringField::K, key_data);
397
398            // Step 4.4. Let algorithm be the [[algorithm]] internal slot of key.
399            // Step 4.5. Let hash be the hash attribute of algorithm.
400            // Step 4.6.
401            // If the name attribute of hash is "SHA-1":
402            //     Set the alg attribute of jwk to the string "HS1".
403            // If the name attribute of hash is "SHA-256":
404            //     Set the alg attribute of jwk to the string "HS256".
405            // If the name attribute of hash is "SHA-384":
406            //     Set the alg attribute of jwk to the string "HS384".
407            // If the name attribute of hash is "SHA-512":
408            //     Set the alg attribute of jwk to the string "HS512".
409            // Otherwise, the name attribute of hash is defined in another applicable
410            // specification:
411            //     Perform any key export steps defined by other applicable specifications, passing
412            //     format and key and obtaining alg.
413            //     Set the alg attribute of jwk to alg.
414            let hash_algorithm = match key.algorithm() {
415                KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algorithm) => {
416                    match algorithm.hash.name() {
417                        CryptoAlgorithm::Sha1 => "HS1",
418                        CryptoAlgorithm::Sha256 => "HS256",
419                        CryptoAlgorithm::Sha384 => "HS384",
420                        CryptoAlgorithm::Sha512 => "HS512",
421                        _ => {
422                            return Err(Error::NotSupported(Some(
423                                "Unsupported hash algorithm for HMAC".into(),
424                            )));
425                        },
426                    }
427                },
428                _ => {
429                    return Err(Error::NotSupported(Some(
430                        "The key algorithm is not HMAC".into(),
431                    )));
432                },
433            };
434            jwk.alg = Some(DOMString::from(hash_algorithm));
435
436            // Step 4.7. Set the key_ops attribute of jwk to the usages attribute of key.
437            jwk.set_key_ops(key.usages());
438
439            // Step 4.8. Set the ext attribute of jwk to the [[extractable]] internal slot of key.
440            jwk.ext = Some(key.Extractable());
441
442            // Step 4.9. Let result be jwk.
443            Ok(ExportedKey::new_jwk(jwk))
444        },
445        // Otherwise:
446        _ => {
447            // throw a NotSupportedError.
448            Err(Error::NotSupported(Some(
449                "Unsupported export key format for HMAC key".into(),
450            )))
451        },
452    }
453}
454
455/// <https://w3c.github.io/webcrypto/#hmac-operations-get-key-length>
456pub(crate) fn get_key_length(
457    normalized_derived_key_algorithm: &HmacImportParams,
458) -> Result<Option<u32>, Error> {
459    // Step 1.
460    let length = match normalized_derived_key_algorithm.length {
461        // If the length member of normalizedDerivedKeyAlgorithm is not present:
462        None => {
463            // Let length be the block size in bits of the hash function identified by the hash
464            // member of normalizedDerivedKeyAlgorithm.
465            hash_function_block_size_in_bits(normalized_derived_key_algorithm.hash.name())?
466        },
467        // Otherwise, if the length member of normalizedDerivedKeyAlgorithm is non-zero:
468        Some(length) if length != 0 => {
469            // Let length be equal to the length member of normalizedDerivedKeyAlgorithm.
470            length
471        },
472        // Otherwise:
473        _ => {
474            // throw a TypeError.
475            return Err(Error::Type(c"[[length]] must not be zero".to_owned()));
476        },
477    };
478
479    // Step 2. Return length.
480    Ok(Some(length))
481}
482
483/// Return the block size in bits of a hash function, according to Figure 1 of
484/// <https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf>.
485fn hash_function_block_size_in_bits(hash: CryptoAlgorithm) -> Result<u32, Error> {
486    match hash {
487        CryptoAlgorithm::Sha1 => Ok(512),
488        CryptoAlgorithm::Sha256 => Ok(512),
489        CryptoAlgorithm::Sha384 => Ok(1024),
490        CryptoAlgorithm::Sha512 => Ok(1024),
491        _ => Err(Error::NotSupported(Some(
492            "Unidentified hash member".to_string(),
493        ))),
494    }
495}