Persist optional TOTP authentication securely
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include <liboath/oath.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <gcrypt.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/****************************************************************************
|
||||
* <Novell-copyright>
|
||||
* 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.
|
||||
* </Novell-copyright>
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef BONGO_AUTH_SECURITY_STORE_H
|
||||
#define BONGO_AUTH_SECURITY_STORE_H
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <bongoauthsecurity.h>
|
||||
|
||||
#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
|
||||
@@ -7,6 +7,7 @@
|
||||
#define LOGGERNAME "msgauth"
|
||||
#include <logger.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user