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}