SRP Authentication Implementation
Overview
The SRP (Secure Remote Password) authentication system has been fully implemented for World of Warcraft 3.3.5a compatibility. This implementation follows the SRP6a protocol as used by the original wowee client.
Components
1. BigNum (include/auth/big_num.hpp)
Wrapper around OpenSSL’s BIGNUM for arbitrary-precision integer arithmetic.
Key Features:
- Little-endian byte array conversion (WoW protocol requirement)
- Modular exponentiation (critical for SRP)
- All standard arithmetic operations
- Random number generation
Usage Example:
// Create from bytes (little-endian)
std::vector<uint8_t> bytes = {0x01, 0x02, 0x03};
BigNum num(bytes, true);
// Modular exponentiation: result = base^exp mod N
BigNum result = base.modPow(exponent, modulus);
// Convert back to bytes
std::vector<uint8_t> output = num.toArray(true, 32); // 32 bytes, little-endian
2. SRP (include/auth/srp.hpp)
Complete SRP6a authentication implementation.
Authentication Flow
Phase 1: Initialization
#include "auth/srp.hpp"
SRP srp;
srp.initialize("username", "password");
What happens:
- Stores credentials until
feed()has used them - Marks SRP as initialized
initializeWithHash(username, authHash) does the same with a stored SHA1(UPPER(user):UPPER(pass)) in place of the password, which is how a remembered login authenticates (AuthHandler::authenticateWithHash).
Phase 2: Server Challenge Processing
When you receive the LOGON_CHALLENGE response from the auth server:
// Extract from server packet:
std::vector<uint8_t> B; // 32 bytes - server public ephemeral
std::vector<uint8_t> g; // 1 byte (0x07)
std::vector<uint8_t> N; // 32 bytes - prime modulus
std::vector<uint8_t> salt; // 32 bytes - salt
// Feed to SRP
srp.feed(B, g, N, salt);
What happens internally:
- Stores server values (B, g, N, salt)
- Computes
x = H(salt | H(USERNAME:PASSWORD)), both uppercased (or uses the stored hash) - Generates random client ephemeral
a(19 bytes) - Computes
A = g^a mod N - Computes scrambler
u = H(A | B) - Computes session key
S = (B - 3*g^x)^(a + u*x) mod N - Splits S, hashes halves, interleaves to create
K(40 bytes) - Computes client proof
M1 = H(H(N)^H(g) | H(username) | salt | A | B | K) - Pre-computes server proof
M2 = H(A | M1 | K) - Zeroes and releases the stored password or hash (
clearCredentials())
Phase 3: Sending Client Proof
Send LOGON_PROOF packet to server:
// Get values to send in packet
std::vector<uint8_t> A = srp.getA(); // 32 bytes
std::vector<uint8_t> M1 = srp.getM1(); // 20 bytes
// Build LOGON_PROOF packet (LogonProofPacket::build):
// - A (32 bytes)
// - M1 (20 bytes)
// - CRC / integrity hash (20 bytes; zeros when the client files are not found)
// - Number of keys (1 byte: 0)
// - Security flags (1 byte: as the server sent them)
// - PIN client salt + hash (16 + 20 bytes, only when flag 0x01 is set)
Phase 4: Server Proof Verification
When you receive LOGON_PROOF response:
// Extract M2 from server response (20 bytes)
std::vector<uint8_t> serverM2; // From packet
// Verify
if (srp.verifyServerProof(serverM2)) {
LOG_INFO("Authentication successful!");
// Get session key for encryption
std::vector<uint8_t> K = srp.getSessionKey(); // 40 bytes
// Now you can connect to world server
} else {
LOG_ERROR("Authentication failed!");
}
Complete Example
AuthHandler (src/auth/auth_handler.cpp) runs this sequence against a live server; the send and receive helpers below stand in for it.
#include "auth/srp.hpp"
#include "core/logger.hpp"
void authenticateWithServer(const std::string& username,
const std::string& password) {
// 1. Initialize SRP
SRP srp;
srp.initialize(username, password);
// 2. Send LOGON_CHALLENGE to server
// (with username, version, build, platform, etc.)
sendLogonChallenge(username);
// 3. Receive server response
auto response = receiveLogonChallengeResponse();
if (!response.isSuccess()) {
LOG_ERROR("Logon challenge failed: ", getAuthResultString(response.result));
return;
}
// 4. Feed server challenge to SRP
srp.feed(response.B, response.g, response.N, response.salt);
// 5. Send LOGON_PROOF
std::vector<uint8_t> A = srp.getA();
std::vector<uint8_t> M1 = srp.getM1();
sendLogonProof(A, M1);
// 6. Receive and verify server proof
auto proofResponse = receiveLogonProofResponse();
if (srp.verifyServerProof(proofResponse.M2)) {
LOG_INFO("Successfully authenticated!");
// Store session key for world server
sessionKey = srp.getSessionKey();
// Proceed to realm list
requestRealmList();
} else {
LOG_ERROR("Server proof verification failed!");
}
}
Packet Structures
LOGON_CHALLENGE (Client → Server)
Offset | Size | Type | Description
-------|------|--------|----------------------------------
0x00 | 1 | uint8 | Opcode (0x00)
0x01 | 1 | uint8 | Protocol version (8 for 3.3.5a)
0x02 | 2 | uint16 | Size (30 + account name length)
0x04 | 4 | char[4]| Game ("WoW\0")
0x08 | 3 | uint8 | Version (major, minor, patch)
0x0B | 2 | uint16 | Build (e.g., 12340 for 3.3.5a)
0x0D | 4 | char[4]| Platform ("68x\0", "x86" reversed)
0x11 | 4 | char[4]| OS ("niW\0", "Win" reversed)
0x15 | 4 | char[4]| Locale ("SUne", "enUS" reversed)
0x19 | 4 | uint32 | Timezone bias
0x1D | 4 | uint32 | IP address (local outbound IPv4, or 0)
0x21 | 1 | uint8 | Account name length
0x22 | N | char[] | Account name (uppercase)
The version, build, platform, OS and locale come from ClientInfo, which the login screen (src/ui/auth_screen.cpp) fills from the active expansion profile. The protocol version is the profile’s, and a vanilla-family profile retries with the other vanilla value (3 or 8) after a failure that looks like a protocol mismatch. The four-character fields are sent reversed and null-padded, because the server reads them as a C string and reverses it.
LOGON_CHALLENGE Response (Server → Client)
Success (status = 0):
Offset | Size | Type | Description
-------|------|--------|----------------------------------
0x00 | 1 | uint8 | Opcode (0x00)
0x01 | 1 | uint8 | Unknown / protocol byte
0x02 | 1 | uint8 | Status (0 = success)
0x03 | 32 | uint8[]| B (server public ephemeral)
0x23 | 1 | uint8 | g length
0x24 | N | uint8[]| g (generator, 1 byte: 0x07)
| 1 | uint8 | N length
| M | uint8[]| N (prime, 32 bytes)
| 32 | uint8[]| salt
| 16 | uint8[]| Checksum salt (for the integrity hash)
| 1 | uint8 | Security flags
| 20 | uint8[]| PIN grid seed (4) + PIN salt (16), if flags & 0x01
| 12 | uint8[]| Matrix card data, if flags & 0x02 (not supported)
| 1 | uint8 | Authenticator required, if flags & 0x04
LOGON_PROOF (Client → Server)
Offset | Size | Type | Description
-------|------|--------|----------------------------------
0x00 | 1 | uint8 | Opcode (0x01)
0x01 | 32 | uint8[]| A (client public ephemeral)
0x21 | 20 | uint8[]| M1 (client proof)
0x35 | 20 | uint8[]| CRC / integrity hash (zeros if not computed)
0x49 | 1 | uint8 | Number of keys (0)
0x4A | 1 | uint8 | Security flags (as the challenge sent them)
0x4B | 16 | uint8[]| PIN client salt, if flags & 0x01
0x5B | 20 | uint8[]| PIN hash, if flags & 0x01
The integrity hash is SHA1(A | HMAC_SHA1(checksumSalt, client files)) (src/auth/integrity.cpp), computed when the client’s executable and companion files are found under WOWEE_INTEGRITY_DIR, Data/expansions/<expansion>/misc or Data/misc. When the challenge sets flag 0x04, an AUTHENTICATOR (0x04) packet carrying the token follows the proof. Below protocol 8, LogonProofPacket::buildLegacy sends the same fields with the flags byte always 0.
LOGON_PROOF Response (Server → Client)
Success:
Offset | Size | Type | Description
-------|------|--------|----------------------------------
0x00 | 1 | uint8 | Opcode (0x01)
0x01 | 1 | uint8 | Status (0 = success)
0x02 | 20 | uint8[]| M2 (server proof)
0x16 | 4 | uint32 | Account flags
0x1A | 4 | uint32 | Survey ID
0x1E | 2 | uint16 | Login flags
Only the status and M2 are read. Older builds send a shorter tail (see docs/packet-framing.md).
Technical Details
Byte Ordering
Critical: All big integers use little-endian byte order in the WoW protocol.
OpenSSL’s BIGNUM uses big-endian internally, so our BigNum class handles conversion:
// When creating from protocol bytes (little-endian)
BigNum value(bytes, true); // true = little-endian
// When converting to protocol bytes
std::vector<uint8_t> output = value.toArray(true, 32); // little-endian, 32 bytes min
Fixed Sizes (WoW 3.3.5a)
Value | Size (bytes) | Description
-------------|--------------|-------------------------------
a (private) | 19 | Client private ephemeral
A (public) | 32 | Client public ephemeral
B (public) | 32 | Server public ephemeral
g | 1 | Generator (0x07)
N | 32 | Prime modulus (256-bit)
s (salt) | 32 | Salt
x | 20 | Salted password hash
u | 20 | Scrambling parameter
S | 32 | Raw session key
K | 40 | Final session key (interleaved)
M1 | 20 | Client proof
M2 | 20 | Server proof
Session Key Interleaving
The session key K is created by:
- Taking raw S (32 bytes)
- Splitting into even/odd bytes (16 bytes each)
- Hashing each half with SHA1 (20 bytes each)
- Interleaving the results (40 bytes total)
S = [s0 s1 s2 s3 s4 s5 ... s31]
S1 = [s0 s2 s4 s6 ... s30] // even indices
S2 = [s1 s3 s5 s7 ... s31] // odd indices
S1_hash = SHA1(S1) // 20 bytes
S2_hash = SHA1(S2) // 20 bytes
K = [S1_hash[0], S2_hash[0], S1_hash[1], S2_hash[1], ...] // 40 bytes
Error Handling
The SRP implementation logs extensively:
[DEBUG] SRP instance created
[DEBUG] Initializing SRP with username: testuser
[DEBUG] SRP initialized
[DEBUG] Feeding SRP challenge data
[DEBUG] SRP challenge data loaded
[DEBUG] Computed x (salted password hash)
[DEBUG] Computing client ephemeral
[DEBUG] Generated valid client ephemeral after 1 attempts
[DEBUG] Computing session key
[DEBUG] Scrambler u calculated
[DEBUG] Session key S calculated
[DEBUG] Interleaved session key K created (40 bytes)
[DEBUG] Computing authentication proofs
[DEBUG] Client proof M1 calculated (20 bytes)
[DEBUG] Expected server proof M2 calculated (20 bytes)
[INFO ] SRP ready: A=<first 8 bytes>... M1=<first 8 bytes>... s_nat=32 A_nat=32 B_nat=32
Common errors:
- “SRP not initialized!” - Call
initialize()beforefeed() - “Failed to generate valid client ephemeral” - Rare, retry connection
- “Server proof verification FAILED!” - The client’s SRP inputs differ from the server’s, which points at a protocol mismatch; a wrong password is refused earlier, in the LOGON_PROOF status.
AuthHandlerreports it as “Server identity verification failed”
Testing
You can test the SRP implementation without a server. tests/test_srp.cpp does this with WoW’s real g and N (ctest -R srp):
void testSRP() {
SRP srp;
srp.initialize("TEST", "TEST");
// Create fake server challenge
std::vector<uint8_t> B(32, 0x42);
std::vector<uint8_t> g{0x07};
std::vector<uint8_t> N(32, 0xFF);
std::vector<uint8_t> salt(32, 0x11);
srp.feed(B, g, N, salt);
// Verify data is generated
assert(srp.getA().size() == 32);
assert(srp.getM1().size() == 20);
assert(srp.getSessionKey().size() == 40);
LOG_INFO("SRP test passed!");
}
Performance
On modern hardware:
initialize(): ~1 μsfeed()(full computation): under 1 ms- Most time spent in modular exponentiation
- N is only 256 bits, and OpenSSL’s BIGNUM is highly optimized
verifyServerProof(): ~1 μs
The expensive operation (session key computation) only happens once per login.
Security Notes
- Random Number Generation: Uses OpenSSL’s
RAND_bytes()for cryptographically secure randomness - Short-lived Plaintext: The password (or stored hash) is kept only until
feed()has computed the proofs, then zeroed and released byclearCredentials();AuthHandler::disconnect()scrubs its own copy and the session key - Forward Secrecy: Ephemeral keys (a, A) are generated per session
- Mutual Authentication: Both client and server prove knowledge of password
- Secure Channel: Session key K keys the world server’s header cipher after auth completes (RC4 on WotLK)
References
- SRP Protocol
- WoWDev Wiki - SRP
- Implementation:
src/auth/srp.cpp,include/auth/srp.hpp - OpenSSL BIGNUM: https://www.openssl.org/docs/man1.1.1/man3/BN_new.html
Implementation Status: ✅ Complete and tested
The SRP implementation is production-ready and fully compatible with WoW 3.3.5a authentication servers.