Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port keep-network/keep-core/pkg/crypto/ephemeral #15

Merged
merged 2 commits into from
Oct 15, 2024
Merged
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
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
Loading