keyhive_wasm 0.3.0

WebAssembly wrappers for keyhive_core
Documentation
use super::{signed::JsSigned, signing_error::JsSigningError};
use keyhive_crypto::{
    signed::SigningError, signer::async_signer::AsyncSigner, verifiable::Verifiable,
};
use thiserror::Error;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;

#[derive(Debug, Clone)]
#[wasm_bindgen(js_name = Signer)]
pub struct JsSigner(pub(crate) JsSignerOptions);

#[wasm_bindgen(js_class = Signer)]
impl JsSigner {
    #[cfg(feature = "web-sys")]
    #[wasm_bindgen]
    pub async fn generate() -> Self {
        Self::generate_web_crypto()
            .await
            .unwrap_or_else(|_| Self::generate_memory())
    }

    #[cfg(not(feature = "web-sys"))]
    #[wasm_bindgen(constructor, js_name = generate)]
    pub async fn generate() -> Self {
        Self::generate_memory()
    }

    #[wasm_bindgen(js_name = generateMemory)]
    pub fn generate_memory() -> Self {
        JsSigner(JsSignerOptions::Memory(
            ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()),
        ))
    }

    #[cfg(feature = "web-sys")]
    #[wasm_bindgen(js_name = generateWebCrypto)]
    pub async fn generate_web_crypto() -> Result<Self, JsGenerateWebCryptoError> {
        use js_sys::Reflect;
        use web_sys::Crypto;

        let crypto_obj = Reflect::get(&js_sys::global(), &"crypto".into())
            .map_err(|_| GenerateWebCryptoError::NoCrypto)?;
        let crypto = crypto_obj
            .dyn_into::<Crypto>()
            .map_err(|_| GenerateWebCryptoError::CryptoNotCrypto)?;
        let subtle = crypto.subtle();

        let usages: Vec<js_sys::JsString> =
            vec!["sign".to_string().into(), "verify".to_string().into()];

        let fut: JsFuture = subtle
            .generate_key_with_str("Ed25519", false, &usages.into())
            .map_err(GenerateWebCryptoError::JsError)?
            .into();

        let keypair: web_sys::CryptoKeyPair =
            fut.await.map_err(GenerateWebCryptoError::JsError)?.into();

        let pk_buf_fut: JsFuture = subtle
            .export_key("raw", &keypair.get_public_key())
            .map_err(GenerateWebCryptoError::JsError)?
            .into();
        let pk_buf: js_sys::ArrayBuffer = pk_buf_fut
            .await
            .map_err(GenerateWebCryptoError::JsError)?
            .into();
        let pk_bytes: Vec<u8> = js_sys::Uint8Array::new(&pk_buf).to_vec();

        Ok(JsSigner(JsSignerOptions::WebCrypto {
            verifying_key: ed25519_dalek::VerifyingKey::try_from(pk_bytes.as_slice())
                .map_err(|_| GenerateWebCryptoError::ParseVerifyingKeyError)?,
            signing_key: keypair.get_private_key(),
        }))
    }

    #[wasm_bindgen(js_name = memorySignerFromBytes)]
    pub fn memory_signer_from_bytes(bytes: &[u8]) -> Result<Self, CannotParseEd25519SigningKey> {
        let arr: [u8; 32] = bytes
            .to_vec()
            .try_into()
            .map_err(|_| CannotParseEd25519SigningKey)?;

        Ok(JsSigner(JsSignerOptions::Memory(
            ed25519_dalek::SigningKey::from_bytes(&arr),
        )))
    }

    #[cfg(feature = "web-sys")]
    #[wasm_bindgen(js_name = webCryptoSigner)]
    pub async fn webcrypto_signer(
        keypair: web_sys::CryptoKeyPair,
    ) -> Result<Self, JsGenerateWebCryptoError> {
        use js_sys::Reflect;
        use web_sys::Crypto;

        let crypto_obj = Reflect::get(&js_sys::global(), &"crypto".into())
            .map_err(|_| GenerateWebCryptoError::NoCrypto)?;
        let crypto = crypto_obj
            .dyn_into::<Crypto>()
            .map_err(|_| GenerateWebCryptoError::CryptoNotCrypto)?;
        let subtle = crypto.subtle();

        let pk_buf_fut: JsFuture = subtle
            .export_key("raw", &keypair.get_public_key())
            .map_err(GenerateWebCryptoError::JsError)?
            .into();
        let pk_buf: js_sys::ArrayBuffer = pk_buf_fut
            .await
            .map_err(GenerateWebCryptoError::JsError)?
            .into();
        let pk_bytes: Vec<u8> = js_sys::Uint8Array::new(&pk_buf).to_vec();

        Ok(JsSigner(JsSignerOptions::WebCrypto {
            verifying_key: ed25519_dalek::VerifyingKey::try_from(pk_bytes.as_slice())
                .map_err(|_| GenerateWebCryptoError::ParseVerifyingKeyError)?,
            signing_key: keypair.get_private_key(),
        }))
    }

    #[wasm_bindgen(getter, js_name = variant)]
    pub fn variant(&self) -> String {
        match &self.0 {
            JsSignerOptions::Memory(_) => "MEMORY".to_string(),

            #[cfg(feature = "web-sys")]
            JsSignerOptions::WebCrypto { .. } => "WEB_CRYPTO".to_string(),
        }
    }

    #[wasm_bindgen(js_name = trySign)]
    pub async fn try_sign(&self, bytes: &[u8]) -> Result<JsSigned, JsSigningError> {
        let signed = self.try_sign_async(bytes.to_vec()).await?;
        Ok(JsSigned(signed))
    }

    #[wasm_bindgen(getter, js_name = verifyingKey)]
    pub fn verifying_key(&self) -> Box<[u8]> {
        Box::new(self.0.verifying_key().to_bytes())
    }

    #[wasm_bindgen(js_name = clone)]
    pub fn js_clone(&self) -> Self {
        Self(self.0.clone())
    }
}

