From 16c617647f92327046cab1d8eed44b4e7f7f005f Mon Sep 17 00:00:00 2001 From: Chris Piker Date: Tue, 12 Dec 2023 01:14:54 -0600 Subject: [PATCH] New credentials functions drafted --- das2/credentials.c | 272 ++++++++++++++++++++++++++++++++++++++++----- das2/credentials.h | 115 ++++++++++++++++--- 2 files changed, 342 insertions(+), 45 deletions(-) diff --git a/das2/credentials.c b/das2/credentials.c index a5a09f4..f14348d 100644 --- a/das2/credentials.c +++ b/das2/credentials.c @@ -1,18 +1,18 @@ -/* Copyright (C) 2017 Chris Piker +/* Copyright (C) 2017-2023 Chris Piker * - * This file is part of libdas2, the Core Das2 C Library. + * This file is part of das2C, the Core Das2 C Library. * - * Libdas2 is free software; you can redistribute it and/or modify it under + * das2C is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 2.1 as published * by the Free Software Foundation. * - * Libdas2 is distributed in the hope that it will be useful, but WITHOUT ANY + * das2C is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License - * version 2.1 along with libdas2; if not, see . + * version 2.1 along with das2C; if not, see . */ #define _POSIX_C_SOURCE 200112L @@ -22,6 +22,7 @@ #include "util.h" #include "array.h" +#include "log.h" #ifdef _WIN32 #include @@ -107,7 +108,7 @@ static char encoding_table[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '4', '5', '6', '7', '8', '9', '+', '/'}; static int mod_table[] = {0, 2, 1}; -char* base64_encode( +char* das_b64_encode( const unsigned char *data, size_t input_length, size_t *output_length ) { @@ -148,20 +149,20 @@ bool das_cred_init( if(sServer == NULL) uLen = 0; else uLen = strlen(sServer); - if((uLen < 4)||(uLen > 127)){sWhich = sServer; } + if((uLen < 4)||(uLen > (DASCRED_SRV_SZ-1))){sWhich = sServer; } if(sRealm == NULL) uLen = 0; else uLen = strlen(sRealm); - if((uLen < 4)||(uLen > 127)){sWhich = sRealm; } + if((uLen < 4)||(uLen > (DASCRED_REALM_SZ-1))){sWhich = sRealm; } if(sHash == NULL) uLen = 0; else uLen = strlen(sHash); - if((uLen < 2)||(uLen > 255)){sWhich = sHash; } + if((uLen < 2)||(uLen > (DASCRED_HASH_SZ-1))){sWhich = sHash; } /* Dataset string can be null */ if(sDataset != NULL){ uLen = strlen(sDataset); - if((uLen < 2)||(uLen > 255)){sWhich = sDataset; } + if((uLen < 2)||(uLen > (DASCRED_DSET_SZ-1))){sWhich = sDataset; } } if(sWhich){ @@ -170,10 +171,10 @@ bool das_cred_init( } memset(pCred, 0, sizeof(das_credential)); - strncpy(pCred->sServer, sServer, 127); - strncpy(pCred->sRealm, sRealm, 127); - strncpy(pCred->sHash, sHash, 255); - if(sDataset != NULL) strncpy(pCred->sDataset, sDataset, 127); + strncpy(pCred->sServer, sServer, DASCRED_SRV_SZ-1); + strncpy(pCred->sRealm, sRealm, DASCRED_REALM_SZ-1); + strncpy(pCred->sHash, sHash, DASCRED_HASH_SZ-1); + if(sDataset != NULL) strncpy(pCred->sDataset, sDataset, DASCRED_DSET_SZ-1); pCred->bValid = true; /* Assume it works until proven otherwise */ @@ -192,20 +193,22 @@ DasCredMngr* new_CredMngr(const char* sKeyStore) DasCredMngr* pThis = (DasCredMngr*)calloc(1, sizeof(DasCredMngr)); - das_credential* pFill = (das_credential*)calloc(1, sizeof(das_credential)); + das_credential fill; + memset(&fill, 0, sizeof(das_credential)); pThis->pCreds = new_DasAry( - "cashed_credentials",vtUnknown, sizeof(DasCredMngr), (const byte*)pFill, + "cashed_credentials",vtUnknown, sizeof(das_credential), (const byte*)(&fill), RANK_1(0), UNIT_DIMENSIONLESS ); pThis->prompt = das_term_prompt; - - pThis->sKeyFile = sKeyStore; - + + if(sKeyStore) + strncpy(pThis->sKeyFile, sKeyStore, 127); + return pThis; } -void del_DasCredMngr(DasCredMngr* pThis){ +void del_CredMngr(DasCredMngr* pThis){ dec_DasAry(pThis->pCreds); free(pThis); } @@ -250,11 +253,43 @@ int CredMngr_addCred(DasCredMngr* pThis, const das_credential* pCred) if(pOld == NULL) DasAry_append(pThis->pCreds, (const byte*)pCred, 1); else - memcpy(pOld->sHash, pCred->sHash, 256); /* Get terminating null */ + memcpy(pOld->sHash, pCred->sHash, DASCRED_HASH_SZ); /* Get terminating null */ return DasAry_size(pThis->pCreds); } +int CredMngr_addUserPass( + DasCredMngr* pThis, const char* sServer, const char* sRealm, + const char* sDataset, const char* sUser, const char* sPass +){ + das_credential cred; + memset(&cred, 0, sizeof(das_credential)); + + char sBuf[DASCRED_HASH_SZ+2]; /* 2 bytes longer than das_credential.sHash + to detect long hash values */ + if(strchr(sUser, ':') != NULL){ + das_error(DASERR_CRED, "The user name cannot contain a colon, ':', character"); + return -1; /* If the error handler allows for a return */ + } + + /* Hash it */ + snprintf(sBuf, DASCRED_HASH_SZ+1, "%s:%s", sUser, sPassword); /* 257 is not an error */ + size_t uLen; + char* sHash = das_b64_encode((unsigned char*)sBuf, strlen(sBuf), &uLen); + /*fprintf(stderr, "DEBUG: Print hash: %s, length %zu\n", sHash, uLen); */ + if(uLen > (DASCRED_HASH_SZ-1)){ + free(sHash); + pThis->sLastAuthMsg[0] = '\0'; + das_error(DASERR_CRED, "Username and password are too large for the hash buffer"); + return -1; + } + + if(! das_cred_init(sServer, sRealm, sDataset, sHash)) + return -1; /* Function sets it's own error message */ + + return CredMngr_addCred(pThis, &cred); +} + const char* CredMngr_getHttpAuth( DasCredMngr* pThis, const char* sServer, const char* sRealm, const char* sDataset @@ -269,8 +304,8 @@ const char* CredMngr_getHttpAuth( const char* sMsg = NULL; if(pThis->sLastAuthMsg[0] != '\0') sMsg = pThis->sLastAuthMsg; - char sBuf[258]; /* 258 is not an error, it's 2 bytes longer than - * das_credential.sHash to detect long hash values */ + char sBuf[DASCRED_HASH_SZ+2]; /* 2 bytes longer than das_credential.sHash + to detect long hash values */ while(true){ /* So I either don't have a credential, or it's not valid. Get a new @@ -284,11 +319,11 @@ const char* CredMngr_getHttpAuth( } /* Hash it */ - snprintf(sBuf, 257, "%s:%s", sUser, sPassword); /* 257 is not an error */ + snprintf(sBuf, DASCRED_HASH_SZ+1, "%s:%s", sUser, sPassword); /* 257 is not an error */ size_t uLen; - char* sHash = base64_encode((unsigned char*)sBuf, strlen(sBuf), &uLen); + char* sHash = das_b64_encode((unsigned char*)sBuf, strlen(sBuf), &uLen); /*fprintf(stderr, "DEBUG: Print hash: %s, length %zu\n", sHash, uLen); */ - if(uLen > 255){ + if(uLen > (DASCRED_HASH_SZ-1)){ free(sHash); pThis->sLastAuthMsg[0] = '\0'; das_error(DASERR_CRED, "Base64 output buffer is too small, tell " @@ -328,7 +363,7 @@ void CredMngr_authFailed( if(pCred != NULL) pCred->bValid = false; if(sMsg != NULL) - strncpy(pThis->sLastAuthMsg, sMsg, 1023); + strncpy(pThis->sLastAuthMsg, sMsg, DASCMGR_MSG_SZ-1); } @@ -338,8 +373,185 @@ das_prompt CredMngr_setPrompt(DasCredMngr* pThis, das_prompt new_prompt){ return old; } -bool CredMngr_save(const DasCredMngr* pThis, const char* sFile){ - /* TODO */ +/* TODO: Add openssh password symetric key protection to the credentials file */ +int CredMngr_save(DasCredMngr* pThis, const char* sSymKey, const char* sFile) +{ + + if(sSymKey != NULL){ + daslog_error("Symetric key encryption of the credentials file is not yet implemented."); + return -1; + } + + /* Write all the current credentials to the given filename */ + const char* sOut = (sFile == NULL) ? pThis->sKeyFile : sFile; + + if(sOut[0] == '\0'){ + daslog_error("Can't save. No credentials file specified either here or" + " in the constructor"); + return -1; + } + + FILE* pOut = fopen(sOut, "wb"); + + int nRet = 0; + das_credential* pCred = NULL; + for(ptrdiff_t i = 0; i < DasAry_size(pThis->pCreds); ++i){ + pCred = (das_credential*)DasAry_getAt(pThis->pCreds, vtUnknown, IDX0(i)); + if(!pCred->bValid) + continue; + + if(pCred->sDataset[0] != '\0') + fprintf(pOut, + "%s|%s|dataset|%s|%s\n", pCred->sServer, pCred->sRealm, pCred->sDataset, + pCred->sHash + ); + else + fprintf(pOut, + "%s|%s|||%s\n", pCred->sServer, pCred->sRealm, pCred->sHash + ); + ++nRet; + } + + fclose(pOut); + + if((sFile != NULL)&&(sFile[0] != '\0')){ + memset(pThis->sKeyFile, 0, DASCMGR_FILE_SZ); + strncpy(pThis->sKeyFile, sFile, DASCMGR_FILE_SZ-1); + } + + return nRet; +} + +/* TODO: Add openssh password symetric key protection to the credentials file */ +int CredMngr_load(DasCredMngr* pThis, const char* sSymKey, const char* sFile) +{ + if(sSymKey != NULL){ + daslog_error("Symetric key encryption of the credentials file is not yet implemented."); + return -1; + } + + const char* sIn = (sFile == NULL) ? pThis->sKeyFile : sFile; + + if(sIn[0] == '\0'){ + daslog_error("Can't load. No credentials file specified either here or" + " in the constructor"); + return -1; + } + + FILE* pIn = fopen(sIn, "rb"); + + // Make an array to the side to hold loaded credentials + das_credential fill; + memset(&fill, 0, sizeof(das_credential)); + + DasAry* pTmpCreds = new_DasAry( + "temp_credentials",vtUnknown, sizeof(das_credential), (const byte*)(&fill), + RANK_1(0), UNIT_DIMENSIONLESS + ); + + int nCreds = 0; + const size_t uLineLen = DASCRED_SRV_SZ+DASCRED_REALM_SZ+DASCRED_DSET_SZ+DASCRED_HASH_SZ+40; + char aLine[ + DASCRED_SRV_SZ+DASCRED_REALM_SZ+DASCRED_DSET_SZ+DASCRED_HASH_SZ+40 + ] = {'\0'}; - return false; + char* aBeg[5] = {NULL}; + char* aEnd[5] = {NULL}; // These point at terminating nulls + char* pChar = NULL; + + das_credential cred; + int nLine = 0; + + size_t iSection; + while(fgets(aLine, uLineLen, pIn)){ + ++nLine; + + memset(aBeg, 0, 5*sizeof(void*)); + memset(aEnd, 0, 5*sizeof(void*)); + + // Section begin and end are the same for empty sections + aBeg[0] = aLine; + aEnd[4] = aLine + strlen(aLine) + 1; + iSection = 0; + for(pChar = aLine; *pChar != '\0'; ++pChar){ + if(*pChar == '|'){ + aEnd[iSection] = pChar; + *pChar = '\0'; + ++iSection; + if(iSection > 4) break; + + aBeg[iSection] = pChar+1; + } + } + + // Only lines with 4 pipes are valid credentials, anything else is text + if(iSection != 4) continue; + + // Strip ends + for(iSection = 0; iSection < 5; ++iSection){ + if(aBeg[iSection] == aEnd[iSection]) continue; + + pChar = aBeg[iSection]; + while((pChar < aEnd[iSection]) && ((*pChar == ' ')||(*pChar == '\t'))){ + ++pChar; + aBeg[iSection] += 1; + } + + pChar = aEnd[iSection]; + while((pChar >= aBeg[iSection]) && ((*pChar == ' ')||(*pChar == '\t'))){ + --pChar; + *(aEnd[iSection]) = '\0'; + aEnd[iSection] -= 1; + } + } + + // Required sections: 0 = server, 1 = Realm, 4 = Hash + if((aBeg[0] == aEnd[0])||(aBeg[1] == aEnd[1])||(aBeg[4] == aEnd[4])) + continue; + + // Expect the key 'dataset' if aEnd[2] is not null + if((*(aEnd[2]) != '\0')&&(strcmp(aBeg[2], "dataset") != 0)){ + daslog_warn_v( + "%s,%d: Hashes for specific datasets must indicate the key 'dataset'", + sIn, nLine + ); + continue; + } + + if(das_cred_init( + &cred, aBeg[0], aBeg[1], *(aBeg[3]) == '\0' ? NULL : aBeg[3], aBeg[4] + )){ + daslog_warn_v("%s,%d: Could not parse credential", sIn, nLine); + continue; + } + + // Add the credential + DasAry_append(pTmpCreds, (const byte*)(&cred), 1); + ++nCreds; + } + + + // Merge in the new credentials from the file + if(nCreds > 0){ + das_credential* pNew = NULL; + das_credential* pOld = NULL; + for(ptrdiff_t i = 0; i < DasAry_size(pTmpCreds); ++i){ + pNew = (das_credential*)DasAry_getAt(pThis->pCreds, vtUnknown, IDX0(i)); + + pOld = _CredMngr_getCred(pThis, pNew->sServer, pNew->sRealm, pNew->sDataset, false); + if(pOld == NULL){ + DasAry_append(pThis->pCreds, (const byte*)pNew, 1); // append always copies + } + else{ + if(pOld->sHash && (strcmp(pOld->sHash, pNew->sHash) != 0)) + memcpy(pOld->sHash, pNew->sHash, DASCRED_HASH_SZ); + else + --nCreds; + } + } + } + + dec_DasAry(pTmpCreds); // Frees the temporary credentials array + + return nCreds; } diff --git a/das2/credentials.h b/das2/credentials.h index 9a8ba1e..fdcf4ea 100644 --- a/das2/credentials.h +++ b/das2/credentials.h @@ -1,18 +1,18 @@ -/* Copyright (C) 2017 Chris Piker +/* Copyright (C) 2017-2023 Chris Piker * - * This file is part of libdas2, the Core Das2 C Library. + * This file is part of das2C, the Core Das2 C Library. * - * Libdas2 is free software; you can redistribute it and/or modify it under + * das2C is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License version 2.1 as published * by the Free Software Foundation. * - * Libdas2 is distributed in the hope that it will be useful, but WITHOUT ANY + * das2C is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License - * version 2.1 along with libdas2; if not, see . + * version 2.1 along with das2C; if not, see . */ #ifndef _das_credmngr_h_ @@ -33,6 +33,21 @@ extern "C" { * @{ */ +/** Encode provided binary data as base64 characters in a new buffer + * + * (Credit: stackoverflow user ryyst) + * @param data A pointer to the data to encode + * @param input_length The number of input bytes to encode + * @param output_length a pointer to location to receive the encoded length + * + * @returns A pointer to a newly allocated buffer of at least size + * output_length, or NULL if calloc failed. + */ +DAS_API char* das_b64_encode( + const unsigned char* data, size_t input_length, size_t* output_length +); + + /** Function signature for swapping out the user-prompt for credentials * acquisition. * @@ -51,13 +66,18 @@ typedef bool (*das_prompt)( const char* sMessage, char* sUser, char* sPassword ); +#define DASCRED_SRV_SZ 128 +#define DASCRED_REALM_SZ 128 +#define DASCRED_DSET_SZ 128 +#define DASCRED_HASH_SZ 256 + /** A single credential*/ typedef struct das_credential_t{ bool bValid; - char sServer[128]; - char sRealm[128]; - char sDataset[128]; - char sHash[256]; + char sServer[DASCRED_SRV_SZ]; + char sRealm[DASCRED_REALM_SZ]; + char sDataset[DASCRED_DSET_SZ]; + char sHash[DASCRED_HASH_SZ]; } das_credential; @@ -87,6 +107,9 @@ DAS_API bool das_cred_init( const char* sDataset, const char* sHash ); +#define DASCMGR_FILE_SZ 128 +#define DASCMGR_MSG_SZ 1024 + /** Credentials manager * Handles a list of login credentials and supplies these as needed for network * operations @@ -94,16 +117,18 @@ DAS_API bool das_cred_init( typedef struct das_credmngr{ DasAry* pCreds; das_prompt prompt; - const char* sKeyFile; - char sLastAuthMsg[1024]; + char sKeyFile[DASCMGR_FILE_SZ]; + char sLastAuthMsg[DASCMGR_MSG_SZ]; } DasCredMngr; /** @} */ /** Initialize a new credentials manager, optionally from a saved list * - * @param sKeyStore If not NULL, the credentials manager will initialize itself - * from the given file. + * @param sKeyStore If not NULL this saves the name of the intended + * credentials storage file. It DOES NOT LOAD ANYTHING! To load + * credentials use CredMngr_load(pThis, "myencryptkey"). + * * @return A new credentials manager allocated on the heap * @memberof DasCredMngr */ @@ -133,6 +158,30 @@ DAS_API void del_CredMngr(DasCredMngr* pThis); */ DAS_API int CredMngr_addCred(DasCredMngr* pThis, const das_credential* pCred); + +/** Manually add a credential to a credentials manager instead of prompting the + * user. + * + * This is a individual string version of CredMngr_addCred that calculates it's + * own base64 hash. + * + * @param pThis The credentials manager structure that will hold the new + * credential in RAM + * + * @param sServer The resource URL including the path, but not including + * fragments or query parameters + * @param sRealm The security realm to which this credential shoud be supplied + * @param sDataset If not NULL, the value of the 'dataset=' parameter that + * must be present for this credential to apply + * @param sUser A user name + * @param sPass A plain-text password + * @returns The new number of cached credentials, or -1 on an error + */ +DAS_API int CredMngr_addUserPass( + DasCredMngr* pThis, const char* sServer, const char* sRealm, + const char* sDataset, const char* sUser, const char* sPass +); + /** Retrieve an HTTP basic authentication token for a given dataset on a given * server. * @@ -184,12 +233,48 @@ DAS_API void CredMngr_authFailed( DAS_API das_prompt CredMngr_setPrompt(DasCredMngr* pThis, das_prompt new_prompt); /** Save the current credentials to the given filename + * + * NOTE: The credentials file is not encrypted. It could be since openssl is + * a required dependency of das2C, but the functionality to do so hasn't + * been implemented * * @param pThis a pointer to a CredMngr structure - * @param sFile the file to hold the loosly encypted credentials + * + * @param sSymKey A key to use for encrypting the credentials file + * (Not yet implemented, added for stable ABI, use NULL here) + * + * @param sFile the file to hold the loosly encypted credentials. If NULL + * then the keyfile indicated in the constructor, new_CredMngr() is + * used. If the file does not exist it is created. + * + * @returns The number of credential rows saved, or -1 on an error. * @memberof DasCredMngr */ -DAS_API bool CredMngr_save(const DasCredMngr* pThis, const char* sFile); +DAS_API int CredMngr_save(DasCredMngr* pThis, const char* sSymKey, const char* sFile); + +/** Merge in credentials from the given filename + * + * NOTE: The credentials file is not encrypted. It could be since openssl is + * a required dependency of das2C, but the functionality to do so hasn't + * been implemented + * + * @param pThis a pointer to a CredMngr structure + * + * @param sSymKey A key to use for encrypting the credentials file + * (Not yet implemented, added for stable ABI, use NULL here) + * + * @param sFile the file to hold the loosly encypted credentials. If the + * file does not exist, 0 is returned. Thus a missing credentials file + * is not considered an error. If sFile is NULL, then the keyfile + * given in the constructor, new_CredMngr() is used. + * + * @returns The number of NEW credential sets and conditions. Thus loading + * the exact same file twice should return 0 on the second load. + * + * @memberof DasCredMngr + */ +DAS_API int CredMngr_load(DasCredMngr* pThis, const char* sSymKey, const char* sFile); + #ifdef __cplusplus }