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);