mirror of
https://github.com/pi-hole/FTL.git
synced 2024-10-26 16:52:18 +02:00
+36
-6
@@ -8,13 +8,15 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
#include "../FTL.h"
|
||||
#include "api.h"
|
||||
#include "../webserver/json_macros.h"
|
||||
#include "../log.h"
|
||||
#include "../config/config.h"
|
||||
#include "FTL.h"
|
||||
#include "api/api.h"
|
||||
#include "webserver/json_macros.h"
|
||||
#include "log.h"
|
||||
#include "config/config.h"
|
||||
// getrandom()
|
||||
#include "../daemon.h"
|
||||
#include "daemon.h"
|
||||
// generate_app_password()
|
||||
#include "config/password.h"
|
||||
|
||||
// TOTP+HMAC
|
||||
#include <nettle/hmac.h>
|
||||
@@ -306,6 +308,34 @@ int generateTOTP(struct ftl_conn *api)
|
||||
JSON_SEND_OBJECT(json);
|
||||
}
|
||||
|
||||
int generateAppPw(struct ftl_conn *api)
|
||||
{
|
||||
// Generate and set app password
|
||||
char *password = NULL, *pwhash = NULL;
|
||||
if(!generate_app_password(&password, &pwhash))
|
||||
{
|
||||
return send_json_error(api,
|
||||
500,
|
||||
"internal_error",
|
||||
"Failed to generate app password",
|
||||
"Check FTL.log for details");
|
||||
}
|
||||
|
||||
// Create JSON object
|
||||
cJSON *tjson = cJSON_CreateObject();
|
||||
JSON_COPY_STR_TO_OBJECT(tjson, "password", password);
|
||||
JSON_COPY_STR_TO_OBJECT(tjson, "hash", pwhash);
|
||||
free(password);
|
||||
password = NULL;
|
||||
free(pwhash);
|
||||
pwhash = NULL;
|
||||
|
||||
// Send JSON response
|
||||
cJSON *json = cJSON_CreateObject();
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "app", tjson);
|
||||
JSON_SEND_OBJECT(json);
|
||||
}
|
||||
|
||||
#if 0
|
||||
#define RFC6238_TESTKEY "12345678901234567890"
|
||||
#define RFC6238_TESTTIME 59
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ static struct {
|
||||
// appear *before* less specific URIs: 1. "/a/b/c", 2. "/a/b", 3. "/a"
|
||||
{ "/api/auth/sessions", "", api_auth_sessions, { false, true, 0 }, true, HTTP_GET },
|
||||
{ "/api/auth/session", "/{id}", api_auth_session_delete, { false, true, 0 }, true, HTTP_DELETE },
|
||||
{ "/api/auth/totp", "", generateTOTP, { false, true, 0 }, false, HTTP_GET },
|
||||
{ "/api/auth/app", "", generateAppPw, { false, true, 0 }, true, HTTP_GET },
|
||||
{ "/api/auth/totp", "", generateTOTP, { false, true, 0 }, true, HTTP_GET },
|
||||
{ "/api/auth", "", api_auth, { false, true, 0 }, false, HTTP_GET | HTTP_POST | HTTP_DELETE },
|
||||
{ "/api/dns/blocking", "", api_dns_blocking, { false, true, 0 }, true, HTTP_GET | HTTP_POST },
|
||||
{ "/api/clients/_suggestions", "", api_client_suggestions, { false, true, 0 }, true, HTTP_GET },
|
||||
|
||||
@@ -95,6 +95,7 @@ bool is_local_api_user(const char *remote_addr) __attribute__((pure));
|
||||
bool verifyTOTP(const uint32_t code);
|
||||
int generateTOTP(struct ftl_conn *api);
|
||||
int printTOTP(void);
|
||||
int generateAppPw(struct ftl_conn *api);
|
||||
|
||||
// Documentation methods
|
||||
int api_docs(struct ftl_conn *api);
|
||||
|
||||
+22
-12
@@ -26,8 +26,7 @@
|
||||
// database session functions
|
||||
#include "database/session-table.h"
|
||||
|
||||
|
||||
static struct session auth_data[API_MAX_CLIENTS] = {{false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}};
|
||||
static struct session auth_data[API_MAX_CLIENTS] = {{false, false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}};
|
||||
|
||||
static void add_request_info(struct ftl_conn *api, const char *csrf)
|
||||
{
|
||||
@@ -271,6 +270,7 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
|
||||
JSON_ADD_NUMBER_TO_OBJECT(session, "valid_until", auth_data[i].valid_until);
|
||||
JSON_REF_STR_IN_OBJECT(session, "remote_addr", auth_data[i].remote_addr);
|
||||
JSON_REF_STR_IN_OBJECT(session, "user_agent", auth_data[i].user_agent);
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "app", auth_data[i].app);
|
||||
JSON_ADD_ITEM_TO_ARRAY(sessions, session);
|
||||
}
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "sessions", sessions);
|
||||
@@ -417,13 +417,6 @@ int api_auth(struct ftl_conn *api)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Did the client authenticate before and we can validate this?
|
||||
int user_id = check_client_auth(api, false);
|
||||
|
||||
// If this is a valid session, we can exit early at this point
|
||||
if(user_id != API_AUTH_UNAUTHORIZED)
|
||||
return send_api_auth_status(api, user_id, now);
|
||||
|
||||
// Login attempt, check password
|
||||
if(api->method == HTTP_POST)
|
||||
{
|
||||
@@ -469,6 +462,13 @@ int api_auth(struct ftl_conn *api)
|
||||
password = json_password->valuestring;
|
||||
}
|
||||
|
||||
// Did the client authenticate before and we can validate this?
|
||||
int user_id = check_client_auth(api, false);
|
||||
|
||||
// If this is a valid session, we can exit early at this point if no password is supplied
|
||||
if(user_id != API_AUTH_UNAUTHORIZED && (password == NULL || strlen(password) == 0))
|
||||
return send_api_auth_status(api, user_id, now);
|
||||
|
||||
// Logout attempt
|
||||
if(api->method == HTTP_DELETE)
|
||||
{
|
||||
@@ -483,8 +483,16 @@ int api_auth(struct ftl_conn *api)
|
||||
// else: Login attempt
|
||||
// - Client tries to authenticate using a password, or
|
||||
// - There no password on this machine
|
||||
const enum password_result result = empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s, true);
|
||||
if(result == PASSWORD_CORRECT)
|
||||
enum password_result result = PASSWORD_INCORRECT;
|
||||
|
||||
// If there is no password (or empty), check if there is any password at all
|
||||
log_info("Password: \"%s\"", password);
|
||||
if(empty_password && (password == NULL || strlen(password) == 0))
|
||||
result = PASSWORD_CORRECT;
|
||||
else
|
||||
result = verify_login(password);
|
||||
|
||||
if(result == PASSWORD_CORRECT || result == APPPASSWORD_CORRECT)
|
||||
{
|
||||
// Accepted
|
||||
|
||||
@@ -494,7 +502,8 @@ int api_auth(struct ftl_conn *api)
|
||||
memset(password, 0, strlen(password));
|
||||
|
||||
// Check possible 2FA token
|
||||
if(strlen(config.webserver.api.totp_secret.v.s) > 0)
|
||||
// Successful login with empty password does not require 2FA
|
||||
if(strlen(config.webserver.api.totp_secret.v.s) > 0 && result != APPPASSWORD_CORRECT)
|
||||
{
|
||||
// Get 2FA token from payload
|
||||
cJSON *json_totp;
|
||||
@@ -555,6 +564,7 @@ int api_auth(struct ftl_conn *api)
|
||||
|
||||
auth_data[i].tls.login = api->request->is_ssl;
|
||||
auth_data[i].tls.mixed = false;
|
||||
auth_data[i].app = result == APPPASSWORD_CORRECT;
|
||||
|
||||
// Generate new SID and CSRF token
|
||||
generateSID(auth_data[i].sid);
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
|
||||
struct session {
|
||||
bool used;
|
||||
bool app;
|
||||
struct {
|
||||
bool login;
|
||||
bool mixed;
|
||||
|
||||
@@ -166,7 +166,6 @@ components:
|
||||
tags:
|
||||
- Authentication
|
||||
operationId: "get_auth_totp"
|
||||
security: []
|
||||
description: Suggest new TOTP credentials for two-factor authentication (2FA)
|
||||
responses:
|
||||
'200':
|
||||
@@ -221,6 +220,31 @@ components:
|
||||
allOf:
|
||||
- $ref: 'common.yaml#/components/errors/unauthorized'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
app:
|
||||
get:
|
||||
summary: Create new application password
|
||||
tags:
|
||||
- Authentication
|
||||
operationId: "add_app"
|
||||
description: |
|
||||
Create a new application password. The generated password is shown only once and cannot be retrieved later so make sure to store it in a safe place. The application password can be used to authenticate against the API instead of the regular password. It does not require 2FA verification. Generating a new application password will invalidate all currently active sessions.
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: 'auth.yaml#/components/schemas/app'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: 'common.yaml#/components/errors/unauthorized'
|
||||
- $ref: 'common.yaml#/components/schemas/took'
|
||||
|
||||
schemas:
|
||||
session:
|
||||
@@ -348,6 +372,20 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
app:
|
||||
type: object
|
||||
description: Application password
|
||||
properties:
|
||||
app:
|
||||
type: object
|
||||
properties:
|
||||
password:
|
||||
type: string
|
||||
hash:
|
||||
type: string
|
||||
example:
|
||||
password: "7ua0rHPZixX6B3bTa5o4Dv08iyEIm/2q9qgLrF9MNVw="
|
||||
hash: "$BALLOON-SHA256$v=1$s=1024,t=32$4ERwJD4XucRP4PcDHNLiAg==$kSy0Eou8RUVtK2UTbc5MCpItV8YC3VRuoGhoENxSQ2I="
|
||||
|
||||
errors:
|
||||
bad_request:
|
||||
|
||||
@@ -396,6 +396,8 @@ components:
|
||||
type: string
|
||||
totp_secret:
|
||||
type: string
|
||||
app_pwhash:
|
||||
type: string
|
||||
excludeClients:
|
||||
type: array
|
||||
items:
|
||||
@@ -668,6 +670,7 @@ components:
|
||||
password: "********"
|
||||
pwhash: ''
|
||||
totp_secret: ''
|
||||
app_pwhash: ''
|
||||
excludeClients: [ '1.2.3.4', 'localhost', 'fe80::345' ]
|
||||
excludeDomains: [ 'google.de', 'pi-hole.net' ]
|
||||
maxHistory: 86400
|
||||
|
||||
@@ -79,6 +79,9 @@ paths:
|
||||
/auth/totp:
|
||||
$ref: 'auth.yaml#/components/paths/totp'
|
||||
|
||||
/auth/app:
|
||||
$ref: 'auth.yaml#/components/paths/app'
|
||||
|
||||
/stats/summary:
|
||||
$ref: 'stats.yaml#/components/paths/summary'
|
||||
|
||||
|
||||
@@ -14,13 +14,10 @@
|
||||
#include "config/toml_helper.h"
|
||||
#include "config/toml_writer.h"
|
||||
#include "config/dnsmasq_config.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "datastructure.h"
|
||||
|
||||
// toml_table_t
|
||||
#include "tomlc99/toml.h"
|
||||
|
||||
// hash_password()
|
||||
#include "config/password.h"
|
||||
|
||||
|
||||
@@ -930,6 +930,13 @@ void initConfig(struct config *conf)
|
||||
conf->webserver.api.totp_secret.f = FLAG_WRITE_ONLY | FLAG_INVALIDATE_SESSIONS;
|
||||
conf->webserver.api.totp_secret.d.s = (char*)"";
|
||||
|
||||
conf->webserver.api.app_pwhash.k = "webserver.api.app_pwhash";
|
||||
conf->webserver.api.app_pwhash.h = "Pi-hole application password.\n After you turn on two-factor (2FA) verification and set up an Authenticator app, you may run into issues if you use apps or other services that don't support two-step verification. In this case, you can create and use an app password to sign in. An app password is a long, randomly generated password that can be used instead of your regular password + TOTP token when signing in to the API. The app password can be generated through the API and will be shown only once. You can revoke the app password at any time. If you revoke the app password, be sure to generate a new one and update your app with the new password.";
|
||||
conf->webserver.api.app_pwhash.a = cJSON_CreateStringReference("<valid Pi-hole password hash>");
|
||||
conf->webserver.api.app_pwhash.t = CONF_STRING;
|
||||
conf->webserver.api.app_pwhash.f = FLAG_INVALIDATE_SESSIONS;
|
||||
conf->webserver.api.app_pwhash.d.s = (char*)"";
|
||||
|
||||
conf->webserver.api.excludeClients.k = "webserver.api.excludeClients";
|
||||
conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses\n Example: [ \"192.168.2.56\", \"fe80::341\", \"localhost\" ]";
|
||||
conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of IP addresses and/or hostnames");
|
||||
|
||||
@@ -230,6 +230,7 @@ struct config {
|
||||
struct conf_item pwhash;
|
||||
struct conf_item password; // This is a pseudo-item
|
||||
struct conf_item totp_secret; // This is a write-only item
|
||||
struct conf_item app_pwhash;
|
||||
struct conf_item excludeClients;
|
||||
struct conf_item excludeDomains;
|
||||
struct conf_item maxHistory;
|
||||
|
||||
+66
-1
@@ -307,7 +307,27 @@ char * __attribute__((malloc)) create_password(const char *password)
|
||||
return balloon_password(password, salt, true);
|
||||
}
|
||||
|
||||
char verify_password(const char *password, const char* pwhash, const bool rate_limiting)
|
||||
enum password_result verify_login(const char *password)
|
||||
{
|
||||
enum password_result pw = verify_password(password, config.webserver.api.pwhash.v.s, true);
|
||||
if(pw == PASSWORD_CORRECT)
|
||||
log_debug(DEBUG_API, "Password correct");
|
||||
else
|
||||
log_debug(DEBUG_API, "Password incorrect");
|
||||
|
||||
// Check if an application password is set and if it matches
|
||||
if(pw == PASSWORD_INCORRECT &&
|
||||
strlen(config.webserver.api.app_pwhash.v.s) > 0 &&
|
||||
verify_password(password, config.webserver.api.app_pwhash.v.s, true) == PASSWORD_CORRECT)
|
||||
{
|
||||
log_debug(DEBUG_API, "App password correct");
|
||||
return APPPASSWORD_CORRECT;
|
||||
}
|
||||
// Return result
|
||||
return pw;
|
||||
}
|
||||
|
||||
enum password_result verify_password(const char *password, const char *pwhash, const bool rate_limiting)
|
||||
{
|
||||
// No password set
|
||||
if(pwhash == NULL || pwhash[0] == '\0')
|
||||
@@ -361,6 +381,10 @@ char verify_password(const char *password, const char* pwhash, const bool rate_l
|
||||
if(config_hash != NULL)
|
||||
free(config_hash);
|
||||
|
||||
// Successful logins do not count against rate-limiting
|
||||
if(result)
|
||||
num_password_attempts--;
|
||||
|
||||
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
|
||||
}
|
||||
else
|
||||
@@ -386,6 +410,10 @@ char verify_password(const char *password, const char* pwhash, const bool rate_l
|
||||
}
|
||||
}
|
||||
|
||||
// Successful logins do not count against rate-limiting
|
||||
if(result)
|
||||
num_password_attempts--;
|
||||
|
||||
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
|
||||
}
|
||||
}
|
||||
@@ -597,3 +625,40 @@ bool set_and_check_password(struct conf_item *conf_item, const char *password)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool generate_app_password(char **password, char **pwhash)
|
||||
{
|
||||
// Generate a 128 bit random salt
|
||||
// genrandom() returns cryptographically secure random data
|
||||
uint8_t salt[SALT_LEN] = { 0 };
|
||||
if(getrandom(salt, sizeof(salt), 0) < 0)
|
||||
{
|
||||
log_err("getrandom() failed in generate_app_password()");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate a 256 bit random password
|
||||
uint8_t password_raw[256/8] = { 0 };
|
||||
if(getrandom(password_raw, sizeof(password_raw), 0) < 0)
|
||||
{
|
||||
log_err("getrandom() failed in generate_app_password()");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encode password as base64
|
||||
*password = base64_encode(password_raw, sizeof(password_raw));
|
||||
|
||||
// Generate balloon PHC-encoded password hash
|
||||
*pwhash = balloon_password(*password, salt, true);
|
||||
|
||||
// Verify that the password hash is valid
|
||||
if(verify_password(*password, *pwhash, false) != PASSWORD_CORRECT)
|
||||
{
|
||||
free(password);
|
||||
free(pwhash);
|
||||
log_warn("Failed to create password hash (verification failed), app password not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
|
||||
void sha256_raw_to_hex(uint8_t *data, char *buffer);
|
||||
char *create_password(const char *password) __attribute__((malloc));
|
||||
char verify_password(const char *password, const char *pwhash, const bool rate_limiting);
|
||||
enum password_result verify_login(const char *password);
|
||||
enum password_result verify_password(const char *password, const char *pwhash, const bool rate_limiting);
|
||||
int run_performance_test(void);
|
||||
bool set_and_check_password(struct conf_item *conf_item, const char *password);
|
||||
bool generate_app_password(char **password, char **pwhash);
|
||||
|
||||
enum password_result {
|
||||
PASSWORD_INCORRECT = 0,
|
||||
PASSWORD_CORRECT = 1,
|
||||
APPPASSWORD_CORRECT = 2,
|
||||
PASSWORD_RATE_LIMITED = -1
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
#include "args.h"
|
||||
// INT_MAX
|
||||
#include <limits.h>
|
||||
|
||||
#include "../datastructure.h"
|
||||
#include "datastructure.h"
|
||||
// openFTLtoml()
|
||||
#include "toml_helper.h"
|
||||
#include "config/toml_helper.h"
|
||||
// delete_all_sessions()
|
||||
#include "api/api.h"
|
||||
|
||||
// Private prototypes
|
||||
static toml_table_t *parseTOML(void);
|
||||
@@ -114,6 +115,11 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf,
|
||||
log_debug(DEBUG_CONFIG, "%s CHANGED", new_conf_item->k);
|
||||
if(new_conf_item->f & FLAG_RESTART_FTL && restart != NULL)
|
||||
*restart = true;
|
||||
|
||||
// Check if this item changed the password, if so, we need to
|
||||
// invalidate all currently active sessions
|
||||
if(new_conf_item->f & FLAG_INVALIDATE_SESSIONS)
|
||||
delete_all_sessions();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -528,6 +528,21 @@ void db_init(void)
|
||||
dbversion = db_get_int(db, DB_VERSION);
|
||||
}
|
||||
|
||||
// Update to version 16 if lower
|
||||
if(dbversion < 16)
|
||||
{
|
||||
// Update to version 16: Add app column to session table
|
||||
log_info("Updating long-term database to version 16");
|
||||
if(!add_session_app_column(db))
|
||||
{
|
||||
log_info("Session table cannot be updated, database not available");
|
||||
dbclose(&db);
|
||||
return;
|
||||
}
|
||||
// Get updated version
|
||||
dbversion = db_get_int(db, DB_VERSION);
|
||||
}
|
||||
|
||||
lock_shm();
|
||||
import_aliasclients(db);
|
||||
unlock_shm();
|
||||
|
||||
@@ -42,6 +42,27 @@ bool create_session_table(sqlite3 *db)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool add_session_app_column(sqlite3 *db)
|
||||
{
|
||||
// Start transaction of database update
|
||||
SQL_bool(db, "BEGIN TRANSACTION;");
|
||||
|
||||
// Create session table
|
||||
SQL_bool(db, "ALTER TABLE session ADD COLUMN app BOOL;");
|
||||
|
||||
// Update database version to 16
|
||||
if(!db_set_FTL_property(db, DB_VERSION, 16))
|
||||
{
|
||||
log_err("add_session_app_column(): Failed to update database version!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finish transaction
|
||||
SQL_bool(db, "COMMIT");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Store all session in database
|
||||
bool backup_db_sessions(struct session *sessions)
|
||||
{
|
||||
@@ -60,7 +81,7 @@ bool backup_db_sessions(struct session *sessions)
|
||||
|
||||
// Insert session into database
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if(sqlite3_prepare_v2(db, "INSERT INTO session (login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed) VALUES (?, ?, ?, ?, ?, ?, ?, ?);", -1, &stmt, 0) != SQLITE_OK)
|
||||
if(sqlite3_prepare_v2(db, "INSERT INTO session (login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed, app) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", -1, &stmt, 0) != SQLITE_OK)
|
||||
{
|
||||
log_err("SQL error in backup_db_sessions(): %s (%d)",
|
||||
sqlite3_errmsg(db), sqlite3_errcode(db));
|
||||
@@ -134,6 +155,13 @@ bool backup_db_sessions(struct session *sessions)
|
||||
sess->tls.mixed ? 1 : 0, sqlite3_errmsg(db), sqlite3_errcode(db));
|
||||
return false;
|
||||
}
|
||||
// 9: app
|
||||
if(sqlite3_bind_int(stmt, 8, sess->app ? 1: 0) != SQLITE_OK)
|
||||
{
|
||||
log_err("Cannot bind app = %d in backup_db_sessions(): %s (%d)",
|
||||
sess->app ? 1 : 0, sqlite3_errmsg(db), sqlite3_errcode(db));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute statement
|
||||
if(sqlite3_step(stmt) != SQLITE_DONE)
|
||||
@@ -200,7 +228,7 @@ bool restore_db_sessions(struct session *sessions)
|
||||
|
||||
// Get all sessions from database
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if(sqlite3_prepare_v2(db, "SELECT login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed FROM session;", -1, &stmt, 0) != SQLITE_OK)
|
||||
if(sqlite3_prepare_v2(db, "SELECT login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed, app FROM session;", -1, &stmt, 0) != SQLITE_OK)
|
||||
{
|
||||
log_err("SQL error in restore_db_sessions(): %s (%d)",
|
||||
sqlite3_errmsg(db), sqlite3_errcode(db));
|
||||
@@ -259,6 +287,9 @@ bool restore_db_sessions(struct session *sessions)
|
||||
// 8: tls_mixed
|
||||
sess->tls.mixed = sqlite3_column_int(stmt, 7) == 1 ? true : false;
|
||||
|
||||
// 8: app
|
||||
sess->app = sqlite3_column_int(stmt, 8) == 1 ? true : false;
|
||||
|
||||
// Mark session as used
|
||||
sess->used = true;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "api/auth.h"
|
||||
|
||||
bool create_session_table(sqlite3 *db);
|
||||
bool add_session_app_column(sqlite3 *db);
|
||||
bool backup_db_sessions(struct session *sessions);
|
||||
bool restore_db_sessions(struct session *sessions);
|
||||
|
||||
|
||||
@@ -621,6 +621,20 @@
|
||||
# <valid TOTP secret (20 Bytes in Base32 encoding)>
|
||||
totp_secret = ""
|
||||
|
||||
# Pi-hole application password.
|
||||
# After you turn on two-factor (2FA) verification and set up an Authenticator app, you
|
||||
# may run into issues if you use apps or other services that don't support two-step
|
||||
# verification. In this case, you can create and use an app password to sign in. An
|
||||
# app password is a long, randomly generated password that can be used instead of your
|
||||
# regular password + TOTP token when signing in to the API. The app password can be
|
||||
# generated through the API and will be shown only once. You can revoke the app
|
||||
# password at any time. If you revoke the app password, be sure to generate a new one
|
||||
# and update your app with the new password.
|
||||
#
|
||||
# Possible values are:
|
||||
# <valid Pi-hole password hash>
|
||||
app_pwhash = ""
|
||||
|
||||
# Array of clients to be excluded from certain API responses
|
||||
# Example: [ "192.168.2.56", "fe80::341", "localhost" ]
|
||||
#
|
||||
|
||||
+25
-2
@@ -453,7 +453,7 @@
|
||||
[[ "${lines[@]}" == *"CREATE TABLE IF NOT EXISTS \"network\" (id INTEGER PRIMARY KEY NOT NULL, hwaddr TEXT UNIQUE NOT NULL, interface TEXT NOT NULL, firstSeen INTEGER NOT NULL, lastQuery INTEGER NOT NULL, numQueries INTEGER NOT NULL, macVendor TEXT, aliasclient_id INTEGER);"* ]]
|
||||
[[ "${lines[@]}" == *"CREATE TABLE IF NOT EXISTS \"network_addresses\" (network_id INTEGER NOT NULL, ip TEXT UNIQUE NOT NULL, lastSeen INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), name TEXT, nameUpdated INTEGER, FOREIGN KEY(network_id) REFERENCES network(id));"* ]]
|
||||
[[ "${lines[@]}" == *"CREATE TABLE aliasclient (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, comment TEXT);"* ]]
|
||||
[[ "${lines[@]}" == *"INSERT INTO ftl VALUES(0,15,'Database version');"* ]] # Expecting FTL database version 14
|
||||
[[ "${lines[@]}" == *"INSERT INTO ftl VALUES(0,16,'Database version');"* ]]
|
||||
# vvv This has been added in version 10 vvv
|
||||
[[ "${lines[@]}" == *"CREATE VIEW queries AS SELECT id, timestamp, type, status, CASE typeof(domain) WHEN 'integer' THEN (SELECT domain FROM domain_by_id d WHERE d.id = q.domain) ELSE domain END domain,CASE typeof(client) WHEN 'integer' THEN (SELECT ip FROM client_by_id c WHERE c.id = q.client) ELSE client END client,CASE typeof(forward) WHEN 'integer' THEN (SELECT forward FROM forward_by_id f WHERE f.id = q.forward) ELSE forward END forward,CASE typeof(additional_info) WHEN 'integer' THEN (SELECT content FROM addinfo_by_id a WHERE a.id = q.additional_info) ELSE additional_info END additional_info, reply_type, reply_time, dnssec, regex_id FROM query_storage q;"* ]]
|
||||
[[ "${lines[@]}" == *"CREATE TABLE domain_by_id (id INTEGER PRIMARY KEY, domain TEXT NOT NULL);"* ]]
|
||||
@@ -465,7 +465,7 @@
|
||||
[[ "${lines[@]}" == *"CREATE TABLE addinfo_by_id (id INTEGER PRIMARY KEY, type INTEGER NOT NULL, content NOT NULL);"* ]]
|
||||
[[ "${lines[@]}" == *"CREATE UNIQUE INDEX addinfo_by_id_idx ON addinfo_by_id(type,content);"* ]]
|
||||
# vvv This has been added in version 15 vvv
|
||||
[[ "${lines[@]}" == *"CREATE TABLE session (id INTEGER PRIMARY KEY, login_at TIMESTAMP NOT NULL, valid_until TIMESTAMP NOT NULL, remote_addr TEXT NOT NULL, user_agent TEXT, sid TEXT NOT NULL, csrf TEXT NOT NULL, tls_login BOOL, tls_mixed BOOL);"* ]]
|
||||
[[ "${lines[@]}" == *"CREATE TABLE session (id INTEGER PRIMARY KEY, login_at TIMESTAMP NOT NULL, valid_until TIMESTAMP NOT NULL, remote_addr TEXT NOT NULL, user_agent TEXT, sid TEXT NOT NULL, csrf TEXT NOT NULL, tls_login BOOL, tls_mixed BOOL, app BOOL);"* ]]
|
||||
}
|
||||
|
||||
@test "Ownership, permissions and type of pihole-FTL.db correct" {
|
||||
@@ -1292,6 +1292,29 @@
|
||||
[[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"took":'*'}' ]]
|
||||
}
|
||||
|
||||
@test "Create, set, and use application password" {
|
||||
run bash -c 'curl -s 127.0.0.1/api/auth/app'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == '{"app":{"password":"'*'","hash":"'*'"},"took":'*'}' ]]
|
||||
|
||||
# Extract password and hash from response
|
||||
password="$(echo ${lines[0]} | jq .app.password)"
|
||||
pwhash="$(echo ${lines[0]} | jq .app.hash)"
|
||||
|
||||
printf "password: %s\n" "${password}"
|
||||
printf "pwhash: %s\n" "${pwhash}"
|
||||
|
||||
# Set app password hash
|
||||
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config/webserver/api/app_pwhash -d "{\"config\":{\"webserver\":{\"api\":{\"app_pwhash\":${0}}}}}"' "${pwhash}"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "{\"config\":{\"webserver\":{\"api\":{\"app_pwhash\":${pwhash}}}},\"took\":"*"}" ]]
|
||||
|
||||
# Login using app password is successful
|
||||
run bash -c 'curl -s -X POST 127.0.0.1/api/auth -d "{\"password\":${0}}" | jq .session.valid' "${password}"
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == "true" ]]
|
||||
}
|
||||
|
||||
@test "API authorization: Setting password" {
|
||||
# Password: ABC
|
||||
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config/webserver/api/password -d "{\"config\":{\"webserver\":{\"api\":{\"password\":\"ABC\"}}}}"'
|
||||
|
||||
Reference in New Issue
Block a user