script/dom/subtlecrypto/
chacha20_poly1305_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 base64ct::{Base64UrlUnpadded, Encoding};
6use chacha20poly1305::aead::{AeadMutInPlace, KeyInit, OsRng};
7use chacha20poly1305::{ChaCha20Poly1305, Key};
8
9use crate::dom::bindings::codegen::Bindings::CryptoKeyBinding::{
10    CryptoKeyMethods, KeyType, KeyUsage,
11};
12use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{JsonWebKey, KeyFormat};
13use crate::dom::bindings::error::Error;
14use crate::dom::bindings::root::DomRoot;
15use crate::dom::bindings::str::DOMString;
16use crate::dom::cryptokey::{CryptoKey, Handle};
17use crate::dom::globalscope::GlobalScope;
18use crate::dom::subtlecrypto::{
19    ALG_CHACHA20_POLY1305, ExportedKey, JsonWebKeyExt, KeyAlgorithmAndDerivatives,
20    SubtleAeadParams, SubtleKeyAlgorithm,
21};
22use crate::script_runtime::CanGc;
23
24/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-encrypt>
25pub(crate) fn encrypt(
26    normalized_algorithm: &SubtleAeadParams,
27    key: &CryptoKey,
28    plaintext: &[u8],
29) -> Result<Vec<u8>, Error> {
30    // Step 1. If the iv member of normalizedAlgorithm does not have a length of 12 bytes, then
31    // throw an OperationError.
32    if normalized_algorithm.iv.len() != 12 {
33        return Err(Error::Operation(Some(
34            "The iv member of normalizedAlgorithm does not have a length of 12 bytes".to_string(),
35        )));
36    }
37
38    // Step 2. If the tagLength member of normalizedAlgorithm is present and is not 128, then throw
39    // an OperationError.
40    if normalized_algorithm
41        .tag_length
42        .is_some_and(|tag_length| tag_length != 128)
43    {
44        return Err(Error::Operation(Some(
45            "The tagLength member of normalizedAlgorithm is present and is not 128".to_string(),
46        )));
47    }
48
49    // Step 3. Let additionalData be the additionalData member of normalizedAlgorithm if present or
50    // the empty octet string otherwise.
51    let additional_data = normalized_algorithm
52        .additional_data
53        .as_deref()
54        .unwrap_or_default();
55
56    // Step 4. Let ciphertext be the output that results from performing the AEAD_CHACHA20_POLY1305
57    // encryption algorithm described in Section 2.8 of [RFC8439], using the key represented by
58    // [[handle]] internal slot of key as the key input parameter, the iv member of
59    // normalizedAlgorithm as the nonce input parameter, plaintext as the plaintext input
60    // parameter, and additionalData as the additional authenticated data (AAD) input parameter.
61    let Handle::ChaCha20Poly1305Key(handle) = key.handle() else {
62        return Err(Error::Operation(Some(
63            "Unable to access key represented by [[handle]] internal slot".to_string(),
64        )));
65    };
66    let mut cipher = ChaCha20Poly1305::new(handle);
67    let nonce = normalized_algorithm.iv.as_slice();
68    let mut ciphertext = plaintext.to_vec();
69    cipher
70        .encrypt_in_place(nonce.into(), additional_data, &mut ciphertext)
71        .map_err(|_| {
72            Error::Operation(Some(
73                "ChaCha20-Poly1305 fails to encrypt plaintext".to_string(),
74            ))
75        })?;
76
77    // Step 5. Return ciphertext.
78    Ok(ciphertext)
79}
80
81/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-decrypt>
82pub(crate) fn decrypt(
83    normalized_algorithm: &SubtleAeadParams,
84    key: &CryptoKey,
85    ciphertext: &[u8],
86) -> Result<Vec<u8>, Error> {
87    // Step 1. If the iv member of normalizedAlgorithm does not have a length of 12 bytes, then
88    // throw an OperationError.
89    if normalized_algorithm.iv.len() != 12 {
90        return Err(Error::Operation(Some(
91            "The iv member of normalizedAlgorithm does not have a length of 12 bytes".to_string(),
92        )));
93    }
94
95    // Step 2. If the tagLength member of normalizedAlgorithm is present and is not 128, then throw
96    // an OperationError.
97    if normalized_algorithm
98        .tag_length
99        .is_some_and(|tag_length| tag_length != 128)
100    {
101        return Err(Error::Operation(Some(
102            "The tagLength member of normalizedAlgorithm is present and is not 128".to_string(),
103        )));
104    }
105
106    // Step 3. If ciphertext has a length less than 128 bits, then throw an OperationError.
107    if ciphertext.len() < 16 {
108        return Err(Error::Operation(Some(
109            "Ciphertext has a length less than 128 bits".to_string(),
110        )));
111    }
112
113    // Step 4. Let additionalData be the additionalData member of normalizedAlgorithm if present or
114    // the empty octet string otherwise.
115    let additional_data = normalized_algorithm
116        .additional_data
117        .as_deref()
118        .unwrap_or_default();
119
120    // Step 5. Perform the AEAD_CHACHA20_POLY1305 decryption algorithm described in Section 2.8 of
121    // [RFC8439], using the key represented by [[handle]] internal slot of key as the key input
122    // parameter, the iv member of normalizedAlgorithm as the nonce input parameter, ciphertext as
123    // the ciphertext input parameter, and additionalData as the additional authenticated data
124    // (AAD) input parameter.
125    //
126    // If the result of the algorithm is the indication of authentication failure:
127    //     throw an OperationError
128    // Otherwise:
129    //     Let plaintext be the resulting plaintext.
130    let Handle::ChaCha20Poly1305Key(handle) = key.handle() else {
131        return Err(Error::Operation(Some(
132            "Unable to access key represented by [[handle]] internal slot".to_string(),
133        )));
134    };
135    let mut cipher = ChaCha20Poly1305::new(handle);
136    let nonce = normalized_algorithm.iv.as_slice();
137    let mut plaintext = ciphertext.to_vec();
138    cipher
139        .decrypt_in_place(nonce.into(), additional_data, &mut plaintext)
140        .map_err(|_| {
141            Error::Operation(Some(
142                "ChaCha20-Poly1305 fails to decrypt ciphertext".to_string(),
143            ))
144        })?;
145
146    // Step 6. Return plaintext.
147    Ok(plaintext)
148}
149
150/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-generate-key>
151pub(crate) fn generate_key(
152    global: &GlobalScope,
153    extractable: bool,
154    usages: Vec<KeyUsage>,
155    can_gc: CanGc,
156) -> Result<DomRoot<CryptoKey>, Error> {
157    // Step 1. If usages contains any entry which is not one of "encrypt", "decrypt", "wrapKey" or
158    // "unwrapKey", then throw a SyntaxError.
159    if usages.iter().any(|usage| {
160        !matches!(
161            usage,
162            KeyUsage::Encrypt | KeyUsage::Decrypt | KeyUsage::WrapKey | KeyUsage::UnwrapKey
163        )
164    }) {
165        return Err(Error::Syntax(Some(
166            "Usages contains an entry which is not one of \"encrypt\", \"decrypt\", \"wrapKey\" \
167            or \"unwrapKey\""
168                .to_string(),
169        )));
170    }
171
172    // Step 2. Generate a 256-bit key.
173    // Step 3. If the key generation step fails, then throw an OperationError.
174    let generated_key = ChaCha20Poly1305::generate_key(&mut OsRng);
175
176    // Step 4. Let key be a new CryptoKey object representing the generated key.
177    // Step 5. Let algorithm be a new KeyAlgorithm.
178    // Step 6. Set the name attribute of algorithm to "ChaCha20-Poly1305".
179    // Step 7. Set the [[algorithm]] internal slot of key to algorithm.
180    // Step 8. Set the [[extractable]] internal slot of key to be extractable.
181    // Step 9. Set the [[usages]] internal slot of key to be usages.
182    let algorithm = SubtleKeyAlgorithm {
183        name: ALG_CHACHA20_POLY1305.to_string(),
184    };
185    let key = CryptoKey::new(
186        global,
187        KeyType::Secret,
188        extractable,
189        KeyAlgorithmAndDerivatives::KeyAlgorithm(algorithm),
190        usages,
191        Handle::ChaCha20Poly1305Key(generated_key),
192        can_gc,
193    );
194
195    // Step 10. Return key.
196    Ok(key)
197}
198
199/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-import-key>
200pub(crate) fn import_key(
201    global: &GlobalScope,
202    format: KeyFormat,
203    key_data: &[u8],
204    extractable: bool,
205    usages: Vec<KeyUsage>,
206    can_gc: CanGc,
207) -> Result<DomRoot<CryptoKey>, Error> {
208    // Step 1. Let keyData be the key data to be imported.
209
210    // Step 2. If usages contains an entry which is not one of "encrypt", "decrypt", "wrapKey" or
211    // "unwrapKey", then throw a SyntaxError.
212    if usages.iter().any(|usage| {
213        !matches!(
214            usage,
215            KeyUsage::Encrypt | KeyUsage::Decrypt | KeyUsage::WrapKey | KeyUsage::UnwrapKey
216        )
217    }) {
218        return Err(Error::Syntax(None));
219    }
220
221    // Step 3.
222    let data;
223    match format {
224        // If format is "raw-secret":
225        KeyFormat::Raw_secret => {
226            // Step 3.1. Let data be keyData.
227            data = key_data.to_vec();
228
229            // Step 3.2. If the length in bits of data is not 256 then throw a DataError.
230            if data.len() != 32 {
231                return Err(Error::Data(None));
232            }
233        },
234        // If format is "jwk":
235        KeyFormat::Jwk => {
236            // Step 3.1.
237            // If keyData is a JsonWebKey dictionary:
238            //     Let jwk equal keyData.
239            // Otherwise:
240            //     Throw a DataError.
241            let jwk = JsonWebKey::parse(GlobalScope::get_cx(), key_data)?;
242
243            // Step 3.2. If the kty field of jwk is not "oct", then throw a DataError.
244            if jwk.kty.as_ref().is_none_or(|kty| kty != "oct") {
245                return Err(Error::Data(None));
246            }
247
248            // Step 3.3. If jwk does not meet the requirements of Section 6.4 of JSON Web
249            // Algorithms [JWA], then throw a DataError.
250            let Some(k) = jwk.k.as_ref() else {
251                return Err(Error::Data(None));
252            };
253
254            // Step 3.4. Let data be the byte sequence obtained by decoding the k field of jwk.
255            data = Base64UrlUnpadded::decode_vec(&k.str()).map_err(|_| Error::Data(None))?;
256
257            // Step 3.5. If the alg field of jwk is present, and is not "C20P", then throw a
258            // DataError.
259            if jwk.alg.as_ref().is_some_and(|alg| alg != "C20P") {
260                return Err(Error::Data(None));
261            }
262
263            // Step 3.6. If usages is non-empty and the use field of jwk is present and is not
264            // "enc", then throw a DataError.
265            if !usages.is_empty() && jwk.use_.as_ref().is_some_and(|use_| use_ != "enc") {
266                return Err(Error::Data(None));
267            }
268
269            // Step 3.7. If the key_ops field of jwk is present, and is invalid according to the
270            // requirements of JSON Web Key [JWK] or does not contain all of the specified usages
271            // values, then throw a DataError.
272            jwk.check_key_ops(&usages)?;
273
274            // Step 3.8. If the ext field of jwk is present and has the value false and extractable
275            // is true, then throw a DataError.
276            if jwk.ext.is_some_and(|ext| !ext) && extractable {
277                return Err(Error::Data(None));
278            }
279        },
280        // Otherwise:
281        _ => {
282            // throw a NotSupportedError.
283            return Err(Error::NotSupported(None));
284        },
285    }
286
287    // Step 4. Let key be a new CryptoKey object representing a key with value data.
288    // Step 5. Let algorithm be a new KeyAlgorithm.
289    // Step 6. Set the name attribute of algorithm to "ChaCha20-Poly1305".
290    // Step 7. Set the [[algorithm]] internal slot of key to algorithm.
291    let algorithm = SubtleKeyAlgorithm {
292        name: ALG_CHACHA20_POLY1305.to_string(),
293    };
294    let key = CryptoKey::new(
295        global,
296        KeyType::Secret,
297        extractable,
298        KeyAlgorithmAndDerivatives::KeyAlgorithm(algorithm),
299        usages,
300        Handle::ChaCha20Poly1305Key(Key::from_exact_iter(data).ok_or(Error::Data(None))?),
301        can_gc,
302    );
303
304    // Step 8. Return key.
305    Ok(key)
306}
307
308/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-export-key>
309pub(crate) fn export_key(format: KeyFormat, key: &CryptoKey) -> Result<ExportedKey, Error> {
310    // Step 1. If the underlying cryptographic key material represented by the [[handle]] internal
311    // slot of key cannot be accessed, then throw an OperationError.
312    let Handle::ChaCha20Poly1305Key(key_handle) = key.handle() else {
313        return Err(Error::Operation(None));
314    };
315
316    // Step 2.
317    let result = match format {
318        // If format is "raw-secret":
319        KeyFormat::Raw_secret => {
320            // Step 2.1. Let data be a byte sequence containing the raw octets of the key
321            // represented by [[handle]] internal slot of key.
322            let data = key_handle.to_vec();
323
324            // Step 2.2 Let result be data.
325            ExportedKey::Bytes(data)
326        },
327        // If format is "jwk":
328        KeyFormat::Jwk => {
329            // Step 2.1. Let jwk be a new JsonWebKey dictionary.
330            // Step 2.2. Set the kty attribute of jwk to the string "oct".
331            // Step 2.3. Set the k attribute of jwk to be a string containing the raw octets of the
332            // key represented by [[handle]] internal slot of key, encoded according to Section 6.4
333            // of JSON Web Algorithms [JWA].
334            // Step 2.4. Set the alg attribute of jwk to the string "C20P".
335            // Step 2.5. Set the key_ops attribute of jwk to equal the usages attribute of key.
336            // Step 2.6. Set the ext attribute of jwk to equal the [[extractable]] internal slot of
337            // key.
338            let jwk = JsonWebKey {
339                kty: Some(DOMString::from("oct")),
340                k: Some(Base64UrlUnpadded::encode_string(key_handle.as_slice()).into()),
341                alg: Some(DOMString::from("C20P")),
342                key_ops: Some(
343                    key.usages()
344                        .iter()
345                        .map(|usage| DOMString::from(usage.as_str()))
346                        .collect::<Vec<DOMString>>(),
347                ),
348                ext: Some(key.Extractable()),
349                ..Default::default()
350            };
351
352            // Step 2.7. Let result be the result of converting jwk to an ECMAScript Object, as
353            // defined by [WebIDL].
354            // NOTE: We convert it to JSObject in SubtleCrypto::ExportKey.
355            ExportedKey::Jwk(Box::new(jwk))
356        },
357        // Otherwise:
358        _ => {
359            // throw a NotSupportedError.
360            return Err(Error::NotSupported(None));
361        },
362    };
363
364    // Step 3. Return result.
365    Ok(result)
366}
367
368/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-get-key-length>
369pub(crate) fn get_key_length() -> Result<Option<u32>, Error> {
370    // Step 1. Return 256.
371    Ok(Some(256))
372}