Using Revaulter as a Go library
Everything revaulter-cli does is also available as a Go package, so applications can encrypt, decrypt, sign, and verify without running the CLI.
go get github.com/italypaleale/revaulter/pkg/revaulterThe package documentation is on pkg.go.dev.
Only
pkg/revaulteris part of the public API and offers a stable API contract.
Creating a client#
A client needs the address of a Revaulter server and a per-user request key, which is shown in the web UI after registration:
client, err := revaulter.New(revaulter.Options{
Server: "https://revaulter.example.com",
RequestKey: os.Getenv("REVAULTER_REQUEST_KEY"),
})
if err != nil {
return err
}If the user authenticates requests with OIDC tokens, pass a JWT from one of their trusted issuers as OIDCToken instead of RequestKey, and set UserID to the user ID shown in the web UI:
client, err := revaulter.New(revaulter.Options{
Server: "https://revaulter.example.com",
OIDCToken: oidcToken,
UserID: os.Getenv("REVAULTER_USER_ID"),
})The client only uses the token to submit requests and fetch public keys: it retrieves each result with a per-request result token issued by the server, so an approval can take longer than the token’s lifetime.
OIDCToken holds a single token. For a long-lived Client, set OIDCTokenProvider instead, which the client calls every time it needs the token. The function should cache the token until it’s about to expire:
client, err := revaulter.New(revaulter.Options{
Server: "https://revaulter.example.com",
OIDCTokenProvider: func(ctx context.Context) string {
// Return a cached token, requesting a new one when it's about to expire
return tokenSource.Token(ctx)
},
UserID: os.Getenv("REVAULTER_USER_ID"),
})Clients are safe for concurrent use. Every operation blocks until the user approves the request in the browser, the operation times out, or the context is canceled:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()Anchor pinning#
Each user has a hybrid (ES384 + ML-DSA-87) anchor key pair that signs the public keys the server advertises. Clients pin the anchor on first contact (the same Trust On First Use model as SSH host keys) and refuse to proceed if it ever changes. Pins are saved in the trust store shared with revaulter-cli, so a server pinned with revaulter-cli trust is already trusted by the library.
Because pinning an anchor is a security decision, the library never pins one on its own. Applications that can ask the user for input should implement the ConfirmAnchor callback:
client, err := revaulter.New(revaulter.Options{
Server: "https://revaulter.example.com",
RequestKey: requestKey,
ConfirmAnchor: func(anchor revaulter.AnchorInfo) (bool, error) {
fmt.Printf("First contact with %s\nFingerprint:\n%s\n", anchor.Server, anchor.FormatFingerprint(2))
// Ask the user, and return true to pin the anchor
return promptUser()
},
})Non-interactive applications should either pin the anchor ahead of time with revaulter-cli trust, or opt into revaulter.AcceptAnchorOnFirstUse, which pins whatever the server presents on the first connection.
Encrypting and decrypting#
enc, err := client.Encrypt(ctx, revaulter.EncryptRequest{
KeyLabel: "my-secret",
Algorithm: revaulter.AlgorithmA256GCM,
Plaintext: []byte("hello world"),
Note: "backup credentials",
})
if err != nil {
return err
}
// enc.Ciphertext, enc.Nonce, and enc.Tag are what you store
dec, err := client.Decrypt(ctx, revaulter.DecryptRequest{
KeyLabel: enc.KeyLabel,
Algorithm: enc.Algorithm,
Ciphertext: enc.Ciphertext,
Nonce: enc.Nonce,
Tag: enc.Tag,
})
if err != nil {
return err
}
fmt.Println(string(dec.Plaintext))Payloads are limited to revaulter.MaxPayloadSize (100 KB), because they’re sent to the browser to be processed. Encrypt larger data locally with a key that Revaulter wraps, which is what revaulter-edit does for files.
Signing and verifying#
Sign hashes the message as the algorithm requires before sending it, so for ES256 and Ed25519ph only the digest leaves the machine. (Ed25519 sends the message itself, so it’s limited to revaulter.MaxPayloadSize)
res, err := client.Sign(ctx, revaulter.SignRequest{
KeyLabel: "release",
Algorithm: revaulter.AlgorithmES256,
Message: manifest,
})res.Signature holds the raw 64-byte signature, in the IEEE P1363 r||s form for ES256.
Verifying does not require the user’s approval: it fetches the public signing key published on the server and checks the signature locally.
err = client.Verify(ctx, revaulter.VerifyRequest{
KeyLabel: "release",
Algorithm: revaulter.AlgorithmES256,
Message: manifest,
Signature: res.Signature,
})
if errors.Is(err, revaulter.ErrInvalidSignature) {
// The signature does not match the message
}Signing keys must be published from the Revaulter web interface. Publishing attaches a proof signed by the anchor, which the library checks against the pinned anchor before returning the key. To verify many signatures, fetch the key once and reuse it:
key, err := client.SigningPublicKey(ctx, "release", revaulter.AlgorithmES256)
if err != nil {
return err
}
err = key.Verify(revaulter.VerifyRequest{Message: manifest, Signature: signature})key.PublicKey is a standard *ecdsa.PublicKey or ed25519.PublicKey, so you can also pass it to other libraries.