From f3bd2227faea145e01cd1ed3ddb1bac4636a07da Mon Sep 17 00:00:00 2001 From: Orbital Date: Wed, 2 Feb 2022 15:39:29 -0600 Subject: [PATCH 1/5] keychain: change KeyFamilyStaticBackup name to reflect its new, broader role --- chanbackup/crypto.go | 6 +++--- chanbackup/single.go | 8 ++++---- keychain/derivation.go | 13 ++++++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/chanbackup/crypto.go b/chanbackup/crypto.go index 8fdb46f685..fa7d862ca8 100644 --- a/chanbackup/crypto.go +++ b/chanbackup/crypto.go @@ -15,15 +15,15 @@ import ( // 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 +// 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.KeyFamilyStaticBackup, + Family: keychain.KeyFamilyBaseEncryption, Index: 0, } diff --git a/chanbackup/single.go b/chanbackup/single.go index 6aee903b7b..f0c3a6f83a 100644 --- a/chanbackup/single.go +++ b/chanbackup/single.go @@ -333,10 +333,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 { diff --git a/keychain/derivation.go b/keychain/derivation.go index 2dea6047b0..21996c6509 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, } From e0fc5bb234c12d3418f242508a15f2f06c33ceac Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Sat, 11 Jul 2020 22:29:00 -0500 Subject: [PATCH 2/5] lnencrypt: Moves the crypto functions in the chanbackup package into its own package called lnencrypt The functions inside of the crypto.go file in chanbackup (like EncryptPayloadToWriter and DecryptPayloadFromReader) can be used by a lot of things outside of just the chanbackup package. We can't just reference them directly from the chanbackup package because it's likely that it would generate circular dependencies. Therefore we need to move these functions into their own package to be referenced by chanbackup and whatever new functionality that needs them --- chanbackup/backupfile_test.go | 3 +- chanbackup/multi.go | 14 +++- chanbackup/multi_test.go | 12 ++- chanbackup/pubsub_test.go | 7 +- chanbackup/recover_test.go | 9 ++- chanbackup/single.go | 13 +++- chanbackup/single_test.go | 12 +-- {chanbackup => lnencrypt}/crypto.go | 97 ++++++++++++------------ {chanbackup => lnencrypt}/crypto_test.go | 69 ++++------------- lnencrypt/test_utils.go | 40 ++++++++++ 10 files changed, 152 insertions(+), 124 deletions(-) rename {chanbackup => lnencrypt}/crypto.go (52%) rename {chanbackup => lnencrypt}/crypto_test.go (60%) create mode 100644 lnencrypt/test_utils.go diff --git a/chanbackup/backupfile_test.go b/chanbackup/backupfile_test.go index 14747f47b3..db379b6f28 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/multi.go b/chanbackup/multi.go index e90bd613e4..40e09f4518 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 0881be60ab..a296f914ea 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 0f7207e6cd..9586605cac 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 12a94a7733..e3e607737b 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 f0c3a6f83a..a9f0bea1f6 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" ) @@ -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 2f08180961..ab418e1901 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/chanbackup/crypto.go b/lnencrypt/crypto.go similarity index 52% rename from chanbackup/crypto.go rename to lnencrypt/crypto.go index fa7d862ca8..5035e0c344 100644 --- a/chanbackup/crypto.go +++ b/lnencrypt/crypto.go @@ -1,7 +1,6 @@ -package chanbackup +package lnencrypt import ( - "bytes" "crypto/rand" "crypto/sha256" "fmt" @@ -12,8 +11,6 @@ import ( "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 payloads. We use this to then // derive the actual key that we'll use for encryption. We do this @@ -27,12 +24,32 @@ var baseEncryptionKeyLoc = keychain.KeyLocator{ 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) { +// 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, @@ -47,33 +64,23 @@ func genEncryptionKey(keyRing keychain.KeyRing) ([]byte, error) { // TODO(roasbeef): throw back in ECDH? - return encryptionKey[:], nil + return &Encrypter{ + encryptionKey: 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 - } +// 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(encryptionKey) + cipher, err := chacha20poly1305.NewX(e.encryptionKey) if err != nil { return err } @@ -84,7 +91,7 @@ func encryptPayloadToWriter(payload bytes.Buffer, w io.Writer, // Finally, we encrypted the final payload, and write out our // ciphertext with nonce pre-pended. - ciphertext := cipher.Seal(nil, nonce[:], payload.Bytes(), nonce[:]) + ciphertext := cipher.Seal(nil, nonce[:], payload, nonce[:]) if _, err := w.Write(nonce[:]); err != nil { return err @@ -96,38 +103,30 @@ func encryptPayloadToWriter(payload bytes.Buffer, w io.Writer, return nil } -// decryptPayloadFromReader attempts to decrypt the encrypted bytes within the +// 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 - } +// 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. - packedBackup, err := ioutil.ReadAll(payload) + packedPayload, err := ioutil.ReadAll(payload) if err != nil { return nil, err } - if len(packedBackup) < chacha20poly1305.NonceSizeX { + if len(packedPayload) < 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:] + 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 serialized the - // SCB. - cipher, err := chacha20poly1305.NewX(encryptionKey) + // ahead and decrypt the final blob so we can properly serialize. + cipher, err := chacha20poly1305.NewX(e.encryptionKey) if err != nil { return nil, err } 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 b5678a5288..4a41af328f 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) { - t.Parallel() - - var b bytes.Buffer - err := encryptPayloadToWriter(b, &b, &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) { +// TestInvalidKeyGeneration tests that key generation fails when deriving the +// key fails. +func TestInvalidKeyGeneration(t *testing.T) { t.Parallel() - var b bytes.Buffer - _, err := decryptPayloadFromReader(&b, &mockKeyRing{true}) + _, err := KeyRingEncrypter(&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 0000000000..1dd381eb38 --- /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 +} From 073c990c75b4f9a5225e3fc7060fe7a1e396746b Mon Sep 17 00:00:00 2001 From: Orbital Date: Tue, 10 May 2022 20:11:19 -0500 Subject: [PATCH 3/5] multi: Add --tor.encryptkey flag functionality to encrypt the Tor private key on disk It's possible that a user might not want the Tor private key to sit on the disk in plaintext (it is a private key after all). So this commit adds a new flag to encrypt the Tor private key on disk using the wallet's seed. When the --tor.encryptkey flag is used, LND will still write the Tor key to the same file, however it will now be encrypted intead of plaintext. This essentially uses the same method to encrypt the Tor private key as is used to encrypt the Static Channel Backup file. --- go.mod | 2 +- go.sum | 2 ++ lncfg/tor.go | 1 + lnd.go | 2 ++ server.go | 11 ++++++++++- watchtower/config.go | 6 ++++++ watchtower/standalone.go | 13 +++++++++++-- 7 files changed, 33 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 0821fad2ed..21973764db 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 343dc56396..880aaf9387 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/lncfg/tor.go b/lncfg/tor.go index be58e7ea5a..2dcd18c931 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 05dbb86964..27ff7bb9df 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/server.go b/server.go index 3d4a3d640e..62527d4785 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 874d584164..f79de9cacb 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 fb1f365be6..c652d9c766 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) From c284574104e9b4f2ad33b7c817979dc51148e748 Mon Sep 17 00:00:00 2001 From: Graham Krizek Date: Sat, 11 Jul 2020 22:49:24 -0500 Subject: [PATCH 4/5] docs: Add a section in configuring_tor about the --tor.encryptkey flag --- docs/configuring_tor.md | 5 +++++ sample-lnd.conf | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/configuring_tor.md b/docs/configuring_tor.md index 1c3c03fe01..a21099c98d 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/sample-lnd.conf b/sample-lnd.conf index 0320a76c91..4ba5d3c453 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] From 14cc7e5892626315349e2227652398a37108f9b8 Mon Sep 17 00:00:00 2001 From: Orbital Date: Tue, 30 Aug 2022 16:36:45 -0500 Subject: [PATCH 5/5] docs: update release notes --- docs/release-notes/release-notes-0.16.0.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 5fda3d4bd4..59869af69f 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