deno_crypto 0.268.0

Web Cryptography API implementation for Deno
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
// Copyright 2018-2026 the Deno authors. MIT license.

//! Rust-side `constructKey()` analogue.
//!
//! Builds a `CryptoKey` cppgc instance plus the `{ cppgc: CryptoKeyHandle }`
//! handle wrapper object, the algorithm dictionary object, and the frozen
//! `usages` array, and stamps the `webidl.brand` / `kKeyObject` /
//! `hostObjectBrand` symbols the same way the legacy JS `constructKey()`
//! helper did. Used by every `SubtleCrypto` method (import/generate/derive
//! /wrap/unwrap/encapsulate/decapsulate/getPublicKey) now that those bodies
//! live in Rust.

use std::cell::RefCell;

use deno_core::cppgc::make_cppgc_object;
use deno_core::v8;

use crate::crypto_key::CryptoKey;
use crate::crypto_key::CryptoKeyType;
use crate::key_store::CryptoKeyHandle;
use crate::shared::RawKeyData;

/// Process-wide cache of the per-isolate symbols the JS `constructKey()`
/// helper used to stamp onto every `CryptoKey`. Populated on the first call
/// to [`stamp_symbols`]; reused on every subsequent call. These are
/// `Symbol.for(...)`-style well-known symbols (or, for `webidl.brand`,
/// fetched once from the loaded webidl ESM), so caching as `Global<Symbol>`
/// is safe across isolates because they always resolve to the same symbol
/// identity per isolate.
pub struct CryptoSymbols {
  pub webidl_brand: v8::Global<v8::Symbol>,
  pub k_key_object: v8::Global<v8::Symbol>,
  pub host_object_brand: v8::Global<v8::Symbol>,
}

thread_local! {
  // Owned (not `Box::leak`ed) so the three `v8::Global<v8::Symbol>` get
  // dropped when the thread exits, releasing the V8 globals before isolate
  // teardown.  Each worker (= each isolate = each thread) has its own
  // instance.
  static SYMBOLS: RefCell<Option<CryptoSymbols>> = const { RefCell::new(None) };
}

/// Register the three brand symbols that the JS `constructKey()` used to
/// stamp onto every `CryptoKey`. Called once per isolate from JS at module
/// init time (`op_crypto_install_symbols`). The symbols are stored in a
/// thread-local because they are isolate-specific but a `OpState` borrow is
/// not always reachable from inside the cppgc methods that need them (the
/// async dispatcher already holds it mutable).
pub fn set_symbols(symbols: CryptoSymbols) {
  SYMBOLS.with(|cell| {
    *cell.borrow_mut() = Some(symbols);
  });
}

/// Lend the registered symbols to `f`. Returns `None` if [`set_symbols`]
/// has not been called on this thread. The borrow scope is short — the
/// callback only needs scope-local `v8::Local`s built from the cached
/// `v8::Global<v8::Symbol>`s — so re-entrancy is not a concern.
pub fn with_symbols<F, R>(_scope: &mut v8::PinScope<'_, '_>, f: F) -> Option<R>
where
  F: FnOnce(&CryptoSymbols) -> R,
{
  SYMBOLS.with(|cell| cell.borrow().as_ref().map(f))
}

/// Per-algorithm dictionary slots that get baked into the `CryptoKey`'s
/// `algorithm` v8 object. The set of slots is intentionally minimal: the
/// WebCrypto spec mandates that only `name` is universal; the rest
/// (`length`, `hash`, `namedCurve`, `modulusLength`, `publicExponent`) are
/// per-algorithm.
#[derive(Default)]
pub struct AlgorithmDict {
  pub name: String,
  pub length: Option<u32>,
  pub hash_name: Option<String>,
  pub named_curve: Option<String>,
  pub modulus_length: Option<u32>,
  pub public_exponent: Option<Vec<u8>>,
}

impl AlgorithmDict {
  pub fn new(name: impl Into<String>) -> Self {
    AlgorithmDict {
      name: name.into(),
      ..Default::default()
    }
  }

  pub fn with_length(mut self, length: u32) -> Self {
    self.length = Some(length);
    self
  }

  pub fn with_hash(mut self, hash_name: impl Into<String>) -> Self {
    self.hash_name = Some(hash_name.into());
    self
  }

  pub fn with_named_curve(mut self, curve: impl Into<String>) -> Self {
    self.named_curve = Some(curve.into());
    self
  }

  pub fn with_modulus_length(mut self, modulus_length: u32) -> Self {
    self.modulus_length = Some(modulus_length);
    self
  }

