Adding Google Authenticator

This commit is contained in:
stephb9959
2022-01-31 13:56:01 -08:00
parent 01f457dd0c
commit a9bd44b3b2
25 changed files with 1229 additions and 65 deletions

View File

@@ -76,7 +76,10 @@ add_executable( owsec
src/framework/RESTAPI_protocol.h
src/framework/StorageClass.h
src/framework/uCentral_Protocol.h
src/framework/qrcodegen.hpp src/framework/qrcodegen.cpp
src/seclibs/qrcode/qrcodegen.hpp src/seclibs/qrcode/qrcodegen.cpp
src/seclibs/cpptotp/bytes.cpp src/seclibs/cpptotp/bytes.h
src/seclibs/cpptotp/otp.cpp src/seclibs/cpptotp/otp.h
src/seclibs/cpptotp/sha1.cpp src/seclibs/cpptotp/sha1.h
src/RESTObjects/RESTAPI_SecurityObjects.h src/RESTObjects/RESTAPI_SecurityObjects.cpp
src/RESTObjects/RESTAPI_ProvObjects.cpp src/RESTObjects/RESTAPI_ProvObjects.h
src/RESTObjects/RESTAPI_GWobjects.h src/RESTObjects/RESTAPI_GWobjects.cpp
@@ -119,7 +122,7 @@ add_executable( owsec
src/storage/orm_actionLinks.cpp src/storage/orm_actionLinks.h
src/storage/orm_avatar.cpp src/storage/orm_avatar.h
src/SpecialUserHelpers.h
src/RESTAPI/RESTAPI_db_helpers.h src/storage/orm_logins.cpp src/storage/orm_logins.h)
src/RESTAPI/RESTAPI_db_helpers.h src/storage/orm_logins.cpp src/storage/orm_logins.h src/RESTAPI/RESTAPI_totp_handler.cpp src/RESTAPI/RESTAPI_totp_handler.h src/TotpCache.cpp src/TotpCache.h src/RESTAPI/RESTAPI_subtotp_handler.cpp src/RESTAPI/RESTAPI_subtotp_handler.h)
if(NOT SMALL_BUILD)
target_link_libraries(owsec PUBLIC

2
build
View File

@@ -1 +1 @@
192
234

View File

@@ -31,6 +31,7 @@
#include "AuthService.h"
#include "SMSSender.h"
#include "ActionLinkManager.h"
#include "TotpCache.h"
namespace OpenWifi {
class Daemon *Daemon::instance_ = nullptr;
@@ -48,6 +49,7 @@ namespace OpenWifi {
ActionLinkManager(),
SMTPMailerService(),
RESTAPI_RateLimiter(),
TotpCache(),
AuthService()
});
}

View File

