#include "pw.h" #include #include #include #include #include // Using bcrypt for now, but alternatives are emerging // https://isopenbsdsecu.re/mitigations/password_hashing/ #define CRYPT_FLAVOR "bcrypt,a" struct pw_context { sqlite3 *conn; const char *dummy_hash; sqlite3_stmt *upsert_stmt; sqlite3_stmt *find_stmt; sqlite3_stmt *token_insert_stmt; }; pw_status pw_init_context(sqlite3 *db, pw_context **out) { int rc, fk_enabled; static char shared_dummy_hash[_PASSWORD_LEN]; pw_context *ctx = malloc(sizeof *ctx); if(!ctx) return PW_NO_MEM; *out = ctx; rc = sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 1, &fk_enabled ); if (rc != SQLITE_OK || !fk_enabled) return PW_SQLITE_ERROR; ctx->conn = db; if (shared_dummy_hash[0] == '\0') { rc = crypt_newhash( "bogus", CRYPT_FLAVOR, shared_dummy_hash, _PASSWORD_LEN); if (rc < 0) return PW_CRYPT_ERROR; } ctx->dummy_hash = shared_dummy_hash; rc = sqlite3_prepare_v2(db, "INSERT INTO login (user, pass) VALUES (?1, ?2) " "ON CONFLICT (user) DO UPDATE SET pass = ?2", -1, &ctx->upsert_stmt, NULL); if (rc != SQLITE_OK) return PW_SQLITE_ERROR; rc = sqlite3_prepare_v2(db, "SELECT pass FROM login WHERE user = ?", -1, &ctx->find_stmt, NULL); if (rc != SQLITE_OK) return PW_SQLITE_ERROR; rc = sqlite3_prepare_v2(db, "INSERT INTO token (login_id, hash) " "SELECT id, ?2 " "FROM login " "WHERE user = ?1", -1, &ctx->token_insert_stmt, NULL); if (rc != SQLITE_OK) return PW_SQLITE_ERROR; return PW_OK; } pw_status pw_set(pw_context *ctx, char *user, char *pass) { int rc; rc = sqlite3_bind_text(ctx->upsert_stmt, 1, user, -1, SQLITE_TRANSIENT); if (rc != SQLITE_OK) return PW_SQLITE_ERROR; char hash[_PASSWORD_LEN]; if (crypt_newhash(pass, CRYPT_FLAVOR, hash, sizeof hash) < 0) return PW_CRYPT_ERROR; rc = sqlite3_bind_text(ctx->upsert_stmt, 2, hash, -1, SQLITE_TRANSIENT); if (rc != SQLITE_OK) return PW_SQLITE_ERROR; rc = sqlite3_step(ctx->upsert_stmt); sqlite3_reset(ctx->upsert_stmt); return (rc == SQLITE_DONE && sqlite3_changes(ctx->conn) > 0) ? PW_OK : PW_SQLITE_ERROR; } pw_status pw_check(pw_context *ctx, char *user, char *pass) { int rc; rc = sqlite3_bind_text(ctx->find_stmt, 1, user, -1, SQLITE_TRANSIENT); if (rc != SQLITE_OK) return PW_SQLITE_ERROR; bool user_exists = sqlite3_step(ctx->find_stmt) == SQLITE_ROW; // Unconditionally run crypt_checkpass() so pw_check() takes a // similar amount of time to complete whether or not the user is // in the db const char *pass_hash = user_exists // bcrypt uses characters in portable range of char ? (const char *)sqlite3_column_text(ctx->find_stmt, 0) // using a custom dummy hash rather than NULL since my // measurements show that crypt_checkpass runs 66% faster // for NULL than for an actual hash : ctx->dummy_hash; rc = crypt_checkpass(pass, pass_hash); sqlite3_reset(ctx->find_stmt); // apps shouldn't expose PW_NO_USER vs PW_BAD_PASS externally, // but it is returned here for debugging purposes if (!user_exists) return PW_NO_USER; return (rc == 0) ? PW_OK : PW_BAD_PASS; } pw_status pw_token_create( pw_context *ctx, char *user, unsigned char **token, unsigned int *token_len ) { #define TOK_LEN 16 unsigned char *tok = malloc(TOK_LEN); if (!tok) return PW_NO_MEM; arc4random_buf(tok, TOK_LEN); *token = tok; *token_len = TOK_LEN; unsigned char hash[EVP_MD_size(EVP_sha256())]; unsigned int hash_len; if (EVP_Digest(tok, TOK_LEN, hash, &hash_len, EVP_sha256(), NULL) != 1) { free(tok); *token = NULL; return PW_CRYPT_ERROR; } int rc = sqlite3_bind_text(ctx->token_insert_stmt, 1, user, -1, SQLITE_TRANSIENT); rc = sqlite3_bind_blob(ctx->token_insert_stmt, 2, hash, hash_len, SQLITE_TRANSIENT); if (rc != SQLITE_OK) { token = NULL; free(tok); return PW_SQLITE_ERROR; } rc = sqlite3_step(ctx->token_insert_stmt); sqlite3_reset(ctx->token_insert_stmt); return (rc == SQLITE_DONE && sqlite3_changes(ctx->conn) > 0) ? PW_OK : PW_SQLITE_ERROR; }