script/dom/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::hmac;
6use base64ct::{Base64UrlUnpadded, Encoding};
7use rand::TryRngCore;
8use rand::rngs::OsRng;
9use script_bindings::codegen::GenericBindings::CryptoKeyBinding::CryptoKeyMethods;
10use script_bindings::domstring::DOMString;
11
12use crate::dom::bindings::codegen::Bindings::CryptoKeyBinding::{KeyType, KeyUsage};
13use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{JsonWebKey, KeyFormat};
14use crate::dom::bindings::error::Error;
15use crate::dom::bindings::root::DomRoot;
16use crate::dom::cryptokey::{CryptoKey, Handle};
17use crate::dom::globalscope::GlobalScope;
18use crate::dom::subtlecrypto::{
19    ALG_HMAC, ALG_SHA1, ALG_SHA256, ALG_SHA384, ALG_SHA512, ExportedKey, JsonWebKeyExt,
20    KeyAlgorithmAndDerivatives, SubtleHmacImportParams, SubtleHmacKeyAlgorithm,
21    SubtleHmacKeyGenParams, SubtleKeyAlgorithm,
22};
23use crate::script_runtime::CanGc;
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.as_str() {
33            ALG_SHA1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
34            ALG_SHA256 => hmac::HMAC_SHA256,
35            ALG_SHA384 => hmac::HMAC_SHA384,
36            ALG_SHA512 => hmac::HMAC_SHA512,
37            _ => return Err(Error::NotSupported),
38        },
39        _ => return Err(Error::NotSupported),
40    };
41    let sign_key = hmac::Key::new(hash_function, key.handle().as_bytes());
42    let mac = hmac::sign(&sign_key, message);
43
44    // Step 2. Return mac.
45    Ok(mac.as_ref().to_vec())
46}
47
48/// <https://w3c.github.io/webcrypto/#hmac-operations-verify>
49pub(crate) fn verify(key: &CryptoKey, message: &[u8], signature: &[u8]) -> Result<bool, Error> {
50    // Step 1. Let mac be the result of performing the MAC Generation operation described in
51    // Section 4 of [FIPS-198-1] using the key represented by the [[handle]] internal slot of key,
52    // the hash function identified by the hash attribute of the [[algorithm]] internal slot of key
53    // and message as the input data text.
54    let hash_function = match key.algorithm() {
55        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algo) => match algo.hash.name.as_str() {
56            ALG_SHA1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY,
57            ALG_SHA256 => hmac::HMAC_SHA256,
58            ALG_SHA384 => hmac::HMAC_SHA384,
59            ALG_SHA512 => hmac::HMAC_SHA512,
60            _ => return Err(Error::NotSupported),
61        },
62        _ => return Err(Error::NotSupported),
63    };
64    let sign_key = hmac::Key::new(hash_function, key.handle().as_bytes());
65    let mac = hmac::sign(&sign_key, message);
66
67    // Step 2. Return true if mac is equal to signature and false otherwise.
68    Ok(mac.as_ref() == signature)
69}
70
71/// <https://w3c.github.io/webcrypto/#hmac-operations-generate-key>
72pub(crate) fn generate_key(
73    global: &GlobalScope,
74    normalized_algorithm: &SubtleHmacKeyGenParams,
75    extractable: bool,
76    usages: Vec<KeyUsage>,
77    can_gc: CanGc,
78) -> Result<DomRoot<CryptoKey>, Error> {
79    // Step 1. If usages contains any entry which is not "sign" or "verify", then throw a SyntaxError.
80    if usages
81        .iter()
82        .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify))
83    {
84        return Err(Error::Syntax(None));
85    }
86
87    // Step 2.
88    let length = match normalized_algorithm.length {
89        // If the length member of normalizedAlgorithm is not present:
90        None => {
91            // Let length be the block size in bits of the hash function identified by the
92            // hash member of normalizedAlgorithm.
93            normalized_algorithm.hash.block_size_in_bits()?
94        },
95        // Otherwise, if the length member of normalizedAlgorithm is non-zero:
96        Some(length) if length != 0 => {
97            // Let length be equal to the length member of normalizedAlgorithm.
98            length
99        },
100        // Otherwise:
101        _ => {
102            // throw an OperationError.
103            return Err(Error::Operation);
104        },
105    };
106
107    // Step 3. Generate a key of length length bits.
108    // Step 4. If the key generation step fails, then throw an OperationError.
109    let mut key_data = vec![0; length as usize];
110    if OsRng.try_fill_bytes(&mut key_data).is_err() {
111        return Err(Error::JSFailed);
112    }
113
114    // Step 6. Let algorithm be a new HmacKeyAlgorithm.
115    // Step 7. Set the name attribute of algorithm to "HMAC".
116    // Step 8. Set the length attribute of algorithm to length.
117    // Step 9. Let hash be a new KeyAlgorithm.
118    // Step 10. Set the name attribute of hash to equal the name member of the hash member of
119    // normalizedAlgorithm.
120    // Step 11. Set the hash attribute of algorithm to hash.
121    let hash = SubtleKeyAlgorithm {
122        name: normalized_algorithm.hash.name.clone(),
123    };
124    let algorithm = SubtleHmacKeyAlgorithm {
125        name: ALG_HMAC.to_string(),
126        hash,
127        length,
128    };
129
130    // Step 5. Let key be a new CryptoKey object representing the generated key.
131    // Step 12. Set the [[type]] internal slot of key to "secret".
132    // Step 13. Set the [[algorithm]] internal slot of key to algorithm.
133    // Step 14. Set the [[extractable]] internal slot of key to be extractable.
134    // Step 15. Set the [[usages]] internal slot of key to be usages.
135    let key = CryptoKey::new(
136        global,
137        KeyType::Secret,
138        extractable,
139        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algorithm),
140        usages,
141        Handle::Hmac(key_data),
142        can_gc,
143    );
144
145    // Step 16. Return key.
146    Ok(key)
147}
148
149/// <https://w3c.github.io/webcrypto/#hmac-operations-import-key>
150pub(crate) fn import_key(
151    global: &GlobalScope,
152    normalized_algorithm: &SubtleHmacImportParams,
153    format: KeyFormat,
154    key_data: &[u8],
155    extractable: bool,
156    usages: Vec<KeyUsage>,
157    can_gc: CanGc,
158) -> Result<DomRoot<CryptoKey>, Error> {
159    // Step 1. Let keyData be the key data to be imported.
160    // Step 2. If usages contains an entry which is not "sign" or "verify", then throw a SyntaxError.
161    // Note: This is not explicitly spec'ed, but also throw a SyntaxError if usages is empty
162    if usages
163        .iter()
164        .any(|usage| !matches!(usage, KeyUsage::Sign | KeyUsage::Verify)) ||
165        usages.is_empty()
166    {
167        return Err(Error::Syntax(None));
168    }
169
170    // Step 3. Let hash be a new KeyAlgorithm.
171    let hash;
172
173    // Step 4.
174    let data;
175    match format {
176        // If format is "raw":
177        KeyFormat::Raw => {
178            // Step 4.1. Let data be keyData.
179            data = key_data.to_vec();
180
181            // Step 4.2. Set hash to equal the hash member of normalizedAlgorithm.
182            hash = normalized_algorithm.hash.clone();
183        },
184        // If format is "jwk":
185        KeyFormat::Jwk => {
186            // Step 2.1. If keyData is a JsonWebKey dictionary: Let jwk equal keyData.
187            // Otherwise: Throw a DataError.
188            // NOTE: Deserialize keyData to JsonWebKey dictionary by running JsonWebKey::parse
189            let jwk = JsonWebKey::parse(GlobalScope::get_cx(), key_data)?;
190
191            // Step 2.2. If the kty field of jwk is not "oct", then throw a DataError.
192            if jwk.kty.as_ref().is_none_or(|kty| kty != "oct") {
193                return Err(Error::Data);
194            }
195
196            // Step 2.3. If jwk does not meet the requirements of Section 6.4 of JSON Web
197            // Algorithms [JWA], then throw a DataError.
198            // NOTE: Done by Step 2.4 and 2.6.
199
200            // Step 2.4. Let data be the byte sequence obtained by decoding the k field of jwk.
201            data = Base64UrlUnpadded::decode_vec(&jwk.k.as_ref().ok_or(Error::Data)?.str())
202                .map_err(|_| Error::Data)?;
203
204            // Step 2.5. Set the hash to equal the hash member of normalizedAlgorithm.
205            hash = normalized_algorithm.hash.clone();
206
207            // Step 2.6.
208            match hash.name.as_str() {
209                // If the name attribute of hash is "SHA-1":
210                ALG_SHA1 => {
211                    // If the alg field of jwk is present and is not "HS1", then throw a DataError.
212                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS1") {
213                        return Err(Error::Data);
214                    }
215                },
216                // If the name attribute of hash is "SHA-256":
217                ALG_SHA256 => {
218                    // If the alg field of jwk is present and is not "HS256", then throw a DataError.
219                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS256") {
220                        return Err(Error::Data);
221                    }
222                },
223                // If the name attribute of hash is "SHA-384":
224                ALG_SHA384 => {
225                    // If the alg field of jwk is present and is not "HS384", then throw a DataError.
226                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS384") {
227                        return Err(Error::Data);
228                    }
229                },
230                // If the name attribute of hash is "SHA-512":
231                ALG_SHA512 => {
232                    // If the alg field of jwk is present and is not "HS512", then throw a DataError.
233                    if jwk.alg.as_ref().is_some_and(|alg| alg != "HS512") {
234                        return Err(Error::Data);
235                    }
236                },
237                // Otherwise,
238                _name => {
239                    // if the name attribute of hash is defined in another applicable specification:
240                    // Perform any key import steps defined by other applicable specifications,
241                    // passing format, jwk and hash and obtaining hash
242                    // NOTE: Currently not support applicable specification.
243                    return Err(Error::NotSupported);
244                },
245            }
246
247            // Step 2.7. If usages is non-empty and the use field of jwk is present and is not
248            // "sig", then throw a DataError.
249            if !usages.is_empty() && jwk.use_.as_ref().is_some_and(|use_| use_ != "sig") {
250                return Err(Error::Data);
251            }
252
253            // Step 2.8. If the key_ops field of jwk is present, and is invalid according to
254            // the requirements of JSON Web Key [JWK] or does not contain all of the specified
255            // usages values, then throw a DataError.
256            jwk.check_key_ops(&usages)?;
257
258            // Step 2.9. If the ext field of jwk is present and has the value false and
259            // extractable is true, then throw a DataError.
260            if jwk.ext.is_some_and(|ext| !ext) && extractable {
261                return Err(Error::Data);
262            }
263        },
264        // Otherwise:
265        _ => {
266            // throw a NotSupportedError.
267            return Err(Error::NotSupported);
268        },
269    }
270
271    // Step 5. Let length be the length in bits of data.
272    let mut length = data.len() as u32 * 8;
273
274    // Step 6. If length is zero then throw a DataError.
275    if length == 0 {
276        return Err(Error::Data);
277    }
278
279    // Step 7. If the length member of normalizedAlgorithm is present:
280    if let Some(given_length) = normalized_algorithm.length {
281        //  If the length member of normalizedAlgorithm is greater than length:
282        if given_length > length {
283            // throw a DataError.
284            return Err(Error::Data);
285        }
286        // Otherwise:
287        else {
288            // Set length equal to the length member of normalizedAlgorithm.
289            length = given_length;
290        }
291    }
292
293    // Step 10. Let algorithm be a new HmacKeyAlgorithm.
294    // Step 11. Set the name attribute of algorithm to "HMAC".
295    // Step 12. Set the length attribute of algorithm to length.
296    // Step 13. Set the hash attribute of algorithm to hash.
297    let algorithm = SubtleHmacKeyAlgorithm {
298        name: ALG_HMAC.to_string(),
299        hash,
300        length,
301    };
302
303    // Step 8. Let key be a new CryptoKey object representing an HMAC key with the first length
304    // bits of data.
305    // Step 9. Set the [[type]] internal slot of key to "secret".
306    // Step 14. Set the [[algorithm]] internal slot of key to algorithm.
307    let truncated_data = data[..length as usize / 8].to_vec();
308    let key = CryptoKey::new(
309        global,
310        KeyType::Secret,
311        extractable,
312        KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(algorithm),
313        usages,
314        Handle::Hmac(truncated_data),
315        can_gc,
316    );
317
318    // Step 15. Return key.
319    Ok(key)
320}
321
322/// <https://w3c.github.io/webcrypto/#hmac-operations-export-key>
323pub(crate) fn export(format: KeyFormat, key: &CryptoKey) -> Result<ExportedKey, Error> {
324    match format {
325        KeyFormat::Raw => match key.handle() {
326            Handle::Hmac(key_data) => Ok(ExportedKey::Raw(key_data.as_slice().to_vec())),
327            _ => Err(Error::Operation),
328        },
329        KeyFormat::Jwk => {
330            let key_data = key.handle().as_bytes();
331            // Step 3. Set the k attribute of jwk to be a string containing data
332            let k = Base64UrlUnpadded::encode_string(key_data);
333            // Step 6.
334            let hash_algorithm = match key.algorithm() {
335                KeyAlgorithmAndDerivatives::HmacKeyAlgorithm(alg) => match &*alg.hash.name {
336                    ALG_SHA1 => "HS1",
337                    ALG_SHA256 => "HS256",
338                    ALG_SHA384 => "HS384",
339                    ALG_SHA512 => "HS512",
340                    _ => return Err(Error::NotSupported),
341                },
342                _ => return Err(Error::NotSupported),
343            };
344
345            // Step 7. Set the key_ops attribute of jwk to the usages attribute of key.
346            let key_ops = key
347                .usages()
348                .iter()
349                .map(|usage| DOMString::from(usage.as_str()))
350                .collect::<Vec<DOMString>>();
351
352            // Step 1. Let jwk be a new JsonWebKey dictionary.
353            let jwk = JsonWebKey {
354                // Step 2. Set the kty attribute of jwk to the string "oct".
355                kty: Some(DOMString::from("oct")),
356                k: Some(DOMString::from(k)),
357                alg: Some(DOMString::from(hash_algorithm.to_string())),
358                // Step 7. Set the key_ops attribute of jwk to equal the usages attribute of key.
359                key_ops: Some(key_ops),
360                // Step 8. Set the ext attribute of jwk to the [[extractable]] internal slot of key.
361                ext: Some(key.Extractable()),
362                ..Default::default()
363            };
364
365            Ok(ExportedKey::Jwk(Box::new(jwk)))
366        },
367        _ => Err(Error::NotSupported),
368    }
369}
370
371/// <https://w3c.github.io/webcrypto/#hmac-operations-get-key-length>
372pub(crate) fn get_key_length(
373    normalized_derived_key_algorithm: &SubtleHmacImportParams,
374) -> Result<Option<u32>, Error> {
375    // Step 1.
376    let length = match normalized_derived_key_algorithm.length {
377        // If the length member of normalizedDerivedKeyAlgorithm is not present:
378        None => {
379            // Let length be the block size in bits of the hash function identified by the hash
380            // member of normalizedDerivedKeyAlgorithm.
381            match normalized_derived_key_algorithm.hash.name.as_str() {
382                ALG_SHA1 => 160,
383                ALG_SHA256 => 256,
384                ALG_SHA384 => 384,
385                ALG_SHA512 => 512,
386                _ => {
387                    return Err(Error::Type("Unidentified hash member".to_string()));
388                },
389            }
390        },
391        // Otherwise, if the length member of normalizedDerivedKeyAlgorithm is non-zero:
392        Some(length) if length != 0 => {
393            // Let length be equal to the length member of normalizedDerivedKeyAlgorithm.
394            length
395        },
396        // Otherwise:
397        _ => {
398            // throw a TypeError.
399            return Err(Error::Type("[[length]] must not be zero".to_string()));
400        },
401    };
402
403    // Step 2. Return length.
404    Ok(Some(length))
405}