encryptedtype

package module
v0.0.0-...-5de8e39 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

encryptedtype

A SQL column type that transparently encrypts plaintext on write and decrypts on read. Doubles as a gqlgen scalar.

EncryptedString wraps github.com/ubgo/crypt. Writes use AES-256-GCM (crypt.Sealer.Seal). Reads use crypt.OpenAuto, which transparently decrypts both AES-256-GCM (modern AEAD) and AES-CBC (peer format) — so existing CBC-encrypted columns continue to read without migration.

Integration Hooks Cost to your dependency tree
database/sql Value() / Scan() stdlib + ubgo/crypt
encoding/json MarshalJSON (always null) / UnmarshalJSON (accepts plaintext) stdlib
gqlgen scalar MarshalGQL (plaintext) / UnmarshalGQL (plaintext) none — duck-typed

Install

go get github.com/ubgo/encryptedtype

Boot wiring (required)

import "github.com/ubgo/encryptedtype"

func main() {
    key := []byte(os.Getenv("ENCRYPTION_KEY"))   // 32 bytes for AES-256
    if err := encryptedtype.SetKey(key); err != nil {
        log.Fatal(err)
    }
    // ... start server
}

The key must be 16, 24, or 32 bytes (AES-128 / AES-192 / AES-256). New writes always use AES-256-GCM regardless of key length; the smaller key sizes still encrypt correctly, they just produce shorter sealing keys for the GCM cipher.

Use

import "github.com/ubgo/encryptedtype"

type Partner struct {
    ID           string
    ClientSecret encryptedtype.EncryptedString
}

// Save → ciphertext column
db.Exec(`INSERT INTO partners(id, client_secret) VALUES (?, ?)`,
    p.ID, encryptedtype.New(plaintext))

// Load → plaintext in memory
var loaded Partner
db.QueryRow(`SELECT id, client_secret FROM partners WHERE id = ?`, "x").
    Scan(&loaded.ID, &loaded.ClientSecret)
fmt.Println(loaded.ClientSecret.Plain())   // recovered plaintext

In a gqlgen gqlgen.yml:

models:
  EncryptedString:
    model: github.com/ubgo/encryptedtype.EncryptedString

For server-only fields, mark with the @internal directive in your schema:

scalar EncryptedString

type Partner {
  id: ID!
  clientSecret: EncryptedString! @internal
}

Behaviour

  • Reads accept both AES-256-GCM and AES-CBC ciphertexts. No migration step needed if you have existing CBC data.
  • Writes always use AES-256-GCM. Future reads of new writes go through the AEAD path.
  • Defense in depth on non-DB outputs. String() returns "[encrypted]", MarshalJSON returns null, GoString() returns "[encrypted]". The plaintext only appears via Plain() (explicit) and MarshalGQL (schema-author opt-in).
  • Zero value is unset. A EncryptedString{} returns false from IsSet, nil from Value, and "" from Plain.
  • Scan(nil) clears — useful for nullable columns.

License

Apache-2.0 — see LICENSE.

Documentation

Overview

Package encryptedtype provides EncryptedString — a SQL column type that transparently encrypts plaintext on write and decrypts on read.

On the way out (Value) the plaintext is sealed with AES-256-GCM via the configured Sealer from github.com/ubgo/crypt.

On the way in (Scan) the ciphertext is opened via crypt.OpenAuto, which transparently dispatches between AES-256-GCM (the modern AEAD format) and AES-CBC (a peer format kept first-class for interop). Existing CBC-encrypted columns continue to read without a migration step.

Boot wiring is required exactly once per process:

key, _ := loadEncryptionKey()
if err := encryptedtype.SetKey(key); err != nil {
    log.Fatal(err)
}

gqlgen integration is duck-typed: this package does not import gqlgen.

Defense in depth: String returns "[encrypted]" and MarshalJSON returns null so the plaintext does not leak via fmt or JSON paths. The only outbound channels that see plaintext are MarshalGQL (because the schema author opted in by exposing the field) and Plain() (an explicit caller-side accessor).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Reset

func Reset()

Reset clears the stored key. Intended for tests that want to verify the "not configured" error path.

func SetKey

func SetKey(key []byte) error

SetKey configures the AES key used by EncryptedString. The key must be exactly 16, 24, or 32 bytes — the same constraints crypt.NewSealer applies. Returns an error if the key is invalid.

SetKey is goroutine-safe and may be called more than once (e.g. for key rotation); callers are responsible for ensuring no Value/Scan is in flight when the key changes.

func SetSealer

func SetSealer(s *crypt.Sealer, key []byte)

SetSealer is a lower-level escape hatch for callers who already hold a *crypt.Sealer. Note that Scan uses crypt.OpenAuto which requires the raw key bytes; SetSealer alone disables the AES-CBC fallback path on reads. Prefer SetKey unless you have a specific reason.

Types

type EncryptedString

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

EncryptedString stores plaintext in memory and encrypts/decrypts at the SQL boundary.

func New

func New(plaintext string) EncryptedString

New returns an EncryptedString holding the given plaintext.

func (EncryptedString) GoString

func (e EncryptedString) GoString() string

GoString returns "[encrypted]".

func (EncryptedString) IsSet

func (e EncryptedString) IsSet() bool

IsSet reports whether the value has been assigned.

func (EncryptedString) MarshalGQL

func (e EncryptedString) MarshalGQL(w io.Writer)

MarshalGQL writes the plaintext as a JSON string. The schema author opted into exposing the field via GraphQL; if the field should be server-only, mark it with the `@internal` directive in the schema.

func (EncryptedString) MarshalJSON

func (e EncryptedString) MarshalJSON() ([]byte, error)

MarshalJSON returns null. JSON consumers that want the plaintext must call Plain() and serialise it explicitly.

func (EncryptedString) Plain

func (e EncryptedString) Plain() string

Plain returns the in-memory plaintext. Callers that hold an EncryptedString already have access to the value; this accessor is for code that needs to operate on the raw string (e.g. forwarding it to a third-party SDK).

func (*EncryptedString) Scan

func (e *EncryptedString) Scan(src any) error

Scan implements [sql.Scanner]. NULL scans into an unset value. A non-NULL value is opened via crypt.OpenAuto so that both AES-256-GCM and AES-CBC ciphertexts decrypt transparently.

func (EncryptedString) String

func (e EncryptedString) String() string

String returns "[encrypted]" — the plaintext does not appear in fmt output, panic traces, or default Stringer paths.

func (*EncryptedString) UnmarshalGQL

func (e *EncryptedString) UnmarshalGQL(v any) error

UnmarshalGQL accepts a plaintext string from a GraphQL input field.

func (*EncryptedString) UnmarshalJSON

func (e *EncryptedString) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts a string (treated as plaintext) or null.

func (EncryptedString) Value

func (e EncryptedString) Value() (driver.Value, error)

Value implements driver.Valuer. Unset values persist as SQL NULL. Set values are sealed with AES-256-GCM via the configured Sealer.

SetKey must be called once at process boot before Value is used. Calling Value before SetKey returns an error.

Jump to

Keyboard shortcuts

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