crypto: WIP genrealize the sas so it can handle in-room and to-device events.
parent
b0ac9d3320
commit
7570cf5ac2
|
@ -112,17 +112,23 @@ impl VerificationMachine {
|
|||
&self,
|
||||
recipient: &UserId,
|
||||
recipient_device: &DeviceId,
|
||||
content: AnyToDeviceEventContent,
|
||||
content: OutgoingContent,
|
||||
) {
|
||||
let request = content_to_request(recipient, recipient_device, content);
|
||||
let request_id = request.txn_id;
|
||||
match content {
|
||||
OutgoingContent::ToDevice(c) => {
|
||||
let request = content_to_request(recipient, recipient_device, c);
|
||||
let request_id = request.txn_id;
|
||||
|
||||
let request = OutgoingRequest {
|
||||
request_id,
|
||||
request: Arc::new(request.into()),
|
||||
};
|
||||
let request = OutgoingRequest {
|
||||
request_id,
|
||||
request: Arc::new(request.into()),
|
||||
};
|
||||
|
||||
self.outgoing_to_device_messages.insert(request_id, request);
|
||||
self.outgoing_to_device_messages.insert(request_id, request);
|
||||
}
|
||||
|
||||
OutgoingContent::Room(c) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_event_helper(&self, sas: &Sas, event: &AnyToDeviceEvent) {
|
||||
|
@ -165,29 +171,41 @@ impl VerificationMachine {
|
|||
room_id: &RoomId,
|
||||
event: &AnySyncRoomEvent,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
match event {
|
||||
AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(m)) => {
|
||||
if let MessageEventContent::VerificationRequest(r) = &m.content {
|
||||
if self.account.user_id() == &r.to {
|
||||
info!(
|
||||
"Received a new verification request from {} {}",
|
||||
m.sender, r.from_device
|
||||
);
|
||||
if let AnySyncRoomEvent::Message(m) = event {
|
||||
match m {
|
||||
AnySyncMessageEvent::RoomMessage(m) => {
|
||||
if let MessageEventContent::VerificationRequest(r) = &m.content {
|
||||
if self.account.user_id() == &r.to {
|
||||
info!(
|
||||
"Received a new verification request from {} {}",
|
||||
m.sender, r.from_device
|
||||
);
|
||||
|
||||
let request = VerificationRequest::from_request_event(
|
||||
self.account.clone(),
|
||||
self.store.clone(),
|
||||
room_id,
|
||||
&m.sender,
|
||||
&m.event_id,
|
||||
r,
|
||||
);
|
||||
let request = VerificationRequest::from_request_event(
|
||||
self.account.clone(),
|
||||
self.private_identity.lock().await.clone(),
|
||||
self.store.clone(),
|
||||
room_id,
|
||||
&m.sender,
|
||||
&m.event_id,
|
||||
r,
|
||||
);
|
||||
|
||||
self.requests.insert(m.event_id.clone(), request);
|
||||
self.requests.insert(m.event_id.clone(), request);
|
||||
}
|
||||
}
|
||||
}
|
||||
AnySyncMessageEvent::KeyVerificationStart(e) => {
|
||||
info!(
|
||||
"Received a new verification start event from {} {}",
|
||||
e.sender, e.content.from_device
|
||||
);
|
||||
|
||||
if let Some((_, request)) = self.requests.remove(&e.content.relation.event_id) {
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -215,7 +233,8 @@ impl VerificationMachine {
|
|||
private_identity,
|
||||
d,
|
||||
self.store.clone(),
|
||||
e,
|
||||
&e.sender,
|
||||
e.content.clone(),
|
||||
self.store.get_user_identity(&e.sender).await?,
|
||||
) {
|
||||
Ok(s) => {
|
||||
|
|
|
@ -23,6 +23,7 @@ use matrix_sdk_common::{
|
|||
ready::ReadyEventContent, start::StartEventContent, Relation, VerificationMethod,
|
||||
},
|
||||
room::message::KeyVerificationRequestEventContent,
|
||||
MessageEvent,
|
||||
},
|
||||
identifiers::{DeviceId, DeviceIdBox, EventId, RoomId, UserId},
|
||||
};
|
||||
|
@ -42,6 +43,7 @@ const SUPPORTED_METHODS: &[VerificationMethod] = &[VerificationMethod::MSasV1];
|
|||
pub struct VerificationRequest {
|
||||
inner: Arc<Mutex<InnerRequest>>,
|
||||
account: ReadOnlyAccount,
|
||||
private_cross_signing_identity: PrivateCrossSigningIdentity,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
room_id: Arc<RoomId>,
|
||||
}
|
||||
|
@ -49,6 +51,7 @@ pub struct VerificationRequest {
|
|||
impl VerificationRequest {
|
||||
pub(crate) fn from_request_event(
|
||||
account: ReadOnlyAccount,
|
||||
private_cross_signing_identity: PrivateCrossSigningIdentity,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
room_id: &RoomId,
|
||||
sender: &UserId,
|
||||
|
@ -66,6 +69,7 @@ impl VerificationRequest {
|
|||
),
|
||||
))),
|
||||
account,
|
||||
private_cross_signing_identity,
|
||||
store,
|
||||
room_id: room_id.clone().into(),
|
||||
}
|
||||
|
@ -80,6 +84,25 @@ impl VerificationRequest {
|
|||
pub fn accept(&self) -> Option<ReadyEventContent> {
|
||||
self.inner.lock().unwrap().accept()
|
||||
}
|
||||
|
||||
pub(crate) fn into_started_sas(
|
||||
&self,
|
||||
event: &MessageEvent<StartEventContent>,
|
||||
device: ReadOnlyDevice,
|
||||
user_identity: Option<UserIdentities>,
|
||||
) -> Result<Sas, OutgoingContent> {
|
||||
match &*self.inner.lock().unwrap() {
|
||||
InnerRequest::Ready(s) => s.into_started_sas(
|
||||
event,
|
||||
self.store.clone(),
|
||||
self.account.clone(),
|
||||
self.private_cross_signing_identity.clone(),
|
||||
device,
|
||||
user_identity,
|
||||
),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -102,6 +125,29 @@ impl InnerRequest {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn into_started_sas(
|
||||
&mut self,
|
||||
event: &MessageEvent<StartEventContent>,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
account: ReadOnlyAccount,
|
||||
private_identity: PrivateCrossSigningIdentity,
|
||||
other_device: ReadOnlyDevice,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> Result<Option<Sas>, OutgoingContent> {
|
||||
if let InnerRequest::Ready(s) = self {
|
||||
Ok(Some(s.into_started_sas(
|
||||
event,
|
||||
store,
|
||||
account,
|
||||
private_identity,
|
||||
other_device,
|
||||
other_identity,
|
||||
)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -252,14 +298,23 @@ struct Ready {
|
|||
|
||||
impl RequestState<Ready> {
|
||||
fn into_started_sas(
|
||||
self,
|
||||
&self,
|
||||
event: &MessageEvent<StartEventContent>,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
account: ReadOnlyAccount,
|
||||
private_identity: PrivateCrossSigningIdentity,
|
||||
other_device: ReadOnlyDevice,
|
||||
other_identity: UserIdentity,
|
||||
) -> Sas {
|
||||
todo!()
|
||||
// Sas::from_start_event(account, private_identity, other_device, other_identity, event)
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> Result<Sas, OutgoingContent> {
|
||||
Sas::from_start_event(
|
||||
account,
|
||||
private_identity,
|
||||
other_device,
|
||||
store,
|
||||
&event.sender,
|
||||
event.content.clone(),
|
||||
other_identity,
|
||||
)
|
||||
}
|
||||
|
||||
fn start_sas(
|
||||
|
@ -289,3 +344,6 @@ struct Passive {
|
|||
/// unique id identifying this verification flow.
|
||||
pub flow_id: EventId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Started {}
|
||||
|
|
|
@ -18,12 +18,17 @@ use std::convert::TryInto;
|
|||
|
||||
use matrix_sdk_common::{
|
||||
events::{
|
||||
key::verification::start::{StartEventContent, StartToDeviceEventContent},
|
||||
key::verification::{
|
||||
start::{StartEventContent, StartMethod, StartToDeviceEventContent},
|
||||
KeyAgreementProtocol,
|
||||
},
|
||||
AnyMessageEventContent, AnyToDeviceEventContent, MessageEvent, ToDeviceEvent,
|
||||
},
|
||||
CanonicalJsonValue,
|
||||
};
|
||||
|
||||
use super::FlowId;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum StartContent {
|
||||
ToDevice(StartToDeviceEventContent),
|
||||
|
@ -31,6 +36,20 @@ pub enum StartContent {
|
|||
}
|
||||
|
||||
impl StartContent {
|
||||
pub fn method(&self) -> &StartMethod {
|
||||
match self {
|
||||
StartContent::ToDevice(c) => &c.method,
|
||||
StartContent::Room(c) => &c.method,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flow_id(&self) -> FlowId {
|
||||
match self {
|
||||
StartContent::ToDevice(c) => FlowId::ToDevice(c.transaction_id.clone()),
|
||||
StartContent::Room(c) => FlowId::InRoom(c.relation.event_id.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_canonical_json(self) -> CanonicalJsonValue {
|
||||
let content = match self {
|
||||
StartContent::Room(c) => serde_json::to_value(c),
|
||||
|
@ -65,12 +84,20 @@ pub enum OutgoingContent {
|
|||
impl From<StartContent> for OutgoingContent {
|
||||
fn from(content: StartContent) -> Self {
|
||||
match content {
|
||||
StartContent::Room(c) => {
|
||||
OutgoingContent::Room(AnyMessageEventContent::KeyVerificationStart(c))
|
||||
}
|
||||
StartContent::ToDevice(c) => {
|
||||
OutgoingContent::ToDevice(AnyToDeviceEventContent::KeyVerificationStart(c))
|
||||
}
|
||||
StartContent::Room(c) => AnyMessageEventContent::KeyVerificationStart(c).into(),
|
||||
StartContent::ToDevice(c) => AnyToDeviceEventContent::KeyVerificationStart(c).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyToDeviceEventContent> for OutgoingContent {
|
||||
fn from(content: AnyToDeviceEventContent) -> Self {
|
||||
OutgoingContent::ToDevice(content)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyMessageEventContent> for OutgoingContent {
|
||||
fn from(content: AnyMessageEventContent) -> Self {
|
||||
OutgoingContent::Room(content)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,12 +20,14 @@ use std::sync::Arc;
|
|||
use matrix_sdk_common::{
|
||||
events::{
|
||||
key::verification::{
|
||||
accept::AcceptToDeviceEventContent, cancel::CancelCode, mac::MacToDeviceEventContent,
|
||||
start::StartToDeviceEventContent,
|
||||
accept::AcceptToDeviceEventContent,
|
||||
cancel::CancelCode,
|
||||
mac::MacToDeviceEventContent,
|
||||
start::{StartEventContent, StartToDeviceEventContent},
|
||||
},
|
||||
AnyToDeviceEvent, AnyToDeviceEventContent, ToDeviceEvent,
|
||||
AnyToDeviceEvent, AnyToDeviceEventContent, MessageEvent, ToDeviceEvent,
|
||||
},
|
||||
identifiers::EventId,
|
||||
identifiers::{EventId, UserId},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -39,6 +41,7 @@ use super::{
|
|||
Accepted, Canceled, Confirmed, Created, Done, FlowId, KeyReceived, MacReceived, SasState,
|
||||
Started,
|
||||
},
|
||||
StartContent,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -78,10 +81,17 @@ impl InnerSas {
|
|||
pub fn from_start_event(
|
||||
account: ReadOnlyAccount,
|
||||
other_device: ReadOnlyDevice,
|
||||
event: &ToDeviceEvent<StartToDeviceEventContent>,
|
||||
sender: &UserId,
|
||||
content: impl Into<StartContent>,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> Result<InnerSas, AnyToDeviceEventContent> {
|
||||
match SasState::<Started>::from_start_event(account, other_device, event, other_identity) {
|
||||
) -> Result<InnerSas, OutgoingContent> {
|
||||
match SasState::<Started>::from_start_event(
|
||||
account,
|
||||
other_device,
|
||||
other_identity,
|
||||
&sender,
|
||||
content,
|
||||
) {
|
||||
Ok(s) => Ok(InnerSas::Started(s)),
|
||||
Err(s) => Err(s.as_content()),
|
||||
}
|
||||
|
@ -110,7 +120,7 @@ impl InnerSas {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn cancel(self, code: CancelCode) -> (InnerSas, Option<AnyToDeviceEventContent>) {
|
||||
pub fn cancel(self, code: CancelCode) -> (InnerSas, Option<OutgoingContent>) {
|
||||
let sas = match self {
|
||||
InnerSas::Created(s) => s.cancel(code),
|
||||
InnerSas::Started(s) => s.cancel(code),
|
||||
|
@ -141,10 +151,7 @@ impl InnerSas {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn receive_event(
|
||||
self,
|
||||
event: &AnyToDeviceEvent,
|
||||
) -> (InnerSas, Option<AnyToDeviceEventContent>) {
|
||||
pub fn receive_event(self, event: &AnyToDeviceEvent) -> (InnerSas, Option<OutgoingContent>) {
|
||||
match event {
|
||||
AnyToDeviceEvent::KeyVerificationAccept(e) => {
|
||||
if let InnerSas::Created(s) = self {
|
||||
|
@ -153,7 +160,7 @@ impl InnerSas {
|
|||
let content = s.as_content();
|
||||
(
|
||||
InnerSas::Accepted(s),
|
||||
Some(AnyToDeviceEventContent::KeyVerificationKey(content)),
|
||||
Some(AnyToDeviceEventContent::KeyVerificationKey(content).into()),
|
||||
)
|
||||
}
|
||||
Err(s) => {
|
||||
|
@ -178,7 +185,7 @@ impl InnerSas {
|
|||
let content = s.as_content();
|
||||
(
|
||||
InnerSas::KeyRecieved(s),
|
||||
Some(AnyToDeviceEventContent::KeyVerificationKey(content)),
|
||||
Some(AnyToDeviceEventContent::KeyVerificationKey(content).into()),
|
||||
)
|
||||
}
|
||||
Err(s) => {
|
||||
|
|
|
@ -30,7 +30,7 @@ use matrix_sdk_common::{
|
|||
cancel::CancelCode,
|
||||
start::{StartEventContent, StartToDeviceEventContent},
|
||||
},
|
||||
AnyToDeviceEvent, AnyToDeviceEventContent, ToDeviceEvent,
|
||||
AnyToDeviceEvent, AnyToDeviceEventContent, MessageEvent, ToDeviceEvent,
|
||||
},
|
||||
identifiers::{DeviceId, EventId, RoomId, UserId},
|
||||
};
|
||||
|
@ -220,15 +220,18 @@ impl Sas {
|
|||
private_identity: PrivateCrossSigningIdentity,
|
||||
other_device: ReadOnlyDevice,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
event: &ToDeviceEvent<StartToDeviceEventContent>,
|
||||
sender: &UserId,
|
||||
content: impl Into<StartContent>,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> Result<Sas, AnyToDeviceEventContent> {
|
||||
) -> Result<Sas, OutgoingContent> {
|
||||
let inner = InnerSas::from_start_event(
|
||||
account.clone(),
|
||||
other_device.clone(),
|
||||
event,
|
||||
&sender,
|
||||
content,
|
||||
other_identity.clone(),
|
||||
)?;
|
||||
|
||||
let flow_id = inner.verification_flow_id();
|
||||
|
||||
Ok(Sas {
|
||||
|
@ -520,7 +523,13 @@ impl Sas {
|
|||
let (sas, content) = sas.cancel(CancelCode::User);
|
||||
*guard = sas;
|
||||
|
||||
content.map(|c| self.content_to_request(c))
|
||||
content.map(|c| {
|
||||
if let OutgoingContent::ToDevice(c) = c {
|
||||
self.content_to_request(c)
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_if_timed_out(&self) -> Option<ToDeviceRequest> {
|
||||
|
@ -531,7 +540,13 @@ impl Sas {
|
|||
let sas: InnerSas = (*guard).clone();
|
||||
let (sas, content) = sas.cancel(CancelCode::Timeout);
|
||||
*guard = sas;
|
||||
content.map(|c| self.content_to_request(c))
|
||||
content.map(|c| {
|
||||
if let OutgoingContent::ToDevice(c) = c {
|
||||
self.content_to_request(c)
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -574,10 +589,7 @@ impl Sas {
|
|||
self.inner.lock().unwrap().decimals()
|
||||
}
|
||||
|
||||
pub(crate) fn receive_event(
|
||||
&self,
|
||||
event: &AnyToDeviceEvent,
|
||||
) -> Option<AnyToDeviceEventContent> {
|
||||
pub(crate) fn receive_event(&self, event: &AnyToDeviceEvent) -> Option<OutgoingContent> {
|
||||
let mut guard = self.inner.lock().unwrap();
|
||||
let sas: InnerSas = (*guard).clone();
|
||||
let (sas, content) = sas.receive_event(event);
|
||||
|
|
|
@ -37,7 +37,7 @@ use matrix_sdk_common::{
|
|||
HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, Relation,
|
||||
ShortAuthenticationString, VerificationMethod,
|
||||
},
|
||||
AnyMessageEventContent, AnyToDeviceEventContent, ToDeviceEvent,
|
||||
AnyMessageEventContent, AnyToDeviceEventContent, MessageEvent, ToDeviceEvent,
|
||||
},
|
||||
identifiers::{DeviceId, EventId, UserId},
|
||||
uuid::Uuid,
|
||||
|
@ -447,7 +447,7 @@ impl SasState<Created> {
|
|||
}
|
||||
|
||||
impl SasState<Started> {
|
||||
/// Create a new SAS verification flow from a m.key.verification.start
|
||||
/// Create a new SAS verification flow from an in-room m.key.verification.start
|
||||
/// event.
|
||||
///
|
||||
/// This will put us in the `started` state.
|
||||
|
@ -463,18 +463,35 @@ impl SasState<Started> {
|
|||
pub fn from_start_event(
|
||||
account: ReadOnlyAccount,
|
||||
other_device: ReadOnlyDevice,
|
||||
event: &ToDeviceEvent<StartToDeviceEventContent>,
|
||||
other_identity: Option<UserIdentities>,
|
||||
sender: &UserId,
|
||||
content: impl Into<StartContent>,
|
||||
) -> Result<SasState<Started>, SasState<Canceled>> {
|
||||
if let StartMethod::MSasV1(content) = &event.content.method {
|
||||
Self::from_start_helper(
|
||||
account,
|
||||
other_device,
|
||||
other_identity,
|
||||
sender,
|
||||
&content.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn from_start_helper(
|
||||
account: ReadOnlyAccount,
|
||||
other_device: ReadOnlyDevice,
|
||||
other_identity: Option<UserIdentities>,
|
||||
sender: &UserId,
|
||||
content: &StartContent,
|
||||
) -> Result<SasState<Started>, SasState<Canceled>> {
|
||||
if let StartMethod::MSasV1(method_content) = content.method() {
|
||||
let sas = OlmSas::new();
|
||||
|
||||
let pubkey = sas.public_key();
|
||||
let commitment = calculate_commitment(&pubkey, event.content.clone());
|
||||
let commitment = calculate_commitment(&pubkey, content.clone());
|
||||
|
||||
error!(
|
||||
"Calculated commitment for pubkey {} and content {:?} {}",
|
||||
pubkey, event.content, commitment
|
||||
pubkey, content, commitment
|
||||
);
|
||||
|
||||
let sas = SasState {
|
||||
|
@ -489,25 +506,25 @@ impl SasState<Started> {
|
|||
creation_time: Arc::new(Instant::now()),
|
||||
last_event_time: Arc::new(Instant::now()),
|
||||
|
||||
verification_flow_id: FlowId::ToDevice(event.content.transaction_id.clone()).into(),
|
||||
verification_flow_id: content.flow_id().into(),
|
||||
|
||||
state: Arc::new(Started {
|
||||
protocol_definitions: content.clone(),
|
||||
protocol_definitions: method_content.clone(),
|
||||
commitment,
|
||||
}),
|
||||
};
|
||||
|
||||
if !content
|
||||
if !method_content
|
||||
.key_agreement_protocols
|
||||
.contains(&KeyAgreementProtocol::Curve25519HkdfSha256)
|
||||
|| !content
|
||||
|| !method_content
|
||||
.message_authentication_codes
|
||||
.contains(&MessageAuthenticationCode::HkdfHmacSha256)
|
||||
|| !content.hashes.contains(&HashAlgorithm::Sha256)
|
||||
|| (!content
|
||||
|| !method_content.hashes.contains(&HashAlgorithm::Sha256)
|
||||
|| (!method_content
|
||||
.short_authentication_string
|
||||
.contains(&ShortAuthenticationString::Decimal)
|
||||
&& !content
|
||||
&& !method_content
|
||||
.short_authentication_string
|
||||
.contains(&ShortAuthenticationString::Emoji))
|
||||
{
|
||||
|
@ -528,7 +545,7 @@ impl SasState<Started> {
|
|||
other_identity,
|
||||
},
|
||||
|
||||
verification_flow_id: FlowId::ToDevice(event.content.transaction_id.clone()).into(),
|
||||
verification_flow_id: content.flow_id().into(),
|
||||
state: Arc::new(Canceled::new(CancelCode::UnknownMethod)),
|
||||
})
|
||||
}
|
||||
|
@ -914,7 +931,7 @@ impl Canceled {
|
|||
}
|
||||
|
||||
impl SasState<Canceled> {
|
||||
pub fn as_content(&self) -> AnyToDeviceEventContent {
|
||||
pub fn as_content(&self) -> OutgoingContent {
|
||||
match self.verification_flow_id.as_ref() {
|
||||
FlowId::ToDevice(s) => {
|
||||
AnyToDeviceEventContent::KeyVerificationCancel(CancelToDeviceEventContent {
|
||||
|
@ -922,6 +939,7 @@ impl SasState<Canceled> {
|
|||
reason: self.state.reason.to_string(),
|
||||
code: self.state.cancel_code.clone(),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue