mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
Merge pull request #6500 from voltagecloud/encrypt-tor-privkey
[tor] Add ability to encrypt the Tor private key on disk
This commit is contained in:
commit
35c59643cd
@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -184,7 +185,7 @@ func assertMultiEqual(t *testing.T, a, b *Multi) {
|
||||
func TestExtractMulti(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// First, as prep, we'll create a single chan backup, then pack that
|
||||
// fully into a multi backup.
|
||||
|
@ -1,140 +0,0 @@
|
||||
package chanbackup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// TODO(roasbeef): interface in front of?
|
||||
|
||||
// baseEncryptionKeyLoc is the KeyLocator that we'll use to derive the base
|
||||
// encryption key used for encrypting all static channel backups. 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.KeyFamilyStaticBackup,
|
||||
Index: 0,
|
||||
}
|
||||
|
||||
// genEncryptionKey derives the key that we'll use to encrypt all of our static
|
||||
// channel backups. 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 genEncryptionKey(keyRing keychain.KeyRing) ([]byte, 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 encryptionKey[:], nil
|
||||
}
|
||||
|
||||
// encryptPayloadToWriter attempts to write the set of bytes contained within
|
||||
// the passed byes.Buffer 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. We
|
||||
// use the passed keyRing to generate the encryption key, see genEncryptionKey
|
||||
// for further details.
|
||||
func encryptPayloadToWriter(payload bytes.Buffer, w io.Writer,
|
||||
keyRing keychain.KeyRing) error {
|
||||
|
||||
// First, we'll derive the key that we'll use to encrypt the payload
|
||||
// for safe storage without giving away the details of any of our
|
||||
// channels. The final operation is:
|
||||
//
|
||||
// key = SHA256(baseKey)
|
||||
encryptionKey, err := genEncryptionKey(keyRing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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(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.Bytes(), 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
|
||||
// genEncryptionKey method.
|
||||
func decryptPayloadFromReader(payload io.Reader,
|
||||
keyRing keychain.KeyRing) ([]byte, error) {
|
||||
|
||||
// First, we'll re-generate the encryption key that we use for all the
|
||||
// SCBs.
|
||||
encryptionKey, err := genEncryptionKey(keyRing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Next, we'll read out the entire blob as we need to isolate the nonce
|
||||
// from the rest of the ciphertext.
|
||||
packedBackup, err := ioutil.ReadAll(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(packedBackup) < chacha20poly1305.NonceSizeX {
|
||||
return nil, fmt.Errorf("payload size too small, must be at "+
|
||||
"least %v bytes", chacha20poly1305.NonceSizeX)
|
||||
}
|
||||
|
||||
nonce := packedBackup[:chacha20poly1305.NonceSizeX]
|
||||
ciphertext := packedBackup[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 serialized the
|
||||
// SCB.
|
||||
cipher, err := chacha20poly1305.NewX(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintext, err := cipher.Open(nil, nonce, ciphertext, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
@ -89,7 +90,12 @@ func (m Multi) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error {
|
||||
|
||||
// With the plaintext multi backup assembled, we'll now encrypt it
|
||||
// directly to the passed writer.
|
||||
return encryptPayloadToWriter(multiBackupBuffer, w, keyRing)
|
||||
e, err := lnencrypt.KeyRingEncrypter(keyRing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate encrypt key %v", err)
|
||||
}
|
||||
|
||||
return e.EncryptPayloadToWriter(multiBackupBuffer.Bytes(), w)
|
||||
}
|
||||
|
||||
// UnpackFromReader attempts to unpack (decrypt+deserialize) a packed
|
||||
@ -99,7 +105,11 @@ func (m *Multi) UnpackFromReader(r io.Reader, keyRing keychain.KeyRing) error {
|
||||
// We'll attempt to read the entire packed backup, and also decrypt it
|
||||
// using the passed key ring which is expected to be able to derive the
|
||||
// encryption keys.
|
||||
plaintextBackup, err := decryptPayloadFromReader(r, keyRing)
|
||||
e, err := lnencrypt.KeyRingEncrypter(keyRing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate encrypt key %v", err)
|
||||
}
|
||||
plaintextBackup, err := e.DecryptPayloadFromReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -27,7 +28,7 @@ func TestMultiPackUnpack(t *testing.T) {
|
||||
multi.StaticBackups = append(multi.StaticBackups, single)
|
||||
}
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
versionTestCases := []struct {
|
||||
// version is the pack/unpack version that we should use to
|
||||
@ -93,14 +94,17 @@ func TestMultiPackUnpack(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
encrypter, err := lnencrypt.KeyRingEncrypter(keyRing)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Next, we'll make a fake packed multi, it'll have an
|
||||
// unknown version relative to what's implemented atm.
|
||||
var fakePackedMulti bytes.Buffer
|
||||
fakeRawMulti := bytes.NewBuffer(
|
||||
bytes.Repeat([]byte{99}, 20),
|
||||
)
|
||||
err := encryptPayloadToWriter(
|
||||
*fakeRawMulti, &fakePackedMulti, keyRing,
|
||||
err = encrypter.EncryptPayloadToWriter(
|
||||
fakeRawMulti.Bytes(), &fakePackedMulti,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to pack fake multi; %v", err)
|
||||
@ -124,7 +128,7 @@ func TestMultiPackUnpack(t *testing.T) {
|
||||
func TestPackedMultiUnpack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// First, we'll make a new unpacked multi with a random channel.
|
||||
testChannel, err := genRandomOpenChannelShell()
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -80,7 +81,7 @@ func (m *mockChannelNotifier) SubscribeChans(chans map[wire.OutPoint]struct{}) (
|
||||
func TestNewSubSwapperSubscribeFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
var swapper mockSwapper
|
||||
chanNotifier := mockChannelNotifier{
|
||||
@ -152,7 +153,7 @@ func assertExpectedBackupSwap(t *testing.T, swapper *mockSwapper,
|
||||
func TestSubSwapperIdempotentStartStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
var chanNotifier mockChannelNotifier
|
||||
|
||||
@ -181,7 +182,7 @@ func TestSubSwapperIdempotentStartStop(t *testing.T) {
|
||||
func TestSubSwapperUpdater(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
chanNotifier := newMockChannelNotifier()
|
||||
swapper := newMockSwapper(keyRing)
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -49,7 +50,7 @@ func (m *mockPeerConnector) ConnectPeer(node *btcec.PublicKey,
|
||||
func TestUnpackAndRecoverSingles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// First, we'll create a number of single chan backups that we'll
|
||||
// shortly back to so we can begin our recovery attempt.
|
||||
@ -123,7 +124,7 @@ func TestUnpackAndRecoverSingles(t *testing.T) {
|
||||
}
|
||||
|
||||
// If we modify the keyRing, then unpacking should fail.
|
||||
keyRing.fail = true
|
||||
keyRing.Fail = true
|
||||
err = UnpackAndRecoverSingles(
|
||||
packedBackups, keyRing, &chanRestorer, &peerConnector,
|
||||
)
|
||||
@ -139,7 +140,7 @@ func TestUnpackAndRecoverSingles(t *testing.T) {
|
||||
func TestUnpackAndRecoverMulti(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// First, we'll create a number of single chan backups that we'll
|
||||
// shortly back to so we can begin our recovery attempt.
|
||||
@ -217,7 +218,7 @@ func TestUnpackAndRecoverMulti(t *testing.T) {
|
||||
}
|
||||
|
||||
// If we modify the keyRing, then unpacking should fail.
|
||||
keyRing.fail = true
|
||||
keyRing.Fail = true
|
||||
err = UnpackAndRecoverMulti(
|
||||
packedMulti, keyRing, &chanRestorer, &peerConnector,
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
@ -333,10 +334,10 @@ func (s *Single) Serialize(w io.Writer) error {
|
||||
// global counter to use as a sequence number for nonces, and want to ensure
|
||||
// that we're able to decrypt these blobs without any additional context. We
|
||||
// derive the key that we use for encryption via a SHA2 operation of the with
|
||||
// the golden keychain.KeyFamilyStaticBackup base encryption key. We then take
|
||||
// the serialized resulting shared secret point, and hash it using sha256 to
|
||||
// obtain the key that we'll use for encryption. When using the AEAD, we pass
|
||||
// the nonce as associated data such that we'll be able to package the two
|
||||
// the golden keychain.KeyFamilyBaseEncryption base encryption key. We then
|
||||
// take the serialized resulting shared secret point, and hash it using sha256
|
||||
// to obtain the key that we'll use for encryption. When using the AEAD, we
|
||||
// pass the nonce as associated data such that we'll be able to package the two
|
||||
// together for storage. Before writing out the encrypted payload, we prepend
|
||||
// the nonce to the final blob.
|
||||
func (s *Single) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error {
|
||||
@ -351,7 +352,11 @@ func (s *Single) PackToWriter(w io.Writer, keyRing keychain.KeyRing) error {
|
||||
// Finally, we'll encrypt the raw serialized SCB (using the nonce as
|
||||
// associated data), and write out the ciphertext prepend with the
|
||||
// nonce that we used to the passed io.Reader.
|
||||
return encryptPayloadToWriter(rawBytes, w, keyRing)
|
||||
e, err := lnencrypt.KeyRingEncrypter(keyRing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate encrypt key %v", err)
|
||||
}
|
||||
return e.EncryptPayloadToWriter(rawBytes.Bytes(), w)
|
||||
}
|
||||
|
||||
// readLocalKeyDesc reads a KeyDescriptor encoded within an unpacked Single.
|
||||
@ -528,7 +533,11 @@ func (s *Single) Deserialize(r io.Reader) error {
|
||||
// payload for whatever reason (wrong key, wrong nonce, etc), then this method
|
||||
// will return an error.
|
||||
func (s *Single) UnpackFromReader(r io.Reader, keyRing keychain.KeyRing) error {
|
||||
plaintext, err := decryptPayloadFromReader(r, keyRing)
|
||||
e, err := lnencrypt.KeyRingEncrypter(keyRing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate key decrypter %v", err)
|
||||
}
|
||||
plaintext, err := e.DecryptPayloadFromReader(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/shachain"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -207,7 +208,7 @@ func TestSinglePackUnpack(t *testing.T) {
|
||||
|
||||
singleChanBackup := NewSingle(channel, []net.Addr{addr1, addr2})
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
versionTestCases := []struct {
|
||||
// version is the pack/unpack version that we should use to
|
||||
@ -312,7 +313,7 @@ func TestSinglePackUnpack(t *testing.T) {
|
||||
func TestPackedSinglesUnpack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// To start, we'll create 10 new singles, and them assemble their
|
||||
// packed forms into a slice.
|
||||
@ -361,7 +362,7 @@ func TestPackedSinglesUnpack(t *testing.T) {
|
||||
func TestSinglePackStaticChanBackups(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// First, we'll create a set of random single, and along the way,
|
||||
// create a map that will let us look up each single by its chan point.
|
||||
@ -407,8 +408,9 @@ func TestSinglePackStaticChanBackups(t *testing.T) {
|
||||
|
||||
// If we attempt to pack again, but force the key ring to fail, then
|
||||
// the entire method should fail.
|
||||
keyRing.Fail = true
|
||||
_, err = PackStaticChanBackups(
|
||||
unpackedSingles, &mockKeyRing{true},
|
||||
unpackedSingles, &lnencrypt.MockKeyRing{Fail: true},
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatalf("pack attempt should fail")
|
||||
@ -432,7 +434,7 @@ func TestSingleUnconfirmedChannel(t *testing.T) {
|
||||
channel.FundingBroadcastHeight = fundingBroadcastHeight
|
||||
|
||||
singleChanBackup := NewSingle(channel, []net.Addr{addr1, addr2})
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &lnencrypt.MockKeyRing{}
|
||||
|
||||
// Pack it and then unpack it again to make sure everything is written
|
||||
// correctly, then check that the block height of the unpacked
|
||||
|
@ -182,3 +182,8 @@ base directory. This will allow `lnd` to recreate the same hidden service upon
|
||||
restart. If you wish to generate a new onion service, you can simply delete this
|
||||
file. The path to this private key file can also be modified with the
|
||||
`--tor.privatekeypath` argument.
|
||||
|
||||
You can optionally encrypt the Tor private key by using the `--tor.encryptkey`
|
||||
flag. This will still write to the same private key files. However instead of
|
||||
writing the plaintext private key, `lnd` encrypts the private key using the
|
||||
wallet's seed and writes the encrypted blob to the file.
|
@ -58,8 +58,10 @@ minimum version needed to build the project.
|
||||
[With the module updated](https://github.com/lightningnetwork/lnd/pull/6836),
|
||||
`lnd` now parses Tor control port messages correctly.
|
||||
|
||||
* [Update Tor module](https://github.com/lightningnetwork/lnd/pull/6526) to
|
||||
allow the option to encrypt the private key on disk.
|
||||
* [Add option to encrypt Tor private
|
||||
key](https://github.com/lightningnetwork/lnd/pull/6500), and [update the Tor
|
||||
module](https://github.com/lightningnetwork/lnd/pull/6526) to pave the way for
|
||||
this functionality.
|
||||
|
||||
* [Fixed potential data race on funding manager
|
||||
restart](https://github.com/lightningnetwork/lnd/pull/6929).
|
||||
@ -110,6 +112,7 @@ minimum version needed to build the project.
|
||||
* Elle Mouton
|
||||
* ErikEk
|
||||
* Eugene Siegel
|
||||
* Graham Krizek
|
||||
* hieblmi
|
||||
* Jesse de Wit
|
||||
* Matt Morehouse
|
||||
|
2
go.mod
2
go.mod
@ -40,7 +40,7 @@ require (
|
||||
github.com/lightningnetwork/lnd/queue v1.1.0
|
||||
github.com/lightningnetwork/lnd/ticker v1.1.0
|
||||
github.com/lightningnetwork/lnd/tlv v1.0.3
|
||||
github.com/lightningnetwork/lnd/tor v1.0.2
|
||||
github.com/lightningnetwork/lnd/tor v1.1.0
|
||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796
|
||||
github.com/miekg/dns v1.1.43
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
|
2
go.sum
2
go.sum
@ -465,6 +465,8 @@ github.com/lightningnetwork/lnd/tlv v1.0.3/go.mod h1:dzR/aZetBri+ZY/fHbwV06fNn/3
|
||||
github.com/lightningnetwork/lnd/tor v1.0.0/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64=
|
||||
github.com/lightningnetwork/lnd/tor v1.0.2 h1:GlumRkKdzXCX0AIvIi2UXKpeY1Q4RT7Lz/CfGpKSLrU=
|
||||
github.com/lightningnetwork/lnd/tor v1.0.2/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64=
|
||||
github.com/lightningnetwork/lnd/tor v1.1.0 h1:iXO7fSzjxTI+p88KmtpbuyuRJeNfgtpl9QeaAliILXE=
|
||||
github.com/lightningnetwork/lnd/tor v1.1.0/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64=
|
||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
|
||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY=
|
||||
github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA=
|
||||
|
@ -102,12 +102,11 @@ const (
|
||||
// p2p level (BOLT-0008).
|
||||
KeyFamilyNodeKey KeyFamily = 6
|
||||
|
||||
// KeyFamilyStaticBackup is the family of keys that will be used to
|
||||
// derive keys that we use to encrypt and decrypt our set of static
|
||||
// backups. These backups may either be stored within watch towers for
|
||||
// a payment, or self stored on disk in a single file containing all
|
||||
// the static channel backups.
|
||||
KeyFamilyStaticBackup KeyFamily = 7
|
||||
// KeyFamilyBaseEncryption is the family of keys that will be used to
|
||||
// derive keys that we use to encrypt and decrypt any general blob data
|
||||
// like static channel backups and the TLS private key. Often used when
|
||||
// encrypting files on disk.
|
||||
KeyFamilyBaseEncryption KeyFamily = 7
|
||||
|
||||
// KeyFamilyTowerSession is the family of keys that will be used to
|
||||
// derive session keys when negotiating sessions with watchtowers. The
|
||||
@ -133,7 +132,7 @@ var VersionZeroKeyFamilies = []KeyFamily{
|
||||
KeyFamilyDelayBase,
|
||||
KeyFamilyRevocationRoot,
|
||||
KeyFamilyNodeKey,
|
||||
KeyFamilyStaticBackup,
|
||||
KeyFamilyBaseEncryption,
|
||||
KeyFamilyTowerSession,
|
||||
KeyFamilyTowerID,
|
||||
}
|
||||
|
@ -13,5 +13,6 @@ type Tor struct {
|
||||
V2 bool `long:"v2" description:"Automatically set up a v2 onion service to listen for inbound connections"`
|
||||
V3 bool `long:"v3" description:"Automatically set up a v3 onion service to listen for inbound connections"`
|
||||
PrivateKeyPath string `long:"privatekeypath" description:"The path to the private key of the onion service being created"`
|
||||
EncryptKey bool `long:"encryptkey" description:"Encrypts the Tor private key file on disk"`
|
||||
WatchtowerKeyPath string `long:"watchtowerkeypath" description:"The path to the private key of the watchtower onion service being created"`
|
||||
}
|
||||
|
2
lnd.go
2
lnd.go
@ -474,6 +474,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
|
||||
if torController != nil {
|
||||
wtCfg.TorController = torController
|
||||
wtCfg.WatchtowerKeyPath = cfg.Tor.WatchtowerKeyPath
|
||||
wtCfg.EncryptKey = cfg.Tor.EncryptKey
|
||||
wtCfg.KeyRing = activeChainControl.KeyRing
|
||||
|
||||
switch {
|
||||
case cfg.Tor.V2:
|
||||
|
139
lnencrypt/crypto.go
Normal file
139
lnencrypt/crypto.go
Normal file
@ -0,0 +1,139 @@
|
||||
package lnencrypt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 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 := ioutil.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
|
||||
}
|
@ -1,41 +1,12 @@
|
||||
package chanbackup
|
||||
package lnencrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
testWalletPrivKey = []byte{
|
||||
0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf,
|
||||
0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9,
|
||||
0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f,
|
||||
0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90,
|
||||
}
|
||||
)
|
||||
|
||||
type mockKeyRing struct {
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (m *mockKeyRing) DeriveNextKey(keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) {
|
||||
return keychain.KeyDescriptor{}, nil
|
||||
}
|
||||
func (m *mockKeyRing) DeriveKey(keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error) {
|
||||
if m.fail {
|
||||
return keychain.KeyDescriptor{}, fmt.Errorf("fail")
|
||||
}
|
||||
|
||||
_, pub := btcec.PrivKeyFromBytes(testWalletPrivKey)
|
||||
return keychain.KeyDescriptor{
|
||||
PubKey: pub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestEncryptDecryptPayload tests that given a static key, we're able to
|
||||
// properly decrypt and encrypted payload. We also test that we'll reject a
|
||||
// ciphertext that has been modified.
|
||||
@ -81,15 +52,16 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
keyRing := &mockKeyRing{}
|
||||
keyRing := &MockKeyRing{}
|
||||
|
||||
for i, payloadCase := range payloadCases {
|
||||
var cipherBuffer bytes.Buffer
|
||||
encrypter, err := KeyRingEncrypter(keyRing)
|
||||
require.NoError(t, err)
|
||||
|
||||
// First, we'll encrypt the passed payload with our scheme.
|
||||
payloadReader := bytes.NewBuffer(payloadCase.plaintext)
|
||||
err := encryptPayloadToWriter(
|
||||
*payloadReader, &cipherBuffer, keyRing,
|
||||
err = encrypter.EncryptPayloadToWriter(
|
||||
payloadCase.plaintext, &cipherBuffer,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable encrypt paylaod: %v", err)
|
||||
@ -107,7 +79,9 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
||||
cipherBuffer.Write(cipherText)
|
||||
}
|
||||
|
||||
plaintext, err := decryptPayloadFromReader(&cipherBuffer, keyRing)
|
||||
plaintext, err := encrypter.DecryptPayloadFromReader(
|
||||
&cipherBuffer,
|
||||
)
|
||||
|
||||
switch {
|
||||
// If this was meant to be a valid decryption, but we failed,
|
||||
@ -131,26 +105,13 @@ func TestEncryptDecryptPayload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidKeyEncryption tests that encryption fails if we're unable to
|
||||
// obtain a valid key.
|
||||
func TestInvalidKeyEncryption(t *testing.T) {
|
||||
// TestInvalidKeyGeneration tests that key generation fails when deriving the
|
||||
// key fails.
|
||||
func TestInvalidKeyGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
err := encryptPayloadToWriter(b, &b, &mockKeyRing{true})
|
||||
_, err := KeyRingEncrypter(&MockKeyRing{true})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error due to fail key gen")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidKeyDecrytion tests that decryption fails if we're unable to
|
||||
// obtain a valid key.
|
||||
func TestInvalidKeyDecrytion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var b bytes.Buffer
|
||||
_, err := decryptPayloadFromReader(&b, &mockKeyRing{true})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error due to fail key gen")
|
||||
t.Fatal("expected error due to fail key gen")
|
||||
}
|
||||
}
|
40
lnencrypt/test_utils.go
Normal file
40
lnencrypt/test_utils.go
Normal file
@ -0,0 +1,40 @@
|
||||
package lnencrypt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
)
|
||||
|
||||
var (
|
||||
testWalletPrivKey = []byte{
|
||||
0x2b, 0xd8, 0x06, 0xc9, 0x7f, 0x0e, 0x00, 0xaf,
|
||||
0x1a, 0x1f, 0xc3, 0x32, 0x8f, 0xa7, 0x63, 0xa9,
|
||||
0x26, 0x97, 0x23, 0xc8, 0xdb, 0x8f, 0xac, 0x4f,
|
||||
0x93, 0xaf, 0x71, 0xdb, 0x18, 0x6d, 0x6e, 0x90,
|
||||
}
|
||||
)
|
||||
|
||||
type MockKeyRing struct {
|
||||
Fail bool
|
||||
}
|
||||
|
||||
func (m *MockKeyRing) DeriveNextKey(
|
||||
keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) {
|
||||
|
||||
return keychain.KeyDescriptor{}, nil
|
||||
}
|
||||
|
||||
func (m *MockKeyRing) DeriveKey(
|
||||
keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error) {
|
||||
|
||||
if m.Fail {
|
||||
return keychain.KeyDescriptor{}, fmt.Errorf("fail")
|
||||
}
|
||||
|
||||
_, pub := btcec.PrivKeyFromBytes(testWalletPrivKey)
|
||||
return keychain.KeyDescriptor{
|
||||
PubKey: pub,
|
||||
}, nil
|
||||
}
|
@ -935,6 +935,8 @@ litecoin.node=ltcd
|
||||
;The path to the private key of the watchtower onion service being created
|
||||
; tor.watchtowerkeypath=/other/path/
|
||||
|
||||
; Instructs lnd to encrypt the private key using the wallet's seed.
|
||||
; tor.encryptkey=true
|
||||
|
||||
[watchtower]
|
||||
|
||||
|
11
server.go
11
server.go
@ -47,6 +47,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/lnpeer"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
@ -2766,13 +2767,21 @@ func (s *server) createNewHiddenService() error {
|
||||
listenPorts = append(listenPorts, port)
|
||||
}
|
||||
|
||||
encrypter, err := lnencrypt.KeyRingEncrypter(s.cc.KeyRing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Once the port mapping has been set, we can go ahead and automatically
|
||||
// create our onion service. The service's private key will be saved to
|
||||
// disk in order to regain access to this service when restarting `lnd`.
|
||||
onionCfg := tor.AddOnionConfig{
|
||||
VirtualPort: defaultPeerPort,
|
||||
TargetPorts: listenPorts,
|
||||
Store: tor.NewOnionFile(s.cfg.Tor.PrivateKeyPath, 0600),
|
||||
Store: tor.NewOnionFile(
|
||||
s.cfg.Tor.PrivateKeyPath, 0600, s.cfg.Tor.EncryptKey,
|
||||
encrypter,
|
||||
),
|
||||
}
|
||||
|
||||
switch {
|
||||
|
@ -98,6 +98,12 @@ type Config struct {
|
||||
// for a watchtower hidden service should be stored.
|
||||
WatchtowerKeyPath string
|
||||
|
||||
// EncryptKey will encrypt the Tor private key on disk.
|
||||
EncryptKey bool
|
||||
|
||||
// KeyRing is the KeyRing to use when encrypting the Tor private key.
|
||||
KeyRing keychain.KeyRing
|
||||
|
||||
// Type specifies the hidden service type (V2 or V3) that the watchtower
|
||||
// will create.
|
||||
Type tor.OnionType
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd/brontide"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/tor"
|
||||
"github.com/lightningnetwork/lnd/watchtower/lookout"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtserver"
|
||||
@ -163,14 +164,22 @@ func (w *Standalone) createNewHiddenService() error {
|
||||
listenPorts = append(listenPorts, port)
|
||||
}
|
||||
|
||||
encrypter, err := lnencrypt.KeyRingEncrypter(w.cfg.KeyRing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Once we've created the port mapping, we can automatically create the
|
||||
// hidden service. The service's private key will be saved on disk in order
|
||||
// to persistently have access to this hidden service across restarts.
|
||||
onionCfg := tor.AddOnionConfig{
|
||||
VirtualPort: DefaultPeerPort,
|
||||
TargetPorts: listenPorts,
|
||||
Store: tor.NewOnionFile(w.cfg.WatchtowerKeyPath, 0600),
|
||||
Type: w.cfg.Type,
|
||||
Store: tor.NewOnionFile(
|
||||
w.cfg.WatchtowerKeyPath, 0600, w.cfg.EncryptKey,
|
||||
encrypter,
|
||||
),
|
||||
Type: w.cfg.Type,
|
||||
}
|
||||
|
||||
addr, err := w.cfg.TorController.AddOnion(onionCfg)
|
||||
|
Loading…
Reference in New Issue
Block a user