From 67c1de5adf934426e7a538cd07208ae08844d4cb Mon Sep 17 00:00:00 2001 From: DannyAAM Date: Thu, 6 Oct 2022 01:22:11 +0800 Subject: [PATCH 01/10] make digits & period of TOTP configurable --- README.md | 6 ++++++ server.cc | 21 ++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3249e87..5d758e2 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ itself. A config file can look like: ``` nthreads = 4; +auth_per_second = 2; +totp_generations = 1; secret = "some-random-string-that-is-relatively-long-used-for-cookie-minting"; webs = ( { @@ -31,6 +33,8 @@ webs = ( username = "user1"; password = "password123!"; totp = "base32otpsecretgoeshere"; + digits = 6; + period = 30; duration = 3600; } ); @@ -43,6 +47,8 @@ webs = ( username = "user2"; password = "password123456"; totp = "base32otpsecretgoeshere"; + digits = 6; + period = 30; duration = 7200; } ); diff --git a/server.cc b/server.cc index f639f29..f2ed7ed 100644 --- a/server.cc +++ b/server.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,8 @@ typedef std::unordered_map StrMap; struct cred_t { std::string password, totp; // Pass and TOTP (binary) + uint8_t digits; // Digits of TOTP + uint32_t period; // Period of TOTP unsigned sduration; // Duration of a valid session (seconds) }; @@ -130,7 +133,7 @@ class AuthenticationServer { if (rl->check(req->ip64)) { std::cerr << "Rate limit hit for ip id " << req->ip64 << std::endl; return "Status: 429\r\nContent-Type: text/plain\r\n" - "Content-Length: 34\r\n\r\nToo many requests, request blocked"; + "Content-Length: 34\r\n\r\nToo many requests, request blocked"; } rl->consume(req->ip64); @@ -143,7 +146,7 @@ class AuthenticationServer { // Validate the authentication to issue a cookie or throw an error if (wcfg->users.count(user) && wcfg->users.at(user).password == pass && - totp_valid(wcfg->users.at(user).totp, totp, totp_generations)) { + totp_valid(wcfg->users.at(user), totp, totp_generations)) { std::cerr << "Login with user " << user << " successful" << std::endl; @@ -193,15 +196,15 @@ class AuthenticationServer { cthread.join(); } - bool totp_valid(std::string key, unsigned input, unsigned generations) { - uint32_t ct = time(0) / 30UL; + bool totp_valid(cred_t user, unsigned input, unsigned generations) { + uint32_t ct = time(0) / user.period; for (int i = -(signed)generations; i < (signed)generations; i++) - if (totp_calc(key, ct + i) == input) + if (totp_calc(user.totp, user.digits, ct + i) == input) return true; return false; } - static unsigned totp_calc(std::string key, uint32_t epoch) { + static unsigned totp_calc(std::string key, uint8_t digits, uint32_t epoch) { // Key comes in binary format already! // Concatenate the epoc in big endian fashion uint8_t msg [8] = { @@ -220,7 +223,7 @@ class AuthenticationServer { // The result is a substr in hash at that offset (pick 32 bits) uint32_t value = (hash[off] << 24) | (hash[off+1] << 16) | (hash[off+2] << 8) | hash[off+3]; value &= 0x7fffffff; - return value % 1000000; + return value % ((uint32_t)pow(10, digits)); } // Receives requests and processes them by replying via a side http call. @@ -340,6 +343,8 @@ int main(int argc, char **argv) { config_setting_t *user = config_setting_get_member(userentry, "username"); config_setting_t *pass = config_setting_get_member(userentry, "password"); config_setting_t *totp = config_setting_get_member(userentry, "totp"); + config_setting_t *digi = config_setting_get_member(userentry, "digits"); + config_setting_t *peri = config_setting_get_member(userentry, "period"); config_setting_t *durt = config_setting_get_member(userentry, "duration"); if (!user || !pass || !totp || !durt) @@ -348,6 +353,8 @@ int main(int argc, char **argv) { wentry.users[config_setting_get_string(user)] = cred_t { .password = config_setting_get_string(pass), .totp = b32dec(b32pad(config_setting_get_string(totp))), + .digits = !digi ? 6 : (uint8_t)config_setting_get_int(digi), + .period = !peri ? 30UL : (uint32_t)config_setting_get_int(peri), .sduration = (unsigned)config_setting_get_int(durt), }; } From 6e7d35412c090d05cd079ac0873ab236038f26ad Mon Sep 17 00:00:00 2001 From: DannyAAM Date: Fri, 7 Oct 2022 00:04:26 +0800 Subject: [PATCH 02/10] add support of totp only host --- README.md | 7 +++++-- server.cc | 40 ++++++++++++++++++++++++++++++---------- templ.py | 5 +++-- templates/gradient.html | 4 ++-- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5d758e2..a10255b 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ webs = ( { hostname = "anotherweb.com"; template = "customtemplate"; + totp_only = true; users = ( { - username = "user2"; - password = "password123456"; + username = "more-like-remainder-here"; totp = "base32otpsecretgoeshere"; digits = 6; period = 30; @@ -65,6 +65,9 @@ at startup, and this will cause logout of all users on a server restart. for each entry a list of users can be defined with their username, password and totp secret (base32 encoded string). The duration is the cookie lifetime in seconds. +For TOTP Only mode, all users (totp secrets) are tried, any one matches will pass +the authentication. + The authenticator supports templates. By default there's one called "gradient", but more can be added. The templates are built in, so one must recompile the binary to add templates. diff --git a/server.cc b/server.cc index f2ed7ed..fb3ff92 100644 --- a/server.cc +++ b/server.cc @@ -52,6 +52,7 @@ struct cred_t { struct web_t { std::string webtemplate; // Template to use + bool totp_only; // Only TOTP, without username/password std::unordered_map users; // User to credential }; @@ -111,6 +112,20 @@ class AuthenticationServer { return (hmac == hmac_calc); } + bool validate_cred(std::string user, std::string pass, unsigned totp, const web_t *wcfg) { + if (wcfg->totp_only) { + for (auto pair : wcfg->users) + if (totp_valid(pair.second, totp, totp_generations)) + return true; + return false; + } + else { + return wcfg->users.count(user) && + wcfg->users.at(user).password == pass && + totp_valid(wcfg->users.at(user), totp, totp_generations); + } + } + std::string process_req(web_req *req, const web_t *wcfg) { std::string rpage = req->getvars["follow_page"]; if (rpage.empty()) @@ -142,12 +157,12 @@ class AuthenticationServer { std::string user = req->postvars["username"]; std::string pass = req->postvars["password"]; unsigned totp = atoi(req->postvars["totp"].c_str()); - std::cerr << "Login attempt for user " << user << std::endl; + if (wcfg->totp_only) + std::cerr << "Login attempt at " << req->host << std::endl; + else + std::cerr << "Login attempt for user " << user << " at " << req->host << std::endl; // Validate the authentication to issue a cookie or throw an error - if (wcfg->users.count(user) && - wcfg->users.at(user).password == pass && - totp_valid(wcfg->users.at(user), totp, totp_generations)) { - + if (validate_cred(user, pass, totp, wcfg)) { std::cerr << "Login with user " << user << " successful" << std::endl; // Render a redirect page to the redirect address (+cookie) @@ -164,7 +179,7 @@ class AuthenticationServer { return "Status: 500\r\nContent-Type: text/plain\r\n" "Content-Length: 23\r\n\r\nCould not find template"; else { - std::string page = templates.at(wcfg->webtemplate)(req->host, rpage, lerror); + std::string page = templates.at(wcfg->webtemplate)(req->host, rpage, wcfg->totp_only, lerror); return "Status: 200\r\nContent-Type: text/html\r\n" "Content-Length: " + std::to_string(page.size()) + "\r\n\r\n" + page; } @@ -331,12 +346,15 @@ int main(int argc, char **argv) { config_setting_t *webentry = config_setting_get_elem(webs_cfg, i); config_setting_t *hostname = config_setting_get_member(webentry, "hostname"); config_setting_t *wtemplate = config_setting_get_member(webentry, "template"); + config_setting_t *totp_only = config_setting_get_member(webentry, "totp_only"); config_setting_t *users_cfg = config_setting_lookup(webentry, "users"); if (!webentry || !hostname || !wtemplate || !users_cfg) RET_ERR("hostname, template and users must be present in the web group"); - web_t wentry = { .webtemplate = config_setting_get_string(wtemplate)}; + web_t wentry = { + .webtemplate = config_setting_get_string(wtemplate), + .totp_only = !totp_only ? false : config_setting_get_bool(totp_only) == CONFIG_TRUE, }; for (int j = 0; j < config_setting_length(users_cfg); j++) { config_setting_t *userentry = config_setting_get_elem(users_cfg, j); @@ -347,11 +365,13 @@ int main(int argc, char **argv) { config_setting_t *peri = config_setting_get_member(userentry, "period"); config_setting_t *durt = config_setting_get_member(userentry, "duration"); - if (!user || !pass || !totp || !durt) - RET_ERR("username, password, totp and duration must be present in the user group"); + if (!wentry.totp_only && !pass) + RET_ERR("either set web group to TOTP only mode or password must be present in the user group"); + if (!user || !totp || !durt) + RET_ERR("username, totp and duration must be present in the user group"); wentry.users[config_setting_get_string(user)] = cred_t { - .password = config_setting_get_string(pass), + .password = !pass ? "" : config_setting_get_string(pass), .totp = b32dec(b32pad(config_setting_get_string(totp))), .digits = !digi ? 6 : (uint8_t)config_setting_get_int(digi), .period = !peri ? 30UL : (uint32_t)config_setting_get_int(peri), diff --git a/templ.py b/templ.py index cc5a672..2873265 100755 --- a/templ.py +++ b/templ.py @@ -5,7 +5,7 @@ # Produce a usable header assetsh = b"#include \n#include \n" -assetsh += b"typedef std::string(*t_templatefn)(std::string, std::string, bool);\n" +assetsh += b"typedef std::string(*t_templatefn)(std::string, std::string, bool, bool);\n" assetsh += b"extern const std::unordered_map templates;\n" # Read template HTML files and generate templates.cc asset @@ -17,9 +17,10 @@ cont = cont.replace(b"\\", b"\\\\").replace(b'"', b'\\"').replace(b"\n", b"\\n") cont = cont.replace(b"{{hostname}}", b'" + hostname + "') cont = cont.replace(b"{{follow_page}}", b'" + follow_page + "') + cont = re.sub(b"{{nototponly}}(.*){{/nototponly}}", b'" + (totp_only ? "" : "\\1") + "', cont) cont = re.sub(b"{{loginfailed}}(.*){{/loginfailed}}", b'" + (err ? "\\1" : "") + "', cont) - assets += b"std::string login_%d(std::string hostname, std::string follow_page, bool err) {\n" % i + assets += b"std::string login_%d(std::string hostname, std::string follow_page, bool totp_only, bool err) {\n" % i assets += b"return \"" + cont + b"\";\n}\n" fnentries.append(b" {\"%s\", %s},\n" % (f.split(".")[0].encode("utf-8"), b"login_%d" % i)) diff --git a/templates/gradient.html b/templates/gradient.html index 8916b88..d80da26 100644 --- a/templates/gradient.html +++ b/templates/gradient.html @@ -70,8 +70,8 @@