Skip to main content

ferrijs_std/crypto/
mod.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4// Compile-time checks for conflicting crypto features
5#[cfg(all(feature = "crypto-rust", feature = "crypto-openssl"))]
6compile_error!("Features `crypto-rust` and `crypto-openssl` are mutually exclusive");
7
8#[cfg(all(feature = "crypto-rust", feature = "crypto-ring"))]
9compile_error!("Features `crypto-rust` and `crypto-ring` are mutually exclusive");
10
11#[cfg(all(feature = "crypto-rust", feature = "crypto-graviola"))]
12compile_error!("Features `crypto-rust` and `crypto-graviola` are mutually exclusive");
13
14#[cfg(all(feature = "crypto-openssl", feature = "crypto-ring"))]
15compile_error!("Features `crypto-openssl` and `crypto-ring` are mutually exclusive");
16
17#[cfg(all(feature = "crypto-openssl", feature = "crypto-graviola"))]
18compile_error!("Features `crypto-openssl` and `crypto-graviola` are mutually exclusive");
19
20#[cfg(all(feature = "crypto-ring", feature = "crypto-graviola"))]
21compile_error!("Features `crypto-ring` and `crypto-graviola` are mutually exclusive");
22
23mod crc32;
24mod hash;
25mod subtle;
26
27mod provider;
28
29use std::slice;
30
31use crate::buffer::Buffer;
32use crate::context::CtxExtension;
33use crate::encoding::{bytes_to_b64_string, bytes_to_hex_string};
34use crate::exceptions::DOMException;
35use crate::utils::{
36    bytes::{get_start_end_indexes, ObjectBytes},
37    error::ErrorExtensions,
38    error_messages::{ERROR_MSG_ARRAY_BUFFER_DETACHED, ERROR_MSG_NOT_ARRAY_BUFFER},
39    module::{export_default, ModuleInfo},
40    result::ResultExt,
41};
42use once_cell::sync::Lazy;
43use rand::RngExt;
44use rquickjs::prelude::Async;
45use rquickjs::{
46    atom::PredefinedAtom,
47    function::{Constructor, Opt},
48    module::{Declarations, Exports, ModuleDef},
49    prelude::{Func, Rest},
50    Class, Ctx, Error, Exception, Function, IntoJs, Null, Object, Result, Value,
51};
52pub use subtle::CryptoKey;
53use subtle::{
54    subtle_decapsulate_bits, subtle_decapsulate_key, subtle_decrypt, subtle_derive_bits,
55    subtle_derive_key, subtle_digest, subtle_encapsulate_bits, subtle_encapsulate_key,
56    subtle_encrypt, subtle_export_key, subtle_generate_key, subtle_import_key, subtle_sign,
57    subtle_unwrap_key, subtle_verify, subtle_wrap_key, SubtleCrypto,
58};
59
60use self::{
61    crc32::{Crc32, Crc32c},
62    hash::{Hash, HashAlgorithm, Hmac},
63};
64
65static CRYPTO_PROVIDER: Lazy<provider::DefaultProvider> =
66    Lazy::new(|| provider::DefaultProvider {});
67
68fn encoded_bytes<'js>(ctx: &Ctx<'js>, bytes: &[u8], encoding: &str) -> Result<Option<Value<'js>>> {
69    match encoding {
70        "hex" => {
71            let hex = bytes_to_hex_string(bytes);
72            let hex = rquickjs::String::from_str(ctx.clone(), &hex)?;
73            Ok(Some(Value::from_string(hex)))
74        },
75        "base64" => {
76            let b64 = bytes_to_b64_string(bytes);
77            let b64 = rquickjs::String::from_str(ctx.clone(), &b64)?;
78            Ok(Some(Value::from_string(b64)))
79        },
80        _ => Ok(None),
81    }
82}
83
84#[inline]
85pub fn random_byte_array(length: usize) -> Vec<u8> {
86    let mut vec = vec![0u8; length];
87    rand::rng().fill(&mut vec[..]);
88    vec
89}
90
91fn get_random_bytes(ctx: Ctx, length: usize) -> Result<Value> {
92    let random_bytes = random_byte_array(length);
93    Buffer(random_bytes).into_js(&ctx)
94}
95
96fn get_random_int(first: i64, second: Opt<i64>) -> Result<i64> {
97    let mut rng = rand::rng();
98    let random_number = match second.0 {
99        Some(max) => rng.random_range(first..max),
100        None => rng.random_range(0..first),
101    };
102
103    Ok(random_number)
104}
105
106fn random_fill<'js>(ctx: Ctx<'js>, obj: Object<'js>, args: Rest<Value<'js>>) -> Result<()> {
107    let args_iter = args.0.into_iter();
108    let mut args_iter = args_iter.rev();
109
110    let callback: Function = args_iter
111        .next()
112        .and_then(|v| v.into_function())
113        .or_throw_msg(&ctx, "Callback required")?;
114    let size = args_iter
115        .next()
116        .and_then(|arg| arg.as_int())
117        .map(|i| i as usize);
118    let offset = args_iter
119        .next()
120        .and_then(|arg| arg.as_int())
121        .map(|i| i as usize);
122
123    ctx.clone().spawn_exit(async move {
124        if let Err(err) = random_fill_sync(ctx.clone(), obj.clone(), Opt(offset), Opt(size)) {
125            let err = err.into_value(&ctx)?;
126            () = callback.call((err,))?;
127
128            return Ok(());
129        }
130        () = callback.call((Null.into_js(&ctx), obj))?;
131        Ok::<_, Error>(())
132    })?;
133    Ok(())
134}
135
136fn random_fill_sync<'js>(
137    ctx: Ctx<'js>,
138    obj: Object<'js>,
139    offset: Opt<usize>,
140    size: Opt<usize>,
141) -> Result<Object<'js>> {
142    let offset = offset.unwrap_or(0);
143
144    if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? {
145        let (array_buffer, source_length, source_offset) = object_bytes
146            .get_array_buffer()?
147            .expect(ERROR_MSG_NOT_ARRAY_BUFFER);
148        let raw = array_buffer
149            .as_raw()
150            .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED)
151            .or_throw(&ctx)?;
152
153        if offset > source_length {
154            return Err(Exception::throw_range(
155                &ctx,
156                "The value of \"offset\" is out of range",
157            ));
158        }
159        if let Some(size) = size.0 {
160            if offset + size > source_length {
161                return Err(Exception::throw_range(
162                    &ctx,
163                    "The value of \"size + offset\" is out of range",
164                ));
165            }
166        }
167
168        let (start, end) = get_start_end_indexes(source_length, size.0, offset);
169
170        // SAFETY: source_offset..+source_length stays in the backing buffer;
171        // start/end are clamped to it above.
172        let bytes = unsafe {
173            slice::from_raw_parts_mut(raw.cast::<u8>().as_ptr().add(source_offset), source_length)
174        };
175
176        rand::rng().fill(&mut bytes[start..end]);
177    }
178
179    Ok(obj)
180}
181
182fn get_random_values<'js>(ctx: Ctx<'js>, obj: Object<'js>) -> Result<Object<'js>> {
183    if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? {
184        if matches!(
185            object_bytes,
186            ObjectBytes::F64Array(_)
187                | ObjectBytes::F32Array(_)
188                | ObjectBytes::F16Array(_)
189                | ObjectBytes::DataView(_, _, _)
190        ) {
191            return Err(DOMException::type_mismatch_error(
192                &ctx,
193                "getRandomValues requires an integer TypedArray",
194            ));
195        }
196
197        let (array_buffer, source_length, source_offset) = object_bytes
198            .get_array_buffer()?
199            .expect(ERROR_MSG_NOT_ARRAY_BUFFER);
200        let raw = array_buffer
201            .as_raw()
202            .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED)
203            .or_throw(&ctx)?;
204
205        if source_length > 0x10000 {
206            return Err(DOMException::quota_exceeded_error(
207                &ctx,
208                "The requested length exceeds 65,536 bytes",
209            ));
210        }
211
212        let bytes = unsafe {
213            std::slice::from_raw_parts_mut(raw.cast::<u8>().as_ptr().add(source_offset), source_length)
214        };
215
216        rand::rng().fill(bytes)
217    }
218
219    Ok(obj)
220}
221
222fn uuidv4() -> String {
223    let uuid = rand::random::<u128>() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000;
224
225    static HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
226    let bytes = uuid.to_be_bytes();
227
228    let mut buf = [0u8; 36];
229
230    // Precomputed positions for 32 hex digits (excluding hyphens)
231    static HEX_POS: [usize; 32] = [
232        0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28,
233        29, 30, 31, 32, 33, 34, 35,
234    ];
235
236    // Map each byte to its hex representation
237    let mut hex_idx = 0;
238    for &byte in &bytes[..] {
239        let high = HEX_CHARS[(byte >> 4) as usize];
240        let low = HEX_CHARS[(byte & 0x0f) as usize];
241
242        buf[HEX_POS[hex_idx]] = high;
243        buf[HEX_POS[hex_idx + 1]] = low;
244        hex_idx += 2;
245    }
246
247    // Insert hyphens at standard positions
248    buf[8] = b'-';
249    buf[13] = b'-';
250    buf[18] = b'-';
251    buf[23] = b'-';
252
253    // SAFETY: The buffer only contains valid UTF-8 characters (hex digits and hyphens)
254    // that were explicitly set from the HEX_CHARS array and hyphen literals
255    unsafe { String::from_utf8_unchecked(buf.to_vec()) }
256}
257
258#[rquickjs::class]
259#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)]
260struct Crypto {}
261
262#[rquickjs::methods]
263impl Crypto {
264    #[qjs(constructor)]
265    pub fn new(ctx: Ctx<'_>) -> Result<Self> {
266        Err(Exception::throw_type(&ctx, "Illegal constructor"))
267    }
268
269    #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
270    pub fn to_string_tag() -> &'static str {
271        stringify!(Crypto)
272    }
273}
274
275pub fn init(ctx: &Ctx<'_>) -> Result<()> {
276    let globals = ctx.globals();
277
278    Class::<Crypto>::define(&globals)?;
279    let crypto = Class::instance(ctx.clone(), Crypto {})?;
280
281    crypto.set("createHash", Func::from(Hash::new))?;
282    crypto.set("createHmac", Func::from(Hmac::new))?;
283    crypto.set("randomBytes", Func::from(get_random_bytes))?;
284    crypto.set("randomInt", Func::from(get_random_int))?;
285    crypto.set("randomUUID", Func::from(uuidv4))?;
286    crypto.set("randomFillSync", Func::from(random_fill_sync))?;
287    crypto.set("randomFill", Func::from(random_fill))?;
288    crypto.set("getRandomValues", Func::from(get_random_values))?;
289
290    Class::<SubtleCrypto>::define(&globals)?;
291    Class::<CryptoKey>::define(&globals)?;
292
293    let subtle = Class::instance(ctx.clone(), SubtleCrypto {})?;
294    subtle.set(
295        "decapsulateBits",
296        Func::from(Async(subtle_decapsulate_bits)),
297    )?;
298    subtle.set("decapsulateKey", Func::from(Async(subtle_decapsulate_key)))?;
299    subtle.set("decrypt", Func::from(Async(subtle_decrypt)))?;
300    subtle.set("deriveKey", Func::from(Async(subtle_derive_key)))?;
301    subtle.set("deriveBits", Func::from(Async(subtle_derive_bits)))?;
302    subtle.set("digest", Func::from(Async(subtle_digest)))?;
303    subtle.set("encrypt", Func::from(Async(subtle_encrypt)))?;
304    subtle.set(
305        "encapsulateBits",
306        Func::from(Async(subtle_encapsulate_bits)),
307    )?;
308    subtle.set("encapsulateKey", Func::from(Async(subtle_encapsulate_key)))?;
309    subtle.set("exportKey", Func::from(Async(subtle_export_key)))?;
310    subtle.set("generateKey", Func::from(Async(subtle_generate_key)))?;
311    subtle.set("importKey", Func::from(Async(subtle_import_key)))?;
312    subtle.set("sign", Func::from(Async(subtle_sign)))?;
313    subtle.set("verify", Func::from(Async(subtle_verify)))?;
314    subtle.set("wrapKey", Func::from(Async(subtle_wrap_key)))?;
315    subtle.set("unwrapKey", Func::from(Async(subtle_unwrap_key)))?;
316    crypto.set("subtle", subtle)?;
317
318    globals.set("crypto", crypto)?;
319
320    Ok(())
321}
322
323pub struct CryptoModule;
324
325impl ModuleDef for CryptoModule {
326    fn declare(declare: &Declarations) -> Result<()> {
327        declare.declare("createHash")?;
328        declare.declare("createHmac")?;
329        declare.declare("Crc32")?;
330        declare.declare("Crc32c")?;
331        declare.declare("randomBytes")?;
332        declare.declare("randomUUID")?;
333        declare.declare("randomInt")?;
334        declare.declare("randomFillSync")?;
335        declare.declare("randomFill")?;
336        declare.declare("getRandomValues")?;
337
338        for algorithm in HashAlgorithm::iter() {
339            declare.declare(algorithm.class_name())?;
340        }
341
342        declare.declare("crypto")?;
343        declare.declare("webcrypto")?;
344        declare.declare("default")?;
345
346        Ok(())
347    }
348
349    fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> {
350        export_default(ctx, exports, |default| {
351            for algorithm in HashAlgorithm::iter() {
352                let class_name: &str = algorithm.class_name();
353                let algo_name = String::from(algorithm.as_str());
354
355                let ctor = Constructor::new_class::<Hash, _, _>(
356                    ctx.clone(),
357                    move |ctx: Ctx<'js>, secret: Opt<ObjectBytes<'js>>| match secret.0 {
358                        Some(secret) => Hash::new_hmac(ctx, algo_name.clone(), secret),
359                        None => Hash::new(ctx, algo_name.clone()),
360                    },
361                )?;
362
363                default.set(class_name, ctor)?;
364            }
365
366            let crypto: Object = ctx.globals().get("crypto")?;
367
368            Class::<Crc32>::define(default)?;
369            Class::<Crc32c>::define(default)?;
370
371            default.set("createHash", Func::from(Hash::new))?;
372            default.set("createHmac", Func::from(Hmac::new))?;
373            default.set("randomBytes", Func::from(get_random_bytes))?;
374            default.set("randomInt", Func::from(get_random_int))?;
375            default.set("randomUUID", Func::from(uuidv4))?;
376            default.set("randomFillSync", Func::from(random_fill_sync))?;
377            default.set("randomFill", Func::from(random_fill))?;
378            default.set("getRandomValues", Func::from(get_random_values))?;
379            default.set("crypto", crypto.clone())?;
380            default.set("webcrypto", crypto)?;
381            Ok(())
382        })?;
383
384        Ok(())
385    }
386}
387
388impl From<CryptoModule> for ModuleInfo<CryptoModule> {
389    fn from(val: CryptoModule) -> Self {
390        ModuleInfo {
391            name: "crypto",
392            module: val,
393        }
394    }
395}