diff --git a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml index 1a7ceb81300..ee16a92b08f 100644 --- a/bindings/matrix-sdk-crypto-nodejs/Cargo.toml +++ b/bindings/matrix-sdk-crypto-nodejs/Cargo.toml @@ -30,8 +30,8 @@ matrix-sdk-common = { version = "0.5.0", path = "../../crates/matrix-sdk-common" matrix-sdk-sled = { version = "0.1.0", path = "../../crates/matrix-sdk-sled", default-features = false, features = ["crypto-store"] } ruma = { git = "https://github.com/ruma/ruma", rev = "96155915f", features = ["client-api-c", "rand", "unstable-msc2676", "unstable-msc2677"] } vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "2404f83f7d3a3779c1f518e4d949f7da9677c3dd" } -napi = { git = "https://github.com/Hywan/napi-rs", branch = "feat-either-n-up-to-26", default-features = false, features = ["napi6", "tokio_rt"] } -napi-derive = { git = "https://github.com/Hywan/napi-rs", branch = "feat-either-n-up-to-26" } +napi = { version = "2.6.1", default-features = false, features = ["napi6", "tokio_rt"] } +napi-derive = "2.6.0" serde_json = "1.0.79" http = "0.2.6" zeroize = "1.3.0" diff --git a/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs b/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs new file mode 100644 index 00000000000..9432c19b245 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/src/attachment.rs @@ -0,0 +1,126 @@ +use std::{ + io::{Cursor, Read}, + ops::Deref, +}; + +use napi::bindgen_prelude::Uint8Array; +use napi_derive::*; + +use crate::into_err; + +/// A type to encrypt and to decrypt anything that can fit in an +/// `Uint8Array`, usually big buffer. +#[napi] +pub struct Attachment; + +#[napi] +impl Attachment { + /// Encrypt the content of the `Uint8Array`. + /// + /// It produces an `EncryptedAttachment`, we can be used to + /// retrieve the media encryption information, or the encrypted + /// data. + #[napi] + pub fn encrypt(array: Uint8Array) -> napi::Result { + let buffer: &[u8] = array.deref(); + + let mut cursor = Cursor::new(buffer); + let mut encryptor = matrix_sdk_crypto::AttachmentEncryptor::new(&mut cursor); + + let mut encrypted_data = Vec::new(); + encryptor.read_to_end(&mut encrypted_data).map_err(into_err)?; + + let media_encryption_info = Some(encryptor.finish()); + + Ok(EncryptedAttachment { + encrypted_data: Uint8Array::new(encrypted_data), + media_encryption_info, + }) + } + + /// Decrypt an `EncryptedAttachment`. + /// + /// The encrypted attachment can be created manually, or from the + /// `encrypt` method. + /// + /// **Warning**: The encrypted attachment can be used only + /// **once**! The encrypted data will still be present, but the + /// media encryption info (which contain secrets) will be + /// destroyed. It is still possible to get a JSON-encoded backup + /// by calling `EncryptedAttachment.mediaEncryptionInfo`. + #[napi] + pub fn decrypt(attachment: &mut EncryptedAttachment) -> napi::Result { + let media_encryption_info = match attachment.media_encryption_info.take() { + Some(media_encryption_info) => media_encryption_info, + None => { + return Err(napi::Error::from_reason( + "The media encryption info are absent from the given encrypted attachment" + .to_string(), + )) + } + }; + + let encrypted_data: &[u8] = attachment.encrypted_data.deref(); + + let mut cursor = Cursor::new(encrypted_data); + let mut decryptor = + matrix_sdk_crypto::AttachmentDecryptor::new(&mut cursor, media_encryption_info) + .map_err(into_err)?; + + let mut decrypted_data = Vec::new(); + decryptor.read_to_end(&mut decrypted_data).map_err(into_err)?; + + Ok(Uint8Array::new(decrypted_data)) + } +} + +/// An encrypted attachment, usually created from `Attachment.encrypt`. +#[napi] +pub struct EncryptedAttachment { + media_encryption_info: Option, + + /// The actual encrypted data. + pub encrypted_data: Uint8Array, +} + +#[napi] +impl EncryptedAttachment { + /// Create a new encrypted attachment manually. + /// + /// It needs encrypted data, stored in an `Uint8Array`, and a + /// [media encryption + /// information](https://docs.rs/matrix-sdk-crypto/latest/matrix_sdk_crypto/struct.MediaEncryptionInfo.html), + /// as a JSON-encoded string. + /// + /// The media encryption information aren't stored as a string: + /// they are parsed, validated and fully deserialized. + /// + /// See [the specification to learn + /// more](https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes). + #[napi(constructor)] + pub fn new(encrypted_data: Uint8Array, media_encryption_info: String) -> napi::Result { + Ok(Self { + encrypted_data, + media_encryption_info: Some( + serde_json::from_str(media_encryption_info.as_str()).map_err(into_err)?, + ), + }) + } + + /// Return the media encryption info as a JSON-encoded string. The + /// structure is fully valid. + /// + /// If the media encryption info have been consumed already, it + /// will return `null`. + #[napi(getter)] + pub fn media_encryption_info(&self) -> Option { + serde_json::to_string(self.media_encryption_info.as_ref()?).ok() + } + + /// Check whether the media encryption info has been consumed by + /// `Attachment.decrypt` already. + #[napi(getter)] + pub fn has_media_encryption_info_been_consumed(&self) -> bool { + self.media_encryption_info.is_none() + } +} diff --git a/bindings/matrix-sdk-crypto-nodejs/src/lib.rs b/bindings/matrix-sdk-crypto-nodejs/src/lib.rs index d346b8953db..26e165fd4b2 100644 --- a/bindings/matrix-sdk-crypto-nodejs/src/lib.rs +++ b/bindings/matrix-sdk-crypto-nodejs/src/lib.rs @@ -16,6 +16,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] //#![warn(missing_docs, missing_debug_implementations)] +pub mod attachment; pub mod encryption; mod errors; pub mod events; diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js new file mode 100644 index 00000000000..86e3eaf8b53 --- /dev/null +++ b/bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js @@ -0,0 +1,77 @@ +const { Attachment, EncryptedAttachment } = require('../'); + +describe(Attachment.name, () => { + const originalData = 'hello'; + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); + + let encryptedAttachment; + + test('can encrypt data', () => { + encryptedAttachment = Attachment.encrypt(textEncoder.encode(originalData)); + + const mediaEncryptionInfo = JSON.parse(encryptedAttachment.mediaEncryptionInfo); + + expect(mediaEncryptionInfo).toMatchObject({ + v: 'v2', + key: { + kty: expect.any(String), + key_ops: expect.arrayContaining(['encrypt', 'decrypt']), + alg: expect.any(String), + k: expect.any(String), + ext: expect.any(Boolean), + }, + iv: expect.stringMatching(/^[A-Za-z0-9\+/]+$/), + hashes: { + sha256: expect.stringMatching(/^[A-Za-z0-9\+/]+$/) + } + }); + + const encryptedData = encryptedAttachment.encryptedData; + expect(encryptedData.every((i) => { i != 0 })).toStrictEqual(false); + }); + + test('can decrypt data', () => { + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false); + + const decryptedAttachment = Attachment.decrypt(encryptedAttachment); + + expect(textDecoder.decode(decryptedAttachment)).toStrictEqual(originalData); + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true); + }); + + test('can only decrypt once', () => { + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true); + + expect(() => { textDecoder.decode(decryptedAttachment) }).toThrow() + }); +}); + +describe(EncryptedAttachment.name, () => { + const originalData = 'hello'; + const textDecoder = new TextDecoder(); + + test('can be created manually', () => { + const encryptedAttachment = new EncryptedAttachment( + new Uint8Array([24, 150, 67, 37, 144]), + JSON.stringify({ + v: 'v2', + key: { + kty: 'oct', + key_ops: [ 'encrypt', 'decrypt' ], + alg: 'A256CTR', + k: 'QbNXUjuukFyEJ8cQZjJuzN6mMokg0HJIjx0wVMLf5BM', + ext: true + }, + iv: 'xk2AcWkomiYAAAAAAAAAAA', + hashes: { + sha256: 'JsRbDXgOja4xvDiF3DwBuLHdxUzIrVYIuj7W/t3aEok' + } + }) + ); + + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(false); + expect(textDecoder.decode(Attachment.decrypt(encryptedAttachment))).toStrictEqual(originalData); + expect(encryptedAttachment.hasMediaEncryptionInfoBeenConsumed).toStrictEqual(true); + }); +}); diff --git a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js index ccc5509369c..020486bd2aa 100644 --- a/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js +++ b/bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js @@ -381,7 +381,7 @@ describe(OlmMachine.name, () => { base64 = signature['ed25519:foobar'].signature.toBase64(); - expect(base64).toMatch(/^[A-Za-z0-9+/]+$/); + expect(base64).toMatch(/^[A-Za-z0-9\+/]+$/); expect(signature['ed25519:foobar'].signature.ed25519.toBase64()).toStrictEqual(base64); } diff --git a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs index 90d031cee14..5f9facfe2c6 100644 --- a/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs +++ b/crates/matrix-sdk-crypto/src/file_encryption/attachments.rs @@ -136,7 +136,7 @@ impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> { let hash = info.hashes.get("sha256").ok_or(DecryptorError::MissingHash)?.as_bytes().to_owned(); - let mut key = info.web_key.k.into_inner(); + let mut key = info.key.k.into_inner(); let iv = info.iv.into_inner(); if key.len() != KEY_SIZE { @@ -270,7 +270,7 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { version: VERSION.to_owned(), hashes: self.hashes, iv: self.iv, - web_key: self.web_key, + key: self.web_key, } } } @@ -279,11 +279,11 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> { /// file. #[derive(Debug, Serialize, Deserialize)] pub struct MediaEncryptionInfo { - #[serde(rename = "v")] /// The version of the encryption scheme. + #[serde(rename = "v")] pub version: String, /// The web key that was used to encrypt the file. - pub web_key: JsonWebKey, + pub key: JsonWebKey, /// The initialization vector that was used to encrypt the file. pub iv: Base64, /// The hashes that can be used to check the validity of the file. @@ -292,7 +292,7 @@ pub struct MediaEncryptionInfo { impl From for MediaEncryptionInfo { fn from(file: EncryptedFile) -> Self { - Self { version: file.v, web_key: file.key, iv: file.iv, hashes: file.hashes } + Self { version: file.v, key: file.key, iv: file.iv, hashes: file.hashes } } } @@ -312,7 +312,7 @@ mod tests { fn example_key() -> MediaEncryptionInfo { let info = json!({ "v": "v2", - "web_key": { + "key": { "kty": "oct", "alg": "A256CTR", "ext": true, diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index b952ad2796e..8d1cdf1101a 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -126,7 +126,7 @@ impl Client { let keys = reader.finish(); ruma::events::room::EncryptedFileInit { url: response.content_uri, - key: keys.web_key, + key: keys.key, iv: keys.iv, hashes: keys.hashes, v: keys.version, @@ -155,7 +155,7 @@ impl Client { let keys = reader.finish(); ruma::events::room::EncryptedFileInit { url: response.content_uri, - key: keys.web_key, + key: keys.key, iv: keys.iv, hashes: keys.hashes, v: keys.version,