Доброго здравия, %username%!
В первой части я рассказал как можно минимальными усилиями защитить БД нашей платежной системы. Но, как заметил один из комментирующих, при компрометации web сервера появляется возможность подсмотреть все логины и пароли пользователей. Тут нам на помощь приходят One time passwords (OTP).
Под катом моя вольная интерпритация данного термина с использованием криптографии эллиптических кривых (ECC). Кстати говоря, платежные системы далеко не единственная сфера применения этой технологии.
Upd:
Ахтунг! При взломе веб сервера все таки есть вероятность подмены платежных реквизитов, так что все таки подписывать лучше не случайную строку (хоть это и защитит от полной компрометации системы, но не защитит от случаев, когда подменяются реквизиты прямо во время платежа), а хэш платежного документа, показывая юзеру при этом все реквизиты платежа в программе.
З.Ы. Генерировать ключ лучше тоже на стороне клиента
Чтобы не придумывать одноразовых карточек с паролями решено было применить ЭЦП. Как раз подвернулся случай использовать так любимые мной в силу своей надежности и скорости эллиптические кривые.
Схема была следующей:
Предстояло писать внешнюю хранимку для MySQL и сопутствующие программы. В качестве криптобиблиотеки был выбран старый добрый OpenSSL. В результате 2х суток без сна на свет появился рабочий вариант из:
Сразу предупреждаю: код не блещет красотой, C не мой основной ЯП.
Теперь по пунктам:
1) Генерируем ключ
2) Подписываем на клиенте этим ключом случайную строку, выданную сервером
3) Проверяем подпись
Таким образом, даже если хакер получит доступ к аккаунту юзера, он не сможет потратить его деньги\сменить пароль\мыло\любые другие функции которые вы посчитаете нужным защитить с использованием этого метода.
А если стырит ключ, то благодаря циклу хэширования пароля с солью 0x20000 (131072) раз запарится брутфорсить даже простые пароли.
В первой части я рассказал как можно минимальными усилиями защитить БД нашей платежной системы. Но, как заметил один из комментирующих, при компрометации web сервера появляется возможность подсмотреть все логины и пароли пользователей. Тут нам на помощь приходят One time passwords (OTP).
Под катом моя вольная интерпритация данного термина с использованием криптографии эллиптических кривых (ECC). Кстати говоря, платежные системы далеко не единственная сфера применения этой технологии.
Upd:
Ахтунг! При взломе веб сервера все таки есть вероятность подмены платежных реквизитов, так что все таки подписывать лучше не случайную строку (хоть это и защитит от полной компрометации системы, но не защитит от случаев, когда подменяются реквизиты прямо во время платежа), а хэш платежного документа, показывая юзеру при этом все реквизиты платежа в программе.
З.Ы. Генерировать ключ лучше тоже на стороне клиента
Чтобы не придумывать одноразовых карточек с паролями решено было применить ЭЦП. Как раз подвернулся случай использовать так любимые мной в силу своей надежности и скорости эллиптические кривые.
Схема была следующей:
- При регистрации юзера выдаем ему зашифрованный файлик с открытым\закрытым ключом и паролем от него. Себе оставляем только открытый ключ.
- Когда юзер хочет совершить платеж или перевод денег генерируем случайным образом строку, которую ему показываем. Сохраняем её в бд с привязкой к пользователю.
- Юзер копирует строку в специальную программу, которая по выданному паролю дешифрует закрытый ключ и подписывает нашу случайную строку.
- Юзер отдает нам подпись, мы её проверяем с помощью открытого ключа и даем добро на операцию.
Предстояло писать внешнюю хранимку для MySQL и сопутствующие программы. В качестве криптобиблиотеки был выбран старый добрый OpenSSL. В результате 2х суток без сна на свет появился рабочий вариант из:
- Програмки, генерирующей ключевую пару и помещающую её в БД (написал на Builder).
- Програмки для пользователя, генерирующей цифровую подпись (на нем же).
- Внешней хранимки, эту цифровую подпись проверяющей (пользователю нужен ответ мгновенно, програмки не катят). Написал на VC.
Сразу предупреждаю: код не блещет красотой, C не мой основной ЯП.
Теперь по пунктам:
1) Генерируем ключ
const KEYSIZE = SHA512_DIGEST_LENGTH+4; // 4 байта на «соль»
unsigned char *pubkey = (unsigned char *)OPENSSL_malloc(10000),
*privkey = (unsigned char *)OPENSSL_malloc(10000); //буферы для открытого и закрытого ключей
AnsiString pub, prv, userid, paypass; //строки для хранения открытого, закрытого ключей и пароля от них
unsigned char md[KEYSIZE]= {0}; // тут будет хеш от пароля
unsigned int i = ParamCount();
paypass = «rAnDom_PaSs»;
EC_GROUP *group = EC_GROUP_new_by_curve_name(NID_sect571r1); // выбираем эллиптическую кривую
EC_GROUP_set_point_conversion_form(group,POINT_CONVERSION_UNCOMPRESSED);
EC_KEY *x = EC_KEY_new();
EC_KEY_set_group(x,group);
BIO *out = BIO_new(BIO_s_mem()); // Писать будем в память
// Код для замедления брутфорса
env_md_ctx_st mdctx; //Контекст для хэша
EVP_MD_CTX_init(&mdctx);
EVP_DigestInit_ex(&mdctx,EVP_sha512(),NULL); //Алгоритм хэширования — SHA512
EVP_DigestUpdate(&mdctx,paypass.c_str(),strlen(paypass.c_str())); // Хэшируем пароль первый раз
for (i = 0; i < 0x20000; i++) {
memcpy(md,mdctx.md_data,SHA512_DIGEST_LENGTH); // копируем хэш в массив
md[64] = i ^ md[0] ^ md[7] ^ md[5] ^ md[23]; //вычисляем дополнительные 4 байта
md[65] = i ^ md[1] ^ md[9] ^ md[6] ^ md[53]; //на основе предыдущего хэша
md[66] = i ^ md[3] ^ md[25] ^ md[11] ^ md[48]; // они будут т.н. раундовой «солью»
md[67] = i ^ md[8] ^ md[18] ^ md[17] ^ md[2];
EVP_DigestUpdate(&mdctx,md,KEYSIZE); // и хэшируем предыдущий хэш+соль
}
EVP_DigestFinal(&mdctx,md,NULL); //заканчиваем считать хэш
EVP_MD_CTX_cleanup(&mdctx);
EC_KEY_generate_key(x); //генерируем ключевую пару
PEM_write_bio_ECPrivateKey(out, x, EVP_aes_256_cbc(), md, KEYSIZE, NULL, NULL); // пишем ключевую пару, зашифрованную по алгоритму AES-256 с ключом, посчитанным на предыдущем шаге
BIO_flush(out);
i = BIO_read(out,privkey,10000); // узнаем количество записанных байт
// для верности сожмем ZLIBом
zByte *compr;
uLong comprLenPrv = 1000*sizeof(int);
zByte *comprPrv = (zByte*)calloc((uInt)comprLenPrv, 1);
compress2(comprPrv, &comprLenPrv, (const Bytef*)privkey, i,Z_BEST_COMPRESSION);
OPENSSL_free(privkey);
BIO_free(out);
out = BIO_new(BIO_s_mem()); //еще один буфер для открытого ключа
PEM_write_bio_EC_PUBKEY(out,x); //пишем его в память в формате PEM
BIO_flush(out);
BIO_read(out,pubkey,10000); // читаем его в кусок памяти
// Волшебный код, чтобы скопировать закрытый зашифрованный ключ в массив байт
TByteDynArray cp;
cp.set_length(comprLenPrv);
for (i = 0; i < comprLenPrv; i++) {
cp[i] = comprPrv[i];
}
//копируем открытый ключ в строку
AnsiString pubk;
pubk.sprintf("%s",pubkey);
// освобождаем паметь
free(comprPrv);
OPENSSL_free(pubkey);
BIO_free(out);
EC_KEY_free(x);
2) Подписываем на клиенте этим ключом случайную строку, выданную сервером
….
unsigned long i;
unsigned char *x;
unsigned char buf [1024]={0};
//Подписываем строчку ключом. key — считанный и расшифрованный ключ из п.1. s — рендомная строчка.
ECDSA_SIG *sig = ECDSA_do_sign(s.c_str(),s.Length(),key);
x = buf;
i = i2d_ECDSA_SIG(sig,&x); // конвертим в двоичную форму
x = buf;
ECDSA_SIG_free(sig);
//чтобы подпись можно было скопировать — конвертим её в base64
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL); // без переводов строк
BIO *mem = BIO_new(BIO_s_mem());
mem = BIO_push(b64,mem);
BIO_write(mem,&buf,i);
BIO_flush(mem);
char *res;
BIO_get_mem_data(mem,&res);
mAnswer->Text = res; // выводим пользователю результат.
3) Проверяем подпись
int ssl_VerifySignature(const char *key, const char *str, const char *csig).
{
OpenSSL_add_all_algorithms();
BIO *bkey = BIO_new(BIO_s_mem());
BIO_write(bkey,key,(int)strlen(key));
BIO_flush(bkey);
EC_KEY *ec = PEM_read_bio_EC_PUBKEY(bkey,NULL,NULL,NULL); //читаем открытый ключ
BIO_free(bkey);
if (!ec) return -2;
unsigned long i;
unsigned char *x;
unsigned char buf [1024]={0};
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64,BIO_FLAGS_BASE64_NO_NL);
BIO *mem = BIO_new(BIO_s_mem());
BIO_write(mem,csig,(int)strlen(csig));
mem = BIO_push(b64,mem);
x = buf;
i = BIO_read(mem,x,1024);
x = buf;
ECDSA_SIG *sig = ECDSA_SIG_new();
sig = d2i_ECDSA_SIG(&sig,(const unsigned char **)&x,i); // читаем подпись
i = ECDSA_do_verify((const unsigned char *)str,(int)strlen(str),sig,ec); // проверяем подпись строки
BIO_free_all(mem);
ECDSA_SIG_free(sig);
return i;
}
Таким образом, даже если хакер получит доступ к аккаунту юзера, он не сможет потратить его деньги\сменить пароль\мыло\любые другие функции которые вы посчитаете нужным защитить с использованием этого метода.
А если стырит ключ, то благодаря циклу хэширования пароля с солью 0x20000 (131072) раз запарится брутфорсить даже простые пароли.