@@ -7,6 +7,7 @@
#include "SMTPMailerService.h"
#include "framework/MicroService.h"
#include "AuthService.h"
#include "TotpCache.h"
namespace OpenWifi {
@@ -39,18 +40,18 @@ namespace OpenWifi {
}
bool MFAServer::SendChallenge(const SecurityObjects::UserInfoAndPolicy &UInfo, const std::string &Method, const std::string &Challenge) {
if(Method=="sms" && SMSSender()->Enabled() && !UInfo.userinfo.userTypeProprietaryInfo.mobiles.empty()) {
if(Method==MFAMETHODS::SMS && SMSSender()->Enabled() && !UInfo.userinfo.userTypeProprietaryInfo.mobiles.empty()) {
std::string Message = "This is your login code: " + Challenge + " Please enter this in your login screen.";
return SMSSender()->Send(UInfo.userinfo.userTypeProprietaryInfo.mobiles[0].number, Message);
}
if(Method=="email" && SMTPMailerService()->Enabled() && !UInfo.userinfo.email.empty()) {
} else if(Method==MFAMETHODS::EMAIL && SMTPMailerService()->Enabled() && !UInfo.userinfo.email.empty()) {
MessageAttributes Attrs;
Attrs[RECIPIENT_EMAIL] = UInfo.userinfo.email;
Attrs[LOGO] = AuthService::GetLogoAssetURI();
Attrs[SUBJECT] = "Login validation code";
Attrs[CHALLENGE_CODE] = Challenge;
return SMTPMailerService()->SendMessage(UInfo.userinfo.email, "verification_code.txt", Attrs);
} else if(Method==MFAMETHODS::AUTHENTICATOR && !UInfo.userinfo.userTypeProprietaryInfo.authenticatorSecret.empty()) {
return true;
}
return false;
@@ -77,7 +78,10 @@ namespace OpenWifi {
}
auto answer = ChallengeResponse->get("answer").toString();
if(Hint->second.Answer!=answer) {
if(Hint->second.Method==MFAMETHODS::AUTHENTICATOR &&
!TotpCache()->ValidateCode(Hint->second.UInfo.userinfo.userTypeProprietaryInfo.authenticatorSecret,answer)) {
return false;
} else if(Hint->second.Answer!=answer) {
return false;
}
@@ -87,12 +91,15 @@ namespace OpenWifi {
}
bool MFAServer::MethodEnabled(const std::string &Method) {
if(Method=="sms")
if(Method==MFAMETHODS::SMS)
return SMSSender()->Enabled();
if(Method=="email")
if(Method==MFAMETHODS::EMAIL)
return SMTPMailerService()->Enabled();
if(Method==MFAMETHODS::AUTHENTICATOR)
return true;
return false;
}

View File

@@ -10,6 +10,17 @@
#include "RESTObjects/RESTAPI_SecurityObjects.h"
namespace OpenWifi {
namespace MFAMETHODS {
inline const static std::string SMS{"sms"};
inline const static std::string EMAIL{"email"};
inline const static std::string AUTHENTICATOR{"authenticator"};
inline const static std::vector<std::string> Methods{ SMS, EMAIL, AUTHENTICATOR };
inline bool Validate(const std::string &M) {
return std::find(cbegin(Methods), cend(Methods),M)!=Methods.end();
}
}
struct MFACacheEntry {
SecurityObjects::UserInfoAndPolicy UInfo;
std::string Answer;
@@ -17,6 +28,7 @@ namespace OpenWifi {
std::string Method;
};
typedef std::map<std::string,MFACacheEntry> MFAChallengeCache;
class MFAServer : public SubSystemServer{

View File

@@ -22,6 +22,8 @@
#include "RESTAPI/RESTAPI_subusers_handler.h"
#include "RESTAPI/RESTAPI_validate_sub_token_handler.h"
#include "RESTAPI/RESTAPI_submfa_handler.h"
#include "RESTAPI/RESTAPI_totp_handler.h"
#include "RESTAPI/RESTAPI_subtotp_handler.h"
namespace OpenWifi {
@@ -44,7 +46,9 @@ namespace OpenWifi {
RESTAPI_suboauth2_handler,
RESTAPI_subuser_handler,
RESTAPI_subusers_handler,
RESTAPI_submfa_handler
RESTAPI_submfa_handler,
RESTAPI_totp_handler,
RESTAPI_subtotp_handler
>(Path, Bindings, L, S,TransactionId);
}

View File

@@ -0,0 +1,36 @@
//
// Created by stephane bourque on 2022-01-31.
//
#include "RESTAPI_subtotp_handler.h"
#include "TotpCache.h"
namespace OpenWifi {
void RESTAPI_subtotp_handler::DoGet() {
auto Reset = GetBoolParameter("reset",false);
std::string QRCode;
if(TotpCache()->StartValidation(UserInfo_.userinfo,true,QRCode,Reset)) {
return SendFileContent(QRCode, "image/svg+xml","qrcode.svg");
}
return BadRequest(RESTAPI::Errors::InvalidCommand);
}
void RESTAPI_subtotp_handler::DoPut() {
auto Value = GetParameter("value","");
auto nextIndex = GetParameter("index",0);
bool moreCodes=false;
if(TotpCache()->ContinueValidation(UserInfo_.userinfo,true,Value,nextIndex,moreCodes)) {
Poco::JSON::Object Answer;
Answer.set("nextIndex", nextIndex);
Answer.set("moreCodes", moreCodes);
return ReturnObject(Answer);
}
return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters);
}
}

View File

@@ -0,0 +1,29 @@
//
// Created by stephane bourque on 2022-01-31.
//
#include "framework/MicroService.h"
namespace OpenWifi {
class RESTAPI_subtotp_handler : public RESTAPIHandler {
public:
RESTAPI_subtotp_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L, RESTAPI_GenericServer &Server, uint64_t TransactionId, bool Internal)
: RESTAPIHandler(bindings, L,
std::vector<std::string>
{
Poco::Net::HTTPRequest::HTTP_GET,
Poco::Net::HTTPRequest::HTTP_PUT,
Poco::Net::HTTPRequest::HTTP_OPTIONS
},
Server,
TransactionId,
Internal) {}
static const std::list<const char *> PathName() { return std::list<const char *>{"/api/v1/subtotp"}; };
void DoGet() final;
void DoPost() final {};
void DoDelete() final {};
void DoPut() final;
private:
};
}

View File

@@ -9,6 +9,8 @@
#include "ACLProcessor.h"
#include "AuthService.h"
#include "RESTAPI/RESTAPI_db_helpers.h"
#include "MFAServer.h"
#include "TotpCache.h"
namespace OpenWifi {
@@ -96,6 +98,12 @@ namespace OpenWifi {
if(NewUser.name.empty())
NewUser.name = NewUser.email;
// You cannot enable MFA during user creation
NewUser.userTypeProprietaryInfo.mfa.enabled = false;
NewUser.userTypeProprietaryInfo.mfa.method = "";
NewUser.userTypeProprietaryInfo.mobiles.clear();
NewUser.userTypeProprietaryInfo.authenticatorSecret.clear();
if(!StorageService()->SubDB().CreateUser(NewUser.email, NewUser)) {
Logger_.information(Poco::format("Could not add user '%s'.",NewUser.email));
return BadRequest(RESTAPI::Errors::RecordNotCreated);
@@ -193,34 +201,46 @@ namespace OpenWifi {
}
if(RawObject->has("userTypeProprietaryInfo")) {
bool ChangingMFA = NewUser.userTypeProprietaryInfo.mfa.enabled && !Existing.userTypeProprietaryInfo.mfa.enabled;
if(NewUser.userTypeProprietaryInfo.mfa.enabled) {
if (!NewUser.userTypeProprietaryInfo.mfa.method.empty() &&
!MFAMETHODS::Validate(NewUser.userTypeProprietaryInfo.mfa.method)) {
return BadRequest(RESTAPI::Errors::BadMFAMethod);
}
bool ChangingMFA =
NewUser.userTypeProprietaryInfo.mfa.enabled && !Existing.userTypeProprietaryInfo.mfa.enabled;
Existing.userTypeProprietaryInfo.mfa.enabled = NewUser.userTypeProprietaryInfo.mfa.enabled;
auto PropInfo = RawObject->get("userTypeProprietaryInfo");
if (ChangingMFA && NewUser.userTypeProprietaryInfo.mfa.method == MFAMETHODS::SMS) {
auto PInfo = PropInfo.extract<Poco::JSON::Object::Ptr>();
if (PInfo->isArray("mobiles")) {
Existing.userTypeProprietaryInfo.mobiles = NewUser.userTypeProprietaryInfo.mobiles;
}
if(ChangingMFA && !NewUser.userTypeProprietaryInfo.mobiles.empty() && !SMSSender()->IsNumberValid(NewUser.userTypeProprietaryInfo.mobiles[0].number,UserInfo_.userinfo.email)){
if (NewUser.userTypeProprietaryInfo.mobiles.empty() ||
!SMSSender()->IsNumberValid(NewUser.userTypeProprietaryInfo.mobiles[0].number,
UserInfo_.userinfo.email)) {
return BadRequest(RESTAPI::Errors::NeedMobileNumber);
}
if(NewUser.userTypeProprietaryInfo.mfa.method=="sms" && Existing.userTypeProprietaryInfo.mobiles.empty()) {
return BadRequest(RESTAPI::Errors::NeedMobileNumber);
} else if (ChangingMFA && NewUser.userTypeProprietaryInfo.mfa.method == MFAMETHODS::AUTHENTICATOR) {
std::string Secret;
Existing.userTypeProprietaryInfo.mobiles.clear();
if(Existing.userTypeProprietaryInfo.authenticatorSecret.empty() && TotpCache()->CompleteValidation(UserInfo_.userinfo,true,Secret)) {
Existing.userTypeProprietaryInfo.authenticatorSecret = Secret;
} else if (!Existing.userTypeProprietaryInfo.authenticatorSecret.empty()) {
// we allow someone to use their old secret
} else {
return BadRequest(RESTAPI::Errors::AuthenticatorVerificationIncomplete);
}
if(!NewUser.userTypeProprietaryInfo.mfa.method.empty()) {
if(NewUser.userTypeProprietaryInfo.mfa.method!="email" && NewUser.userTypeProprietaryInfo.mfa.method!="sms" ) {
return BadRequest(RESTAPI::Errors::BadMFAMethod);
} else if (ChangingMFA && NewUser.userTypeProprietaryInfo.mfa.method == MFAMETHODS::EMAIL) {
// nothing to do for email.
Existing.userTypeProprietaryInfo.mobiles.clear();
}
Existing.userTypeProprietaryInfo.mfa.method = NewUser.userTypeProprietaryInfo.mfa.method;
}
if(Existing.userTypeProprietaryInfo.mfa.enabled && Existing.userTypeProprietaryInfo.mfa.method.empty()) {
return BadRequest(RESTAPI::Errors::BadMFAMethod);
Existing.userTypeProprietaryInfo.mfa.enabled = true;
} else {
Existing.userTypeProprietaryInfo.mobiles.clear();
Existing.userTypeProprietaryInfo.mfa.enabled = false;
}
}

View File

@@ -0,0 +1,35 @@
//
// Created by stephane bourque on 2022-01-31.
//
#include "RESTAPI_totp_handler.h"
#include "TotpCache.h"
namespace OpenWifi {
void RESTAPI_totp_handler::DoGet() {
auto Reset = GetBoolParameter("reset",false);
std::string QRCode;
if(TotpCache()->StartValidation(UserInfo_.userinfo,false,QRCode,Reset)) {
return SendFileContent(QRCode, "image/svg+xml","qrcode.svg");
}
return BadRequest(RESTAPI::Errors::InvalidCommand);
}
void RESTAPI_totp_handler::DoPut() {
auto Value = GetParameter("value","");
auto nextIndex = GetParameter("index",0);
bool moreCodes=false;
if(TotpCache()->ContinueValidation(UserInfo_.userinfo,false,Value,nextIndex,moreCodes)) {
Poco::JSON::Object Answer;
Answer.set("nextIndex", nextIndex);
Answer.set("moreCodes", moreCodes);
return ReturnObject(Answer);
}
return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters);
}
}

View File

@@ -0,0 +1,31 @@
//
// Created by stephane bourque on 2022-01-31.
//
#pragma once
#include "framework/MicroService.h"
namespace OpenWifi {
class RESTAPI_totp_handler : public RESTAPIHandler {
public:
RESTAPI_totp_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L, RESTAPI_GenericServer &Server, uint64_t TransactionId, bool Internal)
: RESTAPIHandler(bindings, L,
std::vector<std::string>
{
Poco::Net::HTTPRequest::HTTP_GET,
Poco::Net::HTTPRequest::HTTP_PUT,
Poco::Net::HTTPRequest::HTTP_OPTIONS
},
Server,
TransactionId,
Internal) {}
static const std::list<const char *> PathName() { return std::list<const char *>{"/api/v1/totp"}; };
void DoGet() final;
void DoPost() final {};
void DoDelete() final {};
void DoPut() final;
private:
};
}

View File

@@ -9,6 +9,8 @@
#include "ACLProcessor.h"
#include "AuthService.h"
#include "RESTAPI/RESTAPI_db_helpers.h"
#include "MFAServer.h"
#include "TotpCache.h"
namespace OpenWifi {
@@ -74,7 +76,6 @@ namespace OpenWifi {
SecurityObjects::UserInfo NewUser;
RESTAPI_utils::from_request(NewUser,*Request);
if(NewUser.userRole == SecurityObjects::UNKNOWN) {
return BadRequest(RESTAPI::Errors::InvalidUserRole);
}
@@ -103,6 +104,12 @@ namespace OpenWifi {
if(NewUser.name.empty())
NewUser.name = NewUser.email;
// You cannot enable MFA during user creation
NewUser.userTypeProprietaryInfo.mfa.enabled = false;
NewUser.userTypeProprietaryInfo.mfa.method = "";
NewUser.userTypeProprietaryInfo.mobiles.clear();
NewUser.userTypeProprietaryInfo.authenticatorSecret.clear();
if(!StorageService()->UserDB().CreateUser(NewUser.email,NewUser)) {
Logger_.information(Poco::format("Could not add user '%s'.",NewUser.email));
return BadRequest(RESTAPI::Errors::RecordNotCreated);
@@ -158,7 +165,6 @@ namespace OpenWifi {
}
}
// The only valid things to change are: changePassword, name,
AssignIfPresent(RawObject,"name", Existing.name);
AssignIfPresent(RawObject,"description", Existing.description);
@@ -204,35 +210,46 @@ namespace OpenWifi {
}
if(RawObject->has("userTypeProprietaryInfo")) {
bool ChangingMFA = NewUser.userTypeProprietaryInfo.mfa.enabled && !Existing.userTypeProprietaryInfo.mfa.enabled;
if(NewUser.userTypeProprietaryInfo.mfa.enabled) {
if (!NewUser.userTypeProprietaryInfo.mfa.method.empty() &&
!MFAMETHODS::Validate(NewUser.userTypeProprietaryInfo.mfa.method)) {
return BadRequest(RESTAPI::Errors::BadMFAMethod);
}
bool ChangingMFA =
NewUser.userTypeProprietaryInfo.mfa.enabled && !Existing.userTypeProprietaryInfo.mfa.enabled;
Existing.userTypeProprietaryInfo.mfa.enabled = NewUser.userTypeProprietaryInfo.mfa.enabled;
auto PropInfo = RawObject->get("userTypeProprietaryInfo");
if (ChangingMFA && NewUser.userTypeProprietaryInfo.mfa.method == MFAMETHODS::SMS) {
auto PInfo = PropInfo.extract<Poco::JSON::Object::Ptr>();
if (PInfo->isArray("mobiles")) {
Existing.userTypeProprietaryInfo.mobiles = NewUser.userTypeProprietaryInfo.mobiles;
}
if(ChangingMFA && !NewUser.userTypeProprietaryInfo.mobiles.empty() && !SMSSender()->IsNumberValid(NewUser.userTypeProprietaryInfo.mobiles[0].number,UserInfo_.userinfo.email)){
if (NewUser.userTypeProprietaryInfo.mobiles.empty() ||
!SMSSender()->IsNumberValid(NewUser.userTypeProprietaryInfo.mobiles[0].number,
UserInfo_.userinfo.email)) {
return BadRequest(RESTAPI::Errors::NeedMobileNumber);
}
if(NewUser.userTypeProprietaryInfo.mfa.method=="sms" && Existing.userTypeProprietaryInfo.mobiles.empty()) {
return BadRequest(RESTAPI::Errors::NeedMobileNumber);
} else if (ChangingMFA && NewUser.userTypeProprietaryInfo.mfa.method == MFAMETHODS::AUTHENTICATOR) {
std::string Secret;
Existing.userTypeProprietaryInfo.mobiles.clear();
if(Existing.userTypeProprietaryInfo.authenticatorSecret.empty() && TotpCache()->CompleteValidation(UserInfo_.userinfo,false,Secret)) {
Existing.userTypeProprietaryInfo.authenticatorSecret = Secret;
} else if (!Existing.userTypeProprietaryInfo.authenticatorSecret.empty()) {
// we allow someone to use their old secret
} else {
return BadRequest(RESTAPI::Errors::AuthenticatorVerificationIncomplete);
}
if(!NewUser.userTypeProprietaryInfo.mfa.method.empty()) {
if(NewUser.userTypeProprietaryInfo.mfa.method!="email" && NewUser.userTypeProprietaryInfo.mfa.method!="sms" ) {
return BadRequest(RESTAPI::Errors::BadMFAMethod);
} else if (ChangingMFA && NewUser.userTypeProprietaryInfo.mfa.method == MFAMETHODS::EMAIL) {
// nothing to do for email.
Existing.userTypeProprietaryInfo.mobiles.clear();
}
Existing.userTypeProprietaryInfo.mfa.method = NewUser.userTypeProprietaryInfo.mfa.method;
}
if(Existing.userTypeProprietaryInfo.mfa.enabled && Existing.userTypeProprietaryInfo.mfa.method.empty()) {
return BadRequest(RESTAPI::Errors::BadMFAMethod);
Existing.userTypeProprietaryInfo.mfa.enabled = true;
} else {
Existing.userTypeProprietaryInfo.mobiles.clear();
Existing.userTypeProprietaryInfo.mfa.enabled = false;
}
}

View File

@@ -172,12 +172,14 @@ namespace OpenWifi::SecurityObjects {
void UserLoginLoginExtensions::to_json(Poco::JSON::Object &Obj) const {
field_to_json(Obj, "mobiles", mobiles);
field_to_json(Obj, "mfa", mfa);
field_to_json(Obj, "authenticatorSecret", authenticatorSecret);
}
bool UserLoginLoginExtensions::from_json(Poco::JSON::Object::Ptr &Obj) {
try {
field_from_json(Obj,"mobiles",mobiles);
field_from_json(Obj,"mfa",mfa);
field_from_json(Obj, "authenticatorSecret", authenticatorSecret);
return true;
} catch (...) {

View File

@@ -82,6 +82,7 @@ namespace OpenWifi {
struct UserLoginLoginExtensions {
std::vector<MobilePhoneNumber> mobiles;
struct MfaAuthInfo mfa;
std::string authenticatorSecret;
void to_json(Poco::JSON::Object &Obj) const;
bool from_json(Poco::JSON::Object::Ptr &Obj);

5
src/TotpCache.cpp Normal file
View File

@@ -0,0 +1,5 @@
//
// Created by stephane bourque on 2022-01-31.
//
#include "TotpCache.h"

149
src/TotpCache.h Normal file
View File

@@ -0,0 +1,149 @@
//
// Created by stephane bourque on 2022-01-31.
//
#ifndef OWSEC_TOTPCACHE_H
#define OWSEC_TOTPCACHE_H
#include "framework/MicroService.h"
#include "seclibs/cpptotp/bytes.h"
#include "seclibs/qrcode/qrcodegen.hpp"
#include "seclibs/cpptotp/otp.h"
namespace OpenWifi {
class TotpCache : public SubSystemServer {
public:
struct Entry {
bool Subscriber=false;
uint64_t Start = 0;
uint64_t Done = 0 ;
uint64_t Verifications = 0 ;
std::string Secret;
std::string QRCode;
};
static auto instance() {
static auto instance = new TotpCache;
return instance;
}
static std::string GenerateSecret(uint Size) {
std::string R;
for(;Size;Size--) {
R += (char) MicroService::instance().Random(33,127);
}
std::string Base32Secret = CppTotp::Bytes::toBase32( CppTotp::Bytes::ByteString{ (const u_char *)R.c_str()});
return Base32Secret;
}
static std::string GenerateQRCode(const std::string &Secret, const std::string &email) {
std::string uri{
"otpauth://totp/" +
MicroService::instance().ConfigGetString("topt.issuer","OpenWiFi") +
":" +
email +
"?secret=" + Secret +
"&issuer=" + MicroService::instance().ConfigGetString("topt.issuer","OpenWiFi")
};
qrcodegen::QrCode qr0 = qrcodegen::QrCode::encodeText(uri.c_str(), qrcodegen::QrCode::Ecc::MEDIUM);
std::string svg = qrcodegen::toSvgString(qr0, 4); // See QrCodeGeneratorDemo
return svg;
}
static bool ValidateCode( const std::string &Secret, const std::string &Code) {
uint64_t Now = std::time(nullptr);
uint32_t p = CppTotp::totp(CppTotp::Bytes::ByteString{ (const u_char *)Secret.c_str()}, Now, 0, 30, 6);
char buffer[16];
sprintf(buffer,"%06u",p);
return Code == buffer;
}
int Start() override {
return 0;
};
void Stop() override {
};
inline bool StartValidation(const SecurityObjects::UserInfo &User, bool Subscriber, std::string & QRCode, bool Reset) {
auto Hint = Cache_.find(User.id);
if(Hint!=Cache_.end() && Hint->second.Subscriber==Subscriber) {
if(Reset) {
Hint->second.Subscriber = Subscriber;
Hint->second.Start = std::time(nullptr);
Hint->second.Done = 0;
Hint->second.Verifications = 0;
Hint->second.Secret = GenerateSecret(32);
Hint->second.QRCode = QRCode = GenerateQRCode(Hint->second.Secret, User.email);
} else {
QRCode = Hint->second.QRCode;
}
return true;
}
auto Secret = GenerateSecret(32);
QRCode = GenerateQRCode(Hint->second.Secret, User.email);
Entry E{ .Subscriber = Subscriber,
.Start = (uint64_t )std::time(nullptr),
.Done = 0,
.Verifications = 0,
.Secret = Secret,
.QRCode = QRCode
};
Cache_[User.id] = E;
return true;
}
inline bool ContinueValidation(const SecurityObjects::UserInfo &User, bool Subscriber, const std::string & code, uint64_t &NextIndex, bool &MoreCodes) {
auto Hint = Cache_.find(User.id);
uint64_t Now = std::time(nullptr);
if(Hint!=Cache_.end() && Subscriber==Hint->second.Subscriber && (Now-Hint->second.Start)<(15*60)) {
if (NextIndex == 1 && Hint->second.Verifications == 0 && ValidateCode(Hint->second.Secret, code)) {
NextIndex++;
Hint->second.Verifications++;
MoreCodes = true;
return true;
}
if (NextIndex == 2 && Hint->second.Verifications == 1 && ValidateCode(Hint->second.Secret, code)) {
MoreCodes = false;
Hint->second.Done = Now;
return true;
}
return false;
}
Cache_.erase(Hint);
return false;
}
inline bool CompleteValidation(const SecurityObjects::UserInfo &User, bool Subscriber, std::string & Secret) {
auto Hint = Cache_.find(User.id);
uint64_t Now = std::time(nullptr);
if(Hint!=Cache_.end() && Subscriber==Hint->second.Subscriber && (Now-Hint->second.Start)<(15*60) && Hint->second.Done!=0) {
Secret = Hint->second.Secret;
Cache_.erase(Hint);
return true;
}
return false;
}
private:
std::map<std::string,Entry> Cache_;
TotpCache() noexcept:
SubSystemServer("TOTP-system", "TOTP-SVR", "totp") {
}
};
inline auto TotpCache() { return TotpCache::instance(); }
}
#endif //OWSEC_TOTPCACHE_H

View File

@@ -60,5 +60,6 @@ namespace OpenWifi::RESTAPI::Errors {
static const std::string InsufficientAccessRights{"Insufficient access rights to complete the operation."};
static const std::string ExpiredToken{"Token has expired, user must login."};
static const std::string SubscriberMustExist{"Subscriber must exist."};
static const std::string AuthenticatorVerificationIncomplete{"Authenticator validation is not complete."};
}

View File

@@ -0,0 +1,341 @@
/**
* @file bytes.cpp
*
* @brief Byte-related operations.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#include "bytes.h"
#include <iostream>
#include <stdexcept>
#include <cassert>
#include <cstdlib>
namespace CppTotp
{
namespace Bytes
{
void clearByteString(ByteString * bstr)
{
volatile Byte * bs = const_cast<volatile Byte *>(bstr->data());
for (size_t i = 0; i < bstr->size(); ++i)
{
bs[i] = Byte(0);
}
}
void swizzleByteStrings(ByteString * target, ByteString * source)
{
clearByteString(target);
target->assign(*source);
clearByteString(source);
}
static char nibbleToLCHex(uint8_t nib)
{
if (nib < 0xa)
{
return static_cast<char>(nib + '0');
}
else if (nib < 0x10)
{
return static_cast<char>((nib - 10) + 'a');
}
else
{
assert(0 && "not actually a nibble");
return '\0';
}
}
static uint8_t hexToNibble(char c)
{
if (c >= '0' && c <= '9')
{
return static_cast<uint8_t>(c - '0');
}
else if (c >= 'A' && c <= 'F')
{
return static_cast<uint8_t>(c - 'A' + 10);
}
else if (c >= 'a' && c <= 'f')
{
return static_cast<uint8_t>(c - 'a' + 10);
}
else
{
assert(0 && "not actually a hex digit");
return 0xff;
}
}
std::string toHexString(const ByteString & bstr)
{
std::string ret;
for (Byte b : bstr)
{
ret.push_back(nibbleToLCHex((b >> 4) & 0x0F));
ret.push_back(nibbleToLCHex((b >> 0) & 0x0F));
}
return ret;
}
ByteString fromHexStringSkipUnknown(const std::string & str)
{
std::string hstr;
for (char c : str)
{
if (
(c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f')
)
{
hstr.push_back(c);
}
// ignore otherwise
}
if (hstr.size() % 2 != 0)
{
throw std::invalid_argument("hex string (unknown characters ignored) length not divisible by 2");
}
ByteString ret;
for (size_t i = 0; i < hstr.size(); i += 2)
{
uint8_t top = hexToNibble(hstr[i+0]);
uint8_t btm = hexToNibble(hstr[i+1]);
ret.push_back((top << 4) | btm);
}
return ret;
}
Bytes::ByteString u32beToByteString(uint32_t num)
{
Bytes::ByteString ret;
ret.push_back((num >> 24) & 0xFF);
ret.push_back((num >> 16) & 0xFF);
ret.push_back((num >> 8) & 0xFF);
ret.push_back((num >> 0) & 0xFF);
return ret;
}
Bytes::ByteString u64beToByteString(uint64_t num)
{
Bytes::ByteString left = u32beToByteString((num >> 32) & 0xFFFFFFFF);
Bytes::ByteString right = u32beToByteString((num >> 0) & 0xFFFFFFFF);
return left + right;
}
static ByteString b32ChunkToBytes(const std::string & str)
{
ByteString ret;
uint64_t whole = 0x00;
size_t padcount = 0;
size_t finalcount;
if (str.length() != 8)
{
throw std::invalid_argument("incorrect length of base32 chunk");
}
size_t i;
for (i = 0; i < 8; ++i)
{
char c = str[i];
uint64_t bits;
if (c == '=')
{
bits = 0;
++padcount;
}
else if (padcount > 0)
{
throw std::invalid_argument("padding character followed by non-padding character");
}
else if (c >= 'A' && c <= 'Z')
{
bits = static_cast<Byte>(c - 'A');
}
else if (c >= '2' && c <= '7')
{
bits = static_cast<Byte>(c - '2' + 26);
}
else
{
throw std::invalid_argument("not a base32 character: " + std::string(1, c));
}
// shift into the chunk
whole |= (bits << ((7-i)*5));
}
switch (padcount)
{
case 0:
finalcount = 5;
break;
case 1:
finalcount = 4;
break;
case 3:
finalcount = 3;
break;
case 4:
finalcount = 2;
break;
case 6:
finalcount = 1;
break;
default:
throw std::invalid_argument("invalid number of padding characters");
}
for (i = 0; i < finalcount; ++i)
{
// shift out of the chunk
ret.push_back(static_cast<Byte>((whole >> ((4-i)*8)) & 0xFF));
}
return ret;
}
static inline uint64_t u64(uint8_t n)
{
return static_cast<uint64_t>(n);
}
static std::string bytesToB32Chunk(const ByteString & bs)
{
if (bs.size() < 1 || bs.size() > 5)
{
throw std::invalid_argument("need a chunk of at least 1 and at most 5 bytes");
}
uint64_t whole = 0x00;
size_t putchars = 2;
std::string ret;
// shift into the chunk
whole |= (u64(bs[0]) << 32);
if (bs.size() > 1)
{
whole |= (u64(bs[1]) << 24);
putchars += 2; // at least 4
}
if (bs.size() > 2)
{
whole |= (u64(bs[2]) << 16);
++putchars; // at least 5
}
if (bs.size() > 3)
{
whole |= (u64(bs[3]) << 8);
putchars += 2; // at least 7
}
if (bs.size() > 4)
{
whole |= u64(bs[4]);
++putchars; // at least 8
}
size_t i;
for (i = 0; i < putchars; ++i)
{
// shift out of the chunk
Byte val = (whole >> ((7-i)*5)) & 0x1F;
// map bits to base32
if (val < 26)
{
ret.push_back(static_cast<char>(val + 'A'));
}
else
{
ret.push_back(static_cast<char>(val - 26 + '2'));
}
}
// pad
for (i = putchars; i < 8; ++i)
{
ret.push_back('=');
}
return ret;
}
ByteString fromBase32(const std::string & b32str)
{
if (b32str.size() % 8 != 0)
{
throw std::invalid_argument("base32 string length not divisible by 8");
}
ByteString ret;
for (size_t i = 0; i < b32str.size(); i += 8)
{
std::string sub(b32str, i, 8);
ByteString chk = b32ChunkToBytes(sub);
ret.append(chk);
}
return ret;
}
ByteString fromUnpaddedBase32(const std::string & b32str)
{
std::string newstr = b32str;
while (newstr.size() % 8 != 0)
{
newstr.push_back('=');
}
return fromBase32(newstr);
}
std::string toBase32(const ByteString & bs)
{
std::string ret;
size_t i, j, len;
for (j = 0; j < bs.size() / 5; ++j)
{
i = j * 5;
ByteString sub(bs, i, 5);
std::string chk = bytesToB32Chunk(sub);
ret.append(chk);
}
i = j * 5;
len = bs.size() - i;
if (len > 0)
{
// block of size < 5 remains
ByteString sub(bs, i, std::string::npos);
std::string chk = bytesToB32Chunk(sub);
ret.append(chk);
}
return ret;
}
}
}

View File

@@ -0,0 +1,70 @@
/**
* @file bytes.h
*
* @brief Byte-related operations.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#ifndef __CPPTOTP_BYTES_H__
#define __CPPTOTP_BYTES_H__
#include <string>
#include <cstdint>
namespace CppTotp
{
namespace Bytes
{
/** The type of a single byte. */
typedef uint8_t Byte;
/** The type of a byte string. */
typedef std::basic_string<Byte> ByteString;
/** Deletes the contents of a byte string. */
void clearByteString(ByteString * bstr);
/** Replaces target with source, clearing as much as possible. */
void swizzleByteStrings(ByteString * target, ByteString * source);
/** Converts a byte string into a hex string. */
std::string toHexString(const ByteString & bstr);
/** Converts an unsigned 32-bit integer into a corresponding byte string. */
ByteString u32beToByteString(uint32_t num);
/** Converts an unsigned 64-bit integer into a corresponding byte string. */
ByteString u64beToByteString(uint64_t num);
/** Converts a Base32 string into the correspoding byte string. */
ByteString fromBase32(const std::string & b32str);
/**
* Converts a potentially unpadded Base32 string into the corresponding byte
* string.
*/
ByteString fromUnpaddedBase32(const std::string & b32str);
/** Converts byte string into the corresponding Base32 string. */
std::string toBase32(const ByteString & b32str);
/** Deletes the contets of a byte string on destruction. */
class ByteStringDestructor
{
private:
/** The byte string to clear. */
ByteString * m_bs;
public:
ByteStringDestructor(ByteString * bs) : m_bs(bs) {}
~ByteStringDestructor() { clearByteString(m_bs); }
};
}
}
#endif

104
src/seclibs/cpptotp/otp.cpp Normal file
View File

@@ -0,0 +1,104 @@
/**
* @file otp.cpp
*
* @brief Implementations of one-time-password-related functions.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#include "otp.h"
#include <iostream>
#include <cassert>
#include <cinttypes>
#include <cstring>
namespace CppTotp
{
Bytes::ByteString hmacSha1_64(const Bytes::ByteString & key, const Bytes::ByteString & msg)
{
return hmacSha1(key, msg, 64);
}
//uint32_t hotp(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t digitCount, HmacFunc hmacf)
uint32_t hotp(const Bytes::ByteString & key, uint64_t counter, size_t digitCount, HmacFunc hmacf)
{
Bytes::ByteString msg = Bytes::u64beToByteString(counter);
Bytes::ByteStringDestructor dmsg(&msg);
Bytes::ByteString hmac = hmacf(key, msg);
Bytes::ByteStringDestructor dhmac(&hmac);
uint32_t digits10 = 1;
for (size_t i = 0; i < digitCount; ++i)
{
digits10 *= 10;
}
// fetch the offset (from the last nibble)
uint8_t offset = hmac[hmac.size()-1] & 0x0F;
// fetch the four bytes from the offset
Bytes::ByteString fourWord = hmac.substr(offset, 4);
Bytes::ByteStringDestructor dfourWord(&fourWord);
// turn them into a 32-bit integer
uint32_t ret =
(fourWord[0] << 24) |
(fourWord[1] << 16) |
(fourWord[2] << 8) |
(fourWord[3] << 0)
;
// snip off the MSB (to alleviate signed/unsigned troubles)
// and calculate modulo digit count
return (ret & 0x7fffffff) % digits10;
}
uint32_t totp(const Bytes::ByteString & key, uint64_t timeNow, uint64_t timeStart, uint64_t timeStep, size_t digitCount, HmacFunc hmacf)
{
uint64_t timeValue = (timeNow - timeStart) / timeStep;
return hotp(key, timeValue, digitCount, hmacf);
}
}
#if TEST_OTP
int main(void)
{
using namespace CppTotp;
uint64_t start = 0;
uint64_t step = 30;
uint8_t digitsH = 6;
uint8_t digitsT = 8;
const Bytes::ByteString key = reinterpret_cast<const uint8_t *>("12345678901234567890");
std::cout
<< (hotp(key, 0, digitsH) == 755224)
<< (hotp(key, 1, digitsH) == 287082)
<< (hotp(key, 2, digitsH) == 359152)
<< (hotp(key, 3, digitsH) == 969429)
<< (hotp(key, 4, digitsH) == 338314)
<< (hotp(key, 5, digitsH) == 254676)
<< (hotp(key, 6, digitsH) == 287922)
<< (hotp(key, 7, digitsH) == 162583)
<< (hotp(key, 8, digitsH) == 399871)
<< (hotp(key, 9, digitsH) == 520489)
<< (totp(key, 59, start, step, digitsT) == 94287082)
<< (totp(key, 1111111109, start, step, digitsT) == 7081804)
<< (totp(key, 1111111111, start, step, digitsT) == 14050471)
<< (totp(key, 1234567890, start, step, digitsT) == 89005924)
<< (totp(key, 2000000000, start, step, digitsT) == 69279037)
<< (totp(key, 20000000000, start, step, digitsT) == 65353130)
<< std::endl;
const Bytes::ByteString tutestkey = reinterpret_cast<const uint8_t *>("HelloWorld");
std::cout << totp(tutestkey, time(NULL), 0, 30, 6) << std::endl;
return 0;
}
#endif

37
src/seclibs/cpptotp/otp.h Normal file
View File

@@ -0,0 +1,37 @@
/**
* @file otp.h
*
* @brief One-time-password-related functions.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#ifndef __CPPTOTP_OTP_H__
#define __CPPTOTP_OTP_H__
#include "bytes.h"
#include "sha1.h"
#include <cstdint>
namespace CppTotp
{
/** The 64-bit-blocksize variant of HMAC-SHA1. */
Bytes::ByteString hmacSha1_64(const Bytes::ByteString & key, const Bytes::ByteString & msg);
/**
* Calculate the HOTP value of the given key, message and digit count.
*/
//uint32_t hotp(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t digitCount = 6, HmacFunc hmac = hmacSha1_64);
uint32_t hotp(const Bytes::ByteString & key, uint64_t counter, size_t digitCount = 6, HmacFunc hmac = hmacSha1_64);
/**
* Calculate the TOTP value from the given parameters.
*/
uint32_t totp(const Bytes::ByteString & key, uint64_t timeNow, uint64_t timeStart, uint64_t timeStep, size_t digitCount = 6, HmacFunc hmac = hmacSha1_64);
}
#endif

View File

@@ -0,0 +1,224 @@
/**
* @file sha1.cpp
*
* @brief Implementation of the SHA-1 hash.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#include "bytes.h"
#include <iostream>
#include <cassert>
namespace CppTotp
{
static inline uint32_t lrot32(uint32_t num, uint8_t rotcount)
{
return (num << rotcount) | (num >> (32 - rotcount));
}
Bytes::ByteString sha1(const Bytes::ByteString & msg)
{
const size_t size_bytes = msg.size();
const uint64_t size_bits = size_bytes * 8;
Bytes::ByteString bstr = msg;
Bytes::ByteStringDestructor asplode(&bstr);
// the size of msg in bits is always even. adding the '1' bit will make
// it odd and therefore incongruent to 448 modulo 512, so we can get
// away with tacking on 0x80 and then the 0x00s.
bstr.push_back(0x80);
while (bstr.size() % (512/8) != (448/8))
{
bstr.push_back(0x00);
}
// append the size in bits (uint64be)
bstr.append(Bytes::u64beToByteString(size_bits));
assert(bstr.size() % (512/8) == 0);
// initialize the hash counters
uint32_t h0 = 0x67452301;
uint32_t h1 = 0xEFCDAB89;
uint32_t h2 = 0x98BADCFE;
uint32_t h3 = 0x10325476;
uint32_t h4 = 0xC3D2E1F0;
// for each 64-byte chunk
for (size_t i = 0; i < bstr.size()/64; ++i)
{
Bytes::ByteString chunk(bstr.begin() + i*64, bstr.begin() + (i+1)*64);
Bytes::ByteStringDestructor xplode(&chunk);
uint32_t words[80];
size_t j;
// 0-15: the chunk as a sequence of 32-bit big-endian integers
for (j = 0; j < 16; ++j)
{
words[j] =
(chunk[4*j + 0] << 24) |
(chunk[4*j + 1] << 16) |
(chunk[4*j + 2] << 8) |
(chunk[4*j + 3] << 0)
;
}
// 16-79: derivatives of 0-15
for (j = 16; j < 32; ++j)
{
// unoptimized
words[j] = lrot32(words[j-3] ^ words[j-8] ^ words[j-14] ^ words[j-16], 1);
}
for (j = 32; j < 80; ++j)
{
// Max Locktyuchin's optimization (SIMD)
words[j] = lrot32(words[j-6] ^ words[j-16] ^ words[j-28] ^ words[j-32], 2);
}
// initialize hash values for the round
uint32_t a = h0;
uint32_t b = h1;
uint32_t c = h2;
uint32_t d = h3;
uint32_t e = h4;
// the loop
for (j = 0; j < 80; ++j)
{
uint32_t f = 0, k = 0;
if (j < 20)
{
f = (b & c) | ((~ b) & d);
k = 0x5A827999;
}
else if (j < 40)
{
f = b ^ c ^ d;
k = 0x6ED9EBA1;
}
else if (j < 60)
{
f = (b & c) | (b & d) | (c & d);
k = 0x8F1BBCDC;
}
else if (j < 80)
{
f = b ^ c ^ d;
k = 0xCA62C1D6;
}
else
{
assert(0 && "how did I get here?");
}
uint32_t tmp = lrot32(a, 5) + f + e + k + words[j];
e = d;
d = c;
c = lrot32(b, 30);
b = a;
a = tmp;
}
// add that to the result so far
h0 += a;
h1 += b;
h2 += c;
h3 += d;
h4 += e;
}
// assemble the digest
Bytes::ByteString first = Bytes::u32beToByteString(h0);
Bytes::ByteStringDestructor x1(&first);
Bytes::ByteString second = Bytes::u32beToByteString(h1);
Bytes::ByteStringDestructor x2(&second);
Bytes::ByteString third = Bytes::u32beToByteString(h2);
Bytes::ByteStringDestructor x3(&third);
Bytes::ByteString fourth = Bytes::u32beToByteString(h3);
Bytes::ByteStringDestructor x4(&fourth);
Bytes::ByteString fifth = Bytes::u32beToByteString(h4);
Bytes::ByteStringDestructor x5(&fifth);
return first + second + third + fourth + fifth;
}
Bytes::ByteString hmacSha1(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t blockSize = 64);
Bytes::ByteString hmacSha1(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t blockSize)
{
Bytes::ByteString realKey = key;
Bytes::ByteStringDestructor asplode(&realKey);
if (realKey.size() > blockSize)
{
// resize by calculating hash
Bytes::ByteString newRealKey = sha1(realKey);
Bytes::swizzleByteStrings(&realKey, &newRealKey);
}
if (realKey.size() < blockSize)
{
// pad with zeroes
realKey.resize(blockSize, 0x00);
}
// prepare the pad keys
Bytes::ByteString innerPadKey = realKey;
Bytes::ByteStringDestructor xplodeI(&innerPadKey);
Bytes::ByteString outerPadKey = realKey;
Bytes::ByteStringDestructor xplodeO(&outerPadKey);
// transform the pad keys
for (size_t i = 0; i < realKey.size(); ++i)
{
innerPadKey[i] = innerPadKey[i] ^ 0x36;
outerPadKey[i] = outerPadKey[i] ^ 0x5c;
}
// sha1(outerPadKey + sha1(innerPadKey + msg))
Bytes::ByteString innerMsg = innerPadKey + msg;
Bytes::ByteStringDestructor xplodeIM(&innerMsg);
Bytes::ByteString innerHash = sha1(innerMsg);
Bytes::ByteStringDestructor xplodeIH(&innerHash);
Bytes::ByteString outerMsg = outerPadKey + innerHash;
Bytes::ByteStringDestructor xplodeOM(&outerMsg);
return sha1(outerMsg);
}
}
#if TEST_SHA1
int main(void)
{
using namespace CppTotp;
const uint8_t * strEmpty = reinterpret_cast<const uint8_t *>("");
const uint8_t * strDog = reinterpret_cast<const uint8_t *>("The quick brown fox jumps over the lazy dog");
const uint8_t * strCog = reinterpret_cast<const uint8_t *>("The quick brown fox jumps over the lazy cog");
const uint8_t * strKey = reinterpret_cast<const uint8_t *>("key");
Bytes::ByteString shaEmpty = sha1(Bytes::ByteString(strEmpty));
Bytes::ByteString shaDog = sha1(Bytes::ByteString(strDog));
Bytes::ByteString shaCog = sha1(Bytes::ByteString(strCog));
Bytes::ByteString hmacShaEmpty = hmacSha1(Bytes::ByteString(), Bytes::ByteString());
Bytes::ByteString hmacShaKeyDog = hmacSha1(strKey, strDog);
std::cout
<< (Bytes::toHexString(shaEmpty) == "da39a3ee5e6b4b0d3255bfef95601890afd80709") << std::endl
<< (Bytes::toHexString(shaDog) == "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12") << std::endl
<< (Bytes::toHexString(shaCog) == "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3") << std::endl
<< std::endl
<< (Bytes::toHexString(hmacShaEmpty) == "fbdb1d1b18aa6c08324b7d64b71fb76370690e1d") << std::endl
<< (Bytes::toHexString(hmacShaKeyDog) == "de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9") << std::endl
<< std::endl;
return 0;
}
#endif

View File

@@ -0,0 +1,34 @@
/**
* @file sha1.h
*
* @brief The SHA-1 hash function.
*
* @copyright The contents of this file have been placed into the public domain;
* see the file COPYING for more details.
*/
#ifndef __CPPTOTP_SHA1_H__
#define __CPPTOTP_SHA1_H__
#include "bytes.h"
namespace CppTotp
{
typedef Bytes::ByteString (*HmacFunc)(const Bytes::ByteString &, const Bytes::ByteString &);
/**
* Calculate the SHA-1 hash of the given message.
*/
Bytes::ByteString sha1(const Bytes::ByteString & msg);
/**
* Calculate the HMAC-SHA-1 hash of the given key/message pair.
*
* @note Most services assume a block size of 64.
*/
Bytes::ByteString hmacSha1(const Bytes::ByteString & key, const Bytes::ByteString & msg, size_t blockSize = 64);
}
#endif