Secure Memory Handling
Introduction
Cryptographic algorithms are only as secure as the secrets they protect. If an attacker can read your secret keys from memory, all the mathematical sophistication in the world won't help. This chapter explores how rippled protects sensitive data in memory, why it matters, and how to write code that doesn't leak secrets.
The Memory Problem
Where Secrets Live
void processTransaction() {
SecretKey sk = loadKeyFromFile();
// Secret key is now in memory:
// - Stack frame
// - CPU registers
// - Potentially CPU cache
// - Maybe swapped to disk
auto sig = sign(pk, sk, tx);
// Function returns
// Stack frame deallocated
// But what happens to the secret key bytes?
}
The problem: Memory isn't automatically erased when you're done with it.
Attack Vectors
1. Memory Dumps
// Process crashes
// Core dump written to disk
// Contains all process memory
// Including secret keys!
2. Swap Files
// System runs out of RAM
// Pages swapped to disk
// Secret keys written to swap file
// May persist even after process exits
3. Hibernation
// System hibernates
// All RAM written to hibernation file
// Includes secret keys
// File remains on disk until next boot
4. Cold Boot Attacks
// System powered off
// RAM still contains data for seconds/minutes
// Attacker boots different OS
// Reads RAM contents
// Recovers secret keys
5. Debugging/Inspection
// Debugger attached to process
// Can read all memory
// Can dump memory to file
// Secret keys exposed
The Solution: Secure Erasure
Why memset() Isn't Enough
// ❌ WRONG - Compiler may optimize this away
void clearKey(uint8_t* key, size_t size) {
memset(key, 0, size);
// Compiler sees: "Memory about to be freed/unused"
// Optimizes: "No need to write zeros, skip this"
// Result: Key NOT actually erased!
}
Compiler optimization example:
void function() {
uint8_t secretKey[32];
// ... use secretKey ...
memset(secretKey, 0, 32); // Compiler: "This write is never read"
// Optimized to: /* nothing */
} // Function returns with secretKey still in memory
The OPENSSL_cleanse Solution
// From src/libxrpl/crypto/secure_erase.cpp
void secure_erase(void* dest, std::size_t bytes)
{
OPENSSL_cleanse(dest, bytes);
}
Why OPENSSL_cleanse works:
// OpenSSL's implementation (simplified concept):
void OPENSSL_cleanse(void* ptr, size_t len)
{
// Mark as volatile to prevent optimization
unsigned char volatile* vptr = (unsigned char volatile*)ptr;
while (len--) {
*vptr++ = 0; // Compiler cannot optimize away volatile writes
}
// Additional measures:
// - Memory barriers
// - Inline assembly on some platforms
// - Function marked with attributes preventing optimization
}
Key properties:
Cannot be optimized away: Compiler forced to execute it
Overwrites memory: Zeros written to actual memory
Works cross-platform: Handles different compiler optimizations
Validated: Extensively tested across compilers and architectures
RAII: Resource Acquisition Is Initialization
The Pattern
class SecretKey
{
private:
std::uint8_t buf_[32];
public:
// Constructor: Acquire resource
SecretKey(Slice const& slice)
{
std::memcpy(buf_, slice.data(), sizeof(buf_));
}
// Destructor: Release resource (automatically called)
~SecretKey()
{
secure_erase(buf_, sizeof(buf_));
}
// Prevent copying (would lead to double-erase issues)
SecretKey(SecretKey const&) = delete;
SecretKey& operator=(SecretKey const&) = delete;
// Allow moving (transfer ownership)
SecretKey(SecretKey&&) noexcept = default;
SecretKey& operator=(SecretKey&&) noexcept = default;
};
Why RAII Matters
Automatic cleanup:
void processTransaction() {
SecretKey sk = randomSecretKey();
// Use key...
auto sig = sign(pk, sk, tx);
// sk destructor automatically called here
// Key erased even if exception thrown
// No manual cleanup needed
}
Exception safety:
void riskyOperation() {
SecretKey sk = loadKey();
doSomething(); // Might throw
doSomethingElse(); // Might throw
finalStep(); // Might throw
// Even if any step throws, sk destructor runs
// Key is securely erased
}
No forgetting:
// ❌ Manual cleanup - easy to forget
void manual() {
uint8_t key[32];
fillRandom(key, 32);
// ... use key ...
secure_erase(key, 32); // Must remember!
// What if we add early return?
// What if exception thrown?
}
// ✅ RAII cleanup - automatic
void automatic() {
SecretKey key = randomSecretKey();
// ... use key ...
// Cleanup happens automatically
// Works with early returns
// Works with exceptions
}
Secure String Handling
The Problem with std::string
// ❌ std::string doesn't securely erase
void badExample() {
std::string secretHex = "1a2b3c4d...";
// Convert to binary
auto secret = parseHex(secretHex);
// Use secret...
// secretHex still in memory!
// std::string's destructor doesn't erase
// Data may be in multiple string instances (SSO, copies, etc.)
}
Solutions
1. Explicit erasure:
void explicitErase() {
std::string secretHex = "1a2b3c4d...";
// Convert to binary
auto secret = parseHex(secretHex);
// Use secret...
// Explicitly erase string contents
secure_erase(
const_cast<char*>(secretHex.data()),
secretHex.size());
// Also erase the converted secret
secure_erase(
const_cast<uint8_t*>(secret.data()),
secret.size());
}
2. Use SecretKey wrapper:
void useWrapper() {
std::string secretHex = "1a2b3c4d...";
// Convert to SecretKey (RAII protection)
SecretKey sk = parseSecretKey(secretHex);
// Erase original string
secure_erase(
const_cast<char*>(secretHex.data()),
secretHex.size());
// sk automatically erased when out of scope
}
3. Avoid std::string for secrets:
// Better: Use fixed-size buffers
void fixedBuffer() {
uint8_t secretBytes[32];
getRandomBytes(secretBytes, 32);
SecretKey sk{Slice{secretBytes, 32}};
secure_erase(secretBytes, 32);
// sk automatically erased
}
Secure Allocators (Advanced)
For highly sensitive applications:
// Custom allocator that:
// 1. Locks memory (prevents swapping to disk)
// 2. Securely erases on deallocation
template<typename T>
class secure_allocator
{
public:
T* allocate(std::size_t n)
{
T* ptr = static_cast<T*>(std::malloc(n * sizeof(T)));
// Lock memory to prevent swapping
#ifdef _WIN32
VirtualLock(ptr, n * sizeof(T));
#else
mlock(ptr, n * sizeof(T));
#endif
return ptr;
}
void deallocate(T* ptr, std::size_t n)
{
// Securely erase
OPENSSL_cleanse(ptr, n * sizeof(T));
// Unlock
#ifdef _WIN32
VirtualUnlock(ptr, n * sizeof(T));
#else
munlock(ptr, n * sizeof(T));
#endif
std::free(ptr);
}
};
// Usage
std::vector<uint8_t, secure_allocator<uint8_t>> secretData;
Benefits:
Memory cannot be swapped to disk
Automatically erased on deallocation
Protected against paging attacks
Drawbacks:
Limited by OS limits on locked memory
Performance overhead
Complexity
When to use:
Extremely sensitive operations
Long-lived secrets
High-security requirements
Stack Scrubbing
The Problem
void function() {
uint8_t secretKey[32];
fillRandom(secretKey, 32);
// Use key...
secure_erase(secretKey, 32);
// Stack frame still contains key!
// Variables below secretKey might contain fragments
}
Solution: Overwrite Stack
void secureFunction() {
// Allocate large array to overwrite stack
uint8_t stackScrubber[4096];
secure_erase(stackScrubber, sizeof(stackScrubber));
// Now continue with sensitive operations
processSecrets();
// Scrub again before returning
secure_erase(stackScrubber, sizeof(stackScrubber));
}
Note: This is paranoid and rarely needed. RAII is usually sufficient.
CPU Registers and Cache
The Challenge
// Secret key passes through:
// 1. CPU registers (during computation)
// 2. L1/L2/L3 cache (for performance)
// 3. TLB (address translation)
// Cannot easily erase these!
Mitigations
1. Minimize lifetime:
{
SecretKey sk = loadKey();
auto sig = sign(pk, sk, tx);
// sk destroyed immediately
} // Scope ends, memory reused quickly
2. Overwrite with new data:
// Perform other operations that use same memory
// This overwrites cache and registers
doOtherWork();
3. Trust hardware:
// Modern CPUs have mechanisms to prevent
// cache-based attacks between processes
// Rely on OS and hardware security features
Best Practices
✅ DO:
// 1. Use RAII wrappers for secrets
SecretKey sk = randomSecretKey();
// Automatic cleanup
// 2. Minimize secret lifetime
{
SecretKey sk = loadKey();
auto sig = sign(pk, sk, tx);
} // Erased immediately
// 3. Explicitly erase temporary buffers
uint8_t temp[32];
crypto_prng()(temp, 32);
SecretKey sk{Slice{temp, 32}};
secure_erase(temp, 32); // Clean up temp
// 4. Use secure_erase, not memset
secure_erase(buffer, size);
// 5. Mark sensitive functions with comments
// SECURITY: This function handles secret keys
void processSecretKey(SecretKey const& sk) {
// ...
}
❌ DON'T:
// ❌ Don't use memset for secrets
memset(secretKey, 0, 32); // May be optimized away
// ❌ Don't use std::string for long-lived secrets
std::string secretKey = /* ... */; // Not erased!
// ❌ Don't copy secrets unnecessarily
SecretKey copy1 = original; // Now two copies!
SecretKey copy2 = original; // Three copies!
// ❌ Don't log secrets
std::cout << "Key: " << hexKey << "\n"; // Logs persist!
// ❌ Don't pass secrets by value
void bad(SecretKey sk); // Copies made
void good(SecretKey const& sk); // No copy
Defensive Programming
Assume the Worst
// Assume: Attacker can read all of your process memory
//
// Defense: Minimize time secrets exist in memory
// Erase immediately when done
// Use RAII to make erasure automatic
Multiple Layers
// 1. RAII (automatic cleanup)
SecretKey sk = randomSecretKey();
// 2. Explicit temporary erasure
uint8_t temp[32];
/* use temp */
secure_erase(temp, 32);
// 3. Scope minimization
{
// Use secret
} // Destroyed here
// 4. Quick reuse of memory
// New allocations overwrite old data
Testing Secure Erasure
Verification (Debug Build)
void testSecureErase() {
uint8_t buffer[32];
// Fill with known pattern
memset(buffer, 0xAA, 32);
// Erase
secure_erase(buffer, 32);
// Verify all zeros
for (int i = 0; i < 32; ++i) {
assert(buffer[i] == 0);
}
}
Memory Inspection (Advanced)
// Use debugger or memory inspection tools
// Verify secrets are actually erased
// Example with gdb:
// (gdb) x/32xb &secretKey // Before erasure
// (gdb) next // Execute secure_erase
// (gdb) x/32xb &secretKey // After erasure - should be zeros
Summary
Secure memory handling protects secrets from attackers who can read process memory:
Use OPENSSL_cleanse: Cannot be optimized away by compiler
Wrap in RAII classes: Automatic cleanup, exception-safe
Minimize lifetime: Create secrets late, destroy early
Explicit erasure: Clean up temporary buffers
Avoid std::string: For long-lived secrets
Test verification: Ensure erasure actually happens
Key principle: Assume attacker can read your memory. Make sure there's nothing to find.
Implementation:
secure_erase()
wrapsOPENSSL_cleanse()
SecretKey
class uses RAIIDestructors automatically erase
Minimize copies and lifetime
Last updated