diff --git a/chanbackup/backupfile_test.go b/chanbackup/backupfile_test.go index 14747f47b..db379b6f2 100644 --- a/chanbackup/backupfile_test.go +++ b/chanbackup/backupfile_test.go @@ -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. diff --git a/chanbackup/crypto.go b/chanbackup/crypto.go deleted file mode 100644 index 8fdb46f68..000000000 --- a/chanbackup/crypto.go +++ /dev/null @@ -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 -} diff --git a/chanbackup/multi.go b/chanbackup/multi.go index e90bd613e..40e09f451 100644 --- a/chanbackup/multi.go +++ b/chanbackup/multi.go @@ -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 } diff --git a/chanbackup/multi_test.go b/chanbackup/multi_test.go index 0881be60a..a296f914e 100644 --- a/chanbackup/multi_test.go +++ b/chanbackup/multi_test.go @@ -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() diff --git a/chanbackup/pubsub_test.go b/chanbackup/pubsub_test.go index 0f7207e6c..9586605ca 100644 --- a/chanbackup/pubsub_test.go +++ b/chanbackup/pubsub_test.go @@ -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) diff --git a/chanbackup/recover_test.go b/chanbackup/recover_test.go index 12a94a773..e3e607737 100644 --- a/chanbackup/recover_test.go +++ b/chanbackup/recover_test.go @@ -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, ) diff --git a/chanbackup/single.go b/chanbackup/single.go index 6aee903b7..a9f0bea1f 100644 --- a/chanbackup/single.go +++ b/chanbackup/single.go @@ -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 } diff --git a/chanbackup/single_test.go b/chanbackup/single_test.go index 2f0818096..ab418e190 100644 --- a/chanbackup/single_test.go +++ b/chanbackup/single_test.go @@ -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 diff --git a/docs/configuring_tor.md b/docs/configuring_tor.md index 1c3c03fe0..a21099c98 100644 --- a/docs/configuring_tor.md +++ b/docs/configuring_tor.md @@ -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. \ No newline at end of file diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 5fda3d4bd..59869af69 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -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 diff --git a/go.mod b/go.mod index 0821fad2e..21973764d 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 343dc5639..880aaf938 100644 --- a/go.sum +++ b/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= diff --git a/keychain/derivation.go b/keychain/derivation.go index 2dea6047b..21996c650 100644 --- a/keychain/derivation.go +++ b/keychain/derivation.go @@ -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, } diff --git a/lncfg/tor.go b/lncfg/tor.go index be58e7ea5..2dcd18c93 100644 --- a/lncfg/tor.go +++ b/lncfg/tor.go @@ -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"` } diff --git a/lnd.go b/lnd.go index 05dbb8696..27ff7bb9d 100644 --- a/lnd.go +++ b/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: diff --git a/lnencrypt/crypto.go b/lnencrypt/crypto.go new file mode 100644 index 000000000..5035e0c34 --- /dev/null +++ b/lnencrypt/crypto.go @@ -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 +} diff --git a/chanbackup/crypto_test.go b/lnencrypt/crypto_test.go similarity index 60% rename from chanbackup/crypto_test.go rename to lnencrypt/crypto_test.go index b5678a528..4a41af328 100644 --- a/chanbackup/crypto_test.go +++ b/lnencrypt/crypto_test.go @@ -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") } } diff --git a/lnencrypt/test_utils.go b/lnencrypt/test_utils.go new file mode 100644 index 000000000..1dd381eb3 --- /dev/null +++ b/lnencrypt/test_utils.go @@ -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 +} diff --git a/sample-lnd.conf b/sample-lnd.conf index 0320a76c9..4ba5d3c45 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -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] diff --git a/server.go b/server.go index 3d4a3d640..62527d478 100644 --- a/server.go +++ b/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 { diff --git a/watchtower/config.go b/watchtower/config.go index 874d58416..f79de9cac 100644 --- a/watchtower/config.go +++ b/watchtower/config.go @@ -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 diff --git a/watchtower/standalone.go b/watchtower/standalone.go index fb1f365be..c652d9c76 100644 --- a/watchtower/standalone.go +++ b/watchtower/standalone.go @@ -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)