  pub fn with_public_exponent(mut self, e: Vec<u8>) -> Self {
    self.public_exponent = Some(e);
    self
  }
}

pub fn build_algorithm_object<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  dict: &AlgorithmDict,
) -> v8::Local<'s, v8::Object> {
  let obj = v8::Object::new(scope);
  set_string(scope, obj, b"name", &dict.name);
  if let Some(length) = dict.length {
    set_u32(scope, obj, b"length", length);
  }
  if let Some(modulus_length) = dict.modulus_length {
    set_u32(scope, obj, b"modulusLength", modulus_length);
  }
  if let Some(ref hash_name) = dict.hash_name {
    let hash_obj = v8::Object::new(scope);
    set_string(scope, hash_obj, b"name", hash_name);
    hash_obj
      .set_integrity_level(scope, v8::IntegrityLevel::Frozen)
      .unwrap();
    let key = one_byte_internalized(scope, b"hash");
    obj.set(scope, key.into(), hash_obj.into());
  }
  if let Some(ref curve) = dict.named_curve {
    set_string(scope, obj, b"namedCurve", curve);
  }
  if let Some(ref pe) = dict.public_exponent {
    let backing = if pe.is_empty() {
      v8::ArrayBuffer::new(scope, 0)
    } else {
      let bs = v8::ArrayBuffer::new_backing_store_from_bytes(
        pe.clone().into_boxed_slice(),
      )
      .make_shared();
      v8::ArrayBuffer::with_backing_store(scope, &bs)
    };
    let u8 = v8::Uint8Array::new(scope, backing, 0, pe.len()).unwrap();
    let key = one_byte_internalized(scope, b"publicExponent");
    obj.set(scope, key.into(), u8.into());
  }
  obj
    .set_integrity_level(scope, v8::IntegrityLevel::Frozen)
    .unwrap();
  obj
}

pub fn build_usages_array<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  usages: &[&str],
) -> v8::Local<'s, v8::Array> {
  let len = usages.len();
  let arr = v8::Array::new(scope, len as i32);
  for (i, u) in usages.iter().enumerate() {
    let s = v8::String::new(scope, u).unwrap();
    arr.set_index(scope, i as u32, s.into());
  }
  let obj: v8::Local<v8::Object> = arr.into();
  obj
    .set_integrity_level(scope, v8::IntegrityLevel::Frozen)
    .unwrap();
  arr
}

pub fn build_handle_object<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  data: RawKeyData,
) -> v8::Local<'s, v8::Object> {
  let handle = make_cppgc_object(scope, CryptoKeyHandle::from_raw(data));
  let wrapper = v8::Object::new(scope);
  let key = one_byte_internalized(scope, b"cppgc");
  wrapper.set(scope, key.into(), handle.into());
  wrapper
}

/// Construct a fully-stamped CryptoKey cppgc instance reachable from JS.
/// Mirrors the legacy JS `constructKey` helper.
#[allow(
  clippy::too_many_arguments,
  reason = "dictionary-style key construction"
)]
pub fn make_crypto_key<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  key_type: CryptoKeyType,
  extractable: bool,
  usages: &[&str],
  alg: AlgorithmDict,
  data: RawKeyData,
) -> v8::Local<'s, v8::Object> {
  let key_data_jsval = key_data_to_jsval(scope, &data);
  let host_object_snapshot = build_host_object_snapshot(
    scope,
    key_type,
    extractable,
    usages,
    &alg,
    &data,
  );
  let handle = build_handle_object(scope, data);
  let algorithm_obj = build_algorithm_object(scope, &alg);
  let usages_arr = build_usages_array(scope, usages);

  let crypto_key = CryptoKey::from_parts(
    scope,
    key_type,
    extractable,
    usages_arr.into(),
    algorithm_obj.into(),
    handle.into(),
  );
  let obj = make_cppgc_object(scope, crypto_key);
  stamp_symbols(scope, obj, key_data_jsval, host_object_snapshot);
  obj
}

