Skip to content

Commit

Permalink
Merge pull request #15 from threshold-network/ephemeral
Browse files Browse the repository at this point in the history
Port keep-network/keep-core/pkg/crypto/ephemeral
  • Loading branch information
lukasz-zimnoch authored Oct 15, 2024
2 parents f54acb8 + 9ef9b71 commit b12f0f1
Show file tree
Hide file tree
Showing 10 changed files with 640 additions and 1 deletion.
65 changes: 65 additions & 0 deletions ephemeral/box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package ephemeral

import (
"crypto/rand"
"errors"
"fmt"
"io"

"golang.org/x/crypto/nacl/secretbox"
)

const (
// KeyLength represents the byte size of the key.
KeyLength = 32

// NonceSize represents the byte size of nonce for XSalsa20 cipher used for
// encryption.
NonceSize = 24
)

// box is used to encrypt and decrypt a plaintext.
type box struct {
key [KeyLength]byte
}

// newBox uses XSalsa20 and Poly1305 to encrypt and decrypt the plaintext
// with the key.
func newBox(key [KeyLength]byte) *box {
return &box{
key: key,
}
}

// encrypt takes the input plaintext and uses XSalsa20 and Poly1305 to encrypt
// the plaintext with the key.
func (b *box) encrypt(plaintext []byte) ([]byte, error) {
// The nonce needs to be unique, but not secure. Therefore we include it
// at the beginning of the ciphertext.
var nonce [NonceSize]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
return nil, fmt.Errorf("key encryption failed [%v]", err)
}

return secretbox.Seal(nonce[:], plaintext, &nonce, &b.key), nil
}

// decrypt takes the input ciphertext and decrypts it.
func (b *box) decrypt(ciphertext []byte) (plaintext []byte, err error) {
defer func() {
// secretbox Open panics for invalid input
if recover() != nil {
err = errors.New("symmetric key decryption failed")
}
}()

var nonce [NonceSize]byte
copy(nonce[:], ciphertext[:NonceSize])

plaintext, ok := secretbox.Open(nil, ciphertext[NonceSize:], &nonce, &b.key)
if !ok {
err = fmt.Errorf("symmetric key decryption failed")
}

return
}
81 changes: 81 additions & 0 deletions ephemeral/box_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package ephemeral

import (
"crypto/sha256"
"fmt"
"reflect"
"testing"
)

var accountPassword = []byte("passW0rd")

func TestBoxEncryptDecrypt(t *testing.T) {
msg := "Keep Calm and Carry On"

box := newBox(sha256.Sum256(accountPassword))

encrypted, err := box.encrypt([]byte(msg))
if err != nil {
t.Fatal(err)
}

decrypted, err := box.decrypt(encrypted)
if err != nil {
t.Fatal(err)
}

decryptedString := string(decrypted)
if decryptedString != msg {
t.Fatalf(
"unexpected message\nexpected: %v\nactual: %v",
msg,
decryptedString,
)
}
}

func TestBoxCiphertextRandomized(t *testing.T) {
msg := `Why do we tell actors to 'break a leg?'
Because every play has a cast.`

box := newBox(sha256.Sum256(accountPassword))

encrypted1, err := box.encrypt([]byte(msg))
if err != nil {
t.Fatal(err)
}

encrypted2, err := box.encrypt([]byte(msg))
if err != nil {
t.Fatal(err)
}

if len(encrypted1) != len(encrypted2) {
t.Fatalf(
"expected the same length of ciphertexts (%v vs %v)",
len(encrypted1),
len(encrypted2),
)
}

if reflect.DeepEqual(encrypted1, encrypted2) {
t.Fatalf("expected two different ciphertexts")
}
}

func TestBoxGracefullyHandleBrokenCipher(t *testing.T) {
box := newBox(sha256.Sum256(accountPassword))

brokenCipher := []byte{0x01, 0x02, 0x03}

_, err := box.decrypt(brokenCipher)

expectedError := fmt.Errorf("symmetric key decryption failed")
if !reflect.DeepEqual(expectedError, err) {
t.Fatalf(
"unexpected error\nexpected: %v\nactual: %v",
expectedError,
err,
)
}
}
9 changes: 9 additions & 0 deletions ephemeral/ephemeral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ephemeral

// SymmetricKey is an ephemeral key shared between two parties that was
// established with Diffie-Hellman key exchange over a channel that does
// not need to be secure.
type SymmetricKey interface {
Encrypt([]byte) ([]byte, error)
Decrypt([]byte) ([]byte, error)
}
61 changes: 61 additions & 0 deletions ephemeral/full_ecdh_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ephemeral

import (
"testing"

"threshold.network/roast/internal/testutils"
)

func TestFullEcdh(t *testing.T) {
//
// players generate ephemeral keypair
//

// player 1
keyPair1, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}

// player 2
keyPair2, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}

//
// players exchange public keys and perform ECDH
//

// player 1:
symmetricKey1 := keyPair1.PrivateKey.Ecdh(keyPair2.PublicKey)

// player 2:
symmetricKey2 := keyPair2.PrivateKey.Ecdh(keyPair1.PublicKey)

//
// players use symmetric key for encryption/decryption
//

msg := "People say nothing is impossible, but I do nothing every day"

// player 1:
encrypted, err := symmetricKey1.Encrypt([]byte(msg))
if err != nil {
t.Fatal(err)
}

//player 2:
decrypted, err := symmetricKey2.Decrypt(encrypted)
if err != nil {
t.Fatal(err)
}

