From dac09e92d112f9ab8af674d857299ac8898f8551 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 24 May 2026 22:25:16 +0200 Subject: [PATCH] fix(housekeeping): hash reset password with BCrypt, not SHA-256 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `HousekeepingResetUserPasswordEvent` was writing a SHA-256 hex digest into `users.password`, but the Nitro auth path (`SessionEndpoints` / `AccountChangeEndpoints` → `AuthHttpUtil.checkPassword`) only does `BCrypt.checkpw`. A SHA-256 hex string doesn't start with `$2…$`, so jbcrypt throws `IllegalArgumentException`, `checkPassword` returns false, and operators saw "credenziali invalide" on every account whose password had been reset from the in-client panel. Switch to `BCrypt.hashpw(plain, BCrypt.gensalt(10))` — same idiom already used by `SessionEndpoints.java:351` and `AccountChangeEndpoints.java:98`. Cost 10 (vs 12 there) is fine for a server-generated 12-char random password: gensalt(10) keeps the operator-facing reset snappy and the output is identical-shape (`$2a$…`) to what jbcrypt 0.4 already accepts. Side-effects: - drops the `MessageDigest` / `NoSuchAlgorithmException` / `StandardCharsets` imports and the local `sha256Hex` helper - repurposes the existing `housekeeping.error.hash_failed` key for `BCrypt.gensalt`'s only failure mode (invalid cost / log_rounds out of range) so the client error surface is unchanged - updates the file javadoc to stop telling future readers to "swap the MessageDigest constant" — Arcturus itself only verifies BCrypt Companion of duckietm/Nitro-V3#157 (`feat/housekeeping-panel`). The client/UI is untouched — packet 9200, the action-result reveal card, the copy button, and the plaintext flow through `message` are all unchanged. --- .../HousekeepingResetUserPasswordEvent.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java index 1f1a57ba..14142002 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java @@ -4,10 +4,8 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; +import org.mindrot.jbcrypt.BCrypt; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.sql.Connection; import java.sql.PreparedStatement; @@ -15,19 +13,18 @@ import java.sql.SQLException; /** * Reset a user's password to a fresh random 12-character alphanumeric - * string. Persists the SHA-256 hex of the new password into - * `users.password` (varchar(64) — sized to hold SHA-256 hex), clears - * `auth_ticket` so any active session can't be re-used to bypass the - * reset, and ships the PLAINTEXT new password back to the operator in - * the action-result `message` so they can communicate it out-of-band. - * - * If your CMS uses a hash other than SHA-256 (bcrypt / argon2 / SHA-1), - * swap the MessageDigest constant — the rest of the flow is hash-agnostic. + * string. Persists a BCrypt `$2a$` hash of the new password into + * `users.password` (matches what `AuthHttpUtil.checkPassword` / + * `SessionEndpoints` / `AccountChangeEndpoints` already write and read), + * clears `auth_ticket` so any active session can't be re-used to bypass + * the reset, and ships the PLAINTEXT new password back to the operator + * in the action-result `message` so they can communicate it out-of-band. */ public class HousekeepingResetUserPasswordEvent extends MessageHandler { private static final String ACTION_KEY = "user.reset_password"; private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; private static final int PASSWORD_LENGTH = 12; + private static final int BCRYPT_COST = 10; private static final SecureRandom RNG = new SecureRandom(); @@ -53,8 +50,8 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler { String hash; try { - hash = sha256Hex(plain); - } catch (NoSuchAlgorithmException e) { + hash = BCrypt.hashpw(plain, BCrypt.gensalt(BCRYPT_COST)); + } catch (IllegalArgumentException e) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.hash_failed")); return; } @@ -89,16 +86,4 @@ public class HousekeepingResetUserPasswordEvent extends MessageHandler { return sb.toString(); } - - private static String sha256Hex(String plain) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(plain.getBytes(StandardCharsets.UTF_8)); - StringBuilder hex = new StringBuilder(digest.length * 2); - - for (byte b : digest) { - hex.append(String.format("%02x", b)); - } - - return hex.toString(); - } }