diff --git a/src/libs/msgapi/auth-backends/CMakeLists.txt b/src/libs/msgapi/auth-backends/CMakeLists.txt index e645202..1ca6535 100644 --- a/src/libs/msgapi/auth-backends/CMakeLists.txt +++ b/src/libs/msgapi/auth-backends/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(authsqlite3 MODULE + auth-security-store.c sqlite.c password.c) @@ -16,4 +17,14 @@ if(BUILD_TESTING) bongoxpl LibGcrypt::LibGcrypt) add_test(NAME auth-password COMMAND auth-password-test) + + add_executable(auth-security-store-test + auth-security-store-test.c + auth-security-store.c) + target_link_libraries(auth-security-store-test PRIVATE + bongoauthsecurity + LibGcrypt::LibGcrypt + OATH::OATH + SQLite3::SQLite3) + add_test(NAME auth-security-store COMMAND auth-security-store-test) endif() diff --git a/src/libs/msgapi/auth-backends/auth-security-store-test.c b/src/libs/msgapi/auth-backends/auth-security-store-test.c new file mode 100644 index 0000000..5d83edc --- /dev/null +++ b/src/libs/msgapi/auth-backends/auth-security-store-test.c @@ -0,0 +1,173 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#include "auth-security-store.h" + +static int +PrepareDatabase(const char *path) +{ + sqlite3 *database = NULL; + int result = -1; + + if (sqlite3_open(path, &database) == SQLITE_OK && + sqlite3_exec(database, + "CREATE TABLE users(username TEXT PRIMARY KEY,password TEXT);" + "INSERT INTO users(username) VALUES('admin');", + NULL, NULL, NULL) == SQLITE_OK) + result = 0; + if (database) + sqlite3_close(database); + return result; +} + +int +main(void) +{ + char directory[] = "/tmp/bongo-auth-store-XXXXXX"; + char database_path[256] = ""; + char key_path[256] = ""; + char secret[BONGO_AUTH_TOTP_SECRET_SIZE]; + char recovery[AUTH_RECOVERY_CODE_COUNT][BONGO_AUTH_CREDENTIAL_SIZE]; + char otp[7]; + char wrong_otp[7]; + char *decoded = NULL; + size_t decoded_size = 0; + time_t now = 1234567890; + AuthSecurityStore store; + struct stat status; + int store_open = 0; + int oath_open = 0; + int enabled; + int required; + int result = 1; + + memset(&store, 0, sizeof(store)); + if (!mkdtemp(directory) || + snprintf(database_path, sizeof(database_path), "%s/userdb.sqlite", + directory) >= (int)sizeof(database_path) || + snprintf(key_path, sizeof(key_path), "%s/auth.key", directory) >= + (int)sizeof(key_path)) + goto cleanup; + if (BongoAuthSecurityInit() != 0) + goto cleanup; + oath_open = 1; + if (PrepareDatabase(database_path) != 0 || + AuthSecurityStoreOpen(&store, database_path, key_path) != 0) + goto cleanup; + store_open = 1; + if (AuthSecurityGetPolicy(&store, "admin", &enabled, &required) != 0 || + enabled || required) + goto cleanup; + if (BongoAuthGenerateTotpSecret(secret, sizeof(secret)) != 0 || + oath_base32_decode(secret, strlen(secret), &decoded, &decoded_size) != + OATH_OK || + oath_totp_generate(decoded, decoded_size, now, 30, 0, 6, otp) != OATH_OK) + goto cleanup; + memcpy(wrong_otp, otp, sizeof(wrong_otp)); + wrong_otp[0] = wrong_otp[0] == '9' ? '0' : (char)(wrong_otp[0] + 1); + if (AuthSecurityEnableTotp(&store, "admin", secret, wrong_otp, now, + recovery) == 0 || + AuthSecurityGetPolicy(&store, "admin", &enabled, &required) != 0 || + enabled) + goto cleanup; + if (AuthSecurityEnableTotp(&store, "admin", secret, otp, now, recovery) != 0 || + recovery[0][0] == '\0' || strcmp(recovery[0], recovery[1]) == 0 || + AuthSecurityGetPolicy(&store, "admin", &enabled, &required) != 0 || + !enabled || required) + goto cleanup; + if (stat(key_path, &status) != 0 || + (status.st_mode & (S_IRWXG | S_IRWXO)) != 0) + goto cleanup; + { + sqlite3_stmt *statement = NULL; + const void *encrypted; + int encrypted_size; + const char *hash; + + if (sqlite3_prepare_v2(store.database, + "SELECT s.totp_secret,r.code_hash FROM auth_security s " + "JOIN auth_recovery_codes r ON r.username=s.username " + "WHERE s.username='admin' LIMIT 1", + -1, &statement, NULL) != SQLITE_OK || + sqlite3_step(statement) != SQLITE_ROW) { + sqlite3_finalize(statement); + goto cleanup; + } + encrypted = sqlite3_column_blob(statement, 0); + encrypted_size = sqlite3_column_bytes(statement, 0); + hash = (const char *)sqlite3_column_text(statement, 1); + if (!encrypted || encrypted_size <= (int)strlen(secret) || + (encrypted_size == (int)strlen(secret) && + memcmp(encrypted, secret, strlen(secret)) == 0) || + !hash || strncmp(hash, "$argon2id$", 10) != 0) { + sqlite3_finalize(statement); + goto cleanup; + } + sqlite3_finalize(statement); + } + if (AuthSecurityVerifySecondFactor(&store, "admin", otp, now) != + AUTH_SECOND_FACTOR_VALID || + AuthSecurityVerifySecondFactor(&store, "admin", wrong_otp, now) != + AUTH_SECOND_FACTOR_INVALID || + AuthSecurityVerifySecondFactor(&store, "admin", recovery[0], now) != + AUTH_SECOND_FACTOR_VALID || + AuthSecurityVerifySecondFactor(&store, "admin", recovery[0], now) != + AUTH_SECOND_FACTOR_INVALID) + goto cleanup; + if (AuthSecuritySetRequireAppPasswords(&store, "admin", 1) != 0 || + AuthSecurityGetPolicy(&store, "admin", &enabled, &required) != 0 || + !enabled || !required) + goto cleanup; + if (AuthSecurityDisableTotp(&store, "admin") != 0 || + AuthSecurityGetPolicy(&store, "admin", &enabled, &required) != 0 || + enabled || required || + AuthSecurityVerifySecondFactor(&store, "admin", otp, now) != + AUTH_SECOND_FACTOR_NOT_REQUIRED || + AuthSecuritySetRequireAppPasswords(&store, "admin", 1) == 0) + goto cleanup; + result = 0; + +cleanup: + free(decoded); + if (store_open) + AuthSecurityStoreClose(&store); + if (oath_open) + BongoAuthSecurityDone(); + unlink(key_path); + unlink(database_path); + { + char path[300]; + snprintf(path, sizeof(path), "%s-wal", database_path); + unlink(path); + snprintf(path, sizeof(path), "%s-shm", database_path); + unlink(path); + } + rmdir(directory); + return result; +} diff --git a/src/libs/msgapi/auth-backends/auth-security-store.c b/src/libs/msgapi/auth-backends/auth-security-store.c new file mode 100644 index 0000000..3d96459 --- /dev/null +++ b/src/libs/msgapi/auth-backends/auth-security-store.c @@ -0,0 +1,576 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "auth-security-store.h" + +#define AUTH_KEY_SIZE 32U +#define AUTH_NONCE_SIZE 12U +#define AUTH_TAG_SIZE 16U +#define AUTH_SECRET_MAGIC "BTOTP1" +#define AUTH_SECRET_MAGIC_SIZE 6U + +static void +SecureClear(void *buffer, size_t length) +{ + volatile unsigned char *cursor = buffer; + + while (length-- > 0) + *cursor++ = 0; +} + +static int +ReadAll(int file, unsigned char *data, size_t length) +{ + while (length > 0) { + ssize_t count = read(file, data, length); + + if (count <= 0) + return -1; + data += count; + length -= (size_t)count; + } + return 0; +} + +static int +WriteAll(int file, const unsigned char *data, size_t length) +{ + while (length > 0) { + ssize_t count = write(file, data, length); + + if (count <= 0) + return -1; + data += count; + length -= (size_t)count; + } + return 0; +} + +static int +LoadKey(const char *path, unsigned char key[AUTH_KEY_SIZE]) +{ + int file; + int flags = O_RDONLY; + +#ifdef O_NOFOLLOW + flags |= O_NOFOLLOW; +#endif +retry: + file = open(path, flags); + if (file >= 0) { + struct stat status; + unsigned char extra; + int valid = fstat(file, &status) == 0 && S_ISREG(status.st_mode) && + (status.st_mode & (S_IRWXG | S_IRWXO)) == 0; + + if (valid) + valid = ReadAll(file, key, AUTH_KEY_SIZE) == 0; + if (valid) + valid = read(file, &extra, 1) == 0; + close(file); + if (!valid) + SecureClear(key, AUTH_KEY_SIZE); + return valid ? 0 : -1; + } + if (errno != ENOENT) + return -1; + gcry_randomize(key, AUTH_KEY_SIZE, GCRY_STRONG_RANDOM); + file = open(path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); + if (file < 0) { + if (errno == EEXIST) + goto retry; + SecureClear(key, AUTH_KEY_SIZE); + return -1; + } + { + int failed = WriteAll(file, key, AUTH_KEY_SIZE) != 0; + + if (!failed && fsync(file) != 0) + failed = 1; + if (close(file) != 0) + failed = 1; + if (!failed) + return 0; + unlink(path); + SecureClear(key, AUTH_KEY_SIZE); + return -1; + } +} + +static int +EncryptSecret(AuthSecurityStore *store, const char *username, + const char *secret, unsigned char **encrypted, + size_t *encrypted_size) +{ + unsigned char key[AUTH_KEY_SIZE]; + gcry_cipher_hd_t cipher = NULL; + unsigned char *output = NULL; + size_t secret_size; + size_t total; + int result = -1; + + if (!store || !username || !secret || !encrypted || !encrypted_size) + return -1; + secret_size = strlen(secret); + if (secret_size == 0 || secret_size >= BONGO_AUTH_TOTP_SECRET_SIZE) + return -1; + if (LoadKey(store->key_path, key) != 0) { + SecureClear(key, sizeof(key)); + return -1; + } + total = AUTH_SECRET_MAGIC_SIZE + AUTH_NONCE_SIZE + AUTH_TAG_SIZE + secret_size; + output = malloc(total); + if (!output) + goto done; + memcpy(output, AUTH_SECRET_MAGIC, AUTH_SECRET_MAGIC_SIZE); + gcry_randomize(output + AUTH_SECRET_MAGIC_SIZE, AUTH_NONCE_SIZE, + GCRY_STRONG_RANDOM); + if (gcry_cipher_open(&cipher, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM, 0) || + gcry_cipher_setkey(cipher, key, sizeof(key)) || + gcry_cipher_setiv(cipher, output + AUTH_SECRET_MAGIC_SIZE, + AUTH_NONCE_SIZE) || + gcry_cipher_authenticate(cipher, username, strlen(username)) || + gcry_cipher_encrypt(cipher, + output + AUTH_SECRET_MAGIC_SIZE + AUTH_NONCE_SIZE + + AUTH_TAG_SIZE, + secret_size, secret, secret_size) || + gcry_cipher_gettag(cipher, + output + AUTH_SECRET_MAGIC_SIZE + AUTH_NONCE_SIZE, + AUTH_TAG_SIZE)) + goto done; + *encrypted = output; + *encrypted_size = total; + output = NULL; + result = 0; + +done: + if (cipher) + gcry_cipher_close(cipher); + SecureClear(key, sizeof(key)); + free(output); + return result; +} + +static int +DecryptSecret(AuthSecurityStore *store, const char *username, + const void *encrypted, size_t encrypted_size, + char secret[BONGO_AUTH_TOTP_SECRET_SIZE]) +{ + const unsigned char *input = encrypted; + unsigned char key[AUTH_KEY_SIZE]; + gcry_cipher_hd_t cipher = NULL; + size_t plain_size; + int result = -1; + + secret[0] = '\0'; + if (!store || !username || !encrypted || + encrypted_size <= AUTH_SECRET_MAGIC_SIZE + AUTH_NONCE_SIZE + + AUTH_TAG_SIZE || + memcmp(input, AUTH_SECRET_MAGIC, AUTH_SECRET_MAGIC_SIZE) != 0) + return -1; + if (LoadKey(store->key_path, key) != 0) { + SecureClear(key, sizeof(key)); + return -1; + } + plain_size = encrypted_size - AUTH_SECRET_MAGIC_SIZE - AUTH_NONCE_SIZE - + AUTH_TAG_SIZE; + if (plain_size >= BONGO_AUTH_TOTP_SECRET_SIZE) + goto done; + if (gcry_cipher_open(&cipher, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM, 0) || + gcry_cipher_setkey(cipher, key, sizeof(key)) || + gcry_cipher_setiv(cipher, input + AUTH_SECRET_MAGIC_SIZE, + AUTH_NONCE_SIZE) || + gcry_cipher_authenticate(cipher, username, strlen(username)) || + gcry_cipher_decrypt(cipher, secret, plain_size, + input + AUTH_SECRET_MAGIC_SIZE + AUTH_NONCE_SIZE + + AUTH_TAG_SIZE, + plain_size) || + gcry_cipher_checktag(cipher, + input + AUTH_SECRET_MAGIC_SIZE + AUTH_NONCE_SIZE, + AUTH_TAG_SIZE)) + goto done; + secret[plain_size] = '\0'; + result = 0; + +done: + if (cipher) + gcry_cipher_close(cipher); + SecureClear(key, sizeof(key)); + if (result != 0) + SecureClear(secret, BONGO_AUTH_TOTP_SECRET_SIZE); + return result; +} + +int +AuthSecurityEnsureSchema(sqlite3 *database) +{ + const char schema[] = + "PRAGMA foreign_keys=ON;" + "CREATE TABLE IF NOT EXISTS auth_security (" + " username TEXT PRIMARY KEY REFERENCES users(username) ON DELETE CASCADE," + " totp_enabled INTEGER NOT NULL DEFAULT 0 CHECK(totp_enabled IN (0,1))," + " require_app_passwords INTEGER NOT NULL DEFAULT 0 " + " CHECK(require_app_passwords IN (0,1))," + " totp_secret BLOB," + " updated_at INTEGER NOT NULL DEFAULT 0," + " CHECK(totp_enabled=0 OR totp_secret IS NOT NULL)," + " CHECK(require_app_passwords=0 OR totp_enabled=1)" + ");" + "CREATE TABLE IF NOT EXISTS auth_recovery_codes (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " username TEXT NOT NULL REFERENCES users(username) ON DELETE CASCADE," + " code_hash TEXT NOT NULL," + " created_at INTEGER NOT NULL," + " used_at INTEGER" + ");" + "CREATE INDEX IF NOT EXISTS auth_recovery_active " + " ON auth_recovery_codes(username,used_at);" + "PRAGMA user_version=1;"; + + return database && sqlite3_exec(database, schema, NULL, NULL, NULL) == SQLITE_OK + ? 0 + : -1; +} + +int +AuthSecurityStoreOpen(AuthSecurityStore *store, const char *database_path, + const char *key_path) +{ + if (!store || !database_path || !key_path) + return -1; + memset(store, 0, sizeof(*store)); + if (sqlite3_open_v2(database_path, &store->database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, + NULL) != SQLITE_OK) + goto fail; + store->key_path = strdup(key_path); + if (!store->key_path) + goto fail; + sqlite3_busy_timeout(store->database, 5000); + if (sqlite3_exec(store->database, + "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;", + NULL, NULL, NULL) != SQLITE_OK || + AuthSecurityEnsureSchema(store->database) != 0) + goto fail; + if (chmod(database_path, S_IRUSR | S_IWUSR) != 0) + goto fail; + return 0; + +fail: + AuthSecurityStoreClose(store); + return -1; +} + +void +AuthSecurityStoreClose(AuthSecurityStore *store) +{ + if (!store) + return; + if (store->database) + sqlite3_close(store->database); + free(store->key_path); + memset(store, 0, sizeof(*store)); +} + +int +AuthSecurityGetPolicy(AuthSecurityStore *store, const char *username, + int *totp_enabled, int *require_app_passwords) +{ + sqlite3_stmt *statement = NULL; + int result = -1; + + if (!store || !store->database || !username || !totp_enabled || + !require_app_passwords) + return -1; + *totp_enabled = 0; + *require_app_passwords = 0; + if (sqlite3_prepare_v2(store->database, + "SELECT totp_enabled,require_app_passwords FROM auth_security " + "WHERE username=?", + -1, &statement, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + result = sqlite3_step(statement); + if (result == SQLITE_ROW) { + *totp_enabled = sqlite3_column_int(statement, 0) != 0; + *require_app_passwords = sqlite3_column_int(statement, 1) != 0; + result = 0; + } else if (result == SQLITE_DONE) { + result = 0; + } else { + result = -1; + } + sqlite3_finalize(statement); + return result; +} + +int +AuthSecurityEnableTotp(AuthSecurityStore *store, const char *username, + const char *secret, const char *otp, time_t now, + char recovery_codes[AUTH_RECOVERY_CODE_COUNT] + [BONGO_AUTH_CREDENTIAL_SIZE]) +{ + char hashes[AUTH_RECOVERY_CODE_COUNT][BONGO_AUTH_HASH_SIZE]; + unsigned char *encrypted = NULL; + size_t encrypted_size = 0; + sqlite3_stmt *statement = NULL; + int transaction = 0; + int result = -1; + int i; + + if (!store || !store->database || !username || !secret || !otp || + !recovery_codes || !BongoAuthVerifyTotp(secret, otp, now)) + return -1; + memset(hashes, 0, sizeof(hashes)); + for (i = 0; i < AUTH_RECOVERY_CODE_COUNT; i++) { + if (BongoAuthGenerateCredential(recovery_codes[i], + BONGO_AUTH_CREDENTIAL_SIZE) != 0 || + BongoAuthHashCredential(recovery_codes[i], hashes[i], + BONGO_AUTH_HASH_SIZE) != 0) + goto done; + } + if (EncryptSecret(store, username, secret, &encrypted, &encrypted_size) != 0) + goto done; + if (sqlite3_exec(store->database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != + SQLITE_OK) + goto done; + transaction = 1; + if (sqlite3_prepare_v2(store->database, + "INSERT INTO auth_security(username,totp_enabled," + "require_app_passwords,totp_secret,updated_at) VALUES(?,1,0,?,?) " + "ON CONFLICT(username) DO UPDATE SET totp_enabled=1," + "require_app_passwords=0,totp_secret=excluded.totp_secret," + "updated_at=excluded.updated_at", + -1, &statement, NULL) != SQLITE_OK) + goto done; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + sqlite3_bind_blob(statement, 2, encrypted, (int)encrypted_size, + SQLITE_TRANSIENT); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)now); + if (sqlite3_step(statement) != SQLITE_DONE) + goto done; + sqlite3_finalize(statement); + statement = NULL; + if (sqlite3_prepare_v2(store->database, + "DELETE FROM auth_recovery_codes WHERE username=?", -1, + &statement, NULL) != SQLITE_OK) + goto done; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + if (sqlite3_step(statement) != SQLITE_DONE) + goto done; + sqlite3_finalize(statement); + statement = NULL; + if (sqlite3_prepare_v2(store->database, + "INSERT INTO auth_recovery_codes(username,code_hash,created_at) " + "VALUES(?,?,?)", + -1, &statement, NULL) != SQLITE_OK) + goto done; + for (i = 0; i < AUTH_RECOVERY_CODE_COUNT; i++) { + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, hashes[i], -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(statement, 3, (sqlite3_int64)now); + if (sqlite3_step(statement) != SQLITE_DONE) + goto done; + sqlite3_reset(statement); + sqlite3_clear_bindings(statement); + } + if (sqlite3_exec(store->database, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) + goto done; + transaction = 0; + result = 0; + +done: + if (statement) + sqlite3_finalize(statement); + if (transaction) + sqlite3_exec(store->database, "ROLLBACK", NULL, NULL, NULL); + if (result != 0) + SecureClear(recovery_codes, AUTH_RECOVERY_CODE_COUNT * + BONGO_AUTH_CREDENTIAL_SIZE); + SecureClear(hashes, sizeof(hashes)); + if (encrypted) { + SecureClear(encrypted, encrypted_size); + free(encrypted); + } + return result; +} + +static int +VerifyRecoveryCode(AuthSecurityStore *store, const char *username, + const char *response, time_t now) +{ + sqlite3_stmt *statement = NULL; + sqlite3_int64 match = 0; + int status; + + if (sqlite3_prepare_v2(store->database, + "SELECT id,code_hash FROM auth_recovery_codes " + "WHERE username=? AND used_at IS NULL ORDER BY id", + -1, &statement, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + while ((status = sqlite3_step(statement)) == SQLITE_ROW) { + const char *hash = (const char *)sqlite3_column_text(statement, 1); + + if (hash && BongoAuthVerifyCredential(hash, response)) { + match = sqlite3_column_int64(statement, 0); + break; + } + } + sqlite3_finalize(statement); + if (!match) + return status == SQLITE_DONE ? 0 : -1; + if (sqlite3_prepare_v2(store->database, + "UPDATE auth_recovery_codes SET used_at=? " + "WHERE id=? AND used_at IS NULL", + -1, &statement, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_int64(statement, 1, (sqlite3_int64)now); + sqlite3_bind_int64(statement, 2, match); + status = sqlite3_step(statement) == SQLITE_DONE && + sqlite3_changes(store->database) == 1; + sqlite3_finalize(statement); + return status ? 1 : -1; +} + +int +AuthSecurityVerifySecondFactor(AuthSecurityStore *store, + const char *username, const char *response, + time_t now) +{ + sqlite3_stmt *statement = NULL; + char secret[BONGO_AUTH_TOTP_SECRET_SIZE]; + const void *encrypted; + int encrypted_size; + int status; + + if (!store || !store->database || !username || !response) + return AUTH_SECOND_FACTOR_ERROR; + if (sqlite3_prepare_v2(store->database, + "SELECT totp_enabled,totp_secret FROM auth_security WHERE username=?", + -1, &statement, NULL) != SQLITE_OK) + return AUTH_SECOND_FACTOR_ERROR; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + status = sqlite3_step(statement); + if (status == SQLITE_DONE || + (status == SQLITE_ROW && sqlite3_column_int(statement, 0) == 0)) { + sqlite3_finalize(statement); + return AUTH_SECOND_FACTOR_NOT_REQUIRED; + } + if (status != SQLITE_ROW || sqlite3_column_type(statement, 1) != SQLITE_BLOB) { + sqlite3_finalize(statement); + return AUTH_SECOND_FACTOR_ERROR; + } + encrypted = sqlite3_column_blob(statement, 1); + encrypted_size = sqlite3_column_bytes(statement, 1); + status = DecryptSecret(store, username, encrypted, (size_t)encrypted_size, + secret); + sqlite3_finalize(statement); + if (status != 0) + return AUTH_SECOND_FACTOR_ERROR; + status = BongoAuthVerifyTotp(secret, response, now); + SecureClear(secret, sizeof(secret)); + if (status) + return AUTH_SECOND_FACTOR_VALID; + status = VerifyRecoveryCode(store, username, response, now); + if (status < 0) + return AUTH_SECOND_FACTOR_ERROR; + return status ? AUTH_SECOND_FACTOR_VALID : AUTH_SECOND_FACTOR_INVALID; +} + +int +AuthSecurityDisableTotp(AuthSecurityStore *store, const char *username) +{ + sqlite3_stmt *statement = NULL; + int transaction = 0; + int result = -1; + + if (!store || !store->database || !username) + return -1; + if (sqlite3_exec(store->database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != + SQLITE_OK) + return -1; + transaction = 1; + if (sqlite3_prepare_v2(store->database, + "INSERT INTO auth_security(username,totp_enabled," + "require_app_passwords,totp_secret,updated_at) VALUES(?,0,0,NULL,?) " + "ON CONFLICT(username) DO UPDATE SET totp_enabled=0," + "require_app_passwords=0,totp_secret=NULL,updated_at=excluded.updated_at", + -1, &statement, NULL) != SQLITE_OK) + goto done; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)time(NULL)); + if (sqlite3_step(statement) != SQLITE_DONE) + goto done; + sqlite3_finalize(statement); + statement = NULL; + if (sqlite3_prepare_v2(store->database, + "DELETE FROM auth_recovery_codes WHERE username=?", -1, + &statement, NULL) != SQLITE_OK) + goto done; + sqlite3_bind_text(statement, 1, username, -1, SQLITE_TRANSIENT); + if (sqlite3_step(statement) != SQLITE_DONE) + goto done; + if (sqlite3_exec(store->database, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) + goto done; + transaction = 0; + result = 0; + +done: + if (statement) + sqlite3_finalize(statement); + if (transaction) + sqlite3_exec(store->database, "ROLLBACK", NULL, NULL, NULL); + return result; +} + +int +AuthSecuritySetRequireAppPasswords(AuthSecurityStore *store, + const char *username, int required) +{ + sqlite3_stmt *statement = NULL; + int result; + + if (!store || !store->database || !username) + return -1; + if (sqlite3_prepare_v2(store->database, + "UPDATE auth_security SET require_app_passwords=?,updated_at=? " + "WHERE username=? AND (?=0 OR totp_enabled=1)", + -1, &statement, NULL) != SQLITE_OK) + return -1; + sqlite3_bind_int(statement, 1, required != 0); + sqlite3_bind_int64(statement, 2, (sqlite3_int64)time(NULL)); + sqlite3_bind_text(statement, 3, username, -1, SQLITE_TRANSIENT); + sqlite3_bind_int(statement, 4, required != 0); + result = sqlite3_step(statement) == SQLITE_DONE && + sqlite3_changes(store->database) == 1; + sqlite3_finalize(statement); + return result ? 0 : -1; +} diff --git a/src/libs/msgapi/auth-backends/auth-security-store.h b/src/libs/msgapi/auth-backends/auth-security-store.h new file mode 100644 index 0000000..ca47751 --- /dev/null +++ b/src/libs/msgapi/auth-backends/auth-security-store.h @@ -0,0 +1,63 @@ +/**************************************************************************** + * + * Copyright (c) 2001 Novell, Inc. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU General Public License + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, contact Novell, Inc. + * + * To contact Novell about this file by physical or electronic mail, you + * may find current contact information at www.novell.com. + * + ****************************************************************************/ + +#ifndef BONGO_AUTH_SECURITY_STORE_H +#define BONGO_AUTH_SECURITY_STORE_H + +#include + +#include +#include + +#include + +#define AUTH_RECOVERY_CODE_COUNT 10 + +typedef struct { + sqlite3 *database; + char *key_path; +} AuthSecurityStore; + +enum { + AUTH_SECOND_FACTOR_ERROR = -1, + AUTH_SECOND_FACTOR_NOT_REQUIRED = 0, + AUTH_SECOND_FACTOR_VALID = 1, + AUTH_SECOND_FACTOR_INVALID = 2 +}; + +int AuthSecurityEnsureSchema(sqlite3 *database); +int AuthSecurityStoreOpen(AuthSecurityStore *store, const char *database_path, + const char *key_path); +void AuthSecurityStoreClose(AuthSecurityStore *store); +int AuthSecurityGetPolicy(AuthSecurityStore *store, const char *username, + int *totp_enabled, int *require_app_passwords); +int AuthSecurityEnableTotp(AuthSecurityStore *store, const char *username, + const char *secret, const char *otp, time_t now, + char recovery_codes[AUTH_RECOVERY_CODE_COUNT] + [BONGO_AUTH_CREDENTIAL_SIZE]); +int AuthSecurityVerifySecondFactor(AuthSecurityStore *store, + const char *username, const char *response, + time_t now); +int AuthSecurityDisableTotp(AuthSecurityStore *store, const char *username); +int AuthSecuritySetRequireAppPasswords(AuthSecurityStore *store, + const char *username, int required); + +#endif diff --git a/src/libs/msgapi/auth-backends/sqlite.c b/src/libs/msgapi/auth-backends/sqlite.c index e064da7..ff05d36 100644 --- a/src/libs/msgapi/auth-backends/sqlite.c +++ b/src/libs/msgapi/auth-backends/sqlite.c @@ -7,6 +7,7 @@ #define LOGGERNAME "msgauth" #include +#include #include #include #include @@ -14,6 +15,7 @@ #include "sqlite.h" #include "password.h" +#include "auth-security-store.h" typedef struct { MsgSQLStatement find_user; @@ -44,14 +46,7 @@ AuthSqlite_Install(void) int dcode; char path[XPL_MAX_PATH + 1]; MsgSQLHandle *handle; - struct stat buf; - AuthSqlite_GetDbPath(path, XPL_MAX_PATH); - if (stat(path, &buf) == 0) { - // FIXME: db already exists - for now, remove, but could/should be more gentle - unlink(path); - } - handle = MsgSQLOpen(path, NULL, 1000); if (NULL == handle) return -2; @@ -59,14 +54,14 @@ AuthSqlite_Install(void) if (MsgSQLBeginTransaction(handle)) goto fail; dcode = sqlite3_exec (handle->db, - "PRAGMA user_version=0;" - "CREATE TABLE users (username TEXT DEFAULT NULL UNIQUE," + "CREATE TABLE IF NOT EXISTS users (username TEXT DEFAULT NULL UNIQUE," " password TEXT DEFAULT NULL" ");" - "INSERT INTO users (username, password)" + "INSERT OR IGNORE INTO users (username, password)" " VALUES ('admin', NULL); ", NULL, NULL, NULL); if (SQLITE_OK != dcode) goto fail; + if (AuthSecurityEnsureSchema(handle->db) != 0) goto fail; if (MsgSQLCommitTransaction(handle)) goto fail; @@ -81,6 +76,33 @@ fail: return -1; } +int +AuthSqlite_Init(void) +{ + char path[XPL_MAX_PATH + 1]; + sqlite3 *database = NULL; + int result = -1; + + AuthSqlite_GetDbPath(path, sizeof(path)); + if (access(path, F_OK) != 0 && errno == ENOENT) + return 0; + if (sqlite3_open_v2(path, &database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, + NULL) != SQLITE_OK) + goto done; + sqlite3_busy_timeout(database, 5000); + if (sqlite3_exec(database, + "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;", + NULL, NULL, NULL) == SQLITE_OK && + AuthSecurityEnsureSchema(database) == 0) + result = 0; + +done: + if (database) + sqlite3_close(database); + return result; +} + int AuthSqlite_FindUser(const char *user) { diff --git a/src/libs/msgapi/auth-backends/sqlite.h b/src/libs/msgapi/auth-backends/sqlite.h index aedb1a2..2c95948 100644 --- a/src/libs/msgapi/auth-backends/sqlite.h +++ b/src/libs/msgapi/auth-backends/sqlite.h @@ -8,4 +8,5 @@ int AuthSqlite_SetPassword(const char *user, const char *password); int AuthSqlite_GetUserStore(const char *user, struct sockaddr_in *store); int AuthSqlite_UserList(char **list[]); int AuthSqlite_InterfaceVersion(void); +int AuthSqlite_Init(void); int AuthODBC_Init(void);