Change sq key adopt to support the key store.
- Change `sq key adopt` to use the key store. - See #205.
This commit is contained in:
parent
54ae8eda30
commit
f3037392f6
1
NEWS
1
NEWS
@ -58,6 +58,7 @@
|
|||||||
- `sq pki certify` can now use the cert store and the key store.
|
- `sq pki certify` can now use the cert store and the key store.
|
||||||
- In `sq key adopt`, change the certificate file parameter from a
|
- In `sq key adopt`, change the certificate file parameter from a
|
||||||
positional parameter to a named parameter, `--cert-file`.
|
positional parameter to a named parameter, `--cert-file`.
|
||||||
|
- `sq key adopt` can now use the key store.
|
||||||
* Changes in 0.36.0
|
* Changes in 0.36.0
|
||||||
- Missing
|
- Missing
|
||||||
* Changes in 0.35.0
|
* Changes in 0.35.0
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
|
|
||||||
use openpgp::cert::amalgamation::ValidAmalgamation;
|
use openpgp::cert::amalgamation::ValidAmalgamation;
|
||||||
use openpgp::cert::CertParser;
|
|
||||||
use openpgp::packet::key;
|
use openpgp::packet::key;
|
||||||
use openpgp::packet::signature::subpacket::SubpacketTag;
|
use openpgp::packet::signature::subpacket::SubpacketTag;
|
||||||
use openpgp::packet::signature::SignatureBuilder;
|
use openpgp::packet::signature::SignatureBuilder;
|
||||||
@ -21,63 +18,48 @@ use sequoia_openpgp as openpgp;
|
|||||||
|
|
||||||
use crate::Sq;
|
use crate::Sq;
|
||||||
use crate::cli;
|
use crate::cli;
|
||||||
use crate::decrypt_key;
|
|
||||||
|
|
||||||
pub fn adopt(sq: Sq, command: cli::key::AdoptCommand) -> Result<()>
|
pub fn adopt(sq: Sq, command: cli::key::AdoptCommand) -> Result<()>
|
||||||
{
|
{
|
||||||
let input = command.cert_file.open()?;
|
let input = command.cert_file.open()?;
|
||||||
let cert = Cert::from_buffered_reader(input)?;
|
let cert = Cert::from_buffered_reader(input)?;
|
||||||
let mut wanted: Vec<(
|
|
||||||
KeyHandle,
|
|
||||||
Option<(
|
|
||||||
Key<key::PublicParts, key::SubordinateRole>,
|
|
||||||
SignatureBuilder,
|
|
||||||
)>,
|
|
||||||
)> = command.key
|
|
||||||
.into_iter()
|
|
||||||
.map(|kh| (kh, None))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let null_policy = &openpgp::policy::NullPolicy::new();
|
let null_policy_;
|
||||||
let adoptee_policy: &dyn Policy = if command.allow_broken_crypto {
|
let adoptee_policy: &dyn Policy = if command.allow_broken_crypto {
|
||||||
null_policy
|
null_policy_ = openpgp::policy::NullPolicy::new();
|
||||||
|
&null_policy_
|
||||||
} else {
|
} else {
|
||||||
sq.policy
|
sq.policy
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the corresponding keys.
|
// Find the corresponding keys.
|
||||||
for keyring in sq.keyrings.iter() {
|
let wanted: Vec<(
|
||||||
for cert in CertParser::from_file(&keyring)
|
KeyHandle,
|
||||||
.context(format!("Parsing: {}", &keyring.display()))?
|
Result<(
|
||||||
|
Cert,
|
||||||
|
Key<key::PublicParts, key::SubordinateRole>,
|
||||||
|
SignatureBuilder,
|
||||||
|
)>,
|
||||||
|
)> = command.key
|
||||||
|
.into_iter()
|
||||||
|
.map(|kh| {
|
||||||
|
let cert = match sq.lookup_one_with_policy(
|
||||||
|
kh.clone(), None, false, adoptee_policy, sq.time)
|
||||||
{
|
{
|
||||||
let cert = cert.context(format!("Parsing {}", keyring.display()))?;
|
Ok(cert) => cert,
|
||||||
|
Err(err) => return (kh, Err(err)),
|
||||||
let vc = match cert.with_policy(adoptee_policy, None) {
|
|
||||||
Ok(vc) => vc,
|
|
||||||
Err(err) => {
|
|
||||||
wprintln!(
|
|
||||||
"Ignoring {} from '{}': {}",
|
|
||||||
cert.keyid().to_hex(),
|
|
||||||
keyring.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for key in vc.keys() {
|
let vc = match cert.with_policy(adoptee_policy, sq.time) {
|
||||||
for (id, ref mut keyo) in wanted.iter_mut() {
|
Ok(vc) => vc,
|
||||||
if id.aliases(key.key_handle()) {
|
Err(err) => return (kh, Err(err)),
|
||||||
match keyo {
|
};
|
||||||
Some((_, _)) =>
|
|
||||||
// We already saw this key.
|
let key = vc.keys().key_handle(kh.clone())
|
||||||
{
|
.next().expect("have key");
|
||||||
()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let sig = key.binding_signature();
|
let sig = key.binding_signature();
|
||||||
let builder: SignatureBuilder = match sig.typ()
|
let builder: SignatureBuilder = match sig.typ() {
|
||||||
{
|
|
||||||
SignatureType::SubkeyBinding => {
|
SignatureType::SubkeyBinding => {
|
||||||
sig.clone().into()
|
sig.clone().into()
|
||||||
}
|
}
|
||||||
@ -86,67 +68,68 @@ pub fn adopt(sq: Sq, command: cli::key::AdoptCommand) -> Result<()>
|
|||||||
| SignatureType::CasualCertification
|
| SignatureType::CasualCertification
|
||||||
| SignatureType::PersonaCertification
|
| SignatureType::PersonaCertification
|
||||||
| SignatureType::GenericCertification => {
|
| SignatureType::GenericCertification => {
|
||||||
// Convert to a binding
|
// Convert to a binding signature.
|
||||||
// signature.
|
let kf = match sig.key_flags().context(
|
||||||
let kf = sig.key_flags().context(
|
"Missing required subpacket, KeyFlags")
|
||||||
"Missing required \
|
{
|
||||||
subpacket, KeyFlags",
|
Ok(kh) => kh,
|
||||||
)?;
|
Err(err) => return (kh, Err(err)),
|
||||||
SignatureBuilder::new(
|
};
|
||||||
SignatureType::SubkeyBinding,
|
match SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||||
)
|
.set_key_flags(kf)
|
||||||
.set_key_flags(kf)?
|
{
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(err) => return (kh, Err(err)),
|
||||||
}
|
}
|
||||||
_ => panic!(
|
}
|
||||||
"Unsupported binding \
|
_ => panic!("Unsupported binding signature: {:?}", sig),
|
||||||
signature: {:?}",
|
|
||||||
sig
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let builder = builder
|
let builder = match builder.set_signature_creation_time(sq.time) {
|
||||||
.set_signature_creation_time(sq.time)?;
|
Ok(b) => b,
|
||||||
|
Err(err) => return (kh, Err(err)),
|
||||||
|
};
|
||||||
|
|
||||||
*keyo = Some((
|
let key = key.key().clone().role_into_subordinate();
|
||||||
key.key().clone().role_into_subordinate(),
|
|
||||||
builder,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are missing any keys, stop now.
|
(kh, Ok((cert, key, builder)))
|
||||||
let missing: Vec<&KeyHandle> = wanted
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(id, keyo)| match keyo {
|
|
||||||
Some(_) => None,
|
|
||||||
None => Some(id),
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if !missing.is_empty() {
|
|
||||||
return Err(anyhow::anyhow!(
|
// If we are missing any keys, stop now.
|
||||||
"Keys not found: {}",
|
let mut missing = false;
|
||||||
missing.iter().map(|&h| h.to_hex()).join(", ")
|
let wanted = wanted.into_iter()
|
||||||
));
|
.filter_map(|(id, keyo)| {
|
||||||
|
match keyo {
|
||||||
|
Ok((cert, key, builder)) => Some((cert, key, builder)),
|
||||||
|
Err(err) => {
|
||||||
|
if ! missing {
|
||||||
|
eprintln!("Missing keys:");
|
||||||
}
|
}
|
||||||
|
|
||||||
let passwords = &mut Vec::new();
|
eprintln!(" - {}: {}", id, err);
|
||||||
|
|
||||||
|
missing = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if missing {
|
||||||
|
return Err(anyhow::anyhow!("Missing some keys"));
|
||||||
|
}
|
||||||
|
|
||||||
// Get a signer.
|
// Get a signer.
|
||||||
let pk = cert.primary_key().key();
|
let pk = cert.primary_key().key();
|
||||||
let mut pk_signer =
|
let mut pk_signer = sq.get_primary_key(&cert, None)
|
||||||
decrypt_key(pk.clone().parts_into_secret()?, passwords)?
|
.with_context(|| {
|
||||||
.into_keypair()?;
|
format!("Getting signer for {}'s primary key",
|
||||||
|
cert.fingerprint())
|
||||||
|
})?.0;
|
||||||
|
|
||||||
// Add the keys and signatures to cert.
|
// Add the keys and signatures to cert.
|
||||||
let mut packets: Vec<Packet> = vec![];
|
let mut packets: Vec<Packet> = vec![];
|
||||||
for (_, ka) in wanted.into_iter() {
|
for (cert, key, mut builder) in wanted.into_iter() {
|
||||||
let (key, mut builder) = ka.expect("Checked for missing keys above.");
|
|
||||||
|
|
||||||
// Set key expiration.
|
// Set key expiration.
|
||||||
if let Some(e) = &command.expire {
|
if let Some(e) = &command.expire {
|
||||||
builder = builder.set_key_expiration_time(&key, e.timestamp())?;
|
builder = builder.set_key_expiration_time(&key, e.timestamp())?;
|
||||||
@ -160,9 +143,14 @@ pub fn adopt(sq: Sq, command: cli::key::AdoptCommand) -> Result<()>
|
|||||||
|
|
||||||
if need_backsig {
|
if need_backsig {
|
||||||
// Derive a signer.
|
// Derive a signer.
|
||||||
let mut subkey_signer =
|
let ka = cert.keys().key_handle(key.fingerprint())
|
||||||
decrypt_key(key.clone().parts_into_secret()?, passwords)?
|
.next()
|
||||||
.into_keypair()?;
|
.expect("have key");
|
||||||
|
|
||||||
|
let mut subkey_signer = sq.get_signer(&ka)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("Getting signer for {}", ka.fingerprint())
|
||||||
|
})?.0;
|
||||||
|
|
||||||
let backsig = builder
|
let backsig = builder
|
||||||
.embedded_signatures()
|
.embedded_signatures()
|
||||||
|
@ -143,9 +143,17 @@ mod integration {
|
|||||||
fn adopt_encryption() -> Result<()> {
|
fn adopt_encryption() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Have Bob adopt alice's encryption subkey.
|
// Have Bob adopt alice's encryption subkey.
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice() ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[ alice_encryption().0.clone() ].to_vec(),
|
[ alice_encryption().0.clone() ].to_vec(),
|
||||||
None,
|
None,
|
||||||
@ -156,6 +164,7 @@ mod integration {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
check(&cert, 2, (bob_primary(), &[alice_encryption()])).is_ok());
|
check(&cert, 2, (bob_primary(), &[alice_encryption()])).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -164,9 +173,17 @@ mod integration {
|
|||||||
fn adopt_signing() -> Result<()> {
|
fn adopt_signing() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt a signing subkey (subkey has secret key material).
|
// Adopt a signing subkey (subkey has secret key material).
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice() ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[ alice_signing().0.clone() ].to_vec(),
|
[ alice_signing().0.clone() ].to_vec(),
|
||||||
None,
|
None,
|
||||||
@ -177,6 +194,7 @@ mod integration {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
check(&cert, 2, (bob_primary(), &[alice_signing()])).is_ok());
|
check(&cert, 2, (bob_primary(), &[alice_signing()])).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -185,10 +203,18 @@ mod integration {
|
|||||||
fn adopt_certification() -> Result<()> {
|
fn adopt_certification() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt a certification subkey (subkey has secret key
|
// Adopt a certification subkey (subkey has secret key
|
||||||
// material).
|
// material).
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice() ].to_vec(),
|
keyrings,
|
||||||
carol(),
|
carol(),
|
||||||
[ alice_primary().0.clone() ].to_vec(),
|
[ alice_primary().0.clone() ].to_vec(),
|
||||||
None,
|
None,
|
||||||
@ -198,6 +224,7 @@ mod integration {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(check(&cert, 4, (carol_primary(), &[alice_primary()])).is_ok());
|
assert!(check(&cert, 4, (carol_primary(), &[alice_primary()])).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -207,8 +234,16 @@ mod integration {
|
|||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
// Adopt an encryption subkey and a signing subkey.
|
// Adopt an encryption subkey and a signing subkey.
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice() ]
|
||||||
|
};
|
||||||
|
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice() ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[
|
[
|
||||||
alice_signing().0.clone(),
|
alice_signing().0.clone(),
|
||||||
@ -225,6 +260,7 @@ mod integration {
|
|||||||
(bob_primary(),
|
(bob_primary(),
|
||||||
&[alice_signing(), alice_encryption()]))
|
&[alice_signing(), alice_encryption()]))
|
||||||
.is_ok());
|
.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -233,9 +269,17 @@ mod integration {
|
|||||||
fn adopt_twice() -> Result<()> {
|
fn adopt_twice() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt the same an encryption subkey twice.
|
// Adopt the same an encryption subkey twice.
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice() ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[
|
[
|
||||||
alice_encryption().0.clone(),
|
alice_encryption().0.clone(),
|
||||||
@ -249,6 +293,7 @@ mod integration {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
check(&cert, 2, (bob_primary(), &[alice_encryption()])).is_ok());
|
check(&cert, 2, (bob_primary(), &[alice_encryption()])).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -280,9 +325,17 @@ mod integration {
|
|||||||
fn adopt_own_encryption() -> Result<()> {
|
fn adopt_own_encryption() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt its own encryption subkey. This should be a noop.
|
// Adopt its own encryption subkey. This should be a noop.
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice(), ].to_vec(),
|
keyrings,
|
||||||
alice(),
|
alice(),
|
||||||
[
|
[
|
||||||
alice_encryption().0.clone(),
|
alice_encryption().0.clone(),
|
||||||
@ -295,6 +348,7 @@ mod integration {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
check(&cert, 3, (alice_primary(), &[alice_encryption()])).is_ok());
|
check(&cert, 3, (alice_primary(), &[alice_encryption()])).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -303,9 +357,17 @@ mod integration {
|
|||||||
fn adopt_own_primary() -> Result<()> {
|
fn adopt_own_primary() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(bob());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ bob() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt own primary key.
|
// Adopt own primary key.
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ bob(), ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[
|
[
|
||||||
bob_primary().0.clone(),
|
bob_primary().0.clone(),
|
||||||
@ -318,6 +380,7 @@ mod integration {
|
|||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
check(&cert, 2, (bob_primary(), &[bob_primary()])).is_ok());
|
check(&cert, 2, (bob_primary(), &[bob_primary()])).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -326,9 +389,17 @@ mod integration {
|
|||||||
fn adopt_missing() -> Result<()> {
|
fn adopt_missing() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(bob());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ bob() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt a key that is not present.
|
// Adopt a key that is not present.
|
||||||
let r = sq.key_adopt(
|
let r = sq.key_adopt(
|
||||||
[ bob(), ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[
|
[
|
||||||
"1234 5678 90AB CDEF 1234 5678 90AB CDEF"
|
"1234 5678 90AB CDEF 1234 5678 90AB CDEF"
|
||||||
@ -341,6 +412,7 @@ mod integration {
|
|||||||
false);
|
false);
|
||||||
|
|
||||||
assert!(r.is_err());
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -349,9 +421,18 @@ mod integration {
|
|||||||
fn adopt_from_multiple() -> Result<()> {
|
fn adopt_from_multiple() -> Result<()> {
|
||||||
let sq = Sq::new();
|
let sq = Sq::new();
|
||||||
|
|
||||||
|
for keystore in [false, true] {
|
||||||
|
let keyrings = if keystore {
|
||||||
|
sq.key_import(alice());
|
||||||
|
sq.key_import(carol());
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
vec![ alice(), carol() ]
|
||||||
|
};
|
||||||
|
|
||||||
// Adopt own primary key.
|
// Adopt own primary key.
|
||||||
let cert = sq.key_adopt(
|
let cert = sq.key_adopt(
|
||||||
[ alice(), carol(), ].to_vec(),
|
keyrings,
|
||||||
bob(),
|
bob(),
|
||||||
[
|
[
|
||||||
alice_signing().0.clone(),
|
alice_signing().0.clone(),
|
||||||
@ -373,6 +454,7 @@ mod integration {
|
|||||||
carol_signing(), carol_encryption()
|
carol_signing(), carol_encryption()
|
||||||
]))
|
]))
|
||||||
.is_ok());
|
.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user