mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-14 11:05:32 -04:00
feat(crypto): Add customized event type for the forwarded room key
This commit is contained in:
@@ -250,6 +250,8 @@ pub fn migrate(
|
||||
InboundGroupSession::from_libolm_pickle(&session.pickle, &data.pickle_key)?.pickle();
|
||||
|
||||
let sender_key = Curve25519PublicKey::from_base64(&session.sender_key)?;
|
||||
let forwarding_chains: Result<Vec<Curve25519PublicKey>, _> =
|
||||
session.forwarding_chains.iter().map(|k| Curve25519PublicKey::from_base64(k)).collect();
|
||||
|
||||
let pickle = matrix_sdk_crypto::olm::PickledInboundGroupSession {
|
||||
pickle,
|
||||
@@ -260,7 +262,7 @@ pub fn migrate(
|
||||
.map(|(k, v)| Ok((DeviceKeyAlgorithm::try_from(k)?, v)))
|
||||
.collect::<anyhow::Result<_>>()?,
|
||||
room_id: RoomId::parse(session.room_id)?,
|
||||
forwarding_chains: session.forwarding_chains,
|
||||
forwarding_chains: forwarding_chains?,
|
||||
imported: session.imported,
|
||||
backed_up: session.backed_up,
|
||||
history_visibility: None,
|
||||
|
||||
@@ -26,7 +26,6 @@ use dashmap::{mapref::entry::Entry, DashMap, DashSet};
|
||||
use ruma::{
|
||||
api::client::keys::claim_keys::v3::Request as KeysClaimRequest,
|
||||
events::{
|
||||
forwarded_room_key::{ToDeviceForwardedRoomKeyEvent, ToDeviceForwardedRoomKeyEventContent},
|
||||
room_key_request::{Action, RequestedKeyInfo, ToDeviceRoomKeyRequestEvent},
|
||||
secret::request::{
|
||||
RequestAction, SecretName, ToDeviceSecretRequestEvent as SecretRequestEvent,
|
||||
@@ -46,6 +45,9 @@ use crate::{
|
||||
session_manager::GroupSessionCache,
|
||||
store::{Changes, CryptoStoreError, SecretImportError, Store},
|
||||
types::events::{
|
||||
forwarded_room_key::{
|
||||
ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent, ForwardedRoomKeyEvent,
|
||||
},
|
||||
secret_send::{SecretSendContent, SecretSendEvent},
|
||||
EventType,
|
||||
},
|
||||
@@ -700,12 +702,12 @@ impl GossipMachine {
|
||||
/// Get an outgoing key info that matches the forwarded room key content.
|
||||
async fn get_key_info(
|
||||
&self,
|
||||
content: &ToDeviceForwardedRoomKeyEventContent,
|
||||
content: &ForwardedMegolmV1AesSha2Content,
|
||||
) -> Result<Option<GossipRequest>, CryptoStoreError> {
|
||||
let info = RequestedKeyInfo::new(
|
||||
content.algorithm.clone(),
|
||||
EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
content.room_id.clone(),
|
||||
content.sender_key.clone(),
|
||||
content.claimed_sender_key.to_base64(),
|
||||
content.session_id.clone(),
|
||||
)
|
||||
.into();
|
||||
@@ -880,10 +882,11 @@ impl GossipMachine {
|
||||
async fn accept_forwarded_room_key(
|
||||
&self,
|
||||
info: &GossipRequest,
|
||||
sender: &UserId,
|
||||
sender_key: Curve25519PublicKey,
|
||||
event: &ToDeviceForwardedRoomKeyEvent,
|
||||
content: &ForwardedMegolmV1AesSha2Content,
|
||||
) -> Result<Option<InboundGroupSession>, CryptoStoreError> {
|
||||
match InboundGroupSession::from_forwarded_key(sender_key, &event.content) {
|
||||
match InboundGroupSession::from_forwarded_key(sender_key, content) {
|
||||
Ok(session) => {
|
||||
let old_session = self
|
||||
.store
|
||||
@@ -913,19 +916,19 @@ impl GossipMachine {
|
||||
|
||||
if let Some(s) = &session {
|
||||
info!(
|
||||
sender = event.sender.as_str(),
|
||||
%sender,
|
||||
sender_key = sender_key.to_base64(),
|
||||
claimed_sender_key = event.content.sender_key.as_str(),
|
||||
claimed_sender_key = content.claimed_sender_key.to_base64(),
|
||||
room_id = s.room_id().as_str(),
|
||||
session_id = session_id.as_str(),
|
||||
"Received a forwarded room key",
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
sender = event.sender.as_str(),
|
||||
%sender,
|
||||
sender_key = sender_key.to_base64(),
|
||||
claimed_sender_key = event.content.sender_key.as_str(),
|
||||
room_id = event.content.room_id.as_str(),
|
||||
claimed_sender_key = content.claimed_sender_key.to_base64(),
|
||||
room_id = %content.room_id,
|
||||
session_id = session_id.as_str(),
|
||||
"Received a forwarded room key but we already have a better version of it",
|
||||
);
|
||||
@@ -935,10 +938,10 @@ impl GossipMachine {
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
sender = event.sender.as_str(),
|
||||
%sender,
|
||||
sender_key = sender_key.to_base64(),
|
||||
claimed_sender_key = event.content.sender_key.as_str(),
|
||||
room_id = event.content.room_id.as_str(),
|
||||
claimed_sender_key = content.claimed_sender_key.to_base64(),
|
||||
room_id = content.room_id.as_str(),
|
||||
"Couldn't create a group session from a received room key"
|
||||
);
|
||||
Err(e.into())
|
||||
@@ -950,20 +953,36 @@ impl GossipMachine {
|
||||
pub async fn receive_forwarded_room_key(
|
||||
&self,
|
||||
sender_key: Curve25519PublicKey,
|
||||
event: &ToDeviceForwardedRoomKeyEvent,
|
||||
event: &ForwardedRoomKeyEvent,
|
||||
) -> Result<Option<InboundGroupSession>, CryptoStoreError> {
|
||||
if let Some(info) = self.get_key_info(&event.content).await? {
|
||||
self.accept_forwarded_room_key(&info, sender_key, event).await
|
||||
} else {
|
||||
warn!(
|
||||
sender = event.sender.as_str(),
|
||||
sender_key = sender_key.to_base64(),
|
||||
room_id = event.content.room_id.as_str(),
|
||||
session_id = event.content.session_id.as_str(),
|
||||
claimed_sender_key = event.content.sender_key.as_str(),
|
||||
"Received a forwarded room key that we didn't request",
|
||||
);
|
||||
Ok(None)
|
||||
match &event.content {
|
||||
ForwardedRoomKeyContent::MegolmV1AesSha2(content) => {
|
||||
if let Some(info) = self.get_key_info(content).await? {
|
||||
self.accept_forwarded_room_key(&info, &event.sender, sender_key, content).await
|
||||
} else {
|
||||
warn!(
|
||||
sender = event.sender.as_str(),
|
||||
sender_key = sender_key.to_base64(),
|
||||
room_id = content.room_id.as_str(),
|
||||
session_id = content.session_id.as_str(),
|
||||
claimed_sender_key = content.claimed_sender_key.to_base64(),
|
||||
algorithm = %event.algorithm(),
|
||||
"Received a forwarded room key that we didn't request",
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
ForwardedRoomKeyContent::Unknown(_) => {
|
||||
warn!(
|
||||
sender = event.sender.as_str(),
|
||||
sender_key = sender_key.to_base64(),
|
||||
algorithm = %event.algorithm(),
|
||||
"Received a forwarded room key with an unsupported algorithm",
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -979,10 +998,9 @@ mod tests {
|
||||
use ruma::{
|
||||
device_id,
|
||||
events::{
|
||||
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
|
||||
room::encrypted::ToDeviceRoomEncryptedEventContent,
|
||||
secret::request::{RequestAction, SecretName, ToDeviceSecretRequestEventContent},
|
||||
AnyToDeviceEvent, ToDeviceEvent as RumaToDeviceEvent, ToDeviceEventContent,
|
||||
ToDeviceEvent as RumaToDeviceEvent, ToDeviceEventContent,
|
||||
},
|
||||
room_id, user_id, DeviceId, RoomId, UserId,
|
||||
};
|
||||
@@ -994,6 +1012,9 @@ mod tests {
|
||||
olm::{Account, OutboundGroupSession, PrivateCrossSigningIdentity, ReadOnlyAccount},
|
||||
session_manager::GroupSessionCache,
|
||||
store::{Changes, CryptoStore, MemoryStore, Store},
|
||||
types::events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent, ToDeviceEvent, ToDeviceEvents,
|
||||
},
|
||||
utilities::json_convert,
|
||||
verification::VerificationMachine,
|
||||
OutgoingRequest, OutgoingRequests,
|
||||
@@ -1284,9 +1305,10 @@ mod tests {
|
||||
|
||||
let export = session.export_at_index(10).await;
|
||||
|
||||
let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
|
||||
let content: ForwardedRoomKeyContent = export.try_into().unwrap();
|
||||
|
||||
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
|
||||
let event =
|
||||
ToDeviceEvent { sender: alice_id().to_owned(), content, other: Default::default() };
|
||||
|
||||
assert!(machine
|
||||
.store
|
||||
@@ -1332,9 +1354,10 @@ mod tests {
|
||||
|
||||
let export = session.export_at_index(15).await;
|
||||
|
||||
let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
|
||||
let content: ForwardedRoomKeyContent = export.try_into().unwrap();
|
||||
|
||||
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
|
||||
let event =
|
||||
ToDeviceEvent { sender: alice_id().to_owned(), content, other: Default::default() };
|
||||
|
||||
let second_session = machine
|
||||
.receive_forwarded_room_key(alice_device.curve25519_key().unwrap(), &event)
|
||||
@@ -1345,9 +1368,10 @@ mod tests {
|
||||
|
||||
let export = session.export_at_index(0).await;
|
||||
|
||||
let content: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
|
||||
let content: ForwardedRoomKeyContent = export.try_into().unwrap();
|
||||
|
||||
let event = RumaToDeviceEvent { sender: alice_id().to_owned(), content };
|
||||
let event =
|
||||
ToDeviceEvent { sender: alice_id().to_owned(), content, other: Default::default() };
|
||||
|
||||
let second_session = machine
|
||||
.receive_forwarded_room_key(alice_device.curve25519_key().unwrap(), &event)
|
||||
@@ -1516,7 +1540,7 @@ mod tests {
|
||||
|
||||
let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap();
|
||||
|
||||
if let AnyToDeviceEvent::ForwardedRoomKey(e) = decrypted.event.deserialize().unwrap() {
|
||||
if let ToDeviceEvents::ForwardedRoomKey(e) = decrypted.event.deserialize_as().unwrap() {
|
||||
let session =
|
||||
alice_machine.receive_forwarded_room_key(decrypted.sender_key, &e).await.unwrap();
|
||||
alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap();
|
||||
@@ -1674,7 +1698,7 @@ mod tests {
|
||||
|
||||
let decrypted = alice_account.decrypt_to_device_event(&event).await.unwrap();
|
||||
|
||||
if let AnyToDeviceEvent::ForwardedRoomKey(e) = decrypted.event.deserialize().unwrap() {
|
||||
if let ToDeviceEvents::ForwardedRoomKey(e) = decrypted.event.deserialize_as().unwrap() {
|
||||
let session =
|
||||
alice_machine.receive_forwarded_room_key(decrypted.sender_key, &e).await.unwrap();
|
||||
alice_machine.store.save_inbound_group_sessions(&[session.unwrap()]).await.unwrap();
|
||||
|
||||
@@ -26,13 +26,8 @@ use atomic::Atomic;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use ruma::{
|
||||
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
|
||||
events::{
|
||||
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
|
||||
key::verification::VerificationMethod,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, OwnedDeviceId,
|
||||
OwnedDeviceKeyId, UserId,
|
||||
events::key::verification::VerificationMethod, serde::Raw, DeviceId, DeviceKeyAlgorithm,
|
||||
DeviceKeyId, EventEncryptionAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
@@ -48,8 +43,11 @@ use crate::{
|
||||
olm::{InboundGroupSession, Session, SignedJsonObject, VerifyJson},
|
||||
store::{Changes, CryptoStore, DeviceChanges, Result as StoreResult},
|
||||
types::{
|
||||
events::room::encrypted::ToDeviceEncryptedEventContent, DeviceKey, DeviceKeys, Signatures,
|
||||
SignedKey,
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent,
|
||||
room::encrypted::ToDeviceEncryptedEventContent, EventType,
|
||||
},
|
||||
DeviceKey, DeviceKeys, Signatures, SignedKey,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
OutgoingVerificationRequest, ReadOnlyAccount, Sas, ToDeviceRequest, VerificationRequest,
|
||||
@@ -277,7 +275,7 @@ impl Device {
|
||||
session.export().await
|
||||
};
|
||||
|
||||
let content: ToDeviceForwardedRoomKeyEventContent = if let Ok(c) = export.try_into() {
|
||||
let content: ForwardedRoomKeyContent = if let Ok(c) = export.try_into() {
|
||||
c
|
||||
} else {
|
||||
// TODO remove this panic.
|
||||
@@ -290,9 +288,10 @@ impl Device {
|
||||
);
|
||||
};
|
||||
|
||||
let event_type = content.event_type();
|
||||
let content = serde_json::to_value(content)?;
|
||||
|
||||
self.encrypt("m.forwarded_room_key", content).await
|
||||
self.encrypt(event_type, content).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1037,7 +1037,11 @@ impl OlmMachine {
|
||||
algorithm_info: AlgorithmInfo::MegolmV1AesSha2 {
|
||||
curve25519_key: session.sender_key().to_base64(),
|
||||
sender_claimed_keys: session.signing_keys().to_owned(),
|
||||
forwarding_curve25519_key_chain: session.forwarding_key_chain().to_vec(),
|
||||
forwarding_curve25519_key_chain: session
|
||||
.forwarding_key_chain()
|
||||
.iter()
|
||||
.map(|k| k.to_base64())
|
||||
.collect(),
|
||||
},
|
||||
verification_state,
|
||||
})
|
||||
|
||||
@@ -23,10 +23,7 @@ use std::{
|
||||
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use ruma::{
|
||||
events::{
|
||||
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
|
||||
room::history_visibility::HistoryVisibility, AnyRoomEvent,
|
||||
},
|
||||
events::{room::history_visibility::HistoryVisibility, AnyRoomEvent},
|
||||
serde::Raw,
|
||||
DeviceKeyAlgorithm, EventEncryptionAlgorithm, OwnedRoomId, RoomId,
|
||||
};
|
||||
@@ -34,7 +31,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use vodozemac::{
|
||||
megolm::{
|
||||
DecryptedMessage, DecryptionError, ExportedSessionKey, InboundGroupSession as InnerSession,
|
||||
DecryptedMessage, DecryptionError, InboundGroupSession as InnerSession,
|
||||
InboundGroupSessionPickle, MegolmMessage, SessionConfig, SessionOrdering,
|
||||
},
|
||||
Curve25519PublicKey, PickleError,
|
||||
@@ -45,7 +42,10 @@ use crate::{
|
||||
error::{EventError, MegolmResult},
|
||||
types::{
|
||||
deserialize_curve_key,
|
||||
events::room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
|
||||
events::{
|
||||
forwarded_room_key::ForwardedMegolmV1AesSha2Content,
|
||||
room::encrypted::{EncryptedEvent, RoomEventEncryptionScheme},
|
||||
},
|
||||
serialize_curve_key,
|
||||
},
|
||||
};
|
||||
@@ -71,7 +71,7 @@ pub struct InboundGroupSession {
|
||||
pub signing_keys: Arc<BTreeMap<DeviceKeyAlgorithm, String>>,
|
||||
/// The Room this GroupSession belongs to
|
||||
pub room_id: Arc<RoomId>,
|
||||
forwarding_chains: Arc<Vec<String>>,
|
||||
forwarding_chains: Arc<Vec<Curve25519PublicKey>>,
|
||||
imported: bool,
|
||||
algorithm: Arc<EventEncryptionAlgorithm>,
|
||||
backed_up: Arc<AtomicBool>,
|
||||
@@ -165,25 +165,24 @@ impl InboundGroupSession {
|
||||
/// to create the `InboundGroupSession`.
|
||||
pub fn from_forwarded_key(
|
||||
sender_key: Curve25519PublicKey,
|
||||
content: &ToDeviceForwardedRoomKeyEventContent,
|
||||
content: &ForwardedMegolmV1AesSha2Content,
|
||||
) -> Result<Self, SessionCreationError> {
|
||||
let key = ExportedSessionKey::from_base64(&content.session_key)?;
|
||||
let algorithm = EventEncryptionAlgorithm::from(content.algorithm.as_str());
|
||||
let algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
|
||||
|
||||
let session = InnerSession::import(&key, SessionConfig::version_1());
|
||||
let session = InnerSession::import(&content.session_key, SessionConfig::version_1());
|
||||
|
||||
let first_known_index = session.first_known_index();
|
||||
let mut forwarding_chains = content.forwarding_curve25519_key_chain.clone();
|
||||
forwarding_chains.push(sender_key.to_base64());
|
||||
forwarding_chains.push(sender_key);
|
||||
|
||||
let mut sender_claimed_key = BTreeMap::new();
|
||||
sender_claimed_key
|
||||
.insert(DeviceKeyAlgorithm::Ed25519, content.sender_claimed_ed25519_key.to_owned());
|
||||
.insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.to_base64());
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Mutex::new(session).into(),
|
||||
session_id: content.session_id.as_str().into(),
|
||||
sender_key,
|
||||
sender_key: content.claimed_sender_key,
|
||||
first_known_index,
|
||||
history_visibility: None.into(),
|
||||
signing_keys: sender_claimed_key.into(),
|
||||
@@ -256,7 +255,7 @@ impl InboundGroupSession {
|
||||
/// Each ed25519 key represents a single device. If device A forwards the
|
||||
/// session to device B and device B to C this list will contain the ed25519
|
||||
/// keys of A and B.
|
||||
pub fn forwarding_key_chain(&self) -> &[String] {
|
||||
pub fn forwarding_key_chain(&self) -> &[Curve25519PublicKey] {
|
||||
&self.forwarding_chains
|
||||
}
|
||||
|
||||
@@ -446,7 +445,7 @@ pub struct PickledInboundGroupSession {
|
||||
/// The list of claimed ed25519 that forwarded us this key. Will be None if
|
||||
/// we directly received this session.
|
||||
#[serde(default)]
|
||||
pub forwarding_chains: Vec<String>,
|
||||
pub forwarding_chains: Vec<Curve25519PublicKey>,
|
||||
/// Flag remembering if the session was directly sent to us by the sender
|
||||
/// or if it was imported.
|
||||
pub imported: bool,
|
||||
|
||||
@@ -14,12 +14,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{
|
||||
events::forwarded_room_key::{
|
||||
ToDeviceForwardedRoomKeyEventContent, ToDeviceForwardedRoomKeyEventContentInit,
|
||||
},
|
||||
DeviceKeyAlgorithm, EventEncryptionAlgorithm, OwnedRoomId,
|
||||
};
|
||||
use ruma::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, OwnedRoomId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod inbound;
|
||||
@@ -32,10 +27,13 @@ pub use outbound::{
|
||||
};
|
||||
use thiserror::Error;
|
||||
pub use vodozemac::megolm::{ExportedSessionKey, SessionKey};
|
||||
use vodozemac::{megolm::SessionKeyDecodeError, Curve25519PublicKey};
|
||||
use zeroize::Zeroize;
|
||||
use vodozemac::{megolm::SessionKeyDecodeError, Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use crate::types::{deserialize_curve_key, serialize_curve_key};
|
||||
use crate::types::{
|
||||
deserialize_curve_key,
|
||||
events::forwarded_room_key::{ForwardedMegolmV1AesSha2Content, ForwardedRoomKeyContent},
|
||||
serialize_curve_key,
|
||||
};
|
||||
|
||||
/// An error type for the creation of group sessions.
|
||||
#[derive(Debug, Error)]
|
||||
@@ -77,7 +75,7 @@ pub struct ExportedRoomKey {
|
||||
/// Chain of Curve25519 keys through which this session was forwarded, via
|
||||
/// m.forwarded_room_key events.
|
||||
#[serde(default)]
|
||||
pub forwarding_curve25519_key_chain: Vec<String>,
|
||||
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
|
||||
}
|
||||
|
||||
/// A backed up version of an `InboundGroupSession`
|
||||
@@ -101,10 +99,10 @@ pub struct BackedUpRoomKey {
|
||||
|
||||
/// Chain of Curve25519 keys through which this session was forwarded, via
|
||||
/// m.forwarded_room_key events.
|
||||
pub forwarding_curve25519_key_chain: Vec<String>,
|
||||
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
|
||||
}
|
||||
|
||||
impl TryInto<ToDeviceForwardedRoomKeyEventContent> for ExportedRoomKey {
|
||||
impl TryInto<ForwardedRoomKeyContent> for ExportedRoomKey {
|
||||
type Error = ();
|
||||
|
||||
/// Convert an exported room key into a content for a forwarded room key
|
||||
@@ -113,7 +111,7 @@ impl TryInto<ToDeviceForwardedRoomKeyEventContent> for ExportedRoomKey {
|
||||
/// This will fail if the exported room key has multiple sender claimed keys
|
||||
/// or if the algorithm of the claimed sender key isn't
|
||||
/// `DeviceKeyAlgorithm::Ed25519`.
|
||||
fn try_into(self) -> Result<ToDeviceForwardedRoomKeyEventContent, Self::Error> {
|
||||
fn try_into(self) -> Result<ForwardedRoomKeyContent, Self::Error> {
|
||||
if self.sender_claimed_keys.len() != 1 {
|
||||
Err(())
|
||||
} else {
|
||||
@@ -123,16 +121,19 @@ impl TryInto<ToDeviceForwardedRoomKeyEventContent> for ExportedRoomKey {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(ToDeviceForwardedRoomKeyEventContentInit {
|
||||
algorithm: self.algorithm,
|
||||
room_id: self.room_id,
|
||||
sender_key: self.sender_key.to_base64(),
|
||||
session_id: self.session_id,
|
||||
session_key: self.session_key.to_base64(),
|
||||
sender_claimed_ed25519_key: claimed_key.to_owned(),
|
||||
forwarding_curve25519_key_chain: self.forwarding_curve25519_key_chain,
|
||||
}
|
||||
.into())
|
||||
Ok(ForwardedRoomKeyContent::MegolmV1AesSha2(
|
||||
ForwardedMegolmV1AesSha2Content {
|
||||
room_id: self.room_id,
|
||||
session_id: self.session_id,
|
||||
session_key: self.session_key,
|
||||
claimed_sender_key: self.sender_key,
|
||||
claimed_ed25519_key: Ed25519PublicKey::from_base64(claimed_key)
|
||||
.map_err(|_| ())?,
|
||||
forwarding_curve25519_key_chain: self.forwarding_curve25519_key_chain.clone(),
|
||||
other: Default::default(),
|
||||
}
|
||||
.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,29 +150,28 @@ impl From<ExportedRoomKey> for BackedUpRoomKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ToDeviceForwardedRoomKeyEventContent> for ExportedRoomKey {
|
||||
impl TryFrom<ForwardedRoomKeyContent> for ExportedRoomKey {
|
||||
type Error = SessionKeyDecodeError;
|
||||
|
||||
/// Convert the content of a forwarded room key into a exported room key.
|
||||
fn try_from(
|
||||
mut forwarded_key: ToDeviceForwardedRoomKeyEventContent,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let mut sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String> = BTreeMap::new();
|
||||
sender_claimed_keys
|
||||
.insert(DeviceKeyAlgorithm::Ed25519, forwarded_key.sender_claimed_ed25519_key);
|
||||
fn try_from(forwarded_key: ForwardedRoomKeyContent) -> Result<Self, Self::Error> {
|
||||
match forwarded_key {
|
||||
ForwardedRoomKeyContent::MegolmV1AesSha2(content) => {
|
||||
let mut sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String> = BTreeMap::new();
|
||||
sender_claimed_keys
|
||||
.insert(DeviceKeyAlgorithm::Ed25519, content.claimed_ed25519_key.to_base64());
|
||||
|
||||
let session_key = ExportedSessionKey::from_base64(&forwarded_key.session_key)?;
|
||||
forwarded_key.session_key.zeroize();
|
||||
let sender_key = Curve25519PublicKey::from_base64(&forwarded_key.sender_key)?;
|
||||
|
||||
Ok(Self {
|
||||
algorithm: forwarded_key.algorithm,
|
||||
room_id: forwarded_key.room_id,
|
||||
session_id: forwarded_key.session_id,
|
||||
forwarding_curve25519_key_chain: forwarded_key.forwarding_curve25519_key_chain,
|
||||
sender_claimed_keys,
|
||||
sender_key,
|
||||
session_key,
|
||||
})
|
||||
Ok(Self {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
room_id: content.room_id,
|
||||
session_id: content.session_id,
|
||||
forwarding_curve25519_key_chain: content.forwarding_curve25519_key_chain,
|
||||
sender_claimed_keys,
|
||||
sender_key: content.claimed_sender_key,
|
||||
session_key: content.session_key,
|
||||
})
|
||||
}
|
||||
ForwardedRoomKeyContent::Unknown(_) => Err(SessionKeyDecodeError::Version(1, 2)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ pub use group_sessions::{
|
||||
pub use session::{PickledSession, Session};
|
||||
pub use signing::{CrossSigningStatus, PickledCrossSigningIdentity, PrivateCrossSigningIdentity};
|
||||
pub(crate) use utility::{SignedJsonObject, VerifyJson};
|
||||
pub use vodozemac::olm::IdentityKeys;
|
||||
pub use vodozemac::{olm::IdentityKeys, Curve25519PublicKey};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
@@ -43,7 +43,6 @@ pub(crate) mod tests {
|
||||
use ruma::{
|
||||
device_id, event_id,
|
||||
events::{
|
||||
forwarded_room_key::ToDeviceForwardedRoomKeyEventContent,
|
||||
room::message::{Relation, Replacement, RoomMessageEventContent},
|
||||
AnyMessageLikeEvent, AnyRoomEvent, MessageLikeEvent,
|
||||
},
|
||||
@@ -54,7 +53,9 @@ pub(crate) mod tests {
|
||||
|
||||
use crate::{
|
||||
olm::{ExportedRoomKey, InboundGroupSession, ReadOnlyAccount, Session},
|
||||
types::events::room::encrypted::EncryptedEvent,
|
||||
types::events::{
|
||||
forwarded_room_key::ForwardedRoomKeyContent, room::encrypted::EncryptedEvent,
|
||||
},
|
||||
utilities::json_convert,
|
||||
};
|
||||
|
||||
@@ -321,7 +322,7 @@ pub(crate) mod tests {
|
||||
let (_, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
|
||||
|
||||
let export = inbound.export().await;
|
||||
let export: ToDeviceForwardedRoomKeyEventContent = export.try_into().unwrap();
|
||||
let export: ForwardedRoomKeyContent = export.try_into().unwrap();
|
||||
let export = ExportedRoomKey::try_from(export).unwrap();
|
||||
|
||||
let imported = InboundGroupSession::from_export(&export)
|
||||
|
||||
@@ -109,6 +109,8 @@ impl Session {
|
||||
/// encrypted, this needs to be the device that was used to create this
|
||||
/// session with.
|
||||
///
|
||||
/// * `event_type` - The type of the event content.
|
||||
///
|
||||
/// * `content` - The content of the event.
|
||||
pub async fn encrypt(
|
||||
&mut self,
|
||||
|
||||
@@ -18,8 +18,8 @@ macro_rules! cryptostore_integration_tests {
|
||||
testing::{get_device, get_other_identity, get_own_identity},
|
||||
ReadOnlyDevice,
|
||||
olm::{
|
||||
InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity,
|
||||
ReadOnlyAccount, Session,
|
||||
Curve25519PublicKey, InboundGroupSession, OlmMessageHash,
|
||||
PrivateCrossSigningIdentity, ReadOnlyAccount, Session,
|
||||
},
|
||||
store::{CryptoStore, GossipRequest, Changes, DeviceChanges, IdentityChanges, RecoveryKey},
|
||||
};
|
||||
@@ -286,7 +286,8 @@ macro_rules! cryptostore_integration_tests {
|
||||
|
||||
let mut export = session.export().await;
|
||||
|
||||
export.forwarding_curve25519_key_chain = vec!["some_chain".to_owned()];
|
||||
let curve_key = Curve25519PublicKey::from_base64("Nn0L2hkcCMFKqynTjyGsJbth7QrVmX3lbrksMkrGOAw").unwrap();
|
||||
export.forwarding_curve25519_key_chain = vec![curve_key];
|
||||
|
||||
let session = InboundGroupSession::from_export(&export).unwrap();
|
||||
|
||||
|
||||
190
crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs
Normal file
190
crates/matrix-sdk-crypto/src/types/events/forwarded_room_key.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Types for `m.forwarded_room_key` to-device events.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::{EventEncryptionAlgorithm, OwnedRoomId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use vodozemac::{megolm::ExportedSessionKey, Curve25519PublicKey, Ed25519PublicKey};
|
||||
|
||||
use super::{EventType, ToDeviceEvent};
|
||||
use crate::types::{
|
||||
deserialize_curve_key, deserialize_curve_key_vec, deserialize_ed25519_key, serialize_curve_key,
|
||||
serialize_curve_key_vec, serialize_ed25519_key,
|
||||
};
|
||||
|
||||
/// The `m.forwarded_room_key` to-device event.
|
||||
pub type ForwardedRoomKeyEvent = ToDeviceEvent<ForwardedRoomKeyContent>;
|
||||
|
||||
impl ForwardedRoomKeyEvent {
|
||||
/// Get the algorithm of the forwarded room key.
|
||||
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
|
||||
self.content.algorithm()
|
||||
}
|
||||
}
|
||||
|
||||
/// The `m.forwarded_room_key` event content.
|
||||
///
|
||||
/// This is an enum over the different room key algorithms we support.
|
||||
///
|
||||
/// This event type is used to forward keys for end-to-end encryption.
|
||||
/// Typically it is encrypted as an m.room.encrypted event, then sent as a
|
||||
/// to-device event.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(try_from = "RoomKeyHelper")]
|
||||
pub enum ForwardedRoomKeyContent {
|
||||
/// The `m.megolm.v1.aes-sha2` variant of the `m.forwarded_room_key`
|
||||
/// content.
|
||||
MegolmV1AesSha2(Box<ForwardedMegolmV1AesSha2Content>),
|
||||
/// An unknown and unsupported variant of the `m.forwarded_room_key`
|
||||
/// content.
|
||||
Unknown(UnknownRoomKeyContent),
|
||||
}
|
||||
|
||||
impl ForwardedRoomKeyContent {
|
||||
/// Get the algorithm of the forwarded room key content.
|
||||
pub fn algorithm(&self) -> EventEncryptionAlgorithm {
|
||||
match self {
|
||||
ForwardedRoomKeyContent::MegolmV1AesSha2(_) => {
|
||||
EventEncryptionAlgorithm::MegolmV1AesSha2
|
||||
}
|
||||
ForwardedRoomKeyContent::Unknown(c) => c.algorithm.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventType for ForwardedRoomKeyContent {
|
||||
const EVENT_TYPE: &'static str = "m.forwarded_room_key";
|
||||
}
|
||||
|
||||
/// The `m.megolm.v1.aes-sha2` variant of the `m.room_key` content.
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct ForwardedMegolmV1AesSha2Content {
|
||||
/// The room where the key is used.
|
||||
pub room_id: OwnedRoomId,
|
||||
|
||||
/// The ID of the session that the key is for.
|
||||
pub session_id: String,
|
||||
|
||||
/// The key to be exchanged. Can be used to create a [`InboundGroupSession`]
|
||||
/// that can be used to decrypt room events.
|
||||
///
|
||||
/// [`InboundGroupSession`]: vodozemac::megolm::InboundGroupSession
|
||||
pub session_key: ExportedSessionKey,
|
||||
|
||||
/// Chain of Curve25519 keys. It starts out empty, but each time the key is
|
||||
/// forwarded to another device, the previous sender in the chain is added
|
||||
/// to the end of the list.
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_curve_key_vec",
|
||||
serialize_with = "serialize_curve_key_vec"
|
||||
)]
|
||||
pub forwarding_curve25519_key_chain: Vec<Curve25519PublicKey>,
|
||||
|
||||
/// The Curve25519 key of the device which initiated the session originally.
|
||||
///
|
||||
/// It is ‘claimed’ because the receiving device has no way to tell that
|
||||
/// the original room_key actually came from a device which owns the private
|
||||
/// part of this key.
|
||||
#[serde(
|
||||
rename = "sender_key",
|
||||
deserialize_with = "deserialize_curve_key",
|
||||
serialize_with = "serialize_curve_key"
|
||||
)]
|
||||
pub claimed_sender_key: Curve25519PublicKey,
|
||||
|
||||
/// The Ed25519 key of the device which initiated the session originally.
|
||||
///
|
||||
/// It is ‘claimed’ because the receiving device has no way to tell that
|
||||
/// the original room_key actually came from a device which owns the private
|
||||
/// part of this key.
|
||||
#[serde(
|
||||
rename = "sender_claimed_ed25519_key",
|
||||
deserialize_with = "deserialize_ed25519_key",
|
||||
serialize_with = "serialize_ed25519_key"
|
||||
)]
|
||||
pub claimed_ed25519_key: Ed25519PublicKey,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub(crate) other: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
/// An unknown and unsupported `m.room_key` algorithm.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct UnknownRoomKeyContent {
|
||||
/// The algorithm of the unknown room key.
|
||||
algorithm: EventEncryptionAlgorithm,
|
||||
/// The other data of the unknown room key.
|
||||
#[serde(flatten)]
|
||||
other: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ForwardedMegolmV1AesSha2Content {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ForwardedMegolmV1AesSha2Content")
|
||||
.field("room_id", &self.room_id)
|
||||
.field("session_id", &self.session_id)
|
||||
.field("forwarding_curve25519_key_chain", &self.forwarding_curve25519_key_chain)
|
||||
.field("claimed_sender_key", &self.claimed_sender_key)
|
||||
.field("claimed_ed25519_key", &self.claimed_ed25519_key)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct RoomKeyHelper {
|
||||
algorithm: EventEncryptionAlgorithm,
|
||||
#[serde(flatten)]
|
||||
other: Value,
|
||||
}
|
||||
|
||||
impl TryFrom<RoomKeyHelper> for ForwardedRoomKeyContent {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(value: RoomKeyHelper) -> Result<Self, Self::Error> {
|
||||
Ok(match value.algorithm {
|
||||
EventEncryptionAlgorithm::MegolmV1AesSha2 => {
|
||||
let content: ForwardedMegolmV1AesSha2Content = serde_json::from_value(value.other)?;
|
||||
Self::MegolmV1AesSha2(content.into())
|
||||
}
|
||||
_ => Self::Unknown(UnknownRoomKeyContent {
|
||||
algorithm: value.algorithm,
|
||||
other: serde_json::from_value(value.other)?,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ForwardedRoomKeyContent {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let helper = match self {
|
||||
Self::MegolmV1AesSha2(r) => RoomKeyHelper {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
other: serde_json::to_value(r).map_err(serde::ser::Error::custom)?,
|
||||
},
|
||||
Self::Unknown(r) => RoomKeyHelper {
|
||||
algorithm: r.algorithm.clone(),
|
||||
other: serde_json::to_value(r.other.clone()).map_err(serde::ser::Error::custom)?,
|
||||
},
|
||||
};
|
||||
|
||||
helper.serialize(serializer)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
//! types. Once deserialized they aim to zeroize all the secret material once
|
||||
//! the type is dropped.
|
||||
|
||||
pub mod forwarded_room_key;
|
||||
pub mod room;
|
||||
pub mod room_key;
|
||||
pub mod secret_send;
|
||||
|
||||
@@ -17,7 +17,6 @@ use std::{collections::BTreeMap, fmt::Debug};
|
||||
use ruma::{
|
||||
events::{
|
||||
dummy::ToDeviceDummyEvent,
|
||||
forwarded_room_key::ToDeviceForwardedRoomKeyEvent,
|
||||
key::verification::{
|
||||
accept::ToDeviceKeyVerificationAcceptEvent, cancel::ToDeviceKeyVerificationCancelEvent,
|
||||
done::ToDeviceKeyVerificationDoneEvent, key::ToDeviceKeyVerificationKeyEvent,
|
||||
@@ -39,7 +38,10 @@ use serde_json::{
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::{
|
||||
room::encrypted::EncryptedToDeviceEvent, room_key::RoomKeyEvent, secret_send::SecretSendEvent,
|
||||
forwarded_room_key::{ForwardedRoomKeyContent, ForwardedRoomKeyEvent},
|
||||
room::encrypted::EncryptedToDeviceEvent,
|
||||
room_key::RoomKeyEvent,
|
||||
secret_send::SecretSendEvent,
|
||||
EventType,
|
||||
};
|
||||
use crate::types::events::from_str;
|
||||
@@ -76,7 +78,7 @@ pub enum ToDeviceEvents {
|
||||
/// The `m.room_key_request` to-device event.
|
||||
RoomKeyRequest(ToDeviceRoomKeyRequestEvent),
|
||||
/// The `m.forwarded_room_key` to-device event.
|
||||
ForwardedRoomKey(ToDeviceForwardedRoomKeyEvent),
|
||||
ForwardedRoomKey(Box<ForwardedRoomKeyEvent>),
|
||||
/// The `m.secret.send` to-device event.
|
||||
SecretSend(SecretSendEvent),
|
||||
/// The `m.secret.request` to-device event.
|
||||
@@ -127,7 +129,7 @@ impl ToDeviceEvents {
|
||||
ToDeviceEvents::RoomEncrypted(_) => ToDeviceEventType::RoomEncrypted,
|
||||
ToDeviceEvents::RoomKey(_) => ToDeviceEventType::RoomKey,
|
||||
ToDeviceEvents::RoomKeyRequest(e) => e.content.event_type(),
|
||||
ToDeviceEvents::ForwardedRoomKey(e) => e.content.event_type(),
|
||||
ToDeviceEvents::ForwardedRoomKey(_) => ToDeviceEventType::ForwardedRoomKey,
|
||||
|
||||
ToDeviceEvents::SecretSend(_) => ToDeviceEventType::SecretSend,
|
||||
ToDeviceEvents::SecretRequest(e) => e.content.event_type(),
|
||||
@@ -197,7 +199,11 @@ impl ToDeviceEvents {
|
||||
Raw::from_json(raw_value)
|
||||
}
|
||||
ToDeviceEvents::ForwardedRoomKey(mut e) => {
|
||||
e.content.session_key.zeroize();
|
||||
match &mut e.content {
|
||||
ForwardedRoomKeyContent::MegolmV1AesSha2(c) => c.session_key.zeroize(),
|
||||
ForwardedRoomKeyContent::Unknown(_) => (),
|
||||
}
|
||||
|
||||
Raw::from_json(to_raw_value(&e)?)
|
||||
}
|
||||
ToDeviceEvents::SecretSend(mut e) => {
|
||||
@@ -410,7 +416,11 @@ mod test {
|
||||
"sender_claimed_ed25519_key": "aj40p+aw64yPIdsxoog8jhPu9i7l7NcFRecuOQblE3Y",
|
||||
"sender_key": "RF3s+E7RkTQTGF2d8Deol0FkQvgII2aJDf3/Jp5mxVU",
|
||||
"session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
|
||||
"session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8Llf..."
|
||||
"session_key": "AQAAAAq2JpkMceK5f6JrZPJWwzQTn59zliuIv0F7apVLXDcZCCT\
|
||||
3LqBjD21sULYEO5YTKdpMVhi9i6ZSZhdvZvp//tzRpDT7wpWVWI\
|
||||
00Y3EPEjmpm/HfZ4MMAKpk+tzJVuuvfAcHBZgpnxBGzYOc/DAqa\
|
||||
pK7Tk3t3QJ1UMSD94HfAqlb1JF5QBPwoh0fOvD8pJdanB8zxz05\
|
||||
tKFdR73/vo2Q/zE3"
|
||||
},
|
||||
"type": "m.forwarded_room_key"
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ pub use device_keys::*;
|
||||
pub use one_time_keys::*;
|
||||
use ruma::{DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, OwnedUserId, UserId};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519Signature};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519PublicKey, Ed25519Signature, KeyError};
|
||||
|
||||
/// Represents a potentially decoded signature (but *not* a validated one).
|
||||
///
|
||||
@@ -248,3 +248,41 @@ where
|
||||
let key = key.to_base64();
|
||||
s.serialize_str(&key)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_ed25519_key<'de, D>(de: D) -> Result<Ed25519PublicKey, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let key: String = Deserialize::deserialize(de)?;
|
||||
Ed25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_ed25519_key<S>(key: &Ed25519PublicKey, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let key = key.to_base64();
|
||||
s.serialize_str(&key)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_curve_key_vec<'de, D>(de: D) -> Result<Vec<Curve25519PublicKey>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let keys: Vec<String> = Deserialize::deserialize(de)?;
|
||||
let keys: Result<Vec<Curve25519PublicKey>, KeyError> =
|
||||
keys.iter().map(|k| Curve25519PublicKey::from_base64(k)).collect();
|
||||
|
||||
keys.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_curve_key_vec<S>(
|
||||
keys: &[Curve25519PublicKey],
|
||||
s: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let keys: Vec<String> = keys.iter().map(|k| k.to_base64()).collect();
|
||||
keys.serialize(s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user