From 5fcb08c4816d997f545a28da09e00ad04289b055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Szczerbi=C5=84ski?= Date: Thu, 7 Nov 2024 19:26:16 +0100 Subject: [PATCH 1/3] Move SecureStorageImplementation from ODBC --- cpp/platform/SecureStorage.cpp | 90 +++++++++++ cpp/platform/SecureStorage.hpp | 94 +++++++++++ cpp/platform/SecureStorageApple.cpp | 239 ++++++++++++++++++++++++++++ cpp/platform/SecureStorageApple.hpp | 110 +++++++++++++ cpp/platform/SecureStorageUnSup.cpp | 42 +++++ cpp/platform/SecureStorageUnSup.hpp | 106 ++++++++++++ cpp/platform/SecureStorageWin.cpp | 147 +++++++++++++++++ cpp/platform/SecureStorageWin.hpp | 107 +++++++++++++ 8 files changed, 935 insertions(+) create mode 100644 cpp/platform/SecureStorage.cpp create mode 100644 cpp/platform/SecureStorage.hpp create mode 100644 cpp/platform/SecureStorageApple.cpp create mode 100644 cpp/platform/SecureStorageApple.hpp create mode 100644 cpp/platform/SecureStorageUnSup.cpp create mode 100644 cpp/platform/SecureStorageUnSup.hpp create mode 100644 cpp/platform/SecureStorageWin.cpp create mode 100644 cpp/platform/SecureStorageWin.hpp diff --git a/cpp/platform/SecureStorage.cpp b/cpp/platform/SecureStorage.cpp new file mode 100644 index 0000000000..7415112229 --- /dev/null +++ b/cpp/platform/SecureStorage.cpp @@ -0,0 +1,90 @@ +/* + * File: SecureStorage.cpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ + +#include "Logger.hpp" +#ifdef __APPLE__ +#include "SecureStorageApple.hpp" +#elif _WIN32 +#include "SecureStorageWin.hpp" +#else +#include "SecureStorageUnSup.hpp" +#endif + +#include "SecureStorage.hpp" + +namespace sf +{ + + bool SecureStorage::storeToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + SecureStorageImpl secStorage; + if (secStorage.storeToken(host, username, credType, token) != SUCCESS) + { + SF_ERROR_LOG("sf", "SecureStorage", "storeToken", + "Failed to store secure token%s", ""); + return false; + } + + SF_DEBUG_LOG("sf", "SecureStorage", "storeToken", + "Successfully stored secure token%s", ""); + return true; + } + + bool SecureStorage::retrieveToken(const char *host, + const char *username, + const char *credType, + char *token, + size_t *tokenLen) + { + SecureStorageImpl secStorage; + if (secStorage.retrieveToken(host, username, credType, token, tokenLen) != SUCCESS) + { + SF_ERROR_LOG("sf", "SecureStorage", "retrieveToken", + "Failed to retrieve secure token%s", ""); + return false; + } + + SF_DEBUG_LOG("sf", "SecureStorage", "storeToken", + "Successfully retrieved secure tokeni%s", ""); + return true; + } + + bool SecureStorage::updateToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + SecureStorageImpl secStorage; + if ( secStorage.updateToken(host, username, credType, token) != SUCCESS) + { + SF_ERROR_LOG("sf", "SecureStorage", "retrieveToken", + "Failed to update secure token%s", ""); + return false; + } + + SF_DEBUG_LOG("sf", "SecureStorage", "updateToken", + "Successfully updated secure token%s", ""); + return true; + } + + bool SecureStorage::removeToken(const char *host, + const char *username, + const char *credType) + { + SecureStorageImpl secStorage; + if ( secStorage.removeToken(host, username, credType) != SUCCESS) + { + SF_ERROR_LOG("sf", "SecureStorage", "removeToken", + "Failed to remove secure token%s", ""); + return false; + } + SF_DEBUG_LOG("sf", "SecureStorage", "removeToken", + "Successfully removed secure token%s", ""); + return true; + } +} diff --git a/cpp/platform/SecureStorage.hpp b/cpp/platform/SecureStorage.hpp new file mode 100644 index 0000000000..2ab89b39ab --- /dev/null +++ b/cpp/platform/SecureStorage.hpp @@ -0,0 +1,94 @@ +/* + * File: SecureStorage.hpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ + +#ifndef PROJECT_SECURESTORAGE_H +#define PROJECT_SECURESTORAGE_H + +#include + +namespace sf +{ + /** + * Class SecureStorage + */ + class SecureStorage + { + public: + + /** + * storeToken + * + * API to secure store id_token in Apple Keychain + * + * @param host - snowflake host name + * @param username - snowflake user name + * @param token - id token to be secured + * @param credType - type of snowflake temporary credential + * @param cred - temporary credential to store + * @return True / False + */ + bool storeToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * retrieveToken + * + * API to retrieve token from Apple Key Chain + * + * @param host - snowflake host url associated + * with the token + * @param username - snowflake username associated with + * the token + * @param credType - type of credential to retrieve + * @param token - on return , populated id credential extracted + * from the keychain + * @param tokenLen - on return, length of the credential retrieved + * @return True / False + */ + bool retrieveToken(const char *host, + const char *username, + const char *credType, + char *token, + size_t *tokenLen); + + /** + * updateToken + * + * API to update an existing token in the keychain. + * + * @param host - snowflake host url associated + * with the token + * @param username - snowflake username assoicated with + * the token + * @param credType - type of credential to be updated + * @param token - credential to be stored in the keychain. + * @return True / False + */ + bool updateToken(const char *host, + const char *username, + const char *credType, + const char *token); + + /** + * removeToken + * + * API to remove a token from the keychain. + * + * @param host - snowflake host url associated + * with the token + * @param username - snowflake username assoicated with + * the token + * @param credType - type of credential to be removed + * @return True / False + */ + bool removeToken(const char *host, + const char *username, + const char *credType); + }; +} + +#endif //PROJECT_SECURESTORAGE_H diff --git a/cpp/platform/SecureStorageApple.cpp b/cpp/platform/SecureStorageApple.cpp new file mode 100644 index 0000000000..d9aca9128e --- /dev/null +++ b/cpp/platform/SecureStorageApple.cpp @@ -0,0 +1,239 @@ +/* + * File: SecureStorageApple.cpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ + +#include +#include "Snowflake.h" +#include "Logger.hpp" +#include "CoreFoundation/CoreFoundation.h" +#include "Security/Security.h" + +#include "SecureStorageApple.hpp" + +#define MAX_TOKEN_LEN 1024 +#define DRIVER_NAME "SNOWFLAKE_ODBC_DRIVER" +#define COLON_CHAR_LENGTH 1 +#define NULL_CHAR_LENGTH 1 + +using namespace Simba; + +namespace sf +{ + + void SecureStorageImpl::convertTarget(const char *host, + const char *username, + const char *credType, + char *targetname, + size_t max_len) + { + simba_strcat(targetname, max_len, host); + simba_strcat(targetname, max_len, ":"); + simba_strcat(targetname, max_len, username); + simba_strcat(targetname, max_len, ":"); + simba_strcat(targetname, max_len, DRIVER_NAME); + simba_strcat(targetname, max_len, ":"); + simba_strcat(targetname, max_len, credType); + } + + SECURE_STORAGE_STATUS SecureStorageImpl::storeToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + /* + * More on OS X Types can be read here: + * https://developer.apple.com/documentation/corefoundation?language=objc + */ + CFTypeRef keys[4]; + CFTypeRef values[4]; + /* + * Concatenate Host & Username in the format: + * `host:username:drivername:credType' + * the target_max_len hence becomes : + * strlen(host)+strlen(username)+strlen(drivername)+strlen(credType)+ (3 * strlen(':')) + * Add 1 to accommodate the NULL character. + */ + size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ + strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + char *targetname = new char[target_max_len]; + convertTarget(host, username, credType, targetname, target_max_len); + + + keys[0] = kSecClass; + keys[1] = kSecAttrServer; + keys[2] = kSecAttrAccount; + keys[3] = kSecValueData; + + values[0] = kSecClassInternetPassword; + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, targetname, kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8); + values[3] = CFStringCreateWithCString(kCFAllocatorDefault, token, kCFStringEncodingUTF8); + + CFDictionaryRef query; + query = CFDictionaryCreate(kCFAllocatorDefault, (const void**) keys, (const void**) values, 4, NULL, NULL); + + OSStatus result = SecItemAdd(query, NULL); + + if (result == errSecDuplicateItem) + { + SF_DEBUG_LOG("sf", "SecureStorageApple", "storeToken", + "Token already exists, updatingi%s", ""); + return updateToken(host, username, credType, token); + } + else if (result == errSecSuccess) + { + SF_DEBUG_LOG("sf", "SecureStorageApple", "storeToken", + "Successfully stored secure token%s", ""); + return SUCCESS; + } + else + { + SF_ERROR_LOG("sf", "SecureStorageApple", "storeToken", + "Failed to store secure token%s", ""); + return ERROR; + } + } + + SECURE_STORAGE_STATUS SecureStorageImpl::retrieveToken(const char *host, + const char *username, + const char *credType, + char *token, + size_t *tokenLen) + { + OSStatus result = errSecSuccess; + SecKeychainItemRef pitem = NULL; + UInt32 plength = 0; + char *pdata = NULL; + size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ + strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + char *targetname = new char[target_max_len]; + convertTarget(host, username, credType, targetname, target_max_len); + + if (tokenLen == NULL) + { + // tokenLen is expected to be Non NULL + SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", + "Token len has to be allocated%s", ""); + return ERROR; + } + + /* + * https://developer.apple.com/documentation/security/1397763-seckeychainfindinternetpassword?language=objc + */ + result = SecKeychainFindInternetPassword(NULL, (uint32)strlen(targetname), + targetname, 0, NULL, + (uint32) strlen(username), + username, 0, NULL, 0, + kSecProtocolTypeAny, + kSecAuthenticationTypeAny, + &plength, (void **)&pdata, + &pitem); + + if (result == errSecItemNotFound) + { + *tokenLen = 0; + token = NULL; + SF_ERROR_LOG("sf", "SecureStorageApple", "storeToken", + "Failed to retrieve secure token - %s", "Token Not Found"); + return NOT_FOUND; + } + else if (result == errSecSuccess) + { + *tokenLen = plength; + if (plength > MAX_TOKEN_LEN) + { + SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", + "Failed to retrieve secure token - %s", "Stored token length greater than max allowed length"); + return ERROR; + } + + SF_DEBUG_LOG("sf", "SecureStorageApple", "retrieveToken", + "Successfully retrieved token%s", ""); + strlcpy(token, pdata, plength+1); + return SUCCESS; + } + else + { + *tokenLen = 0; + token = NULL; + SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", + "Failed to retrieve secure token%s", ""); + return ERROR; + } + } + + SECURE_STORAGE_STATUS SecureStorageImpl::updateToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + OSStatus result = errSecSuccess; + CFTypeRef keys[4]; + CFTypeRef values[4]; + size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ + strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + char *targetname = new char[target_max_len]; + convertTarget(host, username, credType, targetname, target_max_len); + + keys[0] = kSecClass; + keys[1] = kSecAttrServer; + keys[2] = kSecAttrAccount; + keys[3] = kSecMatchLimit; + + values[0] = kSecClassInternetPassword; + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, targetname, kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8); + values[3] = kSecMatchLimitOne; + + CFDictionaryRef extract_query = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, + (const void **)values, 4, NULL, NULL); + result = SecItemDelete(extract_query); + + if (result != errSecSuccess && result != errSecItemNotFound) + { + SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", + "Failed to update secure token%s", ""); + return ERROR; + } + + return storeToken(host, username, credType, token); + } + + SECURE_STORAGE_STATUS SecureStorageImpl::removeToken(const char *host, + const char *username, + const char *credType) + { + OSStatus result = errSecSuccess; + CFTypeRef keys[4]; + CFTypeRef values[4]; + size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ + strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + char *targetname = new char[target_max_len]; + convertTarget(host, username, credType, targetname, target_max_len); + + keys[0] = kSecClass; + keys[1] = kSecAttrServer; + keys[2] = kSecAttrAccount; + keys[3] = kSecMatchLimit; + + values[0] = kSecClassInternetPassword; + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, targetname, kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8); + values[3] = kSecMatchLimitOne; + + CFDictionaryRef extract_query = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, + (const void **)values, 4, NULL, NULL); + result = SecItemDelete(extract_query); + if (result != errSecSuccess && result != errSecItemNotFound) + { + SF_ERROR_LOG("sf", "SecureStorageApple", "removeToken", + "Failed to remove secure token%s", ""); + return ERROR; + } + + SF_DEBUG_LOG("sf", "SecureStorageApple", "removeToken", + "Successfully removed secure token%s", ""); + return SUCCESS; + } +} diff --git a/cpp/platform/SecureStorageApple.hpp b/cpp/platform/SecureStorageApple.hpp new file mode 100644 index 0000000000..2d11d016b8 --- /dev/null +++ b/cpp/platform/SecureStorageApple.hpp @@ -0,0 +1,110 @@ +/* + * File: SecureStorageApple.hpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ + +#ifndef PROJECT_SECURESTORAGE_APPLE_H +#define PROJECT_SECURESTORAGE_APPLE_H + +#include + +typedef enum +{ + NOT_FOUND, + ERROR, + SUCCESS, + UNSUPPORTED +}SECURE_STORAGE_STATUS; + +namespace sf +{ + /** + * Class SecureStorage + */ + class SecureStorageImpl + { + + static void convertTarget(const char *host, + const char *username, + const char *credType, + char *targetname, + size_t max_len); + + public: + + /** + * storeToken + * + * API to secure store credential in Apple Keychain + * + * @param host - snowflake host url + * @param username - snowflake user name + * @param credType - type of snowflake credential to be stored + * @param cred - credential to be secured + * + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS storeToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * retrieveToken + * + * API to retrieve credential from Apple Key Chain + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username associated with + * the credential + * @param cred - snowflake credential to be retrieved + * @param cred - on return , populated credential extracted + * from the keychain + * @param credLen - on return, length of the credential retrieved + * @return NOT_FOUND, ERROR, SUCCESS + */ + SECURE_STORAGE_STATUS retrieveToken(const char *host, + const char *username, + const char *credType, + char *cred, + size_t *credLen); + + /** + * updateToken + * + * API to update an existing credential in the keychain. + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username assoicated with + * the credential + * @param credType - type of snowflake credential to be updated + * @param cred - credential to be stored in the keychain. + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS updateToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * removeToken + * + * API to remove a credential from the keychain. + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username assoicated with + * the credential + * @param credType - type of credential to be removed. + * + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS removeToken(const char *host, + const char *username, + const char *credType); + }; +} + +#endif //PROJECT_SECURESTORAGE_APPLE_H diff --git a/cpp/platform/SecureStorageUnSup.cpp b/cpp/platform/SecureStorageUnSup.cpp new file mode 100644 index 0000000000..74834bf8fe --- /dev/null +++ b/cpp/platform/SecureStorageUnSup.cpp @@ -0,0 +1,42 @@ +/* + * File: SecureStorageUnSup.cpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ + +#include "SecureStorageUnSup.hpp" + +namespace sf +{ + + SECURE_STORAGE_STATUS SecureStorageImpl::storeToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + return UNSUPPORTED; + } + + SECURE_STORAGE_STATUS SecureStorageImpl::retrieveToken(const char *host, + const char *username, + const char *credType, + char *token, + size_t *token_len) + { + return UNSUPPORTED; + } + + SECURE_STORAGE_STATUS SecureStorageImpl::updateToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + return UNSUPPORTED; + } + + SECURE_STORAGE_STATUS SecureStorageImpl::removeToken(const char *host, + const char *username, + const char *credType) + { + return UNSUPPORTED; + } +} diff --git a/cpp/platform/SecureStorageUnSup.hpp b/cpp/platform/SecureStorageUnSup.hpp new file mode 100644 index 0000000000..f0e4e53960 --- /dev/null +++ b/cpp/platform/SecureStorageUnSup.hpp @@ -0,0 +1,106 @@ +/* + * File: SecureStorageUnSup.hpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ +#ifndef PROJECT_SECURESTORAGE_UNSUP_H +#define PROJECT_SECURESTORAGE_UNSUP_H + +#include + +typedef enum +{ + NOT_FOUND, + ERROR, + SUCCESS, + UNSUPPORTED +}SECURE_STORAGE_STATUS; + +namespace sf +{ + /** + * Class SecureStorage + */ + class SecureStorageImpl + { + static void convertTarget(const char *host, + const char *username, + const char *credType, + char *targetname); + + public: + + /** + * storeToken + * + * API to securely store token + * + * @param host - snowflake host url + * @param username - snowflake user name + * @param credType - type of snowflake credential to be stored + * @param cred - credential to be secured + * + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS storeToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * retrieveToken + * + * API to retrieve token + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username associated with + * the credential + * @param cred - snowflake credential to be retrieved + * @param cred - on return , populated credential extracted + * @param credLen - on return, length of the credential retrieved + * @return NOT_FOUND, ERROR, SUCCESS + */ + SECURE_STORAGE_STATUS retrieveToken(const char *host, + const char *username, + const char *credType, + char *cred, + size_t *credLen); + + /** + * updateCredential + * + * API to update an existing token. + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username assoicated with + * the credential + * @param credType - type of snowflake credential to be updated + * @param cred - credential to be stored + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS updateToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * removeToken + * + * API to remove a token. + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username assoicated with + * the credential + * @param credType - type of credential to be removed + * + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS removeToken(const char *host, + const char *username, + const char *credType); + }; +} + +#endif //PROJECT_SECURESTORAGE_UNSUP_H diff --git a/cpp/platform/SecureStorageWin.cpp b/cpp/platform/SecureStorageWin.cpp new file mode 100644 index 0000000000..8468e6ed41 --- /dev/null +++ b/cpp/platform/SecureStorageWin.cpp @@ -0,0 +1,147 @@ +/* + * File: SecureStorage.cpp * + * Copyright (c) 2013-2020 Snowflake Computing + */ + +#include "Logger.hpp" +#include "windows.h" +#include "wincred.h" + +#include "SecureStorageWin.hpp" +#include + +#define MAX_TOKEN_LEN 1024 +#define DRIVER_NAME "SNOWFLAKE_ODBC_DRIVER" +#define COLON_CHAR_LENGTH 1 +#define NULL_CHAR_LENGTH 1 + +namespace sf +{ + + void SecureStorageImpl::convertTarget(const char *host, + const char *username, + const char *credType, + wchar_t *target_wcs, + unsigned int max_len) + { + std::vector buf(max_len); + char* targetname(&buf[0]); + size_t converted_len = 0; + + targetname[0] = '\0'; + strncat_s(targetname, max_len, host, strlen(host)); + strncat_s(targetname, max_len, ":", COLON_CHAR_LENGTH); + strncat_s(targetname, max_len, username, strlen(username)); + strncat_s(targetname, max_len, ":", COLON_CHAR_LENGTH); + strncat_s(targetname, max_len, DRIVER_NAME, strlen(DRIVER_NAME)); + strncat_s(targetname, max_len, ":", COLON_CHAR_LENGTH); + strncat_s(targetname, max_len, credType, strlen(credType)); + + mbstowcs_s(&converted_len, target_wcs, max_len, targetname, max_len); + } + + SECURE_STORAGE_STATUS SecureStorageImpl::storeToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + unsigned int max_len = strlen(host) + strlen(username) + strlen(DRIVER_NAME) + + strlen(credType) + (3 * COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + std::vector buf(max_len); + wchar_t* target_wcs(buf.data()); + CREDENTIALW creds = { 0 }; + + convertTarget(host, username, credType, target_wcs, max_len); + + creds.TargetName = target_wcs; + creds.CredentialBlobSize = strlen(token); + creds.CredentialBlob = (LPBYTE)token; + creds.Persist = CRED_PERSIST_LOCAL_MACHINE; + creds.Type = CRED_TYPE_GENERIC; + + if (!CredWriteW(&creds, 0)) + { + SF_INFO_LOG("sf", "SecureStorageWin", "storeToken", + "Failed to store token.", ""); + return FAILURE; + } + else + { + SF_DEBUG_LOG("sf","SecureStorageWin","storeToken", + "Successfulyy stored id token",""); + return SUCCESS; + } + } + + SECURE_STORAGE_STATUS SecureStorageImpl::retrieveToken(const char *host, + const char *username, + const char *credType, + char *token, + size_t *token_len) + { + unsigned int max_len = strlen(host) + strlen(username) + strlen(DRIVER_NAME) + + strlen(credType) + (3 * COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + std::vector buf(max_len); + wchar_t* target_wcs(buf.data()); + PCREDENTIALW retcreds = { 0 }; + DWORD blobSize = 0; + + SF_DEBUG_LOG("sf", "SecureStorageWin", "retrieveToken", + "Inside retrieveToken implementation", ""); + + convertTarget(host, username, credType, target_wcs, max_len); + + if (!CredReadW(target_wcs, CRED_TYPE_GENERIC, 0, &retcreds)) + { + SF_INFO_LOG("sf", "SecureStorageWin", "retrieveToken", + "Failed to read target or could not find it", ""); + return FAILURE; + } + else + { + SF_DEBUG_LOG("sf", "SecureStorageWin", "retrieveToken", + "Read the token now copying it", ""); + + blobSize = retcreds->CredentialBlobSize; + if (!blobSize) + { + return FAILURE; + } + strncpy_s(token, MAX_TOKEN_LEN, (char *)retcreds->CredentialBlob, size_t(blobSize)+1); + SF_DEBUG_LOG("sf", "SecureStorageWin", "retrieveToken", + "Read the token, copied it will return now.", ""); + } + + *token_len = size_t(blobSize); + CredFree(retcreds); + return SUCCESS; + } + + SECURE_STORAGE_STATUS SecureStorageImpl::updateToken(const char *host, + const char *username, + const char *credType, + const char *token) + { + return storeToken(host, username, credType, token); + } + + SECURE_STORAGE_STATUS SecureStorageImpl::removeToken(const char *host, + const char *username, + const char *credType) + { + unsigned int max_len = strlen(host) + strlen(username) + strlen(DRIVER_NAME) + + strlen(credType) + (3 * COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; + std::vector buf(max_len); + wchar_t* target_wcs(buf.data()); + convertTarget(host, username, credType, target_wcs, max_len); + + if (!CredDeleteW(target_wcs, CRED_TYPE_GENERIC, 0)) + { + return FAILURE; + } + else + { + return SUCCESS; + } + } +} diff --git a/cpp/platform/SecureStorageWin.hpp b/cpp/platform/SecureStorageWin.hpp new file mode 100644 index 0000000000..f83e7c49bb --- /dev/null +++ b/cpp/platform/SecureStorageWin.hpp @@ -0,0 +1,107 @@ +/* + * File: SecureStorage.hpp * + * Copyright (c) 2013-2021 Snowflake Computing + */ + +#ifndef PROJECT_SECURESTORAGE_WIN_H +#define PROJECT_SECURESTORAGE_WIN_H + +typedef enum +{ + NOT_FOUND, + FAILURE, + SUCCESS, + UNSUPPORTED +}SECURE_STORAGE_STATUS; + +namespace sf +{ + /** + * Class SecureStorage + */ + class SecureStorageImpl + { + static void convertTarget(const char *host, + const char *username, + const char *credType, + wchar_t *target_wcs, + unsigned int max_len); + + public: + + /** + * storeToken + * + * API to securely store credential in Windows Credential Manager + * + * @param host - snowflake host url + * @param username - snowflake user name + * @param credType - type of snowflake credential to be stored + * @param cred - credential to be secured + * + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS storeToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * retrieveToken + * + * API to retrieve credential from Windows Cred Manager + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username associated with + * the credential + * @param cred - snowflake credential to be retrieved + * @param cred - on return , populated credential extracted + * from the credential manager + * @param credLen - on return, length of the credential retrieved + * @return NOT_FOUND, ERROR, SUCCESS + */ + SECURE_STORAGE_STATUS retrieveToken(const char *host, + const char *username, + const char *credType, + char *cred, + size_t *credLen); + + /** + * updateToken + * + * API to update an existing credential in the credential manager. + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username assoicated with + * the credential + * @param credType - type of snowflake credential to be updated + * @param cred - credential to be stored in the credential manager. + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS updateToken(const char *host, + const char *username, + const char *credType, + const char *cred); + + /** + * removeToken + * + * API to remove a credential from the credential manager. + * + * @param host - snowflake host url associated + * with the credential + * @param username - snowflake username assoicated with + * the credential + * @param credType - type of credential to be removed. + * + * @return ERROR / SUCCESS + */ + SECURE_STORAGE_STATUS removeToken(const char *host, + const char *username, + const char *credType); + }; +} + +#endif //PROJECT_SECURESTORAGE_WIN_H From 13a58357d82fe672bb8d9acca185a70f1b60b3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Szczerbi=C5=84ski?= Date: Wed, 9 Oct 2024 15:03:34 +0200 Subject: [PATCH 2/3] Implement MFA Token Caching --- CMakeLists.txt | 24 +- cpp/lib/CacheFile.cpp | 227 ++++++++++++++++++ cpp/lib/CacheFile.hpp | 31 +++ cpp/lib/CredentialCache.cpp | 205 ++++++++++++++++ cpp/lib/CredentialCache.hpp | 51 ++++ cpp/platform/FileLock.cpp | 79 ++++++ cpp/platform/FileLock.hpp | 42 ++++ cpp/platform/SecureStorage.cpp | 87 +++---- cpp/platform/SecureStorage.hpp | 44 ++-- cpp/platform/SecureStorageApple.cpp | 224 ++++++++--------- ...StorageApple.hpp => SecureStorageImpl.hpp} | 71 +++--- cpp/platform/SecureStorageUnSup.cpp | 54 +++-- cpp/platform/SecureStorageUnSup.hpp | 106 -------- cpp/platform/SecureStorageWin.cpp | 166 ++++++------- cpp/platform/SecureStorageWin.hpp | 107 --------- include/snowflake/client.h | 6 + include/snowflake/mfa_token_cache.h | 29 +++ lib/client.c | 14 +- lib/connection.c | 24 +- tests/CMakeLists.txt | 16 +- .../test_data/expected_first_mfa_request.json | 19 ++ .../expected_second_mfa_request.json | 20 ++ tests/mock/test_data/first_mfa_response.json | 16 ++ tests/mock/test_mock_mfa_token_caching.c | 134 +++++++++++ tests/test_manual_connect.c | 51 +++- tests/test_unit_credential_cache.cpp | 30 +++ 26 files changed, 1310 insertions(+), 567 deletions(-) create mode 100644 cpp/lib/CacheFile.cpp create mode 100644 cpp/lib/CacheFile.hpp create mode 100644 cpp/lib/CredentialCache.cpp create mode 100644 cpp/lib/CredentialCache.hpp create mode 100644 cpp/platform/FileLock.cpp create mode 100644 cpp/platform/FileLock.hpp rename cpp/platform/{SecureStorageApple.hpp => SecureStorageImpl.hpp} (55%) delete mode 100644 cpp/platform/SecureStorageUnSup.hpp delete mode 100644 cpp/platform/SecureStorageWin.hpp create mode 100644 include/snowflake/mfa_token_cache.h create mode 100644 tests/mock/test_data/expected_first_mfa_request.json create mode 100644 tests/mock/test_data/expected_second_mfa_request.json create mode 100644 tests/mock/test_data/first_mfa_response.json create mode 100644 tests/mock/test_mock_mfa_token_caching.c create mode 100644 tests/test_unit_credential_cache.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 529f322af2..bdecf7c900 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ set(SOURCE_FILES include/snowflake/logger.h include/snowflake/version.h include/snowflake/platform.h + include/snowflake/mfa_token_cache.h lib/client.c lib/constants.h lib/cJSON.h @@ -128,7 +129,8 @@ set(SOURCE_FILES lib/chunk_downloader.h lib/chunk_downloader.c lib/mock_http_perform.h - lib/http_perform.c) + lib/http_perform.c +) set (SOURCE_FILES_PUT_GET cpp/EncryptionProvider.cpp @@ -213,6 +215,7 @@ set(SOURCE_FILES_CPP_WRAPPER include/snowflake/SFURL.hpp include/snowflake/CurlDesc.hpp include/snowflake/CurlDescPool.hpp + cpp/jwt/jwtWrapper.cpp cpp/lib/Exceptions.cpp cpp/lib/Connection.cpp cpp/lib/Statement.cpp @@ -235,7 +238,18 @@ set(SOURCE_FILES_CPP_WRAPPER cpp/lib/ResultSetJson.hpp cpp/lib/Authenticator.cpp cpp/lib/Authenticator.hpp - cpp/jwt/jwtWrapper.cpp + cpp/lib/CacheFile.cpp + cpp/lib/CacheFile.hpp + cpp/lib/CredentialCache.cpp + cpp/lib/CredentialCache.hpp + cpp/platform/SecureStorage.cpp + cpp/platform/SecureStorage.hpp + cpp/platform/SecureStorageApple.cpp + cpp/platform/SecureStorageImpl.hpp + cpp/platform/SecureStorageUnSup.cpp + cpp/platform/SecureStorageWin.cpp + cpp/platform/FileLock.cpp + cpp/platform/FileLock.hpp cpp/util/SnowflakeCommon.cpp cpp/util/SFURL.cpp cpp/util/CurlDesc.cpp @@ -245,7 +259,8 @@ set(SOURCE_FILES_CPP_WRAPPER lib/result_set_json.h lib/query_context_cache.h lib/curl_desc_pool.h - lib/authenticator.h) + lib/authenticator.h +) if (UNIX) if (LINUX) @@ -406,6 +421,7 @@ if (LINUX) deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/uuid/include + deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include include lib) endif() @@ -421,6 +437,7 @@ if (APPLE) deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/aws/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/azure/include deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/cmocka/include + deps-build/${PLATFORM}/${CMAKE_BUILD_TYPE}/picojson/include include lib) endif() @@ -435,6 +452,7 @@ if (WIN32) deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/aws/include deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/azure/include deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/cmocka/include + deps-build/${PLATFORM}/${VSDIR}/${CMAKE_BUILD_TYPE}/picojson/include include lib) if (CMAKE_SIZEOF_VOID_P EQUAL 8) diff --git a/cpp/lib/CacheFile.cpp b/cpp/lib/CacheFile.cpp new file mode 100644 index 0000000000..f1a69d23a8 --- /dev/null +++ b/cpp/lib/CacheFile.cpp @@ -0,0 +1,227 @@ +// +// Created by Jakub Szczerbinski on 14.10.24. +// + +#include +#include +#include + +#include "picojson.h" + +#include "CacheFile.hpp" +#include "CredentialCache.hpp" +#include "snowflake/platform.h" +#include "../logger/SFLogger.hpp" + +#if defined(__linux__) || defined(__APPLE__) +#include +#endif + +namespace Snowflake { + +namespace Client { + + const char* CREDENTIAL_FILE_NAME = "temporary_credential.json"; + + const std::vector CACHE_ROOT_ENV_VARS = + { + "SF_TEMPORARY_CREDENTIAL_CACHE_DIR", + "HOME", + "TMP" + }; + + const std::vector CACHE_DIR_NAMES = + { + ".cache", + "snowflake" + }; + + + bool mkdirIfNotExists(const char *dir) + { + int result = sf_mkdir(dir); + if (result == 0) + { + CXX_LOG_DEBUG("Created %s directory.", dir); + return true; + } + + if (errno == EEXIST) + { + CXX_LOG_TRACE("Directory already exists %s directory.", dir); + return true; + } + + CXX_LOG_ERROR("Failed to create %s directory. Error: %d", dir, errno); + return false; + + } + + boost::optional findCacheDirRoot() { + for (auto const &envVar: CACHE_ROOT_ENV_VARS) + { + char *root = getenv(envVar.c_str()); + if (root != nullptr) + { + return std::string(root); + } + } + return {}; + } + + boost::optional getCredentialFilePath() + { + const auto cacheDirRootOpt = findCacheDirRoot(); + if (!cacheDirRootOpt) + { + return {}; + } + const std::string &cacheDirRoot = cacheDirRootOpt.value(); + + if (!mkdirIfNotExists(cacheDirRoot.c_str())) + { + return {}; + } + + std::string cacheDir = cacheDirRoot; + for (const auto &name: CACHE_DIR_NAMES) + { + cacheDir += PATH_SEP + name; + if (!mkdirIfNotExists(cacheDir.c_str())) + { + return {}; + } + } + return cacheDir + PATH_SEP + CREDENTIAL_FILE_NAME; + }; + + std::string credItemStr(const CredentialKey &key) + { + return key.host + ":" + key.user + ":SNOWFLAKE-ODBC-DRIVER:" + credTypeToString(key.type); + } + + void ensureObject(picojson::value &val) + { + if (!val.is()) + { + val = picojson::value(picojson::object()); + } + } + + std::string readFile(const std::string &path, picojson::value &result) { + if (!boost::filesystem::exists(path)) + { + result = picojson::value(picojson::object()); + return {}; + } + + std::ifstream cacheFile(path); + if (!cacheFile.is_open()) + { + return "Failed to open the file(path=" + path + ")"; + } + + std::string error = picojson::parse(result, cacheFile); + if (!error.empty()) + { + return "Failed to parse the file: " + error; + } + return {}; + } + +#if defined(__linux__) || defined(__APPLE__) + bool ensurePermissions(const std::string& path, mode_t mode) + { + if (chmod(path.c_str(), mode) == -1) + { + CXX_LOG_ERROR("Cannot ensure permissions. chmod(%s, %o) failed with errno=%d", path.c_str(), mode, errno); + return false; + } + + return true; + } +#else + bool ensurePermissions(const std::string& path, unsigned mode) + { + CXX_LOG_ERROR("Cannot ensure permissions on current platform"); + return false; + } +#endif + + std::string writeFile(const std::string &path, const picojson::value &result) { + std::ofstream cacheFile(path, std::ios_base::trunc); + if (!cacheFile.is_open()) + { + return "Failed to open the file"; + } + + if (!ensurePermissions(path, 0700)) + { + return "Cannot ensure correct permissions on a file"; + } + + cacheFile << result.serialize(true); + return {}; + } + + void cacheFileUpdate(picojson::value &cache, const CredentialKey &key, const std::string &credential) + { + ensureObject(cache); + picojson::object &obj = cache.get(); + auto pair = obj.emplace(key.account, picojson::value(picojson::object())); + auto accountCacheIt = pair.first; + + ensureObject(accountCacheIt->second); + accountCacheIt->second.get().emplace(credItemStr(key), credential); + } + + void cacheFileRemove(picojson::value &cache, const CredentialKey &key) + { + ensureObject(cache); + picojson::object &cacheObj = cache.get(); + + auto accountCacheIt = cacheObj.find(key.account); + if (accountCacheIt == cacheObj.end()) + { + return; + } + + ensureObject(accountCacheIt->second); + picojson::object &accountCacheObj = accountCacheIt->second.get(); + accountCacheObj.erase(credItemStr(key)); + if (accountCacheObj.empty()) + { + cacheObj.erase(accountCacheIt); + } + } + + boost::optional cacheFileGet(picojson::value &cache, const CredentialKey &key) { + ensureObject(cache); + picojson::object &cacheObj = cache.get(); + + auto accountCacheIt = cacheObj.find(key.account); + if (accountCacheIt == cacheObj.end()) + { + return {}; + } + + ensureObject(accountCacheIt->second); + picojson::object &accountCacheObj = accountCacheIt->second.get(); + auto it = accountCacheObj.find(credItemStr(key)); + + if (it == accountCacheObj.end()) + { + return {}; + } + + if (!it->second.is()) + { + return {}; + } + + return it->second.get(); + } + +} + +} diff --git a/cpp/lib/CacheFile.hpp b/cpp/lib/CacheFile.hpp new file mode 100644 index 0000000000..3ae0729ef4 --- /dev/null +++ b/cpp/lib/CacheFile.hpp @@ -0,0 +1,31 @@ +#ifndef SNOWFLAKECLIENT_CACHEFILE_HPP +#define SNOWFLAKECLIENT_CACHEFILE_HPP + +#include +#include + +#include "picojson.h" + +#include "CredentialCache.hpp" + +namespace Snowflake { + +namespace Client { + + boost::optional getCredentialFilePath(); + + std::string readFile(const std::string &path, picojson::value &result); + + std::string writeFile(const std::string &path, const picojson::value &result); + + void cacheFileUpdate(picojson::value &cache, const CredentialKey &key, const std::string &credential); + + void cacheFileRemove(picojson::value &cache, const CredentialKey &key); + + boost::optional cacheFileGet(picojson::value &cache, const CredentialKey &key); + +} + +} + +#endif // SNOWFLAKECLIENT_CACHEFILE_HPP diff --git a/cpp/lib/CredentialCache.cpp b/cpp/lib/CredentialCache.cpp new file mode 100644 index 0000000000..574e993a3b --- /dev/null +++ b/cpp/lib/CredentialCache.cpp @@ -0,0 +1,205 @@ +// +// Created by Jakub Szczerbinski on 10.10.24. +// + +#include +#include +#include +#include + +#include "picojson.h" + +#include "snowflake/mfa_token_cache.h" + +#include "CredentialCache.hpp" +#include "../platform/SecureStorage.hpp" +#include "CacheFile.hpp" +#include "../logger/SFLogger.hpp" +#include "../platform/FileLock.hpp" + +namespace Snowflake { + +namespace Client { + + class FileCredentialCache : public CredentialCache { + public: + + boost::optional get(const CredentialKey& key) override + { + auto pathOpt = getCredentialFilePath(); + if (!pathOpt) + { + CXX_LOG_ERROR("Cannot get path to credential file.") + return {}; + } + const std::string path = pathOpt.value(); + + FileLock lock(path); + if (!lock.isLocked()) + { + CXX_LOG_ERROR("Failed to get token. Could not acquire file lock(path=%s)", lock.getPath().c_str()); + } + + picojson::value contents; + + std::string error = readFile(path, contents); + if (!error.empty()) { + CXX_LOG_WARN("Failed to read file(path=%s). Error: %s", path.c_str(), error.c_str()); + contents = picojson::value(picojson::object()); + } + + return cacheFileGet(contents, key); + } + + bool save(const CredentialKey& key, const std::string& credential) override + { + auto pathOpt = getCredentialFilePath(); + if (!pathOpt) + { + CXX_LOG_ERROR("Cannot get path to credential file.") + return {}; + } + + const std::string path = pathOpt.value(); + + FileLock lock(path); + if (!lock.isLocked()) + { + CXX_LOG_ERROR("Failed to save token. Could not acquire file lock(path=%s)", lock.getPath().c_str()); + } + + picojson::value contents; + std::string error = readFile(path, contents); + if (!error.empty()) { + CXX_LOG_WARN("Failed to read file(path=%s). Error: %s", path.c_str(), error.c_str()) + contents = picojson::value(picojson::object()); + } + + cacheFileUpdate(contents, key, credential); + + error = writeFile(path, contents); + if (!error.empty()) { + CXX_LOG_ERROR("Failed to write file(path=%s). Error: %s", path.c_str(), error.c_str()); + return false; + } + + return true; + } + + bool remove(const CredentialKey& key) override + { + auto pathOpt = getCredentialFilePath(); + if (!pathOpt) + { + CXX_LOG_ERROR("Cannot get path to credential file.") + return {}; + } + const std::string path = pathOpt.value(); + + FileLock lock(path); + if (!lock.isLocked()) + { + CXX_LOG_ERROR("Failed to delete token. Could not acquire file lock(path=%s)", lock.getPath().c_str()); + } + + picojson::value contents; + std::string error = readFile(path, contents); + if (!error.empty()) + { + CXX_LOG_WARN("Failed to read file(path=%s). Error: %s", path.c_str(), error.c_str()) + contents = picojson::value(picojson::object()); + } + + cacheFileRemove(contents, key); + + error = writeFile(path, contents); + if (!error.empty()) { + CXX_LOG_ERROR("Failed to write file(path=%s). Error: %s", path.c_str(), error.c_str()); + return false; + } + return true; + } + + ~FileCredentialCache() override = default; + }; + + class SecureStorageCredentialCache : public CredentialCache { + public: + boost::optional get(const CredentialKey &key) override { + return storage.retrieveToken(key.host, key.user, credTypeToString(key.type)); + } + + bool save(const CredentialKey &key, const std::string &credential) override { + return storage.storeToken(key.host, key.user, credTypeToString(key.type), credential); + } + + bool remove(const CredentialKey &key) override { + return storage.removeToken(key.host, key.user, credTypeToString(key.type)); + } + + ~SecureStorageCredentialCache() override = default; + + private: + SecureStorage storage; + }; + + CredentialCache *CredentialCache::make() { +#if defined(_WIN32) || defined(__APPLE__) + return new SecureStorageCredentialCache(); +#else + return new FileCredentialCache(); +#endif + } + +} + +} + + +#ifdef __cplusplus +extern "C" { + using namespace Snowflake::Client; +#endif + +cred_cache_ptr cred_cache_init() { + return CredentialCache::make(); +} + +char* cred_cache_get_credential(cred_cache_ptr tc, const char* account, const char* host, const char* user, CredentialType type) +{ + Snowflake::Client::CredentialKey key = { account, host, user, type }; + auto tokenOpt = reinterpret_cast(tc)->get(key); + if (!tokenOpt) + { + return nullptr; + } + size_t result_size = tokenOpt->size() + 1; + char* result = new char[result_size]; + strncpy(result, tokenOpt->c_str(), result_size); + return result; +} + +void cred_cache_free_credential(char* cred) { + delete[] cred; +} + +void cred_cache_save_credential(cred_cache_ptr tc, const char* account, const char* host, const char* user, CredentialType type, const char *cred) +{ + Snowflake::Client::CredentialKey key = { account, host, user, type }; + reinterpret_cast(tc)->save(key, std::string(cred)); +} + +void cred_cache_remove_credential(cred_cache_ptr tc, const char* account, const char* host, const char* user, CredentialType type) +{ + Snowflake::Client::CredentialKey key = { account, host, user, type }; + reinterpret_cast(tc)->remove(key); +} + +void cred_cache_term(cred_cache_ptr tc) { + delete reinterpret_cast(tc); +} + +#ifdef __cplusplus +}; +#endif + diff --git a/cpp/lib/CredentialCache.hpp b/cpp/lib/CredentialCache.hpp new file mode 100644 index 0000000000..4454be9d9b --- /dev/null +++ b/cpp/lib/CredentialCache.hpp @@ -0,0 +1,51 @@ +// +// Created by Jakub Szczerbinski on 10.10.24. +// + +#ifndef SNOWFLAKECLIENT_CREDENTIALCACHE_HPP +#define SNOWFLAKECLIENT_CREDENTIALCACHE_HPP + +#include +#include +#include + +#include "snowflake/mfa_token_cache.h" + +namespace Snowflake { + +namespace Client { + + struct CredentialKey { + std::string account; + std::string host; + std::string user; + CredentialType type; + }; + + class CredentialCache { + public: + static CredentialCache *make(); + + virtual boost::optional get(const CredentialKey &key) = 0; + + virtual bool save(const CredentialKey &key, const std::string &credential) = 0; + + virtual bool remove(const CredentialKey &key) = 0; + + virtual ~CredentialCache() = default; + }; + + inline std::string credTypeToString(CredentialType type) { + switch (type) { + case CredentialType::MFA_TOKEN: + return "MFA_TOKEN"; + default: + return "UNKNOWN"; + } + } + +} + +} + +#endif //SNOWFLAKECLIENT_CREDENTIALCACHE_HPP diff --git a/cpp/platform/FileLock.cpp b/cpp/platform/FileLock.cpp new file mode 100644 index 0000000000..25a1066265 --- /dev/null +++ b/cpp/platform/FileLock.cpp @@ -0,0 +1,79 @@ +// +// Created by Jakub Szczerbinski on 15.10.24. +// + +#include "FileLock.hpp" + +#include +#include +#include + +#include "../logger/SFLogger.hpp" + +namespace Snowflake { + +namespace Client { + + using Snowflake::Client::SFLogger; + + constexpr int MAX_LOCK_RETRIES = 10; + constexpr std::chrono::seconds lock_retry_sleep(1); + constexpr const char *lock_suffix = ".lck"; + + FileLock::FileLock(const std::string &path_) + : path{path_ + lock_suffix}, + locked(false) { + // Try to create file lock + for (int retries = 0; !locked && retries < MAX_LOCK_RETRIES; retries++) { + bool retry = try_lock(); + + if (!retry) { + break; + } + + std::this_thread::sleep_for(lock_retry_sleep); + } + + if (!locked) { + CXX_LOG_INFO("Failed to acquire file lock(path=%s), resetting the lock", path.c_str()); + boost::system::error_code ec; + bool removed = boost::filesystem::remove(path, ec); + + if (!removed || ec) { + CXX_LOG_ERROR("Failed to reset file lock(path=%s), removing lock failed with: %d", path.c_str(), ec.value()) + } else { + try_lock(); + } + } + } + + FileLock::~FileLock() { + if (locked) { + boost::system::error_code ec; + bool removed = boost::filesystem::remove(path, ec); + if (!removed || ec) { + CXX_LOG_ERROR("Failed to release file lock(path=%s)", path.c_str()); + } + } + } + + bool FileLock::try_lock() { + boost::system::error_code ec; + bool created = boost::filesystem::create_directory(path, ec); + + if (ec) { + CXX_LOG_ERROR("Failed to acquire file lock(path=%s) with error code: %d", path.c_str(), ec.value()); + return false; + } + + if (!created) { + CXX_LOG_INFO("Failed to acquire file lock(path=%s), lock already acquired"); + return true; + } + + locked = true; + return false; + } +} + +} diff --git a/cpp/platform/FileLock.hpp b/cpp/platform/FileLock.hpp new file mode 100644 index 0000000000..2ebdec99b5 --- /dev/null +++ b/cpp/platform/FileLock.hpp @@ -0,0 +1,42 @@ +// +// Created by Jakub Szczerbinski on 15.10.24. +// + +#ifndef SNOWFLAKECLIENT_FILELOCK_HPP +#define SNOWFLAKECLIENT_FILELOCK_HPP + +#include + +namespace Snowflake { + +namespace Client { + + class FileLock { + public: + explicit FileLock(const std::string& path); + + // Disable copying + FileLock(const FileLock &) = delete; + FileLock &operator=(const FileLock &) = delete; + + inline bool isLocked() const { + return locked; + } + + inline const std::string &getPath() const { + return path; + } + + ~FileLock(); + + private: + bool try_lock(); + + std::string path; + bool locked; + }; +} + +} + +#endif //SNOWFLAKECLIENT_FILELOCK_HPP diff --git a/cpp/platform/SecureStorage.cpp b/cpp/platform/SecureStorage.cpp index 7415112229..cee8c17b74 100644 --- a/cpp/platform/SecureStorage.cpp +++ b/cpp/platform/SecureStorage.cpp @@ -3,88 +3,79 @@ * Copyright (c) 2013-2020 Snowflake Computing */ -#include "Logger.hpp" -#ifdef __APPLE__ -#include "SecureStorageApple.hpp" -#elif _WIN32 -#include "SecureStorageWin.hpp" -#else -#include "SecureStorageUnSup.hpp" -#endif +#include "../logger/SFLogger.hpp" +#include "SecureStorageImpl.hpp" #include "SecureStorage.hpp" -namespace sf -{ +namespace Snowflake { - bool SecureStorage::storeToken(const char *host, - const char *username, - const char *credType, - const char *token) +namespace Client { + + using Snowflake::Client::SFLogger; + + bool SecureStorage::storeToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& token) { SecureStorageImpl secStorage; - if (secStorage.storeToken(host, username, credType, token) != SUCCESS) + if (secStorage.storeToken(host, username, credType, token) != SecureStorageStatus::Success) { - SF_ERROR_LOG("sf", "SecureStorage", "storeToken", - "Failed to store secure token%s", ""); + CXX_LOG_ERROR("Failed to store secure token"); return false; } - SF_DEBUG_LOG("sf", "SecureStorage", "storeToken", - "Successfully stored secure token%s", ""); + CXX_LOG_DEBUG("Successfully stored secure token"); return true; } - bool SecureStorage::retrieveToken(const char *host, - const char *username, - const char *credType, - char *token, - size_t *tokenLen) + boost::optional SecureStorage::retrieveToken(const std::string& host, + const std::string& username, + const std::string& credType) { SecureStorageImpl secStorage; - if (secStorage.retrieveToken(host, username, credType, token, tokenLen) != SUCCESS) + std::string result; + if (secStorage.retrieveToken(host, username, credType, result) != SecureStorageStatus::Success) { - SF_ERROR_LOG("sf", "SecureStorage", "retrieveToken", - "Failed to retrieve secure token%s", ""); - return false; + CXX_LOG_ERROR("Failed to retrieve secure token"); + return {}; } - SF_DEBUG_LOG("sf", "SecureStorage", "storeToken", - "Successfully retrieved secure tokeni%s", ""); - return true; + CXX_LOG_DEBUG("Successfully retrieved secure token"); + return result; } - bool SecureStorage::updateToken(const char *host, - const char *username, - const char *credType, - const char *token) + bool SecureStorage::updateToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& token) { SecureStorageImpl secStorage; - if ( secStorage.updateToken(host, username, credType, token) != SUCCESS) + if ( secStorage.updateToken(host, username, credType, token) != SecureStorageStatus::Success) { - SF_ERROR_LOG("sf", "SecureStorage", "retrieveToken", - "Failed to update secure token%s", ""); + CXX_LOG_ERROR("Failed to update secure token"); return false; } - SF_DEBUG_LOG("sf", "SecureStorage", "updateToken", - "Successfully updated secure token%s", ""); + CXX_LOG_DEBUG("Successfully updated secure token"); return true; } - bool SecureStorage::removeToken(const char *host, - const char *username, - const char *credType) + bool SecureStorage::removeToken(const std::string& host, + const std::string& username, + const std::string& credType) { SecureStorageImpl secStorage; - if ( secStorage.removeToken(host, username, credType) != SUCCESS) + if ( secStorage.removeToken(host, username, credType) != SecureStorageStatus::Success) { - SF_ERROR_LOG("sf", "SecureStorage", "removeToken", - "Failed to remove secure token%s", ""); + CXX_LOG_ERROR("Failed to remove secure token"); return false; } - SF_DEBUG_LOG("sf", "SecureStorage", "removeToken", - "Successfully removed secure token%s", ""); + CXX_LOG_DEBUG("Successfully removed secure token"); return true; } + +} + } diff --git a/cpp/platform/SecureStorage.hpp b/cpp/platform/SecureStorage.hpp index 2ab89b39ab..e130091fbe 100644 --- a/cpp/platform/SecureStorage.hpp +++ b/cpp/platform/SecureStorage.hpp @@ -6,10 +6,14 @@ #ifndef PROJECT_SECURESTORAGE_H #define PROJECT_SECURESTORAGE_H -#include +#include +#include +#include -namespace sf -{ + +namespace Snowflake { + +namespace Client { /** * Class SecureStorage */ @@ -29,10 +33,10 @@ namespace sf * @param cred - temporary credential to store * @return True / False */ - bool storeToken(const char *host, - const char *username, - const char *credType, - const char *cred); + bool storeToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& cred); /** * retrieveToken @@ -49,11 +53,9 @@ namespace sf * @param tokenLen - on return, length of the credential retrieved * @return True / False */ - bool retrieveToken(const char *host, - const char *username, - const char *credType, - char *token, - size_t *tokenLen); + boost::optional retrieveToken(const std::string& host, + const std::string& username, + const std::string& credType); /** * updateToken @@ -68,13 +70,13 @@ namespace sf * @param token - credential to be stored in the keychain. * @return True / False */ - bool updateToken(const char *host, - const char *username, - const char *credType, - const char *token); + bool updateToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& token); /** - * removeToken + * remove * * API to remove a token from the keychain. * @@ -85,10 +87,12 @@ namespace sf * @param credType - type of credential to be removed * @return True / False */ - bool removeToken(const char *host, - const char *username, - const char *credType); + bool removeToken(const std::string& host, + const std::string& username, + const std::string& credType); }; } +} + #endif //PROJECT_SECURESTORAGE_H diff --git a/cpp/platform/SecureStorageApple.cpp b/cpp/platform/SecureStorageApple.cpp index d9aca9128e..eb2da695c1 100644 --- a/cpp/platform/SecureStorageApple.cpp +++ b/cpp/platform/SecureStorageApple.cpp @@ -3,43 +3,45 @@ * Copyright (c) 2013-2020 Snowflake Computing */ -#include -#include "Snowflake.h" -#include "Logger.hpp" +#ifdef __APPLE__ + +#include "SecureStorageImpl.hpp" + +#include "../logger/SFLogger.hpp" + #include "CoreFoundation/CoreFoundation.h" #include "Security/Security.h" -#include "SecureStorageApple.hpp" +#include +#include #define MAX_TOKEN_LEN 1024 #define DRIVER_NAME "SNOWFLAKE_ODBC_DRIVER" #define COLON_CHAR_LENGTH 1 #define NULL_CHAR_LENGTH 1 -using namespace Simba; -namespace sf +namespace Snowflake +{ + +namespace Client { - void SecureStorageImpl::convertTarget(const char *host, - const char *username, - const char *credType, - char *targetname, - size_t max_len) + using Snowflake::Client::SFLogger; + + std::string SecureStorageImpl::convertTarget(const std::string& host, + const std::string& username, + const std::string& credType) { - simba_strcat(targetname, max_len, host); - simba_strcat(targetname, max_len, ":"); - simba_strcat(targetname, max_len, username); - simba_strcat(targetname, max_len, ":"); - simba_strcat(targetname, max_len, DRIVER_NAME); - simba_strcat(targetname, max_len, ":"); - simba_strcat(targetname, max_len, credType); + std::stringstream ss; + ss << host << ":" << username << ":" << DRIVER_NAME << ":" << credType; + return ss.str(); } - SECURE_STORAGE_STATUS SecureStorageImpl::storeToken(const char *host, - const char *username, - const char *credType, - const char *token) + SecureStorageStatus SecureStorageImpl::storeToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& cred) { /* * More on OS X Types can be read here: @@ -54,11 +56,7 @@ namespace sf * strlen(host)+strlen(username)+strlen(drivername)+strlen(credType)+ (3 * strlen(':')) * Add 1 to accommodate the NULL character. */ - size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ - strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - char *targetname = new char[target_max_len]; - convertTarget(host, username, credType, targetname, target_max_len); - + std::string target = convertTarget(host, username, credType); keys[0] = kSecClass; keys[1] = kSecAttrServer; @@ -66,9 +64,9 @@ namespace sf keys[3] = kSecValueData; values[0] = kSecClassInternetPassword; - values[1] = CFStringCreateWithCString(kCFAllocatorDefault, targetname, kCFStringEncodingUTF8); - values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8); - values[3] = CFStringCreateWithCString(kCFAllocatorDefault, token, kCFStringEncodingUTF8); + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, target.c_str(), kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username.c_str(), kCFStringEncodingUTF8); + values[3] = CFStringCreateWithCString(kCFAllocatorDefault, cred.c_str(), kCFStringEncodingUTF8); CFDictionaryRef query; query = CFDictionaryCreate(kCFAllocatorDefault, (const void**) keys, (const void**) values, 4, NULL, NULL); @@ -77,104 +75,78 @@ namespace sf if (result == errSecDuplicateItem) { - SF_DEBUG_LOG("sf", "SecureStorageApple", "storeToken", - "Token already exists, updatingi%s", ""); - return updateToken(host, username, credType, token); + CXX_LOG_DEBUG("Token already exists, updating"); + return updateToken(host, username, credType, cred); } - else if (result == errSecSuccess) - { - SF_DEBUG_LOG("sf", "SecureStorageApple", "storeToken", - "Successfully stored secure token%s", ""); - return SUCCESS; - } - else + + if (result != errSecSuccess) { - SF_ERROR_LOG("sf", "SecureStorageApple", "storeToken", - "Failed to store secure token%s", ""); - return ERROR; + CXX_LOG_ERROR("Failed to store secure token"); + return SecureStorageStatus::Error; } + + CXX_LOG_DEBUG("Successfully stored secure token"); + return SecureStorageStatus::Success; } - SECURE_STORAGE_STATUS SecureStorageImpl::retrieveToken(const char *host, - const char *username, - const char *credType, - char *token, - size_t *tokenLen) + SecureStorageStatus SecureStorageImpl::retrieveToken(const std::string& host, + const std::string& username, + const std::string& credType, + std::string& cred) { - OSStatus result = errSecSuccess; SecKeychainItemRef pitem = NULL; UInt32 plength = 0; char *pdata = NULL; - size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ - strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - char *targetname = new char[target_max_len]; - convertTarget(host, username, credType, targetname, target_max_len); + std::string target = convertTarget(host, username, credType); - if (tokenLen == NULL) - { - // tokenLen is expected to be Non NULL - SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", - "Token len has to be allocated%s", ""); - return ERROR; - } + CFTypeRef keys[5]; + keys[0] = kSecClass; + keys[1] = kSecAttrServer; + keys[2] = kSecAttrAccount; + keys[3] = kSecReturnData; + keys[4] = kSecReturnAttributes; - /* - * https://developer.apple.com/documentation/security/1397763-seckeychainfindinternetpassword?language=objc - */ - result = SecKeychainFindInternetPassword(NULL, (uint32)strlen(targetname), - targetname, 0, NULL, - (uint32) strlen(username), - username, 0, NULL, 0, - kSecProtocolTypeAny, - kSecAuthenticationTypeAny, - &plength, (void **)&pdata, - &pitem); - - if (result == errSecItemNotFound) - { - *tokenLen = 0; - token = NULL; - SF_ERROR_LOG("sf", "SecureStorageApple", "storeToken", - "Failed to retrieve secure token - %s", "Token Not Found"); - return NOT_FOUND; - } - else if (result == errSecSuccess) + CFTypeRef values[5]; + values[0] = kSecClassInternetPassword; + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, target.c_str(), kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username.c_str(), kCFStringEncodingUTF8); + values[3] = kCFBooleanTrue; + values[4] = kCFBooleanTrue; + + CFDictionaryRef query = CFDictionaryCreate(kCFAllocatorDefault, (const void**) keys, (const void**) values, 5, NULL, NULL); + CFDictionaryRef result; + OSStatus status = SecItemCopyMatching(query, reinterpret_cast(&result)); + + if (status == errSecItemNotFound) { - *tokenLen = plength; - if (plength > MAX_TOKEN_LEN) - { - SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", - "Failed to retrieve secure token - %s", "Stored token length greater than max allowed length"); - return ERROR; - } - - SF_DEBUG_LOG("sf", "SecureStorageApple", "retrieveToken", - "Successfully retrieved token%s", ""); - strlcpy(token, pdata, plength+1); - return SUCCESS; + cred = ""; + CXX_LOG_ERROR("Failed to retrieve secure token - %s", "Token Not Found"); + return SecureStorageStatus::NotFound; } - else + + if (status != errSecSuccess) { - *tokenLen = 0; - token = NULL; - SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", - "Failed to retrieve secure token%s", ""); - return ERROR; + cred = ""; + CXX_LOG_ERROR("Failed to retrieve secure token"); + return SecureStorageStatus::Error; } + + CXX_LOG_DEBUG("Successfully retrieved token"); + + auto val = reinterpret_cast(CFDictionaryGetValue(result, kSecValueData)); + cred = std::string(reinterpret_cast(CFDataGetBytePtr(val)), CFDataGetLength(val)); + return SecureStorageStatus::Success; } - SECURE_STORAGE_STATUS SecureStorageImpl::updateToken(const char *host, - const char *username, - const char *credType, - const char *token) + SecureStorageStatus SecureStorageImpl::updateToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& token) { OSStatus result = errSecSuccess; CFTypeRef keys[4]; CFTypeRef values[4]; - size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ - strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - char *targetname = new char[target_max_len]; - convertTarget(host, username, credType, targetname, target_max_len); + std::string target = convertTarget(host, username, credType); keys[0] = kSecClass; keys[1] = kSecAttrServer; @@ -182,8 +154,8 @@ namespace sf keys[3] = kSecMatchLimit; values[0] = kSecClassInternetPassword; - values[1] = CFStringCreateWithCString(kCFAllocatorDefault, targetname, kCFStringEncodingUTF8); - values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8); + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, target.c_str(), kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username.c_str(), kCFStringEncodingUTF8); values[3] = kSecMatchLimitOne; CFDictionaryRef extract_query = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, @@ -192,25 +164,21 @@ namespace sf if (result != errSecSuccess && result != errSecItemNotFound) { - SF_ERROR_LOG("sf", "SecureStorageApple", "retrieveToken", - "Failed to update secure token%s", ""); - return ERROR; + CXX_LOG_ERROR("Failed to update secure token"); + return SecureStorageStatus::Error; } return storeToken(host, username, credType, token); } - SECURE_STORAGE_STATUS SecureStorageImpl::removeToken(const char *host, - const char *username, - const char *credType) + SecureStorageStatus SecureStorageImpl::removeToken(const std::string& host, + const std::string& username, + const std::string& credType) { OSStatus result = errSecSuccess; CFTypeRef keys[4]; CFTypeRef values[4]; - size_t target_max_len = strlen(host)+strlen(username)+strlen(DRIVER_NAME)+ - strlen(credType) + (3*COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - char *targetname = new char[target_max_len]; - convertTarget(host, username, credType, targetname, target_max_len); + std::string target = convertTarget(host, username, credType); keys[0] = kSecClass; keys[1] = kSecAttrServer; @@ -218,8 +186,8 @@ namespace sf keys[3] = kSecMatchLimit; values[0] = kSecClassInternetPassword; - values[1] = CFStringCreateWithCString(kCFAllocatorDefault, targetname, kCFStringEncodingUTF8); - values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username, kCFStringEncodingUTF8); + values[1] = CFStringCreateWithCString(kCFAllocatorDefault, target.c_str(), kCFStringEncodingUTF8); + values[2] = CFStringCreateWithCString(kCFAllocatorDefault, username.c_str(), kCFStringEncodingUTF8); values[3] = kSecMatchLimitOne; CFDictionaryRef extract_query = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, @@ -227,13 +195,15 @@ namespace sf result = SecItemDelete(extract_query); if (result != errSecSuccess && result != errSecItemNotFound) { - SF_ERROR_LOG("sf", "SecureStorageApple", "removeToken", - "Failed to remove secure token%s", ""); - return ERROR; + CXX_LOG_ERROR("Failed to remove secure token"); + return SecureStorageStatus::Error; } - SF_DEBUG_LOG("sf", "SecureStorageApple", "removeToken", - "Successfully removed secure token%s", ""); - return SUCCESS; + CXX_LOG_DEBUG("Successfully removed secure token"); + return SecureStorageStatus::Success; } } + +} + +#endif diff --git a/cpp/platform/SecureStorageApple.hpp b/cpp/platform/SecureStorageImpl.hpp similarity index 55% rename from cpp/platform/SecureStorageApple.hpp rename to cpp/platform/SecureStorageImpl.hpp index 2d11d016b8..0131187fe3 100644 --- a/cpp/platform/SecureStorageApple.hpp +++ b/cpp/platform/SecureStorageImpl.hpp @@ -3,32 +3,31 @@ * Copyright (c) 2013-2020 Snowflake Computing */ -#ifndef PROJECT_SECURESTORAGE_APPLE_H -#define PROJECT_SECURESTORAGE_APPLE_H +#ifndef PROJECT_SECURESTORAGE_IMPL_H +#define PROJECT_SECURESTORAGE_IMPL_H -#include +#include -typedef enum +enum class SecureStorageStatus { - NOT_FOUND, - ERROR, - SUCCESS, - UNSUPPORTED -}SECURE_STORAGE_STATUS; + NotFound, + Error, + Success, + Unsupported +}; -namespace sf -{ +namespace Snowflake { + +namespace Client { /** * Class SecureStorage */ class SecureStorageImpl { - static void convertTarget(const char *host, - const char *username, - const char *credType, - char *targetname, - size_t max_len); + static std::string convertTarget(const std::string& host, + const std::string& username, + const std::string& credType); public: @@ -44,10 +43,10 @@ namespace sf * * @return ERROR / SUCCESS */ - SECURE_STORAGE_STATUS storeToken(const char *host, - const char *username, - const char *credType, - const char *cred); + SecureStorageStatus storeToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& cred); /** * retrieveToken @@ -64,11 +63,10 @@ namespace sf * @param credLen - on return, length of the credential retrieved * @return NOT_FOUND, ERROR, SUCCESS */ - SECURE_STORAGE_STATUS retrieveToken(const char *host, - const char *username, - const char *credType, - char *cred, - size_t *credLen); + SecureStorageStatus retrieveToken(const std::string& host, + const std::string& username, + const std::string& credType, + std::string& cred); /** * updateToken @@ -83,13 +81,13 @@ namespace sf * @param cred - credential to be stored in the keychain. * @return ERROR / SUCCESS */ - SECURE_STORAGE_STATUS updateToken(const char *host, - const char *username, - const char *credType, - const char *cred); + SecureStorageStatus updateToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& cred); /** - * removeToken + * remove * * API to remove a credential from the keychain. * @@ -98,13 +96,16 @@ namespace sf * @param username - snowflake username assoicated with * the credential * @param credType - type of credential to be removed. - * + * * @return ERROR / SUCCESS */ - SECURE_STORAGE_STATUS removeToken(const char *host, - const char *username, - const char *credType); + SecureStorageStatus removeToken(const std::string& host, + const std::string& username, + const std::string& credType); }; + +} + } -#endif //PROJECT_SECURESTORAGE_APPLE_H +#endif //PROJECT_SECURESTORAGE_IMPL_H diff --git a/cpp/platform/SecureStorageUnSup.cpp b/cpp/platform/SecureStorageUnSup.cpp index 74834bf8fe..85d642c446 100644 --- a/cpp/platform/SecureStorageUnSup.cpp +++ b/cpp/platform/SecureStorageUnSup.cpp @@ -3,40 +3,48 @@ * Copyright (c) 2013-2020 Snowflake Computing */ -#include "SecureStorageUnSup.hpp" +#if !(defined(__APPLE__) || defined(_WIN32)) -namespace sf -{ +#include "SecureStorageImpl.hpp" - SECURE_STORAGE_STATUS SecureStorageImpl::storeToken(const char *host, - const char *username, - const char *credType, - const char *token) +#include + +namespace Snowflake { + +namespace Client { + + SecureStorageStatus SecureStorageImpl::storeToken(const std::string &host, + const std::string &username, + const std::string &credType, + const std::string &token) { - return UNSUPPORTED; + return SecureStorageStatus::Unsupported; } - SECURE_STORAGE_STATUS SecureStorageImpl::retrieveToken(const char *host, - const char *username, - const char *credType, - char *token, - size_t *token_len) + SecureStorageStatus SecureStorageImpl::retrieveToken(const std::string &host, + const std::string &username, + const std::string &credType, + std::string &token) { - return UNSUPPORTED; + return SecureStorageStatus::Unsupported; } - SECURE_STORAGE_STATUS SecureStorageImpl::updateToken(const char *host, - const char *username, - const char *credType, - const char *token) + SecureStorageStatus SecureStorageImpl::updateToken(const std::string &host, + const std::string &username, + const std::string &credType, + const std::string &token) { - return UNSUPPORTED; + return SecureStorageStatus::Unsupported; } - SECURE_STORAGE_STATUS SecureStorageImpl::removeToken(const char *host, - const char *username, - const char *credType) + SecureStorageStatus SecureStorageImpl::removeToken(const std::string &host, + const std::string &username, + const std::string &credType) { - return UNSUPPORTED; + return SecureStorageStatus::Unsupported; } } + +} + +#endif diff --git a/cpp/platform/SecureStorageUnSup.hpp b/cpp/platform/SecureStorageUnSup.hpp deleted file mode 100644 index f0e4e53960..0000000000 --- a/cpp/platform/SecureStorageUnSup.hpp +++ /dev/null @@ -1,106 +0,0 @@ -/* - * File: SecureStorageUnSup.hpp * - * Copyright (c) 2013-2020 Snowflake Computing - */ -#ifndef PROJECT_SECURESTORAGE_UNSUP_H -#define PROJECT_SECURESTORAGE_UNSUP_H - -#include - -typedef enum -{ - NOT_FOUND, - ERROR, - SUCCESS, - UNSUPPORTED -}SECURE_STORAGE_STATUS; - -namespace sf -{ - /** - * Class SecureStorage - */ - class SecureStorageImpl - { - static void convertTarget(const char *host, - const char *username, - const char *credType, - char *targetname); - - public: - - /** - * storeToken - * - * API to securely store token - * - * @param host - snowflake host url - * @param username - snowflake user name - * @param credType - type of snowflake credential to be stored - * @param cred - credential to be secured - * - * @return ERROR / SUCCESS - */ - SECURE_STORAGE_STATUS storeToken(const char *host, - const char *username, - const char *credType, - const char *cred); - - /** - * retrieveToken - * - * API to retrieve token - * - * @param host - snowflake host url associated - * with the credential - * @param username - snowflake username associated with - * the credential - * @param cred - snowflake credential to be retrieved - * @param cred - on return , populated credential extracted - * @param credLen - on return, length of the credential retrieved - * @return NOT_FOUND, ERROR, SUCCESS - */ - SECURE_STORAGE_STATUS retrieveToken(const char *host, - const char *username, - const char *credType, - char *cred, - size_t *credLen); - - /** - * updateCredential - * - * API to update an existing token. - * - * @param host - snowflake host url associated - * with the credential - * @param username - snowflake username assoicated with - * the credential - * @param credType - type of snowflake credential to be updated - * @param cred - credential to be stored - * @return ERROR / SUCCESS - */ - SECURE_STORAGE_STATUS updateToken(const char *host, - const char *username, - const char *credType, - const char *cred); - - /** - * removeToken - * - * API to remove a token. - * - * @param host - snowflake host url associated - * with the credential - * @param username - snowflake username assoicated with - * the credential - * @param credType - type of credential to be removed - * - * @return ERROR / SUCCESS - */ - SECURE_STORAGE_STATUS removeToken(const char *host, - const char *username, - const char *credType); - }; -} - -#endif //PROJECT_SECURESTORAGE_UNSUP_H diff --git a/cpp/platform/SecureStorageWin.cpp b/cpp/platform/SecureStorageWin.cpp index 8468e6ed41..4ca20b8d10 100644 --- a/cpp/platform/SecureStorageWin.cpp +++ b/cpp/platform/SecureStorageWin.cpp @@ -3,145 +3,129 @@ * Copyright (c) 2013-2020 Snowflake Computing */ -#include "Logger.hpp" +#ifdef _WIN32 + +#include "../logger//SFLogger.hpp" #include "windows.h" #include "wincred.h" -#include "SecureStorageWin.hpp" +#include "SecureStorageImpl.hpp" #include +#include +#include +#include #define MAX_TOKEN_LEN 1024 #define DRIVER_NAME "SNOWFLAKE_ODBC_DRIVER" #define COLON_CHAR_LENGTH 1 #define NULL_CHAR_LENGTH 1 -namespace sf +namespace Snowflake +{ + +namespace Client { - void SecureStorageImpl::convertTarget(const char *host, - const char *username, - const char *credType, - wchar_t *target_wcs, - unsigned int max_len) + using Snowflake::Client::SFLogger; + + std::string SecureStorageImpl::convertTarget(const std::string& host, + const std::string& username, + const std::string& credType) { - std::vector buf(max_len); - char* targetname(&buf[0]); - size_t converted_len = 0; - - targetname[0] = '\0'; - strncat_s(targetname, max_len, host, strlen(host)); - strncat_s(targetname, max_len, ":", COLON_CHAR_LENGTH); - strncat_s(targetname, max_len, username, strlen(username)); - strncat_s(targetname, max_len, ":", COLON_CHAR_LENGTH); - strncat_s(targetname, max_len, DRIVER_NAME, strlen(DRIVER_NAME)); - strncat_s(targetname, max_len, ":", COLON_CHAR_LENGTH); - strncat_s(targetname, max_len, credType, strlen(credType)); - - mbstowcs_s(&converted_len, target_wcs, max_len, targetname, max_len); + std::stringstream ss; + ss << host << ":" << username << ":" << DRIVER_NAME << ":" << credType; + return ss.str(); } - SECURE_STORAGE_STATUS SecureStorageImpl::storeToken(const char *host, - const char *username, - const char *credType, - const char *token) + SecureStorageStatus SecureStorageImpl::storeToken(const std::string &host, + const std::string& username, + const std::string& credType, + const std::string& token) { - unsigned int max_len = strlen(host) + strlen(username) + strlen(DRIVER_NAME) + - strlen(credType) + (3 * COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - std::vector buf(max_len); - wchar_t* target_wcs(buf.data()); - CREDENTIALW creds = { 0 }; - - convertTarget(host, username, credType, target_wcs, max_len); + std::string target = convertTarget(host, username, credType); + std::wstring wide_target = std::wstring(target.begin(), target.end()); - creds.TargetName = target_wcs; - creds.CredentialBlobSize = strlen(token); - creds.CredentialBlob = (LPBYTE)token; + CREDENTIALW creds = { 0 }; + creds.TargetName = (LPWSTR)wide_target.data(); + creds.CredentialBlobSize = token.size(); + creds.CredentialBlob = (LPBYTE)token.data(); creds.Persist = CRED_PERSIST_LOCAL_MACHINE; creds.Type = CRED_TYPE_GENERIC; if (!CredWriteW(&creds, 0)) { - SF_INFO_LOG("sf", "SecureStorageWin", "storeToken", - "Failed to store token.", ""); - return FAILURE; + CXX_LOG_ERROR("Failed to store token."); + return SecureStorageStatus::Error; } else { - SF_DEBUG_LOG("sf","SecureStorageWin","storeToken", - "Successfulyy stored id token",""); - return SUCCESS; + CXX_LOG_DEBUG("Successfulyy stored id token"); + return SecureStorageStatus::Success; } } - SECURE_STORAGE_STATUS SecureStorageImpl::retrieveToken(const char *host, - const char *username, - const char *credType, - char *token, - size_t *token_len) + SecureStorageStatus SecureStorageImpl::retrieveToken(const std::string& host, + const std::string& username, + const std::string& credType, + std::string& token) { - unsigned int max_len = strlen(host) + strlen(username) + strlen(DRIVER_NAME) + - strlen(credType) + (3 * COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - std::vector buf(max_len); - wchar_t* target_wcs(buf.data()); - PCREDENTIALW retcreds = { 0 }; - DWORD blobSize = 0; - - SF_DEBUG_LOG("sf", "SecureStorageWin", "retrieveToken", - "Inside retrieveToken implementation", ""); + std::string target = convertTarget(host, username, credType); + std::wstring wide_target = std::wstring(target.begin(), target.end()); + PCREDENTIALW retcreds = nullptr; - convertTarget(host, username, credType, target_wcs, max_len); - - if (!CredReadW(target_wcs, CRED_TYPE_GENERIC, 0, &retcreds)) + if (!CredReadW(wide_target.data(), CRED_TYPE_GENERIC, 0, &retcreds)) { - SF_INFO_LOG("sf", "SecureStorageWin", "retrieveToken", - "Failed to read target or could not find it", ""); - return FAILURE; + CXX_LOG_ERROR("Failed to read target or could not find it"); + return SecureStorageStatus::Error; } - else + + CXX_LOG_DEBUG("Read the token now copying it"); + + DWORD blobSize = retcreds->CredentialBlobSize; + if (!blobSize) { - SF_DEBUG_LOG("sf", "SecureStorageWin", "retrieveToken", - "Read the token now copying it", ""); - - blobSize = retcreds->CredentialBlobSize; - if (!blobSize) - { - return FAILURE; - } - strncpy_s(token, MAX_TOKEN_LEN, (char *)retcreds->CredentialBlob, size_t(blobSize)+1); - SF_DEBUG_LOG("sf", "SecureStorageWin", "retrieveToken", - "Read the token, copied it will return now.", ""); + return SecureStorageStatus::Error; } - *token_len = size_t(blobSize); + token = ""; + std::copy( + retcreds->CredentialBlob, + retcreds->CredentialBlob + blobSize, + std::back_insert_iterator(token) + ); + + CXX_LOG_DEBUG("Copied token"); + CredFree(retcreds); - return SUCCESS; + return SecureStorageStatus::Success; } - SECURE_STORAGE_STATUS SecureStorageImpl::updateToken(const char *host, - const char *username, - const char *credType, - const char *token) + SecureStorageStatus SecureStorageImpl::updateToken(const std::string& host, + const std::string& username, + const std::string& credType, + const std::string& token) { return storeToken(host, username, credType, token); } - SECURE_STORAGE_STATUS SecureStorageImpl::removeToken(const char *host, - const char *username, - const char *credType) + SecureStorageStatus SecureStorageImpl::removeToken(const std::string& host, + const std::string& username, + const std::string& credType) { - unsigned int max_len = strlen(host) + strlen(username) + strlen(DRIVER_NAME) + - strlen(credType) + (3 * COLON_CHAR_LENGTH) + NULL_CHAR_LENGTH; - std::vector buf(max_len); - wchar_t* target_wcs(buf.data()); - convertTarget(host, username, credType, target_wcs, max_len); + std::string target = convertTarget(host, username, credType); + std::wstring wide_target = std::wstring(target.begin(), target.end()); - if (!CredDeleteW(target_wcs, CRED_TYPE_GENERIC, 0)) + if (!CredDeleteW(wide_target.data(), CRED_TYPE_GENERIC, 0)) { - return FAILURE; + return SecureStorageStatus::Error; } else { - return SUCCESS; + return SecureStorageStatus::Success; } } } + +} + +#endif \ No newline at end of file diff --git a/cpp/platform/SecureStorageWin.hpp b/cpp/platform/SecureStorageWin.hpp deleted file mode 100644 index f83e7c49bb..0000000000 --- a/cpp/platform/SecureStorageWin.hpp +++ /dev/null @@ -1,107 +0,0 @@ -/* - * File: SecureStorage.hpp * - * Copyright (c) 2013-2021 Snowflake Computing - */ - -#ifndef PROJECT_SECURESTORAGE_WIN_H -#define PROJECT_SECURESTORAGE_WIN_H - -typedef enum -{ - NOT_FOUND, - FAILURE, - SUCCESS, - UNSUPPORTED -}SECURE_STORAGE_STATUS; - -namespace sf -{ - /** - * Class SecureStorage - */ - class SecureStorageImpl - { - static void convertTarget(const char *host, - const char *username, - const char *credType, - wchar_t *target_wcs, - unsigned int max_len); - - public: - - /** - * storeToken - * - * API to securely store credential in Windows Credential Manager - * - * @param host - snowflake host url - * @param username - snowflake user name - * @param credType - type of snowflake credential to be stored - * @param cred - credential to be secured - * - * @return ERROR / SUCCESS - */ - SECURE_STORAGE_STATUS storeToken(const char *host, - const char *username, - const char *credType, - const char *cred); - - /** - * retrieveToken - * - * API to retrieve credential from Windows Cred Manager - * - * @param host - snowflake host url associated - * with the credential - * @param username - snowflake username associated with - * the credential - * @param cred - snowflake credential to be retrieved - * @param cred - on return , populated credential extracted - * from the credential manager - * @param credLen - on return, length of the credential retrieved - * @return NOT_FOUND, ERROR, SUCCESS - */ - SECURE_STORAGE_STATUS retrieveToken(const char *host, - const char *username, - const char *credType, - char *cred, - size_t *credLen); - - /** - * updateToken - * - * API to update an existing credential in the credential manager. - * - * @param host - snowflake host url associated - * with the credential - * @param username - snowflake username assoicated with - * the credential - * @param credType - type of snowflake credential to be updated - * @param cred - credential to be stored in the credential manager. - * @return ERROR / SUCCESS - */ - SECURE_STORAGE_STATUS updateToken(const char *host, - const char *username, - const char *credType, - const char *cred); - - /** - * removeToken - * - * API to remove a credential from the credential manager. - * - * @param host - snowflake host url associated - * with the credential - * @param username - snowflake username assoicated with - * the credential - * @param credType - type of credential to be removed. - * - * @return ERROR / SUCCESS - */ - SECURE_STORAGE_STATUS removeToken(const char *host, - const char *username, - const char *credType); - }; -} - -#endif //PROJECT_SECURESTORAGE_WIN_H diff --git a/include/snowflake/client.h b/include/snowflake/client.h index 1dd5f53b30..2b7239ccac 100644 --- a/include/snowflake/client.h +++ b/include/snowflake/client.h @@ -14,6 +14,7 @@ extern "C" { #include "platform.h" #include "version.h" #include "logger.h" +#include "mfa_token_cache.h" /** * API Name @@ -263,6 +264,7 @@ typedef enum SF_ATTRIBUTE { SF_CON_DISABLE_QUERY_CONTEXT_CACHE, SF_CON_INCLUDE_RETRY_REASON, SF_CON_RETRY_TIMEOUT, + SF_CON_CLIENT_REQUEST_MFA_TOKEN, SF_CON_MAX_RETRY, SF_CON_MAX_VARCHAR_SIZE, SF_CON_MAX_BINARY_SIZE, @@ -327,6 +329,7 @@ typedef struct SF_CONNECT { sf_bool insecure_mode; sf_bool ocsp_fail_open; sf_bool autocommit; + sf_bool client_request_mfa_token; char *timezone; char *service_name; char *query_result_format; @@ -364,6 +367,9 @@ typedef struct SF_CONNECT { // the pointer of qcc instance void * qcc; + // MFA Token Cache + cred_cache_ptr token_cache; + // whether to include retry reason in retry for query request sf_bool include_retry_reason; diff --git a/include/snowflake/mfa_token_cache.h b/include/snowflake/mfa_token_cache.h new file mode 100644 index 0000000000..cfa4c26c16 --- /dev/null +++ b/include/snowflake/mfa_token_cache.h @@ -0,0 +1,29 @@ +// +// Created by Jakub Szczerbinski on 10.10.24. +// + +#ifndef SNOWFLAKECLIENT_MFA_TOKEN_CACHE_H +#define SNOWFLAKECLIENT_MFA_TOKEN_CACHE_H + +typedef void* cred_cache_ptr; + +typedef enum { + MFA_TOKEN +} CredentialType; + +#ifdef __cplusplus +extern "C" { +#endif + +cred_cache_ptr cred_cache_init(); +char* cred_cache_get_credential(cred_cache_ptr tc, const char* account, const char* host, const char* user, CredentialType type); +void cred_cache_free_credential(char* cred); +void cred_cache_save_credential(cred_cache_ptr tc, const char* account, const char* host, const char* user, CredentialType type, const char *cred); +void cred_cache_remove_credential(cred_cache_ptr tc, const char* account, const char* host, const char* user, CredentialType type); +void cred_cache_term(cred_cache_ptr tc); + +#ifdef __cplusplus +}; +#endif + +#endif //SNOWFLAKECLIENT_MFA_TOKEN_CACHE_H diff --git a/lib/client.c b/lib/client.c index 587cdb1692..5bb3bd2cd7 100644 --- a/lib/client.c +++ b/lib/client.c @@ -694,6 +694,11 @@ SF_CONNECT *STDCALL snowflake_init() { sf->insecure_mode = SF_BOOLEAN_FALSE; sf->ocsp_fail_open = SF_BOOLEAN_FALSE; sf->autocommit = SF_BOOLEAN_TRUE; +#if defined(__APPLE__) || defined(_WIN32) + sf->client_request_mfa_token = SF_BOOLEAN_TRUE; +#else + sf->client_request_mfa_token = SF_BOOLEAN_FALSE; +#endif sf->qcc_disable = SF_BOOLEAN_FALSE; sf->include_retry_reason = SF_BOOLEAN_TRUE; sf->timezone = NULL; @@ -774,7 +779,7 @@ SF_STATUS STDCALL snowflake_term(SF_CONNECT *sf) { } auth_terminate(sf); - + cred_cache_term(sf->token_cache); qcc_terminate(sf); _mutex_term(&sf->mutex_sequence_counter); @@ -953,6 +958,11 @@ SF_STATUS STDCALL snowflake_connect(SF_CONNECT *sf) { goto cleanup; } + char* mfa_token = NULL; + if (json_copy_string(&mfa_token, data, "mfaToken") == SF_JSON_ERROR_NONE && sf->token_cache) { + cred_cache_save_credential(sf->token_cache, sf->account, sf->host, sf->user, MFA_TOKEN, mfa_token); + } + _mutex_lock(&sf->mutex_parameters); ret = _set_parameters_session_info(sf, data); qcc_deserialize(sf, snowflake_cJSON_GetObjectItem(data, SF_QCC_RSP_KEY)); @@ -1164,6 +1174,8 @@ SF_STATUS STDCALL snowflake_set_attribute( case SF_CON_INCLUDE_RETRY_REASON: sf->include_retry_reason = value ? *((sf_bool *)value) : SF_BOOLEAN_TRUE; break; + case SF_CON_CLIENT_REQUEST_MFA_TOKEN: + sf->client_request_mfa_token = value ? *((sf_bool *) value): SF_BOOLEAN_TRUE; default: SET_SNOWFLAKE_ERROR(&sf->error, SF_STATUS_ERROR_BAD_ATTRIBUTE_TYPE, "Invalid attribute type", diff --git a/lib/connection.c b/lib/connection.c index 94661ac6d9..5f0ade100f 100644 --- a/lib/connection.c +++ b/lib/connection.c @@ -125,7 +125,6 @@ static uint32 uimax(uint32 a, uint32 b) { return (a > b) ? a : b; } - cJSON *STDCALL create_auth_json_body(SF_CONNECT *sf, const char *application, const char *int_app_name, @@ -157,6 +156,7 @@ cJSON *STDCALL create_auth_json_body(SF_CONNECT *sf, autocommit == SF_BOOLEAN_TRUE ? SF_BOOLEAN_INTERNAL_TRUE_STR : SF_BOOLEAN_INTERNAL_FALSE_STR); + snowflake_cJSON_AddStringToObject(session_parameters, "TIMEZONE", timezone); //Create Request Data JSON blob @@ -186,10 +186,28 @@ cJSON *STDCALL create_auth_json_body(SF_CONNECT *sf, { snowflake_cJSON_AddStringToObject(data, "EXT_AUTHN_DUO_METHOD", "push"); } + + if (sf->client_request_mfa_token) { + snowflake_cJSON_AddBoolToObject( + session_parameters, + "CLIENT_REQUEST_MFA_TOKEN", + 1 + ); + + if (sf->token_cache == NULL) { + sf->token_cache = cred_cache_init(); + } + + char* token = cred_cache_get_credential(sf->token_cache, sf->account, sf->host, sf->user, MFA_TOKEN); + if (token != NULL) + { + snowflake_cJSON_AddStringToObject(data, "TOKEN", token); + cred_cache_free_credential(token); + } + } } snowflake_cJSON_AddItemToObject(data, "CLIENT_ENVIRONMENT", client_env); - snowflake_cJSON_AddItemToObject(data, "SESSION_PARAMETERS", - session_parameters); + snowflake_cJSON_AddItemToObject(data, "SESSION_PARAMETERS", session_parameters); //Create body body = snowflake_cJSON_CreateObject(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ed2b26e965..99cf8d9daf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -73,7 +73,9 @@ SET(TESTS_CXX test_unit_snowflake_types_to_string test_unit_azure_client test_unit_query_context_cache - test_unit_sfurl) + test_unit_sfurl + test_unit_credential_cache +) SET(TESTS_PUTGET test_include_aws @@ -87,7 +89,9 @@ SET(TESTS_PERF SET(TESTS_MOCK test_mock_service_name - test_mock_session_gone) + test_mock_session_gone + test_mock_mfa_token_caching +) set(SOURCE_UTILS utils/test_setup.c @@ -271,6 +275,14 @@ if (MOCK) --error-exitcode=1 --suppressions=${VALGRIND_SUPPRESSION} ./${T}) + set(MOCK_TEST_DATA + expected_first_mfa_request.json + expected_second_mfa_request.json + first_mfa_response.json + ) + FOREACH (MTD ${MOCK_TEST_DATA}) + configure_file("mock/test_data/${MTD}" "mock/test_data/${MTD}" COPYONLY) + ENDFOREACH () ENDFOREACH () else() FOREACH (T ${TESTS_C}) diff --git a/tests/mock/test_data/expected_first_mfa_request.json b/tests/mock/test_data/expected_first_mfa_request.json new file mode 100644 index 0000000000..a48a99556e --- /dev/null +++ b/tests/mock/test_data/expected_first_mfa_request.json @@ -0,0 +1,19 @@ +{ + "data": { + "CLIENT_APP_ID": "C API", + "CLIENT_APP_VERSION": "0.0.0", + "ACCOUNT_NAME": "account", + "LOGIN_NAME": "user", + "PASSWORD": "passwd", + "EXT_AUTHN_DUO_METHOD": "passcode", + "PASSCODE": "passcode", + "CLIENT_ENVIRONMENT": { + "OS": "Linux", + "OS_VERSION": "0" + }, + "SESSION_PARAMETERS": { + "AUTOCOMMIT": "TRUE", + "CLIENT_REQUEST_MFA_TOKEN": true + } + } +} \ No newline at end of file diff --git a/tests/mock/test_data/expected_second_mfa_request.json b/tests/mock/test_data/expected_second_mfa_request.json new file mode 100644 index 0000000000..d3bf53ea5b --- /dev/null +++ b/tests/mock/test_data/expected_second_mfa_request.json @@ -0,0 +1,20 @@ +{ + "data": { + "CLIENT_APP_ID": "C API", + "CLIENT_APP_VERSION": "0.0.0", + "ACCOUNT_NAME": "account", + "LOGIN_NAME": "user", + "PASSWORD": "passwd", + "EXT_AUTHN_DUO_METHOD": "passcode", + "PASSCODE": "passcode", + "TOKEN": "MFA_TOKEN", + "CLIENT_ENVIRONMENT": { + "OS": "Linux", + "OS_VERSION": "0" + }, + "SESSION_PARAMETERS": { + "AUTOCOMMIT": "TRUE", + "CLIENT_REQUEST_MFA_TOKEN": true + } + } +} \ No newline at end of file diff --git a/tests/mock/test_data/first_mfa_response.json b/tests/mock/test_data/first_mfa_response.json new file mode 100644 index 0000000000..35c1403767 --- /dev/null +++ b/tests/mock/test_data/first_mfa_response.json @@ -0,0 +1,16 @@ +{ + "data" : { + "token" : "TOKEN", + "masterToken" : "MASTER_TOKEN", + "mfaToken": "MFA_TOKEN", + "sessionInfo": { + "databaseName": "testdb", + "schemaName": "testschema", + "roleName": "testRole", + "warehouseName": "wh" + } + }, + "message" : null, + "code" : null, + "success" : true +} \ No newline at end of file diff --git a/tests/mock/test_mock_mfa_token_caching.c b/tests/mock/test_mock_mfa_token_caching.c new file mode 100644 index 0000000000..ada5deb48a --- /dev/null +++ b/tests/mock/test_mock_mfa_token_caching.c @@ -0,0 +1,134 @@ + +#include +#include "../utils/test_setup.h" +#include "../utils/mock_setup.h" +#include + +const size_t MAX_PATH_LEN = 1024; + +char* load_data(const char* filename) { + char path[MAX_PATH_LEN]; + snprintf(path, MAX_PATH_LEN, "./mock/test_data/%s", filename); + FILE *fp = fopen(path, "r"); + if (fp == NULL) { + log_error("Failed to open the file %s", path); + return NULL; + } + fseek(fp, 0, SEEK_END); + size_t fsize = ftell(fp); + fseek(fp, 0, SEEK_SET); + char *string = malloc(fsize + 1); + fread(string, fsize, 1, fp); + fclose(fp); + return string; +} + +char* first_mfa_request = NULL; +char* first_mfa_response = NULL; +char* second_mfa_request = NULL; + +void setup_mfa_connect_mock_1() { + expect_string(__wrap_http_perform, url, "https://host:443/session/v1/login-request"); + expect_string(__wrap_http_perform, body, first_mfa_request); + expect_string(__wrap_http_perform, request_type_str, "POST"); + expect_value(__wrap_http_perform, header->header_service_name, NULL); + expect_value(__wrap_http_perform, header->header_token, NULL); + will_return(__wrap_http_perform, cast_ptr_to_largest_integral_type(first_mfa_response)); +} + +void setup_mfa_term_mock_1() { + expect_string(__wrap_http_perform, url, "https://host:443/session"); + expect_value(__wrap_http_perform, body, NULL); + expect_string(__wrap_http_perform, request_type_str, "POST"); + expect_value(__wrap_http_perform, header->header_service_name, NULL); + expect_string(__wrap_http_perform, header->header_token, "Authorization: Snowflake Token=\"TOKEN\""); + will_return(__wrap_http_perform, "{}"); +} + +void setup_mfa_connect_mock_2() { + expect_string(__wrap_http_perform, url, "https://host:443/session/v1/login-request"); + expect_string(__wrap_http_perform, body, second_mfa_request); + expect_string(__wrap_http_perform, request_type_str, "POST"); + expect_value(__wrap_http_perform, header->header_service_name, NULL); + expect_value(__wrap_http_perform, header->header_token, NULL); + will_return(__wrap_http_perform, cast_ptr_to_largest_integral_type(first_mfa_response)); +} + +void setup_mfa_term_mock_2() { + expect_string(__wrap_http_perform, url, "https://host:443/session"); + expect_value(__wrap_http_perform, body, NULL); + expect_string(__wrap_http_perform, request_type_str, "POST"); + expect_value(__wrap_http_perform, header->header_service_name, NULL); + expect_string(__wrap_http_perform, header->header_token, "Authorization: Snowflake Token=\"TOKEN\""); + will_return(__wrap_http_perform, "{}"); +} + +#define ACCOUNT "account" +#define HOST "host" +#define USER "user" + +SF_CONNECT* sf_connect_init() { + SF_CONNECT* sf = snowflake_init(); + snowflake_set_attribute(sf, SF_CON_ACCOUNT,ACCOUNT); + snowflake_set_attribute(sf, SF_CON_HOST, HOST); + snowflake_set_attribute(sf, SF_CON_USER, USER); + snowflake_set_attribute(sf, SF_CON_PORT, "443"); + snowflake_set_attribute(sf, SF_CON_PROTOCOL, "https"); + return sf; +} + +void test_mfa_token_caching(void **unused) { + sf_setenv("SF_TEMPORARY_CREDENTIAL_CACHE_DIR", "."); + cred_cache_ptr cred_cache = cred_cache_init(); + cred_cache_remove_credential(cred_cache, ACCOUNT, HOST, USER, MFA_TOKEN); + + { + SF_CONNECT *sf = sf_connect_init(); + snowflake_set_attribute(sf, SF_CON_PASSWORD, "passwd"); + snowflake_set_attribute(sf, SF_CON_PASSCODE, "passcode"); + sf_bool client_request_mfa_token = 1; + snowflake_set_attribute(sf, SF_CON_CLIENT_REQUEST_MFA_TOKEN, &client_request_mfa_token); + setup_mfa_connect_mock_1(); + SF_STATUS status = snowflake_connect(sf); + + if (status != SF_STATUS_SUCCESS) { + dump_error(&(sf->error)); + } + + assert_int_equal(status, SF_STATUS_SUCCESS); + setup_mfa_term_mock_1(); + snowflake_term(sf); + } + + { + SF_CONNECT *sf = sf_connect_init(); + snowflake_set_attribute(sf, SF_CON_PASSWORD, "passwd"); + snowflake_set_attribute(sf, SF_CON_PASSCODE, "passcode"); + sf_bool client_request_mfa_token = 1; + snowflake_set_attribute(sf, SF_CON_CLIENT_REQUEST_MFA_TOKEN, &client_request_mfa_token); + setup_mfa_connect_mock_2(); + SF_STATUS status = snowflake_connect(sf); + + if (status != SF_STATUS_SUCCESS) { + dump_error(&(sf->error)); + } + + assert_int_equal(status, SF_STATUS_SUCCESS); + setup_mfa_term_mock_2(); + snowflake_term(sf); + } +} + + int main(void) { + first_mfa_request = load_data("expected_first_mfa_request.json"); + first_mfa_response = load_data("first_mfa_response.json"); + second_mfa_request = load_data("expected_second_mfa_request.json"); + initialize_test(SF_BOOLEAN_FALSE); + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_mfa_token_caching), + }; + int ret = cmocka_run_group_tests(tests, NULL, NULL); + snowflake_global_term(); + free(first_mfa_request); + return ret; + } diff --git a/tests/test_manual_connect.c b/tests/test_manual_connect.c index 65ebf5a6c2..030a0e5b03 100644 --- a/tests/test_manual_connect.c +++ b/tests/test_manual_connect.c @@ -160,12 +160,57 @@ void test_mfa_connect_with_duo_passcodeInPassword(void** unused) snowflake_term(sf); } +void test_mfa_connect_with_mfa_cache(void** unused) +{ + /* + * Should trigger mfa push notification at most once. + * Make sure ALLOW_CLIENT_MFA_CACHING is set to true + * For more details refer to: https://docs.snowflake.com/en/user-guide/security-mfa#using-mfa-token-caching-to-minimize-the-number-of-prompts-during-authentication-optional + */ + for (int i = 0; i < 2; i++) { + SF_CONNECT *sf = snowflake_init(); + snowflake_set_attribute(sf, SF_CON_APPLICATION_NAME, "ODBC"); + snowflake_set_attribute(sf, SF_CON_APPLICATION_VERSION, "2.30.0"); + snowflake_set_attribute(sf, SF_CON_ACCOUNT, + getenv("SNOWFLAKE_TEST_ACCOUNT")); + snowflake_set_attribute(sf, SF_CON_USER, getenv("SNOWFLAKE_TEST_USER")); + snowflake_set_attribute(sf, SF_CON_PASSWORD, + getenv("SNOWFLAKE_TEST_PASSWORD")); + char *host, *port, *protocol, *passcode; + host = getenv("SNOWFLAKE_TEST_HOST"); + if (host) { + snowflake_set_attribute(sf, SF_CON_HOST, host); + } + port = getenv("SNOWFLAKE_TEST_PORT"); + if (port) { + snowflake_set_attribute(sf, SF_CON_PORT, port); + } + protocol = getenv("SNOWFLAKE_TEST_PROTOCOL"); + if (protocol) { + snowflake_set_attribute(sf, SF_CON_PROTOCOL, protocol); + } + passcode = getenv("SNOWFLAKE_TEST_PASSCODE"); + if (passcode) { + snowflake_set_attribute(sf, SF_CON_PASSCODE, passcode); + } else { + dump_error(&(sf->error)); + } + + SF_STATUS status = snowflake_connect(sf); + if (status != SF_STATUS_SUCCESS) { + dump_error(&(sf->error)); + } + assert_int_equal(status, SF_STATUS_SUCCESS); + snowflake_term(sf); + } +} + void test_none(void** unused) {} int main(void) { - initialize_test(SF_BOOLEAN_FALSE); + initialize_test(SF_BOOLEAN_TRUE); struct CMUnitTest tests[1] = { cmocka_unit_test(test_none) }; @@ -187,6 +232,10 @@ int main(void) tests[0].name = "test_mfa_connect_with_duo_passcodeInPassword"; tests[0].test_func = test_mfa_connect_with_duo_passcodeInPassword; } + else if (strcmp(manual_test, "test_mfa_connect_with_mfa_cache") == 0) { + tests[0].name = "test_mfa_connect_with_mfa_cache"; + tests[0].test_func = test_mfa_connect_with_mfa_cache; + } else { printf("No matching test found for: %s\n", manual_test); } diff --git a/tests/test_unit_credential_cache.cpp b/tests/test_unit_credential_cache.cpp new file mode 100644 index 0000000000..f9f7dbbfd4 --- /dev/null +++ b/tests/test_unit_credential_cache.cpp @@ -0,0 +1,30 @@ +// +// Created by Jakub Szczerbinski on 05.11.24. +// +#include "lib/CredentialCache.hpp" +#include "utils/test_setup.h" + +void test_credential_cache(void **unused) +{ + sf_setenv("SF_TEMPORARY_CREDENTIAL_CACHE_DIR", "."); + std::unique_ptr cache{Snowflake::Client::CredentialCache::make()}; + Snowflake::Client::CredentialKey key { "account", "host", "user", CredentialType::MFA_TOKEN }; + + std::string token = "example_token"; + assert_true(cache->save(key, token)); + assert_true(cache->get(key).value() == token); + + assert_true(cache->remove(key)); + assert_false(cache->get(key).has_value()); +} + +int main(void) { + /* Testing only file based credential cache */ +#ifndef __linux__ + return 0; +#endif + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_credential_cache), + }; + return cmocka_run_group_tests(tests, NULL, NULL); +} From b6fe929f79eae4ca01f37700ba24ec1a5534cd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Szczerbi=C5=84ski?= Date: Tue, 19 Nov 2024 16:31:01 +0100 Subject: [PATCH 3/3] After review fixes --- cpp/lib/CacheFile.cpp | 15 ++++++--------- cpp/platform/SecureStorageApple.cpp | 16 +++------------- cpp/platform/SecureStorageImpl.hpp | 8 ++++---- cpp/platform/SecureStorageWin.cpp | 4 +++- tests/mock/test_mock_mfa_token_caching.c | 2 ++ 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/cpp/lib/CacheFile.cpp b/cpp/lib/CacheFile.cpp index f1a69d23a8..6b05bb6ad4 100644 --- a/cpp/lib/CacheFile.cpp +++ b/cpp/lib/CacheFile.cpp @@ -1,7 +1,3 @@ -// -// Created by Jakub Szczerbinski on 14.10.24. -// - #include #include #include @@ -30,7 +26,7 @@ namespace Client { "TMP" }; - const std::vector CACHE_DIR_NAMES = + const std::vector CACHE_DIR_PATH = { ".cache", "snowflake" @@ -48,7 +44,7 @@ namespace Client { if (errno == EEXIST) { - CXX_LOG_TRACE("Directory already exists %s directory.", dir); + CXX_LOG_TRACE("Directory %s already exists.", dir); return true; } @@ -84,9 +80,9 @@ namespace Client { } std::string cacheDir = cacheDirRoot; - for (const auto &name: CACHE_DIR_NAMES) + for (const auto &segment: CACHE_DIR_PATH) { - cacheDir += PATH_SEP + name; + cacheDir += PATH_SEP + segment; if (!mkdirIfNotExists(cacheDir.c_str())) { return {}; @@ -97,6 +93,7 @@ namespace Client { std::string credItemStr(const CredentialKey &key) { +// TODO Make :SNOWFLAKE-ODBC-DRIVER: part more generic (support generic driver, PHP etc.) return key.host + ":" + key.user + ":SNOWFLAKE-ODBC-DRIVER:" + credTypeToString(key.type); } @@ -168,7 +165,7 @@ namespace Client { { ensureObject(cache); picojson::object &obj = cache.get(); - auto pair = obj.emplace(key.account, picojson::value(picojson::object())); + std::pair pair = obj.emplace(key.account, picojson::value(picojson::object())); auto accountCacheIt = pair.first; ensureObject(accountCacheIt->second); diff --git a/cpp/platform/SecureStorageApple.cpp b/cpp/platform/SecureStorageApple.cpp index eb2da695c1..8b52f72997 100644 --- a/cpp/platform/SecureStorageApple.cpp +++ b/cpp/platform/SecureStorageApple.cpp @@ -16,6 +16,7 @@ #include #define MAX_TOKEN_LEN 1024 +// TODO Make :SNOWFLAKE-ODBC-DRIVER: part more generic (support generic driver, PHP etc.) #define DRIVER_NAME "SNOWFLAKE_ODBC_DRIVER" #define COLON_CHAR_LENGTH 1 #define NULL_CHAR_LENGTH 1 @@ -49,13 +50,6 @@ namespace Client */ CFTypeRef keys[4]; CFTypeRef values[4]; - /* - * Concatenate Host & Username in the format: - * `host:username:drivername:credType' - * the target_max_len hence becomes : - * strlen(host)+strlen(username)+strlen(drivername)+strlen(credType)+ (3 * strlen(':')) - * Add 1 to accommodate the NULL character. - */ std::string target = convertTarget(host, username, credType); keys[0] = kSecClass; @@ -94,9 +88,6 @@ namespace Client const std::string& credType, std::string& cred) { - SecKeychainItemRef pitem = NULL; - UInt32 plength = 0; - char *pdata = NULL; std::string target = convertTarget(host, username, credType); CFTypeRef keys[5]; @@ -120,7 +111,7 @@ namespace Client if (status == errSecItemNotFound) { cred = ""; - CXX_LOG_ERROR("Failed to retrieve secure token - %s", "Token Not Found"); + CXX_LOG_ERROR("Failed to retrieve secure token. Reason: token not found."); return SecureStorageStatus::NotFound; } @@ -175,7 +166,6 @@ namespace Client const std::string& username, const std::string& credType) { - OSStatus result = errSecSuccess; CFTypeRef keys[4]; CFTypeRef values[4]; std::string target = convertTarget(host, username, credType); @@ -192,7 +182,7 @@ namespace Client CFDictionaryRef extract_query = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 4, NULL, NULL); - result = SecItemDelete(extract_query); + OSStatus result = SecItemDelete(extract_query); if (result != errSecSuccess && result != errSecItemNotFound) { CXX_LOG_ERROR("Failed to remove secure token"); diff --git a/cpp/platform/SecureStorageImpl.hpp b/cpp/platform/SecureStorageImpl.hpp index 0131187fe3..62545cba24 100644 --- a/cpp/platform/SecureStorageImpl.hpp +++ b/cpp/platform/SecureStorageImpl.hpp @@ -34,7 +34,7 @@ namespace Client { /** * storeToken * - * API to secure store credential in Apple Keychain + * API to secure store credential * * @param host - snowflake host url * @param username - snowflake user name @@ -51,7 +51,7 @@ namespace Client { /** * retrieveToken * - * API to retrieve credential from Apple Key Chain + * API to retrieve credential * * @param host - snowflake host url associated * with the credential @@ -71,7 +71,7 @@ namespace Client { /** * updateToken * - * API to update an existing credential in the keychain. + * API to update an existing credential. * * @param host - snowflake host url associated * with the credential @@ -89,7 +89,7 @@ namespace Client { /** * remove * - * API to remove a credential from the keychain. + * API to remove a credential. * * @param host - snowflake host url associated * with the credential diff --git a/cpp/platform/SecureStorageWin.cpp b/cpp/platform/SecureStorageWin.cpp index 4ca20b8d10..e6accadc90 100644 --- a/cpp/platform/SecureStorageWin.cpp +++ b/cpp/platform/SecureStorageWin.cpp @@ -59,7 +59,7 @@ namespace Client } else { - CXX_LOG_DEBUG("Successfulyy stored id token"); + CXX_LOG_DEBUG("Successfully stored id token"); return SecureStorageStatus::Success; } } @@ -123,6 +123,8 @@ namespace Client { return SecureStorageStatus::Success; } + + CXX_LOG_DEBUG("Successfully removed id token"); } } diff --git a/tests/mock/test_mock_mfa_token_caching.c b/tests/mock/test_mock_mfa_token_caching.c index ada5deb48a..165624760d 100644 --- a/tests/mock/test_mock_mfa_token_caching.c +++ b/tests/mock/test_mock_mfa_token_caching.c @@ -130,5 +130,7 @@ void test_mfa_token_caching(void **unused) { int ret = cmocka_run_group_tests(tests, NULL, NULL); snowflake_global_term(); free(first_mfa_request); + free(first_mfa_response); + free(second_mfa_request); return ret; }