Skip to content

Commit 4b10465

Browse files
Laizeroxkillerwife
authored andcommitted
[z2716] Implement totp auth
Credit goes to TC/Chaosvex for reversing structures, Laizerox for implementing it and Killerwife for porting it
1 parent 8c66cdd commit 4b10465

11 files changed

Lines changed: 262 additions & 63 deletions

File tree

sql/base/realmd.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
DROP TABLE IF EXISTS `realmd_db_version`;
2323
CREATE TABLE `realmd_db_version` (
24-
`required_z2678_01_realmd` bit(1) DEFAULT NULL
24+
`required_z2716_01_realmd_totp` bit(1) DEFAULT NULL
2525
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='Last applied sql update to DB';
2626

2727
--
@@ -58,6 +58,7 @@ CREATE TABLE `account` (
5858
`expansion` tinyint(3) unsigned NOT NULL DEFAULT '0',
5959
`mutetime` bigint(40) unsigned NOT NULL DEFAULT '0',
6060
`locale` tinyint(3) unsigned NOT NULL DEFAULT '0',
61+
`token` text,
6162
PRIMARY KEY (`id`),
6263
UNIQUE KEY `idx_username` (`username`),
6364
KEY `idx_gmlevel` (`gmlevel`)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE realmd_db_version CHANGE COLUMN required_z2678_01_realmd required_z2716_01_realmd_totp bit;
2+
3+
ALTER TABLE account ADD COLUMN token text AFTER locale;

src/realmd/AuthSocket.cpp

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
*/
2222

2323
#include "Common.h"
24+
#include "Auth/HMACSHA1.h"
25+
#include "Auth/base32.h"
2426
#include "Database/DatabaseEnv.h"
2527
#include "Config/Config.h"
2628
#include "Log.h"
@@ -29,6 +31,8 @@
2931
#include "AuthCodes.h"
3032

3133
#include <openssl/md5.h>
34+
#include <ctime>
35+
3236
//#include "Util.h" -- for commented utf8ToUpperOnlyLatin
3337

3438
extern DatabaseType LoginDatabase;
@@ -40,6 +44,14 @@ enum AccountFlags
4044
ACCOUNT_FLAG_PROPASS = 0x00800000,
4145
};
4246

47+
enum SecurityFlags
48+
{
49+
SECURITY_FLAG_NONE = 0x00,
50+
SECURITY_FLAG_PIN = 0x01,
51+
SECURITY_FLAG_UNK = 0x02,
52+
SECURITY_FLAG_AUTHENTICATOR = 0x04
53+
};
54+
4355
// GCC have alternative #pragma pack(N) syntax and old gcc version not support pack(push,N), also any gcc version not support it at some paltform
4456
#if defined( __GNUC__ )
4557
#pragma pack(1)
@@ -66,6 +78,18 @@ typedef struct AUTH_LOGON_CHALLENGE_C
6678
uint8 I[1];
6779
} sAuthLogonChallenge_C;
6880

