fix(housekeeping): hash reset password with BCrypt, not SHA-256

`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.
This commit is contained in:
simoleo89
2026-05-24 22:25:16 +02:00
parent fbf979419e
commit dac09e92d1
@@ -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();
}
}