Don't keep reinitializing the cipher context for gcm
This commit is contained in:
+128
-53
@@ -54,71 +54,82 @@ const char *cert_chain_t::verify(x509_t::element_type *cert) {
|
|||||||
return X509_verify_cert_error_string(err_code);
|
return X509_verify_cert_error_string(err_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
cipher_t::cipher_t(const crypto::aes_t &key) : ctx { EVP_CIPHER_CTX_new() }, key { key }, padding { true } {}
|
namespace cipher {
|
||||||
int cipher_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
|
gcm_t::gcm_t(const crypto::aes_t &key, const crypto::aes_t &iv, bool padding)
|
||||||
int len;
|
: cipher_t { nullptr, nullptr, key, padding } {
|
||||||
|
this->iv = iv;
|
||||||
auto fg = util::fail_guard([this]() {
|
|
||||||
EVP_CIPHER_CTX_reset(ctx.get());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gen 7 servers use 128-bit AES ECB
|
|
||||||
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
|
||||||
|
|
||||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
|
||||||
auto size = (int)plaintext.size();
|
|
||||||
// Encrypt into the caller's buffer, leaving room for the auth tag to be prepended
|
|
||||||
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data(), &len) != 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
plaintext.resize(len + size);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
static int init_decrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||||
std::vector<std::uint8_t> &plaintext) {
|
ctx.reset(EVP_CIPHER_CTX_new());
|
||||||
auto cipher = tagged_cipher.substr(16);
|
|
||||||
auto tag = tagged_cipher.substr(0, 16);
|
|
||||||
|
|
||||||
auto fg = util::fail_guard([this]() {
|
if(!ctx) {
|
||||||
EVP_CIPHER_CTX_reset(ctx.get());
|
return -1;
|
||||||
});
|
}
|
||||||
|
|
||||||
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
if(EVP_DecryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr) != 1) {
|
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key.data(), iv.data()) != 1) {
|
if(EVP_DecryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int init_encrypt_gcm(cipher_ctx_t &ctx, aes_t *key, aes_t *iv, bool padding) {
|
||||||
|
ctx.reset(EVP_CIPHER_CTX_new());
|
||||||
|
|
||||||
|
// Gen 7 servers use 128-bit AES ECB
|
||||||
|
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_gcm(), nullptr, nullptr, nullptr) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv->size(), nullptr) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(EVP_EncryptInit_ex(ctx.get(), nullptr, nullptr, key->data(), iv->data()) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gcm_t::decrypt(const std::string_view &tagged_cipher, std::vector<std::uint8_t> &plaintext) {
|
||||||
|
if(!decrypt_ctx && init_decrypt_gcm(decrypt_ctx, &key, &iv, padding)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calling with cipher == nullptr results in a parameter change
|
||||||
|
// without requiring a reallocation of the internal cipher ctx.
|
||||||
|
if(EVP_DecryptInit_ex(decrypt_ctx.get(), nullptr, nullptr, nullptr, iv.data()) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto cipher = tagged_cipher.substr(16);
|
||||||
|
auto tag = tagged_cipher.substr(0, 16);
|
||||||
|
|
||||||
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||||
|
|
||||||
int size;
|
int size;
|
||||||
if(EVP_DecryptUpdate(ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
|
if(EVP_CIPHER_CTX_ctrl(decrypt_ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast<char *>(tag.data())) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int len = size;
|
int len = size;
|
||||||
if(EVP_DecryptFinal_ex(ctx.get(), plaintext.data() + size, &len) != 1) {
|
if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data() + size, &len) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,28 +137,28 @@ int cipher_t::decrypt_gcm(aes_t &iv, const std::string_view &tagged_cipher,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
int gcm_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
||||||
int len;
|
if(!encrypt_ctx && init_encrypt_gcm(encrypt_ctx, &key, &iv, padding)) {
|
||||||
|
|
||||||
auto fg = util::fail_guard([this]() {
|
|
||||||
EVP_CIPHER_CTX_reset(ctx.get());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gen 7 servers use 128-bit AES ECB
|
|
||||||
if(EVP_EncryptInit_ex(ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
EVP_CIPHER_CTX_set_padding(ctx.get(), padding);
|
// Calling with cipher == nullptr results in a parameter change
|
||||||
|
// without requiring a reallocation of the internal cipher ctx.
|
||||||
|
if(EVP_EncryptInit_ex(encrypt_ctx.get(), nullptr, nullptr, nullptr, iv.data()) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int len;
|
||||||
|
|
||||||
cipher.resize((plaintext.size() + 15) / 16 * 16);
|
cipher.resize((plaintext.size() + 15) / 16 * 16);
|
||||||
auto size = (int)cipher.size();
|
auto size = (int)cipher.size();
|
||||||
|
|
||||||
// Encrypt into the caller's buffer
|
// Encrypt into the caller's buffer
|
||||||
if(EVP_EncryptUpdate(ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(EVP_EncryptFinal_ex(ctx.get(), cipher.data() + size, &len) != 1) {
|
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +166,70 @@ int cipher_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ecb_t::decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext) {
|
||||||
|
int len;
|
||||||
|
|
||||||
|
auto fg = util::fail_guard([this]() {
|
||||||
|
EVP_CIPHER_CTX_reset(decrypt_ctx.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gen 7 servers use 128-bit AES ECB
|
||||||
|
if(EVP_DecryptInit_ex(decrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_set_padding(decrypt_ctx.get(), padding);
|
||||||
|
|
||||||
|
plaintext.resize((cipher.size() + 15) / 16 * 16);
|
||||||
|
auto size = (int)plaintext.size();
|
||||||
|
// Decrypt into the caller's buffer, leaving room for the auth tag to be prepended
|
||||||
|
if(EVP_DecryptUpdate(decrypt_ctx.get(), plaintext.data(), &size, (const std::uint8_t *)cipher.data(), cipher.size()) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(EVP_DecryptFinal_ex(decrypt_ctx.get(), plaintext.data(), &len) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext.resize(len + size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ecb_t::encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher) {
|
||||||
|
auto fg = util::fail_guard([this]() {
|
||||||
|
EVP_CIPHER_CTX_reset(encrypt_ctx.get());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gen 7 servers use 128-bit AES ECB
|
||||||
|
if(EVP_EncryptInit_ex(encrypt_ctx.get(), EVP_aes_128_ecb(), nullptr, key.data(), nullptr) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_set_padding(encrypt_ctx.get(), padding);
|
||||||
|
|
||||||
|
int len;
|
||||||
|
|
||||||
|
cipher.resize((plaintext.size() + 15) / 16 * 16);
|
||||||
|
auto size = (int)cipher.size();
|
||||||
|
|
||||||
|
// Encrypt into the caller's buffer
|
||||||
|
if(EVP_EncryptUpdate(encrypt_ctx.get(), cipher.data(), &size, (const std::uint8_t *)plaintext.data(), plaintext.size()) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(EVP_EncryptFinal_ex(encrypt_ctx.get(), cipher.data() + size, &len) != 1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.resize(len + size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ecb_t::ecb_t(const aes_t &key, bool padding)
|
||||||
|
: cipher_t { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_new(), key, padding } {}
|
||||||
|
|
||||||
|
} // namespace cipher
|
||||||
|
|
||||||
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
|
aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &pin) {
|
||||||
aes_t key;
|
aes_t key;
|
||||||
|
|
||||||
|
|||||||
+33
-11
@@ -66,24 +66,46 @@ private:
|
|||||||
x509_store_ctx_t _cert_ctx;
|
x509_store_ctx_t _cert_ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
namespace cipher {
|
||||||
|
|
||||||
class cipher_t {
|
class cipher_t {
|
||||||
public:
|
public:
|
||||||
cipher_t(const aes_t &key);
|
cipher_ctx_t decrypt_ctx;
|
||||||
cipher_t(cipher_t &&) noexcept = default;
|
cipher_ctx_t encrypt_ctx;
|
||||||
cipher_t &operator=(cipher_t &&) noexcept = default;
|
|
||||||
|
|
||||||
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
|
||||||
|
|
||||||
int decrypt_gcm(aes_t &iv, const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
|
||||||
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
|
||||||
|
|
||||||
private:
|
|
||||||
cipher_ctx_t ctx;
|
|
||||||
aes_t key;
|
aes_t key;
|
||||||
|
|
||||||
public:
|
|
||||||
bool padding;
|
bool padding;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class ecb_t : public cipher_t {
|
||||||
|
public:
|
||||||
|
ecb_t() = default;
|
||||||
|
ecb_t(ecb_t &&) noexcept = default;
|
||||||
|
ecb_t &operator=(ecb_t &&) noexcept = default;
|
||||||
|
|
||||||
|
ecb_t(const aes_t &key, bool padding = true);
|
||||||
|
|
||||||
|
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
||||||
|
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||||
|
};
|
||||||
|
|
||||||
|
class gcm_t : public cipher_t {
|
||||||
|
public:
|
||||||
|
gcm_t() = default;
|
||||||
|
gcm_t(gcm_t &&) noexcept = default;
|
||||||
|
gcm_t &operator=(gcm_t &&) noexcept = default;
|
||||||
|
|
||||||
|
gcm_t(const crypto::aes_t &key, const crypto::aes_t &iv, bool padding = true);
|
||||||
|
|
||||||
|
int encrypt(const std::string_view &plaintext, std::vector<std::uint8_t> &cipher);
|
||||||
|
int decrypt(const std::string_view &cipher, std::vector<std::uint8_t> &plaintext);
|
||||||
|
|
||||||
|
aes_t &get_iv() { return iv; }
|
||||||
|
|
||||||
|
aes_t iv;
|
||||||
|
};
|
||||||
|
} // namespace cipher
|
||||||
} // namespace crypto
|
} // namespace crypto
|
||||||
|
|
||||||
#endif //SUNSHINE_CRYPTO_H
|
#endif //SUNSHINE_CRYPTO_H
|
||||||
|
|||||||
+2
-4
@@ -222,8 +222,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
|
|||||||
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
|
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
|
||||||
|
|
||||||
std::vector<uint8_t> decrypted;
|
std::vector<uint8_t> decrypted;
|
||||||
crypto::cipher_t cipher(*sess.cipher_key);
|
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||||
cipher.padding = false;
|
|
||||||
|
|
||||||
cipher.decrypt(encrypted_response, decrypted);
|
cipher.decrypt(encrypted_response, decrypted);
|
||||||
|
|
||||||
@@ -242,8 +241,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
|
|||||||
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||||
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
|
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
|
||||||
|
|
||||||
crypto::cipher_t cipher(*sess.cipher_key);
|
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||||
cipher.padding = false;
|
|
||||||
|
|
||||||
std::vector<uint8_t> decrypted;
|
std::vector<uint8_t> decrypted;
|
||||||
cipher.decrypt(challenge, decrypted);
|
cipher.decrypt(challenge, decrypted);
|
||||||
|
|||||||
+14
-15
@@ -224,12 +224,11 @@ struct session_t {
|
|||||||
} audio;
|
} audio;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
crypto::cipher::gcm_t cipher;
|
||||||
|
|
||||||
net::peer_t peer;
|
net::peer_t peer;
|
||||||
} control;
|
} control;
|
||||||
|
|
||||||
crypto::aes_t gcm_key;
|
|
||||||
crypto::aes_t iv;
|
|
||||||
|
|
||||||
safe::mail_raw_t::event_t<bool> shutdown_event;
|
safe::mail_raw_t::event_t<bool> shutdown_event;
|
||||||
safe::signal_t controlEnd;
|
safe::signal_t controlEnd;
|
||||||
|
|
||||||
@@ -495,11 +494,10 @@ void controlBroadcastThread(control_server_t *server) {
|
|||||||
auto tagged_cipher_length = util::endian::big(*(int32_t *)payload.data());
|
auto tagged_cipher_length = util::endian::big(*(int32_t *)payload.data());
|
||||||
std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t)tagged_cipher_length };
|
std::string_view tagged_cipher { payload.data() + sizeof(tagged_cipher_length), (size_t)tagged_cipher_length };
|
||||||
|
|
||||||
crypto::cipher_t cipher { session->gcm_key };
|
|
||||||
cipher.padding = false;
|
|
||||||
|
|
||||||
std::vector<uint8_t> plaintext;
|
std::vector<uint8_t> plaintext;
|
||||||
if(cipher.decrypt_gcm(session->iv, tagged_cipher, plaintext)) {
|
|
||||||
|
auto &cipher = session->control.cipher;
|
||||||
|
if(cipher.decrypt(tagged_cipher, plaintext)) {
|
||||||
// something went wrong :(
|
// something went wrong :(
|
||||||
|
|
||||||
BOOST_LOG(error) << "Failed to verify tag"sv;
|
BOOST_LOG(error) << "Failed to verify tag"sv;
|
||||||
@@ -508,8 +506,8 @@ void controlBroadcastThread(control_server_t *server) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tagged_cipher_length >= 16 + session->iv.size()) {
|
if(tagged_cipher_length >= 16 + sizeof(crypto::aes_t)) {
|
||||||
std::copy(payload.end() - 16, payload.end(), std::begin(session->iv));
|
std::copy(payload.end() - 16, payload.end(), std::begin(cipher.get_iv()));
|
||||||
}
|
}
|
||||||
|
|
||||||
input::print(plaintext.data());
|
input::print(plaintext.data());
|
||||||
@@ -532,14 +530,13 @@ void controlBroadcastThread(control_server_t *server) {
|
|||||||
auto tagged_cipher_length = length - 4;
|
auto tagged_cipher_length = length - 4;
|
||||||
std::string_view tagged_cipher { (char *)header->payload(), (size_t)tagged_cipher_length };
|
std::string_view tagged_cipher { (char *)header->payload(), (size_t)tagged_cipher_length };
|
||||||
|
|
||||||
|
auto &cipher = session->control.cipher;
|
||||||
crypto::aes_t iv {};
|
crypto::aes_t iv {};
|
||||||
iv[0] = (char)seq;
|
iv[0] = (char)seq;
|
||||||
|
cipher.get_iv() = iv;
|
||||||
crypto::cipher_t cipher { session->gcm_key };
|
|
||||||
cipher.padding = false;
|
|
||||||
|
|
||||||
std::vector<uint8_t> plaintext;
|
std::vector<uint8_t> plaintext;
|
||||||
if(cipher.decrypt_gcm(iv, tagged_cipher, plaintext)) {
|
if(cipher.decrypt(tagged_cipher, plaintext)) {
|
||||||
// something went wrong :(
|
// something went wrong :(
|
||||||
|
|
||||||
BOOST_LOG(error) << "Failed to verify tag"sv;
|
BOOST_LOG(error) << "Failed to verify tag"sv;
|
||||||
@@ -1110,8 +1107,10 @@ std::shared_ptr<session_t> alloc(config_t &config, crypto::aes_t &gcm_key, crypt
|
|||||||
session->shutdown_event = mail->event<bool>(mail::shutdown);
|
session->shutdown_event = mail->event<bool>(mail::shutdown);
|
||||||
|
|
||||||
session->config = config;
|
session->config = config;
|
||||||
session->gcm_key = gcm_key;
|
|
||||||
session->iv = iv;
|
session->control.cipher = crypto::cipher::gcm_t {
|
||||||
|
gcm_key, iv, false
|
||||||
|
};
|
||||||
|
|
||||||
session->video.idr_events = mail->event<video::idr_t>(mail::idr);
|
session->video.idr_events = mail->event<video::idr_t>(mail::idr);
|
||||||
session->video.lowseq = 0;
|
session->video.lowseq = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user