81+
typedef struct AUTH_LOGON_PIN_DATA_C
82+
{
83+
uint8 salt[16];
84+
uint8 hash[20];
85+
} sAuthLogonPinData_C;
86+
87+
typedef struct AUTH_LOGON_AUTHENTICATOR_DATA_C
88+
{
89+
uint8 unk; // Has to be 0x01
90+
uint8 keys[6]; // Valid code must be 6 digits
91+
} sAuthLogonAuthenticatorData_C;
92+
6993
// typedef sAuthLogonChallenge_C sAuthReconnectChallenge_C;
7094
/*
7195
typedef struct
@@ -265,7 +289,7 @@ void AuthSocket::SendProof(Sha1Hash sha)
265289
case 6005: // 1.12.2
266290
case 6141: // 1.12.3
267291
{
268-
sAuthLogonProof_S_BUILD_6005 proof;
292+
sAuthLogonProof_S_BUILD_6005 proof{};
269293
memcpy(proof.M2, sha.GetDigest(), 20);
270294
proof.cmd = CMD_AUTH_LOGON_PROOF;
271295
proof.error = 0;
@@ -282,7 +306,7 @@ void AuthSocket::SendProof(Sha1Hash sha)
282306
case 12340: // 3.3.5a
283307
default: // or later
284308
{
285-
sAuthLogonProof_S proof;
309+
sAuthLogonProof_S proof{};
286310
memcpy(proof.M2, sha.GetDigest(), 20);
287311
proof.cmd = CMD_AUTH_LOGON_PROOF;
288312
proof.error = 0;
@@ -358,30 +382,35 @@ bool AuthSocket::_HandleLogonChallenge()
358382

359383
///- Verify that this IP is not in the ip_banned table
360384
// No SQL injection possible (paste the IP address as passed by the socket)
361-
QueryResult* result = LoginDatabase.PQuery("SELECT unbandate FROM ip_banned WHERE "
362-
// permanent still banned
363-
"(unbandate = bandate OR unbandate > UNIX_TIMESTAMP()) AND ip = '%s'", m_address.c_str());
364-
if (result)
385+
std::unique_ptr<QueryResult> ip_banned_result(LoginDatabase.PQuery("SELECT unbandate FROM ip_banned "
386+
"WHERE (unbandate = bandate OR unbandate > UNIX_TIMESTAMP()) AND ip = '%s'", m_address.c_str()));
387+
388+
std::unique_ptr<QueryResult> account_banned_result(LoginDatabase.PQuery(
389+
"SELECT ab.unbandate FROM account_banned ab LEFT JOIN account a ON a.id = ab.id "
390+
"WHERE active = 1 AND a.username = '%s' AND (ab.unbandate = ab.bandate OR ab.unbandate > UNIX_TIMESTAMP())", _safelogin.c_str()));
391+
392+
if (ip_banned_result || account_banned_result)
365393
{
366394
pkt << (uint8)WOW_FAIL_BANNED;
367395
BASIC_LOG("[AuthChallenge] Banned ip %s tries to login!", m_address.c_str());
368-
delete result;
369396
}
370397
else
371398
{
372399
///- Get the account details from the account table
373400
// No SQL injection (escaped user name)
374401

375-
result = LoginDatabase.PQuery("SELECT sha_pass_hash,id,locked,last_ip,gmlevel,v,s FROM account WHERE username = '%s'", _safelogin.c_str());
402+
QueryResult* result = LoginDatabase.PQuery("SELECT sha_pass_hash,id,locked,last_ip,gmlevel,v,s,token FROM account WHERE username = '%s'", _safelogin.c_str());
376403
if (result)
377404
{
405+
Field* fields = result->Fetch();
406+
378407
///- If the IP is 'locked', check that the player comes indeed from the correct IP address
379408
bool locked = false;
380-
if ((*result)[2].GetUInt8() == 1) // if ip is locked
409+
if (fields[2].GetUInt8() == 1) // if ip is locked
381410
{
382-
DEBUG_LOG("[AuthChallenge] Account '%s' is locked to IP - '%s'", _login.c_str(), (*result)[3].GetString());
411+
DEBUG_LOG("[AuthChallenge] Account '%s' is locked to IP - '%s'", _login.c_str(), fields[3].GetString());
383412
DEBUG_LOG("[AuthChallenge] Player address is '%s'", m_address.c_str());
384-
if (strcmp((*result)[3].GetString(), m_address.c_str()))
413+
if (strcmp(fields[3].GetString(), m_address.c_str()))
385414
{
386415
DEBUG_LOG("[AuthChallenge] Account IP differs");
387416
pkt << (uint8) WOW_FAIL_SUSPENDED;
@@ -401,7 +430,7 @@ bool AuthSocket::_HandleLogonChallenge()
401430
{
402431
///- If the account is banned, reject the logon attempt
403432
QueryResult* banresult = LoginDatabase.PQuery("SELECT bandate,unbandate FROM account_banned WHERE "
404-
"id = %u AND active = 1 AND (unbandate > UNIX_TIMESTAMP() OR unbandate = bandate)", (*result)[1].GetUInt32());
433+
"id = %u AND active = 1 AND (unbandate > UNIX_TIMESTAMP() OR unbandate = bandate)", fields[1].GetUInt32());
405434
if (banresult)
406435
{
407436
if ((*banresult)[0].GetUInt64() == (*banresult)[1].GetUInt64())
@@ -420,11 +449,11 @@ bool AuthSocket::_HandleLogonChallenge()
420449
else
421450
{
422451
///- Get the password from the account table, upper it, and make the SRP6 calculation
423-
std::string rI = (*result)[0].GetCppString();
452+
std::string rI = fields[0].GetCppString();
424453

425454
///- Don't calculate (v, s) if there are already some in the database
426-
std::string databaseV = (*result)[5].GetCppString();
427-
std::string databaseS = (*result)[6].GetCppString();
455+
std::string databaseV = fields[5].GetCppString();
456+
std::string databaseS = fields[6].GetCppString();
428457

429458
DEBUG_LOG("database authentication values: v='%s' s='%s'", databaseV.c_str(), databaseS.c_str());
430459

@@ -458,15 +487,21 @@ bool AuthSocket::_HandleLogonChallenge()
458487
pkt.append(s.AsByteArray(), s.GetNumBytes());// 32 bytes
459488
pkt.append(unk3.AsByteArray(16), 16);
460489
uint8 securityFlags = 0;
461-
pkt << uint8(securityFlags); // security flags (0x0...0x04)
462490

463-
if (securityFlags & 0x01) // PIN input
491+
_token = fields[7].GetCppString();
492+
if (!_token.empty() && _build >= 8606) // authenticator was added in 2.4.3
493+
securityFlags = SECURITY_FLAG_AUTHENTICATOR;
494+
495+
pkt << uint8(securityFlags); // security flags (0x0...0x04)
496+
497+
if (securityFlags & SECURITY_FLAG_PIN) // PIN input
464498
{
465499
pkt << uint32(0);
466-
pkt << uint64(0) << uint64(0); // 16 bytes hash?
500+
pkt << uint64(0);
501+
pkt << uint64(0);
467502
}
468503

469-
if (securityFlags & 0x02) // Matrix input
504+
if (securityFlags & SECURITY_FLAG_UNK) // Matrix input
470505
{
471506
pkt << uint8(0);
472507
pkt << uint8(0);
@@ -475,12 +510,10 @@ bool AuthSocket::_HandleLogonChallenge()
475510
pkt << uint64(0);
476511
}
477512

478-
if (securityFlags & 0x04) // Security token input
479-
{
513+
if (securityFlags & SECURITY_FLAG_AUTHENTICATOR) // Authenticator input
480514
pkt << uint8(1);
481-
}
482515

483-
uint8 secLevel = (*result)[4].GetUInt8();
516+
uint8 secLevel = fields[4].GetUInt8();
484517
_accountSecurityLevel = secLevel <= SEC_ADMINISTRATOR ? AccountTypes(secLevel) : SEC_ADMINISTRATOR;
485518

486519
_localizationName.resize(4);
@@ -496,10 +529,9 @@ bool AuthSocket::_HandleLogonChallenge()
496529
delete result;
497530
}
498531
else // no account
499-
{
500532
pkt << (uint8) WOW_FAIL_UNKNOWN_ACCOUNT;
501-
}
502533
}
534+
503535
Write((const char *)pkt.contents(), pkt.size());
504536
return true;
505537
}
@@ -509,7 +541,7 @@ bool AuthSocket::_HandleLogonProof()
509541
{
510542
DEBUG_LOG("Entering _HandleLogonProof");
511543
///- Read the packet
512-
sAuthLogonProof_C lp;
544+
sAuthLogonProof_C lp{};
513545
if (!Read((char*)&lp, sizeof(sAuthLogonProof_C)))
514546
return false;
515547

@@ -519,7 +551,6 @@ bool AuthSocket::_HandleLogonProof()
519551
/// <ul><li> If the client has no valid version
520552
if (!FindBuildInfo(_build))
521553
{
522-
523554
// no patch found
524555
ByteBuffer pkt;
525556
pkt << (uint8) CMD_AUTH_LOGON_CHALLENGE;
@@ -533,7 +564,6 @@ bool AuthSocket::_HandleLogonProof()
533564

534565
///- Continue the SRP6 calculation based on data received from the client
535566
BigNumber A;
536-
537567
A.SetBinary(lp.A, 32);
538568

539569
// SRP safeguard: abort if A==0
@@ -611,6 +641,28 @@ bool AuthSocket::_HandleLogonProof()
611641
///- Check if SRP6 results match (password is correct), else send an error
612642
if (!memcmp(M.AsByteArray(), lp.M1, 20))
613643
{
644+
if (lp.securityFlags & SECURITY_FLAG_AUTHENTICATOR || !_token.empty())
645+
{
646+
sAuthLogonAuthenticatorData_C authData{};
647+
if (!Read((char*) &authData, sizeof(sAuthLogonAuthenticatorData_C)))
648+
{
649+
const char data[4] = {CMD_AUTH_LOGON_PROOF, WOW_FAIL_UNKNOWN_ACCOUNT, 3, 0};
650+
Write(data, sizeof(data));
651+
return true;
652+
}
653+
654+
auto ServerToken = generateToken(_token.c_str());
655+
auto clientToken = atoi((const char*) authData.keys);
656+
if (ServerToken != clientToken)
657+
{
658+
BASIC_LOG("[AuthChallenge] Account %s tried to login with wrong pincode! Given %u Expected %u", _login.c_str(), clientToken, ServerToken);
659+
660+
const char data[4] = { CMD_AUTH_LOGON_PROOF, WOW_FAIL_UNKNOWN_ACCOUNT, 3, 0};
661+
Write(data, sizeof(data));
662+
return true;
663+
}
664+
}
665+
614666
BASIC_LOG("User '%s' successfully authenticated", _login.c_str());
615667

616668
///- Update the sessionkey, last_ip, last login time and reset number of failed logins in the account table for this account
@@ -839,7 +891,6 @@ bool AuthSocket::_HandleRealmList()
839891
hdr.append(pkt);
840892

841893
Write((const char *)hdr.contents(), hdr.size());
842-
843894
return true;
844895
}
845896

@@ -1004,4 +1055,29 @@ bool AuthSocket::_HandleXferAccept()
10041055
ReadSkip(1);
10051056

10061057
return true;
1058+
}
1059+
1060+
int32 AuthSocket::generateToken(char const* b32key)
1061+
{
1062+
size_t keySize = strlen(b32key);
1063+
size_t bufSize = (keySize + 7) / 8 * 5;
1064+
char* encoded = new char[bufSize];
1065+
memset(encoded, 0, bufSize);
1066+
unsigned int hmac_result_size = HMAC_RES_SIZE;
1067+
unsigned char hmac_result[HMAC_RES_SIZE];
1068+
unsigned long timestamp = time(nullptr)/30;
1069+
unsigned char challenge[8];
1070+
1071+
for (int i = 8; i--; timestamp >>= 8)
1072+
challenge[i] = timestamp;
1073+
1074+
base32_decode(b32key, encoded, bufSize);
1075+
HMAC(EVP_sha1(), encoded, bufSize, challenge, 8, hmac_result, &hmac_result_size);
1076+
unsigned int offset = hmac_result[19] & 0xF;
1077+
unsigned int truncHash = (hmac_result[offset] << 24) | (hmac_result[offset + 1] << 16 ) | (hmac_result[offset + 2] << 8) | (hmac_result[offset + 3]);
1078+
truncHash &= 0x7FFFFFFF;
1079+
1080+
delete[] encoded;
1081+
1082+
return truncHash % 1000000;
10071083
}

src/realmd/AuthSocket.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
#include <functional>
3636

37+
#define HMAC_RES_SIZE 20
38+
3739
class AuthSocket : public MaNGOS::Socket
3840
{
3941
public:
@@ -43,6 +45,7 @@ class AuthSocket : public MaNGOS::Socket
4345

4446
void SendProof(Sha1Hash sha);
4547
void LoadRealmlist(ByteBuffer& pkt, uint32 acctid);
48+
int32 generateToken(char const* b32key);
4649

4750
bool _HandleLogonChallenge();
4851
bool _HandleLogonProof();
@@ -77,6 +80,7 @@ class AuthSocket : public MaNGOS::Socket
7780

7881
std::string _login;
7982
std::string _safelogin;
83+
std::string _token;
8084

8185
// Since GetLocaleByName() is _NOT_ bijective, we have to store the locale as a string. Otherwise we can't differ
8286
// between enUS and enGB, which is important for the patch system

src/shared/Auth/AuthCrypt.cpp

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,12 @@
1717
*/
1818

