Add DH1080 keyexchange to the crypt module.

Close #1378
This commit is contained in:
Christophe Beauval
2017-02-09 21:04:00 +01:00
committed by Alexey Sokolov
parent 88d1e27cc6
commit cba58ca862
2 changed files with 296 additions and 7 deletions
+185 -7
View File
@@ -26,10 +26,9 @@
// TODO:
//
// 1) Encrypt key storage file
// 2) Secure key exchange using pub/priv keys and the DH algorithm
// 3) Some way of notifying the user that the current channel is in "encryption
// 2) Some way of notifying the user that the current channel is in "encryption
// mode" verses plain text
// 4) Temporarily disable a target (nick/chan)
// 3) Temporarily disable a target (nick/chan)
//
// NOTE: This module is currently NOT intended to secure you from your shell
// admin.
@@ -43,6 +42,9 @@
#include <znc/Chan.h>
#include <znc/User.h>
#include <znc/IRCNetwork.h>
#include <openssl/dh.h>
#include <openssl/bn.h>
#include <znc/SHA256.h>
#define REQUIRESSL 1
// To be removed in future versions
@@ -50,6 +52,126 @@
#define NICK_PREFIX_KEY "@nick-prefix@"
class CCryptMod : public CModule {
private:
/*
* As used in other implementations like KVIrc, fish10, Quassel, FiSH-irssi, ...
* all the way back to the original located at http://mircryption.sourceforge.net/Extras/McpsFishDH.zip
*/
const char* m_sPrime1080 = "FBE1022E23D213E8ACFA9AE8B9DFADA3EA6B7AC7A7B7E95AB5EB2DF858921FEADE95E6AC7BE7DE6ADBAB8A783E7AF7A7FA6A2B7BEB1E72EAE2B72F9FA2BFB2A2EFBEFAC868BADB3E828FA8BADFADA3E4CC1BE7E8AFE85E9698A783EB68FA07A77AB6AD7BEB618ACF9CA2897EB28A6189EFA07AB99A8A7FA9AE299EFA7BA66DEAFEFBEFBF0B7D8B";
/* Generate our keys once and reuse, just like ssh keys */
std::unique_ptr<DH, decltype(&DH_free)> m_pDH;
CString m_sPrivKey;
CString m_sPubKey;
#if OPENSSL_VERSION_NUMBER < 0X10100000L
static int DH_set0_pqg(DH* dh, BIGNUM* p, BIGNUM* q, BIGNUM* g) {
/* If the fields p and g in dh are nullptr, the corresponding input
* parameters MUST be non-nullptr. q may remain nullptr.
*/
if (dh == nullptr || (dh->p == nullptr && p == nullptr) || (dh->g == nullptr && g == nullptr))
return 0;
if (p != nullptr) {
BN_free(dh->p);
dh->p = p;
}
if (g != nullptr) {
BN_free(dh->g);
dh->g = g;
}
if (q != nullptr) {
BN_free(dh->q);
dh->q = q;
dh->length = BN_num_bits(q);
}
return 1;
}
static void DH_get0_key(const DH* dh, const BIGNUM** pub_key, const BIGNUM** priv_key) {
if (dh != nullptr) {
if (pub_key != nullptr)
*pub_key = dh->pub_key;
if (priv_key != nullptr)
*priv_key = dh->priv_key;
}
}
#endif
bool DH1080_gen() {
/* Generate our keys on first call */
if (m_sPrivKey.empty() || m_sPubKey.empty()) {
int len;
const BIGNUM* bPrivKey = nullptr;
const BIGNUM* bPubKey = nullptr;
BIGNUM* bPrime = nullptr;
BIGNUM* bGen = nullptr;
if (!BN_hex2bn(&bPrime, m_sPrime1080) || !BN_dec2bn(&bGen, "2") || !DH_set0_pqg(m_pDH.get(), bPrime, nullptr, bGen) || !DH_generate_key(m_pDH.get())) {
/* one of them failed */
if (bPrime != nullptr)
BN_clear_free(bPrime);
if (bGen != nullptr)
BN_clear_free(bGen);
return false;
}
/* Get our keys */
DH_get0_key(m_pDH.get(), &bPubKey, &bPrivKey);
/* Get our private key */
len = BN_num_bytes(bPrivKey);
m_sPrivKey.resize(len);
BN_bn2bin(bPrivKey, (unsigned char*)m_sPrivKey.data());
m_sPrivKey.Base64Encode();
/* Get our public key */
len = BN_num_bytes(bPubKey);
m_sPubKey.resize(len);
BN_bn2bin(bPubKey, (unsigned char*)m_sPubKey.data());
m_sPubKey.Base64Encode();
}
return true;
}
bool DH1080_comp(CString& sOtherPubKey, CString& sSecretKey) {
unsigned long len;
unsigned char* key = nullptr;
BIGNUM* bOtherPubKey = nullptr;
/* Prepare other public key */
len = sOtherPubKey.Base64Decode();
bOtherPubKey = BN_bin2bn((unsigned char*)sOtherPubKey.data(), len, nullptr);
/* Generate secret key */
key = (unsigned char*)calloc(DH_size(m_pDH.get()), 1);
if ((len = DH_compute_key(key, bOtherPubKey, m_pDH.get())) == -1) {
sSecretKey = "";
if (bOtherPubKey != nullptr)
BN_clear_free(bOtherPubKey);
if (key != nullptr)
free(key);
return false;
}
/* Get our secret key */
sSecretKey.resize(SHA256_DIGEST_SIZE);
sha256(key, len, (unsigned char*)sSecretKey.data());
sSecretKey.Base64Encode();
sSecretKey.TrimRight("=");
if (bOtherPubKey != nullptr)
BN_clear_free(bOtherPubKey);
if (key != nullptr)
free(key);
return true;
}
CString NickPrefix() {
MCString::iterator it = FindNV(NICK_PREFIX_KEY);
/*
@@ -67,8 +189,10 @@ class CCryptMod : public CModule {
return sStatusPrefix.StartsWith("*") ? "." : "*";
}
public:
MODCONSTRUCTOR(CCryptMod) {
/* MODCONSTRUCTOR(CLASS) is of form "CLASS(...) : CModule(...)" */
MODCONSTRUCTOR(CCryptMod) , m_pDH(DH_new(), DH_free) {
AddHelpCommand();
AddCommand("DelKey", static_cast<CModCommand::ModCmdFunc>(
&CCryptMod::OnDelKeyCommand),
@@ -79,9 +203,13 @@ class CCryptMod : public CModule {
AddCommand("ListKeys", static_cast<CModCommand::ModCmdFunc>(
&CCryptMod::OnListKeysCommand),
"", "List all keys");
AddCommand("KeyX", static_cast<CModCommand::ModCmdFunc>(
&CCryptMod::OnKeyXCommand),
"<Nick>", "Start a DH1080 key exchange with nick");
}
~CCryptMod() override {}
~CCryptMod() override {
}
bool OnLoad(const CString& sArgsi, CString& sMessage) override {
MCString::iterator it = FindNV(NICK_PREFIX_KEY);
@@ -151,7 +279,7 @@ class CCryptMod : public CModule {
sMessage);
GetUser()->PutUser(":" + NickPrefix() + sNickMask + " NOTICE " +
sTarget + " :" + sMessage,
NULL, GetClient());
nullptr, GetClient());
}
CString sMsg = MakeIvec() + sMessage;
@@ -187,7 +315,7 @@ class CCryptMod : public CModule {
GetUser()->PutUser(":" + NickPrefix() + sNickMask +
" PRIVMSG " + sTarget + " :\001ACTION " +
sMessage + "\001",
NULL, GetClient());
nullptr, GetClient());
}
CString sMsg = MakeIvec() + sMessage;
@@ -227,6 +355,41 @@ class CCryptMod : public CModule {
}
EModRet OnPrivNotice(CNick& Nick, CString& sMessage) override {
CString sCommand = sMessage.Token(0);
CString sOtherPubKey = sMessage.Token(1);
if ((sCommand.Equals("DH1080_INIT") || sCommand.Equals("DH1080_INIT_CBC")) && !sOtherPubKey.empty()) {
CString sSecretKey;
CString sTail = sMessage.Token(2); /* For fish10 */
/* remove trailing A */
if (sOtherPubKey.TrimSuffix("A") && DH1080_gen() && DH1080_comp(sOtherPubKey, sSecretKey)) {
PutModule("Received DH1080 public key from " + Nick.GetNick() + ", sending mine...");
PutIRC("NOTICE " + Nick.GetNick() + " :DH1080_FINISH " + m_sPubKey + "A" + (sTail.empty()?"":(" " + sTail)));
SetNV(Nick.GetNick().AsLower(), sSecretKey);
PutModule("Key for " + Nick.GetNick() + " successfully set.");
return HALT;
}
PutModule("Error in " + sCommand + " with " + Nick.GetNick() + ": " + (sSecretKey.empty()?"no secret key computed":sSecretKey));
return CONTINUE;
} else if (sCommand.Equals("DH1080_FINISH") && !sOtherPubKey.empty()) {
/*
* In theory we could get a DH1080_FINISH without us having sent a DH1080_INIT first,
* but then to have any use for the other user, they'd already have our pub key
*/
CString sSecretKey;
/* remove trailing A */
if (sOtherPubKey.TrimSuffix("A") && DH1080_gen() && DH1080_comp(sOtherPubKey, sSecretKey)) {
SetNV(Nick.GetNick().AsLower(), sSecretKey);
PutModule("Key for " + Nick.GetNick() + " successfully set.");
return HALT;
}
PutModule("Error in " + sCommand + " with " + Nick.GetNick() + ": " + (sSecretKey.empty()?"no secret key computed":sSecretKey));
return CONTINUE;
}
FilterIncoming(Nick.GetNick(), Nick, sMessage);
return CONTINUE;
}
@@ -323,6 +486,21 @@ class CCryptMod : public CModule {
}
}
void OnKeyXCommand(const CString& sCommand) {
CString sTarget = sCommand.Token(1);
if (!sTarget.empty()) {
if (DH1080_gen()) {
PutIRC("NOTICE " + sTarget + " :DH1080_INIT " + m_sPubKey + "A");
PutModule("Sent my DH1080 public key to " + sTarget + ", waiting for reply ...");
} else {
PutModule("Error generating our keys, nothing sent.");
}
} else {
PutModule("Usage: KeyX <Nick>");
}
}
void OnListKeysCommand(const CString& sCommand) {
if (BeginNV() == EndNV()) {
PutModule("You have no encryption keys set.");
+111
View File
@@ -80,6 +80,48 @@ class IO {
m_readed += chunk;
}
}
/*
* Reads from Device until pattern is matched and returns this pattern
* up to and excluding the first newline. Pattern itself can contain a newline.
* Have to use second param as the ASSERT_*'s return a non-QByteArray.
*/
void ReadUntilAndGet(QByteArray pattern, QByteArray& match) {
auto deadline = QDateTime::currentDateTime().addSecs(60);
while (true) {
int search = m_readed.indexOf(pattern);
if (search != -1) {
int start = 0;
/* Don't look for what we've already found */
if (pattern != "\n") {
int patlen = pattern.length();
start = search;
pattern = QByteArray("\n");
search = m_readed.indexOf(pattern, start + patlen);
}
if (search != -1) {
match += m_readed.mid(start, search - start);
m_readed.remove(0, search + 1);
return;
}
/* No newline yet, add to retvalue and trunc output */
match += m_readed.mid(start);
m_readed.resize(0);
}
if (m_readed.length() > pattern.length()) {
m_readed = m_readed.right(pattern.length());
}
const int timeout_ms =
QDateTime::currentDateTime().msecsTo(deadline);
ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString();
ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms))
<< "Wanted: " << pattern.toStdString();
QByteArray chunk = m_device->readAll();
if (m_verbose) {
std::cout << chunk.toStdString() << std::flush;
}
m_readed += chunk;
}
}
void Write(QByteArray s = "", bool new_line = true) {
if (!m_device) return;
if (m_verbose) {
@@ -1986,4 +2028,73 @@ TEST_F(ZNCTest, ModuleCSRFOverride) {
EXPECT_THAT(reply, HasSubstr("ipsum"));
}
TEST_F(ZNCTest, ModuleCrypt) {
QFile conf(m_dir.path() + "/configs/znc.conf");
ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text));
QTextStream(&conf) << "ServerThrottle = 1\n";
auto znc = Run();
Z;
auto ircd1 = ConnectIRCd();
Z;
auto client1 = LoginClient();
Z;
client1.Write("znc loadmod controlpanel");
client1.Write("PRIVMSG *controlpanel :CloneUser user user2");
client1.ReadUntil("User [user2] added!");
client1.Write("PRIVMSG *controlpanel :Set Nick user2 nick2");
Z;
client1.Write("znc loadmod crypt");
client1.ReadUntil("Loaded module");
Z;
auto ircd2 = ConnectIRCd();
Z;
auto client2 = ConnectClient();
client2.Write("PASS user2:hunter2");
client2.Write("NICK nick2");
client2.Write("USER user2/test x x :x");
Z;
client2.Write("znc loadmod crypt");
client2.ReadUntil("Loaded module");
Z;
client1.Write("PRIVMSG *crypt :keyx nick2");
client1.ReadUntil("Sent my DH1080 public key to nick2");
Z;
QByteArray pub1("");
ircd1.ReadUntilAndGet("NOTICE nick2 :DH1080_INIT ", pub1);
ircd2.Write(":user!user@user/test " + pub1);
Z;
client2.ReadUntil("Received DH1080 public key from user");
Z;
client2.ReadUntil("Key for user successfully set.");
Z;
QByteArray pub2("");
ircd2.ReadUntilAndGet("NOTICE user :DH1080_FINISH ", pub2);
ircd1.Write(":nick2!user2@user2/test " + pub2);
Z;
client1.ReadUntil("Key for nick2 successfully set.");
Z;
client1.Write("PRIVMSG *crypt :listkeys");
QByteArray key1("");
client1.ReadUntilAndGet("| nick2 | ", key1);
Z;
client2.Write("PRIVMSG *crypt :listkeys");
QByteArray key2("");
client2.ReadUntilAndGet("| user | ", key2);
Z;
ASSERT_EQ(key1.mid(18), key2.mid(18));
client1.Write("PRIVMSG .nick2 :Hello");
QByteArray secretmsg;
ircd1.ReadUntilAndGet("PRIVMSG nick2 :+OK ", secretmsg);
Z;
ircd2.Write(":user!user@user/test " + secretmsg);
client2.ReadUntil("Hello");
Z;
}
} // namespace