Skip to content

feat(bindings/crypto-nodejs): Implement an Attachment API. #818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 8, 2022
4 changes: 2 additions & 2 deletions bindings/matrix-sdk-crypto-nodejs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
126 changes: 126 additions & 0 deletions bindings/matrix-sdk-crypto-nodejs/src/attachment.rs
Original file line number Diff line number Diff line change
@@ -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<EncryptedAttachment> {
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<Uint8Array> {
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<matrix_sdk_crypto::MediaEncryptionInfo>,

/// 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<Self> {
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<String> {
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()
}
}
1 change: 1 addition & 0 deletions bindings/matrix-sdk-crypto-nodejs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
77 changes: 77 additions & 0 deletions bindings/matrix-sdk-crypto-nodejs/tests/attachment.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 1 addition & 1 deletion bindings/matrix-sdk-crypto-nodejs/tests/machine.test.js
Original file line number Diff line number Diff line change
@@ -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);
}

12 changes: 6 additions & 6 deletions crates/matrix-sdk-crypto/src/file_encryption/attachments.rs
Original file line number Diff line number Diff line change
@@ -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<EncryptedFile> 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,
4 changes: 2 additions & 2 deletions crates/matrix-sdk/src/encryption/mod.rs
Original file line number Diff line number Diff line change
@@ -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,