1919
#include "AuthCrypt.h"
20-
#include "Hmac.h"
20+
#include "HMACSHA1.h"
21+
#include "Log.h"
2122
#include "BigNumber.h"
2223

2324
AuthCrypt::AuthCrypt() : _initialized(false) {}
2425

25-
void AuthCrypt::Init(BigNumber *bn)
26-
{
27-
_send_i = _send_j = _recv_i = _recv_j = 0;
28-
29-
const size_t len = 40;
30-
31-
_key.resize(len);
32-
auto const key = bn->AsByteArray();
33-
std::copy(key, key + len, _key.begin());
34-
35-
_initialized = true;
36-
}
37-
3826
void AuthCrypt::DecryptRecv(uint8* data, size_t len)
3927
{
4028
if (!_initialized) return;
@@ -63,3 +51,16 @@ void AuthCrypt::EncryptSend(uint8* data, size_t len)
6351
data[t] = _send_j = x;
6452
}
6553
}
54+
55+
void AuthCrypt::Init(BigNumber *bn)
56+
{
57+
_send_i = _send_j = _recv_i = _recv_j = 0;
58+
59+
const size_t len = 40;
60+
61+
_key.resize(len);
62+
auto const key = bn->AsByteArray();
63+
std::copy(key, key + len, _key.begin());
64+
65+
_initialized = true;
66+
}

0 commit comments

Comments
 (0)