620f136361
Теперь можно зарегистрировать аккаунт, поднять сервер и зайти в него. Пароль не ходит по сети и не хранится на сервере в обратимом виде. Клиент называет аккаунт, сервер отдаёт его соль и одноразовый вызов для этого соединения, клиент тянет из пароля ключ через PBKDF2-HMAC-SHA256 и подписывает им вызов вместе с именем. Пароль пересекает сокет ноль раз, а запись обрезать нельзя: сервер не знает исходного пароля. Отсюда важное следствие, которое сразу отозвало одну ошибку в протоколе: соль обязана прийти раньше, чем клиент что-то доказывает, поэтому аккаунт называется в Hello, а не в Auth. Иначе клиент получает соль не того аккаунта и не может вывести ключ. Учётная запись хранится как PBKDF2 соль, счётчик раундов и хеш. Раунды лежат рядом с записью, а не в исходниках: число, существующее только в коде, однажды становится причиной того, что аккаунт невозможно проверить. Имена уникальны независимо от регистра: Player и player — это один аккаунт. Иначе игрок получает двух персонажей и не понимает, какой его. Троттлинг двумя ведрами, по аккаунту и по адресу: одно не мешает подбирать одно имя, другое не мешает перебирать много имён с разных адресов. Ответ на несуществующий аккаунт выполняет ту же работу, что и на неверный пароль, и занимает то же время. Токены живут только в памяти. Записанный на диск токен пережил бы процесс, который его выдал, а украденный каталог сервера стал бы набором живых сессий. Сессии различаются номером, а не адресом. Ключ по адресу вытеснял всех, кто зашёл с одного IP, то есть всех игроков за одним NAT. Тик-цикл: 20 раз в секунду, с ограничением догоняющих тиков. Мир, который тикает по кадрам, тикает в зависимости от видеокарты — ровно то, от чего мы уходим, перенося симуляцию на свой сервер. Регистрация — команда оператора, а не функция у клиента. Открытая саморегистрация превращает хранилище аккаунтов в спам-цель и в очередь поддержки. Проверено 12 сценариев, и главный из них — не юнит-тест, а настоящий сокет: регистрация, вход, доказательство пароля, токен, его погашение и появление игроком. Отдельно проверено, что неверный пароль отвергается, а старое доказательство не принимается против нового вызова. Тик-цикл замеряется числом тиков за секунду, а не наличием потока. Всё одним JDK, без Gradle и без сети, как и весь проект.
52 lines
1.5 KiB
Java
52 lines
1.5 KiB
Java
package net.rustnotmc.auth;
|
|
|
|
import java.security.SecureRandom;
|
|
|
|
/**
|
|
* The one source of unpredictable bytes in the project.
|
|
*
|
|
* {@link java.util.Random} is seeded from the clock, so anyone who can guess
|
|
* when a process started can predict every value it will ever produce. That is
|
|
* fine for a shuffle and fatal for a session token, a salt and a challenge.
|
|
*/
|
|
public final class Secure {
|
|
|
|
private static final SecureRandom RANDOM = new SecureRandom();
|
|
|
|
private Secure() {
|
|
}
|
|
|
|
public static byte[] bytes(int count) {
|
|
if (count < 0) {
|
|
throw new IllegalArgumentException("negative length: " + count);
|
|
}
|
|
byte[] value = new byte[count];
|
|
RANDOM.nextBytes(value);
|
|
return value;
|
|
}
|
|
|
|
public static int nextInt(int bound) {
|
|
if (bound <= 0) {
|
|
throw new IllegalArgumentException("bound must be positive: " + bound);
|
|
}
|
|
return RANDOM.nextInt(bound);
|
|
}
|
|
|
|
/**
|
|
* Compares two byte arrays without a shortcut for the first mismatch.
|
|
*
|
|
* An array comparison that returns as soon as it sees a difference reports
|
|
* how far it got, and an attacker can use that to learn a token one byte at
|
|
* a time by measuring how long the answer took.
|
|
*/
|
|
public static boolean same(byte[] a, byte[] b) {
|
|
if (a == null || b == null || a.length != b.length) {
|
|
return false;
|
|
}
|
|
int diff = 0;
|
|
for (int i = 0; i < a.length; i++) {
|
|
diff |= a[i] ^ b[i];
|
|
}
|
|
return diff == 0;
|
|
}
|
|
} |