incorruptible

package module
v0.0.0-...-7f4acb1 Latest Latest
Warning

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

Go to latest
Published: Mar 15, 2024 License: MIT Imports: 18 Imported by: 2

README ΒΆ

🍸 Incorruptible       GoDoc Go Report Card

The Incorruptible project provides a safer, shorter and faster Bearer Token for session cookie and Authorization HTTP header. See the limitations.

Incorruptible is also a 🍸 drink that the Garçon de café likes to serve to clients. See the origin of the name.

logo

🎯 Target

  • Safer: State-of-the-art cipher configuration, including expiration time and client IP, shuffled BasE91 alphabet, random padding and salt.

  • Shorter: BasE91 encoded (shorter than Base64), optimized data encoding (no string keys) and adaptive compression. The smallest token is 27 bytes long at default settings.

  • Faster: AES-128 (no RSA), hardwired encryption (AMD/Intel processors) and CPU-friendly serializer.

πŸ‘Ά Motivation

At Teal.Finance, our cookies were based on JWT and gorilla/session. The JWT is well standardized. We use the usual way: JSON, Base64, RSA, HMAC-SHA256… This is not very fast and generates large tokens: the long JSON string is converted into Base64 text, to which the signature stuff is appended.

With the purpose of the session cookie purpose, we are free to innovate. We love challenges. As a hobby we tried to replace gorilla/session. The result is Incorruptible. πŸŽ‰

To make the implementation successful, we updated our security knowledge to the latest research. We also benchmarked Base64/Ascii85/BasE91/Base92 encoders. We think we did a good job, with a good tradeoff between security, performance and low bandwidth.

🀫 Usage

Now we use less JWT and more Incorruptible tokens in production:

  • JWT as authentication provided by the Quid server (trusted third party).
  • Incorruptible as a session token (session cookie).
JWT

The JWT is well suited when multiple servers manage the authentication: It avoids sharing the private key. We use the good old RSA with a 32-bytes key (256 bits). The Auth server is the only one that owns the private key. Thus, the backend manages the user login, since the signature provided by the authentication server is sufficient. So our backend can be moderately secure (no user data). Only the authentication server requires high security (for example, we uninstall the SSH daemon on the machine).

Incorruptible

We use Incorruptible when the backend manages alone its relationship with the frontend alone. The secret is known only to the backend (it does not need to be shared).

πŸ” Encryption

The current trend towards symmetric encryption prefers ChaCha20 / Poly1305 (server-side). In addition to its cryptographic qualities, ChaCha20 is easy to configure and requires few CPU/memory resources (chosen by Wireguard).

On the other hand, AES is faster on AMD/Intel processors (optimized instructions). In addition, the Go crypto allows easy and secure AES configuration.

Therefore, Incorruptible supports both ciphers:

  • ChaCha20-Poly1305
  • AES-128 (256 bits is not relevant for fast short cookie)

We place more emphasis on mastering the encryption configuration than on performance. See also https://go.dev/blog/tls-cipher-suites.

The encryption depends only on standard Go library. The package "math/rand" is used when a strong random number generator is not required ("math/rand" is 40 times faster than "crypto/rand"). The user may call rand.Seed() to randomize the "math/rand" generator.

Read more about our security design.

Please share your thoughts on security or other topics.

πŸͺ Encoding format

Serialization has been designed for the Incorruptible needs. The format consists of:

  • Magic Code (1 byte)
  • Random salt (1 byte)
  • Header bits (1 byte)
  • Expiration time (from 0 to 4 bytes)
  • Client IP (0, 4 or 16 bytes)
  • Conveyed values, up to 31 values (from 0 to 7900 bytes)
  • Optional random padding (padding length is also random)

See also https://pkg.go.dev/github.com/teal-finance/incorruptible/format.

The precision of the expiration time is defined at build time with constants in the source code. The default encoding size is 24 bits, giving a range of 10 years with an accuracy of 20 seconds. The configuration constants allow to easily decrease/increase of the storage from 1 to 4 bytes, reducing/improving the timing precision.

Random padding can also be appended. This feature is currently disabled, but can be enabled in the source code.

If the token is too long, its payload is compressed with Snappy S2.

Then, the entire data bytes are encrypted with AES-GCM 128 bits. This encryption adds 28 bytes: 12 bytes for the nonce, and 16 bytes for the GCM tag including the authentication. We may split the nonce and trim the GCM tag in a future release… Please share your thoughts.

Finally, the cipher-text is encoded with BasE91, which produces cookie-friendly tokens at the cost of increasing the size by 19% (³⁄₁₆). In comparison, Base64 and Ascii85 increase the size by 33% and 25%, respectively.

In the end, the minimum required 3 bytes (Magic+Salt+Header) becomes a 42-bytes long Incorruptible token (BasE91).

🚫 Limitations

Incorruptible works perfectly with a single server. Secrets can be stored in a data vault, or randomly generated at startup time.

However, with multiple servers (load-balancer, authentication server) the encryption key must be shared.

In this last case, JWT/CWT are preferable, since sharing secrets is a weak link in the security chain.

See also Quid, a JWT authentication server with public-key verified signatures.

🍸 Name

The name Incorruptible comes from the incorruptible drink, a mocktail with lemonade, grapefruit, and orange juice.

The Incorruptible project was originally implemented as part of the Teal.Finance/Garcon server. In French, "Garcon" (garΓ§on) is a πŸ’β€β™‚οΈ waiter, who serves drinks to clients. πŸ˜‰

We wanted a name for a drink without alcohol, that uses a single word, and could be understood in different languages. So Incorruptible was our best choice at that time.

✨ Contributions welcome

This new project needs your help to get better. Please suggest your improvements or even further refactoring.

We welcome contributions in many forms, and there is always plenty to do!

πŸ—£οΈ Feedback

If you have some suggestions or need a new feature, please open an issue or contact us at Teal.Finance@pm.me / @TealFinance.

Feel free to pull a request too. Your contributions are welcome. πŸ˜‰

Copyright (c) 2022 Teal.Finance/incorruptible contributors

Teal.Finance/incorruptible is free software, and may be redistributed and/or modified under the terms of the MIT License. SPDX-License-Identifier: MIT

Teal.Finance/incorruptible is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the LICENSE file (alongside the source files) or https://opensource.org/licenses/MIT.

Documentation ΒΆ

Overview ΒΆ

Package incorruptible provides a safer, shorter, faster secret token for session cookie and Authorization HTTP header.

Index ΒΆ

Constants ΒΆ

View Source
const (
	HeaderSize = magicCodeSize + saltSize + metadataSize

	MaxValues int = maskNValues
)
View Source
const (
	// The expiry is stored in 3 bytes, with a 30 seconds precision, starting from 2022.
	ExpiryStartYear = 2022
	ExpiryMaxYear   = ExpiryStartYear + rangeInYears

	ExpirySize = 3 // 24 bits = 10 years with 20-second precision

	PrecisionInSeconds = 20
)
View Source
const (
	HTTP  = "http"
	HTTPS = "https"
)

URL schemes.

View Source
const (
	// Base91MinSize and ciphertextMinSize need to be adapted according
	// on any change about expiry encoding size, padding size...
	Base91MinSize = 42
)
View Source
const (
	EnablePadding = false
)

Variables ΒΆ

This section is empty.

Functions ΒΆ

func AppendIP ΒΆ

func AppendIP(buf []byte, ip net.IP) []byte

func BytesToUint64 ΒΆ

func BytesToUint64(buf []byte) (uint64, error)

func DecodeExpiry ΒΆ

func DecodeExpiry(buf []byte) ([]byte, int64)

func Decrypt ΒΆ

func Decrypt(aead cipher.AEAD, all []byte) (plaintext []byte, err error)

