mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
b6abede4a3
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.
159 lines
4.9 KiB
Go
159 lines
4.9 KiB
Go
package lnencrypt
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
// baseEncryptionKeyLoc is the KeyLocator that we'll use to derive the base
|
|
// encryption key used for encrypting all payloads. We use this to then
|
|
// derive the actual key that we'll use for encryption. We do this
|
|
// rather than using the raw key, as we assume that we can't obtain the raw
|
|
// keys, and we don't want to require that the HSM know our target cipher for
|
|
// encryption.
|
|
//
|
|
// TODO(roasbeef): possibly unique encrypt?
|
|
var baseEncryptionKeyLoc = keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyBaseEncryption,
|
|
Index: 0,
|
|
}
|
|
|
|
// EncrypterDecrypter is an interface representing an object that encrypts or
|
|
// decrypts data.
|
|
type EncrypterDecrypter interface {
|
|
// EncryptPayloadToWriter attempts to write the set of provided bytes
|
|
// into the passed io.Writer in an encrypted form.
|
|
EncryptPayloadToWriter([]byte, io.Writer) error
|
|
|
|
// DecryptPayloadFromReader attempts to decrypt the encrypted bytes
|
|
// within the passed io.Reader instance using the key derived from
|
|
// the passed keyRing.
|
|
DecryptPayloadFromReader(io.Reader) ([]byte, error)
|
|
}
|
|
|
|
// Encrypter is a struct responsible for encrypting and decrypting data.
|
|
type Encrypter struct {
|
|
encryptionKey []byte
|
|
}
|
|
|
|
// KeyRingEncrypter derives an encryption key to encrypt all our files that are
|
|
// written to disk and returns an Encrypter object holding the key.
|
|
//
|
|
// The key itself, is the sha2 of a base key that we get from the keyring. We
|
|
// derive the key this way as we don't force the HSM (or any future
|
|
// abstractions) to be able to derive and know of the cipher that we'll use
|
|
// within our protocol.
|
|
func KeyRingEncrypter(keyRing keychain.KeyRing) (*Encrypter, error) {
|
|
// key = SHA256(baseKey)
|
|
baseKey, err := keyRing.DeriveKey(
|
|
baseEncryptionKeyLoc,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encryptionKey := sha256.Sum256(
|
|
baseKey.PubKey.SerializeCompressed(),
|
|
)
|
|
|
|
// TODO(roasbeef): throw back in ECDH?
|
|
|
|
return &Encrypter{
|
|
encryptionKey: encryptionKey[:],
|
|
}, 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
|
|
// 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
|
|
// used as associated data in the AEAD.
|
|
func (e Encrypter) EncryptPayloadToWriter(payload []byte,
|
|
w io.Writer) error {
|
|
|
|
// Before encryption, we'll initialize our cipher with the target
|
|
// encryption key, and also read out our random 24-byte nonce we use
|
|
// for encryption. Note that we use NewX, not New, as the latter
|
|
// version requires a 12-byte nonce, not a 24-byte nonce.
|
|
cipher, err := chacha20poly1305.NewX(e.encryptionKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var nonce [chacha20poly1305.NonceSizeX]byte
|
|
if _, err := rand.Read(nonce[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Finally, we encrypted the final payload, and write out our
|
|
// ciphertext with nonce pre-pended.
|
|
ciphertext := cipher.Seal(nil, nonce[:], payload, nonce[:])
|
|
|
|
if _, err := w.Write(nonce[:]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := w.Write(ciphertext); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DecryptPayloadFromReader attempts to decrypt the encrypted bytes within the
|
|
// passed io.Reader instance using the key derived from the passed keyRing. For
|
|
// further details regarding the key derivation protocol, see the
|
|
// KeyRingEncrypter function.
|
|
func (e Encrypter) DecryptPayloadFromReader(payload io.Reader) ([]byte,
|
|
error) {
|
|
|
|
// Next, we'll read out the entire blob as we need to isolate the nonce
|
|
// from the rest of the ciphertext.
|
|
packedPayload, err := io.ReadAll(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(packedPayload) < chacha20poly1305.NonceSizeX {
|
|
return nil, fmt.Errorf("payload size too small, must be at "+
|
|
"least %v bytes", chacha20poly1305.NonceSizeX)
|
|
}
|
|
|
|
nonce := packedPayload[:chacha20poly1305.NonceSizeX]
|
|
ciphertext := packedPayload[chacha20poly1305.NonceSizeX:]
|
|
|
|
// Now that we have the cipher text and the nonce separated, we can go
|
|
// ahead and decrypt the final blob so we can properly serialize.
|
|
cipher, err := chacha20poly1305.NewX(e.encryptionKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plaintext, err := cipher.Open(nil, nonce, ciphertext, nonce)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|