sealer

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 21, 2025 License: BSD-2-Clause Imports: 11 Imported by: 1

README

sealer

Go reference under 350 LOC Go Report Card

Provides io.Writer and io.Reader that transparently compresses and encrypts a stream of data using the modern best practices: zstd and ChaCha20-Poly1305 AEAD with an ephemeral key.

Has only two dependencies:

Sealer is a bit like filippo.io/age, but simpler and meant for custom encrypted file formats.

  • supports a custom unencrypted file header that comes before the data (the header is authenticated as part of the first encrypted block, so tampering and corruption will be detected);

  • single secret encryption key only (with a 32-byte user-definable KeyID so that you can look up the key in your system's keystore).

Usage

Install:

go get github.com/andreyvit/sealer@latest

and then:

import (
	"github.com/andreyvit/sealer"
)
Generating a key

A key is just a [32]byte user-defined identifier and a [32]byte secret key material:

key := &sealer.Key{}
copy(key.ID[:], "YA_CAN_PUT_WHATEVER_YA_WANT_HERE")

_, err := io.ReadFull(cryptoRand.Reader, key.Key[:])
if err != nil {
	panic(err)
}

32 bytes of Key ID is enough to hold an integer (or four), a UUID (or two), a string name, or SHA-256 hash of any data — the usage is up to you.

Sealing (aka encrypting)

Example:

// prefix is any []byte you want to prepend to the file, can be nil.
w, err := sealer.Seal(outputWriter, key, prefix, sealer.SealOptions{})
if err != nil {
	panic(err)
}

for range 100 {
	_, err := w.Write(data)
	if err != nil {
		panic(err)
	}
}

// Very important to close the writer to write out the final chunk.
err = w.Close()
if err != nil {
	panic(err)
}

If you provide a prefix, sealer.Seal will write it to the beginning of the file.

Opening (aka decrypting)

Example:

o, err := sealer.Prepare(inputReader, prefix)
if err != nil {
	panic(err)
}

key := lookupKey(o.KeyID)

r, err := o.Open(key)
if err != nil {
	panic(err)
}

// Read from r now, for example:
var opened bytes.Buffer
_, err = io.Copy(&opened, r)
if err != nil {
	panic(err)
}

Unlike sealer, opener will not read the prefix for you — it assumes you've already read the file header to make sense of what it is. So if you want a prefix, read it yourself before calling sealer.Prepare:

prefix := make([]byte, prefixLen)
_, err := io.ReadFull(inputReader, prefix)
if err != nil {
	panic(err)
}

Encryption & Compression

Uses modern best practices for cryptography:

  • ChaCha20-Poly1305 encryption;

  • ephemeral (i.e. per-file) encryption key that is encapsulated by the encryption key;

  • encapsulation uses XChaCha20-Poly1305 with a random 192-bit nonce;

  • encryption splits the file into chunks (32 KB by default) and uses deterministic nonces for these, marking the final chunk's nonce to detect trimming;

  • nothing of the above is configurable.

ChaCha20-Poly1305 has been chosen as a modern and standardized cipher, ensuring wide availability and interoperability. NaCl's XSalsa20-Poly1305 would be similar, but it's not a standard so ChaCha20 seems like a better choice going forward. AES-256-GCM could also be used here, but ChaCha20 has fewer concerns about complicated attack scenarios.

Before encryption, sealer applies zstd compression, it provides an excellent time/compression balance and has an accepted proposal for inclusion in Go stdlib. Until that happens, we use github.com/klauspost/compress/zstd which is an excellent zero-dependency library.

License

Copyright 2025, Andrey Tarantsov. Distributed under the 2-clause BSD license.

Documentation

Overview

Package sealer provides transparent compression and encryption of data.

Example
package main

import (
	"bytes"
	cryptoRand "crypto/rand"
	"fmt"
	"io"

	"github.com/andreyvit/sealer"
)

func main() {
	const prefixLen = 32

	prefix := make([]byte, prefixLen)
	copy(prefix, "MY_DATA_FORMAT_HEADER_GOES_HERE!")

	key := &sealer.Key{}
	copy(key.ID[:], "YA_CAN_PUT_WHATEVER_YA_WANT_HERE")
	_, err := io.ReadFull(cryptoRand.Reader, key.Key[:])
	if err != nil {
		panic(err)
	}

	// generate non-random compressible data to demonstrate compression
	data := make([]byte, 200)
	for i := range data {
		data[i] = byte(i)
	}

	var sealed bytes.Buffer
	var expectedData bytes.Buffer
	{ // Sealing
		w, err := sealer.Seal(&sealed, key, prefix, sealer.SealOptions{})
		if err != nil {
			panic(err)
		}

		var totalUncompressedSize int
		for range 100 {
			_, err := w.Write(data)
			if err != nil {
				panic(err)
			}

			totalUncompressedSize += len(data)
			expectedData.Write(data)
		}

		// Very important to close the writer to write the final chunk.
		err = w.Close()
		if err != nil {
			panic(err)
		}

		fmt.Printf("%d bytes input => %d bytes sealed\n", totalUncompressedSize, sealed.Len())
	}

	{ // Opening
		fmt.Printf("Preparing to open:\n")
		actualPrefix := make([]byte, prefixLen)
		_, err := io.ReadFull(&sealed, actualPrefix)
		if err != nil {
			panic(err)
		}
		fmt.Printf("prefix = %s\n", actualPrefix)

		o, err := sealer.Prepare(&sealed, actualPrefix)
		if err != nil {
			panic(err)
		}
		fmt.Printf("key ID = %s\n", o.KeyID[:])

		r, err := o.Open(key)
		if err != nil {
			panic(err)
		}

		var opened bytes.Buffer
		_, err = io.Copy(&opened, r)
		if err != nil {
			panic(err)
		}

		if !bytes.Equal(opened.Bytes(), expectedData.Bytes()) {
			fmt.Println("data mismatch!")
		}
	}

}
Output:

20000 bytes input => 389 bytes sealed
Preparing to open:
prefix = MY_DATA_FORMAT_HEADER_GOES_HERE!
key ID = YA_CAN_PUT_WHATEVER_YA_WANT_HERE

Index

Examples

Constants

View Source
const (
	// KeySize is the length of Cacha20-Poly1305 key (32 bytes).
	KeySize = chacha20poly1305.KeySize

	// IDSize is the length of a user-defined key ID used by this package
	// (32 bytes).
	IDSize = 32
)
View Source
const DefaultChunkSize int = 32 * 1024

DefaultChunkSize is the default value of SealOptions.ChunkSize used by the sealer.

View Source
const MaxChunkSize int = 1024 * 1024

MaxChunkSize is the maximum value of SealOptions.ChunkSize that can be used by the sealer, and the maximum size that opener will accept, in order to avoid DoS attacks when reading untrusted files.

Variables

View Source
var (
	ErrInvalidKeyString = errors.New("invalid key string")
	ErrKeyNameTooLong   = errors.New("key name too long")
	ErrInvalidKeyName   = errors.New("invalid key name")
)
View Source
var (
	ErrChunkSizeTooLarge  = errors.New("chunk size too large")
	ErrUnsupportedVersion = errors.New("unsupported or corrupted sealed file")
)

Functions

func PrintableID added in v0.2.0

func PrintableID(s string) [IDSize]byte

Types

type Key

type Key struct {
	ID  [IDSize]byte
	Key [KeySize]byte
}

Key is a user-provided encrypted key. It is used once per sealing operation, to encapsulate (i.e. encrypt) an ephemeral file key. You can generate the key bytes by reading from crypto/rand.Reader. NIST recommends that you limit using a single key to no more than 2^32 Seal operations.

func ParseKey added in v0.2.0

func ParseKey(s string) (*Key, error)

func ParseKeys added in v0.2.0

func ParseKeys(s string) ([]*Key, error)

func (*Key) String added in v0.2.0

func (k *Key) String() string

type Keys added in v0.2.0

type Keys []*Key

func KeysVar added in v0.2.0

func KeysVar(v *[]*Key) *Keys

func (Keys) Get added in v0.2.0

func (v Keys) Get() any

func (*Keys) Set added in v0.2.0

func (v *Keys) Set(raw string) (err error)

func (Keys) String added in v0.2.0

func (v Keys) String() string

type Openable

type Openable struct {
	KeyID [IDSize]byte
	// contains filtered or unexported fields
}

func Prepare

func Prepare(in io.Reader, outerPrefix []byte) (*Openable, error)

Prepare read a sealed file header and prepares to open it. Crucially, the Openable returned contains a KeyID which you can use to decide which key to provide to the Open method.

func (*Openable) Open

func (opn *Openable) Open(key *Key) (*Reader, error)

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

func (*Reader) Read

func (r *Reader) Read(p []byte) (n int, err error)

type SealOptions

type SealOptions struct {
	ChunkSize    int
	ZstdLevel    int
	RandomReader io.Reader
}

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

func Seal

func Seal(out io.Writer, key *Key, outerPrefix []byte, opt SealOptions) (*Writer, error)

func (*Writer) Close

func (w *Writer) Close() error

func (*Writer) Write

func (w *Writer) Write(data []byte) (int, error)

Jump to

Keyboard shortcuts

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