Website: Password Hashing
Step-by-step guide for web developers - make your website register, log in, change and reset passwords with AXIOM's PBKDF2-SHA256 password hashes, with full Node.js and PHP code and test vectors.
AXIOM no longer stores passwords as plain text. MEMB_INFO.memb__pwd holds a PBKDF2-SHA256 hash. Any website, CMS, launcher or admin tool that creates accounts or checks passwords must use the same hash, or players get "wrong password" in game.
This guide shows how to change your website. It covers the algorithm, the four account pages (register, login, change password, reset password), and how to test your code.
For the server side (JoinServer settings, migration of old databases) see Password Hashing.
What changed
What is stored in memb__pwd
The password (secret12)
A hash record ($pbkdf2-sha256$i=15000$...)
Column type
varchar(10)
varchar(120)
How login checks the password
SQL: WHERE memb__pwd = @password
Read the record, check it in code
Register writes
The password
hashPassword(password)
Before you start
The column is widened. Run the migration from Password Hashing. After it,
memb__pwdisvarchar(120).JoinServer is in PBKDF2 mode. In
JoinServer.ini:[JoinServerInfo] PasswordHashMode = 2 LegacyPasswordMode = 0 PasswordHashIterations = 15000Note the iteration count. Your website must use the same
PasswordHashIterationsvalue (15000 by default).
The algorithm
PBKDF2-HMAC-SHA256 is in the standard library of every web language. You do not need a DLL, an extension, or a WebZen MD5 port.
Making a hash
Make a salt: 16 random bytes from a secure random source (
crypto.randomBytes,random_bytes,os.urandom). NeverMath.randomorrand(). New salt for every hash, including password changes.Run PBKDF2 with:
InputValueHash function
SHA-256
Password
The password bytes exactly as typed (see Password rules)
Salt
The 16 bytes from step 1
Iterations
PasswordHashIterations(15000)Output length
32 bytes
Base64 the salt and the 32-byte output. Standard base64 (
+and/), with=padding.Join them into the record:
Example (93 characters):
Store the record in
memb__pwd.
Checking a password
Read
memb__pwdfor the account.If it does not start with
$pbkdf2-sha256$i=, it is an old row. See Old accounts.Split the rest on
$into: iterations, base64 salt, base64 output.Run PBKDF2 again with the typed password, the decoded salt and the iterations from the record (not your config value).
Compare the new 32 bytes with the decoded output using a constant-time compare (
timingSafeEqual,hash_equals). Equal = correct password.
Password rules
These come from the game client and JoinServer. Enforce them on the register and change-password forms.
Max 10 characters
The game client sends at most 10 characters. A 12-character website password can never log in to the game
Printable ASCII only (letters, digits, normal symbols)
The game client sends its own character bytes. Your website hashes UTF-8. They only match for plain ASCII
No trimming, no lower-casing
Passwords are case-sensitive. Hash exactly what the user typed
Username max 10 characters
memb___id is varchar(10)
Implementation
The same two helper functions serve every page: hashPassword and verifyPassword.
Built-in crypto only.
hash_pbkdf2 is in PHP core.
Python and C# versions of the same two functions are on the Password Hashing page.
Register
Validate the password, hash it, insert the record.
Keep the other columns your current register page already fills. Only memb__pwd changes.
Login
Read the record by username only, verify in code, and re-hash when asked.
Change password
Check the current password with
verifyPassword, exactly like login.Validate the new one with
isValidGamePassword.Write
hashPassword(newPassword):
Bind @pwd as varchar(120).
Reset password (e-mail link, admin panel)
There is no old password to check. Validate the new one and write hashPassword(newPassword) with the same UPDATE.
You cannot e-mail the user their old password any more - it is not stored. Replace any "send my password" feature with a reset link.
Other places that touch passwords
Search your website code for memb__pwd. Fix every hit:
WHERE ... memb__pwd = @pwd
Read the row, verifyPassword in code
INSERT / UPDATE with the plain password
hashPassword(password)
sql.VarChar(10) or varchar(10) for the password parameter
varchar(120)
[dbo].[fn_md5], HASHBYTES('MD5', ...)
hashPassword / verifyPassword
Showing or e-mailing the password
Remove. Use a reset link
Stored procedures that compare passwords
Move the check into website code
Old accounts
After the migration, some rows still hold the old plain-text password until the player logs in once.
In game: JoinServer checks the old value once, then rewrites it as a hash.
On the website: the code above does the same.
verifyPasswordaccepts the plain-text row and returnsrehash: true, and the login page rewrites it.
Both sides upgrade accounts, so the database moves to hashes on its own.
When the server owner sets LegacyPasswordMode = -1 (old rows rejected), remove the plain-text branch from verifyPassword too, so the website and the game agree.
Test your code
Run your PBKDF2 call with these fixed inputs. The output must match exactly.
password
salt (ASCII)
1
120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b
password
salt (ASCII)
2
ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43
password
salt (ASCII)
4096
c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a
JoinServer runs the same three checks at startup and refuses to start if any fails, so these values are what the server produces.
Full record test: verifyPassword("Test1234", record) must return valid for this record, and invalid for Test1235:
(Salt = bytes 00 01 02 ... 0F.)
Last test - in game:
Register a new account on the website.
In SQL, check
memb__pwdstarts with$pbkdf2-sha256$i=15000$and is 93 characters long.Log in with the game client. It must work.
Change the password on the website, log in to the game with the new one.
Common Issues
Related pages
Password Hashing - server settings, database migration, Python and C# code
JoinServer.ini -
PasswordHashMode,LegacyPasswordMode,PasswordHashIterations,CaseSensitive
Last updated