memguard

package module
v0.22.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2024 License: Apache-2.0 Imports: 8 Imported by: 227

README

MemGuard

Software enclave for storage of sensitive information in memory.


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

Features

  • Sensitive data is encrypted and authenticated in memory with XSalsa20Poly1305. The scheme used 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.

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

View Source
var (
	// StreamChunkSize is the maximum amount of data that is locked into memory at a time.
	// If you get error allocating memory, increase your system's mlock limits.
	// Use 'ulimit -l' to see mlock limit on unix systems.
	StreamChunkSize = c
)

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 added in v0.16.0

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 added in v0.16.0

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 added in v0.16.0

func SafePanic(v interface{})

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

func ScrambleBytes added in v0.16.0

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 added in v0.16.0

type Enclave struct {
	*core.Enclave
}

Enclave is a sealed and encrypted container for sensitive data.

func NewEnclave added in v0.16.0

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 added in v0.16.0

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 added in v0.16.0

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 added in v0.20.0

func (e *Enclave) Size() int

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

type LockedBuffer

type LockedBuffer struct {
	*core.Buffer
}

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 added in v0.16.0

func NewBuffer(size int) *LockedBuffer

NewBuffer creates a mutable data container of the specified size.

func NewBufferFromBytes added in v0.16.0

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 added in v0.17.3

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 added in v0.16.3

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 added in v0.16.3

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 added in v0.16.0

func NewBufferRandom(size int) *LockedBuffer

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

func (*LockedBuffer) ByteArray16 added in v0.16.0

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 added in v0.16.0

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 added in v0.16.0

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 added in v0.16.0

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 added in v0.16.0

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 added in v0.4.0

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 added in v0.4.0

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 added in v0.16.0

func (b *LockedBuffer) Freeze()

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

func (*LockedBuffer) Int16 added in v0.14.0

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 added in v0.14.0

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 added in v0.14.0

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 added in v0.14.0

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 added in v0.16.0

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 added in v0.13.0

func (b *LockedBuffer) IsMutable() bool

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

func (*LockedBuffer) Melt added in v0.16.0

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 added in v0.4.0

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 added in v0.16.4

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

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

func (*LockedBuffer) Scramble added in v0.16.0

func (b *LockedBuffer) Scramble()

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

func (*LockedBuffer) Seal added in v0.16.0

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 added in v0.13.0

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 added in v0.17.0

func (b *LockedBuffer) String() string

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

func (*LockedBuffer) Uint16 added in v0.14.0

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 added in v0.14.0

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 added in v0.14.0

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 added in v0.13.0

func (b *LockedBuffer) Wipe()

Wipe attempts to overwrite the data with zeros.

type Stream added in v0.21.0

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 added in v0.21.0

func NewStream() *Stream

NewStream initialises a new empty Stream object.

func (*Stream) Flush added in v0.22.0

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 added in v0.22.0

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 added in v0.21.0

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.

To be performant, have

func (*Stream) Size added in v0.21.0

func (s *Stream) Size() int

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

func (*Stream) Write added in v0.21.0

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 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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL