README

MemGuard

Secure software enclave for storage of sensitive information in memory.


This package attempts to reduce the likelihood of sensitive data being exposed. It supports all major operating systems and is written in pure Go.

Features

  • Sensitive data is encrypted and authenticated in memory using xSalsa20 and Poly1305 respectively. The scheme also defends against cold-boot attacks.
  • Memory allocation bypasses the language runtime by using system calls to query the kernel for resources directly. This avoids interference from the garbage-collector.
  • Buffers that store plaintext data are fortified with guard pages and canary values to detect spurious accesses and overflows.
  • Effort is taken to prevent sensitive data from touching the disk. This includes locking memory to prevent swapping and handling core dumps.
  • Kernel-level immutability is implemented so that attempted modification of protected regions results in an access violation.
  • Multiple endpoints provide session purging and safe termination capabilities as well as signal handling to prevent remnant data being left behind.
  • Side-channel attacks are mitigated against by making sure that the copying and comparison of data is done in constant-time.
  • Accidental memory leaks are mitigated against by harnessing the garbage-collector to automatically destroy containers that have become unreachable.

Some features were inspired by libsodium, so credits to them.

Full documentation and a complete overview of the API can be found here. Interesting and useful code samples can be found within the examples subpackage.

Installation

$ go get github.com/awnumar/memguard

API is experimental and may have unstable changes. You should pin a version. [modules]

Contributing

  • Submitting program samples to ./examples.
  • Reporting bugs, vulnerabilities, and any difficulties in using the API.
  • Writing useful security and crypto libraries that utilise memguard.
  • Implementing kernel-specific/cpu-specific protections.
  • Submitting performance improvements.

Issues are for reporting bugs and for discussion on proposals. Pull requests should be made against master.

Expand ▾ Collapse ▴

Documentation