decryptedString := string(decrypted)
testutils.AssertStringsEqual(
t,
"decrypted message",
msg,
decryptedString,
)
}
75 changes: 75 additions & 0 deletions ephemeral/private_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ephemeral

import (
"fmt"

"github.com/btcsuite/btcd/btcec"
)

// PrivateKey is an ephemeral private elliptic curve key.
type PrivateKey btcec.PrivateKey

// PublicKey is an ephemeral public elliptic curve key.
type PublicKey btcec.PublicKey

// KeyPair represents the generated ephemeral elliptic curve private and public
// key pair
type KeyPair struct {
PrivateKey *PrivateKey
PublicKey *PublicKey
}

func curve() *btcec.KoblitzCurve {
return btcec.S256()
}

// GenerateKeyPair generates a pair of public and private elliptic curve
// ephemeral key that can be used as an input for ECDH.
func GenerateKeyPair() (*KeyPair, error) {
ecdsaKey, err := btcec.NewPrivateKey(curve())
if err != nil {
return nil, fmt.Errorf(
"could not generate new ephemeral keypair: [%v]",
err,
)
}

return &KeyPair{
(*PrivateKey)(ecdsaKey),
(*PublicKey)(&ecdsaKey.PublicKey),
}, nil
}

// IsKeyMatching verifies if private key is valid for given public key.
// It checks if public key equals `g^privateKey`, where `g` is a base point of
// the curve.
func (pk *PublicKey) IsKeyMatching(privateKey *PrivateKey) bool {
expectedX, expectedY := curve().ScalarBaseMult(privateKey.Marshal())
return expectedX.Cmp(pk.X) == 0 && expectedY.Cmp(pk.Y) == 0
}

// UnmarshalPrivateKey turns a slice of bytes into a `PrivateKey`.
func UnmarshalPrivateKey(bytes []byte) *PrivateKey {
priv, _ := btcec.PrivKeyFromBytes(curve(), bytes)
return (*PrivateKey)(priv)
}

// UnmarshalPublicKey turns a slice of bytes into a `PublicKey`.
func UnmarshalPublicKey(bytes []byte) (*PublicKey, error) {
pubKey, err := btcec.ParsePubKey(bytes, curve())
if err != nil {
return nil, fmt.Errorf("could not parse ephemeral public key: [%v]", err)
}

return (*PublicKey)(pubKey), nil
}

// Marshal turns a `PrivateKey` into a slice of bytes.
func (pk *PrivateKey) Marshal() []byte {
return (*btcec.PrivateKey)(pk).Serialize()
}

// Marshal turns a `PublicKey` into a slice of bytes.
func (pk *PublicKey) Marshal() []byte {
return (*btcec.PublicKey)(pk).SerializeCompressed()
}
59 changes: 59 additions & 0 deletions ephemeral/private_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ephemeral

import (
"reflect"
"testing"
)

func TestMarshalUnmarshalPublicKey(t *testing.T) {
keyPair, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}

marshalled := keyPair.PublicKey.Marshal()

unmarshalled, err := UnmarshalPublicKey(marshalled)
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(unmarshalled, keyPair.PublicKey) {
t.Fatal("unmarshalled public key does not match the original one")
}
}

func TestMarshalUnmarshalPrivateKey(t *testing.T) {
keyPair, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}

marshalled := keyPair.PrivateKey.Marshal()
unmarshalled := UnmarshalPrivateKey(marshalled)

if !reflect.DeepEqual(unmarshalled, keyPair.PrivateKey) {
t.Fatal("unmarshalled private key does not match the original one")
}
}

func TestIsKeyMatching(t *testing.T) {
keyPair1, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}
keyPair2, err := GenerateKeyPair()
if err != nil {
t.Fatal(err)
}

ok := keyPair1.PublicKey.IsKeyMatching(keyPair1.PrivateKey)
if !ok {
t.Fatal("private key does not match the public key")
}

ok = keyPair1.PublicKey.IsKeyMatching(keyPair2.PrivateKey)
if ok {
t.Fatal("private key matches wrong public key")
}
}
37 changes: 37 additions & 0 deletions ephemeral/symmetric_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package ephemeral

import (
"crypto/sha256"

"github.com/btcsuite/btcd/btcec"
)

// SymmetricEcdhKey is an ephemeral Elliptic Curve key created with
// Diffie-Hellman key exchange and implementing `SymmetricKey` interface.
type SymmetricEcdhKey struct {
box *box
}

// Ecdh performs Elliptic Curve Diffie-Hellman operation between public and
// private key. The returned value is `SymmetricEcdhKey` that can be used
// for encryption and decryption.
func (pk *PrivateKey) Ecdh(publicKey *PublicKey) *SymmetricEcdhKey {
shared := btcec.GenerateSharedSecret(
(*btcec.PrivateKey)(pk),
(*btcec.PublicKey)(publicKey),
)

return &SymmetricEcdhKey{
box: newBox(sha256.Sum256(shared)),
}
}

// Encrypt plaintext.
func (sek *SymmetricEcdhKey) Encrypt(plaintext []byte) ([]byte, error) {
return sek.box.encrypt(plaintext)
}

// Decrypt ciphertext.
func (sek *SymmetricEcdhKey) Decrypt(ciphertext []byte) (plaintext []byte, err error) {
return sek.box.decrypt(ciphertext)
}
Loading

0 comments on commit b12f0f1

Please sign in to comment.