mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 01:43:16 +01:00
lnencrypt: add ECDHEncryptor
With this commit we add a new way to encrypt and decrypt a sensitive payload: By using Diffie-Hellman over a local and remote key to derive at a shared secret key.
This commit is contained in:
parent
375bc6950c
commit
b6abede4a3
@ -5,8 +5,8 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
)
|
)
|
||||||
@ -69,6 +69,25 @@ func KeyRingEncrypter(keyRing keychain.KeyRing) (*Encrypter, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ECDHEncrypter derives an encryption key by performing an ECDH operation on
|
||||||
|
// the passed keys. The resulting key is used to encrypt or decrypt files with
|
||||||
|
// sensitive content.
|
||||||
|
func ECDHEncrypter(localKey *btcec.PrivateKey,
|
||||||
|
remoteKey *btcec.PublicKey) (*Encrypter, error) {
|
||||||
|
|
||||||
|
ecdh := keychain.PrivKeyECDH{
|
||||||
|
PrivKey: localKey,
|
||||||
|
}
|
||||||
|
encryptionKey, err := ecdh.ECDH(remoteKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error deriving encryption key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Encrypter{
|
||||||
|
encryptionKey: encryptionKey[:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// EncryptPayloadToWriter attempts to write the set of provided bytes into the
|
// EncryptPayloadToWriter attempts to write the set of provided bytes into the
|
||||||
// passed io.Writer in an encrypted form. We use a 24-byte chachapoly AEAD
|
// passed io.Writer in an encrypted form. We use a 24-byte chachapoly AEAD
|
||||||
// instance with a randomized nonce that's pre-pended to the final payload and
|
// instance with a randomized nonce that's pre-pended to the final payload and
|
||||||
@ -112,7 +131,7 @@ func (e Encrypter) DecryptPayloadFromReader(payload io.Reader) ([]byte,
|
|||||||
|
|
||||||
// Next, we'll read out the entire blob as we need to isolate the nonce
|
// Next, we'll read out the entire blob as we need to isolate the nonce
|
||||||
// from the rest of the ciphertext.
|
// from the rest of the ciphertext.
|
||||||
packedPayload, err := ioutil.ReadAll(payload)
|
packedPayload, err := io.ReadAll(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +36,8 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
|||||||
{
|
{
|
||||||
plaintext: []byte("payload test plain text"),
|
plaintext: []byte("payload test plain text"),
|
||||||
mutator: func(p *[]byte) {
|
mutator: func(p *[]byte) {
|
||||||
// Flip a byte in the payload to render it invalid.
|
// Flip a byte in the payload to render it
|
||||||
|
// invalid.
|
||||||
(*p)[0] ^= 1
|
(*p)[0] ^= 1
|
||||||
},
|
},
|
||||||
valid: false,
|
valid: false,
|
||||||
@ -53,54 +55,55 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyRing := &MockKeyRing{}
|
keyRing := &MockKeyRing{}
|
||||||
|
keyRingEnc, err := KeyRingEncrypter(keyRing)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i, payloadCase := range payloadCases {
|
_, pubKey := btcec.PrivKeyFromBytes([]byte{0x01, 0x02, 0x03, 0x04})
|
||||||
var cipherBuffer bytes.Buffer
|
|
||||||
encrypter, err := KeyRingEncrypter(keyRing)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// First, we'll encrypt the passed payload with our scheme.
|
privKey, err := btcec.NewPrivateKey()
|
||||||
err = encrypter.EncryptPayloadToWriter(
|
require.NoError(t, err)
|
||||||
payloadCase.plaintext, &cipherBuffer,
|
privKeyEnc, err := ECDHEncrypter(privKey, pubKey)
|
||||||
)
|
require.NoError(t, err)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable encrypt paylaod: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a mutator, then we'll wrong the mutator over the
|
for _, payloadCase := range payloadCases {
|
||||||
// cipher text, then reset the main buffer and re-write the new
|
payloadCase := payloadCase
|
||||||
// cipher text.
|
for _, enc := range []*Encrypter{keyRingEnc, privKeyEnc} {
|
||||||
if payloadCase.mutator != nil {
|
enc := enc
|
||||||
cipherText := cipherBuffer.Bytes()
|
|
||||||
|
|
||||||
payloadCase.mutator(&cipherText)
|
// First, we'll encrypt the passed payload with our
|
||||||
|
// scheme.
|
||||||
|
var cipherBuffer bytes.Buffer
|
||||||
|
err = enc.EncryptPayloadToWriter(
|
||||||
|
payloadCase.plaintext, &cipherBuffer,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
cipherBuffer.Reset()
|
// If we have a mutator, then we'll wrong the mutator
|
||||||
cipherBuffer.Write(cipherText)
|
// over the cipher text, then reset the main buffer and
|
||||||
}
|
// re-write the new cipher text.
|
||||||
|
if payloadCase.mutator != nil {
|
||||||
|
cipherText := cipherBuffer.Bytes()
|
||||||
|
|
||||||
plaintext, err := encrypter.DecryptPayloadFromReader(
|
payloadCase.mutator(&cipherText)
|
||||||
&cipherBuffer,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
cipherBuffer.Reset()
|
||||||
// If this was meant to be a valid decryption, but we failed,
|
cipherBuffer.Write(cipherText)
|
||||||
// then we'll return an error.
|
}
|
||||||
case err != nil && payloadCase.valid:
|
|
||||||
t.Fatalf("unable to decrypt valid payload case %v", i)
|
|
||||||
|
|
||||||
// If this was meant to be an invalid decryption, and we didn't
|
plaintext, err := enc.DecryptPayloadFromReader(
|
||||||
// fail, then we'll return an error.
|
&cipherBuffer,
|
||||||
case err == nil && !payloadCase.valid:
|
)
|
||||||
t.Fatalf("payload was invalid yet was able to decrypt")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only if this case was mean to be valid will we ensure the
|
if !payloadCase.valid {
|
||||||
// resulting decrypted plaintext matches the original input.
|
require.Error(t, err)
|
||||||
if payloadCase.valid &&
|
|
||||||
!bytes.Equal(plaintext, payloadCase.plaintext) {
|
continue
|
||||||
t.Fatalf("#%v: expected %v, got %v: ", i,
|
}
|
||||||
payloadCase.plaintext, plaintext)
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(
|
||||||
|
t, plaintext, payloadCase.plaintext,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,7 +114,5 @@ func TestInvalidKeyGeneration(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_, err := KeyRingEncrypter(&MockKeyRing{true})
|
_, err := KeyRingEncrypter(&MockKeyRing{true})
|
||||||
if err == nil {
|
require.Error(t, err)
|
||||||
t.Fatal("expected error due to fail key gen")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user