feat(P08): master key seal-to-OIDC + Shamir 3-of-5 (REQ-147, D-241, C-35)
---ci--- project: orca phase: 8 milestone: v0.12 status: execute ---/ci--- internal/seal/seal.go: AES-256-GCM sealing with HKDF-SHA256 key derivation from OIDC subject. Seal/Unseal (OIDC mode), SealWithCA/ UnsealWithCA (mTLS-only offline path), SaveSealed/LoadSealed (0600), VerifySealedKey. internal/seal/shamir.go: GF(256) Shamir secret sharing. ShamirSplit (5 shards, threshold 3), ShamirCombine (Lagrange interpolation). UnsealWithShamir for IdP-lost recovery (C-35). 9 tests: seal/unseal round-trip, wrong-sub fails, Shamir 3-of-5 recovery (multiple subsets), 2-shards fails, CA mode, mode mismatch, shard encoding, verification. All pass. Full build + vet green.
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
// Package seal implements the master key sealing mechanism (REQ-147,
|
||||
// D-241, C-35). The secrets master key (32 random bytes) is sealed
|
||||
// (encrypted) with a key derived from an OIDC ID token exchange at
|
||||
// unseal time. The raw master key never touches disk; the sealed blob
|
||||
// (salt + ciphertext) is stored at ClusterDir()/master.key.sealed (0600).
|
||||
//
|
||||
// Shamir 3-of-5 recovery: at seal time, 5 shards are generated; the
|
||||
// operator stores them offline. If the IdP is permanently lost, the
|
||||
// master key can be recovered with any 3 of the 5 shards. No backdoor.
|
||||
//
|
||||
// For the mTLS-only offline path (no OIDC), the seal key is derived
|
||||
// from the cluster's own CA (the operator holds the CA, not a password).
|
||||
package seal
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// SealedBlob is the on-disk format for the sealed master key.
|
||||
// Salt is used with the OIDC token sub (or CA fingerprint) to derive
|
||||
// the unwrapping key via HKDF-SHA256.
|
||||
type SealedBlob struct {
|
||||
Salt []byte `json:"salt"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
// Mode indicates how the seal key was derived: "oidc" or "ca".
|
||||
Mode string `json:"mode"`
|
||||
// Hint is a non-secret hint for recovery (e.g. the OIDC issuer URL
|
||||
// or the CA fingerprint). Used to identify which seal key to use.
|
||||
Hint string `json:"hint"`
|
||||
}
|
||||
|
||||
// Seal encrypts the master key with a key derived from the OIDC token
|
||||
// subject + salt. The seal key = HKDF-SHA256(oidcSub, salt, info="orca-master-key-seal").
|
||||
// Returns the sealed blob (to store on disk) + 5 Shamir shards (to
|
||||
// print for offline recovery).
|
||||
func Seal(masterKey []byte, oidcSub string, issuerHint string) (*SealedBlob, [][]byte, error) {
|
||||
if len(masterKey) != 32 {
|
||||
return nil, nil, fmt.Errorf("seal: master key must be 32 bytes, got %d", len(masterKey))
|
||||
}
|
||||
if oidcSub == "" {
|
||||
return nil, nil, fmt.Errorf("seal: oidc sub is empty")
|
||||
}
|
||||
salt := make([]byte, 32)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, nil, fmt.Errorf("seal: salt rand: %w", err)
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, nil, fmt.Errorf("seal: nonce rand: %w", err)
|
||||
}
|
||||
sealKey := deriveSealKey(oidcSub, salt)
|
||||
block, err := aes.NewCipher(sealKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("seal: aes: %w", err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("seal: gcm: %w", err)
|
||||
}
|
||||
ciphertext := aead.Seal(nil, nonce, masterKey, []byte("orca-seal"))
|
||||
blob := &SealedBlob{
|
||||
Salt: salt,
|
||||
Nonce: nonce,
|
||||
Ciphertext: ciphertext,
|
||||
Mode: "oidc",
|
||||
Hint: issuerHint,
|
||||
}
|
||||
// Generate 5 Shamir shards for recovery.
|
||||
shards, err := ShamirSplit(masterKey, 5, 3)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("seal: shamir: %w", err)
|
||||
}
|
||||
return blob, shards, nil
|
||||
}
|
||||
|
||||
// Unseal decrypts the sealed master key using the OIDC token subject.
|
||||
// The seal key = HKDF-SHA256(oidcSub, salt, info="orca-master-key-seal").
|
||||
func Unseal(blob *SealedBlob, oidcSub string) ([]byte, error) {
|
||||
if blob.Mode != "oidc" {
|
||||
return nil, fmt.Errorf("seal: blob mode is %q, not oidc", blob.Mode)
|
||||
}
|
||||
sealKey := deriveSealKey(oidcSub, blob.Salt)
|
||||
block, err := aes.NewCipher(sealKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: aes: %w", err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: gcm: %w", err)
|
||||
}
|
||||
masterKey, err := aead.Open(nil, blob.Nonce, blob.Ciphertext, []byte("orca-seal"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: decrypt (wrong sub or corrupted): %w", err)
|
||||
}
|
||||
return masterKey, nil
|
||||
}
|
||||
|
||||
// UnsealWithShamir recovers the master key from a quorum of Shamir
|
||||
// shards (3 of 5). Used when the IdP is permanently lost (C-35).
|
||||
func UnsealWithShamir(blob *SealedBlob, shards [][]byte) ([]byte, error) {
|
||||
if len(shards) < 3 {
|
||||
return nil, fmt.Errorf("seal: need at least 3 shards, got %d", len(shards))
|
||||
}
|
||||
masterKey, err := ShamirCombine(shards[:3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: shamir combine: %w", err)
|
||||
}
|
||||
if len(masterKey) != 32 {
|
||||
return nil, fmt.Errorf("seal: recovered key is %d bytes, want 32", len(masterKey))
|
||||
}
|
||||
return masterKey, nil
|
||||
}
|
||||
|
||||
// SealWithCA encrypts the master key using a key derived from the
|
||||
// cluster CA fingerprint (mTLS-only offline path, D-241). The seal
|
||||
// key = HKDF-SHA256(caFingerprint, salt, info="orca-master-key-seal-ca").
|
||||
func SealWithCA(masterKey []byte, caFingerprint string) (*SealedBlob, error) {
|
||||
if len(masterKey) != 32 {
|
||||
return nil, fmt.Errorf("seal: master key must be 32 bytes, got %d", len(masterKey))
|
||||
}
|
||||
salt := make([]byte, 32)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return nil, fmt.Errorf("seal: salt rand: %w", err)
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, fmt.Errorf("seal: nonce rand: %w", err)
|
||||
}
|
||||
sealKey := deriveCASealKey(caFingerprint, salt)
|
||||
block, err := aes.NewCipher(sealKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: aes: %w", err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: gcm: %w", err)
|
||||
}
|
||||
ciphertext := aead.Seal(nil, nonce, masterKey, []byte("orca-seal-ca"))
|
||||
return &SealedBlob{
|
||||
Salt: salt,
|
||||
Nonce: nonce,
|
||||
Ciphertext: ciphertext,
|
||||
Mode: "ca",
|
||||
Hint: caFingerprint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UnsealWithCA decrypts using the CA fingerprint.
|
||||
func UnsealWithCA(blob *SealedBlob, caFingerprint string) ([]byte, error) {
|
||||
if blob.Mode != "ca" {
|
||||
return nil, fmt.Errorf("seal: blob mode is %q, not ca", blob.Mode)
|
||||
}
|
||||
sealKey := deriveCASealKey(caFingerprint, blob.Salt)
|
||||
block, err := aes.NewCipher(sealKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: aes: %w", err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: gcm: %w", err)
|
||||
}
|
||||
masterKey, err := aead.Open(nil, blob.Nonce, blob.Ciphertext, []byte("orca-seal-ca"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: decrypt (wrong CA or corrupted): %w", err)
|
||||
}
|
||||
return masterKey, nil
|
||||
}
|
||||
|
||||
// deriveSealKey derives a 32-byte AES key from the OIDC subject + salt
|
||||
// via HKDF-SHA256.
|
||||
func deriveSealKey(oidcSub string, salt []byte) []byte {
|
||||
hk := hkdf.New(sha256.New, []byte(oidcSub), salt, []byte("orca-master-key-seal"))
|
||||
key := make([]byte, 32)
|
||||
hk.Read(key)
|
||||
return key
|
||||
}
|
||||
|
||||
// deriveCASealKey derives a 32-byte AES key from the CA fingerprint +
|
||||
// salt via HKDF-SHA256.
|
||||
func deriveCASealKey(caFingerprint string, salt []byte) []byte {
|
||||
hk := hkdf.New(sha256.New, []byte(caFingerprint), salt, []byte("orca-master-key-seal-ca"))
|
||||
key := make([]byte, 32)
|
||||
hk.Read(key)
|
||||
return key
|
||||
}
|
||||
|
||||
// SaveSealed writes the sealed blob to disk at 0600.
|
||||
func SaveSealed(path string, blob *SealedBlob) error {
|
||||
data, err := json.MarshalIndent(blob, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("seal: marshal: %w", err)
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||
return fmt.Errorf("seal: write tmp: %w", err)
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// LoadSealed reads the sealed blob from disk.
|
||||
func LoadSealed(path string) (*SealedBlob, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("seal: read: %w", err)
|
||||
}
|
||||
var blob SealedBlob
|
||||
if err := json.Unmarshal(data, &blob); err != nil {
|
||||
return nil, fmt.Errorf("seal: parse: %w", err)
|
||||
}
|
||||
return &blob, nil
|
||||
}
|
||||
|
||||
// EncodeShard base64-encodes a shard for display/storage.
|
||||
func EncodeShard(shard []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(shard)
|
||||
}
|
||||
|
||||
// DecodeShard base64-decodes a shard.
|
||||
func DecodeShard(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// VerifySealedKey verifies that a candidate master key matches the
|
||||
// sealed blob (by re-sealing and comparing). Used after unseal to
|
||||
// confirm correctness before use.
|
||||
func VerifySealedKey(blob *SealedBlob, masterKey []byte, oidcSub string) bool {
|
||||
sealKey := deriveSealKey(oidcSub, blob.Salt)
|
||||
block, err := aes.NewCipher(sealKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ct := aead.Seal(nil, blob.Nonce, masterKey, []byte("orca-seal"))
|
||||
return hmac.Equal(ct, blob.Ciphertext)
|
||||
}
|
||||
|
||||
// ensure binary import is used (for shard encoding).
|
||||
var _ = binary.BigEndian
|
||||
@@ -0,0 +1,174 @@
|
||||
package seal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSealUnsealRoundTrip verifies the OIDC seal/unseal round-trip.
|
||||
func TestSealUnsealRoundTrip(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i)
|
||||
}
|
||||
blob, shards, err := Seal(masterKey, "user-oidc-sub-123", "https://idp.example")
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
if len(shards) != 5 {
|
||||
t.Errorf("shards = %d, want 5", len(shards))
|
||||
}
|
||||
if blob.Mode != "oidc" {
|
||||
t.Errorf("mode = %q, want oidc", blob.Mode)
|
||||
}
|
||||
unsealed, err := Unseal(blob, "user-oidc-sub-123")
|
||||
if err != nil {
|
||||
t.Fatalf("Unseal: %v", err)
|
||||
}
|
||||
if !bytes.Equal(unsealed, masterKey) {
|
||||
t.Error("unsealed key != original")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSealWrongSubFails verifies unseal with the wrong subject fails.
|
||||
func TestSealWrongSubFails(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
blob, _, err := Seal(masterKey, "correct-sub", "https://idp")
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
_, err = Unseal(blob, "wrong-sub")
|
||||
if err == nil {
|
||||
t.Error("Unseal with wrong sub should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestShamirRecovery verifies 3-of-5 recovery works.
|
||||
func TestShamirRecovery(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i + 1)
|
||||
}
|
||||
blob, shards, err := Seal(masterKey, "sub-123", "https://idp")
|
||||
if err != nil {
|
||||
t.Fatalf("Seal: %v", err)
|
||||
}
|
||||
// Recover with first 3 shards.
|
||||
recovered, err := UnsealWithShamir(blob, shards[:3])
|
||||
if err != nil {
|
||||
t.Fatalf("UnsealWithShamir (3 shards): %v", err)
|
||||
}
|
||||
if !bytes.Equal(recovered, masterKey) {
|
||||
t.Error("recovered key != original")
|
||||
}
|
||||
// Recover with last 3 shards (different subset).
|
||||
recovered2, err := UnsealWithShamir(blob, shards[2:])
|
||||
if err != nil {
|
||||
t.Fatalf("UnsealWithShamir (last 3): %v", err)
|
||||
}
|
||||
if !bytes.Equal(recovered2, masterKey) {
|
||||
t.Error("recovered key (last 3) != original")
|
||||
}
|
||||
}
|
||||
|
||||
// TestShamirTwoShardsFails verifies 2 shards are insufficient.
|
||||
func TestShamirTwoShardsFails(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
_, shards, _ := Seal(masterKey, "sub", "https://idp")
|
||||
_, err := UnsealWithShamir(nil, shards[:2])
|
||||
if err == nil {
|
||||
t.Error("2 shards should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestShamirSplitCombine verifies direct split/combine round-trip.
|
||||
func TestShamirSplitCombine(t *testing.T) {
|
||||
secret := make([]byte, 32)
|
||||
for i := range secret {
|
||||
secret[i] = byte(i + 100)
|
||||
}
|
||||
if len(secret) != 32 {
|
||||
t.Fatalf("test secret is %d bytes, want 32", len(secret))
|
||||
}
|
||||
shards, err := ShamirSplit(secret, 5, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("ShamirSplit: %v", err)
|
||||
}
|
||||
if len(shards) != 5 {
|
||||
t.Errorf("shards = %d, want 5", len(shards))
|
||||
}
|
||||
// Any 3 shards reconstruct the secret.
|
||||
for _, combo := range [][][]byte{shards[:3], shards[1:4], shards[2:5], [][]byte{shards[0], shards[2], shards[4]}} {
|
||||
recovered, err := ShamirCombine(combo)
|
||||
if err != nil {
|
||||
t.Fatalf("Combine: %v", err)
|
||||
}
|
||||
if !bytes.Equal(recovered, secret) {
|
||||
t.Error("recovered != secret")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSealWithCA verifies the mTLS-only offline path.
|
||||
func TestSealWithCA(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
for i := range masterKey {
|
||||
masterKey[i] = byte(i)
|
||||
}
|
||||
blob, err := SealWithCA(masterKey, "sha256:abc123")
|
||||
if err != nil {
|
||||
t.Fatalf("SealWithCA: %v", err)
|
||||
}
|
||||
if blob.Mode != "ca" {
|
||||
t.Errorf("mode = %q, want ca", blob.Mode)
|
||||
}
|
||||
unsealed, err := UnsealWithCA(blob, "sha256:abc123")
|
||||
if err != nil {
|
||||
t.Fatalf("UnsealWithCA: %v", err)
|
||||
}
|
||||
if !bytes.Equal(unsealed, masterKey) {
|
||||
t.Error("unsealed key != original")
|
||||
}
|
||||
// Wrong CA fingerprint fails.
|
||||
_, err = UnsealWithCA(blob, "sha256:wrong")
|
||||
if err == nil {
|
||||
t.Error("UnsealWithCA with wrong fingerprint should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSealedBlobModeMismatch verifies mode mismatch errors.
|
||||
func TestSealedBlobModeMismatch(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
blob, _ := SealWithCA(masterKey, "fp")
|
||||
_, err := Unseal(blob, "sub") // blob is CA-mode, not OIDC
|
||||
if err == nil {
|
||||
t.Error("Unseal OIDC on CA blob should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeDecodeShard verifies shard base64 round-trip.
|
||||
func TestEncodeDecodeShard(t *testing.T) {
|
||||
shard := []byte{1, 2, 3, 4, 5}
|
||||
encoded := EncodeShard(shard)
|
||||
decoded, err := DecodeShard(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, shard) {
|
||||
t.Error("decode != original")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySealedKey verifies the verification function.
|
||||
func TestVerifySealedKey(t *testing.T) {
|
||||
masterKey := make([]byte, 32)
|
||||
blob, _, _ := Seal(masterKey, "sub", "https://idp")
|
||||
if !VerifySealedKey(blob, masterKey, "sub") {
|
||||
t.Error("VerifySealedKey should confirm correct key")
|
||||
}
|
||||
wrongKey := make([]byte, 32)
|
||||
wrongKey[0] = 1
|
||||
if VerifySealedKey(blob, wrongKey, "sub") {
|
||||
t.Error("VerifySealedKey should reject wrong key")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Package seal: shamir.go implements Shamir's Secret Sharing over
|
||||
// GF(256) for the master key recovery (REQ-147, D-241, C-35). Splits
|
||||
// a 32-byte secret into N shards with threshold T (3-of-5 default).
|
||||
// Any T shards reconstruct the secret; fewer than T reveal nothing.
|
||||
package seal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ShamirSplit splits secret into n shards with threshold t. Any t
|
||||
// shards can reconstruct the secret; fewer reveal nothing. Returns
|
||||
// n shards (each is secret-length + 1 byte index). The first byte of
|
||||
// each shard is the x-coordinate (1..n); the remaining bytes are the
|
||||
// y-coordinates evaluated at x over GF(256).
|
||||
func ShamirSplit(secret []byte, n, t int) ([][]byte, error) {
|
||||
if t < 2 || t > n {
|
||||
return nil, fmt.Errorf("shamir: threshold %d must be 2..n (%d)", t, n)
|
||||
}
|
||||
if n > 254 {
|
||||
return nil, fmt.Errorf("shamir: n %d exceeds 254 (GF(256) limit)", n)
|
||||
}
|
||||
if len(secret) == 0 {
|
||||
return nil, fmt.Errorf("shamir: secret is empty")
|
||||
}
|
||||
|
||||
// Generate t-1 random coefficients (degree t-1 polynomial).
|
||||
coeffs := make([][]byte, t)
|
||||
coeffs[0] = secret // constant term = the secret
|
||||
for i := 1; i < t; i++ {
|
||||
c := make([]byte, len(secret))
|
||||
if _, err := rand.Read(c); err != nil {
|
||||
return nil, fmt.Errorf("shamir: coeff rand: %w", err)
|
||||
}
|
||||
coeffs[i] = c
|
||||
}
|
||||
|
||||
shards := make([][]byte, n)
|
||||
for x := 1; x <= n; x++ {
|
||||
shard := make([]byte, len(secret)+1)
|
||||
shard[0] = byte(x) // x-coordinate
|
||||
for j := 0; j < len(secret); j++ {
|
||||
// Evaluate the polynomial at x over GF(256):
|
||||
// y = coeffs[0][j] + coeffs[1][j]*x + coeffs[2][j]*x^2 + ...
|
||||
y := byte(0)
|
||||
xPow := byte(1) // x^0
|
||||
for k := 0; k < t; k++ {
|
||||
y ^= gfMul(coeffs[k][j], xPow)
|
||||
xPow = gfMul(xPow, byte(x))
|
||||
}
|
||||
shard[j+1] = y
|
||||
}
|
||||
shards[x-1] = shard
|
||||
}
|
||||
return shards, nil
|
||||
}
|
||||
|
||||
// ShamirCombine reconstructs the secret from >= threshold shards
|
||||
// using Lagrange interpolation over GF(256). Extra shards (beyond
|
||||
// threshold) are ignored.
|
||||
func ShamirCombine(shards [][]byte) ([]byte, error) {
|
||||
if len(shards) < 2 {
|
||||
return nil, fmt.Errorf("shamir: need at least 2 shards, got %d", len(shards))
|
||||
}
|
||||
// Verify all shards have the same length.
|
||||
shardLen := len(shards[0])
|
||||
if shardLen < 2 {
|
||||
return nil, fmt.Errorf("shamir: shard too short (%d)", shardLen)
|
||||
}
|
||||
for _, s := range shards {
|
||||
if len(s) != shardLen {
|
||||
return nil, fmt.Errorf("shamir: shard length mismatch")
|
||||
}
|
||||
}
|
||||
secretLen := shardLen - 1
|
||||
secret := make([]byte, secretLen)
|
||||
|
||||
// Lagrange interpolation: for each byte position, recover the
|
||||
// constant term (the secret byte) from the y-values at the
|
||||
// given x-coordinates.
|
||||
for j := 0; j < secretLen; j++ {
|
||||
// Collect (x, y) pairs for this byte position.
|
||||
xs := make([]byte, len(shards))
|
||||
ys := make([]byte, len(shards))
|
||||
for i, s := range shards {
|
||||
xs[i] = s[0]
|
||||
ys[i] = s[j+1]
|
||||
}
|
||||
// Compute Lagrange basis at x=0 (recover the constant term).
|
||||
secret[j] = lagrangeAtZero(xs, ys)
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// lagrangeAtZero computes the Lagrange interpolation at x=0 over
|
||||
// GF(256), which recovers the constant term (the secret).
|
||||
func lagrangeAtZero(xs, ys []byte) byte {
|
||||
result := byte(0)
|
||||
for i := range xs {
|
||||
// Basis polynomial L_i(0) = product over j!=i of (0 - x_j) / (x_i - x_j)
|
||||
num := byte(1)
|
||||
den := byte(1)
|
||||
for j := range xs {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
num = gfMul(num, xs[j]) // (0 - x_j) = x_j in GF(256) (addition = XOR)
|
||||
den = gfMul(den, xs[i]^xs[j])
|
||||
}
|
||||
// L_i(0) = num / den = num * den^-1
|
||||
lagrange := gfMul(num, gfInv(den))
|
||||
result ^= gfMul(ys[i], lagrange)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// gfMul multiplies two elements in GF(256) using the standard
|
||||
// Russian-peasant algorithm with the AES polynomial (0x11B).
|
||||
func gfMul(a, b byte) byte {
|
||||
var result byte
|
||||
for i := 0; i < 8; i++ {
|
||||
if b&1 != 0 {
|
||||
result ^= a
|
||||
}
|
||||
hiBit := a & 0x80
|
||||
a <<= 1
|
||||
if hiBit != 0 {
|
||||
a ^= 0x1B // AES irreducible polynomial
|
||||
}
|
||||
b >>= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// gfInv computes the multiplicative inverse in GF(256) via
|
||||
// exponentiation (a^254 = a^-1 in GF(256), since a^255 = 1).
|
||||
func gfInv(a byte) byte {
|
||||
if a == 0 {
|
||||
return 0 // 0 has no inverse; callers ensure den != 0
|
||||
}
|
||||
// a^254 = a^(11111110b)
|
||||
result := a
|
||||
for i := 0; i < 6; i++ {
|
||||
result = gfMul(result, result) // a^(2^(i+1))
|
||||
// Set the bit for 254 = 0b11111110
|
||||
}
|
||||
// a^254 = a^2 * a^4 * a^8 * a^16 * a^32 * a^64 * a^128
|
||||
// = a^(2+4+8+16+32+64+128) = a^254
|
||||
// Recompute properly via repeated squaring with accumulation.
|
||||
result = byte(1)
|
||||
acc := a
|
||||
for bit := 1; bit < 256; bit <<= 1 {
|
||||
if bit&254 != 0 { // 254 = 0b11111110
|
||||
result = gfMul(result, acc)
|
||||
}
|
||||
acc = gfMul(acc, acc)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user