Decrypt decrypts the ciphertext using any AEAD cipher. The parameter "all" contains the nonce + the ciphertext + the potential GCM tag. in the format "nonce|ciphertext|tag" where '|' indicates concatenation.

func Encrypt ΒΆ

func Encrypt(aead cipher.AEAD, plaintext []byte) []byte

Encrypt encrypts data using the given cipher. Output takes the form "nonce|ciphertext|tag" where '|' indicates concatenation.

"math/rand" is 40 times faster than "crypto/rand" see: https://github.com/SimonWaldherr/golang-benchmarks#random

func MagicCode ΒΆ

func MagicCode(buf []byte) uint8

func Marshal ΒΆ

func Marshal(tv TValues, magic uint8) ([]byte, error)

Marshal serializes a TValues in a short way. The format starts with a magic code (2 bytes), followed by the expiry time, the client IP, the user-defined values, and ends with random salt as padding for a final size aligned on 32 bits.

func NewAESCipher ΒΆ

func NewAESCipher(secretKey []byte) cipher.AEAD

NewAESCipher creates a cipher with Encrypt() and Decrypt() functions for AEAD (Authenticated Encryption with Associated Data).

Implementation is based on: - https://wikiless.org/wiki/Authenticated_encryption - https://go.dev/blog/tls-cipher-suites - https://github.com/gtank/cryptopasta

The underlying algorithm is AES-128 GCM: - AES is a symmetric encryption, faster than asymmetric (e.g. RSA) - 128-bit key is sufficient for most usages (256-bits is much slower)

Assumption design: This function should be used on AES-supported hardware like AMD/Intel processors providing optimized AES instructions set. If this is not your case, please use NewChaChaCipher().

GCM (Galois Counter Mode) is preferred over CBC (Cipher Block Chaining) because of CBC-specific attacks and configuration difficulties. But, CBC is faster and does not have any weakness in our server-side use case. If requested, this implementation may change to use CBC. Your feedback or suggestions are welcome, please contact us.

This package follows the Golang Cryptography Principles: https://golang.org/design/cryptography-principles Secure implementation, faultlessly configurable, performant and state-of-the-art updated.

func NewChaCipher ΒΆ

func NewChaCipher(secretKey []byte) cipher.AEAD

NewChaCipher creates a cipher for ChaCha20-Poly1305. with Encrypt() and Decrypt() functions.

func NewCipher ΒΆ

func NewCipher(secretKey []byte) cipher.AEAD

func PutExpiry ΒΆ

func PutExpiry(buf []byte, unix int64) error

func Uint64ToBytes ΒΆ

func Uint64ToBytes(v uint64) []byte

Uint64ToBytes works on the byte-level encoding of the Incorruptible token.

Types ΒΆ

type Incorruptible ΒΆ

type Incorruptible struct {
	SetIP bool // If true => put the remote IP in the token.
	// contains filtered or unexported fields
}

func New ΒΆ

func New(writeErr WriteErr, urls []*url.URL, secretKey []byte, cookieName string, maxAge int, setIP bool) *Incorruptible

New creates a new Incorruptible. The order of the parameters are consistent with garcon.NewJWTChecker (see Teal-Finance/Garcon). The Garcon middleware constructors use a garcon.Writer as first parameter. Please share your thoughts/feedback, we can still change that.

func (*Incorruptible) BearerToken ΒΆ

func (incorr *Incorruptible) BearerToken(r *http.Request) (string, error)

BearerToken returns the token (in base91 format) from the HTTP Authorization header.

func (*Incorruptible) Chk ΒΆ

func (incorr *Incorruptible) Chk(next http.Handler) http.Handler

Chk is a middleware accepting requests only if it has a valid Incorruptible cookie, Chk does not consider the "Authorization" header (only the token within the cookie). Use instead the Vet() middleware to also verify the "Authorization" header. Chk finally stores the decoded token in the request context. In dev. mode, Chk accepts requests without valid cookie but does not store invalid tokens.

