slip39

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: MIT Imports: 13 Imported by: 0

README

go-slip39

Go Tests Go Reference Go Report Card

Go implementation of SLIP-0039: Shamir's Secret Sharing for Mnemonic Codes.

Split a secret into mnemonic shares. Any threshold number of shares can reconstruct the original secret. Supports two-level group schemes for organizational key recovery.

Security

  • Constant-time GF(2^8): Bitsliced arithmetic translated from the Trezor firmware. Zero data-dependent branches, zero table lookups. Immune to cache-timing and Spectre attacks.
  • Memory zeroing: All intermediate secrets are zeroed via //go:noinline functions. The caller is responsible for zeroing the returned secret from Combine. The Go garbage collector may copy data before zeroing occurs; for hardware-grade security, use a hardware wallet.
  • Constant-time digest comparison: crypto/subtle.ConstantTimeCompare for share verification.
  • No math/big: BitStream encoding replaces big integer arithmetic, eliminating limb-zeroing concerns.
  • Concurrency: Split and Combine are safe for concurrent use. No shared mutable state. The wordlist is immutable after init.
  • Minimal dependencies: Only golang.org/x/crypto (PBKDF2). Everything else is Go stdlib.

Install

go get github.com/shurlinet/go-slip39

Requires Go 1.25 or later (due to golang.org/x/crypto dependency).

Usage

package main

import (
    "fmt"
    "log"

    "github.com/shurlinet/go-slip39"
)

