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 ¶
- Constants
- Variables
- func EncodeString(key []byte, content string) ([]byte, error)
- func Handler(key []byte, recordSize int, h http.Handler) http.Handler
- func NewRandomSalt() []byte
- func Pipe(src io.Reader, key []byte, recordSize int, keyID string) (io.ReadCloser, error)
- type Cipher
- type Client
- type Encoding
- type Error
- type Header
- type Reader
- type ResponseWriter
- type Writer
Examples ¶
Constants ¶
const SaltLength int = 16
SaltLength as defined in RFC 8188.
Variables ¶
var ( AES128GCM = &Encoding{"aes128gcm", 128} AES256GCM = &Encoding{"aes256gcm", 256} )
Supported encodings.
Functions ¶
func EncodeString ¶
EncodeString encodes the given string using the given key, and a random salt.
func 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 ¶
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 ¶
Scan implements [sql.Scanner] and returns an error if v is not a []byte that starts with a valid ECE header.
func (*Cipher) UnmarshalJSON ¶
UnmarshalJSON implements [json.Unmarshaler], and returns an error if b does not start with a valid ECE header.
type 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".
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 (*Client) Do ¶
Do encrypts the content of req.Body before it's sent, and decrypts the content of resp.Body before it's read.
type Encoding ¶
Encoding represents a type of supported encoding.
func EncodingFromString ¶
EncodingFromString returns the encoding that corresponds to the given string.
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.
type Error ¶
type Error struct {
// contains filtered or unexported fields
}
Error represents an ECE-related error that occurred during decryption.
type Header ¶
type Header []byte
Header represents the header of an encrypted message.
Structure:
+-----------+--------+-----------+---------------+ | salt (16) | rs (4) | idLen (1) | keyID (idLen) | +-----------+--------+-----------+---------------+
func (*Header) ReadFrom ¶
ReadFrom reads from r until the header h is fully formed.
ReadFrom implements io.ReaderFrom.
func (Header) RecordSize ¶
RecordSize returns the size of a single record in a message.
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:
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.
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 (*Writer) Close ¶
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.