Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions go/tdh2/tdh2hybridCCP/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## tdh2hybridCCP: Hybrid TDH2 and ChaCha20-Poly1305

This fork of /tdh2/tdh2easy provides a hybrid encryption scheme that uses **Threshold Diffie-Hellman (TDH2)** which is secure against adaptive chosen-ciphertext attacks (CCA2), combined with a ***modern symmetric stream cipher*** **ChaCha20-Poly1305** ***instead of*** **AES-256 in Galois/Counter Mode (GCM)**.

### ChaCha20-Poly1305 replaces AES-256-GCM
The modern stream cipher provides:
- Authenticated Encryption with Associated Data (AEAD), also called Additional Authenticated Data (AAD):
- It encrypts sensitive payload data while allowing additional, authenticated but not encrypted metadata ("associated data") to be authenticated along with the ciphertext which detects any tampering.
- AEAD has become the standard for securing communication, replacing older, less secure methods that combined encryption and Message Authentication Code (MAC) separately.
- Performance:
- Stream ciphers are often faster than AES on devices without hardware acceleration.
- Designed to be fast and efficient, often outperforming separate encryption and authentication mechanisms.
- Verification during Decryption: If the authentication tag does not match the decrypted data and associated data, the decryption fails, ensuring integrity.
- Support for larger plaintext: up to 256 GB compared to maximum ca. 64 GB with AES (RFC5084).

### Example
The [`func TestHybrid()`](./hybrid_test.go) provides running code that steps through the cycle of Distribted Key Generation (DKG), hybrid encryption of plaintext, decryption of shares by parties and their verification before a combiner aggregates the decryption shares, and finally decrypts the ciphertext.

Run it together with other `*_test.go` files after change into subdir `tdh2hybridCCP` of this repo:
```
~/tdh2/go/tdh2/tdh2hybridCCP$ go test
Message encrypted successfully.
Decrypted Message: The quick brown fox jumps over the lazy dog's back 0123456789.
PASS
ok github.com/hb9cwp/tdh2/go/tdh2/tdh2hybridCCP 0.109s
```

### References

The implementation "SG02" of TDH2, the threshold cryptosystem proposed by Shoup and Gennaro[^1], in the Rust library "Thetacrypt"[^2] motivated the replacement of AES-GCM by ChaCha20-Poly1305 and the name for this fork of `tdh2easy`:

> "We apply a ***hybrid*** approach to encrypt a _symmetric key_ under the _threshold key_ and the actual _plaintext_ under the _symmetric key_. As a _symmetric encryption scheme_, we use the ***ChaCha20Poly1305***, a stream cipher combined with a message authentication code."

Both were designed by Daniel J. Bernstein, and published in RFC 7539 by the IRTF
then updated in [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439).