fn stamp_symbols<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  key: v8::Local<'s, v8::Object>,
  k_key_object_val: v8::Local<'s, v8::Value>,
  host_object_snapshot: v8::Local<'s, v8::Value>,
) {
  // Clone the three cached `v8::Global<v8::Symbol>`s out while we hold the
  // thread-local borrow, then materialize them as `v8::Local`s once the
  // borrow has been released (`v8::Local::new` needs `scope`, which we
  // can't lend through the closure because the closure can't capture
  // `scope` while the with-borrow is active).
  let Some((brand_g, k_key_object_g, host_brand_g)) =
    with_symbols(scope, |syms| {
      (
        syms.webidl_brand.clone(),
        syms.k_key_object.clone(),
        syms.host_object_brand.clone(),
      )
    })
  else {
    return;
  };
  let brand = v8::Local::new(scope, &brand_g);
  let k_key_object = v8::Local::new(scope, &k_key_object_g);
  let host_brand_sym = v8::Local::new(scope, &host_brand_g);
  // The brand symbols must be non-enumerable to match the legacy JS
  // `ObjectDefineProperty(key, sym, { value: ... })` shape. Without
  // `DONT_ENUM` the cppgc instance exposes them as plain own properties,
  // which breaks `assert.deepStrictEqual` of two distinct keys with the
  // same material (test/parallel/test-assert-deep.js Crypto subtest) and
  // shows up as `[Symbol(...)]` slots in `Deno.inspect()` output.
  let _ = key.define_own_property(
    scope,
    brand.into(),
    brand.into(),
    v8::PropertyAttribute::DONT_ENUM,
  );

  let _ = key.define_own_property(
    scope,
    k_key_object.into(),
    k_key_object_val,
    v8::PropertyAttribute::DONT_ENUM,
  );

  // The hostObjectBrand is a function-valued property that the
  // structured-clone serializer calls; replicate the legacy JS shape.
  // The legacy JS used `ObjectDefineProperty(key, hostObjectBrand, { value:
  // () => snapshot })` -- a closure-bound function. From Rust the same
  // shape is built via a FunctionTemplate whose `data` slot carries the
  // snapshot and whose body returns it.
  let ft = v8::FunctionTemplate::builder(host_object_thunk)
    .data(host_object_snapshot)
    .build(scope);
  let host_fn = ft.get_function(scope).unwrap();
  let _ = key.define_own_property(
    scope,
    host_brand_sym.into(),
    host_fn.into(),
    v8::PropertyAttribute::DONT_ENUM,
  );
}

fn host_object_thunk(
  _scope: &mut v8::PinScope,
  args: v8::FunctionCallbackArguments,
  mut rv: v8::ReturnValue,
) {
  rv.set(args.data());
}

/// Build the `{ type: "CryptoKey", keyType, extractable, usages, algorithm,
/// keyData }` snapshot the legacy `hostObjectBrand` getter returned.
fn build_host_object_snapshot<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  key_type: CryptoKeyType,
  extractable: bool,
  usages: &[&str],
  alg: &AlgorithmDict,
  data: &RawKeyData,
) -> v8::Local<'s, v8::Value> {
  let obj = v8::Object::new(scope);
  let type_key = one_byte_internalized(scope, b"type");
  let type_val = v8::String::new(scope, "CryptoKey").unwrap();
  obj.set(scope, type_key.into(), type_val.into());

  let key_type_key = one_byte_internalized(scope, b"keyType");
  let key_type_val = v8::String::new(scope, key_type_str(key_type)).unwrap();
  obj.set(scope, key_type_key.into(), key_type_val.into());

  let ext_key = one_byte_internalized(scope, b"extractable");
  let ext_val = v8::Boolean::new(scope, extractable);
  obj.set(scope, ext_key.into(), ext_val.into());

  let usages_key = one_byte_internalized(scope, b"usages");
  let usages_val = build_usages_array(scope, usages);
  obj.set(scope, usages_key.into(), usages_val.into());

  let alg_key = one_byte_internalized(scope, b"algorithm");
  let alg_val = build_algorithm_object(scope, alg);
  obj.set(scope, alg_key.into(), alg_val.into());

  let kd_key = one_byte_internalized(scope, b"keyData");
  let kd_val = key_data_to_jsval(scope, data);
  obj.set(scope, kd_key.into(), kd_val);

  obj.into()
}

fn key_type_str(t: CryptoKeyType) -> &'static str {
  match t {
    CryptoKeyType::Public => "public",
    CryptoKeyType::Private => "private",
    CryptoKeyType::Secret => "secret",
  }
}

