secret
import "github.com/infobloxopen/devedge-sdk/secret"Package secret covers the two ways the framework keeps a sensitive field out of plaintext storage:
- Verify-only credentials (
(infoblox.field.v1.opts).credential) — an API key or token that the server mints, hands to the client once, and only ever verifies. TheCredentialMinterproduces a prefixed split token and a salted one-way hash; there is no reversible copy. Use this for API keys. - Retrievable secrets (
(infoblox.field.v1.opts).secret) — a value the service must read back and replay. TheEncryptorinterface hashes it for lookup and encrypts it (a reversible cipher) for recovery. Use this only when the plaintext genuinely has to come back.
Generated storage code calls into this package: it mints via CredentialMinter for a credential
field and hashes/encrypts via Encryptor for a secret field, and it never stores either as
plaintext.
Verify-only credentials
The CredentialMinter implements the gold-standard “hash, never encrypt” storage for an API key: a
prefixed split token, minted by the server, returned to the client once, verified in constant time,
with no reversible copy at rest. It is stdlib-only (FIPS-clean, no new dependencies).
// HashSpec is self-describing so a stored credential still verifies after the
// process default changes. Algo is one of AlgoSHA512_256 (default), AlgoSHA384,
// or "pbkdf2-sha256:<iters>:<keylen>".
type HashSpec struct{ Algo string }
// StoredCredential is the at-rest form: the public lookup id (UNIQUE), the salt
// and salted hash (base64), and the hash spec. It holds NO reversible secret.
type StoredCredential struct {
PublicID string
Salt string
Hash string
Spec HashSpec
}
// CredentialMinter mints verify-only credentials. The zero value is usable:
// an empty Prefix defaults to DefaultPrefix ("ib"), an empty Spec.Algo to
// AlgoSHA512_256, and a non-positive SecretBits to DefaultSecretBits (256).
type CredentialMinter struct {
Prefix string
Spec HashSpec
SecretBits int
}
// Mint returns the full token "<prefix>_<public_id>_<secret>" — hand it to the
// client ONCE — and the StoredCredential to persist. The secret is never retained.
func (m *CredentialMinter) Mint() (token string, stored StoredCredential, err error)
// Parse splits a presented token into prefix, publicID, and secret.
func Parse(token string) (prefix, publicID, secret string, err error)
// Verify recomputes stored.Spec's hash of (salt||secret) and constant-time-compares
// it to stored.Hash. It returns (false, nil) on a mismatch and an error only when a
// stored field is malformed or the algorithm is unsupported.
func Verify(secret string, stored StoredCredential) (ok bool, err error)Default hash — a fast FIPS hash, not a slow KDF. Because the SDK mints the secret with ≥256 bits
of entropy, a slow KDF (PBKDF2) buys nothing and adds a per-verify CPU cost. The default is
SHA-512/256 over the salted 256-bit secret: fast, FIPS-approved, length-extension-safe. SHA-384
and pbkdf2-sha256:<iters>:<keylen> are available for CNSA-2.0 or low-entropy/defense-in-depth use;
select them via HashSpec.Algo. The chosen spec is stored per credential, so an algorithm change
never invalidates existing credentials.
minter := &secret.CredentialMinter{Prefix: "ib"} // defaults: SHA-512/256, 256-bit secret
token, stored, err := minter.Mint()
// token → client, once. stored → your credential columns.
// Later, to verify a presented token:
_, publicID, presented, _ := secret.Parse(token)
// load StoredCredential by publicID (an indexed, UNIQUE lookup), then:
ok, err := secret.Verify(presented, stored)Generated storage code (see Storage integration) wires all of this for a
credential field: it stores the split columns, mints on Create, and emits Verify<Field>.
Retrievable secrets
Package secret also provides encrypt, decrypt, and hash operations for retrievable secret
fields. It implements the Encryptor interface that generated storage code calls when a proto field
carries the (infoblox.field.v1.opts).secret annotation. The generated storage code calls the
Encryptor to hash and encrypt those fields and never stores their plaintext. Use this when you need
to supply or swap the encryption backend for a service that stores retrievable secret fields.
Encryptor
Encryptor is the interface that both built-in backends implement.
type Encryptor interface {
Encrypt(ctx context.Context, plaintext string) (ciphertext string, err error)
Decrypt(ctx context.Context, ciphertext string) (plaintext string, err error)
Hash(ctx context.Context, plaintext string) (hash string, err error)
}The three methods serve distinct storage roles:
Encrypt/Decrypt— reversible operations that write and read the<field>_ciphercolumn.Hash— a deterministic one-way operation that writes the<field>_hashcolumn. Because the same plaintext always produces the same hash,LookupBy<Field>Hashcan locate a record without decrypting it.
Both built-in implementations satisfy Encryptor, so you can swap backends without changing any
other code.
NewDev
func NewDev(key []byte) EncryptorReturns an Encryptor that uses AES-256-GCM for encrypt/decrypt and HMAC-SHA256 for hash. The key
is held in process memory and must be exactly 32 bytes (a 256-bit key).
NewDev in production. Use NewVaultTransit instead. NewDev panics if
len(key) != 32: both a short key and an over-length key are rejected. An over-length key is
not truncated to its first 32 bytes — silently discarding the tail would make two keys that
share a 32-byte prefix interchangeable and make a partial rotation a no-op.enc := secret.NewDev(devKey) // devKey must be exactly 32 bytesHow it works: Encrypt generates a fresh random GCM nonce on each call and prepends it to the
ciphertext, then base64-encodes the result. Decrypt reverses that process. Hash returns
base64(HMAC-SHA256(key, plaintext)).
NewVaultTransit
func NewVaultTransit(addr, token, keyName string) *VaultTransitEncryptorReturns an Encryptor backed by HashiCorp Vault’s Transit Secrets Engine. Use this backend in
production. The implementation uses plain HTTP with no Vault SDK dependency.
| Parameter | Description |
|---|---|
addr | Vault server address, e.g. http://localhost:8200 |
token | A Vault token with encrypt/decrypt policy on keyName |
keyName | The Transit key name — must already exist in Vault |
type VaultTransitEncryptor struct { /* unexported */ }
func (v *VaultTransitEncryptor) Encrypt(ctx context.Context, plaintext string) (string, error)
func (v *VaultTransitEncryptor) Decrypt(ctx context.Context, ciphertext string) (string, error)
func (v *VaultTransitEncryptor) Hash(ctx context.Context, plaintext string) (string, error)
func (v *VaultTransitEncryptor) Rewrap(ctx context.Context, ciphertext string) (string, error)EncryptcallsPOST /v1/transit/encrypt/<keyName>.DecryptcallsPOST /v1/transit/decrypt/<keyName>.Hashruns locally asHMAC-SHA256keyed onsha256(token), so hash lookups require no Vault round-trip and remain deterministic.RewrapcallsPOST /v1/transit/rewrap/<keyName>to re-encrypt existing ciphertext under the latest key version without exposing the plaintext. Use this during key rotation.
Each request sets X-Vault-Token and Content-Type: application/json. A non-200 response becomes
an error that carries Vault’s status code and response body.
See the Secret Fields guide for Vault engine setup and policy configuration.
Storage integration
Both storage generators (protoc-gen-storage and protoc-gen-ent) wire this package for you.
A credential field gets <field>_public_id (UNIQUE), <field>_salt, <field>_hash, and
<field>_hashspec columns — no plaintext column and no cipher. Create mints via the
CredentialMinter and returns the token once; a Verify<Field> helper parses a presented token,
looks the record up by public_id (tenant-independent), and calls secret.Verify:
token, stored, _ := minter.Mint() // → PublicID/Salt/Hash/Hashspec columns; token returned once
rec, ok, err := VerifySecretValue(ctx, client, token) // parse → lookup by public_id → constant-time verifyThe repository constructor accepts a *secret.CredentialMinter when the message has credential
fields; a nil minter fails loud with persistence.ErrNoMinter rather than minting nothing.
A secret field gets <field>_hash and <field>_cipher columns, and the generator calls the
Encryptor in Create and Update:
h, _ := enc.Hash(ctx, entity.KeyValue) // → KeyValueHash (indexed, for lookup)
c, _ := enc.Encrypt(ctx, entity.KeyValue) // → KeyValueCipher (recoverable)The repository constructor accepts the Encryptor when the message has secret fields; a nil
encryptor with a non-empty secret fails loud with persistence.ErrNoEncryptor. See
Secret fields.