Skip to content
CredenShare
SDKs

Go SDK

The official Go client — encrypts locally, assembles the link, verifies webhooks. Standard library only.

github.com/CredenShare/credenshare-sdk-go

Go 1.21 and above. Standard library only — HKDF is forty lines of crypto/hmac rather than a module, because a security client earning a dependency for that is a poor trade.

Installing

go get github.com/CredenShare/credenshare-sdk-go

A Go module resolves straight from its repository through the proxy, so there was never a registry to wait for. Name a version if you want one — @v0.1.3 — otherwise go get takes the latest tag.

The proxy's @v/list endpoint lags behind a fresh tag by a while, so a version can be missing from that listing and still resolve perfectly — go get and @latest fetch on demand. If a brand-new version appears absent, ask for it by name rather than assuming it failed to publish.

Quickstart

main.go
package main

import (
    "context"
    "fmt"
    "os"

    credenshare "github.com/CredenShare/credenshare-sdk-go"
)

func main() {
    client, err := credenshare.New(os.Getenv("CREDENSHARE_KEY"), nil)
    if err != nil {
        panic(err)
    }

    share, err := client.CreateShare(context.Background(), credenshare.CreateParams{
        Title: "Staging deploy credentials",
        Fields: []credenshare.Field{
            {Key: "Username", Value: "deploy-bot", Type: "text"},
            {Key: "Password", Value: "correct horse", Type: "password"},
        },
    })
    if err != nil {
        panic(err)
    }

    fmt.Println(share.Link)
    // https://crs.sh/aB3dEf12#1xK9...
}

That link is the secret. The key rides in the fragment, which browsers never transmit. Anyone holding the link can read the content; we cannot, and cannot recover it for you.

Note that fmt.Println(share) prints <Share aB3dEf12 (link withheld)> — the String method deliberately withholds the link, so %v in a log will not leak it. Print share.Link when you actually want it.

The field object

Field.Key is the visible label, not an identifier — it is what the recipient reads. Go's types stop the label: spelling that catches the dynamic clients, but a caller unmarshalling from JSON with the wrong member name lands in the same place: Key empty, every field rendered blank, nothing erroring. ValidateFields refuses that before anything is sent.

Type is one of text, password, date, multiline, markdown, source_code, exported as FieldTypes. Validation checks only that Type is present, not that it is a member of that set.

Optional field members survive.Field carries an Extra map[string]json.RawMessage overflow map, so members this client does not name — selectedProgrammingLanguage, filename, anything a newer sender adds — are kept on decrypt and written back on re-encrypt. The cost is that Field is no longer comparable — f1 == f2 and map[Field]T do not compile against this version. Use Field.Equal(other), which compares the three known members plus Extra as raw bytes, and key maps on Field.Key.

Creating

share, err := client.CreateShare(ctx, credenshare.CreateParams{
    Title:    "Production database",
    Fields:   []credenshare.Field{{Key: "Password", Value: "s3cr3t", Type: "password"}},
    Passcode: "hunter2",
})

A passcode is mixed into the content key derivation, not just checked by the server — so it is not a server-side gate you could bypass, and a passcode-protected share cannot be opened from the link alone. The server receives only a one-way verifier. Send the link and the passcode over different channels.

Set Custody: true to also wrap the content key to the custody public key derived from your credential's third part, which keeps the share readable from your dashboard rather than only from its link. Credential.WrapToCustody computes it locally; the custody secret never leaves your machine.

Requires the shares:write scope.

Listing and expiring

page, err := client.ListShares(ctx, 50, 1)
fmt.Println(page.Total, page.HasMore())

err = client.IterateShares(ctx, 100, func(s credenshare.ShareSummary) error {
    fmt.Println(s.ShortCode, s.ExpiredAt)
    return nil
})

err = client.ExpireShare(ctx, "aB3dEf12")

ListShares and GetShare need shares:read; CreateShare and ExpireShare need shares:write. There is no hierarchy between them, so a key minted with only shares:write fails on ListShares with a permission error — the most common first surprise.

Both return metadata only, never content and never a key. A short code belonging to another account reports exactly as one that does not exist.

ExpireShare removes the share rather than flagging it, so a later GetShare returns ErrNotFound. A share you expired and one that never existed are indistinguishable afterwards.

Verifying webhooks

