Вместо написания функций, которые запрашивают и проверяют, выполните проверку в отдельной функции.
Например, допустим, вы используете
#define MAX_NAME_LEN 31
#define MAX_PASS_LEN 31
typedef struct {
char name[MAX_NAME_LEN + 1];
char pass[MAX_PASS_LEN + 1];
} userinfo;
для хранения информации об известных пользователях.Ваша база данных пользователей может тогда быть
#define MAX_USERS 100
static userinfo known_user[MAX_USERS];
static size_t known_users = 0;
. Чтобы зарегистрировать нового пользователя, вы пишете функцию, которая принимает имя пользователя и пароль в качестве параметров:
int register_user(const char *user, const char *pass)
{
const size_t userlen = (user) ? strlen(user) : 0;
const size_t passlen = (pass) ? strlen(pass) : 0;
size_t i;
if (userlen < 1)
return -1; /* Empty username */
if (userlen > MAX_NAME_LEN)
return -2; /* Username is too long */
if (passlen < 1)
return -3; /* Empty password */
if (passlen > MAX_PASS_LEN)
return -4; /* Password is too long */
/* Check if this user is already known. */
for (i = 0; i < known_users; i++)
if (!strcmp(user, known_user[i].name))
return -5; /* Username is already registered. */
if (known_users >= MAX_USERS)
return -6; /* User database is full. */
/* Add this user to the user database. */
strncpy(known_user[known_users].name, user, MAX_NAME_LEN + 1);
strncpy(known_user[known_users].pass, pass, MAX_PASS_LEN + 1);
known_users++;
return 0; /* Success */
}
Обратите внимание, что (user) ? strlen(user) : 0
являетсятроичное выражение.Это означает, что если user
не равно NULL, выражение оценивается как strlen(user)
, в противном случае оно оценивается как 0. (Да, это похоже на компактную форму предложения if
.) Обычно можно увидеть strlen()
обернута так, потому что strlen(NULL)
небезопасен и может привести к сбою программы: это страшно Неопределенное поведение , в соответствии со стандартом C.(Говорят, что UB может вызвать появление демонов у вас из носа, но я думаю, это просто страшная история от серых бород, заставляющая нас воспринимать UB всерьез.)
(!strcmp(...))
эквивалентно (strcmp(...) == 0)
и истинно, если две строки точно совпадают.
Аналогично, чтобы проверить, совпадает ли пара имя пользователя и пароль с известным пользователем,
int verify_user(const char *user, const char *pass)
{
const size_t userlen = (user) ? strlen(user) : 0;
const size_t passlen = (pass) ? strlen(pass) : 0;
size_t i;
if (userlen < 1)
return -1; /* Empty username */
if (userlen > MAX_NAME_LEN)
return -2; /* Username is too long */
if (passlen < 1)
return -3; /* Empty password */
if (passlen > MAX_PASS_LEN)
return -4; /* Password is too long */
for (i = 0; i < known_users; i++)
if (!strcmp(user, known_user[i].name)) {
if (!strcmp(pass, known_user[i].pass))
return 0; /* Valid user */
else
return -6; /* Wrong password */
}
return -7; /* No such user. */
}
На этом этапе вы можете написать небольшую тестовую программу и проверить, работают ли вышеуказанные функции.Это называется модульное тестирование :
int main(void)
{
int result;
result = register_user("foo", "bar");
if (result) {
fprintf(stderr, "register_user(\"foo\", \"bar\") failed: %d.\n", result);
exit(EXIT_FAILURE);
} else
fprintf(stderr, "register_user(\"foo\", \"bar\") worked as expected (0).\n");
result = register_user("user", "pass");
if (result) {
fprintf(stderr, "register_user(\"user\", \"pass\") failed: %d.\n", result);
exit(EXIT_FAILURE);
} else
fprintf(stderr, "register_user(\"user\", \"pass\") worked as expected (0).\n");
result = register_user("foo", "irrelevant");
if (result) {
fprintf(stderr, "register_user(\"foo\", \"irrelevant\") failed. Expected -5, received %d.\n", result);
exit(EXIT_FAILURE);
} else
fprintf(stderr, "register_user(\"foo\", \"irrelevant\") worked as expected (-5).\n");
result = verify_user("no such", "user or password");
if (result != -7) {
fprintf(stderr, "verify_user(\"no such\", \"user or password\") failed. Expected -7, received %d.\n", result);
exit(EXIT_FAILURE);
} else
fprintf(stderr, "verify_user(\"no such\", \"user or password\") worked as expected (-7).\n");
result = verify_user("foo", "wrong password");
if (result != -6) {
fprintf(stderr, "verify_user(\"foo\", \"wrong password\") failed. Expected -6, received %d.\n", result);
exit(EXIT_FAILURE);
} else
fprintf(stderr, "verify_user(\"foo\", \"wrong password\") worked as expected (-6).\n");
result = verify_user("foo", "bar");
if (result != 0) {
fprintf(stderr, "verify_user(\"foo\", \"bar\") failed. Expected 0, received %d.\n", result);
exit(EXIT_FAILURE);
} else
fprintf(stderr, "verify_user(\"foo\", \"bar\") worked as expected (0).\n");
printf("All tests successful.\n");
return EXIT_SUCCESS;
}
Когда вы научитесь лучше программировать, вы можете «автоматизировать» такое тестирование различными способами.
Только когда ваша тестовая программаработает корректно для тестов (и проверяет правильные и неправильные входные данные и их результаты!), продолжая решение исходной задачи.
Таким образом, вы знаете, что эти две функции работают, и вы знаете, что любые проблемыили ошибки должны быть в другом месте.(Если вы не уверены, вы можете добавить эти данные в программу модульного тестирования и проверить!)
Не рекомендуется хранить пароли в виде простого текста.Когда-либо.Вам также не нужно этого делать.
Что вам нужно сделать, это использовать хеш-функцию для преобразования пароля в большое число;хеш-функция должна быть криптографически безопасной .Пароль также должен быть salted : случайное число или строка, которые сохраняются в виде обычного текста, которые добавляются (или добавляются, или оба) к паролю, и комбинация передается в хэш-функцию.Чтобы замедлить любую атаку по словарю, хеш-функция обычно применяется большое количество раз;это число может варьироваться, но фиксированное число раундов, как правило, столь же безопасно.
В этом случае пользовательская информация выглядит примерно так:
#define MAX_NAME_LEN 31
#define HASH_SIZE 64 /* for 512-bit hashes like SHA512 */
#define SALT_SIZE 16 /* for 128-bit salt */
#define HASH_ROUNDS 5000
typedef struct {
char name[MAX_NAME_LEN + 1];
unsigned char salt[SALT_SIZE];
unsigned char hash[HASH_SIZE];
} userinfo;
Скажем, криптографически безопасная хеш-функция, которую мыМожно использовать, объявлено как
void crypto_hash(unsigned char *hash, const void *data, const size_t size);
, и у нас есть криптографически безопасный генератор случайных чисел, который генерирует случайную соль, когда нам нужно, используя
void crypto_salt(unsigned char *salt);
, тогда наша функция регистрации пользователя становится
int register_user(const char *user, const char *pass)
{
const size_t userlen = (user) ? strlen(user) : 0;
const size_t passlen = (pass) ? strlen(pass) : 0;
unsigned char *temp;
unsigned char hash[2][HASH_SIZE];
size_t i;
if (userlen < 1)
return -1; /* Empty username */
if (userlen > MAX_NAME_LEN)
return -2; /* Username is too long */
if (passlen < 1)
return -3; /* Empty password */
/* TODO: Password strength checking! */
/* Check if this user is already known. */
for (i = 0; i < known_users; i++)
if (!strcmp(user, known_user[i].name))
return -5; /* Username is already registered. */
if (known_users >= MAX_USERS)
return -6; /* User database is full. */
/* Generate a random salt for this new user. */
crypto_salt(known_user[i].salt);
/* Allocate a temporary buffer for the salt and the password.
Here, for simplicity, we just prepend the salt to the password,
and include the string-terminating nul byte, '\0'.
This is to protect against a certain type of tail attacks. */
temp = malloc(SALT_SIZE + pass_len + 1);
if (!temp)
return -8; /* Not enough memory. */
/* Combine the salt and the password, including '\0' at end. */
memcpy(temp, known_user[known_users].salt, SALT_SIZE);
memcpy(temp + SALT_SIZE, pass, passlen + 1);
/* First hashing round. */
crypto_hash(hash[0], temp, SALT_SIZE + passlen + 1);
/* The temporary buffer is no longer needed.
We wipe, then free it. */
memset(temp, 0, SALT_SIZE + passlen + 1);
free(temp);
/* Repeated rounds. */
for (i = 0; i < HASH_ROUNDS - 2; i++)
crypto_hash(hash[(i+1) & 1], hash[i & 1], HASH_SIZE);
/* Final round. Since i was incremented after the last
crypto_hash() call, the last hash is in hash[i & 1]. */
crypto_hash(known_user[known_users].hash, hash[i & 1], HASH_SIZE);
/* Wipe the temporary hash table. */
memset(hash[0], 0, HASH_SIZE);
memset(hash[1], 0, HASH_SIZE);
/* Add this user to the user database. */
strncpy(known_user[known_users].name, user, MAX_NAME_LEN + 1);
known_users++;
return 0; /* Success */
}
Используются два временных слота хэша, потому что обычно функции crypto_hash()
ожидают, что результат будет где-то еще, чем источник.
Проверка имени пользователя и пароля становится
int verify_user(const char *user, const char *pass)
{
const size_t userlen = (user) ? strlen(user) : 0;
const size_t passlen = (pass) ? strlen(pass) : 0;
unsigned char *temp;
unsigned char hash[2][HASH_SIZE];
size_t i, u;
int result;
if (userlen < 1)
return -1; /* Empty username */
if (userlen > MAX_NAME_LEN)
return -2; /* Username is too long */
if (passlen < 1)
return -3; /* Empty password */
for (u = 0; i < known_users; i++)
if (!strcmp(user, known_user[u].name))
break; /* u will be less than known_users */
if (u >= known_users)
return -7; /* No such user */
/* known_user[u] matches the name. */
/* Allocate a temporary area for combining this users' salt
and the supplied password. */
temp = malloc(HASH_SIZE + passlen + 1);
if (!temp)
return -8; /* Out of memory. */
/* Combine the salt and the password the same way as when
the user and password was registered. */
memcpy(temp, known_user[u].salt, SALT_SIZE);
memcpy(temp + SALT_SIZE, pass, passlen + 1);
/* Calculate the hash the same way. First pass: */
crypto_hash(hash[0], temp, SALT_SIZE + passlen + 1);
/* Temporary buffer is no longer needed, so clear and free it. */
memset(temp, 0, SALT_SIZE + passlen + 1);
free(temp);
/* Repeated rounds, including the final round. */
for (i = 0; i < HASH_ROUNDS - 1; i++)
crypto_hash(hash[(i+1) & 1], hash[i & 1], HASH_SIZE);
/* Because i was incremented after the last crypto_hash() call,
hash[i & 1] is where the final hash is. Compare. */
if (!memcmp(hash[i & 1], known_user[u].hash))
result = 0; /* Valid password for the user! */
else
result = -6; /* Wrong password. */
/* Wipe the temporary hash buffer. */
memset(hash[0], 0, HASH_SIZE);
memset(hash[1], 0, HASH_SIZE);
return result;
}
В практическом приложении лучше всего , а не различать «неизвестный пользователь» и «неправильный пароль» , потому что зная, какие имена пользователей действительны, может облегчить некоторые атаки.