Brazil's biggest MU Online portal — since 2003
Tutorial Advanced Infrastructure

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.

RO Rodrigo · Updated on May 12, 2014 · ⏱ 17 min read
Quick answer

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

SchemeComputation speedResistant to GPU/ASICRecommended use today
Unsalted MD5Extremely fastNoNever (legacy, must migrate)
Unsalted SHA1Very fastNoNever (legacy, must migrate)
SHA256 with simple saltFastPartiallyAcceptable, but not ideal
bcrypt (cost 10-12)Slow by designYesRecommended
Argon2idSlow and memory-resistantYes (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_INFO or 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:

StrategyProsCons
Leave as is (legacy hash remains)Simple, no extra effortOld accounts stay vulnerable indefinitely
Force a password reset by email after X months of inactivityMigrates all accounts eventuallyRequires a working email system and may generate support tickets
Require a password reset on the next login for very old accountsMigrates on reactivationMay 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

  1. Restore a copy of the production database into staging.
  2. Simulate logins for accounts with version = 0 and confirm they authenticate correctly and get migrated to version = 1/2.
  3. Simulate logins for already-migrated accounts and confirm bcrypt/Argon2 validation works without recomputing again.
  4. Test wrong passwords under both schemes, making sure the system rejects them correctly without leaking which scheme is in use (same generic error message).
  5. 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

SymptomLikely causeFix
A player migrated on the WebEngine can't log into the game clientGameServer not synced with the new logic/columnReplicate the dual validation in the GameServer code
Login is extremely slow after the changebcrypt/Argon2 cost set too high for the hardwareLower the cost factor while keeping acceptable security (e.g., cost 10-12)
Old accounts never migrateNo policy for inactive accountsImplement a forced reset by email after a defined period
Error message reveals the scheme in useDifferent error messages per hash versionStandardize a single generic "invalid username or password" message
Silent failure when updating the hash on loginError writing the new column not handledAdd 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.

RO
Founder & editor-in-chief

Rodrigo has run ViciadosMU since the portal's early days. A specialist in MU Online server creation and administration, game history and the evolution of the seasons — he wrote much of the archive before 2024.

Keep reading

Related articles