When dealing with sensitive data, it should be protected from modification and unauthorized viewing as much as possible. This is what this ProtectedPtr class aims to do: encrypt your data when you aren't using it, and decrypt your data when you need access to it, using Windows CryptProtectMemory API.
The goals for this class are:
- Encrypt memory as long as possible
- Automatically decrypt data when it needs to be accessed
- Automatically re-encrypt data when possible
- Securely wipe all copies of unencrypted data at the end of their scope
- Manage protected data securely
I provide a constant time comparison function in the operator==, but for objects to be compared in constant time (for at least the only method I know of) they need to be in byte arrays. This is why the Serializer classes exist, to convert the data type being used by the ProtectedPtr into byte arrays, and to return a reference to the raw data. In a primitive type this is trivial, but for a user-created object, dereferencing the object itself may not return the data needed. For example in a std::pair, the pair::first and/or pair::second functions would need to be called in order to return the data that needs to be encrypted/decrypted/converted to byte arrays.
As the class already decrypts the data automatically, I was originally going to attempt to automatically encrypt the protected data after it is returned. So this would change the const operator* to:
const T& operator*() const
{
ProtectMemory(false);
T temp = *protectedData;
ProtectMemory(true);
return temp;
}
Because the data returned can't be modified, I figured I can return a copy of the unencrypted data and encrypted the original data.
My problem with this is that I have no control over the 'temp' variable. The user could keep it unencrypted for the length of the client program, making all the work of the ProtectedPtr class for naught.
Is there any security or usability problems or fixes I can apply? Any other areas of concern for this class?
#include "Windows.h"
#include "Wincrypt.h"
#include <string>
#include <memory>
#pragma comment(lib, "crypt32.lib")
#pragma once
namespace Protected_Ptr
{
//base class that converts data of type T to a byte array,
//gets size of data, and returns reference to raw data
template <class T>
class Serializer
{
public:
//return size of data
virtual std::size_t getSize(const T& obj) const = 0;
//return reference to raw data
virtual T* getRawData(T& obj) const = 0;
//convert data into byte array
virtual byte* serialize(T& obj) const = 0;
};
template <class T>
class PrimitiveSerializer : public Serializer<T>
{
public:
virtual std::size_t getSize(const T& obj) const { return sizeof(obj); }
virtual T* getRawData(T& obj) const { return &obj; }
virtual byte* serialize(T& obj) const
{
const size_t size = getSize(obj);
byte* out = new byte[size];
memcpy(out, getRawData(obj), size);
return out;
}
};
class StringSerializer : public Serializer<std::string>
{
public:
virtual std::size_t getSize(const std::string& str) const { return str.length(); }
virtual std::string* getRawData(std::string& str) const { return &str; }
virtual byte* serialize(std::string& str) const
{
const std::size_t size = getSize(str);
byte* out = new byte[size];
memcpy(out, str.c_str(), size);
return out;
}
};
template <class T, class S = PrimitiveSerializer<T>>
class ProtectedPtr
{
public:
explicit ProtectedPtr(bool wipeOnExit = true) noexcept
: protectedData(nullptr), overwriteOnExit(wipeOnExit) {};
explicit ProtectedPtr(T *obj, bool wipeOnExit = true)
: overrideOnExit(wipeOnExit)
{
assign(obj);
ProtectMemory(true);
}
explicit ProtectedPtr(ProtectedPtr other) : protectedData(nullptr)
{
other.swap(*this);
}
explicit ProtectedPtr(ProtectedPtr&& other) : protectedData(nullptr) noexcept
{
other.swap(*this);
}
~ProtectedPtr()
{
ProtectMemory(false);
SecureWipeData();
}
void SetWipeOnExit(bool wipe) { overwriteOnExit = wipe; }
bool IsProtected() const { return isEncrypted };
void ProtectMemory(bool encrypt)
{
size_t mod;
size_t dataBlockSize;
size_t dataSize = sizeof(*protectedData);
//CryptProtectMemory requires data to be a multiple of its block size
if (mod = dataSize % CRYPTPROTECTMEMORY_BLOCK_SIZE)
dataBlockSize = dataSize + (CRYPTPROTECTMEMORY_BLOCK_SIZE - mod);
else
dataBlockSize = dataSize;
if (encrypt && !isEncrypted)
{
isEncrypted = true;
if (!CryptProtectMemory(getRawPtr(), dataBlockSize,
CRYPTPROTECTMEMORY_SAME_PROCESS))
{
cerr << "CryptProtectMemory failed: " << GetLastError() << endl;
}
}
else if (!encrypt && isEncrypted)
{
isEncrypted = false;
if (!CryptUnprotectMemory(getRawPtr(), dataBlockSize,
CRYPTPROTECTMEMORY_SAME_PROCESS))
{
cerr << "CryptProtectMemory failed: " << GetLastError() << endl;
}
}
SecureZeroMemory(&mod, sizeof(mod));
SecureZeroMemory(&dataSize, sizeof(dataSize));
SecureZeroMemory(&dataBlockSize, sizeof(dataBlockSize));
}
void SecureWipeData()
{
if (overwriteOnExit)
SecureZeroMemory(getRawPtr(), sizeof(*protectedData));
}
void swap(ProtectedPtr& other) noexcept
{
using std::swap;
//make sure data is encrypted
ProtectMemory(true);
other.ProtectMemory(true);
swap(*this->protectedData, other.protectedData);
swap(*this->isEncrypted, other.isEncrypted);
swap(*this->overwriteOnExit, other.overwriteOnExit);
}
T& operator*()
{
ProtectMemory(false);
return *protectedData;
}
const T& operator*() const
{
ProtectMemory(false);
return *protectedData;
}
T* const operator->()
{
ProtectMemory(false);
return protectedData.operator->();
}
const T* const operator->() const
{
ProtectMemory(false);
return protectedData.operator->();
}
ProtectedPtr& operator=(ProtectedPtr rhs)
{
rhs.swap(*this);
return *this;
}
ProtectedPtr& operator=(ProtectedPtr&& rhs) noexcept
{
rhs.swap(*this);
return *this;
}
//constant time comparison
bool operator==(ProtectedPtr& other)
{
if (sizeof(*protectedData) != sizeof(*other))
return false;
volatile byte* thisData = serializeData();
ProtectMemory(true);
volatile byte* otherData = other.serializeData();
other.ProtectMemory(true);
volatile byte result = 0;
for (int i = 0; i < sizeof(*protectedData); i++)
{
result |= thisData[i] ^ otherData[i];
//securely wipe unencrypted copies of data
thisData[i] = 0;
otherData[i] = 0;
}
return result == 0;
}
bool operator!=(ProtectedPtr& other)
{
return !(*this == other);
}
explicit operator bool() const { return (bool)protectedData; }
T& get() { return this->operator*(); }
const T& data() const { return this->operator*(); }
void assign(T *obj)
{
//if protectedData is already pointing to something,
//securely overwrite and delete it
if (protectedData)
{
ProtectMemory(false);
SecureWipeData();
protectedData.release();
}
//point to copy of data, encrypt it, and overwrite
//original unencrypted data
protectedData = std::make_unique<T>(*obj);
ProtectMemory(true);
SecureZeroMemory(obj, sizeof(obj));
}
bool empty() const { return (bool)*this; }
private:
//returns reference to data pointed to
T* getRawPtr() { return serializer.getRawData(*protectedData); }
byte* serializeData()
{
ProtectMemory(false);
return serializer.serialize(*protectedData);
}
S serializer;
std::unique_ptr<T> protectedData;
bool isEncrypted = false;
bool overwriteOnExit;
};
template <class T>
void swap(ProtectedPtr<T>& lhs, ProtectedPtr<T>& rhs) noexcept { lhs.swap(rhs); }
}