Migrating legacy password encryption to a modern scheme on your MU Online server
Understand the risks of legacy password schemes (raw MD5, unsalted SHA1) used on old MU Online servers and learn how to migrate to modern hashing (bcrypt/Argon2) without forcing every player to reset their password at once.
A significant number of MU Online private servers still run on databases inherited from old emulators, where account passwords are stored using raw MD5 or unsalted SHA1 — schemes that were acceptable 15-20 years ago, but that modern GPUs can now crack in seconds for common passwords. Migrating that
A significant number of MU Online private servers still run on databases inherited from old emulators, where account passwords are stored using raw MD5 or unsalted SHA1 — schemes that were acceptable 15-20 years ago, but that modern GPUs can now crack in seconds for common passwords. Migrating that scheme to modern hashing like bcrypt or Argon2 is one of the highest-impact security improvements an admin can make, but it needs to be done without forcing thousands of players to reset their password all at once. This tutorial covers the problem, the incremental migration strategy, and the sync points between GameServer and WebEngine.
Why legacy hashing is a real risk
MD5 and unsalted SHA1 have two core weaknesses: they're too fast (allowing billions of attempts per second on modern hardware) and, without salt, they allow precomputed rainbow tables — meaning an attacker with access to a database dump can recover common passwords almost instantly, without even needing brute force. If the same player reuses that password on other services (email, social media), a leak from your MU server becomes an entry point into accounts outside the game — a serious reputational risk for the admin.
Comparing hashing schemes
| Scheme | Computation speed | Resistant to GPU/ASIC | Recommended use today |
|---|---|---|---|
| Unsalted MD5 | Extremely fast | No | Never (legacy, must migrate) |
| Unsalted SHA1 | Very fast | No | Never (legacy, must migrate) |
| SHA256 with simple salt | Fast | Partially | Acceptable, but not ideal |
| bcrypt (cost 10-12) | Slow by design | Yes | Recommended |
| Argon2id | Slow and memory-resistant | Yes (the most robust) | Recommended when available |
The core difference is that bcrypt and Argon2 are deliberately slow and tunable (cost factor), making brute-force attacks impractical at scale, even with dedicated hardware.
Prerequisites before starting the migration
- A complete, tested backup of the accounts database (
MEMB_INFOor equivalent) before making any changes. - A staging environment with a copy of the production database to test the migration logic risk-free.
- Access to the GameServer's (or the authentication module's) and the WebEngine's source code.
- A bcrypt/Argon2 library available in the language used by the GameServer (C++/Delphi/C#) and the WebEngine (PHP/.NET).
- A scheduled maintenance window with low traffic for the final deploy.
Step 1 — Map every place that validates passwords
Before migrating, identify every location that compares login passwords: the GameServer itself (game client authentication), the WebEngine (panel login), and any auxiliary API (mobile app, Discord bot with linked login). If any of these points isn't updated, the player can end up with inconsistent access across them.
Step 2 — Add a hash version column
Add a column (password_hash_version or similar) to the accounts table, indicating which scheme is stored for that record:
ALTER TABLE MEMB_INFO ADD password_hash_version TINYINT NOT NULL DEFAULT 0;
-- 0 = legacy MD5/SHA1, 1 = bcrypt, 2 = Argon2id
This column is what lets the system know, on every login, which algorithm to use to validate that specific record — without it, you can't have two schemes safely coexisting.
Step 3 — Implement dual validation (legacy + modern)
In the authentication code (GameServer and WebEngine), the login logic now checks the hash version before validating:
function validateLogin($enteredPassword, $storedHash, $version) {
if ($version == 0) {
// Legacy scheme (example: unsalted MD5)
return md5($enteredPassword) === $storedHash;
} elseif ($version == 1) {
return password_verify($enteredPassword, $storedHash); // bcrypt
} elseif ($version == 2) {
return password_verify($enteredPassword, $storedHash); // Argon2id (same API in modern PHP)
}
return false;
}
Step 4 — Automatically recompute the hash on successful login
This is the core step of the incremental migration: whenever a player with password_hash_version = 0 logs in successfully, the system takes advantage of the fact that the plain-text password was just typed correctly and recomputes it with the new scheme, updating the column:
if ($version == 0 && validateLogin($enteredPassword, $storedHash, 0)) {
$newHash = password_hash($enteredPassword, PASSWORD_BCRYPT, ['cost' => 12]);
updatePasswordInDatabase($accountId, $newHash, 1); // version 1 = bcrypt
}
This way, the migration happens transparently, one player at a time, as they naturally log in — with no mass reset required.
Step 5 — Define a policy for inactive accounts
Accounts that haven't logged in for a long time will never go through the automatic recomputation, since it only happens at login time. Define an explicit policy:
| Strategy | Pros | Cons |
|---|---|---|
| Leave as is (legacy hash remains) | Simple, no extra effort | Old accounts stay vulnerable indefinitely |
| Force a password reset by email after X months of inactivity | Migrates all accounts eventually | Requires a working email system and may generate support tickets |
| Require a password reset on the next login for very old accounts | Migrates on reactivation | May frustrate players returning after years |
For most servers, combining "automatic migration on login" with "forced reset by email after a long period (e.g., 12 months) for inactive accounts" is the most reasonable balance.
Step 6 — Sync GameServer and WebEngine
If the GameServer and the WebEngine validate passwords independently (a common architecture in MU emulators), make sure both read the same version column and implement the same dual-validation logic. A common mistake is updating only the WebEngine and forgetting the GameServer (or vice versa), leaving the player authenticated on one side and rejected on the other.
Step 7 — Test thoroughly in staging
- Restore a copy of the production database into staging.
- Simulate logins for accounts with
version = 0and confirm they authenticate correctly and get migrated toversion = 1/2. - Simulate logins for already-migrated accounts and confirm bcrypt/Argon2 validation works without recomputing again.
- Test wrong passwords under both schemes, making sure the system rejects them correctly without leaking which scheme is in use (same generic error message).
- Measure login response time with bcrypt/Argon2 under load — the deliberately high cost shouldn't cause a noticeable timeout for the player.
Step 8 — Deploy to production with monitoring
Deploy during the scheduled maintenance window, with a backup taken right before. Monitor the login success rate and authentication response time during the first few hours — a spike in login failures right after the deploy signals something is wrong in the dual-validation logic.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| A player migrated on the WebEngine can't log into the game client | GameServer not synced with the new logic/column | Replicate the dual validation in the GameServer code |
| Login is extremely slow after the change | bcrypt/Argon2 cost set too high for the hardware | Lower the cost factor while keeping acceptable security (e.g., cost 10-12) |
| Old accounts never migrate | No policy for inactive accounts | Implement a forced reset by email after a defined period |
| Error message reveals the scheme in use | Different error messages per hash version | Standardize a single generic "invalid username or password" message |
| Silent failure when updating the hash on login | Error writing the new column not handled | Add error handling and logging in the hash recomputation step |
Password migration checklist
- Every password validation point mapped (GameServer, WebEngine, auxiliary APIs).
- Hash version column added to the accounts table.
- Dual validation (legacy + modern) implemented at every point.
- Automatic hash recomputation on successful login implemented and tested.
- Policy defined for inactive accounts that never log in.
- GameServer and WebEngine synced on the same logic and column.
- Thoroughly tested in staging, including error cases and performance.
- Deploy done in a maintenance window with backup and active monitoring.
With the password scheme modernized, the natural next security step is to review the other protection layers of your environment — like admin panel authentication and general database hardening — as part of the infrastructure strategy described in the MU Online server setup tutorial.
Frequently asked questions
Why do old MU servers use unsalted MD5 or SHA1?
Because these emulators were written more than 15 years ago, when MD5/SHA1 were still considered acceptable and hash-cracking hardware was much slower. Today, with modern GPUs, unsalted MD5 can be brute-forced or looked up via rainbow tables in seconds for common passwords.
Does migrating the password scheme break current players' accounts?
It doesn't have to. The right approach is an incremental migration: keep the old hash working for login, but on the player's next successful authentication, recompute the password with the new scheme (bcrypt/Argon2) and replace the stored hash, transparently.
Bcrypt or Argon2 — which should I choose for the WebEngine/MU server?
Both are suitable; bcrypt has broader support in the older PHP/.NET libraries used by MU panels, while Argon2 is more modern and the winner of the Password Hashing Competition. If your stack already supports Argon2 natively, prefer it; otherwise, bcrypt with an adequate cost factor is a solid, proven choice.
Do I need to change the scheme in the GameServer, the WebEngine, or both?
Both, if they validate passwords independently (common in setups where the game client authenticates directly against the GameServer and the WebEngine authenticates separately for the panel). If you don't sync both points, the player may end up with a valid password on one side and invalid on the other.
How do I avoid taking down the whole server during the migration?
Do the migration in a scheduled maintenance window with low traffic, migrate the read logic first (accepting both hash formats), test thoroughly in staging, and only then enable writing in the new format. Never make the full switch in production without this dual-transition period.