func (*Incorruptible) Cookie ΒΆ

func (incorr *Incorruptible) Cookie(_ int) *http.Cookie

Cookie returns a pointer to the default cookie values. This can be used to customize some cookie values (may break), and also to facilitate testing.

func (*Incorruptible) CookieName ΒΆ

func (incorr *Incorruptible) CookieName() string

func (*Incorruptible) CookieToken ΒΆ

func (incorr *Incorruptible) CookieToken(r *http.Request) (string, error)

CookieToken returns the token (in base91 format) from the cookie.

func (*Incorruptible) DeadCookie ΒΆ

func (incorr *Incorruptible) DeadCookie() *http.Cookie

DeadCookie returns an Incorruptible cookie without Value and with "Max-Age=0" in order to delete the Incorruptible cookie in the current HTTP session.

Example:

func logout(w http.ResponseWriter, r *http.Request) {
    http.SetCookie(w, Incorruptible.DeadCookie())
}

func (*Incorruptible) Decode ΒΆ

func (incorr *Incorruptible) Decode(base91 string) (TValues, error)

func (*Incorruptible) DecodeBearerToken ΒΆ

func (incorr *Incorruptible) DecodeBearerToken(r *http.Request) (TValues, error)

func (*Incorruptible) DecodeCookieToken ΒΆ

func (incorr *Incorruptible) DecodeCookieToken(r *http.Request) (TValues, error)

func (*Incorruptible) DecodeToken ΒΆ

func (incorr *Incorruptible) DecodeToken(r *http.Request) (TValues, []any)

func (*Incorruptible) Encode ΒΆ

func (incorr *Incorruptible) Encode(tv TValues) (string, error)

func (*Incorruptible) NewCookie ΒΆ

func (incorr *Incorruptible) NewCookie(r *http.Request, keyValues ...KVal) (*http.Cookie, TValues, error)

NewCookie creates a new cookie based on default values. the HTTP request parameter is used to get the remote IP (only when incorr.SetIP is true).

func (*Incorruptible) NewCookieFromToken ΒΆ

func (incorr *Incorruptible) NewCookieFromToken(token string, maxAge int) *http.Cookie

func (*Incorruptible) NewCookieFromValues ΒΆ

func (incorr *Incorruptible) NewCookieFromValues(tv TValues) (*http.Cookie, error)

func (*Incorruptible) NewTValues ΒΆ

func (incorr *Incorruptible) NewTValues(r *http.Request, keyValues ...KVal) (TValues, error)

func (*Incorruptible) Set ΒΆ

func (incorr *Incorruptible) Set(next http.Handler) http.Handler

Set is a middleware putting a "session" cookie when the request has no valid "incorruptible" token. The token is searched in the "session" cookie and in the first "Authorization" header. The "session" cookie (that is added in the response) contains a minimalist "incorruptible" token. Finally, Set stores the decoded token in the request context.

func (*Incorruptible) Vet ΒΆ

func (incorr *Incorruptible) Vet(next http.Handler) http.Handler

Vet is a middleware accepting requests having a valid Incorruptible token either in the cookie or in the first "Authorization" header. Vet finally stores the decoded token in the request context. In dev. mode, Vet accepts requests without a valid token but does not store invalid tokens.

type KBool ΒΆ

type KBool struct {
	Key int
	Val bool
}

func Bool ΒΆ

func Bool(k int, v ...bool) KBool

func (KBool) Bool ΒΆ

func (kv KBool) Bool() bool

func (KBool) Get ΒΆ

func (kv KBool) Get(tv *TValues) (KVal, error)

func (KBool) Int64 ΒΆ

func (kv KBool) Int64() int64

func (KBool) Set ΒΆ

func (kv KBool) Set(tv *TValues) error

func (KBool) String ΒΆ

func (kv KBool) String() string

func (KBool) Uint64 ΒΆ

func (kv KBool) Uint64() uint64

