Skip to content

Commit

Permalink
feat(dehydration): support for dehydration key and storage
Browse files Browse the repository at this point in the history
  • Loading branch information
BillCarsonFr committed Dec 13, 2024
1 parent 8142a01 commit 412de49
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 9 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
other functions are used. The behaviour is unchanged and still available on
Node.js.

- Expose new API `DehydratedDevices.getDehydratedDeviceKey`, `DehydratedDevices.saveDehydratedDeviceKey`
and `DehydratedDevices.deleteDehydratedDeviceKey` to store/load the dehydrated device pickle key.
This allows client to automatically rotate the dehydrated device to avoid one-time-keys exhaustion and
to_device accumulation. `DehydratedDevices.keysForUpload` and `DehydratedDevices.rehydrate` now use the
`DehydratedDeviceKey` as parameter instead of a raw UInt8Array.
Use `DehydratedDeviceKey::createKeyFromArray` to migrate.

# matrix-sdk-crypto-wasm v11.0.0

- Update matrix-rust-sdk to `70bcddfba5e19`.
Expand Down
97 changes: 89 additions & 8 deletions src/dehydrated_devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! WASM wrapper for `matrix_sdk_crypto::dehydrated_devices`.
use js_sys::{Array, JsString, Uint8Array};
use matrix_sdk_crypto::dehydrated_devices;
use matrix_sdk_crypto::{dehydrated_devices, store::DehydratedDeviceKey as SdkDehydratedDeviceKey};
use wasm_bindgen::prelude::*;

use crate::{identifiers::DeviceId, requests::PutDehydratedDeviceRequest, store::RoomKeyInfo};
Expand All @@ -21,6 +21,51 @@ impl From<dehydrated_devices::DehydratedDevices> for DehydratedDevices {
}
}

/// Dehydrated device key
#[wasm_bindgen]
#[derive(Debug)]
pub struct DehydratedDeviceKey {
pub(crate) inner: Uint8Array,
}

#[wasm_bindgen]
impl DehydratedDeviceKey {
/// Generates a new random pickle key.
#[wasm_bindgen(js_name = "createRandomKey")]
pub fn create_random_key() -> Result<DehydratedDeviceKey, JsError> {
Ok(SdkDehydratedDeviceKey::new()?.into())
}

/// Generates a new random pickle key.
#[wasm_bindgen(js_name = "createKeyFromArray")]
pub fn create_key_from_array(array: Uint8Array) -> Result<DehydratedDeviceKey, JsError> {
Ok(SdkDehydratedDeviceKey::from_slice(array.to_vec().as_slice())?.into())
}

/// Convert the pickle key to a base 64 encoded string.
#[wasm_bindgen(js_name = "toBase64")]
pub fn to_base64(&self) -> JsString {
let binding = self.inner.to_vec();
let inner: &[u8; 32] = binding.as_slice().try_into().expect("Expected 32 byte array");

SdkDehydratedDeviceKey::from(inner).to_base64().into()
}
}

// Zero out on drop
impl Drop for DehydratedDeviceKey {
fn drop(&mut self) {
self.inner.fill(0, 0, 32);
}
}

impl From<matrix_sdk_crypto::store::DehydratedDeviceKey> for DehydratedDeviceKey {
fn from(pickle_key: matrix_sdk_crypto::store::DehydratedDeviceKey) -> Self {
let vec: Vec<u8> = pickle_key.into();
DehydratedDeviceKey { inner: vec.as_slice().into() }
}
}

