-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from threshold-network/ephemeral
Port keep-network/keep-core/pkg/crypto/ephemeral
- Loading branch information
Showing
10 changed files
with
640 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.