type KInt64 ΒΆ

type KInt64 struct {
	Key int
	Val int64
}

func Int64 ΒΆ

func Int64(k int, v ...int64) KInt64

func (KInt64) Bool ΒΆ

func (kv KInt64) Bool() bool

func (KInt64) Get ΒΆ

func (kv KInt64) Get(tv *TValues) (KVal, error)

func (KInt64) Int64 ΒΆ

func (kv KInt64) Int64() int64

func (KInt64) Set ΒΆ

func (kv KInt64) Set(tv *TValues) error

func (KInt64) String ΒΆ

func (kv KInt64) String() string

func (KInt64) Uint64 ΒΆ

func (kv KInt64) Uint64() uint64

type KString ΒΆ

type KString struct {
	Key int
	Val string
}

func String ΒΆ

func String(k int, v ...string) KString

func (KString) Bool ΒΆ

func (kv KString) Bool() bool

func (KString) Get ΒΆ

func (kv KString) Get(tv *TValues) (KVal, error)

func (KString) Int64 ΒΆ

func (kv KString) Int64() int64

func (KString) Set ΒΆ

func (kv KString) Set(tv *TValues) error

func (KString) String ΒΆ

func (kv KString) String() string

func (KString) Uint64 ΒΆ

func (kv KString) Uint64() uint64

type KUint64 ΒΆ

type KUint64 struct {
	Key int
	Val uint64
}

func Uint64 ΒΆ

func Uint64(k int, v ...uint64) KUint64

func (KUint64) Bool ΒΆ

func (kv KUint64) Bool() bool

func (KUint64) Get ΒΆ

func (kv KUint64) Get(tv *TValues) (KVal, error)

func (KUint64) Int64 ΒΆ

func (kv KUint64) Int64() int64

func (KUint64) Set ΒΆ

func (kv KUint64) Set(tv *TValues) error

func (KUint64) String ΒΆ

func (kv KUint64) String() string

func (KUint64) Uint64 ΒΆ

func (kv KUint64) Uint64() uint64

type KVal ΒΆ

type KVal interface {
	Set(*TValues) error
	Get(*TValues) (KVal, error)
	Uint64() uint64
	Int64() int64
	Bool() bool
	String() string
}

type Metadata ΒΆ

type Metadata byte

func GetMetadata ΒΆ

func GetMetadata(buf []byte) Metadata

func NewMetadata ΒΆ

func NewMetadata(ipLength int, compressed bool, nValues int) (Metadata, error)

NewMetadata sets the metadata bits within the token.

func (Metadata) DecodeIP ΒΆ

func (meta Metadata) DecodeIP(buf []byte) ([]byte, net.IP)

func (Metadata) IsCompressed ΒΆ

func (meta Metadata) IsCompressed() bool

func (Metadata) NValues ΒΆ

func (meta Metadata) NValues() int

func (Metadata) PayloadMinSize ΒΆ

func (meta Metadata) PayloadMinSize() int

func (Metadata) PutHeader ΒΆ

func (meta Metadata) PutHeader(buf []byte, magic uint8)

PutHeader fills the magic code, the salt and the metadata.

"math/rand" is 40 times faster than "crypto/rand" see: https://github.com/SimonWaldherr/golang-benchmarks#random

type Serializer ΒΆ

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

type TValues ΒΆ

type TValues struct {
	Expires int64  // Unix time UTC (seconds since 1970)
	IP      net.IP // TOTO: use netip.Addr
	Values  [][]byte
}

TValues (Token Values) represents the decoded form of an Incorruptible token.

func EmptyTValues ΒΆ

func EmptyTValues() TValues

EmptyTValues returns an empty TValues that can be used to generate a minimalist token.

func FromCtx ΒΆ

func FromCtx(r *http.Request) (TValues, bool)

FromCtx gets the decoded token from the request context.

func NewTValues ΒΆ

func NewTValues(keyValues ...KVal) (TValues, error)

