mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-02-09 16:33:10 +00:00
Remove InstallationKeyPair
This commit is contained in:
parent
143cb38052
commit
3797eca0bf
@ -53,6 +53,11 @@ impl PrivateKey32 {
|
||||
pub fn random() -> PrivateKey32 {
|
||||
Self::random_from_rng(&mut OsRng)
|
||||
}
|
||||
|
||||
// Convenience function to generate a PublicKey32
|
||||
pub fn public_key(&self) -> PublicKey32 {
|
||||
PublicKey32::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for PrivateKey32 {
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
use double_ratchets::{InstallationKeyPair, RatchetState, hkdf::PrivateV1Domain};
|
||||
use crypto::PrivateKey32;
|
||||
use double_ratchets::{RatchetState, hkdf::PrivateV1Domain};
|
||||
|
||||
fn main() {
|
||||
// === Initial shared secret (X3DH / prekey result in real systems) ===
|
||||
let shared_secret = [42u8; 32];
|
||||
|
||||
let bob_dh = InstallationKeyPair::generate();
|
||||
let bob_dh = PrivateKey32::random();
|
||||
|
||||
let mut alice: RatchetState<PrivateV1Domain> =
|
||||
RatchetState::init_sender(shared_secret, bob_dh.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_dh.public_key());
|
||||
let mut bob: RatchetState<PrivateV1Domain> = RatchetState::init_receiver(shared_secret, bob_dh);
|
||||
|
||||
let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!");
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
//! Demonstrates out-of-order message handling with skipped keys persistence.
|
||||
//!
|
||||
//! Run with: cargo run --example out_of_order_demo --features storage
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crypto::PrivateKey32;
|
||||
#[cfg(feature = "storage")]
|
||||
use double_ratchets::{
|
||||
InstallationKeyPair, RatchetState, SqliteStorage, StorageConfig, hkdf::DefaultDomain,
|
||||
state::Header,
|
||||
RatchetState, SqliteStorage, StorageConfig, hkdf::DefaultDomain, state::Header,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@ -22,10 +22,10 @@ fn run_demo() {
|
||||
|
||||
// Setup
|
||||
let shared_secret = [0x42u8; 32];
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = PrivateKey32::random();
|
||||
|
||||
let alice_state: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
let bob_state: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_receiver(shared_secret, bob_keypair);
|
||||
|
||||
@ -81,9 +81,9 @@ fn run_demo() {
|
||||
.expect("Failed to create storage");
|
||||
|
||||
// Re-setup
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = PrivateKey32::random();
|
||||
let alice_state: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
let bob_state: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_receiver(shared_secret, bob_keypair);
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
use double_ratchets::{InstallationKeyPair, RatchetState, hkdf::PrivateV1Domain};
|
||||
use double_ratchets::{RatchetState, hkdf::PrivateV1Domain, types::DhPrivateKey};
|
||||
|
||||
fn main() {
|
||||
// === Initial shared secret (X3DH / prekey result in real systems) ===
|
||||
let shared_secret = [42u8; 32];
|
||||
|
||||
let bob_dh = InstallationKeyPair::generate();
|
||||
let bob_dh = DhPrivateKey::random();
|
||||
|
||||
let mut alice: RatchetState<PrivateV1Domain> =
|
||||
RatchetState::init_sender(shared_secret, bob_dh.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_dh.public_key().clone());
|
||||
let mut bob: RatchetState<PrivateV1Domain> = RatchetState::init_receiver(shared_secret, bob_dh);
|
||||
|
||||
let (ciphertext, header) = alice.encrypt_message(b"Hello Bob!");
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
//! Run with: cargo run --example storage_demo --features storage
|
||||
//! For SQLCipher: cargo run --example storage_demo --features sqlcipher
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crypto::PrivateKey32;
|
||||
#[cfg(feature = "storage")]
|
||||
use double_ratchets::{
|
||||
InstallationKeyPair, RatchetSession, SqliteStorage, StorageConfig, hkdf::PrivateV1Domain,
|
||||
};
|
||||
use double_ratchets::{RatchetSession, SqliteStorage, StorageConfig, hkdf::PrivateV1Domain};
|
||||
|
||||
fn main() {
|
||||
println!("=== Double Ratchet Storage Demo ===\n");
|
||||
@ -140,7 +140,7 @@ fn ensure_tmp_directory() {
|
||||
fn run_conversation(alice_storage: &mut SqliteStorage, bob_storage: &mut SqliteStorage) {
|
||||
// === Setup: Simulate X3DH key exchange ===
|
||||
let shared_secret = [0x42u8; 32]; // In reality, this comes from X3DH
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = PrivateKey32::random();
|
||||
|
||||
let conv_id = "conv1";
|
||||
|
||||
@ -148,7 +148,7 @@ fn run_conversation(alice_storage: &mut SqliteStorage, bob_storage: &mut SqliteS
|
||||
alice_storage,
|
||||
conv_id,
|
||||
shared_secret,
|
||||
bob_keypair.public().clone(),
|
||||
bob_keypair.public_key(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
use crypto::PrivateKey32;
|
||||
use safer_ffi::prelude::*;
|
||||
|
||||
use crate::InstallationKeyPair;
|
||||
|
||||
#[derive_ReprC]
|
||||
#[repr(opaque)]
|
||||
pub struct FFIInstallationKeyPair(pub(crate) InstallationKeyPair);
|
||||
pub struct FFIInstallationKeyPair(pub(crate) PrivateKey32);
|
||||
|
||||
#[ffi_export]
|
||||
fn installation_key_pair_generate() -> repr_c::Box<FFIInstallationKeyPair> {
|
||||
Box::new(FFIInstallationKeyPair(InstallationKeyPair::generate())).into()
|
||||
Box::new(FFIInstallationKeyPair(PrivateKey32::random())).into()
|
||||
}
|
||||
|
||||
#[ffi_export]
|
||||
fn installation_key_pair_public(keypair: &FFIInstallationKeyPair) -> [u8; 32] {
|
||||
keypair.0.public().clone().to_bytes()
|
||||
keypair.0.public_key().to_bytes()
|
||||
}
|
||||
|
||||
#[ffi_export]
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
use crypto::{PrivateKey32, PublicKey32};
|
||||
use rand_core::OsRng;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
use crate::types::SharedSecret;
|
||||
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct InstallationKeyPair {
|
||||
secret: PrivateKey32,
|
||||
public: PublicKey32,
|
||||
}
|
||||
|
||||
impl InstallationKeyPair {
|
||||
pub fn generate() -> Self {
|
||||
let secret = PrivateKey32::random_from_rng(OsRng);
|
||||
let public = PublicKey32::from(&secret);
|
||||
Self { secret, public }
|
||||
}
|
||||
|
||||
pub fn dh(&self, their_public: &PublicKey32) -> SharedSecret {
|
||||
self.secret.diffie_hellman(their_public).to_bytes()
|
||||
}
|
||||
|
||||
pub fn public(&self) -> &PublicKey32 {
|
||||
&self.public
|
||||
}
|
||||
|
||||
/// Export the secret key as raw bytes for serialization/storage.
|
||||
pub fn secret_bytes(&self) -> &[u8; 32] {
|
||||
self.secret.as_bytes()
|
||||
}
|
||||
|
||||
/// Import the secret key from raw bytes.
|
||||
pub fn from_secret_bytes(bytes: [u8; 32]) -> Self {
|
||||
let secret = PrivateKey32::from(bytes);
|
||||
let public = PublicKey32::from(&secret);
|
||||
Self { secret, public }
|
||||
}
|
||||
}
|
||||
@ -2,14 +2,12 @@ pub mod aead;
|
||||
pub mod errors;
|
||||
pub mod ffi;
|
||||
pub mod hkdf;
|
||||
pub mod keypair;
|
||||
pub mod reader;
|
||||
pub mod state;
|
||||
#[cfg(feature = "storage")]
|
||||
pub mod storage;
|
||||
pub mod types;
|
||||
|
||||
pub use keypair::InstallationKeyPair;
|
||||
pub use state::{Header, RatchetState};
|
||||
#[cfg(feature = "storage")]
|
||||
pub use storage::{RatchetSession, SessionError, SqliteStorage, StorageConfig, StorageError};
|
||||
|
||||
@ -8,9 +8,8 @@ use crate::{
|
||||
aead::{decrypt, encrypt},
|
||||
errors::RatchetError,
|
||||
hkdf::{DefaultDomain, HkdfInfo, kdf_chain, kdf_root},
|
||||
keypair::InstallationKeyPair,
|
||||
reader::Reader,
|
||||
types::{ChainKey, MessageKey, Nonce, RootKey, SharedSecret},
|
||||
types::{ChainKey, DhPrivateKey, MessageKey, Nonce, RootKey, SharedSecret},
|
||||
};
|
||||
|
||||
/// Current binary format version.
|
||||
@ -28,7 +27,7 @@ pub struct RatchetState<D: HkdfInfo = DefaultDomain> {
|
||||
pub sending_chain: Option<ChainKey>,
|
||||
pub receiving_chain: Option<ChainKey>,
|
||||
|
||||
pub dh_self: InstallationKeyPair,
|
||||
pub dh_self: DhPrivateKey,
|
||||
pub dh_remote: Option<PublicKey32>,
|
||||
|
||||
pub msg_send: u32,
|
||||
@ -104,8 +103,7 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
write_option(&mut buf, self.sending_chain);
|
||||
write_option(&mut buf, self.receiving_chain);
|
||||
|
||||
let dh_secret = self.dh_self.secret_bytes();
|
||||
buf.extend_from_slice(dh_secret);
|
||||
buf.extend_from_slice(self.dh_self.as_bytes());
|
||||
|
||||
write_option(&mut buf, dh_remote);
|
||||
|
||||
@ -141,7 +139,7 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
let receiving_chain = reader.read_option()?;
|
||||
|
||||
let mut dh_self_bytes: [u8; 32] = reader.read_array()?;
|
||||
let dh_self = InstallationKeyPair::from_secret_bytes(dh_self_bytes);
|
||||
let dh_self = DhPrivateKey::from(dh_self_bytes);
|
||||
dh_self_bytes.zeroize();
|
||||
|
||||
let dh_remote = reader.read_option()?.map(PublicKey32::from);
|
||||
@ -234,11 +232,11 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
///
|
||||
/// A new `RatchetState` ready to send the first message.
|
||||
pub fn init_sender(shared_secret: SharedSecret, remote_pub: PublicKey32) -> Self {
|
||||
let dh_self = InstallationKeyPair::generate();
|
||||
let dh_self = DhPrivateKey::random();
|
||||
|
||||
// Initial DH
|
||||
let dh_out = dh_self.dh(&remote_pub);
|
||||
let (root_key, sending_chain) = kdf_root::<D>(&shared_secret, &dh_out);
|
||||
let dh_out = dh_self.diffie_hellman(&remote_pub);
|
||||
let (root_key, sending_chain) = kdf_root::<D>(&shared_secret, dh_out.as_bytes());
|
||||
|
||||
Self {
|
||||
root_key,
|
||||
@ -271,7 +269,7 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `RatchetState` ready to receive the first message.
|
||||
pub fn init_receiver(shared_secret: SharedSecret, dh_self: InstallationKeyPair) -> Self {
|
||||
pub fn init_receiver(shared_secret: SharedSecret, dh_self: DhPrivateKey) -> Self {
|
||||
Self {
|
||||
root_key: shared_secret,
|
||||
|
||||
@ -297,8 +295,8 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
///
|
||||
/// * `remote_pub` - The new DH public key from the sender.
|
||||
pub fn dh_ratchet_receive(&mut self, remote_pub: PublicKey32) {
|
||||
let dh_out = self.dh_self.dh(&remote_pub);
|
||||
let (new_root, recv_chain) = kdf_root::<D>(&self.root_key, &dh_out);
|
||||
let dh_out = self.dh_self.diffie_hellman(&remote_pub);
|
||||
let (new_root, recv_chain) = kdf_root::<D>(&self.root_key, dh_out.as_bytes());
|
||||
|
||||
self.root_key = new_root;
|
||||
self.receiving_chain = Some(recv_chain);
|
||||
@ -312,9 +310,9 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
pub fn dh_ratchet_send(&mut self) {
|
||||
let remote = self.dh_remote.expect("no remote DH key");
|
||||
|
||||
self.dh_self = InstallationKeyPair::generate();
|
||||
let dh_out = self.dh_self.dh(&remote);
|
||||
let (new_root, send_chain) = kdf_root::<D>(&self.root_key, &dh_out);
|
||||
self.dh_self = DhPrivateKey::random();
|
||||
let dh_out = self.dh_self.diffie_hellman(&remote);
|
||||
let (new_root, send_chain) = kdf_root::<D>(&self.root_key, dh_out.as_bytes());
|
||||
|
||||
self.root_key = new_root;
|
||||
self.sending_chain = Some(send_chain);
|
||||
@ -345,7 +343,7 @@ impl<D: HkdfInfo> RatchetState<D> {
|
||||
*chain = next_chain;
|
||||
|
||||
let header = Header {
|
||||
dh_pub: self.dh_self.public().clone(),
|
||||
dh_pub: PublicKey32::from(&self.dh_self),
|
||||
msg_num: self.msg_send,
|
||||
prev_chain_len: self.prev_chain_len,
|
||||
};
|
||||
@ -478,10 +476,10 @@ mod tests {
|
||||
let shared_secret = [0x42; 32];
|
||||
|
||||
// Bob generates his long-term keypair
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = DhPrivateKey::random();
|
||||
|
||||
// Alice initializes as sender, knowing Bob's public key
|
||||
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
|
||||
// Bob initializes as receiver with his private key
|
||||
let bob = RatchetState::init_receiver(shared_secret, bob_keypair);
|
||||
@ -554,7 +552,7 @@ mod tests {
|
||||
bob.decrypt_message(&ct, header).unwrap();
|
||||
|
||||
// Bob performs DH ratchet by trying to send
|
||||
let old_bob_pub = bob.dh_self.public().clone();
|
||||
let old_bob_pub = bob.dh_self.public_key();
|
||||
let (bob_ct, bob_header) = {
|
||||
let mut b = bob.clone();
|
||||
b.encrypt_message(b"reply")
|
||||
@ -562,7 +560,7 @@ mod tests {
|
||||
assert_ne!(bob_header.dh_pub, old_bob_pub);
|
||||
|
||||
// Alice receives Bob's message with new DH pub → ratchets
|
||||
let old_alice_pub = alice.dh_self.public().clone();
|
||||
let old_alice_pub = alice.dh_self.public_key();
|
||||
let old_root = alice.root_key;
|
||||
|
||||
// Even if decrypt fails (wrong key), ratchet should happen
|
||||
@ -688,8 +686,8 @@ mod tests {
|
||||
restored.dh_remote.map(|pk| pk.to_bytes())
|
||||
);
|
||||
assert_eq!(
|
||||
alice.dh_self.public().to_bytes(),
|
||||
restored.dh_self.public().to_bytes()
|
||||
alice.dh_self.public_key().to_bytes(),
|
||||
restored.dh_self.public_key().to_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
use crypto::PublicKey32;
|
||||
|
||||
use crate::{
|
||||
InstallationKeyPair,
|
||||
errors::RatchetError,
|
||||
hkdf::HkdfInfo,
|
||||
state::{Header, RatchetState},
|
||||
types::SharedSecret,
|
||||
types::{DhPrivateKey, SharedSecret},
|
||||
};
|
||||
|
||||
use super::{SqliteStorage, StorageError};
|
||||
@ -93,7 +92,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> {
|
||||
storage: &'a mut SqliteStorage,
|
||||
conversation_id: impl Into<String>,
|
||||
shared_secret: SharedSecret,
|
||||
dh_self: InstallationKeyPair,
|
||||
dh_self: DhPrivateKey,
|
||||
) -> Result<Self, StorageError> {
|
||||
let conversation_id = conversation_id.into();
|
||||
if storage.exists(&conversation_id)? {
|
||||
@ -180,7 +179,7 @@ impl<'a, D: HkdfInfo + Clone> RatchetSession<'a, D> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{hkdf::DefaultDomain, keypair::InstallationKeyPair, storage::StorageConfig};
|
||||
use crate::{hkdf::DefaultDomain, storage::StorageConfig, types::DhPrivateKey};
|
||||
|
||||
fn create_test_storage() -> SqliteStorage {
|
||||
SqliteStorage::new(StorageConfig::InMemory).unwrap()
|
||||
@ -191,9 +190,9 @@ mod tests {
|
||||
let mut storage = create_test_storage();
|
||||
|
||||
let shared_secret = [0x42; 32];
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = DhPrivateKey::random();
|
||||
let alice: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
|
||||
// Create session
|
||||
{
|
||||
@ -214,9 +213,9 @@ mod tests {
|
||||
let mut storage = create_test_storage();
|
||||
|
||||
let shared_secret = [0x42; 32];
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = DhPrivateKey::random();
|
||||
let alice: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
|
||||
// Create and encrypt
|
||||
{
|
||||
@ -238,9 +237,9 @@ mod tests {
|
||||
let mut storage = create_test_storage();
|
||||
|
||||
let shared_secret = [0x42; 32];
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_keypair = DhPrivateKey::random();
|
||||
let alice: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
let bob: RatchetState<DefaultDomain> =
|
||||
RatchetState::init_receiver(shared_secret, bob_keypair);
|
||||
|
||||
@ -278,8 +277,8 @@ mod tests {
|
||||
let mut storage = create_test_storage();
|
||||
|
||||
let shared_secret = [0x42; 32];
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let bob_pub = bob_keypair.public().clone();
|
||||
let bob_keypair = DhPrivateKey::random();
|
||||
let bob_pub = bob_keypair.public_key();
|
||||
|
||||
// First call creates
|
||||
{
|
||||
|
||||
@ -280,7 +280,7 @@ fn blob_to_array<const N: usize>(blob: Vec<u8>) -> [u8; N] {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{hkdf::DefaultDomain, keypair::InstallationKeyPair};
|
||||
use crate::hkdf::DefaultDomain;
|
||||
|
||||
fn create_test_storage() -> SqliteStorage {
|
||||
SqliteStorage::new(StorageConfig::InMemory).unwrap()
|
||||
@ -288,8 +288,8 @@ mod tests {
|
||||
|
||||
fn create_test_state() -> (RatchetState<DefaultDomain>, RatchetState<DefaultDomain>) {
|
||||
let shared_secret = [0x42; 32];
|
||||
let bob_keypair = InstallationKeyPair::generate();
|
||||
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public().clone());
|
||||
let bob_keypair = DhPrivateKey::random();
|
||||
let alice = RatchetState::init_sender(shared_secret, bob_keypair.public_key());
|
||||
let bob = RatchetState::init_receiver(shared_secret, bob_keypair);
|
||||
(alice, bob)
|
||||
}
|
||||
@ -309,8 +309,8 @@ mod tests {
|
||||
assert_eq!(alice.msg_recv, loaded.msg_recv);
|
||||
assert_eq!(alice.prev_chain_len, loaded.prev_chain_len);
|
||||
assert_eq!(
|
||||
alice.dh_self.public().to_bytes(),
|
||||
loaded.dh_self.public().to_bytes()
|
||||
alice.dh_self.public_key().as_bytes(),
|
||||
loaded.dh_self.public_key().as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -53,11 +53,10 @@ impl<D: HkdfInfo> From<&RatchetState<D>> for RatchetStateRecord {
|
||||
|
||||
impl RatchetStateRecord {
|
||||
pub fn into_ratchet_state<D: HkdfInfo>(self, skipped_keys: Vec<SkippedKey>) -> RatchetState<D> {
|
||||
use crate::keypair::InstallationKeyPair;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
let dh_self = InstallationKeyPair::from_secret_bytes(self.dh_self_secret);
|
||||
let dh_self = DhPrivateKey::from(self.dh_self_secret);
|
||||
let dh_remote = self.dh_remote.map(PublicKey32::from);
|
||||
|
||||
let skipped: HashMap<(PublicKey32, u32), MessageKey> = skipped_keys
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
use crypto::PrivateKey32;
|
||||
|
||||
/// Type alias for diffie-hellman private keys.
|
||||
pub type DhPrivateKey = PrivateKey32;
|
||||
|
||||
/// Type alias for root keys (32 bytes).
|
||||
pub type RootKey = [u8; 32];
|
||||
/// Type alias for chain keys (sending/receiving, 32 bytes).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user