Дэвид Лебланк - 19 смертных грехов, угрожающих безопасности программ
Стандартный и надежный способ дает алгоритм PBKDF2 (password–based key derivation function, Version 2.0 – функция выработки ключа на основе пароля, версия 2.0). Он описан в документе Public Key Cryptography Standard # 5 (PRCS #5–стандарт криптографии с открытым ключом). Хотя первоначально эта функция разрабатывалась для создания криптографического ключа из пароля, но она хороша и для наших целей, поскольку является стандартной, открытой и удовлетворяет всем выдвинутым требованиям. Она односторонняя, но детерминированная. Можно задать размер результата, так что выбирайте валидаторы длиной не менее 128 битов (16 байтов).
У этой функции есть также особенности, затрудняющие атаку полным перебором. Во–первых, необходимо задать затравку, в качестве которой выбирается случайное значение. Это защищает от атак с предварительным вычислением. Затравка необходима для проверки пароля, поэтому можно хранить ее в составе результата, возвращаемого алгоритмом PBKDF2. Восьмибайтовой затравки достаточно, если она выбрана действительно случайно (см. грех 18).
Во–вторых, можно сделать так, что вычисление результата будет занимать относительно много времени. Идея в том, что законный пользователь введет пароль лишь один раз, так что вполне может подождать одну секунду, пока он будет проверяться. Но если противнику придется ждать одну секунду для каждого варианта, то офлайновая атака с перебором по словарю окажется весьма проблематичной. Управлять временем вычисления можно, задав счетчик итераций, определяющий, сколько раз повторять вызов основной функции. Возникает вопрос: «Так сколько итераций задавать?» Ответ зависит от того, сколько времени вы согласны ждать при использовании самой дешевой из имеющейся у вас аппаратуры. Если алгоритм реализован внутри недорого встраиваемого устройства, то 5000 итераций достаточно (в предположении, что он написан на С или машинном языке; желательно, чтобы алгоритм был реализован максимально эффективно, тогда вы сможете задать большее число итераций). Десять тысяч считается приемлемым значением счетчика в общем случае, если речь не идет о низкопроизводительном встраиваемом оборудовании или машинах 15–летней давности. На современных ПК (класса Pentium 4 и сравнимых с ним) можно говорить о 50–100 тысячах итераций. Чем число меньше, тем проще задача противника. Ведь он–то не ограничен встраиваемым устройством. Конечно, если вы зададите слишком много итераций, а приложение выполняет много операций аутентификации, то его производительность может пострадать. Поэтому начните с высокого значения и займитесь измерениями; не считайте при этом, что единственная причина медленной работы приложения – это большое число итераций.
Отметим попутно, что в API защиты данных (Data Protection API) (см. грех 12) в Windows используется алгоритм PBKDF2 с 4000 итераций, чтобы затруднить задачу противнику, пытающемуся взломать пароль. Ясно, что это значение маловато, если вы собираетесь поддерживать свое приложение на современных ОС, работающих на не слишком старом оборудовании (скажем, последних пяти лет выпуска).
В некоторых библиотеках имеется функция PBKDF2 (но, как правило, старая версия, которая не так хорошо спроектирована). Если же нет, то ее легко построить на базе любой реализации хэшированного кода аутентификации сообщений (НМАС – Hash–based Message Authentication Code). Вот, например, реализация на языке Python, где на вход подаются затравка и счетчик итераций, а полученное на выходе значение можно использовать в качестве валидатора пароля:
...import hmac, sha, struct
def PBKDF2(password, salt, ic=10000, outlen=16, digest=sha):
m = hmac.HMAC(key=password,digestmod=digest)
l = outlen / digest.digestsize
if outlen % digest.digestsize:
l = l + 1
T = ""
for i in range(0,1):
h = m.copy()
h.update(salt + struct.pack("!I", i+1))
state = h.digest()
for i in range(1, ic):
h = m.copy()
h.update(state)
next = h.digest()
r = ''
for i in range(len(state)):
r += chr(ord(state[i]) ^ ord(next[i]))
state = r
T += state
return T[:outlen];
Напомним, вы должны сами выбрать значение затравки и сохранить его в результате, который возвращает PBKDF2. Хорошую затравку можно получить от функции os.urandom(8), которая вернет восемь случайных байтов криптографического качества, полученных от операционной системы.
Предположим, что вы хотите проверить пароль, имея затравку и валидатор. Сделать это просто:
...def validate(typed_password, salt, validator):
if PBKDF2(typed_password, salt) == validator:
return True
else:
return False
Мы использовали стандартный алгоритм SHA1 и счетчик итераций 10000. Функция PBKDF2 легко переводится на любой язык. Вот, скажем, реализация на С с использованием алгоритма SHA1 из библиотеки OpenSSL:
...#include <openssl/evp.h>
#include <openssl/hmac.h>
#define HLEN (20) /* используем SHA-1 */
int pbkdf2(unsigned char *pw, unsigned int pwlen, char *salt,
unsigned long long saltlen, unsigned int ic,
unsigned char *dk, unsigned long long dklen) {
unsigned long l, r, i, j;
unsigned char txt[4], hash[HLEN*2], tmp[HLEN], *p = dk;
unsigned char *lhix, *hix, *swap;
short k;
int outlen;
if (dklen > (((unsigned long long)1)<<32)-1)*HLEN) {
abort();
}
l = dklen/HLEN;
r = dklen%HLEN;
for (i=1; i <= l; i++) {
sprintf(txt, "%04u", (unsigned int)i);
HMAC(EVP_sha1(), pw, pwlen, txt, 4, hash, &outlen);
lhix = hash;
hix = hash + HLEN;
for (k=0; k < HLEN; k++) {
tmp[k] = hash[k];
}
for (j=1; j < ic; j++) {
HMAC(EVP_sha1(),pw, pwlen, lhix, HLEN, hix, &outlen);
for (k=0; k < HLEN; k++) {
tmp[k] ^= hix[k];
}
swap = hix;
hix = lhix;
lhix = swap;
}
for (k=0; k < HLEN; k++) {
*p++ = tmp[k];
}
}
if (r) {
sprintf(txt, "%04u", (unsigned int)i);
HMAC(EVP_sha1(), pw, pwlen, txt, 4, hash, &outlen);
lhix = hash;
hix = hash + HLEN;
for (k=0; k < HLEN; k++) {
tmp[k] = hash[k];
}
for (j=1; j < ic; j++) {
HMAC(EVP_sha1(),pw, pwlen, lhix, HLEN, hix, &outlen);
for (k=0; k < HLEN; k++) {
tmp[k] ^= hix[k];
}
swap = hix;
hix = lhix;
lhix = swap;
}
for (k=0; k < r; k++) {
*p++ = tmp[k];
}
}
return 0;
}
А вот код, написанный на C#:
...static string GetPBKDF2(string pwd, byte[] salt, int iter) {
PasswordDerivedBytes p =
new PasswordDerivedBytes(pwd, salt, "SHA1", iter);
return p.GetBytes(20);
}
Рекомендации по выбору протокола
Если вы аутентифицируете пользователей в уже сложившейся среде, в которой применяется инфраструктура паролей на базе Kerberos, то этой инфраструктурой и следует пользоваться. В особенности это относится к случаю, когда нужно аутентифицировать пользователя в домене Windows.
Если такой подход не имеет смысла, то ответ в большой степени зависит от конкретного приложения. В идеальном мире нужно было бы применить сильный протокол проверки паролей (то есть протокол с нулевым знанием), например Secure Remote Password (SRP, см. http://srp.stanford.edu), но лишь немногие сейчас пользуются такими протоколами, поскольку возможны осложнения с правами на интеллектуальную собственность.
Любое решение, кроме сильного протокола, сопряжено с риском. Мы рекомендуем остановиться на том, которое оправдано в конкретной ситуации, даже если это что–то вроде «ввести пароль, вычислить его свертку и послать свертку серверу» (это все же чуть лучше, чем послать пароль серверу, который вычислит свертку сам). Но любой протокол можно использовать лишь по защищенному соединению (с помощью SSL/TLS или аналогичной технологии), после того как клиент успешно аутентифицирует сервера, следуя рекомендациям из греха 10.
Если по какой–то причине вы не можете воспользоваться протоколом типа SSL/TLS и не хотите рисковать, применяя сильный протокол проверки паролей, то настоятельно советуем обратиться к профессиональному криптографу. В противном случае риск нарваться на неприятности очень высок!
Рекомендации по переустановке паролей
Блокировать пользователя за слишком большое число неудачных попыток ввести пароль – значит напрашиваться на DoS–атаку. Типичный пользователь в конце концов решит, что забыл пароль, и обратится к имеющейся процедуре переустановки пароля.
Вместо этого введите какое–нибудь разумное ограничение, скажем, 50 попыток входа в час, и обнаруживайте атаки на основе предположения, что законный пользователь вряд ли станет входить в систему так часто.
Альтернативное решение, дающее тот же эффект, – замедлять процедуру аутентификации после нескольких неудачных попыток. Например, обнаружив три неудачные попытки входа за короткий промежуток времени, вы можете задержать отправку сообщений сервером, так чтобы на завершение протокола аутентификации уходило десять секунд (когда атака прекратится, можно восстановить нормальную обработку).
Но это эффективно лишь в том случае, когда вы ограничиваете число одновременных попыток входа. Разумно ввести ограничение «не больше одного входа за раз». На самом деле без такого ограничения нельзя доказать корректность даже самых сильных протоколов.
Мы рекомендуем сделать борьбу с атаками частью стандартных рабочих процедур. Например, можно вести черный список IP–адресов на уровне сети. Кроме того, если зарегистрировано много попыток войти от имени некоторой учетной записи, то можно при следующем входе известить об этом ее владельца и предложить ему сменить пароль.
Если пользователь решит переустановить свой пароль, сделайте эту процедуру максимально усложненной или даже невозможной для человека. Разрешать это следует лишь тогда, когда пользователь сможет убедительно подтвердить свою личность, предъявив один или несколько предметов, которыми только он может обладать, например паспорт или копию водительских прав.