impl Verifiable for JsSigner {
    fn verifying_key(&self) -> ed25519_dalek::VerifyingKey {
        self.0.verifying_key()
    }
}

impl AsyncSigner for JsSigner {
    async fn try_sign_bytes_async(
        &self,
        bytes: &[u8],
    ) -> Result<ed25519_dalek::Signature, SigningError> {
        self.0.try_sign_bytes_async(bytes).await
    }
}

#[derive(Debug, Clone)]
pub enum JsSignerOptions {
    Memory(ed25519_dalek::SigningKey),

    #[cfg(feature = "web-sys")]
    WebCrypto {
        verifying_key: ed25519_dalek::VerifyingKey,
        signing_key: web_sys::CryptoKey,
    },
}

impl Verifiable for JsSignerOptions {
    fn verifying_key(&self) -> ed25519_dalek::VerifyingKey {
        match self {
            JsSignerOptions::Memory(key) => key.verifying_key(),

            #[cfg(feature = "web-sys")]
            JsSignerOptions::WebCrypto { verifying_key, .. } => *verifying_key,
        }
    }
}

impl AsyncSigner for JsSignerOptions {
    async fn try_sign_bytes_async(
        &self,
        bytes: &[u8],
    ) -> Result<ed25519_dalek::Signature, SigningError> {
        match self {
            JsSignerOptions::Memory(key) => key.try_sign_bytes_async(bytes).await,

            #[cfg(feature = "web-sys")]
            JsSignerOptions::WebCrypto { signing_key, .. } => {
                use js_sys::Reflect;
                use web_sys::Crypto;

                let crypto_obj =
                    Reflect::get(&js_sys::global(), &"crypto".into()).expect("crypto to exist");
                let crypto = crypto_obj
                    .dyn_into::<Crypto>()
                    .expect("crypto to be a Crypto object");
                let subtle = crypto.subtle();

                let signature_promise = subtle
                    .sign_with_object_and_u8_array(
                        &js_sys::JsString::from("Ed25519").into(),
                        &signing_key.clone(),
                        bytes,
                    )
                    .map_err(|_| {
                        SigningError::SigningFailed(ed25519_dalek::SignatureError::new())
                    })?;

                let js_signature = JsFuture::from(signature_promise).await.map_err(|_| {
                    SigningError::SigningFailed(ed25519_dalek::SignatureError::new())
                })?;

                let signature_bytes = js_sys::Uint8Array::new(&js_signature).to_vec();

                Ok(
                    ed25519_dalek::Signature::from_slice(signature_bytes.as_slice()).map_err(
                        |_| SigningError::SigningFailed(ed25519_dalek::SignatureError::new()),
                    )?,
                )
            }
        }
    }
}

#[wasm_bindgen]
#[derive(Debug, Clone, Copy, Error)]
#[error("Cannot parse ed25519 signing key")]
pub struct CannotParseEd25519SigningKey;

#[cfg(feature = "web-sys")]
#[wasm_bindgen(js_name = GenerateWebCryptoError)]
#[derive(Debug, Clone, Error)]
#[error(transparent)]
pub struct JsGenerateWebCryptoError(#[from] GenerateWebCryptoError);

#[wasm_bindgen(js_class = GenerateWebCryptoError)]
impl JsGenerateWebCryptoError {
    pub fn message(&self) -> String {
        self.0.to_string()
    }
}

#[cfg(feature = "web-sys")]
#[derive(Debug, Clone, Error)]
pub enum GenerateWebCryptoError {
    #[error("No crypto object found")]
    NoCrypto,

    #[error("globalthis.crypto object wasn't an instance of Crypto")]
    CryptoNotCrypto,

    #[error("No web crypto object found")]
    NoWebCrypto,

    #[error("JsError: {0:?}")]
    JsError(JsValue),

    #[error("Cannot parse verifying key")]
    ParseVerifyingKeyError,
}