From cba58ca86263ae26b6a213dab569efc4545bce82 Mon Sep 17 00:00:00 2001 From: Christophe Beauval Date: Thu, 9 Feb 2017 21:04:00 +0100 Subject: [PATCH] Add DH1080 keyexchange to the crypt module. Close #1378 --- modules/crypt.cpp | 192 ++++++++++++++++++++++++++++++++++++-- test/integration/main.cpp | 111 ++++++++++++++++++++++ 2 files changed, 296 insertions(+), 7 deletions(-) diff --git a/modules/crypt.cpp b/modules/crypt.cpp index c41e57c8..5145b878 100644 --- a/modules/crypt.cpp +++ b/modules/crypt.cpp @@ -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 #include #include +#include +#include +#include #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 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( &CCryptMod::OnDelKeyCommand), @@ -79,9 +203,13 @@ class CCryptMod : public CModule { AddCommand("ListKeys", static_cast( &CCryptMod::OnListKeysCommand), "", "List all keys"); + AddCommand("KeyX", static_cast( + &CCryptMod::OnKeyXCommand), + "", "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 "); + } + } + void OnListKeysCommand(const CString& sCommand) { if (BeginNV() == EndNV()) { PutModule("You have no encryption keys set."); diff --git a/test/integration/main.cpp b/test/integration/main.cpp index 613f25df..0702b055 100644 --- a/test/integration/main.cpp +++ b/test/integration/main.cpp @@ -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