use std::{
cmp::Ordering,
fmt,
ops::Deref,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use ruma::{
events::room::history_visibility::HistoryVisibility, serde::JsonObject, DeviceKeyAlgorithm,
OwnedRoomId, RoomId,
};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use vodozemac::{
megolm::{
DecryptedMessage, DecryptionError, InboundGroupSession as InnerSession,
InboundGroupSessionPickle, MegolmMessage, SessionConfig, SessionOrdering,
},
Curve25519PublicKey, Ed25519PublicKey, PickleError,
};
use super::{
BackedUpRoomKey, ExportedRoomKey, OutboundGroupSession, SenderData, SenderDataType,
SessionCreationError, SessionKey,
};
use crate::{
error::{EventError, MegolmResult},
types::{
deserialize_curve_key,
events::{
forwarded_room_key::{
ForwardedMegolmV1AesSha2Content, ForwardedMegolmV2AesSha2Content,
ForwardedRoomKeyContent,
},
olm_v1::DecryptedForwardedRoomKeyEvent,
room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
},
serialize_curve_key, EventEncryptionAlgorithm, SigningKeys,
},
};
#[derive(Clone)]
pub(crate) struct SessionCreatorInfo {
pub curve25519_key: Curve25519PublicKey,
pub signing_keys: Arc<SigningKeys<DeviceKeyAlgorithm>>,
}
#[derive(Clone)]
pub struct InboundGroupSession {
inner: Arc<Mutex<InnerSession>>,
session_id: Arc<str>,
first_known_index: u32,
pub(crate) creator_info: SessionCreatorInfo,
pub sender_data: SenderData,
pub room_id: OwnedRoomId,
imported: bool,
algorithm: Arc<EventEncryptionAlgorithm>,
history_visibility: Arc<Option<HistoryVisibility>>,
backed_up: Arc<AtomicBool>,
}
impl InboundGroupSession {
pub fn new(
sender_key: Curve25519PublicKey,
signing_key: Ed25519PublicKey,
room_id: &RoomId,
session_key: &SessionKey,
sender_data: SenderData,
encryption_algorithm: EventEncryptionAlgorithm,
history_visibility: Option<HistoryVisibility>,
) -> Result<Self, SessionCreationError> {
let config = OutboundGroupSession::session_config(&encryption_algorithm)?;
let session = InnerSession::new(session_key, config);
let session_id = session.session_id();
let first_known_index = session.first_known_index();
let mut keys = SigningKeys::new();
keys.insert(DeviceKeyAlgorithm::Ed25519, signing_key.into());
Ok(InboundGroupSession {
inner: Arc::new(Mutex::new(session)),
history_visibility: history_visibility.into(),
session_id: session_id.into(),
first_known_index,
creator_info: SessionCreatorInfo {
curve25519_key: sender_key,
signing_keys: keys.into(),
},
sender_data,
room_id: room_id.into(),
imported: false,
algorithm: encryption_algorithm.into(),
backed_up: AtomicBool::new(false).into(),
})
}
pub fn from_export(exported_session: &ExportedRoomKey) -> Result<Self, SessionCreationError> {
Self::try_from(exported_session)
}
#[allow(dead_code)]
fn from_backup(
room_id: &RoomId,
backup: BackedUpRoomKey,
) -> Result<Self, SessionCreationError> {
let session = InnerSession::import(&backup.session_key, SessionConfig::default());
let session_id = session.session_id();
Self::from_export(&ExportedRoomKey {
algorithm: backup.algorithm,
room_id: room_id.to_owned(),
sender_key: backup.sender_key,
session_id,
forwarding_curve25519_key_chain: vec![],
session_key: backup.session_key,
sender_claimed_keys: backup.sender_claimed_keys,
})
}
pub async fn pickle(&self) -> PickledInboundGroupSession {
let pickle = self.inner.lock().await.pickle();
PickledInboundGroupSession {
pickle,
sender_key: self.creator_info.curve25519_key,
signing_key: (*self.creator_info.signing_keys).clone(),
sender_data: self.sender_data.clone(),
room_id: self.room_id().to_owned(),
imported: self.imported,
backed_up: self.backed_up(),
history_visibility: self.history_visibility.as_ref().clone(),
algorithm: (*self.algorithm).to_owned(),
}
}
pub async fn export(&self) -> ExportedRoomKey {
self.export_at_index(self.first_known_index()).await
}
pub fn sender_key(&self) -> Curve25519PublicKey {
self.creator_info.curve25519_key
}
pub fn backed_up(&self) -> bool {
self.backed_up.load(SeqCst)
}
pub fn reset_backup_state(&self) {
self.backed_up.store(false, SeqCst)
}
pub fn mark_as_backed_up(&self) {
self.backed_up.store(true, SeqCst)
}
pub fn signing_keys(&self) -> &SigningKeys<DeviceKeyAlgorithm> {
&self.creator_info.signing_keys
}
pub async fn export_at_index(&self, message_index: u32) -> ExportedRoomKey {
let message_index = std::cmp::max(self.first_known_index(), message_index);
let session_key =
self.inner.lock().await.export_at(message_index).expect("Can't export session");
ExportedRoomKey {
algorithm: self.algorithm().to_owned(),
room_id: self.room_id().to_owned(),
sender_key: self.creator_info.curve25519_key,
session_id: self.session_id().to_owned(),
forwarding_curve25519_key_chain: vec![],
sender_claimed_keys: (*self.creator_info.signing_keys).clone(),
session_key,
}
}
pub fn from_pickle(pickle: PickledInboundGroupSession) -> Result<Self, PickleError> {
let session: InnerSession = pickle.pickle.into();
let first_known_index = session.first_known_index();
let session_id = session.session_id();
Ok(InboundGroupSession {
inner: Mutex::new(session).into(),
session_id: session_id.into(),
creator_info: SessionCreatorInfo {
curve25519_key: pickle.sender_key,
signing_keys: pickle.signing_key.into(),
},
sender_data: pickle.sender_data,
history_visibility: pickle.history_visibility.into(),
first_known_index,
room_id: (*pickle.room_id).into(),
backed_up: AtomicBool::from(pickle.backed_up).into(),
algorithm: pickle.algorithm.into(),
imported: pickle.imported,
})
}
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn algorithm(&self) -> &EventEncryptionAlgorithm {
&self.algorithm
}
pub fn first_known_index(&self) -> u32 {
self.first_known_index
}
pub fn has_been_imported(&self) -> bool {
self.imported
}
pub async fn compare(&self, other: &InboundGroupSession) -> SessionOrdering {
if Arc::ptr_eq(&self.inner, &other.inner) {
SessionOrdering::Equal
} else if self.sender_key() != other.sender_key()
|| self.signing_keys() != other.signing_keys()
|| self.algorithm() != other.algorithm()
|| self.room_id() != other.room_id()
{
SessionOrdering::Unconnected
} else {
let mut other_inner = other.inner.lock().await;
match self.inner.lock().await.compare(&mut other_inner) {
SessionOrdering::Equal => {
match self.sender_data.compare_trust_level(&other.sender_data) {
Ordering::Less => SessionOrdering::Worse,
Ordering::Equal => SessionOrdering::Equal,
Ordering::Greater => SessionOrdering::Better,
}
}
result => result,
}
}
}
pub(crate) async fn decrypt_helper(
&self,
message: &MegolmMessage,
) -> Result<DecryptedMessage, DecryptionError> {
self.inner.lock().await.decrypt(message)
}
pub async fn to_backup(&self) -> BackedUpRoomKey {
self.export().await.into()
}
pub async fn decrypt(&self, event: &EncryptedEvent) -> MegolmResult<(JsonObject, u32)> {
let decrypted = match &event.content.scheme {
RoomEventEncryptionScheme::MegolmV1AesSha2(c) => {
self.decrypt_helper(&c.ciphertext).await?
}
#[cfg(feature = "experimental-algorithms")]
RoomEventEncryptionScheme::MegolmV2AesSha2(c) => {
self.decrypt_helper(&c.ciphertext).await?
}
RoomEventEncryptionScheme::Unknown(_) => {
return Err(EventError::UnsupportedAlgorithm.into());
}
};
let plaintext = String::from_utf8_lossy(&decrypted.plaintext);
let mut decrypted_object = serde_json::from_str::<JsonObject>(&plaintext)?;
let server_ts: i64 = event.origin_server_ts.0.into();
decrypted_object.insert("sender".to_owned(), event.sender.to_string().into());
decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into());
decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into());
let room_id = decrypted_object
.get("room_id")
.and_then(|r| r.as_str().and_then(|r| RoomId::parse(r).ok()));
if room_id.as_deref() != Some(self.room_id()) {
return Err(EventError::MismatchedRoom(self.room_id().to_owned(), room_id).into());
}
decrypted_object.insert(
"unsigned".to_owned(),
serde_json::to_value(&event.unsigned).unwrap_or_default(),
);
if let Some(decrypted_content) =
decrypted_object.get_mut("content").and_then(|c| c.as_object_mut())
{
if !decrypted_content.contains_key("m.relates_to") {
if let Some(relation) = &event.content.relates_to {
decrypted_content.insert("m.relates_to".to_owned(), relation.to_owned());
}
}
}
Ok((decrypted_object, decrypted.message_index))
}
#[cfg(test)]
pub(crate) fn mark_as_imported(&mut self) {
self.imported = true;
}
pub fn sender_data_type(&self) -> SenderDataType {
self.sender_data.to_type()
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for InboundGroupSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InboundGroupSession").field("session_id", &self.session_id()).finish()
}
}
impl PartialEq for InboundGroupSession {
fn eq(&self, other: &Self) -> bool {
self.session_id() == other.session_id()
}
}
#[derive(Serialize, Deserialize)]
#[allow(missing_debug_implementations)]
pub struct PickledInboundGroupSession {
pub pickle: InboundGroupSessionPickle,
#[serde(deserialize_with = "deserialize_curve_key", serialize_with = "serialize_curve_key")]
pub sender_key: Curve25519PublicKey,
pub signing_key: SigningKeys<DeviceKeyAlgorithm>,
#[serde(default)]
pub sender_data: SenderData,
pub room_id: OwnedRoomId,
pub imported: bool,
#[serde(default)]
pub backed_up: bool,
pub history_visibility: Option<HistoryVisibility>,
#[serde(default = "default_algorithm")]
pub algorithm: EventEncryptionAlgorithm,
}
fn default_algorithm() -> EventEncryptionAlgorithm {
EventEncryptionAlgorithm::MegolmV1AesSha2
}
impl TryFrom<&ExportedRoomKey> for InboundGroupSession {
type Error = SessionCreationError;
fn try_from(key: &ExportedRoomKey) -> Result<Self, Self::Error> {
let config = OutboundGroupSession::session_config(&key.algorithm)?;
let session = InnerSession::import(&key.session_key, config);
let first_known_index = session.first_known_index();
Ok(InboundGroupSession {
inner: Mutex::new(session).into(),
session_id: key.session_id.to_owned().into(),
creator_info: SessionCreatorInfo {
curve25519_key: key.sender_key,
signing_keys: key.sender_claimed_keys.to_owned().into(),
},
sender_data: SenderData::default(),
history_visibility: None.into(),
first_known_index,
room_id: key.room_id.to_owned(),
imported: true,
algorithm: key.algorithm.to_owned().into(),
backed_up: AtomicBool::from(false).into(),
})
}
}
impl From<&ForwardedMegolmV1AesSha2Content> for InboundGroupSession {
fn from(value: &ForwardedMegolmV1AesSha2Content) -> Self {
let session = InnerSession::import(&value.session_key, SessionConfig::version_1());
let session_id = session.session_id().into();
let first_known_index = session.first_known_index();
InboundGroupSession {
inner: Mutex::new(session).into(),
session_id,
creator_info: SessionCreatorInfo {
curve25519_key: value.claimed_sender_key,
signing_keys: SigningKeys::from([(
DeviceKeyAlgorithm::Ed25519,
value.claimed_ed25519_key.into(),
)])
.into(),
},
sender_data: SenderData::default(),
history_visibility: None.into(),
first_known_index,
room_id: value.room_id.to_owned(),
imported: true,
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(),
backed_up: AtomicBool::from(false).into(),
}
}
}
impl From<&ForwardedMegolmV2AesSha2Content> for InboundGroupSession {
fn from(value: &ForwardedMegolmV2AesSha2Content) -> Self {
let session = InnerSession::import(&value.session_key, SessionConfig::version_2());
let session_id = session.session_id().into();
let first_known_index = session.first_known_index();
InboundGroupSession {
inner: Mutex::new(session).into(),
session_id,
creator_info: SessionCreatorInfo {
curve25519_key: value.claimed_sender_key,
signing_keys: value.claimed_signing_keys.to_owned().into(),
},
sender_data: SenderData::default(),
history_visibility: None.into(),
first_known_index,
room_id: value.room_id.to_owned(),
imported: true,
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2.into(),
backed_up: AtomicBool::from(false).into(),
}
}
}
impl TryFrom<&DecryptedForwardedRoomKeyEvent> for InboundGroupSession {
type Error = SessionCreationError;
fn try_from(value: &DecryptedForwardedRoomKeyEvent) -> Result<Self, Self::Error> {
match &value.content {
ForwardedRoomKeyContent::MegolmV1AesSha2(c) => Ok(Self::from(c.deref())),
#[cfg(feature = "experimental-algorithms")]
ForwardedRoomKeyContent::MegolmV2AesSha2(c) => Ok(Self::from(c.deref())),
ForwardedRoomKeyContent::Unknown(c) => {
Err(SessionCreationError::Algorithm(c.algorithm.to_owned()))
}
}
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_let;
use matrix_sdk_test::async_test;
use ruma::{
device_id, events::room::history_visibility::HistoryVisibility, room_id, user_id, DeviceId,
UserId,
};
use vodozemac::{
megolm::{SessionKey, SessionOrdering},
Curve25519PublicKey, Ed25519PublicKey,
};
use crate::{
olm::{InboundGroupSession, KnownSenderData, SenderData},
types::EventEncryptionAlgorithm,
Account,
};
fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
}
fn alice_device_id() -> &'static DeviceId {
device_id!("ALICEDEVICE")
}
#[async_test]
async fn test_can_deserialise_pickled_session_without_sender_data() {
let pickle = r#"
{
"pickle": {
"initial_ratchet": {
"inner": [ 124, 251, 213, 204, 108, 247, 54, 7, 179, 162, 15, 107, 154, 215,
220, 46, 123, 113, 120, 162, 225, 246, 237, 203, 125, 102, 190, 212,
229, 195, 136, 185, 26, 31, 77, 140, 144, 181, 152, 177, 46, 105,
202, 6, 53, 158, 157, 170, 31, 155, 130, 87, 214, 110, 143, 55, 68,
138, 41, 35, 242, 230, 194, 15, 16, 145, 116, 94, 89, 35, 79, 145,
245, 117, 204, 173, 166, 178, 49, 131, 143, 61, 61, 15, 211, 167, 17,
2, 79, 110, 149, 200, 223, 23, 185, 200, 29, 64, 55, 39, 147, 167,
205, 224, 159, 101, 218, 249, 203, 30, 175, 174, 48, 252, 40, 131,
52, 135, 91, 57, 211, 96, 105, 58, 55, 68, 250, 24 ],
"counter": 0
},
"signing_key": [ 93, 185, 171, 61, 173, 100, 51, 9, 157, 180, 214, 39, 131, 80, 118,
130, 199, 232, 163, 197, 45, 23, 227, 100, 151, 59, 19, 102, 38,
149, 43, 38 ],
"signing_key_verified": true,
"config": {
"version": "V1"
}
},
"sender_key": "AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8",
"signing_key": {
"ed25519": "wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"
},
"room_id": "!test:localhost",
"forwarding_chains": ["tb6kQKjk+SJl2KnfQ0lKVOZl6gDFMcsb9HcUP9k/4hc"],
"imported": false,
"backed_up": false,
"history_visibility": "shared",
"algorithm": "m.megolm.v1.aes-sha2"
}
"#;
let deserialized = serde_json::from_str(pickle).unwrap();
let unpickled = InboundGroupSession::from_pickle(deserialized).unwrap();
assert_eq!(unpickled.session_id(), "XbmrPa1kMwmdtNYng1B2gsfoo8UtF+NklzsTZiaVKyY");
assert_let!(
SenderData::UnknownDevice { legacy_session, owner_check_failed } =
unpickled.sender_data
);
assert!(legacy_session);
assert!(!owner_check_failed);
}
#[async_test]
async fn test_can_serialise_pickled_session_with_sender_data() {
let igs = InboundGroupSession::new(
Curve25519PublicKey::from_base64("AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8")
.unwrap(),
Ed25519PublicKey::from_base64("wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww").unwrap(),
room_id!("!test:localhost"),
&create_session_key(),
SenderData::unknown(),
EventEncryptionAlgorithm::MegolmV1AesSha2,
Some(HistoryVisibility::Shared),
)
.unwrap();
let pickled = igs.pickle().await;
let serialised = serde_json::to_string(&pickled).unwrap();
let expected_inner = vec![
193, 203, 223, 152, 33, 132, 200, 168, 24, 197, 79, 174, 231, 202, 45, 245, 128, 131,
178, 165, 148, 37, 241, 214, 178, 218, 25, 33, 68, 48, 153, 104, 122, 6, 249, 198, 97,
226, 214, 75, 64, 128, 25, 138, 98, 90, 138, 93, 52, 206, 174, 3, 84, 149, 101, 140,
238, 156, 103, 107, 124, 144, 139, 104, 253, 5, 100, 251, 186, 118, 208, 87, 31, 218,
123, 234, 103, 34, 246, 100, 39, 90, 216, 72, 187, 86, 202, 150, 100, 116, 204, 254,
10, 154, 216, 133, 61, 250, 75, 100, 195, 63, 138, 22, 17, 13, 156, 123, 195, 132, 111,
95, 250, 24, 236, 0, 246, 93, 230, 100, 211, 165, 211, 190, 181, 87, 42, 181,
];
assert_eq!(
serde_json::from_str::<serde_json::Value>(&serialised).unwrap(),
serde_json::json!({
"pickle":{
"initial_ratchet":{
"inner": expected_inner,
"counter":0
},
"signing_key":[
213,161,95,135,114,153,162,127,217,74,64,2,59,143,93,5,190,157,120,
80,89,8,87,129,115,148,104,144,152,186,178,109
],
"signing_key_verified":true,
"config":{"version":"V1"}
},
"sender_key":"AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8",
"signing_key":{"ed25519":"wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"},
"sender_data":{
"UnknownDevice":{
"legacy_session":false
}
},
"room_id":"!test:localhost",
"imported":false,
"backed_up":false,
"history_visibility":"shared",
"algorithm":"m.megolm.v1.aes-sha2"
})
);
}
#[async_test]
async fn test_can_deserialise_pickled_session_with_sender_data() {
let pickle = r#"
{
"pickle": {
"initial_ratchet": {
"inner": [ 124, 251, 213, 204, 108, 247, 54, 7, 179, 162, 15, 107, 154, 215,
220, 46, 123, 113, 120, 162, 225, 246, 237, 203, 125, 102, 190, 212,
229, 195, 136, 185, 26, 31, 77, 140, 144, 181, 152, 177, 46, 105,
202, 6, 53, 158, 157, 170, 31, 155, 130, 87, 214, 110, 143, 55, 68,
138, 41, 35, 242, 230, 194, 15, 16, 145, 116, 94, 89, 35, 79, 145,
245, 117, 204, 173, 166, 178, 49, 131, 143, 61, 61, 15, 211, 167, 17,
2, 79, 110, 149, 200, 223, 23, 185, 200, 29, 64, 55, 39, 147, 167,
205, 224, 159, 101, 218, 249, 203, 30, 175, 174, 48, 252, 40, 131,
52, 135, 91, 57, 211, 96, 105, 58, 55, 68, 250, 24 ],
"counter": 0
},
"signing_key": [ 93, 185, 171, 61, 173, 100, 51, 9, 157, 180, 214, 39, 131, 80, 118,
130, 199, 232, 163, 197, 45, 23, 227, 100, 151, 59, 19, 102, 38,
149, 43, 38 ],
"signing_key_verified": true,
"config": {
"version": "V1"
}
},
"sender_key": "AmM1DvVJarsNNXVuX7OarzfT481N37GtDwvDVF0RcR8",
"signing_key": {
"ed25519": "wTRTdz4rn4EY+68cKPzpMdQ6RAlg7T8cbTmEjaXuUww"
},
"sender_data":{
"UnknownDevice":{
"legacy_session":false
}
},
"room_id": "!test:localhost",
"forwarding_chains": ["tb6kQKjk+SJl2KnfQ0lKVOZl6gDFMcsb9HcUP9k/4hc"],
"imported": false,
"backed_up": false,
"history_visibility": "shared",
"algorithm": "m.megolm.v1.aes-sha2"
}
"#;
let deserialized = serde_json::from_str(pickle).unwrap();
let unpickled = InboundGroupSession::from_pickle(deserialized).unwrap();
assert_eq!(unpickled.session_id(), "XbmrPa1kMwmdtNYng1B2gsfoo8UtF+NklzsTZiaVKyY");
assert_let!(
SenderData::UnknownDevice { legacy_session, owner_check_failed } =
unpickled.sender_data
);
assert!(!legacy_session);
assert!(!owner_check_failed);
}
#[async_test]
async fn test_session_comparison() {
let alice = Account::with_device_id(alice_id(), alice_device_id());
let room_id = room_id!("!test:localhost");
let (_, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
let worse = InboundGroupSession::from_export(&inbound.export_at_index(10).await).unwrap();
let mut copy = InboundGroupSession::from_pickle(inbound.pickle().await).unwrap();
assert_eq!(inbound.compare(&worse).await, SessionOrdering::Better);
assert_eq!(worse.compare(&inbound).await, SessionOrdering::Worse);
assert_eq!(inbound.compare(&inbound).await, SessionOrdering::Equal);
assert_eq!(inbound.compare(©).await, SessionOrdering::Equal);
copy.creator_info.curve25519_key =
Curve25519PublicKey::from_base64("XbmrPa1kMwmdtNYng1B2gsfoo8UtF+NklzsTZiaVKyY")
.unwrap();
assert_eq!(inbound.compare(©).await, SessionOrdering::Unconnected);
}
#[async_test]
async fn test_session_comparison_sender_data() {
let alice = Account::with_device_id(alice_id(), alice_device_id());
let room_id = room_id!("!test:localhost");
let (_, mut inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
let sender_data = SenderData::SenderVerified(KnownSenderData {
user_id: alice.user_id().into(),
device_id: Some(alice.device_id().into()),
master_key: alice.identity_keys().ed25519.into(),
});
let mut better = InboundGroupSession::from_pickle(inbound.pickle().await).unwrap();
better.sender_data = sender_data.clone();
assert_eq!(inbound.compare(&better).await, SessionOrdering::Worse);
assert_eq!(better.compare(&inbound).await, SessionOrdering::Better);
inbound.sender_data = sender_data;
assert_eq!(better.compare(&inbound).await, SessionOrdering::Equal);
}
fn create_session_key() -> SessionKey {
SessionKey::from_base64(
"\
AgAAAADBy9+YIYTIqBjFT67nyi31gIOypZQl8day2hkhRDCZaHoG+cZh4tZLQIAZimJail0\
0zq4DVJVljO6cZ2t8kIto/QVk+7p20Fcf2nvqZyL2ZCda2Ei7VsqWZHTM/gqa2IU9+ktkwz\
+KFhENnHvDhG9f+hjsAPZd5mTTpdO+tVcqtdWhX4dymaJ/2UpAAjuPXQW+nXhQWQhXgXOUa\
JCYurJtvbCbqZGeDMmVIoqukBs2KugNJ6j5WlTPoeFnMl6Guy9uH2iWWxGg8ZgT2xspqVl5\
CwujjC+m7Dh1toVkvu+bAw\
",
)
.unwrap()
}
}