shardseal

module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT

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:

  1. A fresh random 256-bit key is generated.
  2. Your secret (any length) is encrypted with that key using ChaCha20-Poly1305.
  3. Only the key is split with Shamir's Secret Sharing into parts shares.

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 key K is read from crypto/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 as nonce(12) ‖ encrypted ‖ tag(16), so the nonce travels with the data.

  • Polynomial construction (math.go): Shamir's scheme builds a random polynomial of degree threshold − 1

    f(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) and a₁ … a_{t-1} are random. All arithmetic is done modulo a fixed 270-bit prime p using math/big.

  • Share generation (math.go): for each of the parts shares, a random x is 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 term a₀ = f(0) = K is reconstructed directly with the Lagrange formula evaluated at z = 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 same x (so x_j − x_i ≡ 0), the inverse fails and Combine returns a duplicate or colliding share x error.

  • 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 threshold shares and the ciphertext. The ciphertext is not secret (it is AEAD-encrypted), but losing it means losing the data. Store it redundantly.
  • Choose threshold deliberately. threshold must be ≥ 1 and ≤ parts. A higher threshold is more secure but less available; pick the smallest k that meets your trust model (e.g. 3-of-5 for a small team). Avoid threshold = 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 threshold of them together reconstruct the key — handle them with the same care as credentials.
  • Don't reuse or hand-craft shares. Shares with a duplicate x coordinate cannot be combined (Combine rejects them). Always use shares produced by a single Split call for a given secret.
  • Verify failures fail closed. A wrong key, too few shares, or a modified ciphertext all cause Combine to 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 x coordinates. Shares use random x values rather than 1, 2, 3, …. Collisions are astronomically unlikely with 160-bit x, and Combine rejects them if they occur.
  • No authenticated binding between a share and its ciphertext. Mixing shares from different Split calls (or with the wrong ciphertext) will fail decryption via the AEAD tag, but is not detected earlier.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL