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:
Oliver Gugger 2023-11-16 07:59:22 -06:00
parent 375bc6950c
commit b6abede4a3
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
2 changed files with 65 additions and 45 deletions

View File

@ -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
} }

View File

@ -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")
}
} }