mirror of
https://github.com/denoland/deno.git
synced 2024-11-28 16:20:57 -05:00
fix(ext/node): rewrite X509Certificate resource and add publicKey()
(#24988)
**Changes**: - Remove unsafe usage, rewrite Rust representation with `yoke`. - Implement `X509Certificate.prototype.publicKey()` Fixes https://github.com/denoland/deno/issues/23307
This commit is contained in:
parent
d6f662ac82
commit
b61fd622a5
6 changed files with 168 additions and 38 deletions
42
Cargo.lock
generated
42
Cargo.lock
generated
|
@ -256,7 +256,7 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
"synstructure",
|
"synstructure 0.12.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1831,6 +1831,7 @@ dependencies = [
|
||||||
"simd-json",
|
"simd-json",
|
||||||
"sm3",
|
"sm3",
|
||||||
"spki",
|
"spki",
|
||||||
|
"stable_deref_trait",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
|
@ -7059,6 +7060,17 @@ dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synstructure"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syntect"
|
name = "syntect"
|
||||||
version = "5.2.0"
|
version = "5.2.0"
|
||||||
|
@ -8517,9 +8529,22 @@ checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
|
"yoke-derive",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke-derive"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
"synstructure 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.32"
|
version = "0.7.32"
|
||||||
|
@ -8546,6 +8571,21 @@ name = "zerofrom"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
|
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
|
||||||
|
dependencies = [
|
||||||
|
"zerofrom-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom-derive"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.72",
|
||||||
|
"synstructure 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zeroize"
|
name = "zeroize"
|
||||||
|
|
|
@ -90,13 +90,14 @@ signature.workspace = true
|
||||||
simd-json = "0.13.4"
|
simd-json = "0.13.4"
|
||||||
sm3 = "0.4.2"
|
sm3 = "0.4.2"
|
||||||
spki.workspace = true
|
spki.workspace = true
|
||||||
|
stable_deref_trait = "1.2.0"
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
winapi.workspace = true
|
winapi.workspace = true
|
||||||
x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
|
x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
|
||||||
x509-parser = "0.15.0"
|
x509-parser = "0.15.0"
|
||||||
yoke = "0.7.4"
|
yoke = { version = "0.7.4", features = ["derive"] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys.workspace = true
|
windows-sys.workspace = true
|
||||||
|
|
|
@ -311,6 +311,7 @@ deno_core::extension!(deno_node,
|
||||||
ops::crypto::x509::op_node_x509_get_valid_to,
|
ops::crypto::x509::op_node_x509_get_valid_to,
|
||||||
ops::crypto::x509::op_node_x509_get_serial_number,
|
ops::crypto::x509::op_node_x509_get_serial_number,
|
||||||
ops::crypto::x509::op_node_x509_key_usage,
|
ops::crypto::x509::op_node_x509_key_usage,
|
||||||
|
ops::crypto::x509::op_node_x509_public_key,
|
||||||
ops::fs::op_node_fs_exists_sync<P>,
|
ops::fs::op_node_fs_exists_sync<P>,
|
||||||
ops::fs::op_node_fs_exists<P>,
|
ops::fs::op_node_fs_exists<P>,
|
||||||
ops::fs::op_node_cp_sync<P>,
|
ops::fs::op_node_cp_sync<P>,
|
||||||
|
|
|
@ -45,6 +45,7 @@ use spki::der::Reader as _;
|
||||||
use spki::DecodePublicKey as _;
|
use spki::DecodePublicKey as _;
|
||||||
use spki::EncodePublicKey as _;
|
use spki::EncodePublicKey as _;
|
||||||
use spki::SubjectPublicKeyInfoRef;
|
use spki::SubjectPublicKeyInfoRef;
|
||||||
|
use x509_parser::x509;
|
||||||
|
|
||||||
use super::dh;
|
use super::dh;
|
||||||
use super::dh::DiffieHellmanGroup;
|
use super::dh::DiffieHellmanGroup;
|
||||||
|
@ -518,6 +519,58 @@ impl KeyObjectHandle {
|
||||||
Ok(KeyObjectHandle::AsymmetricPrivate(private_key))
|
Ok(KeyObjectHandle::AsymmetricPrivate(private_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_x509_public_key(
|
||||||
|
spki: &x509::SubjectPublicKeyInfo,
|
||||||
|
) -> Result<KeyObjectHandle, AnyError> {
|
||||||
|
use x509_parser::der_parser::asn1_rs::oid;
|
||||||
|
use x509_parser::public_key::PublicKey;
|
||||||
|
|
||||||
|
let key = match spki.parsed()? {
|
||||||
|
PublicKey::RSA(key) => {
|
||||||
|
let public_key = RsaPublicKey::new(
|
||||||
|
rsa::BigUint::from_bytes_be(key.modulus),
|
||||||
|
rsa::BigUint::from_bytes_be(key.exponent),
|
||||||
|
)?;
|
||||||
|
AsymmetricPublicKey::Rsa(public_key)
|
||||||
|
}
|
||||||
|
PublicKey::EC(point) => {
|
||||||
|
let data = point.data();
|
||||||
|
if let Some(params) = &spki.algorithm.parameters {
|
||||||
|
let curve_oid = params.as_oid()?;
|
||||||
|
const ID_SECP224R1: &[u8] = &oid!(raw 1.3.132.0.33);
|
||||||
|
const ID_SECP256R1: &[u8] = &oid!(raw 1.2.840.10045.3.1.7);
|
||||||
|
const ID_SECP384R1: &[u8] = &oid!(raw 1.3.132.0.34);
|
||||||
|
|
||||||
|
match curve_oid.as_bytes() {
|
||||||
|
ID_SECP224R1 => {
|
||||||
|
let public_key = p224::PublicKey::from_sec1_bytes(data)?;
|
||||||
|
AsymmetricPublicKey::Ec(EcPublicKey::P224(public_key))
|
||||||
|
}
|
||||||
|
ID_SECP256R1 => {
|
||||||
|
let public_key = p256::PublicKey::from_sec1_bytes(data)?;
|
||||||
|
AsymmetricPublicKey::Ec(EcPublicKey::P256(public_key))
|
||||||
|
}
|
||||||
|
ID_SECP384R1 => {
|
||||||
|
let public_key = p384::PublicKey::from_sec1_bytes(data)?;
|
||||||
|
AsymmetricPublicKey::Ec(EcPublicKey::P384(public_key))
|
||||||
|
}
|
||||||
|
_ => return Err(type_error("unsupported ec named curve")),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(type_error("missing ec parameters"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PublicKey::DSA(_) => {
|
||||||
|
let verifying_key = dsa::VerifyingKey::from_public_key_der(spki.raw)
|
||||||
|
.map_err(|_| type_error("malformed DSS public key"))?;
|
||||||
|
AsymmetricPublicKey::Dsa(verifying_key)
|
||||||
|
}
|
||||||
|
_ => return Err(type_error("unsupported x509 public key type")),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(KeyObjectHandle::AsymmetricPublic(key))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_asymmetric_public_key_from_js(
|
pub fn new_asymmetric_public_key_from_js(
|
||||||
key: &[u8],
|
key: &[u8],
|
||||||
format: &str,
|
format: &str,
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use deno_core::v8;
|
|
||||||
|
|
||||||
use x509_parser::der_parser::asn1_rs::Any;
|
use x509_parser::der_parser::asn1_rs::Any;
|
||||||
use x509_parser::der_parser::asn1_rs::Tag;
|
use x509_parser::der_parser::asn1_rs::Tag;
|
||||||
|
@ -11,19 +10,33 @@ use x509_parser::extensions;
|
||||||
use x509_parser::pem;
|
use x509_parser::pem;
|
||||||
use x509_parser::prelude::*;
|
use x509_parser::prelude::*;
|
||||||
|
|
||||||
|
use super::KeyObjectHandle;
|
||||||
|
|
||||||
|
use std::ops::Deref;
|
||||||
|
use yoke::Yoke;
|
||||||
|
use yoke::Yokeable;
|
||||||
|
|
||||||
use digest::Digest;
|
use digest::Digest;
|
||||||
|
|
||||||
|
enum CertificateSources {
|
||||||
|
Der(Box<[u8]>),
|
||||||
|
Pem(pem::Pem),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Yokeable)]
|
||||||
|
struct CertificateView<'a> {
|
||||||
|
cert: X509Certificate<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct Certificate {
|
pub(crate) struct Certificate {
|
||||||
_buf: Vec<u8>,
|
inner: Yoke<CertificateView<'static>, Box<CertificateSources>>,
|
||||||
pem: Option<pem::Pem>,
|
|
||||||
cert: X509Certificate<'static>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl deno_core::GarbageCollected for Certificate {}
|
impl deno_core::GarbageCollected for Certificate {}
|
||||||
|
|
||||||
impl Certificate {
|
impl Certificate {
|
||||||
fn fingerprint<D: Digest>(&self) -> Option<String> {
|
fn fingerprint<D: Digest>(&self) -> Option<String> {
|
||||||
self.pem.as_ref().map(|pem| {
|
if let CertificateSources::Pem(pem) = self.inner.backing_cart().as_ref() {
|
||||||
let mut hasher = D::new();
|
let mut hasher = D::new();
|
||||||
hasher.update(&pem.contents);
|
hasher.update(&pem.contents);
|
||||||
let bytes = hasher.finalize();
|
let bytes = hasher.finalize();
|
||||||
|
@ -33,13 +46,15 @@ impl Certificate {
|
||||||
hex.push_str(&format!("{:02X}:", byte));
|
hex.push_str(&format!("{:02X}:", byte));
|
||||||
}
|
}
|
||||||
hex.pop();
|
hex.pop();
|
||||||
hex
|
Some(hex)
|
||||||
})
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for Certificate {
|
impl<'a> Deref for CertificateView<'a> {
|
||||||
type Target = X509Certificate<'static>;
|
type Target = X509Certificate<'a>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.cert
|
&self.cert
|
||||||
|
@ -47,36 +62,35 @@ impl std::ops::Deref for Certificate {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
pub fn op_node_x509_parse<'s>(
|
#[cppgc]
|
||||||
scope: &'s mut v8::HandleScope,
|
pub fn op_node_x509_parse(
|
||||||
#[buffer] buf: &[u8],
|
#[buffer] buf: &[u8],
|
||||||
) -> Result<v8::Local<'s, v8::Object>, AnyError> {
|
) -> Result<Certificate, AnyError> {
|
||||||
let pem = match pem::parse_x509_pem(buf) {
|
let source = match pem::parse_x509_pem(buf) {
|
||||||
Ok((_, pem)) => Some(pem),
|
Ok((_, pem)) => CertificateSources::Pem(pem),
|
||||||
Err(_) => None,
|
Err(_) => CertificateSources::Der(buf.to_vec().into_boxed_slice()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cert = pem
|
let inner =
|
||||||
.as_ref()
|
Yoke::<CertificateView<'static>, Box<CertificateSources>>::try_attach_to_cart(
|
||||||
.map(|pem| pem.parse_x509())
|
Box::new(source),
|
||||||
.unwrap_or_else(|| X509Certificate::from_der(buf).map(|(_, cert)| cert))?;
|
|source| {
|
||||||
|
let cert = match source {
|
||||||
let cert = Certificate {
|
CertificateSources::Pem(pem) => pem.parse_x509()?,
|
||||||
_buf: buf.to_vec(),
|
CertificateSources::Der(buf) => {
|
||||||
// SAFETY: Extending the lifetime of the certificate. Backing buffer is
|
X509Certificate::from_der(buf).map(|(_, cert)| cert)?
|
||||||
// owned by the resource.
|
}
|
||||||
cert: unsafe {
|
};
|
||||||
std::mem::transmute::<X509Certificate<'_>, X509Certificate<'_>>(cert)
|
Ok::<_, AnyError>(CertificateView { cert })
|
||||||
},
|
},
|
||||||
pem,
|
)?;
|
||||||
};
|
|
||||||
|
|
||||||
let obj = deno_core::cppgc::make_cppgc_object(scope, cert);
|
Ok(Certificate { inner })
|
||||||
Ok(obj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2(fast)]
|
#[op2(fast)]
|
||||||
pub fn op_node_x509_ca(#[cppgc] cert: &Certificate) -> Result<bool, AnyError> {
|
pub fn op_node_x509_ca(#[cppgc] cert: &Certificate) -> Result<bool, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
Ok(cert.is_ca())
|
Ok(cert.is_ca())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +99,7 @@ pub fn op_node_x509_check_email(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
#[string] email: &str,
|
#[string] email: &str,
|
||||||
) -> Result<bool, AnyError> {
|
) -> Result<bool, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
let subject = cert.subject();
|
let subject = cert.subject();
|
||||||
if subject
|
if subject
|
||||||
.iter_email()
|
.iter_email()
|
||||||
|
@ -144,6 +159,7 @@ pub fn op_node_x509_fingerprint512(
|
||||||
pub fn op_node_x509_get_issuer(
|
pub fn op_node_x509_get_issuer(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
) -> Result<String, AnyError> {
|
) -> Result<String, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
Ok(x509name_to_string(cert.issuer(), oid_registry())?)
|
Ok(x509name_to_string(cert.issuer(), oid_registry())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,9 +168,21 @@ pub fn op_node_x509_get_issuer(
|
||||||
pub fn op_node_x509_get_subject(
|
pub fn op_node_x509_get_subject(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
) -> Result<String, AnyError> {
|
) -> Result<String, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
Ok(x509name_to_string(cert.subject(), oid_registry())?)
|
Ok(x509name_to_string(cert.subject(), oid_registry())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[op2]
|
||||||
|
#[cppgc]
|
||||||
|
pub fn op_node_x509_public_key(
|
||||||
|
#[cppgc] cert: &Certificate,
|
||||||
|
) -> Result<KeyObjectHandle, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
|
let public_key = &cert.tbs_certificate.subject_pki;
|
||||||
|
|
||||||
|
KeyObjectHandle::new_x509_public_key(public_key)
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt to convert attribute to string. If type is not a string, return value is the hex
|
// Attempt to convert attribute to string. If type is not a string, return value is the hex
|
||||||
// encoding of the attribute value
|
// encoding of the attribute value
|
||||||
fn attribute_value_to_string(
|
fn attribute_value_to_string(
|
||||||
|
@ -220,6 +248,7 @@ fn x509name_to_string(
|
||||||
pub fn op_node_x509_get_valid_from(
|
pub fn op_node_x509_get_valid_from(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
) -> Result<String, AnyError> {
|
) -> Result<String, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
Ok(cert.validity().not_before.to_string())
|
Ok(cert.validity().not_before.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,6 +257,7 @@ pub fn op_node_x509_get_valid_from(
|
||||||
pub fn op_node_x509_get_valid_to(
|
pub fn op_node_x509_get_valid_to(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
) -> Result<String, AnyError> {
|
) -> Result<String, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
Ok(cert.validity().not_after.to_string())
|
Ok(cert.validity().not_after.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +266,7 @@ pub fn op_node_x509_get_valid_to(
|
||||||
pub fn op_node_x509_get_serial_number(
|
pub fn op_node_x509_get_serial_number(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
) -> Result<String, AnyError> {
|
) -> Result<String, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
let mut s = cert.serial.to_str_radix(16);
|
let mut s = cert.serial.to_str_radix(16);
|
||||||
s.make_ascii_uppercase();
|
s.make_ascii_uppercase();
|
||||||
Ok(s)
|
Ok(s)
|
||||||
|
@ -245,6 +276,7 @@ pub fn op_node_x509_get_serial_number(
|
||||||
pub fn op_node_x509_key_usage(
|
pub fn op_node_x509_key_usage(
|
||||||
#[cppgc] cert: &Certificate,
|
#[cppgc] cert: &Certificate,
|
||||||
) -> Result<u16, AnyError> {
|
) -> Result<u16, AnyError> {
|
||||||
|
let cert = cert.inner.get().deref();
|
||||||
let key_usage = cert
|
let key_usage = cert
|
||||||
.extensions()
|
.extensions()
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -17,9 +17,13 @@ import {
|
||||||
op_node_x509_get_valid_to,
|
op_node_x509_get_valid_to,
|
||||||
op_node_x509_key_usage,
|
op_node_x509_key_usage,
|
||||||
op_node_x509_parse,
|
op_node_x509_parse,
|
||||||
|
op_node_x509_public_key,
|
||||||
} from "ext:core/ops";
|
} from "ext:core/ops";
|
||||||
|
|
||||||
import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts";
|
import {
|
||||||
|
KeyObject,
|
||||||
|
PublicKeyObject,
|
||||||
|
} from "ext:deno_node/internal/crypto/keys.ts";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts";
|
import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts";
|
||||||
import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts";
|
import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts";
|
||||||
|
@ -144,10 +148,9 @@ export class X509Certificate {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
get publicKey(): KeyObject {
|
get publicKey(): PublicKeyObject {
|
||||||
notImplemented("crypto.X509Certificate.prototype.publicKey");
|
const handle = op_node_x509_public_key(this.#handle);
|
||||||
|
return new PublicKeyObject(handle);
|
||||||
return {} as KeyObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw(): Buffer {
|
get raw(): Buffer {
|
||||||
|
|
Loading…
Reference in a new issue