import "github.com/CredenShare/credenshare-sdk-go/webhooks"

func handler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)   // the RAW bytes, before any decoding
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    if err := webhooks.Verify(body, r.Header.Get(webhooks.SignatureHeader),
        []string{os.Getenv("WEBHOOK_SECRET")}, nil); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    // ...
}

Verify the raw body. Read it with io.ReadAll and verify those bytes. Re-serialising decoded JSON changes them — key order, spacing, escapes — and the signature will not match.

Pass both secrets while rotating. For 24 hours after a rotation, deliveries carry both signatures:

webhooks.Verify(body, header, []string{newSecret, oldSecret}, nil)

Verify returns only an error. A (bool, error) signature invites ok, _ := Verify(...), and a receiver that ignores the error accepts everything while looking like it checks.

The header name is webhooks.SignatureHeader and the ±5-minute window is webhooks.DefaultTolerance. Options.Tolerance is a *time.Duration, so nil means the default and zero genuinely means zero: use webhooks.ToleranceOf(30 * time.Second) for a custom window, or webhooks.NoTolerance() to demand an exact timestamp.

Configuration

client, err := credenshare.New(credential, &credenshare.Options{
    BaseURL:    "https://api.credenshare.io/v1",
    LinkOrigin: "https://crs.sh",
    HTTPClient: myClient,
    MaxRetries: credenshare.Retries(2),
    Timeout:    30 * time.Second,
})

LinkOrigin changes only the links CreateShare hands back; it is never sent to the API. The SDK reads no environment variables — os.Getenv above is your own read.

Options.Timeout sets the per-attempt timeout, defaulting to credenshare.DefaultTimeout (30 seconds). It also applies to an HTTPClient you supply that has no timeout of its own; one that already sets a timeout keeps it.

Rough edges

Real behaviour worth knowing before it surprises you.

  • Field is no longer comparable. The overflow map that preserves optional members means f1 == f2 and map[Field]T stop compiling. Field.Equal(other) replaces ==; key maps on Field.Key.
  • errors.Is(err, ErrAPI) matches every refusal, and the specific sentinels — ErrNotFound, ErrRateLimited, ErrIdempotencyConflict and the rest — still match through Unwrap. Use errors.As for *credenshare.APIError when you want to read .Status and .Code.
  • The idempotency key is generated per call and never surfaced. It protects a same-process network retry, which the client performs itself. It does not protect a re-run — a job that crashes after POSTing generates a fresh key next time and mints a second copy of the secret. Set IdempotencyKey yourself if you need that, and note that doing so does not make a second CreateShare a no-op: salt and IV are fresh per call, so the body differs and the API answers 409 with ErrIdempotencyConflict.
  • IterateShares can fail for a paging reason. Ask for page 3, get a response claiming page 1, and it stops with an error wrapping ErrAPI rather than looping. It otherwise walks until a page comes back short, or until the reported total is reached — unlike Node, Python and Rust it carries no absolute page ceiling, which only matters against a server that returns full pages indefinitely.
  • Only network failures are retried, never an HTTP status. A 500 is surfaced because it may have committed.
  • ErrServiceUnavailable is now only a genuine 503 — the API answering that nothing was created, safe to retry. Exhausted transport retries wrap ErrDeliveryUnknown instead, because Do returns once headers arrive, so a failure after that can still mean the request was processed.
  • ErrAuthentication is narrower than it sounds. A revoked or unknown credential arrives as HTTP 403 and therefore as the permission error.
  • ReadLink always fails — by design, since the recipient path is guarded by proof-of-work and captcha checks bearer auth would skip. It wraps credenshare.ErrNotSupported, so errors.Is matches it.
  • A webhook secret with a trailing newline fails. The blank check trims, but the HMAC is keyed with the untrimmed string. Go, Python and Rust behave this way — trim it yourself. Node now refuses such a secret by name.

Checking your build

From your own module, with nothing cloned:

go run github.com/CredenShare/credenshare-sdk-go/cmd/credenshare-conformance@latest
# 24 passed. This installation conforms to the wire specification.

Inside a clone of the repository, go run ./cmd/credenshare-conformance does the same thing. Add -v for one line per vector. The fixture is embedded with //go:embed, so it travels with the compiled binary; the command exits non-zero on failure and works as a deployment gate. See Conformance vectors.