tor: Add option to encrypt Tor private key

This commit lays the groundwork for enabling the option of encrypting a Tor private key on disk, and removes the onion type parameters from the OnionStore interface methods, since they are unused.
This commit is contained in:
Orbital
2022-05-09 13:20:39 -05:00
parent 9f013f5058
commit 177f365538
3 changed files with 186 additions and 64 deletions

View File

@@ -1,13 +1,22 @@
package tor package tor
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
) )
var ( var (
// ErrEncryptedTorPrivateKey is thrown when a tor private key is
// encrypted, but the user requested an unencrypted key.
ErrEncryptedTorPrivateKey = errors.New("it appears the Tor private key " +
"is encrypted but you didn't pass the --tor.encryptkey flag. " +
"Please restart lnd with the --tor.encryptkey flag or delete " +
"the Tor key file for regeneration")
// ErrNoPrivateKey is an error returned by the OnionStore.PrivateKey // ErrNoPrivateKey is an error returned by the OnionStore.PrivateKey
// method when a private key hasn't yet been stored. // method when a private key hasn't yet been stored.
ErrNoPrivateKey = errors.New("private key not found") ErrNoPrivateKey = errors.New("private key not found")
@@ -24,19 +33,34 @@ const (
V3 V3
) )
const (
// V2KeyParam is a parameter that Tor accepts for a new V2 service.
V2KeyParam = "RSA1024"
// V3KeyParam is a parameter that Tor accepts for a new V3 service.
V3KeyParam = "ED25519-V3"
)
// OnionStore is a store containing information about a particular onion // OnionStore is a store containing information about a particular onion
// service. // service.
type OnionStore interface { type OnionStore interface {
// StorePrivateKey stores the private key according to the // StorePrivateKey stores the private key according to the
// implementation of the OnionStore interface. // implementation of the OnionStore interface.
StorePrivateKey(OnionType, []byte) error StorePrivateKey([]byte) error
// PrivateKey retrieves a stored private key. If it is not found, then // PrivateKey retrieves a stored private key. If it is not found, then
// ErrNoPrivateKey should be returned. // ErrNoPrivateKey should be returned.
PrivateKey(OnionType) ([]byte, error) PrivateKey() ([]byte, error)
// DeletePrivateKey securely removes the private key from the store. // DeletePrivateKey securely removes the private key from the store.
DeletePrivateKey(OnionType) error DeletePrivateKey() error
}
// EncrypterDecrypter is used for encrypting and decrypting the onion service
// private key.
type EncrypterDecrypter interface {
EncryptPayloadToWriter([]byte, io.Writer) error
DecryptPayloadFromReader(io.Reader) ([]byte, error)
} }
// OnionFile is a file-based implementation of the OnionStore interface that // OnionFile is a file-based implementation of the OnionStore interface that
@@ -44,6 +68,8 @@ type OnionStore interface {
type OnionFile struct { type OnionFile struct {
privateKeyPath string privateKeyPath string
privateKeyPerm os.FileMode privateKeyPerm os.FileMode
encryptKey bool
encrypter EncrypterDecrypter
} }
// A compile-time constraint to ensure OnionFile satisfies the OnionStore // A compile-time constraint to ensure OnionFile satisfies the OnionStore
@@ -52,31 +78,85 @@ var _ OnionStore = (*OnionFile)(nil)
// NewOnionFile creates a file-based implementation of the OnionStore interface // NewOnionFile creates a file-based implementation of the OnionStore interface
// to store an onion service's private key. // to store an onion service's private key.
func NewOnionFile(privateKeyPath string, func NewOnionFile(privateKeyPath string, privateKeyPerm os.FileMode,
privateKeyPerm os.FileMode) *OnionFile { encryptKey bool, encrypter EncrypterDecrypter) *OnionFile {
return &OnionFile{ return &OnionFile{
privateKeyPath: privateKeyPath, privateKeyPath: privateKeyPath,
privateKeyPerm: privateKeyPerm, privateKeyPerm: privateKeyPerm,
encryptKey: encryptKey,
encrypter: encrypter,
} }
} }
// StorePrivateKey stores the private key at its expected path. // StorePrivateKey stores the private key at its expected path. It also
func (f *OnionFile) StorePrivateKey(_ OnionType, privateKey []byte) error { // encrypts the key before storing it if requested.
return ioutil.WriteFile(f.privateKeyPath, privateKey, f.privateKeyPerm) func (f *OnionFile) StorePrivateKey(privateKey []byte) error {
privateKeyContent := privateKey
if f.encryptKey {
var b bytes.Buffer
err := f.encrypter.EncryptPayloadToWriter(
privateKey, &b,
)
if err != nil {
return err
}
privateKeyContent = b.Bytes()
}
err := ioutil.WriteFile(
f.privateKeyPath, privateKeyContent, f.privateKeyPerm,
)
if err != nil {
return fmt.Errorf("unable to write private key "+
"to file: %v", err)
}
return nil
} }
// PrivateKey retrieves the private key from its expected path. If the file // PrivateKey retrieves the private key from its expected path. If the file does
// does not exist, then ErrNoPrivateKey is returned. // not exist, then ErrNoPrivateKey is returned.
func (f *OnionFile) PrivateKey(_ OnionType) ([]byte, error) { func (f *OnionFile) PrivateKey() ([]byte, error) {
if _, err := os.Stat(f.privateKeyPath); os.IsNotExist(err) { _, err := os.Stat(f.privateKeyPath)
if err != nil && errors.Is(err, os.ErrNotExist) {
return nil, ErrNoPrivateKey return nil, ErrNoPrivateKey
} }
return ioutil.ReadFile(f.privateKeyPath)
// Try to read the Tor private key to pass into the AddOnion call.
privateKeyContent, err := ioutil.ReadFile(f.privateKeyPath)
if err != nil {
return nil, err
}
// If the privateKey starts with either v2 or v3 key params then
// it's likely not encrypted and we can return the data as is.
if bytes.HasPrefix(privateKeyContent, []byte(V2KeyParam)) ||
bytes.HasPrefix(privateKeyContent, []byte(V3KeyParam)) {
return privateKeyContent, nil
}
// If the privateKeyContent is encrypted but --tor.encryptkey
// wasn't set we return an error.
if !f.encryptKey {
return nil, ErrEncryptedTorPrivateKey
}
// Attempt to decrypt the key.
reader := bytes.NewReader(privateKeyContent)
privateKeyContent, err = f.encrypter.DecryptPayloadFromReader(
reader,
)
if err != nil {
return nil, err
}
return privateKeyContent, nil
} }
// DeletePrivateKey removes the file containing the private key. // DeletePrivateKey removes the file containing the private key.
func (f *OnionFile) DeletePrivateKey(_ OnionType) error { func (f *OnionFile) DeletePrivateKey() error {
return os.Remove(f.privateKeyPath) return os.Remove(f.privateKeyPath)
} }
@@ -117,13 +197,13 @@ func (c *Controller) prepareKeyparam(cfg AddOnionConfig) (string, error) {
switch cfg.Type { switch cfg.Type {
// TODO(yy): drop support for v2. // TODO(yy): drop support for v2.
case V2: case V2:
keyParam = "NEW:RSA1024" keyParam = "NEW:" + V2KeyParam
case V3: case V3:
keyParam = "NEW:ED25519-V3" keyParam = "NEW:" + V3KeyParam
} }
if cfg.Store != nil { if cfg.Store != nil {
privateKey, err := cfg.Store.PrivateKey(cfg.Type) privateKey, err := cfg.Store.PrivateKey()
switch err { switch err {
// Proceed to request a new onion service. // Proceed to request a new onion service.
case ErrNoPrivateKey: case ErrNoPrivateKey:
@@ -142,11 +222,13 @@ func (c *Controller) prepareKeyparam(cfg AddOnionConfig) (string, error) {
// prepareAddOnion constructs a cmd command string based on the specified // prepareAddOnion constructs a cmd command string based on the specified
// config. // config.
func (c *Controller) prepareAddOnion(cfg AddOnionConfig) (string, error) { func (c *Controller) prepareAddOnion(cfg AddOnionConfig) (string, string,
error) {
// Create the keyParam. // Create the keyParam.
keyParam, err := c.prepareKeyparam(cfg) keyParam, err := c.prepareKeyparam(cfg)
if err != nil { if err != nil {
return "", err return "", "", err
} }
// Now, we'll create a mapping from the virtual port to each target // Now, we'll create a mapping from the virtual port to each target
@@ -178,7 +260,7 @@ func (c *Controller) prepareAddOnion(cfg AddOnionConfig) (string, error) {
// await its response. // await its response.
cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam) cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam)
return cmd, nil return cmd, keyParam, nil
} }
// AddOnion creates an ephemeral onion service and returns its onion address. // AddOnion creates an ephemeral onion service and returns its onion address.
@@ -202,7 +284,7 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
} }
// Construct the cmd command. // Construct the cmd command.
cmd, err := c.prepareAddOnion(cfg) cmd, keyParam, err := c.prepareAddOnion(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -234,11 +316,19 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
return nil, errors.New("service id not found in reply") return nil, errors.New("service id not found in reply")
} }
// If a new onion service was created and an onion store was provided, // If a new onion service was created, use the new private key for
// we'll store its private key to disk in the event that it needs to be // storage.
// recreated later on. newPrivateKey, ok := replyParams["PrivateKey"]
if privateKey, ok := replyParams["PrivateKey"]; cfg.Store != nil && ok { if ok {
err := cfg.Store.StorePrivateKey(cfg.Type, []byte(privateKey)) keyParam = newPrivateKey
}
// If an onion store was provided and a key return wasn't requested,
// we'll store its private key to disk in the event that it needs to
// be recreated later on. We write the private key to disk every time
// in case the user toggles the --tor.encryptkey flag.
if cfg.Store != nil {
err := cfg.Store.StorePrivateKey([]byte(keyParam))
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to write private key "+ return nil, fmt.Errorf("unable to write private key "+
"to file: %v", err) "to file: %v", err)
@@ -254,6 +344,7 @@ func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
return &OnionAddr{ return &OnionAddr{
OnionService: serviceID + ".onion", OnionService: serviceID + ".onion",
Port: cfg.VirtualPort, Port: cfg.VirtualPort,
PrivateKey: keyParam,
}, nil }, nil
} }

View File

@@ -1,8 +1,8 @@
package tor package tor
import ( import (
"bytes"
"errors" "errors"
"io"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -10,40 +10,61 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// TestOnionFile tests that the OnionFile implementation of the OnionStore var (
privateKey = []byte("RSA1024 hide_me_plz")
anotherKey = []byte("another_key")
)
// TestOnionFile tests that the File implementation of the OnionStore
// interface behaves as expected. // interface behaves as expected.
func TestOnionFile(t *testing.T) { func TestOnionFile(t *testing.T) {
t.Parallel() t.Parallel()
privateKey := []byte("hide_me_plz") tempDir := t.TempDir()
privateKeyPath := filepath.Join(t.TempDir(), "secret") privateKeyPath := filepath.Join(tempDir, "secret")
mockEncrypter := MockEncrypter{}
// Create a new file-based onion store. A private key should not exist // Create a new file-based onion store. A private key should not exist
// yet. // yet.
onionFile := NewOnionFile(privateKeyPath, 0600) onionFile := NewOnionFile(
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey { privateKeyPath, 0600, false, mockEncrypter,
t.Fatalf("expected ErrNoPrivateKey, got \"%v\"", err) )
} _, err := onionFile.PrivateKey()
require.ErrorIs(t, err, ErrNoPrivateKey)
// Store the private key and ensure what's stored matches. // Store the private key and ensure what's stored matches.
if err := onionFile.StorePrivateKey(V2, privateKey); err != nil { err = onionFile.StorePrivateKey(privateKey)
t.Fatalf("unable to store private key: %v", err) require.NoError(t, err)
}
storePrivateKey, err := onionFile.PrivateKey(V2) storePrivateKey, err := onionFile.PrivateKey()
require.NoError(t, err, "unable to retrieve private key") require.NoError(t, err)
if !bytes.Equal(storePrivateKey, privateKey) { require.Equal(t, storePrivateKey, privateKey)
t.Fatalf("expected private key \"%v\", got \"%v\"",
string(privateKey), string(storePrivateKey))
}
// Finally, delete the private key. We should no longer be able to // Finally, delete the private key. We should no longer be able to
// retrieve it. // retrieve it.
if err := onionFile.DeletePrivateKey(V2); err != nil { err = onionFile.DeletePrivateKey()
t.Fatalf("unable to delete private key: %v", err) require.NoError(t, err)
}
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey { _, err = onionFile.PrivateKey()
t.Fatal("found deleted private key") require.ErrorIs(t, err, ErrNoPrivateKey)
}
// Create a new file-based onion store that encrypts the key this time
// to ensure that an encrypted key is properly handled.
encryptedOnionFile := NewOnionFile(
privateKeyPath, 0600, true, mockEncrypter,
)
err = encryptedOnionFile.StorePrivateKey(privateKey)
require.NoError(t, err)
storedPrivateKey, err := encryptedOnionFile.PrivateKey()
require.NoError(t, err, "unable to retrieve encrypted private key")
// Check that PrivateKey returns anotherKey, to make sure the mock
// decrypter is actually called.
require.Equal(t, storedPrivateKey, anotherKey)
err = encryptedOnionFile.DeletePrivateKey()
require.NoError(t, err)
} }
// TestPrepareKeyParam checks that the key param is created as expected. // TestPrepareKeyParam checks that the key param is created as expected.
@@ -63,7 +84,7 @@ func TestPrepareKeyParam(t *testing.T) {
// Create a mock store which returns the test private key. // Create a mock store which returns the test private key.
store := &mockStore{} store := &mockStore{}
store.On("PrivateKey", cfg.Type).Return(testKey, nil) store.On("PrivateKey").Return(testKey, nil)
// Check that the test private is returned. // Check that the test private is returned.
cfg = AddOnionConfig{Type: V3, Store: store} cfg = AddOnionConfig{Type: V3, Store: store}
@@ -75,7 +96,7 @@ func TestPrepareKeyParam(t *testing.T) {
// Create a mock store which returns ErrNoPrivateKey. // Create a mock store which returns ErrNoPrivateKey.
store = &mockStore{} store = &mockStore{}
store.On("PrivateKey", cfg.Type).Return(nil, ErrNoPrivateKey) store.On("PrivateKey").Return(nil, ErrNoPrivateKey)
// Check that the V3 keyParam is returned. // Check that the V3 keyParam is returned.
cfg = AddOnionConfig{Type: V3, Store: store} cfg = AddOnionConfig{Type: V3, Store: store}
@@ -87,7 +108,7 @@ func TestPrepareKeyParam(t *testing.T) {
// Create a mock store which returns an dummy error. // Create a mock store which returns an dummy error.
store = &mockStore{} store = &mockStore{}
store.On("PrivateKey", cfg.Type).Return(nil, dummyErr) store.On("PrivateKey").Return(nil, dummyErr)
// Check that an error is returned. // Check that an error is returned.
cfg = AddOnionConfig{Type: V3, Store: store} cfg = AddOnionConfig{Type: V3, Store: store}
@@ -158,14 +179,14 @@ func TestPrepareAddOnion(t *testing.T) {
tc := tc tc := tc
if tc.cfg.Store != nil { if tc.cfg.Store != nil {
store.On("PrivateKey", tc.cfg.Type).Return( store.On("PrivateKey").Return(
testKey, tc.expectedErr, testKey, tc.expectedErr,
) )
} }
controller := NewController("", tc.targetIPAddress, "") controller := NewController("", tc.targetIPAddress, "")
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
cmd, err := controller.prepareAddOnion(tc.cfg) cmd, _, err := controller.prepareAddOnion(tc.cfg)
require.Equal(t, tc.expectedErr, err) require.Equal(t, tc.expectedErr, err)
require.Equal(t, tc.expectedCmd, cmd) require.Equal(t, tc.expectedCmd, cmd)
@@ -184,20 +205,27 @@ type mockStore struct {
// interface. // interface.
var _ OnionStore = (*mockStore)(nil) var _ OnionStore = (*mockStore)(nil)
func (m *mockStore) StorePrivateKey(ot OnionType, key []byte) error { func (m *mockStore) StorePrivateKey(key []byte) error {
args := m.Called(ot, key) args := m.Called(key)
return args.Error(0) return args.Error(0)
} }
func (m *mockStore) PrivateKey(ot OnionType) ([]byte, error) { func (m *mockStore) PrivateKey() ([]byte, error) {
args := m.Called(ot) args := m.Called()
if args.Get(0) == nil { return []byte("hide_me_plz"), args.Error(1)
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
} }
func (m *mockStore) DeletePrivateKey(ot OnionType) error { func (m *mockStore) DeletePrivateKey() error {
args := m.Called(ot) args := m.Called()
return args.Error(0) return args.Error(0)
} }
type MockEncrypter struct{}
func (m MockEncrypter) EncryptPayloadToWriter(_ []byte, _ io.Writer) error {
return nil
}
func (m MockEncrypter) DecryptPayloadFromReader(_ io.Reader) ([]byte, error) {
return anotherKey, nil
}

View File

@@ -45,6 +45,9 @@ type OnionAddr struct {
// Port is the port of the onion address. // Port is the port of the onion address.
Port int Port int
// PrivateKey is the onion address' private key.
PrivateKey string
} }
// A compile-time check to ensure that OnionAddr implements the net.Addr // A compile-time check to ensure that OnionAddr implements the net.Addr