mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralsec.git
synced 2025-11-01 19:27:59 +00:00
Adding Google Authenticator
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
36
src/RESTAPI/RESTAPI_subtotp_handler.cpp
Normal file
36
src/RESTAPI/RESTAPI_subtotp_handler.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
29
src/RESTAPI/RESTAPI_subtotp_handler.h
Normal file
29
src/RESTAPI/RESTAPI_subtotp_handler.h
Normal 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:
|
||||
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
src/RESTAPI/RESTAPI_totp_handler.cpp
Normal file
35
src/RESTAPI/RESTAPI_totp_handler.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
31
src/RESTAPI/RESTAPI_totp_handler.h
Normal file
31
src/RESTAPI/RESTAPI_totp_handler.h
Normal 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:
|
||||
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (...) {
|
||||
|
||||
|
||||
@@ -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
5
src/TotpCache.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Created by stephane bourque on 2022-01-31.
|
||||
//
|
||||
|
||||
#include "TotpCache.h"
|
||||
149
src/TotpCache.h
Normal file
149
src/TotpCache.h
Normal 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
|
||||
@@ -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."};
|
||||
}
|
||||
|
||||
|
||||
341
src/seclibs/cpptotp/bytes.cpp
Normal file
341
src/seclibs/cpptotp/bytes.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
70
src/seclibs/cpptotp/bytes.h
Normal file
70
src/seclibs/cpptotp/bytes.h
Normal 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
104
src/seclibs/cpptotp/otp.cpp
Normal 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
37
src/seclibs/cpptotp/otp.h
Normal 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
|
||||
224
src/seclibs/cpptotp/sha1.cpp
Normal file
224
src/seclibs/cpptotp/sha1.cpp
Normal 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
|
||||
34
src/seclibs/cpptotp/sha1.h
Normal file
34
src/seclibs/cpptotp/sha1.h
Normal 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
|
||||
Reference in New Issue
Block a user