Your application’s encrypted records and signed values must survive restarts.
Provision a key once, protect it, and load the intended material wherever that
purpose runs. The key package turns stored bytes into immutable, validated key
values; it does not choose your configuration source or manage rotation.
Choose Material
Choose the type required by the consumer, not the file extension. A shared secret protects data using the same secret on both sides. A signing identity instead separates the private key that signs from the public key that verifies.
| Type | Material | Use |
|---|---|---|
key.Shared |
Exactly 32 secret bytes | Symmetric encryption or shared-secret signing, with no public counterpart. |
key.Signing |
ECDSA P-256 private identity | Signatures verified with the matching public key. |
key.Exchange |
X25519 private identity | Key agreement inside an existing authenticated protocol. |
Use independent material for each purpose. For example, give encrypted records,
signed links, and
pagination cursors separate Shared values.
AES-256-GCM and ChaCha20-Poly1305 consume Shared for encryption; link signers and
cursor codecs use it for HMAC-based checks, not public-key signatures.
Signing is not an encryption key, and Exchange is neither a signer nor an
encryption protocol. These types do not accept arbitrary algorithms: an RSA or
Ed25519 key is not a Signing identity, and a P-256 ECDH key is not an Exchange
identity. For Web Push, follow VAPID Keys. For Apple Sign
In, load the Apple-issued P-256 key; local generation cannot establish provider
provenance. Provider API tokens, webhook secrets, password-hash salts, session
identifiers, and TLS certificates are
different inputs, not additional key types.
Generate Keys
Use Key Commands to provision protected files, validate them, and extract public components. That guide owns command syntax, output permissions, and recovery from incomplete writes.
Add the package from your application’s module directory:
go get arkoren.dev/foundation/key
Import arkoren.dev/foundation/key. If you write your own provisioning tool,
use the package’s generators. This disposable in-memory example creates a shared
key without displaying or persisting its secret:
material, err := key.GenerateShared()
Generation uses crypto/rand and does not store the result. GenerateSigning
creates an independent P-256 identity; GenerateExchange creates an independent
X25519 identity. Provision production material outside application startup and
store it in protected deployment storage with a protected backup. Never generate
a replacement to hide a missing startup key: existing data or identities still
depend on the original.
Load Configuration
Your application selects configuration sources and resolves
secret-store values. The package does not read an
implicit APP_KEY, fetch secrets, or search parent directories. For example,
an application’s configuration layer can supply these named inputs:
type KeyConfig struct {
EncryptionBase64 string
SigningFile string
}
With an existing config KeyConfig, parse the encryption secret already read
from protected storage:
material, err := key.ParseShared(config.EncryptionBase64)
Handle err before constructing an encryption consumer. On success, material
is a key.Shared; continue with Encryption Keys for
driver wiring and encrypted-data use. ParseShared requires padded standard
base64 decoding to exactly 32 bytes. It accepts surrounding whitespace but
rejects embedded line breaks, noncanonical padding bits, and malformed encoding.
Base64 is an encoding, not encryption. NewShared(raw) imports exactly 32 raw
bytes instead; neither operation proves entropy. Do not use a password or a
predictable 32-byte value as a key.
For a mounted signing file, restrict read access to the application and trusted operators before loading it. Keep the file and its parent directories under trusted control: loaders follow the supplied path, including symlinks, and do not check deployment permissions or sandbox untrusted paths.
identity, err := key.LoadSigning(config.SigningFile)
On success, identity is a key.Signing. Loading reads without modifying the
file. LoadShared(path) reads base64 text; LoadExchange(path) reads X25519
private PEM. If configuration already contains PEM bytes, use
ParseSigning(contents) or ParseExchange(contents) instead.
Both private PEM parsers require exactly one unencrypted PKCS #8 PRIVATE KEY
block with the matching algorithm. They accept surrounding whitespace, but
reject extra blocks, PEM headers, non-whitespace junk, trailing DER bytes, other
curves, and alternate forms such as SEC 1 EC PRIVATE KEY. Encoded inputs and
file reads are limited to 64 KiB, including surrounding whitespace.
For existing native keys, NewSigning(private) accepts an *ecdsa.PrivateKey
from crypto/ecdsa, validates P-256 and public/private consistency, and copies
the material. NewExchange(private) accepts an X25519 *ecdh.PrivateKey from
crypto/ecdh. Neither constructor retains the caller’s mutable key object.
Malformed, unsupported, and oversized material returns key.ErrInvalid; check
it with errors.Is from the standard-library errors package. File operations
preserve OS error causes, so a missing file can match os.ErrNotExist instead.
Validation errors do not include key contents or input-derived parser diagnostics.
Do not log the source configuration when reporting a failure.
Export Material
Before exporting a key passed in by another component, check its Err() result.
All three types have invalid zero values; Err() returns key.ErrInvalid for
them. An empty export is not a replacement for validation: zero shared values
export empty bytes or text, and zero asymmetric identities export nil native
keys or PEM bytes. A zero signing identity’s VAPIDPublicKey() is empty text.
After successfully loading the signing identity, obtain only its public
component for a verifier:
public := identity.PublicPEM()
public is SPKI PUBLIC KEY PEM without private material. PublicKey() returns
a native public key instead. Both asymmetric types support these exports.
Signing.VAPIDPublicKey() is a different representation: the uncompressed P-256
public point in unpadded base64url, not PEM or DER. Use it only where the
Web Push setup requires it.
Shared.Bytes()andShared.Base64()expose the shared secret.PEM()on either asymmetric type exports unencrypted PKCS #8 private material, andPrivateKey()exposes a native private key. Never send these exports to logs, browser responses, or public configuration. Persist them only through your protected provisioning path; an encoding does not protect a secret.
Formatting key values with fmt and logging them through slog produces fixed
redaction markers. That is not a persistence format or a general serialization
guarantee. Do not serialize an identity to publish a public key; explicitly
export its public component. Strings or native objects obtained through private
exports do not inherit the key value’s redaction behavior.
Inputs and mutable exports are detached from the immutable value. Modifying an exported slice or native key does not change the original or later exports. This ownership rule does not promise to erase secret copies from process memory.
Using Exchange Keys
Use an Exchange identity only when integrating a protocol that already defines
peer authentication and key derivation. PrivateKey() and PublicKey() supply
native X25519 keys; ParseExchangePublic(contents) imports a peer’s X25519
SPKI PUBLIC KEY PEM under the same strict envelope and size rules as private
PEM. Parsing checks representation, not who owns the key. It also does not reject
every low-order point; native ECDH rejects those inputs when agreement is attempted.
An attacker who substitutes a peer public key can establish a different shared secret with you. Authenticate peer keys through your protocol and apply its key-derivation function with distinct purpose contexts. Never use raw ECDH output directly as an encryption key.
The package supplies no handshake, transport, or key-derivation API. Keep these responsibilities in the existing protocol rather than designing a new encryption protocol from the key containers.
Rotate Keys
The application owns persistence, deployment, and rotation. Loading a new file does not update an already constructed consumer. Keep the intended material consistent across replicas serving the same purpose, and coordinate replacement with the consumer’s compatibility rules.
Retain previous encryption keys while stored values or backups need them; follow encryption rotation for the rollout and re-encryption sequence. Replacing a link or cursor secret can invalidate issued values; plan around their lifetime and the consumer’s supported key handling. VAPID replacement requires subscription migration, not merely a different private key on the server.
For constructors, formats, and exports, see the
key API reference.