Skip to main content

script/dom/webcrypto/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 chacha20poly1305::aead::{AeadMutInPlace, KeyInit, OsRng};
6use chacha20poly1305::{ChaCha20Poly1305, Key};
7use js::context::JSContext;
8use zeroize::Zeroizing;
9
10use crate::dom::bindings::codegen::Bindings::CryptoKeyBinding::{
11    CryptoKeyMethods, KeyType, KeyUsage,
12};
13use crate::dom::bindings::codegen::Bindings::SubtleCryptoBinding::{JsonWebKey, KeyFormat};
14use crate::dom::bindings::error::Error;
15use crate::dom::bindings::root::DomRoot;
16use crate::dom::bindings::str::DOMString;
17use crate::dom::cryptokey::{CryptoKey, Handle};
18use crate::dom::globalscope::GlobalScope;
19use crate::dom::subtlecrypto::{
20    CryptoAlgorithm, ExportedKey, JsonWebKeyExt, JwkStringField, KeyAlgorithmAndDerivatives,
21    SubtleAeadParams, SubtleKeyAlgorithm,
22};
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    cx: &mut JSContext,
153    global: &GlobalScope,
154    extractable: bool,
155    usages: Vec<KeyUsage>,
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. Set the [[type]] internal slot of key to "secret".
178    // Step 6. Let algorithm be a new KeyAlgorithm.
179    // Step 7. Set the name attribute of algorithm to "ChaCha20-Poly1305".
180    // Step 8. Set the [[algorithm]] internal slot of key to algorithm.
181    // Step 9. Set the [[extractable]] internal slot of key to be extractable.
182    // Step 10. Set the [[usages]] internal slot of key to be usages.
183    let algorithm = SubtleKeyAlgorithm {
184        name: CryptoAlgorithm::ChaCha20Poly1305,
185    };
186    let key = CryptoKey::new(
187        cx,
188        global,
189        KeyType::Secret,
190        extractable,
191        KeyAlgorithmAndDerivatives::KeyAlgorithm(algorithm),
192        usages,
193        Handle::ChaCha20Poly1305Key(generated_key),
194    );
195
196    // Step 11. Return key.
197    Ok(key)
198}
199
200/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-import-key>
201pub(crate) fn import_key(
202    cx: &mut JSContext,
203    global: &GlobalScope,
204    format: KeyFormat,
205    key_data: &[u8],
206    extractable: bool,
207    usages: Vec<KeyUsage>,
208) -> Result<DomRoot<CryptoKey>, Error> {
209    // Step 1. Let keyData be the key data to be imported.
210
211    // Step 2. If usages contains an entry which is not one of "encrypt", "decrypt", "wrapKey" or
212    // "unwrapKey", then throw a SyntaxError.
213    if usages.iter().any(|usage| {
214        !matches!(
215            usage,
216            KeyUsage::Encrypt | KeyUsage::Decrypt | KeyUsage::WrapKey | KeyUsage::UnwrapKey
217        )
218    }) {
219        return Err(Error::Syntax(Some(
220            "Usages contains an entry which is not one of \"encrypt\", \"decrypt\", \"wrapKey\" \
221            or \"unwrapKey\""
222                .to_string(),
223        )));
224    }
225
226    // Step 3.
227    let data: Zeroizing<Vec<u8>>;
228    match format {
229        // If format is "raw-secret":
230        KeyFormat::Raw_secret => {
231            // Step 3.1. Let data be keyData.
232            data = key_data.to_vec().into();
233
234            // Step 3.2. If the length in bits of data is not 256 then throw a DataError.
235            if data.len() != 32 {
236                return Err(Error::Data(Some(
237                    "The length in bits of data is not 256".to_string(),
238                )));
239            }
240        },
241        // If format is "jwk":
242        KeyFormat::Jwk => {
243            // Step 3.1.
244            // If keyData is a JsonWebKey dictionary:
245            //     Let jwk equal keyData.
246            // Otherwise:
247            //     Throw a DataError.
248            let jwk = JsonWebKey::parse(cx, key_data)?;
249
250            // Step 3.2. If the kty field of jwk is not "oct", then throw a DataError.
251            if jwk.kty.as_ref().is_none_or(|kty| kty != "oct") {
252                return Err(Error::Data(Some(
253                    "The kty field of jwk is not \"oct\"".to_string(),
254                )));
255            }
256
257            // Step 3.3. If jwk does not meet the requirements of Section 6.4 of JSON Web
258            // Algorithms [JWA], then throw a DataError.
259            // Step 3.4. Let data be the byte sequence obtained by decoding the k field of jwk.
260            data = jwk.decode_required_string_field(JwkStringField::K)?;
261
262            // Step 3.5. If the alg field of jwk is present, and is not "C20P", then throw a
263            // DataError.
264            if jwk.alg.as_ref().is_some_and(|alg| alg != "C20P") {
265                return Err(Error::Data(Some(
266                    "The alg field of jwk is present, and is not \"C20P\"".to_string(),
267                )));
268            }
269
270            // Step 3.6. If usages is non-empty and the use field of jwk is present and is not
271            // "enc", then throw a DataError.
272            if !usages.is_empty() && jwk.use_.as_ref().is_some_and(|use_| use_ != "enc") {
273                return Err(Error::Data(Some(
274                    "Usages is non-empty and the use field of jwk is present and is not \"enc\""
275                        .to_string(),
276                )));
277            }
278
279            // Step 3.7. If the key_ops field of jwk is present, and is invalid according to the
280            // requirements of JSON Web Key [JWK] or does not contain all of the specified usages
281            // values, then throw a DataError.
282            jwk.check_key_ops(&usages)?;
283
284            // Step 3.8. If the ext field of jwk is present and has the value false and extractable
285            // is true, then throw a DataError.
286            if jwk.ext.is_some_and(|ext| !ext) && extractable {
287                return Err(Error::Data(Some(
288                    "The ext field of jwk is present and has the value false and \
289                    extractable is true"
290                        .to_string(),
291                )));
292            }
293        },
294        // Otherwise:
295        _ => {
296            // throw a NotSupportedError.
297            return Err(Error::NotSupported(Some(
298                "ChaCha20-Poly1305 does not support this import key format".to_string(),
299            )));
300        },
301    }
302
303    // Step 4. Let key be a new CryptoKey object representing a key with value data.
304    // Step 5. Set the [[type]] internal slot of key to "secret".
305    // Step 6. Let algorithm be a new KeyAlgorithm.
306    // Step 7. Set the name attribute of algorithm to "ChaCha20-Poly1305".
307    // Step 8. Set the [[algorithm]] internal slot of key to algorithm.
308    let handle =
309        Handle::ChaCha20Poly1305Key(Key::from_exact_iter(data.to_vec()).ok_or(Error::Data(
310            Some("ChaCha20-Poly1305 fails to create key from data".to_string()),
311        ))?);
312    let algorithm = SubtleKeyAlgorithm {
313        name: CryptoAlgorithm::ChaCha20Poly1305,
314    };
315    let key = CryptoKey::new(
316        cx,
317        global,
318        KeyType::Secret,
319        extractable,
320        KeyAlgorithmAndDerivatives::KeyAlgorithm(algorithm),
321        usages,
322        handle,
323    );
324
325    // Step 9. Return key.
326    Ok(key)
327}
328
329/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-export-key>
330pub(crate) fn export_key(format: KeyFormat, key: &CryptoKey) -> Result<ExportedKey, Error> {
331    // Step 1. If the underlying cryptographic key material represented by the [[handle]] internal
332    // slot of key cannot be accessed, then throw an OperationError.
333    let Handle::ChaCha20Poly1305Key(key_handle) = key.handle() else {
334        return Err(Error::Operation(Some(
335            "The underlying cryptographic key material represented by the [[handle]] internal \
336            slot of key cannot be accessed"
337                .to_string(),
338        )));
339    };
340
341    // Step 2.
342    let result = match format {
343        // If format is "raw-secret":
344        KeyFormat::Raw_secret => {
345            // Step 2.1. Let data be a byte sequence containing the raw octets of the key
346            // represented by [[handle]] internal slot of key.
347            let data = key_handle.to_vec();
348
349            // Step 2.2 Let result be data.
350            ExportedKey::new_bytes(data)
351        },
352        // If format is "jwk":
353        KeyFormat::Jwk => {
354            // Step 2.1. Let jwk be a new JsonWebKey dictionary.
355            let mut jwk = JsonWebKey::default();
356
357            // Step 2.2. Set the kty attribute of jwk to the string "oct".
358            jwk.kty = Some(DOMString::from("oct"));
359
360            // Step 2.3. Set the k attribute of jwk to be a string containing the raw octets of the
361            // key represented by [[handle]] internal slot of key, encoded according to Section 6.4
362            // of JSON Web Algorithms [JWA].
363            jwk.encode_string_field(JwkStringField::K, key_handle.as_slice());
364
365            // Step 2.4. Set the alg attribute of jwk to the string "C20P".
366            jwk.alg = Some(DOMString::from("C20P"));
367
368            // Step 2.5. Set the key_ops attribute of jwk to equal the usages attribute of key.
369            jwk.set_key_ops(key.usages());
370
371            // Step 2.6. Set the ext attribute of jwk to equal the [[extractable]] internal slot of
372            // key.
373            jwk.ext = Some(key.Extractable());
374
375            // Step 2.7. Let result be jwk.
376            ExportedKey::new_jwk(jwk)
377        },
378        // Otherwise:
379        _ => {
380            // throw a NotSupportedError.
381            return Err(Error::NotSupported(Some(
382                "ChaCha20-Poly1305 does not support this import key format".to_string(),
383            )));
384        },
385    };
386
387    // Step 3. Return result.
388    Ok(result)
389}
390
391/// <https://wicg.github.io/webcrypto-modern-algos/#chacha20-poly1305-operations-get-key-length>
392pub(crate) fn get_key_length() -> Result<Option<u32>, Error> {
393    // Step 1. Return 256.
394    Ok(Some(256))
395}