ece

package module
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 7, 2023 License: Apache-2.0 Imports: 17 Imported by: 0

README

GoDoc reference

Encrypted-Content-Encoding for HTTP

This a Go implementation of RFC 8188, specifically the draft published on June 2017.

ECE for HTTP defines a way to use standard HTTP content encoding to exchange AES-GCM encrypted payloads between a client and server.

While the RFC only mentions 128-bit encryption with AES-128-GCM, this library provides support for AES-256-GCM as well when a key sufficiently long (32 bytes) is provided.

Library

The library exposes 4 basic elements:

  1. A Reader to decrypt;
  2. A Writer to encrypt;
  3. An HTTP middleware to handle server-side encryption/decryption;
  4. An HTTP Client.
Reader

Reader deciphers data from a reader (io.Reader) containing encrypted data.

var key []byte            // Main decryption key
var cipher io.ReadCloser  // AES-GCM encrypted data

r := ece.NewReader(key, cipher)
plain, err := io.ReadAll(r)
if err != nil {
  log.Fatalf("error during decryption: %v", err)
}
defer r.Close()

fmt.Println(plain) // plain version of the content of cipher.
Writer

Writer writes encrypted data into another writer (io.Writer).

var key = []byte("16 or 32 bytes long key")   // Main decryption key
var dest io.Writer                            // Where cipher will be written

var (
  salt        = ece.NewRandomSalt()     // Must be random
  recordSize  = 4096                    // Record size
  keyID       = "ID of the main key"    // (Empty string to omit)
)
w, err := ece.NewWriter(key, salt, recordSize, keyID, dest)
if err != nil {
  log.Fatalf("error initializing writer: %v", err)
}
defer w.Close()     // Cipher may be mis-formatted if omitted

if _, err := io.WriteString(w, "Hello, World!"); err != nil {
  log.Fatalf("error writing cipher: %v", err)
}

log.Println("dest now contains encrypted data")
HTTP Handler

Handler is an HTTP middleware you can use to transparently decrypt incoming requests and encrypt outgoing responses.

Incoming requests are decrypted if they come with a header Content-Encoding set to either aes128gcm or aes256gcm. Similarly, responses are encrypted if the request's Accept-Encoding or X-Accept-Encoding headers are set to either value.

h := http.HandlerFunc(
  func(w http.ResponseWriter, r *http.Request) {
    // r.Body now contains plain data if the client sent
    // encrypted request.

    // w.Write will encrypt the data before sending
    // it back.
  },
)

var (
  key = []byte("256-bit long key")
  rs  = 4096
)
http.ListenAndServe(":8000", ece.Handler(key, rs, h))
HTTP Client

Client is a wrapper around http.Client, and handles the encryption of outgoing requests, and the decryption of responses.

Requests are systematically encrypted, while responses are only decrypted if the Content-Encoding header is set to aes128gcm or aes256gcm.

var (
  keyID       = "ID of the key below"              // (Empty string to omit)
  key         = []byte("16 or 32 byte long key")
  payload     = strings.NewReader(`{"key": "value"}`)
)

c, err := ece.NewClient(keyID, key)
if err != nil {
  log.Fatalf("error initializing the client: %v", err)
}

resp, err := c.Post("https://api.example.com", "application/json", payload)
if err != nil {
  log.Fatalf("HTTP request failed: %v", err)
}

// payload was encrypted before it was sent

// resp.Body is decrypted if the server returned an encrypted response.
data, err := io.ReadAll(resp.Body)
if err != nil {
  log.Fatalf("error reading response: %v", err)
}

log.Println(data) // plain data

Contributions

Contributions are welcome via Pull Requests.

About us

What if you're hit by a bus tomorrow? Posterity helps you make a plan in the event something happens to you.

Documentation

Overview

Package ece provides support for reading and writing streams encoded using ECE (Encrypted-Content-Encoding) for HTTP, as defined in RFC8188.

Reader can read and decipher encrypted data, while Writer can be used to write a cipher into an underlying io.Writer.

Client is an HTTP client capable of encrypting requests before they're sent, and decrypting responses as they're received.

Handler is an HTTP middleware capable of transparently decrypting incoming requests and encrypting outgoing responses for clients that support it.

AES-GCM

While RFC8188 only mentions AES-128-GCM, this implementation extends it with support for 256-bit encryption (i.e. AES-256-GCM).

Use 32-byte keys for AES-256-GCM, and 16-byte ones for AES-128-GCM.

	key := ece.AES256GCM.RandomKey()
	fmt.Println(len(key))

 -> 32

Record Size

ECE encrypts data in chunks of predetermined length. The value can be anything above 17 characters, which corresponds to the AES-GCM tag length (16 bytes), plus a block-delimiter (1 byte).

