diff options
-rw-r--r-- | sql/base/auth_database.sql | 7 | ||||
-rw-r--r-- | sql/updates/auth/master/2023_12_26_00_auth.sql | 4 | ||||
-rw-r--r-- | src/common/Cryptography/Authentication/SRP6.cpp | 203 | ||||
-rw-r--r-- | src/common/Cryptography/Authentication/SRP6.h | 258 | ||||
-rw-r--r-- | src/common/Cryptography/BigNumber.cpp | 7 | ||||
-rw-r--r-- | src/common/Cryptography/BigNumber.h | 2 | ||||
-rw-r--r-- | src/server/bnetserver/REST/LoginHttpSession.h | 8 | ||||
-rw-r--r-- | src/server/bnetserver/REST/LoginRESTService.cpp | 165 | ||||
-rw-r--r-- | src/server/bnetserver/REST/LoginRESTService.h | 29 | ||||
-rw-r--r-- | src/server/database/Database/Implementation/LoginDatabase.cpp | 9 | ||||
-rw-r--r-- | src/server/database/Database/Implementation/LoginDatabase.h | 3 | ||||
-rw-r--r-- | src/server/game/Accounts/AccountMgr.cpp | 32 | ||||
-rw-r--r-- | src/server/game/Accounts/BattlenetAccountMgr.cpp | 76 | ||||
-rw-r--r-- | src/server/game/Accounts/BattlenetAccountMgr.h | 3 | ||||
-rw-r--r-- | src/server/shared/Networking/Http/HttpService.cpp | 10 | ||||
-rw-r--r-- | src/server/shared/Networking/Http/HttpService.h | 6 |
16 files changed, 627 insertions, 195 deletions
diff --git a/sql/base/auth_database.sql b/sql/base/auth_database.sql index b1c72397cba..93c547bd4ed 100644 --- a/sql/base/auth_database.sql +++ b/sql/base/auth_database.sql @@ -419,7 +419,9 @@ DROP TABLE IF EXISTS `battlenet_accounts`; CREATE TABLE `battlenet_accounts` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Identifier', `email` varchar(320) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - `sha_pass_hash` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `srp_version` tinyint(3) NOT NULL DEFAULT '1', + `salt` binary(32) NOT NULL, + `verifier` blob NOT NULL, `joindate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '127.0.0.1', `failed_logins` int unsigned NOT NULL DEFAULT '0', @@ -2778,7 +2780,8 @@ INSERT INTO `updates` VALUES ('2023_12_13_00_auth.sql','C3D6AA45BECD5A7F8A420FE0022AAF6A349C5E3F','RELEASED','2023-12-13 06:42:48',0), ('2023_12_19_00_auth.sql','6761E7111F613E57A7D684E18E38FCA3F5CD66A6','RELEASED','2023-12-19 08:54:36',0), ('2023_12_21_00_auth.sql','DB294EF35C00AA92C79786F7A0BFBCE739D4E193','RELEASED','2023-12-21 09:08:30',0), -('2023_12_24_00_auth.sql','F59B3A895750FD83177324B89BFCEBD8A43DD577','RELEASED','2023-12-24 06:24:58',0); +('2023_12_24_00_auth.sql','F59B3A895750FD83177324B89BFCEBD8A43DD577','RELEASED','2023-12-24 06:24:58',0), +('2023_12_26_00_auth.sql','5C8716F7F6E2792E15A42BDA8F2D855A7DE95FC5','RELEASED','2023-12-26 13:38:58',0); /*!40000 ALTER TABLE `updates` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/updates/auth/master/2023_12_26_00_auth.sql b/sql/updates/auth/master/2023_12_26_00_auth.sql new file mode 100644 index 00000000000..3c27a8c7ac5 --- /dev/null +++ b/sql/updates/auth/master/2023_12_26_00_auth.sql @@ -0,0 +1,4 @@ +ALTER TABLE `battlenet_accounts` + ADD `srp_version` tinyint(3) NOT NULL DEFAULT '1' AFTER `email`, + ADD `salt` binary(32) AFTER `srp_version`, + ADD `verifier` blob AFTER `salt`; diff --git a/src/common/Cryptography/Authentication/SRP6.cpp b/src/common/Cryptography/Authentication/SRP6.cpp index c172bbc1d3d..e015b8c33f6 100644 --- a/src/common/Cryptography/Authentication/SRP6.cpp +++ b/src/common/Cryptography/Authentication/SRP6.cpp @@ -17,42 +17,113 @@ #include "SRP6.h" #include "CryptoRandom.h" -#include "Util.h" #include <algorithm> #include <functional> -using SHA1 = Trinity::Crypto::SHA1; -using SRP6 = Trinity::Crypto::SRP6; +namespace Trinity::Crypto::SRP +{ +using namespace std::string_literals; + +BigNumber const GruntSRP6::N = "894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7"s; +BigNumber const GruntSRP6::g = 7; + +BigNumber const BnetSRP6v1Base::N = "86A7F6DEEB306CE519770FE37D556F29944132554DED0BD68205E27F3231FEF5A10108238A3150C59CAF7B0B6478691C13A6ACF5E1B5ADAFD4A943D4A21A142B800E8A55F8BFBAC700EB77A7235EE5A609E350EA9FC19F10D921C2FA832E4461B7125D38D254A0BE873DFC27858ACB3F8B9F258461E4373BC3A6C2A9634324AB"s; +BigNumber const BnetSRP6v1Base::g = 2; + +BigNumber const BnetSRP6v2Base::N = "AC6BDB41324A9A9BF166DE5E1389582FAF72B6651987EE07FC3192943DB56050A37329CBB4A099ED8193E0757767A13DD52312AB4B03310DCD7F48A9DA04FD50E8083969EDB767B0CF6095179A163AB3661A05FBD5FAAAE82918A9962F0B93B855F97993EC975EEAA80D740ADBF4FF747359D041D5C33EA71D281E446B14773BCA97B43A23FB801676BD207A436C6481F1D2B9078717461A5B9D32E688F87748544523B524B0D57D5EA77A2775D2ECFA032CFBDBF52FB3786160279004E57AE6AF874E7303CE53299CCC041C7BC308D82A5698F3A8D0C38271AE35F8E9DBFBB694B5C803D89F7AE435DE236D525F54759B65E372FCD68EF20FA7111F9E4AFF73"s; +BigNumber const BnetSRP6v2Base::g = 2; + +SRP6::SRP6(BigNumber const& i, Salt const& salt, Verifier const& verifier, BigNumber const& N, BigNumber const& g, BigNumber const& k) + : s(salt), I(i), b(CalculatePrivateB(N)), v(verifier), B(CalculatePublicB(N, g, k)) +{ +} + +SRP6::SRP6(ForRegistrationTag) + : s(GetRandomBytes<SALT_LENGTH>()), _used(true) +{ +} -/*static*/ std::array<uint8, 1> const SRP6::g = { 7 }; -/*static*/ std::array<uint8, 32> const SRP6::N = HexStrToByteArray<32>("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", true); -/*static*/ BigNumber const SRP6::_g(SRP6::g); -/*static*/ BigNumber const SRP6::_N(N); +Optional<BigNumber> SRP6::VerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1) +{ + ASSERT(!_used, "A single SRP6 object must only ever be used to verify ONCE!"); + _used = true; + + return DoVerifyClientEvidence(A, clientM1); +} -/*static*/ std::pair<SRP6::Salt, SRP6::Verifier> SRP6::MakeRegistrationData(std::string const& username, std::string const& password) +bool SRP6::CheckCredentials(std::string const& username, std::string const& password) const { - std::pair<SRP6::Salt, SRP6::Verifier> res; - Crypto::GetRandomBytes(res.first); // random salt - res.second = CalculateVerifier(username, password, res.first); - return res; + return v == CalculateVerifier(username, password, s); } -/*static*/ SRP6::Verifier SRP6::CalculateVerifier(std::string const& username, std::string const& password, SRP6::Salt const& salt) +BigNumber SRP6::CalculatePrivateB(BigNumber const& N) +{ + BigNumber b; + b.SetRand(N.GetNumBits()); + b %= N - 1; + return b; +} + +BigNumber SRP6::CalculatePublicB(BigNumber const& N, BigNumber const& g, BigNumber const& k) const +{ + return (g.ModExp(b, N) + (v * k)) % N; +} + +Verifier SRP6::CalculateVerifier(std::string const& username, std::string const& password, Salt const& salt) const { // v = g ^ H(s || H(u || ':' || p)) mod N - return _g.ModExp( - SHA1::GetDigestOf( - salt, - SHA1::GetDigestOf(username, ":", password) - ) - ,_N).ToByteArray<32>(); + return Getg().ModExp( + CalculateX(username, password, salt), + GetN() + ).ToByteVector(); +} + +GruntSRP6::GruntSRP6(std::string const& username, Salt const& salt, Verifier const& verifier) + : SRP6(SHA1::GetDigestOf(username), salt, verifier, N, g, 3) +{ +} + +BigNumber GruntSRP6::CalculateServerEvidence(BigNumber const& A, BigNumber const& clientM1, BigNumber const& K) const +{ + return SHA1::GetDigestOf(A.ToByteArray<EPHEMERAL_KEY_LENGTH>(), clientM1.ToByteArray<SHA1::DIGEST_LENGTH>(), K.ToByteArray<SHA1::DIGEST_LENGTH * 2>()); +} + +BigNumber GruntSRP6::CalculateX(std::string const& username, std::string const& password, Salt const& salt) const +{ + return SHA1::GetDigestOf( + salt, + SHA1::GetDigestOf(username, ":", password) + ); } -/*static*/ SessionKey SRP6::SHA1Interleave(SRP6::EphemeralKey const& S) +Optional<BigNumber> GruntSRP6::DoVerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1) +{ + if ((A % N).IsZero()) + return {}; + + BigNumber const u(SHA1::GetDigestOf(A.ToByteArray<EPHEMERAL_KEY_LENGTH>(), B.ToByteArray<EPHEMERAL_KEY_LENGTH>())); + EphemeralKey const S = (A * (v.ModExp(u, N))).ModExp(b, N).ToByteArray<EPHEMERAL_KEY_LENGTH>(); + + SessionKey K = SHA1Interleave(S); + + // NgHash = H(N) xor H(g) + SHA1::Digest const NHash = SHA1::GetDigestOf(N.ToByteArray<32>()); + SHA1::Digest const gHash = SHA1::GetDigestOf(g.ToByteArray<1>()); + SHA1::Digest NgHash; + std::transform(NHash.begin(), NHash.end(), gHash.begin(), NgHash.begin(), std::bit_xor<>()); + + BigNumber const ourM = SHA1::GetDigestOf(NgHash, I.ToByteArray<SHA1::DIGEST_LENGTH>(), s, A.ToByteArray<EPHEMERAL_KEY_LENGTH>(), B.ToByteArray<EPHEMERAL_KEY_LENGTH>(), K); + if (ourM == clientM1) + return K; + + return {}; +} + +GruntSRP6::SessionKey GruntSRP6::SHA1Interleave(EphemeralKey const& S) { // split S into two buffers - std::array<uint8, EPHEMERAL_KEY_LENGTH/2> buf0, buf1; - for (size_t i = 0; i < EPHEMERAL_KEY_LENGTH/2; ++i) + std::array<uint8, EPHEMERAL_KEY_LENGTH / 2> buf0, buf1; + for (size_t i = 0; i < S.size() / 2; ++i) { buf0[i] = S[2 * i + 0]; buf1[i] = S[2 * i + 1]; @@ -60,13 +131,13 @@ using SRP6 = Trinity::Crypto::SRP6; // find position of first nonzero byte size_t p = 0; - while (p < EPHEMERAL_KEY_LENGTH && !S[p]) ++p; + while (p < S.size() && !S[p]) ++p; if (p & 1) ++p; // skip one extra byte if p is odd p /= 2; // offset into buffers // hash each of the halves, starting at the first nonzero byte - SHA1::Digest const hash0 = SHA1::GetDigestOf(buf0.data() + p, EPHEMERAL_KEY_LENGTH/2 - p); - SHA1::Digest const hash1 = SHA1::GetDigestOf(buf1.data() + p, EPHEMERAL_KEY_LENGTH/2 - p); + SHA1::Digest const hash0 = SHA1::GetDigestOf(buf0.data() + p, S.size() / 2 - p); + SHA1::Digest const hash1 = SHA1::GetDigestOf(buf1.data() + p, S.size() / 2 - p); // stick the two hashes back together SessionKey K; @@ -78,32 +149,74 @@ using SRP6 = Trinity::Crypto::SRP6; return K; } -SRP6::SRP6(std::string const& username, Salt const& salt, Verifier const& verifier) - : _I(SHA1::GetDigestOf(username)), _b(Crypto::GetRandomBytes<32>()), _v(verifier), s(salt), B(_B(_b, _v)) {} +BnetSRP6Base::BnetSRP6Base(BigNumber const& i, Salt const& salt, Verifier const& verifier, BigNumber const& N, BigNumber const& g, BigNumber const& k) + : SRP6(i, salt, verifier, N, g, k) +{ +} -Optional<SessionKey> SRP6::VerifyChallengeResponse(EphemeralKey const& A, SHA1::Digest const& clientM) +BigNumber BnetSRP6Base::CalculateServerEvidence(BigNumber const& A, BigNumber const& clientM1, BigNumber const& K) const { - ASSERT(!_used, "A single SRP6 object must only ever be used to verify ONCE!"); - _used = true; + std::array evidenceBns{ &A, &clientM1, &K }; + return DoCalculateEvidence(evidenceBns); +} - BigNumber const _A(A); - if ((_A % _N).IsZero()) +Optional<BigNumber> BnetSRP6Base::DoVerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1) +{ + BigNumber N = GetN(); + if ((A % N).IsZero()) return {}; - BigNumber const u(SHA1::GetDigestOf(A, B)); - EphemeralKey const S = (_A * (_v.ModExp(u, _N))).ModExp(_b, N).ToByteArray<32>(); + BigNumber const u = CalculateU(A); + if ((u % N).IsZero()) + return {}; - SessionKey K = SHA1Interleave(S); + BigNumber const S = (A * v.ModExp(u, N)).ModExp(b, N); - // NgHash = H(N) xor H(g) - SHA1::Digest const NHash = SHA1::GetDigestOf(N); - SHA1::Digest const gHash = SHA1::GetDigestOf(g); - SHA1::Digest NgHash; - std::transform(NHash.begin(), NHash.end(), gHash.begin(), NgHash.begin(), std::bit_xor<>()); - - SHA1::Digest const ourM = SHA1::GetDigestOf(NgHash, _I, s, A, B, K); - if (ourM == clientM) - return K; - else + std::array evidenceBns{ &A, &B, &S }; + BigNumber const ourM = DoCalculateEvidence(evidenceBns); + if (ourM != clientM1) return {}; + + return S; +} + +std::vector<uint8> BnetSRP6Base::GetBrokenEvidenceVector(BigNumber const& bn) +{ + int32 bytes = (bn.GetNumBits() + 8) >> 3; + return bn.ToByteVector(bytes, false); +} + +BnetSRP6v1Base::BnetSRP6v1Base(std::string const& username, Salt const& salt, Verifier const& verifier, BigNumber const& k) + : BnetSRP6Base(SHA256::GetDigestOf(username), salt, verifier, N, g, k) +{ +} + +BigNumber BnetSRP6v1Base::CalculateX(std::string const& username, std::string const& password, Salt const& salt) const +{ + return SHA256::GetDigestOf( + salt, + SHA256::GetDigestOf(username, ":", password) + ); +} + +BnetSRP6v2Base::BnetSRP6v2Base(std::string const& username, Salt const& salt, Verifier const& verifier, BigNumber const& k) + : BnetSRP6Base(SHA256::GetDigestOf(username), salt, verifier, N, g, k) +{ +} + +BigNumber BnetSRP6v2Base::CalculateX(std::string const& username, std::string const& password, Salt const& salt) const +{ + SHA512::Digest xBytes = {}; + std::string tmp = username + ":" + password; + PKCS5_PBKDF2_HMAC(tmp.c_str(), tmp.length(), salt.data(), salt.size(), GetXIterations(), EVP_sha512(), xBytes.size(), xBytes.data()); + BigNumber x(xBytes, false); + if (xBytes[0] & 0x80) + { + std::array<uint8, 65> fix = {}; + fix[64] = 1; + x -= fix; + } + + return x % (N - 1); +} } diff --git a/src/common/Cryptography/Authentication/SRP6.h b/src/common/Cryptography/Authentication/SRP6.h index 8b1a0aafd59..b29f9bff1a4 100644 --- a/src/common/Cryptography/Authentication/SRP6.h +++ b/src/common/Cryptography/Authentication/SRP6.h @@ -18,68 +18,224 @@ #ifndef TRINITY_SRP6_H #define TRINITY_SRP6_H -#include "AuthDefines.h" #include "BigNumber.h" -#include "Define.h" #include "CryptoHash.h" +#include "Define.h" #include "Optional.h" #include <array> +#include <span> -namespace Trinity +namespace Trinity::Crypto { -namespace Crypto +namespace SRP { + static constexpr size_t SALT_LENGTH = 32; + using Salt = std::array<uint8, SALT_LENGTH>; + + using Verifier = std::vector<uint8>; + class TC_COMMON_API SRP6 { - public: - static constexpr size_t SALT_LENGTH = 32; - using Salt = std::array<uint8, SALT_LENGTH>; - static constexpr size_t VERIFIER_LENGTH = 32; - using Verifier = std::array<uint8, VERIFIER_LENGTH>; - static constexpr size_t EPHEMERAL_KEY_LENGTH = 32; - using EphemeralKey = std::array<uint8, EPHEMERAL_KEY_LENGTH>; - - static std::array<uint8, 1> const g; - static std::array<uint8, 32> const N; - - // username + password must be passed through Utf8ToUpperOnlyLatin FIRST! - static std::pair<Salt, Verifier> MakeRegistrationData(std::string const& username, std::string const& password); - // username + password must be passed through Utf8ToUpperOnlyLatin FIRST! - static bool CheckLogin(std::string const& username, std::string const& password, Salt const& salt, Verifier const& verifier) - { - return (verifier == CalculateVerifier(username, password, salt)); - } - - static SHA1::Digest GetSessionVerifier(EphemeralKey const& A, SHA1::Digest const& clientM, SessionKey const& K) - { - return SHA1::GetDigestOf(A, clientM, K); - } - - SRP6(std::string const& username, Salt const& salt, Verifier const& verifier); - Optional<SessionKey> VerifyChallengeResponse(EphemeralKey const& A, SHA1::Digest const& clientM); - - private: - bool _used = false; // a single instance can only be used to verify once - - static Verifier CalculateVerifier(std::string const& username, std::string const& password, Salt const& salt); - static SessionKey SHA1Interleave(EphemeralKey const& S); - - /* global algorithm parameters */ - static BigNumber const _g; // a [g]enerator for the ring of integers mod N, algorithm parameter - static BigNumber const _N; // the modulus, an algorithm parameter; all operations are mod this - - static EphemeralKey _B(BigNumber const& b, BigNumber const& v) { return ((_g.ModExp(b,_N) + (v * 3)) % N).ToByteArray<EPHEMERAL_KEY_LENGTH>(); } - - /* per-instantiation parameters, set on construction */ - SHA1::Digest const _I; // H(I) - the username, all uppercase - BigNumber const _b; // b - randomly chosen by the server, 19 bytes, never given out - BigNumber const _v; // v - the user's password verifier, derived from s + H(USERNAME || ":" || PASSWORD) - - public: - Salt const s; // s - the user's password salt, random, used to calculate v on registration - EphemeralKey const B; // B = 3v + g^b + protected: + struct ForRegistrationTag { }; + + public: + explicit SRP6(BigNumber const& i, Salt const& salt, Verifier const& verifier, BigNumber const& N, BigNumber const& g, BigNumber const& k); + explicit SRP6(ForRegistrationTag); + + SRP6(SRP6 const&) = delete; + SRP6(SRP6&&) = delete; + SRP6& operator=(SRP6 const&) = delete; + SRP6& operator=(SRP6&&) = delete; + + virtual ~SRP6() = default; + + virtual BigNumber const& GetN() const = 0; + virtual BigNumber const& Getg() const = 0; + + Optional<BigNumber> VerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1); + + virtual BigNumber CalculateServerEvidence(BigNumber const& A, BigNumber const& clientM1, BigNumber const& K) const = 0; + + template<typename Impl> + static std::pair<Salt, Verifier> MakeRegistrationData(std::string const& username, std::string const& password) + { + Impl impl(ForRegistrationTag{}); + return { impl.s, impl.CalculateVerifier(username, password, impl.s) }; + } + + bool CheckCredentials(std::string const& username, std::string const& password) const; + + Salt const s; // s - the user's password salt, random, used to calculate v on registration + + protected: + static BigNumber CalculatePrivateB(BigNumber const& N); + + BigNumber CalculatePublicB(BigNumber const& N, BigNumber const& g, BigNumber const& k) const; + + virtual BigNumber CalculateX(std::string const& username, std::string const& password, Salt const& salt) const = 0; + + Verifier CalculateVerifier(std::string const& username, std::string const& password, Salt const& salt) const; + + virtual Optional<BigNumber> DoVerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1) = 0; + + BigNumber const I; // H(I) - the username, all uppercase + BigNumber const b; // b - randomly chosen by the server, same length as N, never given out + BigNumber const v; // v - the user's password verifier, derived from s + H(USERNAME || ":" || PASSWORD) + + public: + BigNumber const B; // B = k*v + g^b + + private: + bool _used = false; // a single instance can only be used to verify once + }; + + class TC_COMMON_API GruntSRP6 final : public SRP6 + { + public: + static constexpr size_t EPHEMERAL_KEY_LENGTH = 32; + + using EphemeralKey = std::array<uint8, EPHEMERAL_KEY_LENGTH>; + + using SessionKey = std::array<uint8, SHA1::DIGEST_LENGTH * 2>; + + static BigNumber const N; // the modulus, an algorithm parameter; all operations are mod this + static BigNumber const g; // a [g]enerator for the ring of integers mod N, algorithm parameter + + explicit GruntSRP6(std::string const& username, Salt const& salt, Verifier const& verifier); + explicit GruntSRP6(ForRegistrationTag t) : SRP6(t) { } + + BigNumber const& GetN() const override { return N; } + BigNumber const& Getg() const override { return g; } + + BigNumber CalculateServerEvidence(BigNumber const& A, BigNumber const& clientM1, BigNumber const& K) const override; + + protected: + BigNumber CalculateX(std::string const& username, std::string const& password, Salt const& salt) const override; + + Optional<BigNumber> DoVerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1) override; + + static SessionKey SHA1Interleave(EphemeralKey const& S); + }; + + class TC_COMMON_API BnetSRP6Base : public SRP6 + { + public: + explicit BnetSRP6Base(BigNumber const& i, Salt const& salt, Verifier const& verifier, BigNumber const& N, BigNumber const& g, BigNumber const& k); + explicit BnetSRP6Base(ForRegistrationTag t) : SRP6(t) { } + + BigNumber CalculateServerEvidence(BigNumber const& A, BigNumber const& clientM1, BigNumber const& K) const final; + + virtual uint8 GetVersion() const = 0; + virtual uint32 GetXIterations() const = 0; + + protected: + Optional<BigNumber> DoVerifyClientEvidence(BigNumber const& A, BigNumber const& clientM1) final; + + virtual BigNumber CalculateU(BigNumber const& A) const = 0; + + virtual BigNumber DoCalculateEvidence(std::span<BigNumber const*> bns) const = 0; + + template<typename CryptoHash> + BigNumber DoCalculateEvidence(std::span<BigNumber const*> bns) const + { + CryptoHash hash; + for (BigNumber const* bn : bns) + hash.UpdateData(GetBrokenEvidenceVector(*bn)); + + hash.Finalize(); + return BigNumber(hash.GetDigest(), false); + } + + static std::vector<uint8> GetBrokenEvidenceVector(BigNumber const& bn); + }; + + class TC_COMMON_API BnetSRP6v1Base : public BnetSRP6Base + { + public: + static BigNumber const N; // the modulus, an algorithm parameter; all operations are mod this + static BigNumber const g; // a [g]enerator for the ring of integers mod N, algorithm parameter + + explicit BnetSRP6v1Base(std::string const& username, Salt const& salt, Verifier const& verifier, BigNumber const& k); + explicit BnetSRP6v1Base(ForRegistrationTag t) : BnetSRP6Base(t) { } + + BigNumber const& GetN() const final { return N; } + BigNumber const& Getg() const final { return g; } + + uint8 GetVersion() const final { return 1; } + uint32 GetXIterations() const final { return 1; } + + protected: + BigNumber CalculateX(std::string const& username, std::string const& password, Salt const& salt) const final; + }; + + class TC_COMMON_API BnetSRP6v2Base : public BnetSRP6Base + { + public: + static BigNumber const N; // the modulus, an algorithm parameter; all operations are mod this + static BigNumber const g; // a [g]enerator for the ring of integers mod N, algorithm parameter + + explicit BnetSRP6v2Base(std::string const& username, Salt const& salt, Verifier const& verifier, BigNumber const& k); + explicit BnetSRP6v2Base(ForRegistrationTag t) : BnetSRP6Base(t) { } + + BigNumber const& GetN() const final { return N; } + BigNumber const& Getg() const final { return g; } + + uint8 GetVersion() const final { return 2; } + uint32 GetXIterations() const final { return 15000; } + + protected: + BigNumber CalculateX(std::string const& username, std::string const& password, Salt const& salt) const final; + }; + + template<typename CryptoHash> + class BnetSRP6v1 final : public BnetSRP6v1Base + { + public: + BnetSRP6v1(std::string const& username, Salt const& salt, Verifier const& verifier) + : BnetSRP6v1Base(username, salt, verifier, BigNumber(CryptoHash::GetDigestOf(N.ToByteArray<128>(false), g.ToByteArray<128>(false)), false)) + { + } + + explicit BnetSRP6v1(ForRegistrationTag t) : BnetSRP6v1Base(t) { } + + protected: + BigNumber CalculateU(BigNumber const& A) const override + { + return BigNumber(CryptoHash::GetDigestOf(A.ToByteArray<128>(false), B.ToByteArray<128>(false)), false); + } + + BigNumber DoCalculateEvidence(std::span<BigNumber const*> bns) const override + { + return BnetSRP6Base::DoCalculateEvidence<CryptoHash>(bns); + } + }; + + template<typename CryptoHash> + class BnetSRP6v2 final : public BnetSRP6v2Base + { + public: + BnetSRP6v2(std::string const& username, Salt const& salt, Verifier const& verifier) + : BnetSRP6v2Base(username, salt, verifier, BigNumber(CryptoHash::GetDigestOf(N.ToByteArray<256>(false), g.ToByteArray<256>(false)), false)) + { + } + + explicit BnetSRP6v2(ForRegistrationTag t) : BnetSRP6v2Base(t) { } + + protected: + BigNumber CalculateU(BigNumber const& A) const override + { + return BigNumber(CryptoHash::GetDigestOf(A.ToByteArray<256>(false), B.ToByteArray<256>(false)), false); + } + + BigNumber DoCalculateEvidence(std::span<BigNumber const*> bns) const override + { + return BnetSRP6Base::DoCalculateEvidence<CryptoHash>(bns); + } }; } + +using SRP::SRP6; } #endif diff --git a/src/common/Cryptography/BigNumber.cpp b/src/common/Cryptography/BigNumber.cpp index b9f6a1c947c..f220ca319be 100644 --- a/src/common/Cryptography/BigNumber.cpp +++ b/src/common/Cryptography/BigNumber.cpp @@ -120,7 +120,7 @@ BigNumber& BigNumber::operator%=(BigNumber const& bn) BN_CTX *bnctx; bnctx = BN_CTX_new(); - BN_mod(_bn, _bn, bn._bn, bnctx); + BN_nnmod(_bn, _bn, bn._bn, bnctx); BN_CTX_free(bnctx); return *this; @@ -166,6 +166,11 @@ int32 BigNumber::GetNumBytes() const return BN_num_bytes(_bn); } +int32 BigNumber::GetNumBits() const +{ + return BN_num_bits(_bn); +} + uint32 BigNumber::AsDword() const { return (uint32)BN_get_word(_bn); diff --git a/src/common/Cryptography/BigNumber.h b/src/common/Cryptography/BigNumber.h index d36c8a9f718..3ec6cc65590 100644 --- a/src/common/Cryptography/BigNumber.h +++ b/src/common/Cryptography/BigNumber.h @@ -34,6 +34,7 @@ class TC_COMMON_API BigNumber BigNumber(uint32 v) : BigNumber() { SetDword(v); } BigNumber(int32 v) : BigNumber() { SetDword(v); } BigNumber(std::string const& v) : BigNumber() { SetHexStr(v); } + BigNumber(std::vector<uint8> const& v, bool littleEndian = true) : BigNumber() { SetBinary(v.data(), v.size(), littleEndian); } template <size_t Size> BigNumber(std::array<uint8, Size> const& v, bool littleEndian = true) : BigNumber() { SetBinary(v.data(), Size, littleEndian); } @@ -113,6 +114,7 @@ class TC_COMMON_API BigNumber BigNumber Exp(BigNumber const&) const; int32 GetNumBytes() const; + int32 GetNumBits() const; struct bignum_st* BN() { return _bn; } struct bignum_st const* BN() const { return _bn; } diff --git a/src/server/bnetserver/REST/LoginHttpSession.h b/src/server/bnetserver/REST/LoginHttpSession.h index 17c94b55bda..2ee70d5afa5 100644 --- a/src/server/bnetserver/REST/LoginHttpSession.h +++ b/src/server/bnetserver/REST/LoginHttpSession.h @@ -19,9 +19,15 @@ #define TRINITYCORE_LOGIN_HTTP_SESSION_H #include "HttpSslSocket.h" +#include "SRP6.h" namespace Battlenet { +struct LoginSessionState : public Trinity::Net::Http::SessionState +{ + std::unique_ptr<Trinity::Crypto::SRP::BnetSRP6Base> Srp; +}; + class LoginHttpSession : public Trinity::Net::Http::SslSocket<LoginHttpSession> { public: @@ -36,6 +42,8 @@ public: Trinity::Net::Http::RequestHandlerResult RequestHandler(Trinity::Net::Http::RequestContext& context) override; + LoginSessionState* GetSessionState() const { return static_cast<LoginSessionState*>(_state.get()); } + protected: std::shared_ptr<Trinity::Net::Http::SessionState> ObtainSessionState(Trinity::Net::Http::RequestContext& context) const override; }; diff --git a/src/server/bnetserver/REST/LoginRESTService.cpp b/src/server/bnetserver/REST/LoginRESTService.cpp index 7636cbe7bf3..b591d248134 100644 --- a/src/server/bnetserver/REST/LoginRESTService.cpp +++ b/src/server/bnetserver/REST/LoginRESTService.cpp @@ -25,6 +25,7 @@ #include "IteratorPair.h" #include "ProtobufJSON.h" #include "Resolver.h" +#include "Timer.h" #include "Util.h" namespace Battlenet @@ -47,7 +48,7 @@ bool LoginRESTService::StartNetwork(Trinity::Asio::IoContext& ioContext, std::st return HandleGetForm(std::move(session), context); }); - RegisterHandler(boost::beast::http::verb::get, "/bnetserver/gameAccounts/", [this](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) + RegisterHandler(boost::beast::http::verb::get, "/bnetserver/gameAccounts/", [](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { return HandleGetGameAccounts(std::move(session), context); }); @@ -105,7 +106,7 @@ bool LoginRESTService::StartNetwork(Trinity::Asio::IoContext& ioContext, std::st input->set_input_id("password"); input->set_type("password"); input->set_label("Password"); - input->set_max_length(16); + input->set_max_length(128); input = _formInputs.add_inputs(); input->set_input_id("log_in_submit"); @@ -114,6 +115,8 @@ bool LoginRESTService::StartNetwork(Trinity::Asio::IoContext& ioContext, std::st _loginTicketDuration = sConfigMgr->GetIntDefault("LoginREST.TicketDuration", 3600); + MigrateLegacyPasswordHashes(); + _acceptor->AsyncAcceptWithCallback<&LoginRESTService::OnSocketAccept>(); return true; } @@ -213,17 +216,17 @@ LoginRESTService::RequestHandlerResult LoginRESTService::HandleGetGameAccounts(s return RequestHandlerResult::Async; } -LoginRESTService::RequestHandlerResult LoginRESTService::HandleGetPortal(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) +LoginRESTService::RequestHandlerResult LoginRESTService::HandleGetPortal(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const { context.response.set(boost::beast::http::field::content_type, "text/plain"); context.response.body() = Trinity::StringFormat("{}:{}", GetHostnameForClient(session->GetRemoteIpAddress()), sConfigMgr->GetIntDefault("BattlenetPort", 1119)); return RequestHandlerResult::Handled; } -LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostLogin(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) +LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostLogin(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const { - JSON::Login::LoginForm loginForm; - if (!::JSON::Deserialize(context.request.body(), &loginForm)) + std::shared_ptr<JSON::Login::LoginForm> loginForm = std::make_shared<JSON::Login::LoginForm>(); + if (!::JSON::Deserialize(context.request.body(), loginForm.get())) { JSON::Login::LoginResult loginResult; loginResult.set_authentication_state(JSON::Login::LOGIN); @@ -238,27 +241,22 @@ LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostLogin(std::sh return RequestHandlerResult::Handled; } - std::string login; - std::string password; - - for (int32 i = 0; i < loginForm.inputs_size(); ++i) + auto getInputValue = [](JSON::Login::LoginForm const* loginForm, std::string_view inputId) -> std::string { - if (loginForm.inputs(i).input_id() == "account_name") - login = loginForm.inputs(i).value(); - else if (loginForm.inputs(i).input_id() == "password") - password = loginForm.inputs(i).value(); - } + for (int32 i = 0; i < loginForm->inputs_size(); ++i) + if (loginForm->inputs(i).input_id() == inputId) + return loginForm->inputs(i).value(); + return ""; + }; + std::string login(getInputValue(loginForm.get(), "account_name")); Utf8ToUpperOnlyLatin(login); - Utf8ToUpperOnlyLatin(password); LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_AUTHENTICATION); stmt->setString(0, login); - std::string sentPasswordHash = CalculateShaPassHash(login, password); - session->QueueQuery(LoginDatabase.AsyncQuery(stmt) - .WithChainingPreparedCallback([this, session, context = std::move(context), login = std::move(login), sentPasswordHash = std::move(sentPasswordHash)](QueryCallback& callback, PreparedQueryResult result) mutable + .WithChainingPreparedCallback([this, session, context = std::move(context), loginForm = std::move(loginForm), getInputValue](QueryCallback& callback, PreparedQueryResult result) mutable { if (!result) { @@ -270,15 +268,33 @@ LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostLogin(std::sh return; } + std::string login(getInputValue(loginForm.get(), "account_name")); + Utf8ToUpperOnlyLatin(login); + bool passwordCorrect = false; + Field* fields = result->Fetch(); uint32 accountId = fields[0].GetUInt32(); - std::string pass_hash = fields[1].GetString(); - uint32 failedLogins = fields[2].GetUInt32(); - std::string loginTicket = fields[3].GetString(); - uint32 loginTicketExpiry = fields[4].GetUInt32(); - bool isBanned = fields[5].GetUInt64() != 0; + if (!session->GetSessionState()->Srp) + { + SrpVersion version = SrpVersion(fields[1].GetInt8()); + std::string srpUsername = ByteArrayToHexStr(Trinity::Crypto::SHA256::GetDigestOf(login)); + Trinity::Crypto::SRP::Salt s = fields[2].GetBinary<Trinity::Crypto::SRP::SALT_LENGTH>(); + Trinity::Crypto::SRP::Verifier v = fields[3].GetBinary(); + session->GetSessionState()->Srp = CreateSrpImplementation(version, SrpHashFunction::Sha256, srpUsername, s, v); + + std::string password(getInputValue(loginForm.get(), "password")); + if (version == SrpVersion::v1) + Utf8ToUpperOnlyLatin(password); + + passwordCorrect = session->GetSessionState()->Srp->CheckCredentials(srpUsername, password); + } + + uint32 failedLogins = fields[4].GetUInt32(); + std::string loginTicket = fields[5].GetString(); + uint32 loginTicketExpiry = fields[6].GetUInt32(); + bool isBanned = fields[7].GetUInt64() != 0; - if (sentPasswordHash != pass_hash) + if (!passwordCorrect) { if (!isBanned) { @@ -358,7 +374,7 @@ LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostLogin(std::sh return RequestHandlerResult::Async; } -LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostRefreshLoginTicket(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) +LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostRefreshLoginTicket(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const { std::string ticket = ExtractAuthorization(context.request); if (ticket.empty()) @@ -397,23 +413,100 @@ LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostRefreshLoginT return RequestHandlerResult::Async; } -std::string LoginRESTService::CalculateShaPassHash(std::string const& name, std::string const& password) +std::unique_ptr<Trinity::Crypto::SRP::BnetSRP6Base> LoginRESTService::CreateSrpImplementation(SrpVersion version, SrpHashFunction hashFunction, + std::string const& username, Trinity::Crypto::SRP::Salt const& salt, Trinity::Crypto::SRP::Verifier const& verifier) { - Trinity::Crypto::SHA256 email; - email.UpdateData(name); - email.Finalize(); + if (version == SrpVersion::v2) + { + if (hashFunction == SrpHashFunction::Sha256) + return std::make_unique<Trinity::Crypto::SRP::BnetSRP6v2<Trinity::Crypto::SHA256>>(username, salt, verifier); + if (hashFunction == SrpHashFunction::Sha512) + return std::make_unique<Trinity::Crypto::SRP::BnetSRP6v2<Trinity::Crypto::SHA512>>(username, salt, verifier); + } - Trinity::Crypto::SHA256 sha; - sha.UpdateData(ByteArrayToHexStr(email.GetDigest())); - sha.UpdateData(":"); - sha.UpdateData(password); - sha.Finalize(); + if (version == SrpVersion::v1) + { + if (hashFunction == SrpHashFunction::Sha256) + return std::make_unique<Trinity::Crypto::SRP::BnetSRP6v1<Trinity::Crypto::SHA256>>(username, salt, verifier); + if (hashFunction == SrpHashFunction::Sha512) + return std::make_unique<Trinity::Crypto::SRP::BnetSRP6v1<Trinity::Crypto::SHA512>>(username, salt, verifier); + } - return ByteArrayToHexStr(sha.GetDigest(), true); + return nullptr; +} + +std::shared_ptr<Trinity::Net::Http::SessionState> LoginRESTService::CreateNewSessionState(boost::asio::ip::address const& address) +{ + std::shared_ptr<LoginSessionState> state = std::make_shared<LoginSessionState>(); + InitAndStoreSessionState(state, address); + return state; } void LoginRESTService::OnSocketAccept(boost::asio::ip::tcp::socket&& sock, uint32 threadIndex) { sLoginService.OnSocketOpen(std::move(sock), threadIndex); } + +void LoginRESTService::MigrateLegacyPasswordHashes() const +{ + if (!LoginDatabase.Query("SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = SCHEMA() AND TABLE_NAME = 'battlenet_accounts' AND COLUMN_NAME = 'sha_pass_hash'")) + return; + + TC_LOG_INFO(_logger, "Updating password hashes..."); + uint32 const start = getMSTime(); + // the auth update query nulls salt/verifier if they cannot be converted + // if they are non-null but s/v have been cleared, that means a legacy tool touched our auth DB (otherwise, the core might've done it itself, it used to use those hacks too) + QueryResult result = LoginDatabase.Query("SELECT id, sha_pass_hash, IF((salt IS null) OR (verifier IS null), 0, 1) AS shouldWarn FROM battlenet_accounts WHERE sha_pass_hash != DEFAULT(sha_pass_hash) OR salt IS NULL OR verifier IS NULL"); + if (!result) + { + TC_LOG_INFO(_logger, ">> No password hashes to update - this took us {} ms to realize", GetMSTimeDiffToNow(start)); + return; + } + + bool hadWarning = false; + uint32 c = 0; + LoginDatabaseTransaction tx = LoginDatabase.BeginTransaction(); + do + { + uint32 const id = (*result)[0].GetUInt32(); + + Trinity::Crypto::SRP::Salt salt = Trinity::Crypto::GetRandomBytes<Trinity::Crypto::SRP::SALT_LENGTH>(); + BigNumber x = Trinity::Crypto::SHA256::GetDigestOf(salt, HexStrToByteArray<Trinity::Crypto::SHA256::DIGEST_LENGTH>((*result)[1].GetString(), true)); + Trinity::Crypto::SRP::Verifier verifier = Trinity::Crypto::SRP::BnetSRP6v1Base::g.ModExp(x, Trinity::Crypto::SRP::BnetSRP6v1Base::N).ToByteVector(); + + if ((*result)[2].GetInt64()) + { + if (!hadWarning) + { + hadWarning = true; + TC_LOG_WARN(_logger, + " ========\n" + "(!) You appear to be using an outdated external account management tool.\n" + "(!) Update your external tool.\n" + "(!!) If no update is available, refer your tool's developer to https://github.com/TrinityCore/TrinityCore/issues/25157.\n" + " ========"); + } + } + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_LOGON); + stmt->setInt8(0, AsUnderlyingType(SrpVersion::v1)); + stmt->setBinary(1, salt); + stmt->setBinary(2, verifier); + stmt->setUInt32(3, id); + tx->Append(stmt); + + tx->Append(Trinity::StringFormat("UPDATE battlenet_accounts SET sha_pass_hash = DEFAULT(sha_pass_hash) WHERE id = {}", id).c_str()); + + if (tx->GetSize() >= 10000) + { + LoginDatabase.CommitTransaction(tx); + tx = LoginDatabase.BeginTransaction(); + } + + ++c; + } while (result->NextRow()); + LoginDatabase.CommitTransaction(tx); + + TC_LOG_INFO(_logger, ">> {} password hashes updated in {} ms", c, GetMSTimeDiffToNow(start)); +} } diff --git a/src/server/bnetserver/REST/LoginRESTService.h b/src/server/bnetserver/REST/LoginRESTService.h index 1313493e023..9d48f1d2710 100644 --- a/src/server/bnetserver/REST/LoginRESTService.h +++ b/src/server/bnetserver/REST/LoginRESTService.h @@ -24,6 +24,18 @@ namespace Battlenet { +enum class SrpVersion : int8 +{ + v1 = 1, + v2 = 2 +}; + +enum class SrpHashFunction +{ + Sha256 = 0, + Sha512 = 1 +}; + enum class BanMode { BAN_IP = 0, @@ -48,19 +60,24 @@ public: std::string const& GetHostnameForClient(boost::asio::ip::address const& address) const; uint16 GetPort() const { return _port; } + std::shared_ptr<Trinity::Net::Http::SessionState> CreateNewSessionState(boost::asio::ip::address const& address) override; + private: static void OnSocketAccept(boost::asio::ip::tcp::socket&& sock, uint32 threadIndex); static std::string ExtractAuthorization(HttpRequest const& request); - RequestHandlerResult HandleGetForm(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); - RequestHandlerResult HandleGetGameAccounts(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); - RequestHandlerResult HandleGetPortal(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); + RequestHandlerResult HandleGetForm(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const; + static RequestHandlerResult HandleGetGameAccounts(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); + RequestHandlerResult HandleGetPortal(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const; + + RequestHandlerResult HandlePostLogin(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const; + RequestHandlerResult HandlePostRefreshLoginTicket(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) const; - RequestHandlerResult HandlePostLogin(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); - RequestHandlerResult HandlePostRefreshLoginTicket(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); + static std::unique_ptr<Trinity::Crypto::SRP::BnetSRP6Base> CreateSrpImplementation(SrpVersion version, SrpHashFunction hashFunction, + std::string const& username, Trinity::Crypto::SRP::Salt const& salt, Trinity::Crypto::SRP::Verifier const& verifier); - static std::string CalculateShaPassHash(std::string const& name, std::string const& password); + void MigrateLegacyPasswordHashes() const; JSON::Login::FormInputs _formInputs; std::string _bindIP; diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index d646194ae8c..27aeaa91535 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -120,7 +120,7 @@ void LoginDatabaseConnection::DoPrepareStatements() #define BnetAccountInfo "ba.id, UPPER(ba.email), ba.locked, ba.lock_country, ba.last_ip, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate, bab.unbandate = bab.bandate" #define BnetGameAccountInfo "a.id, a.username, ab.unbandate, ab.unbandate = ab.bandate, aa.SecurityLevel" - PrepareStatement(LOGIN_SEL_BNET_AUTHENTICATION, "SELECT ba.id, ba.sha_pass_hash, ba.failed_logins, ba.LoginTicket, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id WHERE email = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_SEL_BNET_AUTHENTICATION, "SELECT ba.id, ba.srp_version, ba.salt, ba.verifier, ba.failed_logins, ba.LoginTicket, ba.LoginTicketExpiry, bab.unbandate > UNIX_TIMESTAMP() OR bab.unbandate = bab.bandate FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id WHERE email = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_BNET_AUTHENTICATION, "UPDATE battlenet_accounts SET LoginTicket = ?, LoginTicketExpiry = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_BNET_EXISTING_AUTHENTICATION, "SELECT LoginTicketExpiry FROM battlenet_accounts WHERE LoginTicket = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_BNET_EXISTING_AUTHENTICATION_BY_ID, "SELECT LoginTicket FROM battlenet_accounts WHERE id = ?", CONNECTION_ASYNC); @@ -135,11 +135,12 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_SEL_BNET_LAST_PLAYER_CHARACTERS, "SELECT lpc.accountId, lpc.region, lpc.battlegroup, lpc.realmId, lpc.characterName, lpc.characterGUID, lpc.lastPlayedTime FROM account_last_played_character lpc LEFT JOIN account a ON lpc.accountId = a.id WHERE a.battlenet_account = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_BNET_LAST_PLAYER_CHARACTERS, "DELETE FROM account_last_played_character WHERE accountId = ? AND region = ? AND battlegroup = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_INS_BNET_LAST_PLAYER_CHARACTERS, "INSERT INTO account_last_played_character (accountId, region, battlegroup, realmId, characterName, characterGUID, lastPlayedTime) VALUES (?,?,?,?,?,?,?)", CONNECTION_ASYNC); - PrepareStatement(LOGIN_INS_BNET_ACCOUNT, "INSERT INTO battlenet_accounts (`email`,`sha_pass_hash`) VALUES (?, ?)", CONNECTION_SYNCH); + PrepareStatement(LOGIN_INS_BNET_ACCOUNT, "INSERT INTO battlenet_accounts (`email`,`srp_version`,`salt`,`verifier`) VALUES (?, ?, ?, ?)", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_BNET_ACCOUNT_EMAIL_BY_ID, "SELECT email FROM battlenet_accounts WHERE id = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_BNET_ACCOUNT_ID_BY_EMAIL, "SELECT id FROM battlenet_accounts WHERE email = ?", CONNECTION_SYNCH); - PrepareStatement(LOGIN_UPD_BNET_PASSWORD, "UPDATE battlenet_accounts SET sha_pass_hash = ? WHERE id = ?", CONNECTION_ASYNC); - PrepareStatement(LOGIN_SEL_BNET_CHECK_PASSWORD, "SELECT 1 FROM battlenet_accounts WHERE id = ? AND sha_pass_hash = ?", CONNECTION_SYNCH); + PrepareStatement(LOGIN_UPD_BNET_LOGON, "UPDATE battlenet_accounts SET srp_version = ?, salt = ?, verifier = ? WHERE id = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_SEL_BNET_CHECK_PASSWORD, "SELECT srp_version, salt, verifier FROM battlenet_accounts WHERE id = ?", CONNECTION_SYNCH); + PrepareStatement(LOGIN_SEL_BNET_CHECK_PASSWORD_BY_EMAIL, "SELECT srp_version, salt, verifier FROM battlenet_accounts WHERE email = ?", CONNECTION_BOTH); PrepareStatement(LOGIN_UPD_BNET_ACCOUNT_LOCK, "UPDATE battlenet_accounts SET locked = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_BNET_ACCOUNT_LOCK_CONTRY, "UPDATE battlenet_accounts SET lock_country = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_BNET_ACCOUNT_ID_BY_GAME_ACCOUNT, "SELECT battlenet_account FROM account WHERE id = ?", CONNECTION_BOTH); diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index d70241e2dca..9fa77877cbd 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -129,8 +129,9 @@ enum LoginDatabaseStatements : uint32 LOGIN_INS_BNET_ACCOUNT, LOGIN_SEL_BNET_ACCOUNT_EMAIL_BY_ID, LOGIN_SEL_BNET_ACCOUNT_ID_BY_EMAIL, - LOGIN_UPD_BNET_PASSWORD, + LOGIN_UPD_BNET_LOGON, LOGIN_SEL_BNET_CHECK_PASSWORD, + LOGIN_SEL_BNET_CHECK_PASSWORD_BY_EMAIL, LOGIN_UPD_BNET_ACCOUNT_LOCK, LOGIN_UPD_BNET_ACCOUNT_LOCK_CONTRY, LOGIN_SEL_BNET_ACCOUNT_ID_BY_GAME_ACCOUNT, diff --git a/src/server/game/Accounts/AccountMgr.cpp b/src/server/game/Accounts/AccountMgr.cpp index 0229dd686b4..113e81153ba 100644 --- a/src/server/game/Accounts/AccountMgr.cpp +++ b/src/server/game/Accounts/AccountMgr.cpp @@ -29,6 +29,8 @@ #include "World.h" #include "WorldSession.h" +using AccountSRP6 = Trinity::Crypto::SRP::GruntSRP6; + AccountMgr::AccountMgr() { } AccountMgr::~AccountMgr() @@ -60,9 +62,9 @@ AccountOpResult AccountMgr::CreateAccount(std::string username, std::string pass LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_ACCOUNT); stmt->setString(0, username); - std::pair<Trinity::Crypto::SRP6::Salt, Trinity::Crypto::SRP6::Verifier> registrationData = Trinity::Crypto::SRP6::MakeRegistrationData(username, password); - stmt->setBinary(1, registrationData.first); - stmt->setBinary(2, registrationData.second); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData<AccountSRP6>(username, password); + stmt->setBinary(1, salt); + stmt->setBinary(2, verifier); stmt->setString(3, email); stmt->setString(4, email); @@ -184,10 +186,10 @@ AccountOpResult AccountMgr::ChangeUsername(uint32 accountId, std::string newUser stmt->setUInt32(1, accountId); LoginDatabase.Execute(stmt); - std::pair<Trinity::Crypto::SRP6::Salt, Trinity::Crypto::SRP6::Verifier> registrationData = Trinity::Crypto::SRP6::MakeRegistrationData(newUsername, newPassword); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData<AccountSRP6>(newUsername, newPassword); stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON); - stmt->setBinary(0, registrationData.first); - stmt->setBinary(1, registrationData.second); + stmt->setBinary(0, salt); + stmt->setBinary(1, verifier); stmt->setUInt32(2, accountId); LoginDatabase.Execute(stmt); @@ -212,11 +214,11 @@ AccountOpResult AccountMgr::ChangePassword(uint32 accountId, std::string newPass Utf8ToUpperOnlyLatin(username); Utf8ToUpperOnlyLatin(newPassword); - std::pair<Trinity::Crypto::SRP6::Salt, Trinity::Crypto::SRP6::Verifier> registrationData = Trinity::Crypto::SRP6::MakeRegistrationData(username, newPassword); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData<AccountSRP6>(username, newPassword); LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGON); - stmt->setBinary(0, registrationData.first); - stmt->setBinary(1, registrationData.second); + stmt->setBinary(0, salt); + stmt->setBinary(1, verifier); stmt->setUInt32(2, accountId); LoginDatabase.Execute(stmt); @@ -354,9 +356,9 @@ bool AccountMgr::CheckPassword(std::string username, std::string password) if (PreparedQueryResult result = LoginDatabase.Query(stmt)) { - Trinity::Crypto::SRP6::Salt salt = (*result)[0].GetBinary<Trinity::Crypto::SRP6::SALT_LENGTH>(); - Trinity::Crypto::SRP6::Verifier verifier = (*result)[1].GetBinary<Trinity::Crypto::SRP6::VERIFIER_LENGTH>(); - if (Trinity::Crypto::SRP6::CheckLogin(username, password, salt, verifier)) + Trinity::Crypto::SRP::Salt salt = (*result)[0].GetBinary<Trinity::Crypto::SRP::SALT_LENGTH>(); + Trinity::Crypto::SRP::Verifier verifier = (*result)[1].GetBinary(); + if (AccountSRP6(username, salt, verifier).CheckCredentials(username, password)) return true; } @@ -378,9 +380,9 @@ bool AccountMgr::CheckPassword(uint32 accountId, std::string password) if (PreparedQueryResult result = LoginDatabase.Query(stmt)) { - Trinity::Crypto::SRP6::Salt salt = (*result)[0].GetBinary<Trinity::Crypto::SRP6::SALT_LENGTH>(); - Trinity::Crypto::SRP6::Verifier verifier = (*result)[1].GetBinary<Trinity::Crypto::SRP6::VERIFIER_LENGTH>(); - if (Trinity::Crypto::SRP6::CheckLogin(username, password, salt, verifier)) + Trinity::Crypto::SRP::Salt salt = (*result)[0].GetBinary<Trinity::Crypto::SRP::SALT_LENGTH>(); + Trinity::Crypto::SRP::Verifier verifier = (*result)[1].GetBinary(); + if (AccountSRP6(username, salt, verifier).CheckCredentials(username, password)) return true; } diff --git a/src/server/game/Accounts/BattlenetAccountMgr.cpp b/src/server/game/Accounts/BattlenetAccountMgr.cpp index 08a807b8db6..8b57ac71093 100644 --- a/src/server/game/Accounts/BattlenetAccountMgr.cpp +++ b/src/server/game/Accounts/BattlenetAccountMgr.cpp @@ -17,29 +17,41 @@ #include "BattlenetAccountMgr.h" #include "AccountMgr.h" -#include "CryptoHash.h" #include "DatabaseEnv.h" +#include "SRP6.h" #include "Util.h" using GameAccountMgr = AccountMgr; +using BnetSRP6_OLD = Trinity::Crypto::SRP::BnetSRP6v1<Trinity::Crypto::SHA256>; +using BnetSRP6 = Trinity::Crypto::SRP::BnetSRP6v2<Trinity::Crypto::SHA256>; + +enum class SrpVersion : int8 +{ + v1 = 1, // password length limit 16 characters, case-insensitive, uses SHA256 to generate verifier + v2 = 2 // password length limit 128 characters, case-sensitive, uses PBKDF2 with SHA512 to generate verifier +}; AccountOpResult Battlenet::AccountMgr::CreateBattlenetAccount(std::string email, std::string password, bool withGameAccount, std::string* gameAccountName) { if (utf8length(email) > MAX_BNET_EMAIL_STR) return AccountOpResult::AOR_NAME_TOO_LONG; - if (utf8length(password) > MAX_PASS_STR) + if (utf8length(password) > MAX_BNET_PASS_STR) return AccountOpResult::AOR_PASS_TOO_LONG; Utf8ToUpperOnlyLatin(email); - Utf8ToUpperOnlyLatin(password); if (GetId(email)) return AccountOpResult::AOR_NAME_ALREADY_EXIST; + std::string srpUsername = GetSrpUsername(email); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData<BnetSRP6>(srpUsername, password); + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_BNET_ACCOUNT); stmt->setString(0, email); - stmt->setString(1, CalculateShaPassHash(email, password)); + stmt->setInt8(1, AsUnderlyingType(SrpVersion::v2)); + stmt->setBinary(2, salt); + stmt->setBinary(3, verifier); LoginDatabase.DirectExecute(stmt); uint32 newAccountId = GetId(email); @@ -48,7 +60,9 @@ AccountOpResult Battlenet::AccountMgr::CreateBattlenetAccount(std::string email, if (withGameAccount) { *gameAccountName = std::to_string(newAccountId) + "#1"; - GameAccountMgr::instance()->CreateAccount(*gameAccountName, password, email, newAccountId, 1); + std::string gameAccountPassword = password.substr(0, MAX_PASS_STR); + Utf8ToUpperOnlyLatin(gameAccountPassword); + GameAccountMgr::instance()->CreateAccount(*gameAccountName, gameAccountPassword, email, newAccountId, 1); } return AccountOpResult::AOR_OK; @@ -60,14 +74,17 @@ AccountOpResult Battlenet::AccountMgr::ChangePassword(uint32 accountId, std::str if (!GetName(accountId, username)) return AccountOpResult::AOR_NAME_NOT_EXIST; // account doesn't exist - Utf8ToUpperOnlyLatin(username); - Utf8ToUpperOnlyLatin(newPassword); - if (utf8length(newPassword) > MAX_PASS_STR) + if (utf8length(newPassword) > MAX_BNET_PASS_STR) return AccountOpResult::AOR_PASS_TOO_LONG; - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_PASSWORD); - stmt->setString(0, CalculateShaPassHash(username, newPassword)); - stmt->setUInt32(1, accountId); + std::string srpUsername = GetSrpUsername(username); + auto [salt, verifier] = Trinity::Crypto::SRP6::MakeRegistrationData<BnetSRP6>(srpUsername, newPassword); + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_LOGON); + stmt->setInt8(0, AsUnderlyingType(SrpVersion::v2)); + stmt->setBinary(1, salt); + stmt->setBinary(2, verifier); + stmt->setUInt32(3, accountId); LoginDatabase.Execute(stmt); return AccountOpResult::AOR_OK; @@ -80,14 +97,26 @@ bool Battlenet::AccountMgr::CheckPassword(uint32 accountId, std::string password if (!GetName(accountId, username)) return false; - Utf8ToUpperOnlyLatin(username); - Utf8ToUpperOnlyLatin(password); - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_CHECK_PASSWORD); stmt->setUInt32(0, accountId); - stmt->setString(1, CalculateShaPassHash(username, password)); - return LoginDatabase.Query(stmt) != nullptr; + if (PreparedQueryResult result = LoginDatabase.Query(stmt)) + { + Trinity::Crypto::SRP::Salt salt = (*result)[1].GetBinary<Trinity::Crypto::SRP::SALT_LENGTH>(); + Trinity::Crypto::SRP::Verifier verifier = (*result)[2].GetBinary(); + switch (SrpVersion((*result)[0].GetInt8())) + { + case SrpVersion::v1: + Utf8ToUpperOnlyLatin(password); + return BnetSRP6_OLD(username, salt, verifier).CheckCredentials(username, password); + case SrpVersion::v2: + return BnetSRP6(username, salt, verifier).CheckCredentials(username, password); + default: + break; + } + } + + return false; } AccountOpResult Battlenet::AccountMgr::LinkWithGameAccount(std::string_view email, std::string_view gameAccountName) @@ -179,17 +208,8 @@ uint8 Battlenet::AccountMgr::GetMaxIndex(uint32 accountId) return 0; } -std::string Battlenet::AccountMgr::CalculateShaPassHash(std::string_view name, std::string_view password) +std::string Battlenet::AccountMgr::GetSrpUsername(std::string name) { - Trinity::Crypto::SHA256 email; - email.UpdateData(name); - email.Finalize(); - - Trinity::Crypto::SHA256 sha; - sha.UpdateData(ByteArrayToHexStr(email.GetDigest())); - sha.UpdateData(":"); - sha.UpdateData(password); - sha.Finalize(); - - return ByteArrayToHexStr(sha.GetDigest(), true); + Utf8ToUpperOnlyLatin(name); + return ByteArrayToHexStr(Trinity::Crypto::SHA256::GetDigestOf(name)); } diff --git a/src/server/game/Accounts/BattlenetAccountMgr.h b/src/server/game/Accounts/BattlenetAccountMgr.h index bfeb02302f7..b4fe7626b42 100644 --- a/src/server/game/Accounts/BattlenetAccountMgr.h +++ b/src/server/game/Accounts/BattlenetAccountMgr.h @@ -25,6 +25,7 @@ class QueryCallback; enum class AccountOpResult : uint8; #define MAX_BNET_EMAIL_STR 320 +#define MAX_BNET_PASS_STR 128 namespace Battlenet { @@ -42,7 +43,7 @@ namespace Battlenet [[nodiscard]] TC_GAME_API QueryCallback GetIdByGameAccountAsync(uint32 gameAccountId); TC_GAME_API uint8 GetMaxIndex(uint32 accountId); - TC_GAME_API std::string CalculateShaPassHash(std::string_view name, std::string_view password); + TC_GAME_API std::string GetSrpUsername(std::string name); } } diff --git a/src/server/shared/Networking/Http/HttpService.cpp b/src/server/shared/Networking/Http/HttpService.cpp index 3e80d08959c..8bb533d73c0 100644 --- a/src/server/shared/Networking/Http/HttpService.cpp +++ b/src/server/shared/Networking/Http/HttpService.cpp @@ -81,9 +81,9 @@ RequestHandlerResult DispatcherService::HandleRequest(std::shared_ptr<AbstractSo return context.handler->Func(std::move(session), context); } -RequestHandlerResult DispatcherService::HandlePathNotFound(std::shared_ptr<AbstractSocket> /*session*/, RequestContext& context) +RequestHandlerResult DispatcherService::HandleBadRequest(std::shared_ptr<AbstractSocket> /*session*/, RequestContext& context) { - context.response.result(boost::beast::http::status::not_found); + context.response.result(boost::beast::http::status::bad_request); return RequestHandlerResult::Handled; } @@ -93,6 +93,12 @@ RequestHandlerResult DispatcherService::HandleUnauthorized(std::shared_ptr<Abstr return RequestHandlerResult::Handled; } +RequestHandlerResult DispatcherService::HandlePathNotFound(std::shared_ptr<AbstractSocket> /*session*/, RequestContext& context) +{ + context.response.result(boost::beast::http::status::not_found); + return RequestHandlerResult::Handled; +} + void DispatcherService::RegisterHandler(boost::beast::http::verb method, std::string_view path, std::function<RequestHandlerResult(std::shared_ptr<AbstractSocket> session, RequestContext& context)> handler, RequestHandlerFlag flags) diff --git a/src/server/shared/Networking/Http/HttpService.h b/src/server/shared/Networking/Http/HttpService.h index 01c66146ae3..e68f8d2795f 100644 --- a/src/server/shared/Networking/Http/HttpService.h +++ b/src/server/shared/Networking/Http/HttpService.h @@ -61,8 +61,9 @@ public: RequestHandlerResult HandleRequest(std::shared_ptr<AbstractSocket> session, RequestContext& context); - RequestHandlerResult HandlePathNotFound(std::shared_ptr<AbstractSocket> session, RequestContext& context); - RequestHandlerResult HandleUnauthorized(std::shared_ptr<AbstractSocket> session, RequestContext& context); + static RequestHandlerResult HandleBadRequest(std::shared_ptr<AbstractSocket> session, RequestContext& context); + static RequestHandlerResult HandleUnauthorized(std::shared_ptr<AbstractSocket> session, RequestContext& context); + static RequestHandlerResult HandlePathNotFound(std::shared_ptr<AbstractSocket> session, RequestContext& context); protected: void RegisterHandler(boost::beast::http::verb method, std::string_view path, @@ -179,7 +180,6 @@ protected: return threads; } -private: Asio::IoContext* _ioContext; std::string _logger; }; |