/// Reconstruct the `getKeyData(handle)` JS shape from raw key data.
/// `RawKeyData::Raw` returns a bare `Uint8Array`; `Secret`/`Private`/
/// `Public` return `{ type, data }`; `SeededPrivate` returns
/// `{ seed, privateKey }`.
fn key_data_to_jsval<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  data: &RawKeyData,
) -> v8::Local<'s, v8::Value> {
  fn u8a<'s>(
    scope: &mut v8::PinScope<'s, '_>,
    bytes: &[u8],
  ) -> v8::Local<'s, v8::Uint8Array> {
    let backing = if bytes.is_empty() {
      v8::ArrayBuffer::new(scope, 0)
    } else {
      let bs = v8::ArrayBuffer::new_backing_store_from_bytes(
        bytes.to_vec().into_boxed_slice(),
      )
      .make_shared();
      v8::ArrayBuffer::with_backing_store(scope, &bs)
    };
    v8::Uint8Array::new(scope, backing, 0, bytes.len()).unwrap()
  }
  match data {
    RawKeyData::Raw(b) => u8a(scope, b).into(),
    RawKeyData::Secret(b) => tagged(scope, "secret", b),
    RawKeyData::Private(b) => tagged(scope, "private", b),
    RawKeyData::Public(b) => tagged(scope, "public", b),
    RawKeyData::SeededPrivate { seed, private_key } => {
      let obj = v8::Object::new(scope);
      let pk_key = one_byte_internalized(scope, b"privateKey");
      let pk_arr = u8a(scope, private_key);
      obj.set(scope, pk_key.into(), pk_arr.into());
      if let Some(seed) = seed {
        let seed_key = one_byte_internalized(scope, b"seed");
        let seed_arr = u8a(scope, seed);
        obj.set(scope, seed_key.into(), seed_arr.into());
      }
      obj.into()
    }
  }
}

fn tagged<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  kind: &str,
  bytes: &[u8],
) -> v8::Local<'s, v8::Value> {
  let obj = v8::Object::new(scope);
  let type_key = one_byte_internalized(scope, b"type");
  let type_val = v8::String::new(scope, kind).unwrap();
  obj.set(scope, type_key.into(), type_val.into());
  let data_key = one_byte_internalized(scope, b"data");
  let backing = if bytes.is_empty() {
    v8::ArrayBuffer::new(scope, 0)
  } else {
    let bs = v8::ArrayBuffer::new_backing_store_from_bytes(
      bytes.to_vec().into_boxed_slice(),
    )
    .make_shared();
    v8::ArrayBuffer::with_backing_store(scope, &bs)
  };
  let data_arr = v8::Uint8Array::new(scope, backing, 0, bytes.len()).unwrap();
  obj.set(scope, data_key.into(), data_arr.into());
  obj.into()
}

fn set_string<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  obj: v8::Local<'s, v8::Object>,
  field: &[u8],
  value: &str,
) {
  let k = one_byte_internalized(scope, field);
  let v = v8::String::new(scope, value).unwrap();
  obj.set(scope, k.into(), v.into());
}

fn set_u32<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  obj: v8::Local<'s, v8::Object>,
  field: &[u8],
  value: u32,
) {
  let k = one_byte_internalized(scope, field);
  let n = v8::Number::new(scope, value as f64);
  obj.set(scope, k.into(), n.into());
}

fn one_byte_internalized<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  bytes: &[u8],
) -> v8::Local<'s, v8::String> {
  v8::String::new_from_one_byte(scope, bytes, v8::NewStringType::Internalized)
    .unwrap()
}

/// Body of the `Crypto.registerSymbols(webidlBrand, kKeyObject)` static
/// method. Called once during module load to hand the WebIDL brand symbol
/// (private to the webidl ESM) and the node:crypto `kKeyObject` symbol
/// (private to ext/node) over to the crypto cppgc methods, which need them
/// to brand every freshly-constructed `CryptoKey`. Lives here as a Rust
/// function so it can be reused from a `#[static_method]` on either
/// `Crypto` or `SubtleCrypto` without introducing a new standalone op.
pub fn register_symbols<'s>(
  scope: &mut v8::PinScope<'s, '_>,
  webidl_brand: v8::Local<'s, v8::Value>,
  k_key_object: v8::Local<'s, v8::Value>,
) -> bool {
  let Ok(webidl_brand) = v8::Local::<v8::Symbol>::try_from(webidl_brand) else {
    return false;
  };
  let Ok(k_key_object) = v8::Local::<v8::Symbol>::try_from(k_key_object) else {
    return false;
  };
  let host_obj = {
    let name = v8::String::new(scope, "Deno.core.hostObject").unwrap();
    v8::Symbol::for_key(scope, name)
  };
  set_symbols(CryptoSymbols {
    webidl_brand: v8::Global::new(scope, webidl_brand),
    k_key_object: v8::Global::new(scope, k_key_object),
    host_object_brand: v8::Global::new(scope, host_obj),
  });
  true
}