#[wasm_bindgen]
impl DehydratedDevices {
/// Create a new [`DehydratedDevice`] which can be uploaded to the server.
Expand All @@ -33,18 +78,54 @@ impl DehydratedDevices {
#[wasm_bindgen]
pub async fn rehydrate(
&self,
pickle_key: &Uint8Array,
pickle_key: &DehydratedDeviceKey,
device_id: &DeviceId,
device_data: &str,
) -> Result<RehydratedDevice, JsError> {
let pickle_key: [u8; 32] =
pickle_key.to_vec().try_into().map_err(|_| JsError::new("Wrong key length"))?;
let sdk_pickle_key =
SdkDehydratedDeviceKey::from_slice(pickle_key.inner.to_vec().as_slice())?;

Ok(self
.inner
.rehydrate(&pickle_key, &device_id.inner, serde_json::from_str(device_data)?)
.rehydrate(&sdk_pickle_key, &device_id.inner, serde_json::from_str(device_data)?)
.await?
.into())
}

/// Get the cached dehydrated device pickle key if any.
///
/// None if the key was not previously cached (via
/// [`Self::save_dehydrated_device_pickle_key`]).
///
/// Should be used to periodically rotate the dehydrated device to avoid
/// OTK exhaustion and accumulation of to_device messages.
#[wasm_bindgen(js_name = "getDehydratedDeviceKey")]
pub async fn get_dehydrated_device_key(&self) -> Result<Option<DehydratedDeviceKey>, JsError> {
let key = self.inner.get_dehydrated_device_pickle_key().await?;
Ok(key.map(DehydratedDeviceKey::from))
}

/// Store the dehydrated device pickle key in the crypto store.
///
/// This is useful if the client wants to periodically rotate dehydrated
/// devices to avoid OTK exhaustion and accumulated to_device problems.
#[wasm_bindgen(js_name = "saveDehydratedDeviceKey")]
pub async fn save_dehydrated_device_key(
&self,
pickle_key: &DehydratedDeviceKey,
) -> Result<(), JsError> {
let sdk_pickle_key =
SdkDehydratedDeviceKey::from_slice(pickle_key.inner.to_vec().as_slice())?;
self.inner.save_dehydrated_device_pickle_key(&sdk_pickle_key).await?;
Ok(())
}

/// Clear the dehydrated device pickle key saved in the crypto store.
#[wasm_bindgen(js_name = "deleteDehydratedDeviceKey")]
pub async fn delete_dehydrated_device_key(&self) -> Result<(), JsError> {
self.inner.delete_dehydrated_device_pickle_key().await?;
Ok(())
}
}

#[wasm_bindgen]
Expand Down Expand Up @@ -104,10 +185,10 @@ impl DehydratedDevice {
pub async fn keys_for_upload(
&self,
initial_device_display_name: JsString,
pickle_key: Uint8Array,
pickle_key: &DehydratedDeviceKey,
) -> Result<PutDehydratedDeviceRequest, JsError> {
let pickle_key: [u8; 32] =
pickle_key.to_vec().try_into().map_err(|_| JsError::new("Wrong key length"))?;
let pickle_key = SdkDehydratedDeviceKey::from_slice(pickle_key.inner.to_vec().as_slice())?;

Ok(self
.inner
.keys_for_upload(initial_device_display_name.into(), &pickle_key)
Expand Down
39 changes: 38 additions & 1 deletion tests/dehydrated_devices.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DehydratedDeviceKey,
DecryptionSettings,
DeviceId,
DeviceLists,
Expand All @@ -21,6 +22,41 @@ afterEach(() => {
});

describe("dehydrated devices", () => {
test("can save and restore dehydrated device pickle key", async () => {
const user = new UserId("@alice:example.org");
// set up OlmMachine to dehydrated device
const machine = await OlmMachine.initialize(user, new DeviceId("ABCDEFG"));

const dehydratedDevices = machine.dehydratedDevices();
const key = DehydratedDeviceKey.createRandomKey();

await dehydratedDevices.saveDehydratedDeviceKey(key);

const loaded_key: DehydratedDeviceKey = await dehydratedDevices.getDehydratedDeviceKey();

expect(key.toBase64()).toEqual(loaded_key.toBase64());
});

test("can delete a previously saved pickle key", async () => {
const user = new UserId("@alice:example.org");
// set up OlmMachine to dehydrated device
const machine = await OlmMachine.initialize(user, new DeviceId("ABCDEFG"));
const dehydratedDevices = machine.dehydratedDevices();

const key = DehydratedDeviceKey.createRandomKey();

await dehydratedDevices.saveDehydratedDeviceKey(key);

const loaded_key = await dehydratedDevices.getDehydratedDeviceKey();

expect(loaded_key).toBeDefined();

await dehydratedDevices.deleteDehydratedDeviceKey();

const loaded_key_after = await dehydratedDevices.getDehydratedDeviceKey();
expect(loaded_key_after).toBeUndefined();
});

test("can dehydrate and rehydrate a device", async () => {
const room = new RoomId("!test:localhost");
const user = new UserId("@alice:example.org");
Expand Down Expand Up @@ -52,7 +88,8 @@ describe("dehydrated devices", () => {
// create dehydrated device
const dehydratedDevices = machine.dehydratedDevices();
const device = await dehydratedDevices.create();
const key = new Uint8Array(32);

const key = DehydratedDeviceKey.createKeyFromArray(new Uint8Array(32));
const dehydrationRequest = await device.keysForUpload("Dehydrated device", key);
const dehydrationBody = JSON.parse(dehydrationRequest.body);

Expand Down

0 comments on commit 412de49

Please sign in to comment.