diff --git a/bindings/matrix-sdk-crypto-js/src/lib.rs b/bindings/matrix-sdk-crypto-js/src/lib.rs index 96cb8715d..4adbbaf26 100644 --- a/bindings/matrix-sdk-crypto-js/src/lib.rs +++ b/bindings/matrix-sdk-crypto-js/src/lib.rs @@ -26,6 +26,7 @@ pub mod olm; pub mod requests; pub mod responses; pub mod sync_events; +pub mod types; pub mod vodozemac; mod tracing; diff --git a/bindings/matrix-sdk-crypto-js/src/machine.rs b/bindings/matrix-sdk-crypto-js/src/machine.rs index 5116d8f18..308c2442b 100644 --- a/bindings/matrix-sdk-crypto-js/src/machine.rs +++ b/bindings/matrix-sdk-crypto-js/src/machine.rs @@ -16,6 +16,7 @@ use crate::{ vodozemac, responses::{self, response_from_string}, sync_events, + types, }; /// State machine implementation of the Olm/Megolm encryption protocol @@ -297,6 +298,16 @@ impl OlmMachine { }) } + /// Sign the given message using our device key and if available + /// cross-signing master key. + pub fn sign(&self, message: String) -> Promise { + let me = self.inner.clone(); + + future_to_promise::<_, types::Signatures>(async move { + Ok(me.sign(&message).await.into()) + }) + } + /// Invalidate the currently active outbound group session for the /// given room. /// diff --git a/bindings/matrix-sdk-crypto-js/src/types.rs b/bindings/matrix-sdk-crypto-js/src/types.rs new file mode 100644 index 000000000..2e4436317 --- /dev/null +++ b/bindings/matrix-sdk-crypto-js/src/types.rs @@ -0,0 +1,165 @@ +use js_sys::Map; +use wasm_bindgen::prelude::*; + +use crate::{ + identifiers::{DeviceKeyId, UserId}, + vodozemac::Ed25519Signature, +}; + +/// A collection of `Signature`. +#[wasm_bindgen] +#[derive(Debug, Default)] +pub struct Signatures { + inner: matrix_sdk_crypto::types::Signatures, +} + +impl From for Signatures { + fn from(inner: matrix_sdk_crypto::types::Signatures) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl Signatures { + /// Creates a new, empty, signatures collection. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + matrix_sdk_crypto::types::Signatures::new().into() + } + + /// Add the given signature from the given signer and the given key ID to + /// the collection. + #[wasm_bindgen(js_name = "addSignature")] + pub fn add_signature( + &mut self, + signer: &UserId, + key_id: &DeviceKeyId, + signature: &Ed25519Signature, + ) -> Option { + self.inner + .add_signature(signer.inner.clone(), key_id.inner.clone(), signature.inner) + .map(Into::into) + } + + /// Try to find an Ed25519 signature from the given signer with + /// the given key ID. + #[wasm_bindgen(js_name = "getSignature")] + pub fn get_signature(&self, signer: &UserId, key_id: &DeviceKeyId) -> Option { + self.inner.get_signature(signer.inner.as_ref(), key_id.inner.as_ref()).map(Into::into) + } + + /// Get the map of signatures that belong to the given user. + pub fn get(&self, signer: &UserId) -> Option { + let map = Map::new(); + + for (device_key_id, maybe_signature) in + self.inner.get(signer.inner.as_ref()).map(|map| { + map.iter().map(|(device_key_id, maybe_signature)| { + ( + device_key_id.as_str().to_owned(), + MaybeSignature::from(maybe_signature.clone()), + ) + }) + })? + { + map.set(&device_key_id.into(), &maybe_signature.into()); + } + + Some(map) + } + + /// Remove all the signatures we currently hold. + pub fn clear(&mut self) { + self.inner.clear(); + } + + /// Do we hold any signatures or is our collection completely + /// empty. + #[wasm_bindgen(getter, js_name = "isEmpty")] + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// How many signatures do we currently hold. + #[wasm_bindgen(getter)] + pub fn count(&self) -> usize { + self.inner.signature_count() + } +} + +/// Represents a potentially decoded signature (but not a validated +/// one). +#[wasm_bindgen] +#[derive(Debug)] +pub struct Signature { + inner: matrix_sdk_crypto::types::Signature, +} + +impl From for Signature { + fn from(inner: matrix_sdk_crypto::types::Signature) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl Signature { + /// Get the Ed25519 signature, if this is one. + #[wasm_bindgen(getter)] + pub fn ed25519(&self) -> Option { + self.inner.ed25519().map(Into::into) + } + + /// Convert the signature to a base64 encoded string. + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + +type MaybeSignatureInner = + Result; + +/// Represents a signature that is either valid _or_ that could not be +/// decoded. +#[wasm_bindgen] +#[derive(Debug)] +pub struct MaybeSignature { + inner: MaybeSignatureInner, +} + +impl From for MaybeSignature { + fn from(inner: MaybeSignatureInner) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl MaybeSignature { + /// Check whether the signature has been successfully decoded. + #[wasm_bindgen(getter, js_name = "isValid")] + pub fn is_valid(&self) -> bool { + self.inner.is_ok() + } + + /// Check whether the signature could not be successfully decoded. + #[wasm_bindgen(getter, js_name = "isInvalid")] + pub fn is_invalid(&self) -> bool { + self.inner.is_err() + } + + /// The signature, if successfully decoded. + #[wasm_bindgen(getter)] + pub fn signature(&self) -> Option { + self.inner.as_ref().cloned().map(Into::into).ok() + } + + /// The base64 encoded string that is claimed to contain a + /// signature but could not be decoded, if any. + #[wasm_bindgen(getter, js_name = "invalidSignatureSource")] + pub fn invalid_signature_source(&self) -> Option { + match &self.inner { + Ok(_) => None, + Err(signature) => Some(signature.source.clone()), + } + } +} diff --git a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs index e8ac54d90..e80889227 100644 --- a/bindings/matrix-sdk-crypto-js/src/vodozemac.rs +++ b/bindings/matrix-sdk-crypto-js/src/vodozemac.rs @@ -25,6 +25,37 @@ impl Ed25519PublicKey { } } +/// An Ed25519 digital signature, can be used to verify the +/// authenticity of a message. +#[wasm_bindgen] +#[derive(Debug)] +pub struct Ed25519Signature { + pub(crate) inner: vodozemac::Ed25519Signature, +} + +impl From for Ed25519Signature { + fn from(inner: vodozemac::Ed25519Signature) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl Ed25519Signature { + /// Try to create an Ed25519 signature from an unpadded base64 + /// representation. + #[wasm_bindgen(constructor)] + pub fn new(signature: String) -> Result { + Ok(Self { inner: vodozemac::Ed25519Signature::from_base64(signature.as_str())? }) + } + + /// Serialize a Ed25519 signature to an unpadded base64 + /// representation. + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } +} + /// A Curve25519 public key. #[wasm_bindgen] #[derive(Debug, Clone)] diff --git a/bindings/matrix-sdk-crypto-js/tests/machine.test.js b/bindings/matrix-sdk-crypto-js/tests/machine.test.js index 03d219bb8..c798aac53 100644 --- a/bindings/matrix-sdk-crypto-js/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-js/tests/machine.test.js @@ -1,4 +1,4 @@ -const { OlmMachine, UserId, DeviceId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState, CrossSigningStatus } = require('../pkg/matrix_sdk_crypto_js'); +const { OlmMachine, UserId, DeviceId, DeviceKeyId, RoomId, DeviceLists, RequestType, KeysUploadRequest, KeysQueryRequest, KeysClaimRequest, EncryptionSettings, DecryptedRoomEvent, VerificationState, CrossSigningStatus, MaybeSignature } = require('../pkg/matrix_sdk_crypto_js'); describe(OlmMachine.name, () => { test('can be instantiated with the async initializer', async () => { @@ -350,4 +350,46 @@ describe(OlmMachine.name, () => { expect(crossSigningStatus.hasSelfSigning).toStrictEqual(false); expect(crossSigningStatus.hasUserSigning).toStrictEqual(false); }); + + test('can sign a message', async () => { + const m = await machine(); + const signatures = await m.sign('foo'); + + expect(signatures.isEmpty).toStrictEqual(false); + expect(signatures.count).toStrictEqual(1); + + let base64; + + // `get` + { + const signature = signatures.get(user); + + expect(signature.has('ed25519:foobar')).toStrictEqual(true); + + const s = signature.get('ed25519:foobar'); + + expect(s).toBeInstanceOf(MaybeSignature); + + expect(s.isValid).toStrictEqual(true); + expect(s.isInvalid).toStrictEqual(false); + expect(s.invalidSignatureSource).toBeUndefined(); + + base64 = s.signature.toBase64(); + + expect(base64).toMatch(/^[A-Za-z0-9\+/]+$/); + expect(s.signature.ed25519.toBase64()).toStrictEqual(base64); + } + + // `getSignature` + { + const signature = signatures.getSignature(user, new DeviceKeyId('ed25519:foobar')); + expect(signature.toBase64()).toStrictEqual(base64); + } + + // Unknown signatures. + { + expect(signatures.get(new UserId('@hello:example.org'))).toBeUndefined(); + expect(signatures.getSignature(user, new DeviceKeyId('world:foobar'))).toBeUndefined(); + } + }); });