Documentation
¶
Overview ¶
Package dealcode maps a non-negative integer counter (from a database sequence or any other source that never repeats) to a short, fixed-alphabet, random-looking string called a code, and back.
The mapping is a bijection (a keyed permutation, FF1 format-preserving encryption per NIST SP 800-38G), so two different counters can never produce the same code: uniqueness of codes reduces entirely to uniqueness of counters. Codes start at a minimum length and grow one character at a time only when the current length is exhausted. Without the key, codes carry no usable order or volume information.
This package implements format version 1 of the dealcode specification (SPEC.md at the repository root) and is byte-for-byte interoperable with the other language implementations in the same repository.
A Codec is immutable and safe for concurrent use by multiple goroutines; create one per code namespace at startup and reuse it:
codec, err := dealcode.New(dealcode.Config{
KeyString: os.Getenv("DEALCODE_KEY"),
Domain: "orders",
})
if err != nil {
log.Fatal(err)
}
code, err := codec.Encode(42) // e.g. "4b71b7"
n, err := codec.Decode(code) // 42
Dealcode codes are not authentication tokens: the code space is small and an online attacker can guess valid codes at a rate proportional to issued/capacity. Rate-limit lookups, and use >=128-bit random tokens for anything security-critical.
Example ¶
Example maps counters to codes and back with the default hex alphabet. In production, load the key from your secret manager and never change it once codes have been issued.
package main
import (
"encoding/hex"
"fmt"
"log"
dealcode "github.com/algorix-hq/dealcode/go"
)
func main() {
key, _ := hex.DecodeString("000102030405060708090a0b0c0d0e0f")
codec, err := dealcode.New(dealcode.Config{Key: key})
if err != nil {
log.Fatal(err)
}
code, _ := codec.Encode(1)
fmt.Println(code)
n, _ := codec.Decode(code)
fmt.Println(n)
}
Output: 38fa1e 1
Index ¶
- Variables
- func Preset(name string) (chars string, ok bool)
- type Codec
- func (c *Codec) Alphabet() string
- func (c *Codec) Capacity() uint64
- func (c *Codec) Decode(code string) (int64, error)
- func (c *Codec) Domain() string
- func (c *Codec) Encode(n int64) (string, error)
- func (c *Codec) MaxLength() int
- func (c *Codec) MinLength() int
- func (c *Codec) Radix() int
- func (c *Codec) String() string
- type Config
- type CycleCodec
- func (c *CycleCodec) Alphabet() string
- func (c *CycleCodec) Capacity() uint64
- func (c *CycleCodec) CycleOf(n int64) (int64, error)
- func (c *CycleCodec) Decode(code string, cycle int64) (int64, error)
- func (c *CycleCodec) Domain() string
- func (c *CycleCodec) Encode(n int64) (string, error)
- func (c *CycleCodec) Length() int
- func (c *CycleCodec) MaxCycle() int64
- func (c *CycleCodec) Radix() int
- func (c *CycleCodec) String() string
- type CyclingConfig
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrConfig reports an invalid codec configuration: bad key material, // alphabet, lengths, or domain. It is returned only by New. ErrConfig = errors.New("dealcode: invalid configuration") // ErrRange reports an Encode counter outside [0, Capacity()). ErrRange = errors.New("dealcode: counter out of range") // ErrInvalidCode reports a Decode input that fails length, charset, or // stage-range validation — i.e. a string this codec never issued. ErrInvalidCode = errors.New("dealcode: invalid code") )
Sentinel errors. Every error returned by this package wraps exactly one of these, so callers can classify failures with errors.Is while the returned error itself carries a descriptive message:
n, err := codec.Decode(input)
if errors.Is(err, dealcode.ErrInvalidCode) {
// input was never issued by this codec
}
Functions ¶
Types ¶
type Codec ¶
type Codec struct {
// contains filtered or unexported fields
}
Codec is a bijective counter <-> code mapping (dealcode format version 1).
A Codec is immutable after New and safe for concurrent use by multiple goroutines without external locking. It is cheap to keep around: create one per code namespace at startup and reuse it.
func New ¶
New validates cfg and builds a Codec. All configuration violations from SPEC.md §2 are reported as errors wrapping ErrConfig.
func (*Codec) Alphabet ¶
Alphabet returns the codec's alphabet characters in numeral order (the character at index i represents numeral value i).
func (*Codec) Capacity ¶
Capacity returns the number of encodable counters: min(radix^MaxLength, 2^63). Encode accepts exactly [0, Capacity()).
func (*Codec) Decode ¶
Decode maps a code back to its counter (SPEC.md §7). The alphabet's normalization (e.g. hex is case-insensitive, crockford also folds O->0 and I/L->1) is applied first; custom alphabets require an exact match. Any string this codec could never have issued — wrong length, characters outside the alphabet, or a value outside the code's stage or the counter space — yields an error wrapping ErrInvalidCode.
Decode success only proves the code is consistent with the key; the application still decides whether counter n actually exists.
func (*Codec) Encode ¶
Encode maps counter n to its code (SPEC.md §5). The code's length depends only on n's stage: MinLength() characters until the counter reaches radix^MinLength, one more character per exhausted stage after that. Encode is O(1) in n and returns an error wrapping ErrRange when n is outside [0, Capacity()).
type Config ¶
type Config struct {
// Key is binary key material. Bytes of length exactly 16, 24, or 32 are
// used directly as the AES key; any other non-zero length is expanded to
// an AES-256 key via SHA-256("dealcode/v1/kdf" || Key).
Key []byte
// KeyString is string key material (a passphrase, hex blob, base64 blob —
// anything). It is always expanded, regardless of length or content, via
// SHA-256("dealcode/v1/kdf" || UTF-8 bytes); a hex-looking string is not
// auto-decoded. A passphrase key is exactly as strong as the passphrase;
// prefer >=128-bit random material (e.g. `openssl rand -hex 32`).
KeyString string
// Alphabet is a preset name — "dec", "hex", "base32", "crockford",
// "base36", "base58", "base62", "base64url" (see Preset) — or a custom
// alphabet string of 2 to 94 distinct printable ASCII characters
// (0x21-0x7E). Preset names win on conflict. Empty defaults to "hex".
Alphabet string
// MinLength is the length codes start at. Zero defaults to 6. It must be
// at least 2, with radix^MinLength >= 100 (the FF1 structural minimum).
MinLength int
// MaxLength is the length codes may grow to. Zero defaults to the largest
// L with radix^L <= 2^63-1 (hex: 15, dec: 18, base32/crockford: 12,
// base58/base62/base64url: 10, ...). It must satisfy
// MinLength <= MaxLength and radix^MaxLength <= 2^128. Set
// MinLength == MaxLength for fixed-length codes.
MaxLength int
// Domain is an application-chosen namespace label (e.g. "orders",
// "coupons"), bound into the FF1 tweak: two codecs with the same key but
// different domains produce unrelated permutations. It must be valid
// UTF-8 of at most 255 bytes. Empty is a valid (default) domain.
Domain string
}
Config describes a dealcode codec (SPEC.md §2).
Exactly one of Key and KeyString must be set. For a given code namespace (one counter sequence) the entire configuration — key material, alphabet, lengths, and domain — must never change once codes have been issued; changing any of it creates a second, unrelated permutation whose outputs may collide with already-issued codes.
type CycleCodec ¶
type CycleCodec struct {
// contains filtered or unexported fields
}
CycleCodec is a fixed-length cycling codec (dealcode mode v1c): codes are always exactly Length() characters, and the counter space is spent in cycles of Capacity() codes each. Counter n belongs to cycle n / Capacity() with in-cycle value n % Capacity(); every cycle is a different permutation of the same code space (a different FF1 tweak), so when the space is exhausted it refills in a new order instead of growing.
Codes REPEAT across cycles by design (pigeonhole: the same space is being refilled). Keep at most one cycle's codes live per uniqueness scope — a global UNIQUE(code) index spanning cycles WILL fire; scope it as UNIQUE(cycle, code) — and persist which cycle each live code belongs to: Decode needs it, and the library cannot recover the cycle from the code string.
A CycleCodec is immutable after NewCycling and safe for concurrent use by multiple goroutines without external locking.
func NewCycling ¶
func NewCycling(cfg CyclingConfig) (*CycleCodec, error)
NewCycling validates cfg and builds a CycleCodec. All configuration violations from SPEC.md §11.1 are reported as errors wrapping ErrConfig.
func (*CycleCodec) Alphabet ¶
func (c *CycleCodec) Alphabet() string
Alphabet returns the codec's alphabet characters in numeral order (the character at index i represents numeral value i).
func (*CycleCodec) Capacity ¶
func (c *CycleCodec) Capacity() uint64
Capacity returns the number of codes per cycle: radix^Length(). It is a uint64 because the boundary configuration radix^Length == 2^63 is legal and 2^63 overflows int64.
func (*CycleCodec) CycleOf ¶
func (c *CycleCodec) CycleOf(n int64) (int64, error)
CycleOf returns the cycle that counter n belongs to: n / Capacity(). It returns an error wrapping ErrRange when n is negative (every non-negative int64 is a valid counter in cycling mode).
func (*CycleCodec) Decode ¶
func (c *CycleCodec) Decode(code string, cycle int64) (int64, error)
Decode maps a code issued in the given cycle back to its counter (SPEC.md §11.2). The cycle is required: the same string recurs in every cycle, mapping to a different counter each time, so a code alone is ambiguous by design.
A cycle outside [0, MaxCycle()] yields an error wrapping ErrRange. Any string this codec could never have issued in that cycle — wrong length, characters outside the alphabet (after the alphabet's normalization), or a counter at or beyond 2^63 (possible only in the final partial cycle) — yields an error wrapping ErrInvalidCode.
Decode success only proves the code is consistent with the key and cycle; the application still decides whether counter n actually exists.
func (*CycleCodec) Domain ¶
func (c *CycleCodec) Domain() string
Domain returns the codec's namespace label.
func (*CycleCodec) Encode ¶
func (c *CycleCodec) Encode(n int64) (string, error)
Encode maps counter n to its fixed-length code (SPEC.md §11.2). The code belongs to cycle n / Capacity() — the caller must record that cycle (or the currently active cycle) to decode later. Encode returns an error wrapping ErrRange when n is negative; every non-negative int64 is a valid counter.
Codes repeat across cycles: Encode(n) and Encode(n + Capacity()) can return the same string for two different counters. See CycleCodec.
func (*CycleCodec) Length ¶
func (c *CycleCodec) Length() int
Length returns the fixed code length: every code is exactly this many characters, in every cycle.
func (*CycleCodec) MaxCycle ¶
func (c *CycleCodec) MaxCycle() int64
MaxCycle returns the largest usable cycle number: (2^63 - 1) / Capacity(). Decode accepts cycles in [0, MaxCycle()].
func (*CycleCodec) Radix ¶
func (c *CycleCodec) Radix() int
Radix returns the number of characters in the alphabet.
func (*CycleCodec) String ¶
func (c *CycleCodec) String() string
String describes the codec's public configuration. Key material never appears in the output.
type CyclingConfig ¶
type CyclingConfig struct {
// Key is binary key material, with exactly the rules of Config.Key.
Key []byte
// KeyString is string key material, with exactly the rules of
// Config.KeyString.
KeyString string
// Alphabet is a preset name or custom alphabet string, with exactly the
// rules of Config.Alphabet. Empty defaults to "hex".
Alphabet string
// Length is the fixed code length L: every code is exactly L characters
// in every cycle. Zero defaults to 6. It must be in [2, 128] with
// 100 <= radix^L <= 2^63 (exactly 2^63 is allowed); for larger fixed
// spaces use Codec with MinLength == MaxLength instead.
Length int
// Domain is an application-chosen namespace label, with exactly the
// rules of Config.Domain.
Domain string
}
CyclingConfig describes a fixed-length cycling codec (SPEC.md §11).
Exactly one of Key and KeyString must be set; the key rules are identical to Config's. As with Config, the entire configuration — key material, alphabet, length, and domain — must never change once codes have been issued.