diff --git a/src/api/2fa.c b/src/api/2fa.c index 40c732c9..1b608c3f 100644 --- a/src/api/2fa.c +++ b/src/api/2fa.c @@ -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 @@ -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 diff --git a/src/api/api.c b/src/api/api.c index 45e95b99..e8e230ae 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -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 }, diff --git a/src/api/api.h b/src/api/api.h index 981cf60f..4757e3db 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -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); diff --git a/src/api/auth.c b/src/api/auth.c index 3398e163..c1ee1c09 100644 --- a/src/api/auth.c +++ b/src/api/auth.c @@ -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); diff --git a/src/api/auth.h b/src/api/auth.h index f5f11d85..8cfd5d13 100644 --- a/src/api/auth.h +++ b/src/api/auth.h @@ -50,6 +50,7 @@ struct session { bool used; + bool app; struct { bool login; bool mixed; diff --git a/src/api/docs/content/specs/auth.yaml b/src/api/docs/content/specs/auth.yaml index c6b36f71..117f7eb0 100644 --- a/src/api/docs/content/specs/auth.yaml +++ b/src/api/docs/content/specs/auth.yaml @@ -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: diff --git a/src/api/docs/content/specs/config.yaml b/src/api/docs/content/specs/config.yaml index f01d7e7f..db3154d1 100644 --- a/src/api/docs/content/specs/config.yaml +++ b/src/api/docs/content/specs/config.yaml @@ -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 diff --git a/src/api/docs/content/specs/main.yaml b/src/api/docs/content/specs/main.yaml index 37a131e4..64d0fafe 100644 --- a/src/api/docs/content/specs/main.yaml +++ b/src/api/docs/content/specs/main.yaml @@ -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' diff --git a/src/config/cli.c b/src/config/cli.c index 22eb2a2c..ddc23db9 100644 --- a/src/config/cli.c +++ b/src/config/cli.c @@ -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" diff --git a/src/config/config.c b/src/config/config.c index ea84af52..418a3f29 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -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(""); + 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"); diff --git a/src/config/config.h b/src/config/config.h index 1a17cbf8..c230627e 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -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; diff --git a/src/config/password.c b/src/config/password.c index 411444d5..1b01922d 100644 --- a/src/config/password.c +++ b/src/config/password.c @@ -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; +} diff --git a/src/config/password.h b/src/config/password.h index 5a3e4053..a4cc96b6 100644 --- a/src/config/password.h +++ b/src/config/password.h @@ -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)); diff --git a/src/config/toml_reader.c b/src/config/toml_reader.c index e7a41325..19221549 100644 --- a/src/config/toml_reader.c +++ b/src/config/toml_reader.c @@ -18,10 +18,11 @@ #include "args.h" // INT_MAX #include - -#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(); } } diff --git a/src/database/common.c b/src/database/common.c index 5c5266d3..4fd30b6c 100644 --- a/src/database/common.c +++ b/src/database/common.c @@ -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(); diff --git a/src/database/session-table.c b/src/database/session-table.c index 51b8e24b..f99bd67c 100644 --- a/src/database/session-table.c +++ b/src/database/session-table.c @@ -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; } diff --git a/src/database/session-table.h b/src/database/session-table.h index a2d48118..7f733577 100644 --- a/src/database/session-table.h +++ b/src/database/session-table.h @@ -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); diff --git a/test/pihole.toml b/test/pihole.toml index 424177d9..2fc68e64 100644 --- a/test/pihole.toml +++ b/test/pihole.toml @@ -621,6 +621,20 @@ # 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: + # + app_pwhash = "" + # Array of clients to be excluded from certain API responses # Example: [ "192.168.2.56", "fe80::341", "localhost" ] # diff --git a/test/test_suite.bats b/test/test_suite.bats index 98ce99e9..af19c27d 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -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\"}}}}"'