mirror of
https://github.com/pi-hole/FTL.git
synced 2024-10-26 16:52:18 +02:00
Add TOTP 2FA to web interface and API
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
+339
@@ -0,0 +1,339 @@
|
||||
/* Pi-hole: A black hole for Internet advertisements
|
||||
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
|
||||
* Network-wide ad blocking via your own hardware.
|
||||
*
|
||||
* FTL Engine
|
||||
* API Implementation 2FA methods
|
||||
*
|
||||
* 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 <sys/random.h>
|
||||
|
||||
// TOTP+HMAC
|
||||
#include <nettle/hmac.h>
|
||||
#include <nettle/sha1.h>
|
||||
|
||||
static uint32_t hotp(const uint8_t *key, size_t key_len, const uint64_t counter, const uint8_t digits)
|
||||
{
|
||||
// Initialize HMAC-SHA1 (RFC 2104)
|
||||
// TOTP uses HMAC-SHA1 (RFC 6238, section 5.1)
|
||||
struct hmac_sha1_ctx ctx;
|
||||
hmac_sha1_set_key(&ctx, key_len, key);
|
||||
|
||||
// Convert counter to big endian
|
||||
const uint64_t counter_be = htobe64(counter);
|
||||
|
||||
// Compute HMAC-SHA1
|
||||
hmac_sha1_update(&ctx, sizeof(counter_be), (uint8_t*)&counter_be);
|
||||
uint8_t out[SHA1_DIGEST_SIZE];
|
||||
hmac_sha1_digest(&ctx, SHA1_DIGEST_SIZE, out);
|
||||
|
||||
// Truncate HMAC-SHA1 for ease of use
|
||||
// RFC 6238 (section 5.3): offset = last nibble of hash
|
||||
const uint8_t offset = out[SHA1_DIGEST_SIZE-1] & 0x0F;
|
||||
// RFC 6238 (section 5.3): binary = (hash[offset] & 0x7F) << 24 |
|
||||
// (hash[offset+1] & 0xFF) << 16 |
|
||||
// (hash[offset+2] & 0xFF) << 8 |
|
||||
// (hash[offset+3] & 0xFF)
|
||||
const uint32_t binary = (out[offset] & 0x7F) << 24 |
|
||||
(out[offset+1] & 0xFF) << 16 |
|
||||
(out[offset+2] & 0xFF) << 8 |
|
||||
(out[offset+3] & 0xFF);
|
||||
// RFC 6238 (section 5.3): HOTP = binary mod 10^digits
|
||||
uint32_t mask = 10;
|
||||
for(unsigned int i = 1; i < digits; i++)
|
||||
mask *= 10;
|
||||
return binary % mask;
|
||||
}
|
||||
|
||||
// RFC 6238 (section 4.1): T0 is the Unix time to start counting time steps
|
||||
// (default value is 0, i.e., the Unix epoch) and is also a system parameter.
|
||||
#define RFC6238_T0 0
|
||||
// RFC 6238 (section 5.2): We RECOMMEND a default time-step size of 30 seconds.
|
||||
// This default value of 30 seconds is selected as a balance between security
|
||||
// and usability.
|
||||
#define RFC6238_X 30
|
||||
// RFC 6238 (section 4, R6): The algorithm MUST use a strong shared secret. The
|
||||
// length of the shared secret MUST be at least 128 bits (16 Byte). This
|
||||
// document RECOMMENDs a shared secret length of 160 bits (20 Byte).
|
||||
#define RFC6238_SECRET_LEN 160/8
|
||||
// The number of digits to truncate to is not specified in RFC 6238. RFC 4226
|
||||
// (section 5.3) specifies that the default is 6 (up to 8) digits, however, the
|
||||
// example given in RFC 6238 uses 8 digits.
|
||||
#define RFC6238_DIGITS 6
|
||||
|
||||
static uint32_t totp(const uint8_t *key, const size_t key_len, const time_t now)
|
||||
{
|
||||
// Get time
|
||||
// RFC 6238 (section 4.2): T = (Current Unix time - T0) / X
|
||||
// T is an integer and represents the number of time steps between the
|
||||
// initial time T0 and the current time. T needs to be big endian
|
||||
const uint64_t T = (now - RFC6238_T0) / RFC6238_X;
|
||||
|
||||
// RFC 6238 (section 4.2): TOTP(K, T) = HOTP(K,C) with C = T
|
||||
return hotp(key, key_len, T, RFC6238_DIGITS);
|
||||
}
|
||||
|
||||
static bool decode_base32_to_uint8_array(const char *base32, uint8_t *out, const size_t out_len)
|
||||
{
|
||||
// Base32 alphabet
|
||||
const char *b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
// Check input for validity
|
||||
if(out_len == 0 || out_len*8/5 < strlen(base32) || out_len*8%5 != 0)
|
||||
{
|
||||
log_err("Decoding base32 2FA secret failed, invalid length (%zu)", out_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize output array
|
||||
memset(out, 0, out_len);
|
||||
|
||||
// Iterate over input string
|
||||
size_t out_pos = 0u;
|
||||
for(size_t i = 0; i < strlen(base32); i++)
|
||||
{
|
||||
// Get current character
|
||||
const char c = base32[i];
|
||||
|
||||
// Get position of current character in base32 alphabet
|
||||
const char *c_pos = strchr(b32, toupper(c));
|
||||
if(c_pos == NULL)
|
||||
{
|
||||
log_err("Decoding base32 2FA secret failed, invalid character '%c'", c);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get value of current character
|
||||
const uint8_t c_val = (uint8_t)(c_pos-b32);
|
||||
|
||||
// Iterate over 5 bits of the current character
|
||||
for(unsigned int j = 0; j < 5; j++)
|
||||
{
|
||||
// Current bit position
|
||||
const unsigned int bit = 4-j;
|
||||
|
||||
// Get current bit in the current character
|
||||
const uint8_t c_bit = (c_val >> bit) & 1;
|
||||
|
||||
// Get current byte position in the output array
|
||||
out_pos = (i*5+j)/8;
|
||||
|
||||
// If we are out of bounds, return false
|
||||
if(out_pos >= out_len)
|
||||
{
|
||||
log_err("Decoding base32 2FA secret failed, out of bounds (%zu >= %zu)", out_pos, out_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set current bit in the output array
|
||||
out[out_pos] |= c_bit << (7-((i*5+j)%8));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool encode_uint8_t_array_to_base32(const uint8_t *in, const size_t in_len, char *base32, size_t base32_len)
|
||||
{
|
||||
// Base32 alphabet
|
||||
const char *b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
// Check input for validity
|
||||
if(in_len == 0 || in_len > base32_len*5/8 || in_len%5 != 0)
|
||||
{
|
||||
log_err("Encoding base32 2FA secret failed, invalid input length");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize base32 output array
|
||||
memset(base32, 0, base32_len);
|
||||
|
||||
// Iterate over input string
|
||||
size_t base32_pos = 0u;
|
||||
for(size_t i = 0; i < in_len; i++)
|
||||
{
|
||||
// Get current byte
|
||||
const uint8_t b = in[i];
|
||||
|
||||
// Iterate over 8 bits of the current byte
|
||||
for(unsigned int j = 0; j < 8; j++)
|
||||
{
|
||||
// Current bit position
|
||||
const unsigned int bit = 7-j;
|
||||
|
||||
// Get current bit in the current byte
|
||||
const uint8_t b_bit = (b >> bit) & 1;
|
||||
|
||||
// Get current byte position in the base32 output array
|
||||
base32_pos = (i*8+j)/5;
|
||||
|
||||
// If we are out of bounds, return false
|
||||
if(base32_pos >= base32_len)
|
||||
{
|
||||
log_err("Decoding base32 2FA secret failed, base32 output array is too small");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set current bit in the base32 output array
|
||||
base32[base32_pos] |= b_bit << (4-((i*8+j)%5));
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over base32 output array and replace each byte with its
|
||||
// corresponding character in the base32 alphabet
|
||||
for(size_t i = 0; i <= base32_pos; i++)
|
||||
base32[i] = b32[(uint8_t)base32[i]];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t last_code = 0;
|
||||
bool verifyTOTP(const uint32_t incode)
|
||||
{
|
||||
// Decode base32 secret
|
||||
uint8_t decoded_secret[RFC6238_SECRET_LEN];
|
||||
if(!decode_base32_to_uint8_array(config.webserver.api.totp_secret.v.s, decoded_secret, sizeof(decoded_secret)))
|
||||
return false;
|
||||
|
||||
// Get current time
|
||||
const time_t now = time(NULL);
|
||||
|
||||
// Verify code for the previous, the current and the next time step
|
||||
for(int i = -1; i <= 1; i++)
|
||||
{
|
||||
const uint32_t gencode = totp(decoded_secret, sizeof(decoded_secret), now + i*RFC6238_X);
|
||||
|
||||
// Verify code
|
||||
// RFC 6238 (section 4.2): If the calculated value matches the value
|
||||
// provided by the user, then the user is authenticated
|
||||
// RFC 6238 (section 4.3): The server MUST NOT accept a TOTP value
|
||||
// generated more than 30 seconds in the future
|
||||
// RFC 6238 (section 4.3): The server MUST NOT accept a TOTP value
|
||||
// generated more than 30 seconds in the past
|
||||
// RFC 6238 (section 4.3): The server MUST NOT accept a TOTP value
|
||||
// it accepted previously
|
||||
if(gencode == incode)
|
||||
{
|
||||
if(gencode == last_code)
|
||||
{
|
||||
log_warn("2FA code has already been used (%i, %u), please wait %ld seconds",
|
||||
i, gencode, RFC6238_X - (now % RFC6238_X));
|
||||
return false;
|
||||
}
|
||||
log_info("2FA code verified successfully at %i", i);
|
||||
last_code = gencode;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Print TOTP code to stdout (for CLI use)
|
||||
int printTOTP(void)
|
||||
{
|
||||
// Decode base32 secret
|
||||
uint8_t decoded_secret[RFC6238_SECRET_LEN];
|
||||
if(!decode_base32_to_uint8_array(config.webserver.api.totp_secret.v.s, decoded_secret, sizeof(decoded_secret)))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
// Get current time
|
||||
const time_t now = time(NULL);
|
||||
const uint32_t code = totp(decoded_secret, sizeof(decoded_secret), now);
|
||||
|
||||
printf("%u\n", code);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
// A QR code may be generated from the data using
|
||||
// otpauth://totp/<label>?secret=<secret>&issuer=<issuer>&algorithm=<algorithm>&digits=<digits>&period=<period>
|
||||
int generateTOTP(struct ftl_conn *api)
|
||||
{
|
||||
// Generate random secret using the system's random number generator
|
||||
uint8_t random_secret[RFC6238_SECRET_LEN];
|
||||
if(getrandom(random_secret, sizeof(random_secret), 0) < (ssize_t)sizeof(random_secret))
|
||||
{
|
||||
return send_json_error(api, 500, "internal_error", "Failed to generate random secret", strerror(errno));
|
||||
}
|
||||
|
||||
// Encode base32 secret
|
||||
const size_t base32_len = sizeof(random_secret)*8/5+1;
|
||||
char *base32 = calloc(base32_len, sizeof(char));
|
||||
if(!encode_uint8_t_array_to_base32(random_secret, sizeof(random_secret), base32, base32_len))
|
||||
return send_json_error(api, 500, "internal_error", "Failed to encode secret", "Check FTL.log for details");
|
||||
|
||||
// Create JSON object
|
||||
cJSON *tjson = cJSON_CreateObject();
|
||||
JSON_REF_STR_IN_OBJECT(tjson, "type", "totp");
|
||||
JSON_REF_STR_IN_OBJECT(tjson, "label", "Pi-hole%20API%3Api.hole");
|
||||
JSON_REF_STR_IN_OBJECT(tjson, "issuer", "Pi-hole%20API");
|
||||
JSON_REF_STR_IN_OBJECT(tjson, "algorithm", "SHA1");
|
||||
JSON_ADD_NUMBER_TO_OBJECT(tjson, "digits", RFC6238_DIGITS);
|
||||
JSON_ADD_NUMBER_TO_OBJECT(tjson, "period", RFC6238_X);
|
||||
JSON_ADD_NUMBER_TO_OBJECT(tjson, "offset", RFC6238_T0);
|
||||
JSON_COPY_STR_TO_OBJECT(tjson, "secret", base32);
|
||||
free(base32);
|
||||
base32 = NULL;
|
||||
|
||||
// Generate a few codes to show the user how to use the secret
|
||||
cJSON *codes = cJSON_CreateArray();
|
||||
for(int i = 0; i < 5; i++)
|
||||
{
|
||||
const time_t now = time(NULL) + (i-1)*RFC6238_X;
|
||||
const uint32_t code = totp(random_secret, sizeof(random_secret), now);
|
||||
JSON_ADD_NUMBER_TO_ARRAY(codes, code);
|
||||
}
|
||||
JSON_ADD_ITEM_TO_OBJECT(tjson, "codes", codes);
|
||||
|
||||
// Send JSON response
|
||||
cJSON *json = cJSON_CreateObject();
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "totp", tjson);
|
||||
JSON_SEND_OBJECT(json);
|
||||
}
|
||||
|
||||
#if 0
|
||||
#define RFC6238_TESTKEY "12345678901234567890"
|
||||
#define RFC6238_TESTTIME 59
|
||||
#define RFC6238_TESTTOTP 94287082
|
||||
|
||||
int test_totp(struct ftl_conn *api)
|
||||
{
|
||||
// Generate base32 secret
|
||||
uint8_t secret[sizeof(RFC6238_TESTKEY)-1];
|
||||
for(size_t i = 0; i < sizeof(secret); i++)
|
||||
secret[i] = RFC6238_TESTKEY[i];
|
||||
|
||||
// Encode base32 secret
|
||||
char base32_secret[sizeof(secret)*8/5+1];
|
||||
if(!encode_uint8_t_array_to_base32(secret, sizeof(secret), base32_secret, sizeof(base32_secret)))
|
||||
return false;
|
||||
|
||||
// Decode base32 secret
|
||||
uint8_t decoded_secret[sizeof(RFC6238_TESTKEY)-1];
|
||||
if(!decode_base32_to_uint8_array(base32_secret, decoded_secret, sizeof(decoded_secret)))
|
||||
return false;
|
||||
|
||||
// Get test time
|
||||
const time_t now = RFC6238_TESTTIME;
|
||||
|
||||
// Verify code for the current time and the previous and next time step
|
||||
for(int i = -1; i <= 1; i++)
|
||||
{
|
||||
// Verify code
|
||||
const time_t t = now + i*RFC6238_X;
|
||||
if(totp(decoded_secret, sizeof(decoded_secret), t) == RFC6238_TESTTOTP)
|
||||
log_info("Code is valid for time %ld", t);
|
||||
}
|
||||
|
||||
return 200;
|
||||
}
|
||||
#endif
|
||||
@@ -9,6 +9,7 @@
|
||||
# Please see LICENSE file for your rights under this license.
|
||||
|
||||
set(sources
|
||||
2fa.c
|
||||
action.c
|
||||
api_helper.h
|
||||
api.h
|
||||
|
||||
@@ -36,6 +36,7 @@ static struct {
|
||||
// Note: The order of appearance matters here, more specific URIs have to
|
||||
// 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/totp", "", generateTOTP, { false, true, 0 }, false, 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", "/{client}", api_list, { false, true, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
|
||||
|
||||
@@ -80,6 +80,11 @@ int api_auth(struct ftl_conn *api);
|
||||
void delete_all_sessions(void);
|
||||
int api_auth_sessions(struct ftl_conn *api);
|
||||
|
||||
// 2FA methods
|
||||
bool verifyTOTP(const uint32_t code);
|
||||
int generateTOTP(struct ftl_conn *api);
|
||||
int printTOTP(void);
|
||||
|
||||
// Documentation methods
|
||||
int api_docs(struct ftl_conn *api);
|
||||
|
||||
|
||||
@@ -268,6 +268,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
|
||||
{
|
||||
cJSON *session = JSON_NEW_OBJECT();
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
|
||||
JSON_ADD_NULL_TO_OBJECT(session, "sid");
|
||||
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", -1);
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
|
||||
@@ -279,6 +280,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
|
||||
{
|
||||
cJSON *session = JSON_NEW_OBJECT();
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
|
||||
JSON_REF_STR_IN_OBJECT(session, "sid", auth_data[user_id].sid);
|
||||
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", auth_data[user_id].valid_until - now);
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
|
||||
@@ -288,6 +290,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
|
||||
// No valid session
|
||||
cJSON *session = JSON_NEW_OBJECT();
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "valid", false);
|
||||
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
|
||||
JSON_ADD_NULL_TO_OBJECT(session, "sid");
|
||||
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", -1);
|
||||
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
|
||||
@@ -522,6 +525,33 @@ int api_auth(struct ftl_conn *api)
|
||||
if(response_correct || empty_password)
|
||||
{
|
||||
// Accepted
|
||||
|
||||
// Check possible 2FA token
|
||||
if(strlen(config.webserver.api.totp_secret.v.s) > 0)
|
||||
{
|
||||
// Get 2FA token from payload
|
||||
cJSON *json_twofa;
|
||||
if((json_twofa = cJSON_GetObjectItemCaseSensitive(api->payload.json, "totp")) == NULL)
|
||||
{
|
||||
const char *message = "No 2FA token found in JSON payload";
|
||||
log_debug(DEBUG_API, "API auth error: %s", message);
|
||||
return send_json_error(api, 400,
|
||||
"bad_request",
|
||||
message,
|
||||
NULL);
|
||||
}
|
||||
|
||||
if(!verifyTOTP(json_twofa->valueint))
|
||||
{
|
||||
// 2FA token is invalid
|
||||
return send_json_error(api, 401,
|
||||
"unauthorized",
|
||||
"Invalid 2FA token",
|
||||
NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// Find unused authentication slot
|
||||
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
|
||||
{
|
||||
// Expired slow, mark as unused
|
||||
|
||||
+6
-1
@@ -511,7 +511,12 @@ static int api_config_get(struct ftl_conn *api)
|
||||
conf_item->k, conf_item->t);
|
||||
continue;
|
||||
}
|
||||
JSON_ADD_ITEM_TO_OBJECT(leaf, "value", val);
|
||||
|
||||
// Special case: write-only values
|
||||
if(conf_item->f & FLAG_WRITE_ONLY)
|
||||
JSON_ADD_ITEM_TO_OBJECT(leaf, "value", val);
|
||||
else
|
||||
JSON_REF_STR_IN_OBJECT(leaf, "value", "<write-only property>");
|
||||
|
||||
// Add default value
|
||||
cJSON *dval = addJSONvalue(conf_item->t, &conf_item->d);
|
||||
|
||||
@@ -125,6 +125,26 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: 'common.yaml#/components/errors/unauthorized'
|
||||
totp:
|
||||
get:
|
||||
summary: Suggest new TOTP credentials
|
||||
tags:
|
||||
- Authentication
|
||||
operationId: "get_auth_totp"
|
||||
description: Suggest new TOTP credentials for two-factor authentication (2FA)
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: 'auth.yaml#/components/schemas/totp'
|
||||
'401':
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: 'common.yaml#/components/errors/unauthorized'
|
||||
|
||||
schemas:
|
||||
session:
|
||||
@@ -144,10 +164,14 @@ components:
|
||||
- valid
|
||||
- sid
|
||||
- validity
|
||||
- totp
|
||||
properties:
|
||||
valid:
|
||||
type: boolean
|
||||
description: Valid session indicator (client is authenticated)
|
||||
totp:
|
||||
type: boolean
|
||||
description: Whether 2FA (TOTP) is enabled on this Pi-hole
|
||||
sid:
|
||||
type: string
|
||||
description: Session ID
|
||||
@@ -205,6 +229,33 @@ components:
|
||||
valid_until: 1580000300
|
||||
remote_addr: "192.168.0.34"
|
||||
user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0"
|
||||
totp:
|
||||
type: object
|
||||
description: TOTP secret suggestion
|
||||
properties:
|
||||
totp:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
issuer:
|
||||
type: string
|
||||
algorithm:
|
||||
type: string
|
||||
digits:
|
||||
type: integer
|
||||
period:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
secret:
|
||||
type: string
|
||||
codes:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
|
||||
errors:
|
||||
bad_request:
|
||||
@@ -232,6 +283,7 @@ components:
|
||||
challenge: null
|
||||
session:
|
||||
valid: true
|
||||
totp: false
|
||||
sid: null
|
||||
validity: 300
|
||||
no_login_required:
|
||||
@@ -240,6 +292,7 @@ components:
|
||||
challenge: null
|
||||
session:
|
||||
valid: true
|
||||
totp: false
|
||||
sid: null
|
||||
validity: -1
|
||||
login_required:
|
||||
@@ -248,6 +301,7 @@ components:
|
||||
challenge: "a2926b025bcc8618c632f81cd6cf7c37ee051c08aab74b565fd5126350fcd056"
|
||||
session:
|
||||
valid: false
|
||||
totp: false
|
||||
sid: null
|
||||
validity: -1
|
||||
login_failed:
|
||||
@@ -256,6 +310,7 @@ components:
|
||||
challenge: null
|
||||
session:
|
||||
valid: false
|
||||
totp: false
|
||||
sid: null
|
||||
validity: -1
|
||||
errors:
|
||||
|
||||
@@ -362,6 +362,8 @@ components:
|
||||
type: string
|
||||
pwhash:
|
||||
type: string
|
||||
totp_secret:
|
||||
type: string
|
||||
excludeClients:
|
||||
type: array
|
||||
items:
|
||||
@@ -616,6 +618,7 @@ components:
|
||||
prettyJSON: false
|
||||
password: "********"
|
||||
pwhash: ''
|
||||
totp_secret: ''
|
||||
excludeClients: [ '1.2.3.4', 'localhost', 'fe80::345' ]
|
||||
excludeDomains: [ 'google.de', 'pi-hole.net' ]
|
||||
temp:
|
||||
|
||||
@@ -60,6 +60,9 @@ paths:
|
||||
/auth/sessions:
|
||||
$ref: 'auth.yaml#/components/paths/session_list'
|
||||
|
||||
/auth/totp:
|
||||
$ref: 'auth.yaml#/components/paths/totp'
|
||||
|
||||
/stats/summary:
|
||||
$ref: 'stats.yaml#/components/paths/summary'
|
||||
|
||||
|
||||
+15
@@ -46,6 +46,8 @@
|
||||
#include "zip/gzip.h"
|
||||
// teleporter functions
|
||||
#include "zip/teleporter.h"
|
||||
// printTOTP()
|
||||
#include "api/api.h"
|
||||
|
||||
// defined in dnsmasq.c
|
||||
extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal);
|
||||
@@ -252,6 +254,19 @@ void parse_args(int argc, char* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set config option through CLI
|
||||
if(argc == 2 && strcmp(argv[1], "--totp") == 0)
|
||||
{
|
||||
cli_mode = true;
|
||||
log_ctrl(false, false);
|
||||
readFTLconf(&config, false);
|
||||
log_ctrl(false, true);
|
||||
clear_debug_flags(); // No debug printing wanted
|
||||
exit(printTOTP());
|
||||
}
|
||||
|
||||
|
||||
// Create teleporter archive through CLI
|
||||
if(argc == 2 && strcmp(argv[1], "--teleporter") == 0)
|
||||
{
|
||||
|
||||
+8
-1
@@ -431,6 +431,10 @@ int get_config_from_CLI(const char *key, const bool quiet)
|
||||
if(strcmp(item->k, key) != 0)
|
||||
continue;
|
||||
|
||||
// Skip write-only options
|
||||
if(item->f & FLAG_WRITE_ONLY)
|
||||
continue;
|
||||
|
||||
// This is the config option we are looking for
|
||||
conf_item = item;
|
||||
break;
|
||||
@@ -449,7 +453,10 @@ int get_config_from_CLI(const char *key, const bool quiet)
|
||||
return conf_item->v.b ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
|
||||
// Print value
|
||||
writeTOMLvalue(stdout, -1, conf_item->t, &conf_item->v);
|
||||
if(conf_item-> f & FLAG_WRITE_ONLY)
|
||||
puts("<write-only property>");
|
||||
else
|
||||
writeTOMLvalue(stdout, -1, conf_item->t, &conf_item->v);
|
||||
putchar('\n');
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
|
||||
+9
-2
@@ -888,12 +888,19 @@ void initConfig(struct config *conf)
|
||||
conf->webserver.api.pwhash.d.s = (char*)"";
|
||||
|
||||
conf->webserver.api.password.k = "webserver.api.password";
|
||||
conf->webserver.api.password.h = "Pi-hole web interface and API password. When set to something different than \""PASSWORD_VALUE"\", this porperty will compute the corresponding password hash to set webserver.api.pwhash";
|
||||
conf->webserver.api.password.h = "Pi-hole web interface and API password. When set to something different than \""PASSWORD_VALUE"\", this property will compute the corresponding password hash to set webserver.api.pwhash";
|
||||
conf->webserver.api.password.a = cJSON_CreateStringReference("<valid Pi-hole password>");
|
||||
conf->webserver.api.password.t = CONF_PASSWORD;
|
||||
conf->webserver.api.password.f = FLAG_WRITE_ONLY;
|
||||
conf->webserver.api.password.f = FLAG_PSEUDO_ITEM;
|
||||
conf->webserver.api.password.d.s = (char*)"";
|
||||
|
||||
conf->webserver.api.totp_secret.k = "webserver.api.totp_secret";
|
||||
conf->webserver.api.totp_secret.h = "Pi-hole 2FA TOTP secret. When set to something different than \"""\", 2FA authentication will be enforced for the API and the web interface. This setting is write-only, you can not read the secret back.";
|
||||
conf->webserver.api.totp_secret.a = cJSON_CreateStringReference("<valid TOTP secret (20 Bytes in Base32 encoding)>");
|
||||
conf->webserver.api.totp_secret.t = CONF_STRING;
|
||||
conf->webserver.api.totp_secret.f = FLAG_WRITE_ONLY | FLAG_INVALIDATE_SESSIONS;
|
||||
conf->webserver.api.totp_secret.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");
|
||||
|
||||
+4
-2
@@ -81,8 +81,9 @@ enum conf_type {
|
||||
|
||||
#define FLAG_RESTART_DNSMASQ (1 << 0)
|
||||
#define FLAG_ADVANCED_SETTING (1 << 1)
|
||||
#define FLAG_WRITE_ONLY (1 << 2)
|
||||
#define FLAG_PSEUDO_ITEM (1 << 2)
|
||||
#define FLAG_INVALIDATE_SESSIONS (1 << 3)
|
||||
#define FLAG_WRITE_ONLY (1 << 4)
|
||||
|
||||
struct conf_item {
|
||||
const char *k; // item Key
|
||||
@@ -214,7 +215,8 @@ struct config {
|
||||
struct conf_item localAPIauth;
|
||||
struct conf_item prettyJSON;
|
||||
struct conf_item pwhash;
|
||||
struct conf_item password; // This is a write-only item
|
||||
struct conf_item password; // This is a pseudo-item
|
||||
struct conf_item totp_secret; // This is a write-only item
|
||||
struct conf_item excludeClients;
|
||||
struct conf_item excludeDomains;
|
||||
struct {
|
||||
|
||||
@@ -60,7 +60,7 @@ bool writeFTLtoml(const bool verbose)
|
||||
struct conf_item *conf_item = get_conf_item(&config, i);
|
||||
|
||||
// Skip write-only items
|
||||
if(conf_item->f & FLAG_WRITE_ONLY)
|
||||
if(conf_item->f & FLAG_PSEUDO_ITEM)
|
||||
continue;
|
||||
|
||||
// Get path depth
|
||||
|
||||
@@ -555,6 +555,14 @@
|
||||
# <valid Pi-hole password hash>
|
||||
pwhash = ""
|
||||
|
||||
# Pi-hole 2FA TOTP secret. When set to something different than "", 2FA authentication
|
||||
# will be enforced for the API and the web interface. This setting is write-only, you
|
||||
# can not read the secret back.
|
||||
#
|
||||
# Possible values are:
|
||||
# <valid TOTP secret (20 Bytes in Base32 encoding)>
|
||||
totp_secret = ""
|
||||
|
||||
# Array of clients to be excluded from certain API responses
|
||||
# Example: [ "192.168.2.56", "fe80::341", "localhost" ]
|
||||
#
|
||||
|
||||
@@ -1231,7 +1231,7 @@
|
||||
@test "API authorization (without password): No login required" {
|
||||
run bash -c 'curl -s 127.0.0.1:8080/api/auth'
|
||||
printf "%s\n" "${lines[@]}"
|
||||
[[ ${lines[0]} == '{"challenge":null,"session":{"valid":true,"sid":null,"validity":-1}}' ]]
|
||||
[[ ${lines[0]} == '{"challenge":null,"session":{"valid":true,"totp":false,"sid":null,"validity":-1}}' ]]
|
||||
}
|
||||
|
||||
@test "API authorization (with password): FTL challenges us" {
|
||||
|
||||
Reference in New Issue
Block a user