Overview

    Package memguard implements a secure software enclave for the storage of sensitive information in memory.

    package main
    
    import (
    	"fmt"
    	"os"
    
    	"github.com/awnumar/memguard"
    )
    
    func main() {
    	// Safely terminate in case of an interrupt signal
    	memguard.CatchInterrupt()
    
    	// Purge the session when we return
    	defer memguard.Purge()
    
    	// Generate a key sealed inside an encrypted container
    	key := memguard.NewEnclaveRandom(32)
    
    	// Passing the key off to another function
    	key = invert(key)
    
    	// Decrypt the result returned from invert
    	keyBuf, err := key.Open()
    	if err != nil {
    		fmt.Fprintln(os.Stderr, err)
    		return
    	}
    	defer keyBuf.Destroy()
    
    	// Um output it
    	fmt.Println(keyBuf.Bytes())
    }
    
    func invert(key *memguard.Enclave) *memguard.Enclave {
    	// Decrypt the key into a local copy
    	b, err := key.Open()
    	if err != nil {
    		memguard.SafePanic(err)
    	}
    	defer b.Destroy() // Destroy the copy when we return
    
    	// Open returns the data in an immutable buffer, so make it mutable
    	b.Melt()
    
    	// Set every element to its complement
    	for i := range b.Bytes() {
    		b.Bytes()[i] = ^b.Bytes()[i]
    	}
    
    	// Return the new data in encrypted form
    	return b.Seal() // <- sealing also destroys b
    }
    

    There are two main container objects exposed in this API. Enclave objects encrypt data and store the ciphertext whereas LockedBuffers are more like guarded memory allocations. There is a limit on the maximum number of LockedBuffer objects that can exist at any one time, imposed by the system's mlock limits. There is no limit on Enclaves.

    The general workflow is to store sensitive information in Enclaves when it is not immediately needed and decrypt it when and where it is. After use, the LockedBuffer should be destroyed.

    If you need access to the data inside a LockedBuffer in a type not covered by any methods provided by this API, you can type-cast the allocation's memory to whatever type you want.

    key := memguard.NewBuffer(32)
    keyArrayPtr := (*[32]byte)(unsafe.Pointer(&key.Bytes()[0])) // do not dereference
    

    This is of course an unsafe operation and so care must be taken to ensure that the cast is valid and does not result in memory unsafety. Further examples of code and interesting use-cases can be found in the examples subpackage.

    Several functions exist to make the mass purging of data very easy. It is recommended to make use of them when appropriate.

    // Start an interrupt handler that will clean up memory before exiting
    memguard.CatchInterrupt()
    
    // Purge the session when returning from the main function of your program
    defer memguard.Purge()
    
    // Use the safe variants of exit functions provided in the stdlib
    memguard.SafeExit(1)
    memguard.SafePanic(err)
    
    // Destroy LockedBuffers as soon as possible after using them
    b, err := enclave.Open()
    if err != nil {
    	memguard.SafePanic(err)
    }
    defer b.Destroy()
    

    Core dumps are disabled by default. If you absolutely require them, you can enable them by using unix.Setrlimit to set RLIMIT_CORE to an appropriate value.

    Index

    Constants

    This section is empty.

    Variables

    This section is empty.

    Functions

    func CatchInterrupt

    func CatchInterrupt()

      CatchInterrupt is a wrapper around CatchSignal that makes it easy to safely handle receiving interrupt signals. If an interrupt is received, the process will wipe sensitive data in memory before terminating.

      A subsequent call to CatchSignal will override this call.

      func CatchSignal

      func CatchSignal(f func(os.Signal), signals ...os.Signal)

        CatchSignal assigns a given function to be run in the event of a signal being received by the process. If no signals are provided all signals will be caught.

        1. Signal is caught by the process
        2. Interrupt handler is executed
        3. Secure session state is wiped
        4. Process terminates with exit code 1
        

        This function can be called multiple times with the effect that only the last call will have any effect.

        func Purge

        func Purge()

          Purge resets the session key to a fresh value and destroys all existing LockedBuffers. Existing Enclave objects will no longer be decryptable.

          func SafeExit

          func SafeExit(c int)

            SafeExit destroys everything sensitive before exiting with a specified status code.

            func SafePanic

            func SafePanic(v interface{})

              SafePanic wipes all it can before calling panic(v).

              func ScrambleBytes

              func ScrambleBytes(buf []byte)

                ScrambleBytes overwrites an arbitrary buffer with cryptographically-secure random bytes.

                func WipeBytes

                func WipeBytes(buf []byte)

                  WipeBytes overwrites an arbitrary buffer with zeroes.

                  Types

                  type Enclave

                  type Enclave struct {
                  	*core.Enclave
                  }

                    Enclave is a sealed and encrypted container for sensitive data.

                    func NewEnclave

                    func NewEnclave(src []byte) *Enclave

                      NewEnclave seals up some data into an encrypted enclave object. The buffer is wiped after the data is copied. If the length of the buffer is zero, the function will return nil.

                      A LockedBuffer may alternatively be converted into an Enclave object using its Seal method. This will also have the effect of destroying the LockedBuffer.

                      func NewEnclaveRandom

                      func NewEnclaveRandom(size int) *Enclave

                        NewEnclaveRandom generates and seals arbitrary amounts of cryptographically-secure random bytes into an encrypted enclave object. If size is not strictly positive the function will return nil.

                        func (*Enclave) Open

                        func (e *Enclave) Open() (*LockedBuffer, error)

                          Open decrypts an Enclave object and places its contents into an immutable LockedBuffer. An error will be returned if decryption failed.

                          func (*Enclave) Size

                          func (e *Enclave) Size() int

                            Size returns the number of bytes of data stored within an Enclave.

                            type LockedBuffer

                            type LockedBuffer struct {
                            	*core.Buffer
                            	// contains filtered or unexported fields
                            }

                              LockedBuffer is a structure that holds raw sensitive data.

                              The number of LockedBuffers that you are able to create is limited by how much memory your system's kernel allows each process to mlock/VirtualLock. Therefore you should call Destroy on LockedBuffers that you no longer need or defer a Destroy call after creating a new LockedBuffer.

                              func NewBuffer

                              func NewBuffer(size int) *LockedBuffer

                                NewBuffer creates a mutable data container of the specified size.

                                func NewBufferFromBytes

                                func NewBufferFromBytes(src []byte) *LockedBuffer

                                  NewBufferFromBytes constructs an immutable buffer from a byte slice. The source buffer is wiped after the value has been copied over to the created container.

                                  func NewBufferFromEntireReader

                                  func NewBufferFromEntireReader(r io.Reader) (*LockedBuffer, error)

                                    NewBufferFromEntireReader reads from an io.Reader into an immutable buffer. It will continue reading until EOF.

                                    A nil error is returned precisely when we managed to read all the way until EOF. Any data read is returned in either case.

                                    func NewBufferFromReader

                                    func NewBufferFromReader(r io.Reader, size int) (*LockedBuffer, error)

                                      NewBufferFromReader reads some number of bytes from an io.Reader into an immutable LockedBuffer.

                                      An error is returned precisely when the number of bytes read is less than the requested amount. Any data read is returned in either case.

                                      func NewBufferFromReaderUntil

                                      func NewBufferFromReaderUntil(r io.Reader, delim byte) (*LockedBuffer, error)

                                        NewBufferFromReaderUntil constructs an immutable buffer containing data sourced from an io.Reader object.

                                        If an error is encountered before the delimiter value, the error will be returned along with the data read up until that point.

                                        func NewBufferRandom

                                        func NewBufferRandom(size int) *LockedBuffer

                                          NewBufferRandom constructs an immutable buffer filled with cryptographically-secure random bytes.

                                          func (*LockedBuffer) ByteArray16

                                          func (b *LockedBuffer) ByteArray16() *[16]byte

                                            ByteArray16 returns a pointer to some 16 byte array. Care must be taken not to dereference the pointer and instead pass it around as-is.

                                            The length of the buffer must be at least 16 bytes in size and the LockedBuffer should not be destroyed. In either of these cases a nil value is returned.

                                            func (*LockedBuffer) ByteArray32

                                            func (b *LockedBuffer) ByteArray32() *[32]byte

                                              ByteArray32 returns a pointer to some 32 byte array. Care must be taken not to dereference the pointer and instead pass it around as-is.

                                              The length of the buffer must be at least 32 bytes in size and the LockedBuffer should not be destroyed. In either of these cases a nil value is returned.

                                              func (*LockedBuffer) ByteArray64

                                              func (b *LockedBuffer) ByteArray64() *[64]byte

                                                ByteArray64 returns a pointer to some 64 byte array. Care must be taken not to dereference the pointer and instead pass it around as-is.

                                                The length of the buffer must be at least 64 bytes in size and the LockedBuffer should not be destroyed. In either of these cases a nil value is returned.

                                                func (*LockedBuffer) ByteArray8

                                                func (b *LockedBuffer) ByteArray8() *[8]byte

                                                  ByteArray8 returns a pointer to some 8 byte array. Care must be taken not to dereference the pointer and instead pass it around as-is.

                                                  The length of the buffer must be at least 8 bytes in size and the LockedBuffer should not be destroyed. In either of these cases a nil value is returned.

                                                  func (*LockedBuffer) Bytes

                                                  func (b *LockedBuffer) Bytes() []byte

                                                    Bytes returns a byte slice referencing the protected region of memory.

                                                    func (*LockedBuffer) Copy

                                                    func (b *LockedBuffer) Copy(src []byte)

                                                      Copy performs a time-constant copy into a LockedBuffer. Move is preferred if the source is not also a LockedBuffer or if the source is no longer needed.

                                                      func (*LockedBuffer) CopyAt

                                                      func (b *LockedBuffer) CopyAt(offset int, src []byte)

                                                        CopyAt performs a time-constant copy into a LockedBuffer at an offset. Move is preferred if the source is not also a LockedBuffer or if the source is no longer needed.

                                                        func (*LockedBuffer) Destroy

                                                        func (b *LockedBuffer) Destroy()

                                                          Destroy wipes and frees the underlying memory of a LockedBuffer. The LockedBuffer will not be accessible or usable after this calls is made.

                                                          func (*LockedBuffer) EqualTo

                                                          func (b *LockedBuffer) EqualTo(buf []byte) bool

                                                            EqualTo performs a time-constant comparison on the contents of a LockedBuffer with a given buffer. A destroyed LockedBuffer will always return false.

                                                            func (*LockedBuffer) Freeze

                                                            func (b *LockedBuffer) Freeze()

                                                              Freeze makes a LockedBuffer's memory immutable. The call can be reversed with Melt.

                                                              func (*LockedBuffer) Int16

                                                              func (b *LockedBuffer) Int16() []int16

                                                                Int16 returns a slice pointing to the protected region of memory with the data represented as a sequence of signed 16 bit integers. Its length will be half that of the byte slice, excluding any remaining part that doesn't form a complete int16 value.

                                                                If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                func (*LockedBuffer) Int32

                                                                func (b *LockedBuffer) Int32() []int32

                                                                  Int32 returns a slice pointing to the protected region of memory with the data represented as a sequence of signed 32 bit integers. Its length will be one quarter that of the byte slice, excluding any remaining part that doesn't form a complete int32 value.

                                                                  If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                  func (*LockedBuffer) Int64

                                                                  func (b *LockedBuffer) Int64() []int64

                                                                    Int64 returns a slice pointing to the protected region of memory with the data represented as a sequence of signed 64 bit integers. Its length will be one eighth that of the byte slice, excluding any remaining part that doesn't form a complete int64 value.

                                                                    If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                    func (*LockedBuffer) Int8

                                                                    func (b *LockedBuffer) Int8() []int8

                                                                      Int8 returns a slice pointing to the protected region of memory with the data represented as a sequence of signed 8 bit integers. If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                      func (*LockedBuffer) IsAlive

                                                                      func (b *LockedBuffer) IsAlive() bool

                                                                        IsAlive returns a boolean value indicating if a LockedBuffer is alive, i.e. that it has not been destroyed.

                                                                        func (*LockedBuffer) IsMutable

                                                                        func (b *LockedBuffer) IsMutable() bool

                                                                          IsMutable returns a boolean value indicating if a LockedBuffer is mutable.

                                                                          func (*LockedBuffer) Melt

                                                                          func (b *LockedBuffer) Melt()

                                                                            Melt makes a LockedBuffer's memory mutable. The call can be reversed with Freeze.

                                                                            func (*LockedBuffer) Move

                                                                            func (b *LockedBuffer) Move(src []byte)

                                                                              Move performs a time-constant move into a LockedBuffer. The source is wiped after the bytes are copied.

                                                                              func (*LockedBuffer) MoveAt

                                                                              func (b *LockedBuffer) MoveAt(offset int, src []byte)

                                                                                MoveAt performs a time-constant move into a LockedBuffer at an offset. The source is wiped after the bytes are copied.

                                                                                func (*LockedBuffer) Reader

                                                                                func (b *LockedBuffer) Reader() *bytes.Reader

                                                                                  Reader returns a Reader object referencing the protected region of memory.

                                                                                  func (*LockedBuffer) Scramble

                                                                                  func (b *LockedBuffer) Scramble()

                                                                                    Scramble attempts to overwrite the data with cryptographically-secure random bytes.

                                                                                    func (*LockedBuffer) Seal

                                                                                    func (b *LockedBuffer) Seal() *Enclave

                                                                                      Seal takes a LockedBuffer object and returns its contents encrypted inside a sealed Enclave object. The LockedBuffer is subsequently destroyed and its contents wiped.

                                                                                      If Seal is called on a destroyed buffer, a nil enclave is returned.

                                                                                      func (*LockedBuffer) Size

                                                                                      func (b *LockedBuffer) Size() int

                                                                                        Size gives you the length of a given LockedBuffer's data segment. A destroyed LockedBuffer will have a size of zero.

                                                                                        func (*LockedBuffer) String

                                                                                        func (b *LockedBuffer) String() string

                                                                                          String returns a string representation of the protected region of memory.

                                                                                          func (*LockedBuffer) Uint16

                                                                                          func (b *LockedBuffer) Uint16() []uint16

                                                                                            Uint16 returns a slice pointing to the protected region of memory with the data represented as a sequence of unsigned 16 bit integers. Its length will be half that of the byte slice, excluding any remaining part that doesn't form a complete uint16 value.

                                                                                            If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                                            func (*LockedBuffer) Uint32

                                                                                            func (b *LockedBuffer) Uint32() []uint32

                                                                                              Uint32 returns a slice pointing to the protected region of memory with the data represented as a sequence of unsigned 32 bit integers. Its length will be one quarter that of the byte slice, excluding any remaining part that doesn't form a complete uint32 value.

                                                                                              If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                                              func (*LockedBuffer) Uint64

                                                                                              func (b *LockedBuffer) Uint64() []uint64

                                                                                                Uint64 returns a slice pointing to the protected region of memory with the data represented as a sequence of unsigned 64 bit integers. Its length will be one eighth that of the byte slice, excluding any remaining part that doesn't form a complete uint64 value.

                                                                                                If called on a destroyed LockedBuffer, a nil slice will be returned.

                                                                                                func (*LockedBuffer) Wipe

                                                                                                func (b *LockedBuffer) Wipe()

                                                                                                  Wipe attempts to overwrite the data with zeros.

                                                                                                  type Stream

                                                                                                  type Stream struct {
                                                                                                  	sync.Mutex
                                                                                                  	// contains filtered or unexported fields
                                                                                                  }

                                                                                                    Stream is an in-memory encrypted container implementing the reader and writer interfaces.

                                                                                                    It is most useful when you need to store lots of data in memory and are able to work on it in chunks.

                                                                                                    func NewStream

                                                                                                    func NewStream() *Stream

                                                                                                      NewStream initialises a new empty Stream object.

                                                                                                      func (*Stream) Flush

                                                                                                      func (s *Stream) Flush() (*LockedBuffer, error)

                                                                                                        Flush reads all of the data from a Stream and returns it inside a LockedBuffer. If an error is encountered before all the data could be read, it is returned along with any data read up until that point.

                                                                                                        func (*Stream) Next

                                                                                                        func (s *Stream) Next() (*LockedBuffer, error)

                                                                                                          Next grabs the next chunk of data from the Stream and returns it decrypted inside a LockedBuffer. Any error from the stream is forwarded.

                                                                                                          func (*Stream) Read

                                                                                                          func (s *Stream) Read(buf []byte) (int, error)

                                                                                                            Read decrypts and places some data from a Stream object into a provided buffer.

                                                                                                            If there is no data, the call will return an io.EOF error. If the caller provides a buffer that is too small to hold the next chunk of data, the remaining bytes are re-encrypted and added to the front of the queue to be returned in the next call.

                                                                                                            func (*Stream) Size

                                                                                                            func (s *Stream) Size() int

                                                                                                              Size returns the number of bytes of data currently stored within a Stream object.

                                                                                                              func (*Stream) Write

                                                                                                              func (s *Stream) Write(data []byte) (int, error)

                                                                                                                Write encrypts and writes some given data to a Stream object.

                                                                                                                The data is broken down into page-sized chunks and added to the stream in order. The last thing to be written to the stream is the last thing that will be read back.

                                                                                                                Directories

                                                                                                                Path Synopsis
                                                                                                                examples