// uses std crypt() to hash/salt and verify passwords enum { BCRYPT_SALT_LEN = 22, // bcrypt uses 22 base64 characters for salt BCRYPT_MIN_COST = 4, BCRYPT_MAX_COST = 31, BCRYPT_DEFAULT_COST = 10 }; static const char *base64_chars = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; static str generate_salt(arena *A, int cost) { unsigned char rand_bytes[BCRYPT_SALT_LEN]; FILE *urandom = fopen("/dev/urandom", "r"); if (!urandom) { perror("Failed to open /dev/urandom"); exit(1); } if (fread(rand_bytes, 1, BCRYPT_SALT_LEN, urandom) != BCRYPT_SALT_LEN) { fclose(urandom); fprintf(stderr, "Failed to read random bytes\n"); exit(1); } fclose(urandom); // Format: $2b$$<22 character salt> char salt_buf[BCRYPT_SALT_LEN + 10]; int pos = sprintf(salt_buf, "$2b$%02d$", cost); for (int i = 0; i < BCRYPT_SALT_LEN; i++) { salt_buf[pos++] = base64_chars[rand_bytes[i] & 0x3f]; } salt_buf[pos] = '\0'; return str_new_from_buf(A, salt_buf, pos); } static str create_password_hash(arena *A, str password, int cost) { if (cost < BCRYPT_MIN_COST) cost = BCRYPT_MIN_COST; if (cost > BCRYPT_MAX_COST) cost = BCRYPT_MAX_COST; str salt = generate_salt(A, cost); char *salt_cstr = str_to_cstr(A, salt); char *password_cstr = str_to_cstr(A, password); char *hash = crypt(password_cstr, salt_cstr); if (!hash) { return (str){0}; } return str_new_from_buf(A, hash, strlen(hash)); } static bool verify_password(str plain_password, str hashed_password) { uint8_t buf[4096]; arena tmp; arena_init(&tmp, buf, sizeof(buf)); char *plain_cstr = str_to_cstr(&tmp, plain_password); char *hash_cstr = str_to_cstr(&tmp, hashed_password); char *new_hash = crypt(plain_cstr, hash_cstr); if (!new_hash) { return false; } return strcmp(new_hash, hash_cstr) == 0; }