Files
nuki_hub/lib/DuoAuthLibrary/src/DuoAuthLib.cpp
2025-02-16 23:30:27 +01:00

726 lines
22 KiB
C++

/**
*@license
*
*Copyright 2020 Cisco Systems, Inc. or its affiliates
*Modifications copyright (C) 2025 iranl / Nuki Hub
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/
/**
* @file DuoAuthLib.cpp
* @brief An Arduino Library to simplify the operations of performing Duo Multi-Factor Authentication within your ESP32 Micro Controller project.
*
* @url https://github.com/CiscoDevNet/Arduino-DuoAuthLibrary-ESP32
* @version 1.0.0
* @author Gary Oppel <gaoppel@cisco.com>
* @url https://github.com/technyon/nuki_hub/lib/DuoAuthLibrary
* @version 1.1.0
* @author iranl <25727444+iranl@users.noreply.github.com>
* Modified to enable using ESP32 CA Certificate bundle
*/
//Include DuoAuthLib Library Header
#include "DuoAuthLib.h"
// Include ESP32 MDEDTLS Library
#include "mbedtls/md.h"
// Load CA CRT BUNDLE
extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_bundle_start");
extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_bundle_end");
// Include ESP NetworkClientSecure Library
#include <NetworkClientSecure.h>
// Include ESP HTTPClient Library
#include <HTTPClient.h>
// Include ArduinoJSON Library
#include <ArduinoJson.h>
//----------------------------------------------------------------
//DuoAuthLib Constructor
//----------------------------------------------------------------
//Function that handles the creation and setup of instance
DuoAuthLib::DuoAuthLib()
{
//Initialize Return Variables
_duoApiStat = "";
_duoAuthTxId = "";
_duoApiAuthResponseResult = "";
_duoApiAuthResponseStatus = "";
_duoApiAuthResponseStatusMessage = "";
_duoApiFailureCode = 0;
_duoApiFailureMessage = "";
_duoPushType[0] = '\0';
_init = false;
_async = false;
}
//Function that handles the deletion/removals of instance
DuoAuthLib::~DuoAuthLib()
{
}
//----------------------------------------------------------------
//'begin(...)' - used to initialize and prepare for an Auth request.
void DuoAuthLib::begin(const char* duoApiHost, const char* duoApiIKey, const char* duoApiSKey, struct tm* timeInfo)
{
_timeinfo = timeInfo;
_duoHost = (char *)duoApiHost;
_duoIkey = (char *)duoApiIKey;
_duoSkey = (char *)duoApiSKey;
//Set Default HTTP Timeout to 30 Seconds
_httpTimeout = _defaultTimeout;
//Set Default IP Address to 0.0.0.0
strcpy(_ipAddress, "0.0.0.0");
//Set the library initialized to True
_init = true;
}
//----------------------------------------------------------------
//DuoAuthLib Public Methods
//----------------------------------------------------------------
//----------------------------------------------------------------
// Duo Ping API
// https://duo.com/docs/authapi#/ping
bool DuoAuthLib::ping()
{
if(!_init){
return false;
}
bool pingSuccess = false;
bool apiResponse = submitApiRequest(0, (char *)DUO_PING_API_PATH, "", (char *)"", (char *)"");
if(apiResponse == true){
if(_httpCode == 200){
pingSuccess = true;
}
}
return pingSuccess;
}
//----------------------------------------------------------------
// Duo Check API
// https://duo.com/docs/authapi#/check
bool DuoAuthLib::checkApiKey()
{
if(!_init){
return false;
}
bool duoAuthRequestResult = false;
if(getLocalTime(_timeinfo)){
char hmac_password[41];
char timeStringBuffer[TIME_BUF_STR_SIZE];
//Get Current time from timeinfo
strftime(timeStringBuffer, sizeof(timeStringBuffer), "%a, %d %b %Y %T %z", _timeinfo);
// Create empty char array to hold our string
char hmacPayload[SIGNATURE_PAYLOAD_BUFFER_SIZE + CHECK_AUTH_BUFFER_SIZE];
// Generate the required URL Request Contents for the DUO API Call
generateHmacPayload(hmacPayload, timeStringBuffer, (char *)"GET", _duoHost, DUO_CHECK_API_PATH, (char *)"");
// Generate the required URL Request Contents for the DUO API Call
calculateHmacSha1((char *)_duoSkey, hmacPayload, hmac_password);
bool apiResponse = submitApiRequest(0, timeStringBuffer, DUO_CHECK_API_PATH, hmac_password, (char *)"");
if(apiResponse == true){
bool processResult = processResponse(&_lastHttpResponse);
if(processResult == true){
duoAuthRequestResult = true;
}
}
}
return duoAuthRequestResult;
}
//----------------------------------------------------------------
// Duo Auth Status API
// https://duo.com/docs/authapi#/auth_status
bool DuoAuthLib::authStatus(String transactionId)
{
if(!_init){
return false;
}
bool duoAuthRequestResult = false;
if(transactionId.length() == 36){
char txId[37];
//Convert String to Character Array
transactionId.toCharArray(txId, 37);
if(getLocalTime(_timeinfo)){
char hmac_password[41];
char timeStringBuffer[TIME_BUF_STR_SIZE];
char authRequestContents[CHECK_AUTH_BUFFER_SIZE];
authRequestContents[0] = '\0';
//Get Current time from timeinfo
strftime(timeStringBuffer, sizeof(timeStringBuffer), "%a, %d %b %Y %T %z", _timeinfo);
addRequestParameter(authRequestContents, (char *)TRANSACTION_PARAM, txId, true);
// Create empty char array to hold our string
char hmacPayload[SIGNATURE_PAYLOAD_BUFFER_SIZE + CHECK_AUTH_BUFFER_SIZE];
// Generate the required URL Request Contents for the DUO API Call
generateHmacPayload(hmacPayload, timeStringBuffer, (char *)"GET", _duoHost, DUO_AUTHSTATUS_API_PATH, authRequestContents);
// Generate the required URL Request Contents for the DUO API Call
calculateHmacSha1((char *)_duoSkey, hmacPayload, hmac_password);
bool apiResponse = submitApiRequest(0, timeStringBuffer, DUO_AUTHSTATUS_API_PATH, hmac_password, authRequestContents);
if(apiResponse == true){
bool processResult = processResponse(&_lastHttpResponse);
if(processResult == true){
duoAuthRequestResult = true;
}
}
}
}
return duoAuthRequestResult;
}
//----------------------------------------------------------------
// Duo Auth API
// https://duo.com/docs/authapi#/auth
// https://duo.com/docs/authapi#authentication
bool DuoAuthLib::pushAuth(char* userName, bool async)
{
if(!_init){
return false;
}
bool authSuccess = false;
_async = async;
//Create new Buffer using the method below
int userStrLen = encodeUrlBufferSize(userName);
char encodedUsername[userStrLen];
encodeUrl(encodedUsername, userName);
return performAuth(encodedUsername, (char*)"", PUSH);
}
//----------------------------------------------------------------
// Duo Auth API
// https://duo.com/docs/authapi#/auth
// https://duo.com/docs/authapi#authentication
bool DuoAuthLib::passcodeAuth(char* userName, char* userPasscode)
{
if(!_init){
return false;
}
//Create new Buffer using the method below
int userStrLen = encodeUrlBufferSize(userName);
char encodedUsername[userStrLen];
encodeUrl(encodedUsername, userName);
return performAuth(encodedUsername, userPasscode, PASSCODE);
}
//----------------------------------------------------------------
//Duo - Set Public IP Address of Device for Auth API Request
void DuoAuthLib::setIPAddress(char* ipAddress)
{
if(strlen(ipAddress) < MAX_IP_LENGTH){
strcpy(_ipAddress, ipAddress);
}
}
//----------------------------------------------------------------
//Duo - Set the Push Type String that is displayed to the end user
//See the Parameter 'Type' within the 'Duo Push' Section of the
//Duo API Documentation: https://duo.com/docs/authapi#/auth
void DuoAuthLib::setPushType(char* pushType)
{
int typeLength = strlen(pushType);
if(typeLength > 0 && typeLength < MAX_TYPE_LENGTH){
//Create new Buffer using the method below
int userStrLen = encodeUrlBufferSize(pushType);
char encodedUsername[userStrLen];
encodeUrl(encodedUsername, pushType);
strcpy(_duoPushType, encodedUsername);
}
}
//----------------------------------------------------------------
//Duo - Set the HTTP Timeout from the default of 30 Seconds
void DuoAuthLib::setHttpTimeout(int httpTimeout)
{
_httpTimeout = httpTimeout;
}
//----------------------------------------------------------------
//Duo Authentication API Result
//https://duo.com/docs/authapi#/auth
//https://duo.com/docs/authapi#authentication
//Returns True if the Duo Authentication Result is 'allow'
bool DuoAuthLib::authSuccessful()
{
if(_duoApiAuthResponseResult == "allow"){
return true;
}else{
return false;
}
}
//----------------------------------------------------------------
//Duo Authentication API Result
//https://duo.com/docs/authapi#/auth
//https://duo.com/docs/authapi#authentication
//Returns True if the Duo Authentication Result is 'waiting'
bool DuoAuthLib::pushWaiting()
{
if(_duoApiAuthResponseResult == "waiting"){
return true;
}else{
return false;
}
}
//----------------------------------------------------------------
String DuoAuthLib::getHttpResponse()
{
return _lastHttpResponse;
}
String DuoAuthLib::getApiStat()
{
return _duoApiStat;
}
String DuoAuthLib::getAuthResponseResult()
{
return _duoApiAuthResponseResult;
}
String DuoAuthLib::getAuthResponseStatus()
{
return _duoApiAuthResponseStatus;
}
String DuoAuthLib::getAuthResponseStatusMessage()
{
return _duoApiAuthResponseStatusMessage;
}
String DuoAuthLib::getApiFailureCode()
{
return String(_duoApiFailureCode);
}
String DuoAuthLib::getApiFailureMessage()
{
return _duoApiFailureMessage;
}
String DuoAuthLib::getAuthTxId()
{
return _duoAuthTxId;
}
int DuoAuthLib::getHttpCode() {
return _httpCode;
}
//----------------------------------------------------------------
//----------------------------------------------------------------
//DuoAuthLib Private Methods
//----------------------------------------------------------------
//https://duo.com/docs/authapi#/auth
//https://duo.com/docs/authapi#authentication
bool DuoAuthLib::performAuth(char* userName, char* userPasscode, enum DUO_AUTH_METHOD authMethod)
{
bool duoAuthRequestResult = false;
if(getLocalTime(_timeinfo)){
char hmac_password[41];
char timeStringBuffer[TIME_BUF_STR_SIZE];
char authRequestContents[AUTH_REQUEST_BUFFER_SIZE];
authRequestContents[0] = '\0';
//Get Current time from timeinfo
strftime(timeStringBuffer, sizeof(timeStringBuffer), "%a, %d %b %Y %T %z", _timeinfo);
//Check the authentication method and build the request accordingly.
if(authMethod == PUSH){
if(_async){
addRequestParameter(authRequestContents, (char *)ASYNC_PARAM, (char *)"1");
}
addRequestParameter(authRequestContents, (char *)DEVICE_PARAM, (char *)AUTO_PARAM);
addRequestParameter(authRequestContents, (char *)FACTOR_PARAM, (char *)PUSH_PARAM);
addRequestParameter(authRequestContents, (char *)IPADDR_PARAM, _ipAddress);
if(strlen(_duoPushType) > 0){
addRequestParameter(authRequestContents, (char *)TYPE_PARAM, _duoPushType);
}
addRequestParameter(authRequestContents, (char *)USERNAME_PARAM, userName, true);
}else if(authMethod == PASSCODE){
addRequestParameter(authRequestContents, (char *)FACTOR_PARAM, (char *)PASSCODE_PARAM);
addRequestParameter(authRequestContents, (char *)IPADDR_PARAM, _ipAddress);
addRequestParameter(authRequestContents, (char *)PASSCODE_PARAM, userPasscode);
if(strlen(_duoPushType) > 0){
addRequestParameter(authRequestContents, (char *)TYPE_PARAM, _duoPushType);
}
addRequestParameter(authRequestContents, (char *)USERNAME_PARAM, userName, true);
}else{
return duoAuthRequestResult;
}
// Create empty char array to hold our string
char hmacPayload[SIGNATURE_PAYLOAD_BUFFER_SIZE + AUTH_REQUEST_BUFFER_SIZE];
// Generate the required URL Request Contents for the DUO API Call
generateHmacPayload(hmacPayload, timeStringBuffer, (char *)"POST", _duoHost, DUO_AUTH_API_PATH, authRequestContents);
// Generate the required URL Request Contents for the DUO API Call
calculateHmacSha1((char *)_duoSkey, hmacPayload, hmac_password);
bool apiResponse = submitApiRequest(1, timeStringBuffer, DUO_AUTH_API_PATH, hmac_password, authRequestContents);
if(apiResponse == true){
bool processResult = processResponse(&_lastHttpResponse);
if(processResult == true){
duoAuthRequestResult = true;
}
}
}
return duoAuthRequestResult;
}
//----------------------------------------------------------------
//Create and Submit API Request to Duo API Server
bool DuoAuthLib::submitApiRequest(uint8_t apiMethod, char *timeString, const char* apiPath, char *hmacPassword, char* requestContents)
{
NetworkClientSecure *clientDuoAuth = new NetworkClientSecure;
if (clientDuoAuth)
{
clientDuoAuth->setCACertBundle(x509_crt_imported_bundle_bin_start, x509_crt_imported_bundle_bin_end - x509_crt_imported_bundle_bin_start);
{
//Create our HTTPClient Instance
HTTPClient http;
//Build the Request URL based on the Method.
//Append the requestContents to the end of
//the URL for an HTTP GET request
String requestUrl = "https://";
requestUrl += _duoHost;
requestUrl += apiPath;
if(apiMethod == 0 && strlen(requestContents) > 0){
requestUrl += '?';
requestUrl += requestContents;
}
http.begin(requestUrl); //Specify the URL
// HTTP Connection Timeout
http.setTimeout(_httpTimeout);
//Set User Agent Header
http.setUserAgent(_duoUserAgent);
//Set Host Header
http.addHeader("Host", _duoHost);
//Add the required Date Header for DUO API Calls
if(timeString){
http.addHeader(F("Date"), String(timeString));
}
//Add Content Type Header for POST requests
if(apiMethod == 1){
http.addHeader(F("Content-Type"), F("application/x-www-form-urlencoded"));
}
//Add the required HTTP Authorization Header for the DUO API Call
if(hmacPassword){
http.setAuthorization(_duoIkey, hmacPassword);
}
if(apiMethod == 1){
_httpCode = http.POST(requestContents);
}else if(apiMethod == 0){
_httpCode = http.GET();
}else{
http.end();
return false;
}
//----------------------------------------------------------------------------------------
//Valid Duo API Endpoints HTTP(S) Response codes. Only respond with a valid request for
//these values:
// 200 - Success
// 400 - Invalid or missing parameters.
// 401 - The "Authorization" and/or "Date" headers were missing or invalid.
//NOTE: Other HTTP Codes exist; however, only those for the API endpoints are noted above,
// Please refer to the Duo Auth API Documentation @ https://duo.com/docs/authapi
//----------------------------------------------------------------------------------------
if ((_httpCode == 200) || (_httpCode == 400) || (_httpCode == 401)) { //Check for the returning code
_lastHttpResponse = http.getString();
http.end();
return true;
}else{
_lastHttpResponse = "";
http.end();
return false;
}
}
delete clientDuoAuth;
}
return false;
}
bool DuoAuthLib::processResponse(String* serializedJsonData)
{
JsonDocument doc;
_duoApiStat = "";
_duoAuthTxId = "";
_duoApiAuthResponseResult = "";
_duoApiAuthResponseStatus = "";
_duoApiAuthResponseStatusMessage = "";
_duoApiFailureCode = 0;
_duoApiFailureMessage = "";
//Deserialize the json response from the Duo API Endpoints
DeserializationError error = deserializeJson(doc, *serializedJsonData);
//Check if have an error in our Deserialization before proceeding.
if(!error){
const char* apiStat = doc["stat"];
if(apiStat){
_duoApiStat = String(apiStat);
if(strcmp(apiStat,"OK") == 0){
JsonObject response = doc["response"];
if(_async){
const char* duoTxId = response["txid"];
if(duoTxId){
_duoAuthTxId = String(duoTxId);
}else{
_duoAuthTxId = "";
return false;
}
}else{
const char* duoResult = response["result"];
const char* duoStatus = response["status"];
const char* duoStatusMsg = response["status_msg"];
if(duoResult && duoStatus && duoStatusMsg){
_duoApiAuthResponseResult = String(duoResult);
_duoApiAuthResponseStatus = String(duoStatus);
_duoApiAuthResponseStatusMessage = String(duoStatusMsg);
}else{
_duoApiAuthResponseResult = "";
_duoApiAuthResponseStatus = "";
_duoApiAuthResponseStatusMessage = "";
}
}
return true;
}else if(strcmp(apiStat,"FAIL") == 0){
_duoApiFailureCode = doc["code"];
const char* failureMessage = doc["message"];
if(failureMessage){
_duoApiFailureMessage = String(failureMessage);
}
return false;
}else{
return false;
}
}else{
return false;
}
}else{
_duoApiFailureCode = -1;
_duoApiFailureMessage = "DuoAuthLib: Error processing received response";
return false;
}
}
void DuoAuthLib::addRequestParameter(char *requestBuffer, char* field, char* value, bool lastEntry)
{
if(lastEntry == false){
strcat(requestBuffer, field);
strcat(requestBuffer, EQUALS_PAYLOAD_PARAM);
strcat(requestBuffer, value);
strcat(requestBuffer, AMPERSAND_PAYLOAD_PARAM);
}else{
strcat(requestBuffer, field);
strcat(requestBuffer, EQUALS_PAYLOAD_PARAM);
strcat(requestBuffer, value);
}
}
void DuoAuthLib::generateHmacPayload(char *hmacPayload, char* timeBuffer, char* httpMethod, char* duoHost, const char* duoApiPath, char* postContents)
{
hmacPayload[0] = 0;
strcat(hmacPayload, timeBuffer);
strcat(hmacPayload, NEWLINE_PAYLOAD_PARAM);
strcat(hmacPayload, httpMethod);
strcat(hmacPayload, NEWLINE_PAYLOAD_PARAM);
strcat(hmacPayload, duoHost);
strcat(hmacPayload, NEWLINE_PAYLOAD_PARAM);
strcat(hmacPayload, duoApiPath);
strcat(hmacPayload, NEWLINE_PAYLOAD_PARAM);
strcat(hmacPayload, postContents);
}
void DuoAuthLib::calculateHmacSha1(char *signingKey, char *dataPayload, char *hmacSignatureChar)
{
byte hmacSignature[20];
hmacSignatureChar[0] = 0;
mbedtls_md_context_t mbedTlsContext;
//Select the SHA1 Hashtype
mbedtls_md_type_t mbedTlsHashType = MBEDTLS_MD_SHA1;
const size_t payloadLength = strlen(dataPayload);
const size_t keyLength = strlen(signingKey);
mbedtls_md_init(&mbedTlsContext);
mbedtls_md_setup(&mbedTlsContext, mbedtls_md_info_from_type(mbedTlsHashType), 1);
mbedtls_md_hmac_starts(&mbedTlsContext, (const unsigned char *) signingKey, keyLength);
mbedtls_md_hmac_update(&mbedTlsContext, (const unsigned char *) dataPayload, payloadLength);
mbedtls_md_hmac_finish(&mbedTlsContext, hmacSignature);
mbedtls_md_free(&mbedTlsContext);
for(int i= 0; i< sizeof(hmacSignature); i++){
char str[3];
sprintf(str, "%02x", (int)hmacSignature[i]);
strcat(hmacSignatureChar, str);
}
}
//----------------------------------------------------------------
//Functions to read in a character array and output the calculated
//buffer size, and Encode the input excluding the below
//characters:
// 48-57 = Numbers ( 0123456789 )
// 65-90 = UPPPERCASE LETTERS ( ABCDEF )
// 97-122 = lowercase Letters ( abcdef )
// 45 = Dash ( - )
// 46 = Period ( . )
// 95 = Underscore ( _ )
// 126 = Tilde ( ~ )
//----------------------------------------------------------------
//----------------------------------------------------------------
//Calculate the length of the Character Array based on Input
//This function also takes into account Terminating Null
int DuoAuthLib::encodeUrlBufferSize(char *stringToEncode)
{
//Start the count at 1 to account for the terminating null character
int newStrLen = 1;
//Loop Through Char Array t
for(int i= 0; i< strlen(stringToEncode); i++){
int charAscii = (int)stringToEncode[i];
if((charAscii > 47 && charAscii < 58) || (charAscii > 64 && charAscii < 91) || (charAscii > 96 && charAscii < 123) || (charAscii == 45) || (charAscii == 46) || (charAscii == 95) || (charAscii == 126)){
//Found Regular Character
//Increment by 1
newStrLen++;
}else{
//Found Character that requires URL encoding
//Increment by 3
newStrLen += 3;
}
}
return newStrLen;
}
//----------------------------------------------------------------
//Function to read in a character array and output a URL Encoded
//and write the new variable to the destination variable
void DuoAuthLib::encodeUrl(char *destination, char *stringToEncode)
{
//Empty our Character Array before proceeding
destination[0] = '\0';
//Loop Through Char Array to perform urlEncode as required
for(int i= 0; i< strlen(stringToEncode); i++){
int charAscii = (int)stringToEncode[i];
if((charAscii > 47 && charAscii < 58) || (charAscii > 64 && charAscii < 91) || (charAscii > 96 && charAscii < 123) || (charAscii == 45) || (charAscii == 95) || (charAscii == 126) || (charAscii == 46)){
char str[2];
//Output only the Single Character as it does not need to be encoded
sprintf(str, "%c", (int)stringToEncode[i]);
strcat(destination, str);
}else{
char str[4];
//Output the URL Encoded Format '%XX'
sprintf(str, "%%%02X", (int)stringToEncode[i]);
strcat(destination, str);
}
}
}