The ideal value depends on your use case. Smaller values create longer ciphers but require less memory to be decrypted, while larger values generate shorter ciphers, but require more memory for decryption.

RFC8188 recommends using multiples of 16.

Index

Examples

Constants

View Source
const SaltLength int = 16

SaltLength as defined in RFC 8188.

Variables

View Source
var (
	AES128GCM = &Encoding{"aes128gcm", 128}
	AES256GCM = &Encoding{"aes256gcm", 256}
)

Supported encodings.

Functions

func EncodeString

func EncodeString(key []byte, content string) ([]byte, error)

EncodeString encodes the given string using the given key, and a random salt.

func Handler

func Handler(key []byte, recordSize int, h http.Handler) http.Handler

Handler is an HTTP middleware that can transparently decrypt incoming requests and encrypt outgoing responses.

Incoming requests are decrypted if their Content-Encoding header is either "aes128gcm" or "aes256gcm". Similarly, responses are encrypted if the the Accept-Encoding (or X-Accept-Encoding) header is set to either value.

If the configured key doesn't match the encoding scheme announced in a request, the server will responds with status code 415 Unsupported Media Type.

Example
h := http.HandlerFunc(
	func(w http.ResponseWriter, r *http.Request) {
		// r.Body now contains plain data if the client sent
		// encrypted request.

		// w.Write will encrypt the data before sending
		// it back.
	},
)

var (
	key = []byte("256-bit long key")
	rs  = 4096
)
http.ListenAndServe(":8000", Handler(key, rs, h))
Output:

func NewRandomSalt

func NewRandomSalt() []byte

NewRandomSalt returns a randomly generated salt (SaltLength bytes).

func Pipe

func Pipe(src io.Reader, key []byte, recordSize int, keyID string) (io.ReadCloser, error)

Pipe returns a reader from which the encrypted content in src can be read in clear.

Pipe will read src until EOF is reached.

Example
var plain io.ReadCloser

r, err := Pipe(plain, key, 4096, "")
if err != nil {
	log.Fatal(err)
}

http.Post("example.com", "application/octet/stream", r)

// The HTTP POST request was sent with the content of plain
// encrypted.
Output:

Types

type Cipher

type Cipher []byte

Cipher represents an ECE-encoded cipher.

Cipher is useful to validate a value parsed from using [json.Unmarshaler], or read from a database with [sql.Scanner].

func (*Cipher) Scan

func (c *Cipher) Scan(src any) error

Scan implements [sql.Scanner] and returns an error if v is not a []byte that starts with a valid ECE header.

func (*Cipher) UnmarshalJSON

func (c *Cipher) UnmarshalJSON(b []byte) error

UnmarshalJSON implements [json.Unmarshaler], and returns an error if b does not start with a valid ECE header.

type Client

type Client struct {
	Strict bool

	*http.Client
	// contains filtered or unexported fields
}

Client is a wrapper around http.Client, and handles the encryption of outgoing requests, and the decryption of responses.

Requests are systematically encrypted, while responses are only decrypted if the Content-Encoding header is set to "aes128gcm" or "aes256gcm".

Example
var (
	keyID   = "ID of the key below" // (Empty string to omit)
	key     = []byte("16 or 32 byte long key")
	payload = strings.NewReader(`{"key": "value"}`)
)

c, err := NewClient(keyID, key)
if err != nil {
	log.Fatalf("error initializing the client: %v", err)
}

resp, err := c.Post("https://api.example.com", "application/json", payload)
if err != nil {
	log.Fatalf("HTTP request failed: %v", err)
}

// payload was encrypted before it was sent
// resp.Body is decrypted if the server returned an encrypted response.
data, err := io.ReadAll(resp.Body)
if err != nil {
	log.Fatalf("error reading response: %v", err)
}

log.Println(data) // plain data
Output:

func NewClient

func NewClient(keyID string, key []byte) (*Client, error)

NewClient returns a new HTTP client capable of encoding and decoding ECE.

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do encrypts the content of req.Body before it's sent, and decrypts the content of resp.Body before it's read.

func (*Client) Get

func (c *Client) Get(url string) (*http.Response, error)

Get issues a GET to the specified URL.

func (*Client) Post

func (c *Client) Post(url, contentType string, body io.Reader) (*http.Response, error)

Post issues a POST to the specified URL.

type Encoding

type Encoding struct {
	Name string
	Bits int
}

Encoding represents a type of supported encoding.

func EncodingFromString

func EncodingFromString(encoding string) (*Encoding, bool)

EncodingFromString returns the encoding that corresponds to the given string.

func (*Encoding) NewReader

func (e *Encoding) NewReader(key []byte, r io.ReadCloser) (io.Reader, error)

NewReader returns a new reader for this encoding.

func (*Encoding) NewWriter

