goencryptionmemoryaeskey-management

How to securely wipe xtsCipher internal key material in Go?


I'm using the golang.org/x/crypto/xts package to create an XTS-AES cipher in Go, like this:

xtsCipher, err := xts.NewCipher(aes.NewCipher, key)
if err != nil {
    log.Fatal(err)
}

And I am using an in-file C function to wipe-out the keyXTS which is of type []byte. Like this:

package main

/*
#include <stddef.h>
#include <stdio.h>
#ifdef _WIN32
    #include <windows.h>
#endif

// Securely zeroes memory. Returns 0 on success; nonzero on error.
int secureZero(void *ptr, size_t n) {
    if (ptr == NULL || n == 0) {
        fprintf(stderr, "Error: secureZero received an invalid pointer or zero size.\n");
        return -1;
    }
#ifdef _WIN32
    RtlSecureZeroMemory(ptr, n);
    return 0;
#else
    volatile unsigned char *p = (volatile unsigned char *)ptr;
    for (size_t i = 0; i < n; ++i) {
        p[i] = 0;
    }
    return 0;
#endif
}
*/
import "C"
import "unsafe"
import "crypto/rand"
import "log"
import "fmt"
import "crypto/aes"
import "golang.org/x/crypto/xts"

// zeroMemory securely wipes a byte slice.
func zeroMemory(data []byte) error {
    if len(data) == 0 {
        return nil
    }
    if ret := C.secureZero(unsafe.Pointer(&data[0]), C.size_t(len(data))); ret != 0 {
        return fmt.Errorf("secureZero failed with error code: %d", ret)
    }
    return nil
}

func main() {
    key := make([]byte, 64)
    if _, err := rand.Read(key); err != nil {
        log.Fatalf("failed to generate key: %v", err)
    }
    defer func() {
        if err := zeroMemory(key); err != nil {
            log.Printf("failed to zero memory: %v", err)
        }
    }()

    xtsCipher, err := xts.NewCipher(aes.NewCipher, key)
    if err != nil {
        log.Fatal(err)
    }
}

But I assume xtsCipher still has an internal copy of the key.

  1. Is my assumption wrong? does xts.NewCipher just use the underlaying array of keyXTS? Which should mean that if I zero the memory of keyXTS it would also zero out xtsCipher.
  2. Is there a way to explicitly clear xtsCipher's internal state?
  3. If not, are there any workarounds, like using unsafe or reflection? But since xtsCipher is not a built-in type I don't think so.
  4. What other approach would help me prevent keeping the internal state of the keyXTS variable inside of the xtsCipher lingering in memory?

Solution

  • 1. ... does xts.NewCipher just use the underlying array of keyXTS?

    No. An AES cipher contains a private copy of the key. And AES-XTS contains two AES ciphers as internal state.

    2. Is there a way to explicitly clear xtsCipher's internal state?

    No, as of right now (Go 1.24), there is no public API to clear AES or XTS state.

    3. If not, are there any workarounds, like using unsafe or reflection? But since xtsCipher is not a built-in type I don't think so.

    You can do a lot with unsafe API. However, using unsafe or reflection to clear the internal state of xtsCipher is not recommended because it can lead to undefined behavior and potential security vulnerabilities, not to mention being non-portable across different platforms, Go runtime versions and crypto modes, such as e.g. native crypto (with/without fips140), system crypto, BoringCrypto, etc.

    With that said, if you really want to do it using unsafe API, here's a solution for the two most common crypto modes, fips140 native crypto and BoringCrypto:

    func unsafeClearXTSCipher(v *xts.Cipher) {
        r := reflect.ValueOf(*v)
        unsafeClearAESCipher(r.FieldByName("k1").Elem().Elem())
        unsafeClearAESCipher(r.FieldByName("k2").Elem().Elem())
    }
    
    // unsafeClearAESCipher clears an instance of an AES cipher. Currently supports
    // crypto/internal/fips140/aes.Block and crypto/internal/boring.aesCipher.
    func unsafeClearAESCipher(v reflect.Value) {
        key := v.FieldByName("key")
        if key.IsValid() {
            // "key" field exists only in boring crypto
            unsafeClearValue(key)
        }
        unsafeClearValue(v.FieldByName("enc"))
        unsafeClearValue(v.FieldByName("dec"))
    }
    
    func unsafeClearValue(v reflect.Value) {
        t := v.Type()
        fmt.Println(t.Kind(), t.Size())
        switch t.Kind() {
        case reflect.Array, reflect.Struct:
            arrType := reflect.ArrayOf(int(t.Size()), reflect.TypeOf(uint8(0)))
            reflect.NewAt(arrType, v.Addr().UnsafePointer()).Elem().SetZero()
        case reflect.Slice:
            arrType := reflect.ArrayOf(v.Len(), reflect.TypeOf(uint8(0)))
            reflect.NewAt(arrType, v.Index(0).Addr().UnsafePointer()).Elem().SetZero()
        default:
            panic("unsupported value type " + t.String())
        }
    }
    

    You can verify in a debugger that after invoking unsafeClearXTSCipher(xtsCipher), the ciphers internal state contains all-zeros.

    For other crypto modes you'd have reverse-engineer the crypto internal state and expand the solution.

    4. What other approach would help me prevent keeping the internal state of the keyXTS variable inside of the xtsCipher lingering in memory?

    It depends on what you're trying to protect against. There are 3rd-party libraries like libsodium, that make an effort to protect key memory. There are also hardware solutions that store the key in hardware, like go-tpm2.