normalizeUpperOnlyLatin(trim($username)); $normalizedPassword = $this->normalizeUpperOnlyLatin($password); $normalizedEmail = $this->normalizeUpperOnlyLatin(trim($email)); [$saltHex, $verifierHex] = $this->makeRegistrationData($normalizedUsername, $normalizedPassword); $connection = DB::connection(config('moonwell.auth_connection')); return $connection->transaction(function () use ( $connection, $normalizedEmail, $normalizedUsername, $saltHex, $verifierHex, ): array { if ($this->accountExists($connection, 'username', $normalizedUsername)) { throw DuplicateGameAccountException::forUsername(); } if ( config('moonwell.registration.enforce_unique_email') && $normalizedEmail !== '' && $this->accountExists($connection, 'email', $normalizedEmail) ) { throw DuplicateGameAccountException::forEmail(); } try { $connection->insert( 'INSERT INTO account (username, salt, verifier, expansion, reg_mail, email, joindate) VALUES (?, UNHEX(?), UNHEX(?), ?, ?, ?, ?)', [ $normalizedUsername, $saltHex, $verifierHex, config('moonwell.registration.expansion'), $normalizedEmail, $normalizedEmail, now()->toDateTimeString(), ], ); } catch (QueryException $exception) { if ($this->isDuplicateKeyException($exception)) { throw DuplicateGameAccountException::forUsername(); } throw $exception; } $accountId = (int) $connection->getPdo()->lastInsertId(); $connection->insert( 'INSERT IGNORE INTO realmcharacters (realmid, acctid, numchars) SELECT id, ?, 0 FROM realmlist', [$accountId], ); $connection->statement( 'INSERT INTO account_access (id, gmlevel, RealmID, comment) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE gmlevel = VALUES(gmlevel), comment = VALUES(comment)', [ $accountId, config('moonwell.registration.gmlevel'), config('moonwell.registration.realm_id'), config('moonwell.registration.access_comment'), ], ); return [ 'id' => $accountId, 'username' => $normalizedUsername, ]; }, 3); } private function accountExists(ConnectionInterface $connection, string $column, string $value): bool { return $connection->table('account')->where($column, $value)->exists(); } /** * @return array{0: string, 1: string} */ private function makeRegistrationData(string $username, string $password): array { $salt = random_bytes(self::BYTE_LENGTH); $innerDigest = hash('sha1', sprintf('%s:%s', $username, $password), true); $xDigest = hash('sha1', $salt.$innerDigest, true); $modulus = BigInteger::fromBase(self::MODULUS_HEX, 16); $exponent = BigInteger::fromBytes(strrev($xDigest), false); $verifier = BigInteger::of(self::GENERATOR)->modPow($exponent, $modulus); $verifierBytes = strrev(str_pad($verifier->toBytes(false), self::BYTE_LENGTH, "\0", STR_PAD_LEFT)); return [ strtoupper(bin2hex($salt)), strtoupper(bin2hex($verifierBytes)), ]; } private function normalizeUpperOnlyLatin(string $value): string { return strtoupper($value); } private function isDuplicateKeyException(QueryException $exception): bool { return (string) $exception->getCode() === '23000'; } }