[^1]: [Securing Threshold Cryptosystems against Chosen Ciphertext Attack](https://www.shoup.net/papers/thresh1.pdf), Victor Shoup & Rosario Gennaro, September 18, 2001.

[^2]: [Thetacrypt: A Distributed Service for Threshold Cryptography](https://arxiv.org/pdf/2502.03247), Cryptology and Data Security Research Group at the University of Bern, 6 February 2025.

133 changes: 133 additions & 0 deletions go/tdh2/tdh2hybridCCP/hybrid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package tdh2hybridCCP

import (
"bytes"
"fmt"
"testing"
)

func TestHybrid(t *testing.T) {
// Optional: Rename this to 'func main() {...}' to convert to
// a self-contained Go program. Also replace 't.' by 'log.' and
// add prefix 'tdh2hybridCCP.' to import functions & objects.
// Alternatively, rename it to 'func ExampleHybrid()' or similar to test
// only for final output, see https://pkg.go.dev/testing#hdr-Examples

// 1. Setup: Define the threshold (k) and total participants (n).
// We need at least 2 parties to decrypt out of 3 total.
var k, n int = 2, 3

// Perform a distributed key generation (DKG) protocol to create a
// Master Secret, a collective Public Key, and n individual
// Private Key Shares.
// Note: The Master Secret (ms) returned is ignored here, but it will
// be required for re-keying by Redeal(pk, ms, k, n).
//ms, pubKey, privShares, err := tdh2hybridCCP.GenerateKeys(k, n)
_, pubKey, privShares, err := GenerateKeys(k, n)
if err != nil {
t.Fatalf("Failed to generate keys: %v", err)
}

// 2. Encryption
message := []byte("The quick brown fox jumps over the lazy dog's back 0123456789.")

// Anyone can encrypt using the Public Key only.
//cipherText, err := Encrypt(pubKey, message)
aaData := []byte("tests additional authenticated, but not encrypted metadata")
//cipherText, err := tdh2ccp.EncryptWithAaD(pubKey, message, aaData)
cipherText, err := EncryptWithAaD(pubKey, message, aaData)
//var emptyLabel [tdh2.InputSize]byte
//cipherText, err := EncryptWithLabelAndAaD(pubKey, message, emptyLabel, aaData)
if err != nil {
t.Fatalf("Encryption failed: %v", err)
}
fmt.Println("Message encrypted successfully.")

// 3. Decryption of all n shares
// Each participant creates a 'decryption share' from the ciphertext
// using their own private key share, returns a *DecryptionShare.
// ToDo: generalize for k of n (loop)
share0, err := Decrypt(cipherText, privShares[0])
if err != nil {
t.Fatalf("Decryption share0 by party 0 failed: %v", err)
}
share1, err := Decrypt(cipherText, privShares[1])
if err != nil {
t.Fatalf("Decryption share1 by party 1 failed: %v", err)
}
share2, err := Decrypt(cipherText, privShares[2])
if err != nil {
t.Fatalf("Decryption share2 by party 2 failed: %v", err)
}

// 4. Verification: Combiner verifies decrypted shares before aggregating them.
// Observe comment from Aggregate(): "Ciphertext and shares MUST be verified
// before calling Aggregate ..."
// ToDo: generalize for k of n (loop)
err = VerifyShare(cipherText, pubKey, share0)
if err != nil {
t.Fatalf("Verify share0 by combiner failed: %v", err)
}
err = VerifyShare(cipherText, pubKey, share1)
if err != nil {
t.Fatalf("Verify share1 by combiner failed: %v", err)
}
err = VerifyShare(cipherText, pubKey, share2)
if err != nil {
t.Fatalf("Verify share2 by combiner failed: %v", err)
}

// 5. Aggregation: Combine min. k of n decrypted shares to recover the
// original message in cleartext.
// ToDo: Perform fuzzing over cleartext of other messages lenght
// from 0 to max. (2^32 -1)*64 = 256 GB (the first block of 64 byte
// is used by Poly1305), see comment in sym.go.

// Create a slice of the pointers, not a slice of byte slices.
// All the shares have to be distinct and their number has to be
// at least the threshold k.
//decryptionShares := []*tdh2hybridCCP.DecryptionShare{share0, share1}
decryptionShares := []*DecryptionShare{share0, share1}
if _, err := Aggregate(cipherText, decryptionShares, n); err != nil {
t.Fatalf("Aggregation of share0, share1 failed: %v", err)
}
decryptionShares = []*DecryptionShare{share0, share2}
if _, err := Aggregate(cipherText, decryptionShares, n); err != nil {
t.Fatalf("Aggregation of share0, share2 failed: %v", err)
}
decryptionShares = []*DecryptionShare{share1, share2}
if _, err := Aggregate(cipherText, decryptionShares, n); err != nil {
t.Fatalf("Aggregation of share1, share2 failed: %v", err)
}
decryptionShares = []*DecryptionShare{share2, share0} // rotate (reverse) order
if _, err := Aggregate(cipherText, decryptionShares, n); err != nil {
t.Fatalf("Aggregation of share2, share0 failed: %v", err)
}
decryptionShares = []*DecryptionShare{share0, share1, share2} // all shares
if _, err := Aggregate(cipherText, decryptionShares, n); err != nil {
t.Fatalf("Aggregation of share0, share1, share2 failed: %v", err)
}
// make Aggregate() fail:
decryptionShares = []*DecryptionShare{share1, share1} // shares not distinct
if _, err := Aggregate(cipherText, decryptionShares, n); err == nil {
t.Fatalf("Aggregation of share1, share1 must fail: %v", err)
}
decryptionShares = []*DecryptionShare{share1, share1, share2} // shares not distinct
if _, err := Aggregate(cipherText, decryptionShares, n); err == nil {
t.Fatalf("Aggregation of share1, share1, share2 must fail: %v", err)
}
decryptionShares = []*DecryptionShare{share1} // fewer shares than threshold k
if _, err := Aggregate(cipherText, decryptionShares, n); err == nil {
t.Fatalf("Aggregation of share1 must fail: %v", err)
}

decryptionShares = []*DecryptionShare{share0, share1} // repeat one last time
decryptedMsg, err := Aggregate(cipherText, decryptionShares, n)
if err != nil {
t.Fatalf("Aggregation of share0, share1 failed: %v", err)
}
if !bytes.Equal(decryptedMsg, message) {
t.Fatalf("decrypeted message does not match cleartext\n got: %#v\n want: %#v", decryptedMsg, message)
}
fmt.Printf("Decrypted Message: %s\n", string(decryptedMsg))
}
77 changes: 77 additions & 0 deletions go/tdh2/tdh2hybridCCP/sym.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package tdh2hybridCCP

import (
"bytes"
"crypto/rand"
"fmt"

"golang.org/x/crypto/chacha20poly1305"
)

// symKey generates a symmetric key.
func symKey(keySize int) ([]byte, error) {
key := make([]byte, keySize)
if _, err := rand.Read(key); err != nil {
return nil, fmt.Errorf("cannot generate key")
}
return key, nil
}

// symEncrypt encrypts the message using the ChaCha20Poly1305 AEAD cipher.
func symEncrypt(msg, key, aaData []byte) ([]byte, []byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, nil, fmt.Errorf("cannot use ChaCha20Poly1305: %w", err)
}

// Counter overflow is catastrophic security failure because keystream repeats
// and attacker can XOR two ciphertexts to cancel out keystream!
// Never reuse a (key, nonce) pair for more than the limit:
// * AES-256-GCM block size is 16 byte, max. 2^32 *16 = 64 GB (conservative
// limit) and RFC 5084 2^36 - 32 bytes ≈ 68.7 GB (theoretical maximum)
// if uint64(len(msg)) > ((1<<32)-2)*uint64(block.BlockSize()) {
// * ChaCha20-Poly1305 block size is 64 byte, max. 2^32 *64 = 256 GB
// which allows 4× larger messages than AES-256-GCM.
// Its block 0 is used by Poly1305:
if uint64(len(msg)) > ((1<<32)-1)*uint64(64) { //
return nil, nil, fmt.Errorf("message too long")
}
// * XChaCha20-Poly1305 (Extended Nonce Variant) uses a 64-bit counter
// instead of 32-bit, and nonce size of 24 vs 12 bytes. Its block
// size is also 64 byte, max 2^64 *64 ≈ 1.18 × 10^21 bytes (1 Zettabyte!)
// which is far beyond any practical use case, e.g. practically unlimited.

// Generate random nonce (12 bytes for ChaCha20Poly1305, same as AES-GCM)
nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, nil, fmt.Errorf("failed to generate nonce: %w", err)
}

// Encrypt: prepend nonce to ciphertext is done by passing nonce into first parameter 'dst'
// Format: [nonce][ciphertext + aaData + authN tag]
//return aead.Seal(nonce, nonce, msg, nil), nonce, nil // returns (ctxt, nonce, err)
return aead.Seal(nonce, nonce, msg, aaData), nonce, nil // returns (ctxt, nonce, err)
}

// symDecrypt decrypts the ciphertext using theChaCha20-Poly1305 cipher.
func symDecrypt(nonce, ctxt, key, aaData []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, fmt.Errorf("failed to create ChaCha20Poly1305 cipher: %w", err)
}
if len(ctxt) < aead.NonceSize() {
return nil, fmt.Errorf("ciphertext too short")
}

// Extract nonce and encrypted data
nonceRecovered := ctxt[:aead.NonceSize()]
if !bytes.Equal(nonceRecovered, nonce) {
return nil, fmt.Errorf("nonce mismatch")
}
encryptedData := ctxt[aead.NonceSize():]

// Decrypt and verify: AEAD authenticates additional, non-encrypted aaData which
// detects which detects any tampering with metadata
//return aead.Open(nil, nonceRecovered, encryptedData, nil) // authN fails if aaData was set
return aead.Open(nil, nonceRecovered, encryptedData, aaData)
}
Loading