func (e *Encoding) NewWriter(key, salt []byte, recordSize int, keyID string, w io.Writer) (io.Writer, error)

NewWriter returns a new writer for this encoding.

func (*Encoding) RandomKey

func (e *Encoding) RandomKey() []byte

RandomKey returns a random key suitable for this encoding. The function will panic if it can't generate random data using crypto/rand.

func (*Encoding) String

func (e *Encoding) String() string

String returns the name of e.

type Error

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

Error represents an ECE-related error that occurred during decryption.

func (*Error) Error

func (err *Error) Error() string

Error implements the error interface.

func (*Error) Unwrap

func (err *Error) Unwrap() error

Unwrap reveals the underlying error.

type Header []byte

Header represents the header of an encrypted message.

Structure:

+-----------+--------+-----------+---------------+
| salt (16) | rs (4) | idLen (1) | keyID (idLen) |
+-----------+--------+-----------+---------------+

func NewHeader

func NewHeader(salt []byte, recordSize int, keyID string) (Header, error)

NewHeader returns a new encoding header with the given parameters.

func (Header) KeyID

func (h Header) KeyID() string

KeyID returns the ID of the key used to encrypt a message.

func (*Header) ReadFrom

func (h *Header) ReadFrom(r io.Reader) (n int64, err error)

ReadFrom reads from r until the header h is fully formed.

ReadFrom implements io.ReaderFrom.

func (Header) RecordSize

func (h Header) RecordSize() int

RecordSize returns the size of a single record in a message.

func (Header) Salt

func (h Header) Salt() []byte

Salt returns the random salt in h.

type Reader

type Reader struct {
	Header Header // nil until enough bytes are read
	// contains filtered or unexported fields
}

Reader decrypts data form an underlying io.Reader.

Example
var key []byte           // Main decryption key
var cipher io.ReadCloser // AES-GCM encrypted data

r := NewReader(key, cipher)
plain, err := io.ReadAll(r)
if err != nil {
	log.Fatalf("error during decryption: %v", err)
}
defer r.Close()

fmt.Println(plain) // plain version of the content of cipher.
Output:

func NewReader

func NewReader(key []byte, r io.Reader) *Reader

NewReader deciphers data read from r.

func (*Reader) Close

func (d *Reader) Close() error

Close closes the underlying reader if it implements io.Closer.

func (*Reader) Read

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

Read implements io.Reader.

func (*Reader) WriteTo

func (d *Reader) WriteTo(dst io.Writer) (n int64, err error)

WriteTo copies the content of d into dst using a buffer optimized for the record size declared in the header of the ECE cipher.

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

ResponseWriter wraps a pre-existing http.ResponseWriter to add supports for encryption using ECE.

func NewResponseWriter

func NewResponseWriter(key []byte, recordSize int, w http.ResponseWriter) (*ResponseWriter, error)

NewResponseWriter upgrades w to write ECE-encoded data in HTTP responses.

func (*ResponseWriter) Flush

func (w *ResponseWriter) Flush()

Flush implements http.Flusher.

Flush must be called in order for the data written to the underlying ResponseWriter to be formatted correctly.

func (*ResponseWriter) Write

func (w *ResponseWriter) Write(p []byte) (int, error)

See the documentation for http.ResponseWriter.

type Writer

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

Writer encrypts data before it's written to an underlying io.Writer.

Example
var key []byte     // Main decryption key
var dest io.Writer // Where the encrypted data will be written

var (
	salt       = NewRandomSalt()      // Must be random
	recordSize = 4096                 // Bytes per block in the cipher
	keyID      = "ID of the main key" // (Empty string to omit)
)
w, err := NewWriter(key, salt, recordSize, keyID, dest)
if err != nil {
	log.Fatalf("error initializing writer: %v", err)
}
defer w.Close() // Cipher may be mis-formatted if omitted

if _, err := io.WriteString(w, "Hello, World!"); err != nil {
	log.Fatalf("error writing cipher: %v", err)
}

log.Println("dest now contains encrypted data")
Output:

func NewWriter

func NewWriter(key, salt []byte, recordSize int, keyID string, w io.Writer) (*Writer, error)

NewWriter writes encrypted data into w.

func (*Writer) Close

func (e *Writer) Close() (err error)

Close Flushes any remaining data in the buffer, and tries to close the underlying writer if it implements io.Closer. It's an error to call Write() after calling Close().

func (*Writer) Flush

func (e *Writer) Flush()

Flush writes any currently buffered data to the underlying writer.

func (*Writer) ReadFrom

func (e *Writer) ReadFrom(r io.Reader) (n int64, err error)

ReadFrom copies the content of r into the writer using a buffer optimized for the configured record size.

func (*Writer) Write

func (e *Writer) Write(p []byte) (n int, err error)

Write implements io.Writer.

Jump to

Keyboard shortcuts

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