func main() {
    secret := []byte{0xBB, 0x54, 0xAA, 0xC4, 0xB8, 0x9D, 0xC8, 0x68,
        0xBA, 0x37, 0xD9, 0xCC, 0x21, 0xB2, 0xCE, 0xCE}

    // Split into 2-of-3 shares with a passphrase.
    groups, err := slip39.Split(secret, []byte("my passphrase"),
        slip39.WithGroups([]slip39.Group{
            {Threshold: 2, Count: 3},
        }),
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Share 1:", groups[0][0])
    fmt.Println("Share 2:", groups[0][1])
    fmt.Println("Share 3:", groups[0][2])

    // Recover from any 2 shares.
    recovered, err := slip39.Combine(groups[0][:2], []byte("my passphrase"))
    if err != nil {
        log.Fatal(err)
    }
    defer slip39.ZeroBytes(recovered) // Caller zeroes the secret.

    fmt.Printf("Recovered: %x\n", recovered)
}

API

func Split(secret []byte, passphrase []byte, opts ...Option) ([][]string, error)
func Combine(mnemonics []string, passphrase []byte) ([]byte, error)

func WithGroupThreshold(t int) Option
func WithGroups(groups []Group) Option
func WithIterationExponent(e int) Option
func WithExtendable(ext bool) Option
func WithRandom(r io.Reader) Option   // Testing only.

func ZeroBytes(b []byte)

type Group struct {
    Threshold int
    Count     int
}

Secret requirements: Even number of bytes, 16-64 (128-512 bits).

Passphrase: Printable ASCII only (codes 32-126). Empty/nil is valid. A wrong passphrase produces a different secret, not an error (plausible deniability by design).

Multi-Group Example

// Three groups, any 2 needed:
//   Family: 2-of-3
//   Lawyer: 1-of-1
//   Vault:  3-of-5
groups, err := slip39.Split(secret, nil,
    slip39.WithGroupThreshold(2),
    slip39.WithGroups([]slip39.Group{
        {Threshold: 2, Count: 3},
        {Threshold: 1, Count: 1},
        {Threshold: 3, Count: 5},
    }),
)

Spec Compliance (SLIP-0039)

Feature Status Notes
2-level Shamir (groups + members) Supported Up to 16 groups, 16 members each
Feistel cipher (PBKDF2-HMAC-SHA256) Supported 4 rounds, configurable iteration exponent 0-15
RS1024 checksum Supported Exhaustive single-error detection verified (20,460 cases)
Extendable backup flag Supported Both modes, default true per spec
Passphrase encryption Supported Printable ASCII, plausible deniability by design
Secret sizes 128-512 bits Supported 16-64 bytes, even length
1024-word English wordlist Supported SHA256-verified at init against SatoshiLabs canonical source
45 official test vectors All pass Negative vectors mapped to specific error sentinels
Cross-impl verification Verified 77 encode round-trips byte-match Python encoder

Differences from Other Implementations

Feature go-slip39 (shurlinet) Python ref go-slip39 (duodekalexeis) Trezor C
Constant-time GF(256) Bitsliced No No Bitsliced
Memory zeroing Yes No (immutable) No Yes
Fuzz testing 5 targets No No Yes
Anti-tamper tests Yes No No N/A
Dependencies x/crypto only Many gonum, golang-set None
Max secret size 64 bytes No limit 32 bytes 32 bytes

Consumer Responsibilities

Zero the recovered secret. Combine returns a []byte that the caller must zero after use: defer slip39.ZeroBytes(recovered).

Wrong passphrase = different secret, not an error. This is plausible deniability by design. There is no way to detect a wrong passphrase. The caller must verify the recovered secret independently (e.g., derive a public key and compare).

Share storage. Shares are mnemonic strings. How you store and distribute them is your responsibility. The library provides the math; you provide the operational security.

Testing

  • 45 SatoshiLabs spec vectors (mandatory gate)
  • 6 Python cross-implementation vectors (deterministic seed, python-shamir-mnemonic 0.3.1)
  • 77 encode round-trips byte-matching the Python encoder
  • 65,536 exhaustive GF(256) multiplication verifications (bitsliced vs table oracle)
  • 20,460 exhaustive RS1024 single-error detection cases
  • 9 AI threat defense tests (Feistel round count, iteration base, digest length, special indices, wordlist hash, ZeroBytes effectiveness, secret-in-error scan, GF(256) exhaustive, reduction polynomial)
  • Property tests (share independence, determinism, threshold enforcement, concurrent safety)
  • 5 fuzz targets (Combine, RoundTrip, Interpolate, BitStream, Feistel)
  • Anti-tamper tests (HMAC argument order, customization string bytes, threshold encoding, independent HMAC oracle)
go test -race -count=1 ./...

Examples

See the examples/ directory:

  • basic - 1-of-1 split and combine, no passphrase
  • sharing - 2-of-3 with passphrase
  • multigroup - 2-of-3 groups (family/lawyer/vault scenario)

Dependencies

Dependency Purpose
golang.org/x/crypto PBKDF2-HMAC-SHA256 (Feistel cipher round function)

No other external dependencies. Wordlist is embedded via go:embed.

AI Transparency

This library was developed with assistance from Claude (Anthropic). All code was reviewed, tested against reference implementations, and verified against the SLIP-0039 specification. The test suite includes 9 AI threat defense tests designed to catch both human and AI-introduced errors. The AI generated code; the human made every design decision, reviewed every line, and owns every bug.

Acknowledgments

License

MIT. See LICENSE.

GF(2^8) arithmetic translated from Trezor firmware (Apache 2.0). See THIRD_PARTY_LICENSES.

Documentation

Overview

Package slip39 implements SLIP-0039: Shamir's Secret Sharing for Mnemonic Codes.

Split splits a secret into mnemonic shares using a two-level (group + member) Shamir scheme. Combine reconstructs the original secret from a sufficient set of shares.

The secret is encrypted with a passphrase via a 4-round Feistel cipher using PBKDF2-HMAC-SHA256 before splitting. An empty passphrase is valid and provides plausible deniability: any passphrase produces a valid-looking secret, but only the correct passphrase produces the original one.

All GF(2^8) arithmetic is bitsliced and constant-time. Intermediate secrets are zeroed after use. The caller is responsible for zeroing the returned secret from Combine and the input secret passed to Split.

Split is non-deterministic: each call produces different shares due to random identifier and random polynomial coefficients. Use WithRandom for deterministic testing only.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidMnemonic   = errors.New("slip39: invalid mnemonic")
	ErrInvalidChecksum   = errors.New("slip39: invalid checksum")
	ErrInvalidSecret     = errors.New("slip39: invalid secret")
	ErrInvalidPassphrase = errors.New("slip39: invalid passphrase")
	ErrInvalidShares     = errors.New("slip39: invalid shares")
	ErrDigestMismatch    = errors.New("slip39: digest verification failed")
)

