intermediate 30 min

Secure memory handling

Why secrets must be wiped from memory and how rippled does it (`secure_erase` / `OPENSSL_cleanse`).

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain the memory attack surface for secret keys.
  • Understand why `memset` isn't enough and how `secure_erase` works.
  • Recognise RAII patterns that protect key material.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈30 min · Intermediate · builds on Key generation & derivation

A private key is only as safe as the memory it sits in. In this short but important module you'll learn the attack surface for secrets in a running node, why a plain memset isn't enough to erase them, and how rippled uses secure_erase and RAII to make sure key material doesn't linger. It's a small habit that prevents a catastrophic mistake.

The Memory Problem

In brief: secrets live in RAM, on the stack, and in CPU registers, all of which can leak.

Where secrets leak from memory: core dumps, swap files, hibernation images, attached debuggers, and cold-boot attacks all read RAM; the defence is to keep secrets alive briefly and wipe them.

Where Secrets Live

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

In brief: wipe secrets with a routine the compiler is not allowed to optimize away.

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:

Key properties:

  1. Cannot be optimized away: Compiler forced to execute it
  2. Overwrites memory: Zeros written to actual memory
  3. Works cross-platform: Handles different compiler optimizations
  4. Validated: Extensively tested across compilers and architectures

Watch out. A plain memset can be optimized away as a "dead store", leaving the secret in memory. Always use secure_erase / OPENSSL_cleanse for key material.

RAII: Resource Acquisition Is Initialization

In brief: tie a secret's lifetime to a scope so it is wiped automatically when you leave it.

The RAII life of a SecretKey: construction copies the bytes and wipes the source, use borrows without copying, and destruction runs secure_erase automatically with no path around it.

The Pattern

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:

Secure String Handling

The Problem with std::string

Solutions

1. Explicit erasure:

2. Use SecretKey wrapper:

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:

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:

DON'T:

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

Testing Secure Erasure

Verification (Debug Build)

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

This short module covered why secrets must be wiped from memory and how rippled does it. You saw the attack surface (RAM, stack, registers), why a plain memset is not enough (the compiler can optimise the dead store away), and how secure_erase / OPENSSL_cleanse and RAII patterns make sure key material is cleared automatically. A small habit that prevents a catastrophic mistake.

To remember:

  • Secrets linger in heap, stack, registers, swap, and core dumps
  • A plain memset before free is a dead store: the compiler may remove it entirely
  • Use secure_erase (OPENSSL_cleanse underneath): the compiler cannot optimize it away
  • RAII ties wiping to scope: SecretKey's destructor erases the key material
  • Avoid std::string for secrets: copies and reallocations scatter traces
  • Grep secure_erase in the tree to see every wipe site
  • Watch out: logs are the other leak channel; never JLOG a seed or secret key, even at trace level

Next up. Your keys are safe in memory; now they need to face users. Next: Base58Check encoding, the armor that turns bytes into typo-proof addresses.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026