README
¶
shardseal
A small Go library that combines Shamir's Secret Sharing (SSS) with authenticated encryption (ChaCha20-Poly1305) to split a secret of any size into n shares, of which any k (the threshold) are enough to reconstruct it — and fewer than k reveal nothing useful.
import "github.com/lbodlev888/shardseal"
What it does
Plain Shamir's Secret Sharing can only share a number smaller than the field prime — roughly a single key's worth of data, not an arbitrary file or message. shardseal works around this with a hybrid / envelope scheme:
- A fresh random 256-bit key is generated.
- Your secret (any length) is encrypted with that key using ChaCha20-Poly1305.
- Only the key is split with Shamir's Secret Sharing into
partsshares.
So you get back two things:
- a ciphertext (the encrypted secret — not itself secret, but keep it available), and
- a set of shares (each one a point on a secret polynomial).
To recover the secret you need the ciphertext plus any threshold of the shares. The shares reconstruct the key; the key decrypts the ciphertext.
This means the size of the secret is bounded only by available memory, while Shamir only ever has to protect a fixed 256-bit value.
How it works behind the scenes
Splitting (Split)
secret ──► [ChaCha20-Poly1305 encrypt with random key K] ──► ciphertext (nonce ‖ encrypted ‖ tag)
K ──► [Shamir split] ──► share₁, share₂, … shareₙ
-
Key generation (
crypto.go): a 256-bit keyKis read fromcrypto/rand. -
Encryption (
crypto.go): a random 12-byte nonce is generated and the secret is sealed with ChaCha20-Poly1305. The output ciphertext is laid out asnonce(12) ‖ encrypted ‖ tag(16), so the nonce travels with the data. -
Polynomial construction (
math.go): Shamir's scheme builds a random polynomial of degreethreshold − 1f(z) = a₀ + a₁·z + a₂·z² + … + a_{t-1}·z^{t-1} (mod p)where the constant term
a₀ = K(the secret to be shared) anda₁ … a_{t-1}are random. All arithmetic is done modulo a fixed 270-bit primepusingmath/big. -
Share generation (
math.go): for each of thepartsshares, a randomxis chosen and the share is the point(x, f(x) mod p).f(x)is evaluated with Horner's method.
Because the polynomial has degree t − 1, it takes at least t distinct points to determine it uniquely — that is the threshold.
Combining (Combine)
threshold shares ──► [Lagrange interpolation at z=0] ──► K
ciphertext + K ──► [ChaCha20-Poly1305 decrypt] ──► secret
-
Lagrange interpolation (
shamir.go): the constant terma₀ = f(0) = Kis reconstructed directly with the Lagrange formula evaluated atz = 0:K = Σ_i y_i · Π_{j≠i} x_j / (x_j − x_i) (mod p)Modular division uses the modular inverse (
big.Int.ModInverse). If two shares share the samex(sox_j − x_i ≡ 0), the inverse fails andCombinereturns aduplicate or colliding share xerror. -
Key recovery: the interpolated value is packed into a 32-byte key buffer. If it does not fit in 32 bytes, the shares are rejected as insufficient/invalid.
-
Decryption: the first 12 bytes of the ciphertext are split off as the nonce, and ChaCha20-Poly1305 verifies the tag and decrypts. A wrong key (from wrong/insufficient shares) or tampered ciphertext fails authentication and returns an error rather than garbage.
The field
The library works in GF(p) with a fixed prime
p = 1457818733796714268733772119829610931731984739405691564346822445166680593811908207
This p is 270 bits, comfortably larger than the 256-bit key it must hold, which is required for correctness (the secret must be representable as a field element).
The cryptography: ChaCha20-Poly1305
shardseal uses golang.org/x/crypto/chacha20poly1305, an AEAD (Authenticated Encryption with Associated Data) construction standardised in RFC 8439.
- ChaCha20 is a stream cipher that produces a keystream XOR-ed with the plaintext. It uses a 256-bit key and a 96-bit (12-byte) nonce. It is fast in software and constant-time, avoiding the cache-timing pitfalls of table-based AES on hardware without AES-NI.
- Poly1305 is a one-time message authentication code. It computes a 128-bit (16-byte) authentication tag over the ciphertext.
Together they give confidentiality + integrity: any modification of the ciphertext (or use of the wrong key) is detected, and decryption fails closed. In this library the AEAD step is what protects the actual secret payload, while Shamir protects the key.
Sizes used (from chacha20poly1305):
| Constant | Value |
|---|---|
| Key size | 32 bytes |
| Nonce size | 12 bytes |
| Tag/overhead | 16 bytes |
Installation
go get github.com/lbodlev888/shardseal
Requires Go 1.26+ and golang.org/x/crypto.
Usage
Splitting a secret
package main
import (
"fmt"
"github.com/lbodlev888/shardseal"
)
func main() {
secret := []byte("the launch codes are 0000")
// Split into 5 shares, any 3 of which can reconstruct the secret.
ciphertext, shares, err := shardseal.Split(secret, 5, 3)
if err != nil {
panic(err)
}
// Persist/distribute these:
fmt.Printf("ciphertext (base64-encode this for storage): %x\n", ciphertext)
for i, s := range shares {
fmt.Printf("share %d: %s\n", i+1, s.String()) // "base64(x);base64(y)"
}
}
Each share serialises (via Share.String()) to a compact base64(x);base64(y) string that is safe to print, store, or hand to a shareholder.
Reconstructing a secret
// Re-parse the share strings you collected back into Share values.
var collected []shardseal.Share
for _, str := range []string{shareStr1, shareStr3, shareStr5} { // any 3 of the 5
s, err := shardseal.ParseShare(str)
if err != nil {
panic(err)
}
collected = append(collected, s)
}
secret, err := shardseal.Combine(ciphertext, collected)
if err != nil {
panic(err) // wrong/insufficient shares or tampered ciphertext
}
fmt.Printf("recovered: %s\n", secret)
API surface
| Function | Description |
|---|---|
Split(secret []byte, parts, threshold int) ([]byte, []Share, error) |
Encrypt the secret and split the key into parts shares with the given threshold. Returns the ciphertext and the shares. |
Combine(ciphertext []byte, shares []Share) ([]byte, error) |
Reconstruct the key from >= threshold shares and decrypt the ciphertext. |
Share.String() string |
Serialise a share to base64(x);base64(y). |
ParseShare(s string) (Share, error) |
Parse a serialised share back into a Share. |
Best practices
- Keep the ciphertext available. The shares alone are useless — reconstruction needs both
thresholdshares and the ciphertext. The ciphertext is not secret (it is AEAD-encrypted), but losing it means losing the data. Store it redundantly. - Choose
thresholddeliberately.thresholdmust be≥ 1and≤ parts. A higher threshold is more secure but less available; pick the smallestkthat meets your trust model (e.g. 3-of-5 for a small team). Avoidthreshold = 1, which makes the polynomial a constant so every share reveals the key. - Distribute shares independently. The whole point is that no single location holds enough shares to recover the secret. Storing several shares together defeats the scheme.
- Treat shares as sensitive until used. Each share leaks no information on its own, but
thresholdof them together reconstruct the key — handle them with the same care as credentials. - Don't reuse or hand-craft shares. Shares with a duplicate
xcoordinate cannot be combined (Combinerejects them). Always use shares produced by a singleSplitcall for a given secret. - Verify failures fail closed. A wrong key, too few shares, or a modified ciphertext all cause
Combineto return an error rather than wrong plaintext — always check the error, never ignore it.
Implementation notes & caveats
If you intend to use this in production, be aware of the following properties of the current code:
- Random
xcoordinates. Shares use randomxvalues rather than1, 2, 3, …. Collisions are astronomically unlikely with 160-bitx, andCombinerejects them if they occur. - No authenticated binding between a share and its ciphertext. Mixing shares from different
Splitcalls (or with the wrong ciphertext) will fail decryption via the AEAD tag, but is not detected earlier.