NewTValues returns an empty TValues that can be used to generate a minimalist token.

func Unmarshal ΒΆ

func Unmarshal(buf []byte) (TValues, error)

func (TValues) Bool ΒΆ

func (tv TValues) Bool(key int) (bool, error)

func (TValues) BoolIfAny ΒΆ

func (tv TValues) BoolIfAny(key int, defaultValue ...bool) bool

func (TValues) CompareExpiry ΒΆ

func (tv TValues) CompareExpiry() int

func (*TValues) EmptyIP ΒΆ

func (tv *TValues) EmptyIP()

func (TValues) ExpiryTime ΒΆ

func (tv TValues) ExpiryTime() time.Time

func (*TValues) Get ΒΆ

func (tv *TValues) Get(keyValues ...KVal) ([]KVal, error)

func (TValues) Int64 ΒΆ

func (tv TValues) Int64(key int) (int64, error)

func (TValues) Int64IfAny ΒΆ

func (tv TValues) Int64IfAny(key int, defaultValue ...int64) int64

func (TValues) KBool ΒΆ

func (tv TValues) KBool(k int, v ...bool) KBool

func (TValues) KInt64 ΒΆ

func (tv TValues) KInt64(k int, v ...int64) KInt64

func (TValues) KString ΒΆ

func (tv TValues) KString(k int, v ...string) KString

func (TValues) KUint64 ΒΆ

func (tv TValues) KUint64(k int, v ...uint64) KUint64

func (TValues) MaxAge ΒΆ

func (tv TValues) MaxAge() int

func (TValues) NoIP ΒΆ

func (tv TValues) NoIP() bool

NoIP returns true when no IP is set within the TValues. NoIP returns false when an IP is present.

func (*TValues) Set ΒΆ

func (tv *TValues) Set(keyValues ...KVal) error

func (*TValues) SetBool ΒΆ

func (tv *TValues) SetBool(key int, val bool) error

func (*TValues) SetExpiry ΒΆ

func (tv *TValues) SetExpiry(maxAge int)

func (*TValues) SetExpiryDuration ΒΆ

func (tv *TValues) SetExpiryDuration(d time.Duration)

func (*TValues) SetExpiryTime ΒΆ

func (tv *TValues) SetExpiryTime(t time.Time)

func (*TValues) SetInt64 ΒΆ

func (tv *TValues) SetInt64(key int, val int64) error

func (*TValues) SetRemoteIP ΒΆ

func (tv *TValues) SetRemoteIP(r *http.Request) error

func (*TValues) SetString ΒΆ

func (tv *TValues) SetString(key int, val string) error

func (*TValues) SetUint64 ΒΆ

func (tv *TValues) SetUint64(key int, val uint64) error

func (*TValues) ShortenIP4Length ΒΆ

func (tv *TValues) ShortenIP4Length()

func (TValues) String ΒΆ

func (tv TValues) String(key int) (string, error)

func (TValues) StringIfAny ΒΆ

func (tv TValues) StringIfAny(key int, defaultValue ...string) string

func (TValues) ToCtx ΒΆ

func (tv TValues) ToCtx(r *http.Request) *http.Request

ToCtx stores the decoded token in the request context.

func (TValues) Uint64 ΒΆ

func (tv TValues) Uint64(key int) (uint64, error)

func (TValues) Uint64IfAny ΒΆ

func (tv TValues) Uint64IfAny(key int, defaultValue ...uint64) uint64

func (TValues) Valid ΒΆ

func (tv TValues) Valid(r *http.Request) error

func (TValues) ValidExpiry ΒΆ

func (tv TValues) ValidExpiry() bool

func (TValues) ValidIP ΒΆ

func (tv TValues) ValidIP(r *http.Request) error

type WriteErr ΒΆ

type WriteErr func(w http.ResponseWriter, r *http.Request, statusCode int, messages ...any)

Jump to

Keyboard shortcuts

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