diff --git a/src/api/auth.c b/src/api/auth.c index c1ee1c09..c0b31a83 100644 --- a/src/api/auth.c +++ b/src/api/auth.c @@ -25,6 +25,8 @@ #include "config/password.h" // database session functions #include "database/session-table.h" +// base64_decode() +#include "config/password.h" static struct session auth_data[API_MAX_CLIENTS] = {{false, false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}}; @@ -400,6 +402,47 @@ static void generateSID(char *sid) sid[SID_SIZE-1] = '\0'; } +static char *basic_auth(struct ftl_conn *api) +{ + const char *auth_header = mg_get_header(api->conn, "Authorization"); + if(auth_header == NULL) + return NULL; + + // Check if this is a Basic Auth header + if(strncmp(auth_header, "Basic ", 6) != 0) + return NULL; + + // Decode Base64 + size_t length = 0; + char *decoded = (char*)base64_decode(auth_header + 6, &length); + if(decoded == NULL) + return NULL; + + // Extract username and password + char *username = decoded; + char *password = strchr(decoded, ':'); + if(password == NULL) + { + free(decoded); + return NULL; + } + *password = '\0'; + password++; + + // Check if username is correct + if(strcmp(username, "pi-hole") != 0) + { + free(decoded); + return NULL; + } + + char *password_copy = strdup(password); + free(decoded); + + // Return copy of password + return password_copy; +} + // api/auth // GET: Check authentication // POST: Login @@ -417,6 +460,9 @@ 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); + // Login attempt, check password if(api->method == HTTP_POST) { @@ -462,8 +508,9 @@ 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 there is no password, check if user provided a password via HTTP Basic Auth + if(password == NULL || strlen(password) == 0) + password = basic_auth(api); // 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)) @@ -484,7 +531,7 @@ int api_auth(struct ftl_conn *api) // - Client tries to authenticate using a password, or // - There no password on this machine 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)) diff --git a/src/api/docs/content/specs/auth.yaml b/src/api/docs/content/specs/auth.yaml index 117f7eb0..396ca493 100644 --- a/src/api/docs/content/specs/auth.yaml +++ b/src/api/docs/content/specs/auth.yaml @@ -9,7 +9,7 @@ components: operationId: "get_auth" security: [] description: | - The API may chose to reply with a valid session if no authentication is needed for this server. + The API may chose to reply with a valid session if no authentication is needed for this server. It is possible to initiate a session using HTTP Basic Auth (username: `pi-hole`). responses: '200': description: OK diff --git a/src/config/password.c b/src/config/password.c index 1b01922d..98d3f2e4 100644 --- a/src/config/password.c +++ b/src/config/password.c @@ -77,7 +77,7 @@ static char * __attribute__((malloc)) double_sha256_password(const char *passwor return strdup(response); } -static char * __attribute__((malloc)) base64_encode(const uint8_t *data, const size_t length) +char * __attribute__((malloc)) base64_encode(const uint8_t *data, const size_t length) { // Base64 encoding requires 4 bytes for every 3 bytes of input, plus // additional bytes for padding. The output buffer must be large enough @@ -94,7 +94,7 @@ static char * __attribute__((malloc)) base64_encode(const uint8_t *data, const s return encoded; } -static uint8_t * __attribute__((malloc)) base64_decode(const char *data, size_t *length) +uint8_t * __attribute__((malloc)) base64_decode(const char *data, size_t *length) { // Base64 decoding requires 3 bytes for every 4 bytes of input, plus // additional bytes for padding. The output buffer must be large enough diff --git a/src/config/password.h b/src/config/password.h index a4cc96b6..8d882119 100644 --- a/src/config/password.h +++ b/src/config/password.h @@ -15,6 +15,8 @@ #include void sha256_raw_to_hex(uint8_t *data, char *buffer); +char * __attribute__((malloc)) base64_encode(const uint8_t *data, const size_t length); +uint8_t * __attribute__((malloc)) base64_decode(const char *data, size_t *length); char *create_password(const char *password) __attribute__((malloc)); enum password_result verify_login(const char *password); enum password_result verify_password(const char *password, const char *pwhash, const bool rate_limiting); @@ -30,6 +32,6 @@ enum password_result { } __attribute__((packed)); // The maximum number of password attempts per second -#define MAX_PASSWORD_ATTEMPTS_PER_SECOND 3 +#define MAX_PASSWORD_ATTEMPTS_PER_SECOND 6 #endif //PASSWORD_H diff --git a/test/test_suite.bats b/test/test_suite.bats index af19c27d..b6168c19 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1337,6 +1337,18 @@ [[ ${lines[0]} == "true" ]] } +@test "API authorization (HTTP Basic Auth): Incorrect password is rejected if password auth is enabled" { + run bash -c 'curl -s -u pi-hole:XXX 127.0.0.1/api/auth | jq .session.valid' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'false' ]] +} + +@test "API authorization (HTTP Basic Auth): Correct password is accepted" { + run bash -c 'curl -s -u pi-hole:ABC 127.0.0.1/api/auth | jq .session.valid' + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == 'true' ]] +} + @test "Test TLS/SSL server using self-signed certificate" { # -s: silent # -I: HEAD request