Error sentinels for SLIP-0039 operations. All error returns use %w wrapping so callers can use errors.Is.

Functions

func Combine

func Combine(mnemonics []string, passphrase []byte) ([]byte, error)

Combine reconstructs a secret from mnemonic shares.

The passphrase must match the one used during Split. A wrong passphrase produces a different secret without any error (plausible deniability by design).

The caller is responsible for zeroing the returned secret when done.

Example
secret := bytes.Repeat([]byte{0x42}, 16)

// Split into 2-of-3.
groups, err := slip39.Split(secret, nil,
	slip39.WithGroups([]slip39.Group{
		{Threshold: 2, Count: 3},
	}),
	slip39.WithIterationExponent(0),
)
if err != nil {
	fmt.Println("error:", err)
	return
}

// Recover from any 2 of 3 shares.
recovered, err := slip39.Combine(groups[0][:2], nil)
if err != nil {
	fmt.Println("error:", err)
	return
}
defer slip39.ZeroBytes(recovered)

fmt.Println("Recovery successful:", bytes.Equal(recovered, secret))
Output:
Recovery successful: true

func Split

func Split(secret []byte, passphrase []byte, opts ...Option) ([][]string, error)

Split splits a secret into mnemonic shares.

The secret must be an even number of bytes, at least 16 and at most 64. The passphrase must contain only printable ASCII characters (codes 32-126) and is used to encrypt the secret before splitting. A nil or empty passphrase is valid.

Returns a slice of groups, where each group is a slice of mnemonic strings.

Example
secret := bytes.Repeat([]byte{0x42}, 16)

groups, err := slip39.Split(secret, nil,
	slip39.WithGroups([]slip39.Group{
		{Threshold: 2, Count: 3},
	}),
	slip39.WithIterationExponent(0),
)
if err != nil {
	fmt.Println("error:", err)
	return
}

fmt.Printf("Generated %d shares\n", len(groups[0]))
Output:
Generated 3 shares

func ZeroBytes

func ZeroBytes(b []byte)

ZeroBytes overwrites a byte slice with zeros. Callers must use this to erase sensitive data (secrets, shares, intermediates) before the slice becomes unreachable.

Types

type Group

type Group struct {
	Threshold int // number of shares required to reconstruct the group secret
	Count     int // total number of shares in this group
}

Group defines a threshold scheme for one share group.

type Option

type Option func(*splitConfig)

Option configures Split behavior.

func WithExtendable

func WithExtendable(ext bool) Option

WithExtendable sets the extendable backup flag. When true, the backup can be extended with additional groups without invalidating existing shares. Default: true (spec default).

func WithGroupThreshold

func WithGroupThreshold(t int) Option

WithGroupThreshold sets the number of groups required to reconstruct the secret. Default: 1.

func WithGroups

func WithGroups(groups []Group) Option

WithGroups sets the group configuration. Each Group specifies a member threshold and member count. Default: single group with Threshold=1, Count=1.

func WithIterationExponent

func WithIterationExponent(e int) Option

WithIterationExponent sets the PBKDF2 iteration exponent for the Feistel cipher. Total iterations per round = 2500 << e. Higher values increase encryption time exponentially: exponent 0 = 10,000 total, 1 = 20,000, 2 = 40,000. Range: 0-15. Default: 1.

func WithRandom

func WithRandom(r io.Reader) Option

WithRandom overrides the random source for deterministic testing. In production, the default crypto/rand.Reader is used. This option is for testing only; do not use in production.

Directories

Path Synopsis
examples
basic command
Example: basic 1-of-1 split and combine with no passphrase.
Example: basic 1-of-1 split and combine with no passphrase.
multigroup command
Example: multi-group sharing (2-of-3 groups: family, lawyer, vault).
Example: multi-group sharing (2-of-3 groups: family, lawyer, vault).
sharing command
Example: 2-of-3 sharing with a passphrase.
Example: 2-of-3 sharing with a passphrase.

Jump to

Keyboard shortcuts

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