mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
177f365538
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.
394 lines
12 KiB
Go
394 lines
12 KiB
Go
package tor
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
)
|
|
|
|
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
|
|
// method when a private key hasn't yet been stored.
|
|
ErrNoPrivateKey = errors.New("private key not found")
|
|
)
|
|
|
|
// OnionType denotes the type of the onion service.
|
|
type OnionType int
|
|
|
|
const (
|
|
// V2 denotes that the onion service is V2.
|
|
V2 OnionType = iota
|
|
|
|
// V3 denotes that the onion service is 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
|
|
// service.
|
|
type OnionStore interface {
|
|
// StorePrivateKey stores the private key according to the
|
|
// implementation of the OnionStore interface.
|
|
StorePrivateKey([]byte) error
|
|
|
|
// PrivateKey retrieves a stored private key. If it is not found, then
|
|
// ErrNoPrivateKey should be returned.
|
|
PrivateKey() ([]byte, error)
|
|
|
|
// DeletePrivateKey securely removes the private key from the store.
|
|
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
|
|
// stores an onion service's private key.
|
|
type OnionFile struct {
|
|
privateKeyPath string
|
|
privateKeyPerm os.FileMode
|
|
encryptKey bool
|
|
encrypter EncrypterDecrypter
|
|
}
|
|
|
|
// A compile-time constraint to ensure OnionFile satisfies the OnionStore
|
|
// interface.
|
|
var _ OnionStore = (*OnionFile)(nil)
|
|
|
|
// NewOnionFile creates a file-based implementation of the OnionStore interface
|
|
// to store an onion service's private key.
|
|
func NewOnionFile(privateKeyPath string, privateKeyPerm os.FileMode,
|
|
encryptKey bool, encrypter EncrypterDecrypter) *OnionFile {
|
|
|
|
return &OnionFile{
|
|
privateKeyPath: privateKeyPath,
|
|
privateKeyPerm: privateKeyPerm,
|
|
encryptKey: encryptKey,
|
|
encrypter: encrypter,
|
|
}
|
|
}
|
|
|
|
// StorePrivateKey stores the private key at its expected path. It also
|
|
// encrypts the key before storing it if requested.
|
|
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 does
|
|
// not exist, then ErrNoPrivateKey is returned.
|
|
func (f *OnionFile) PrivateKey() ([]byte, error) {
|
|
_, err := os.Stat(f.privateKeyPath)
|
|
if err != nil && errors.Is(err, os.ErrNotExist) {
|
|
return nil, ErrNoPrivateKey
|
|
}
|
|
|
|
// 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.
|
|
func (f *OnionFile) DeletePrivateKey() error {
|
|
return os.Remove(f.privateKeyPath)
|
|
}
|
|
|
|
// AddOnionConfig houses all of the required parameters in order to
|
|
// successfully create a new onion service or restore an existing one.
|
|
type AddOnionConfig struct {
|
|
// Type denotes the type of the onion service that should be created.
|
|
Type OnionType
|
|
|
|
// VirtualPort is the externally reachable port of the onion address.
|
|
VirtualPort int
|
|
|
|
// TargetPorts is the set of ports that the service will be listening
|
|
// on locally. The Tor server will use choose a random port from this
|
|
// set to forward the traffic from the virtual port.
|
|
//
|
|
// NOTE: If nil/empty, the virtual port will be used as the only target
|
|
// port.
|
|
TargetPorts []int
|
|
|
|
// Store is responsible for storing all onion service related
|
|
// information.
|
|
//
|
|
// NOTE: If not specified, then nothing will be stored, making onion
|
|
// services unrecoverable after shutdown.
|
|
Store OnionStore
|
|
}
|
|
|
|
// prepareKeyparam takes a config and prepares the key param to be used inside
|
|
// ADD_ONION.
|
|
func (c *Controller) prepareKeyparam(cfg AddOnionConfig) (string, error) {
|
|
// We'll start off by checking if the store contains an existing
|
|
// private key. If it does not, then we should request the server to
|
|
// create a new onion service and return its private key. Otherwise,
|
|
// we'll request the server to recreate the onion server from our
|
|
// private key.
|
|
var keyParam string
|
|
switch cfg.Type {
|
|
// TODO(yy): drop support for v2.
|
|
case V2:
|
|
keyParam = "NEW:" + V2KeyParam
|
|
case V3:
|
|
keyParam = "NEW:" + V3KeyParam
|
|
}
|
|
|
|
if cfg.Store != nil {
|
|
privateKey, err := cfg.Store.PrivateKey()
|
|
switch err {
|
|
// Proceed to request a new onion service.
|
|
case ErrNoPrivateKey:
|
|
|
|
// Recover the onion service with the private key found.
|
|
case nil:
|
|
keyParam = string(privateKey)
|
|
|
|
default:
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return keyParam, nil
|
|
}
|
|
|
|
// prepareAddOnion constructs a cmd command string based on the specified
|
|
// config.
|
|
func (c *Controller) prepareAddOnion(cfg AddOnionConfig) (string, string,
|
|
error) {
|
|
|
|
// Create the keyParam.
|
|
keyParam, err := c.prepareKeyparam(cfg)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
// Now, we'll create a mapping from the virtual port to each target
|
|
// port. If no target ports were specified, we'll use the virtual port
|
|
// to provide a one-to-one mapping.
|
|
var portParam string
|
|
|
|
// Helper function which appends the correct Port param depending on
|
|
// whether the user chose to use a custom target IP address or not.
|
|
pushPortParam := func(targetPort int) {
|
|
if c.targetIPAddress == "" {
|
|
portParam += fmt.Sprintf("Port=%d,%d ", cfg.VirtualPort,
|
|
targetPort)
|
|
} else {
|
|
portParam += fmt.Sprintf("Port=%d,%s:%d ",
|
|
cfg.VirtualPort, c.targetIPAddress, targetPort)
|
|
}
|
|
}
|
|
|
|
if len(cfg.TargetPorts) == 0 {
|
|
pushPortParam(cfg.VirtualPort)
|
|
} else {
|
|
for _, targetPort := range cfg.TargetPorts {
|
|
pushPortParam(targetPort)
|
|
}
|
|
}
|
|
|
|
// Send the command to create the onion service to the Tor server and
|
|
// await its response.
|
|
cmd := fmt.Sprintf("ADD_ONION %s %s", keyParam, portParam)
|
|
|
|
return cmd, keyParam, nil
|
|
}
|
|
|
|
// AddOnion creates an ephemeral onion service and returns its onion address.
|
|
// Once created, the new onion service will remain active until either,
|
|
// - the onion service is removed via `DEL_ONION`.
|
|
// - the Tor daemon terminates.
|
|
// - the controller connection that originated the `ADD_ONION` is closed.
|
|
//
|
|
// Each connection can only see its own ephemeral services. If a service needs
|
|
// to survive beyond current controller connection, use the "Detach" flag when
|
|
// creating new service via `ADD_ONION`.
|
|
func (c *Controller) AddOnion(cfg AddOnionConfig) (*OnionAddr, error) {
|
|
// Before sending the request to create an onion service to the Tor
|
|
// server, we'll make sure that it supports V3 onion services if that
|
|
// was the type requested.
|
|
// TODO(yy): drop support for v2.
|
|
if cfg.Type == V3 {
|
|
if err := supportsV3(c.version); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Construct the cmd command.
|
|
cmd, keyParam, err := c.prepareAddOnion(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Send the command to create the onion service to the Tor server and
|
|
// await its response.
|
|
_, reply, err := c.sendCommand(cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If successful, the reply from the server should be of the following
|
|
// format, depending on whether a private key has been requested:
|
|
//
|
|
// C: ADD_ONION RSA1024:[Blob Redacted] Port=80,8080
|
|
// S: 250-ServiceID=testonion1234567
|
|
// S: 250 OK
|
|
//
|
|
// C: ADD_ONION NEW:RSA1024 Port=80,8080
|
|
// S: 250-ServiceID=testonion1234567
|
|
// S: 250-PrivateKey=RSA1024:[Blob Redacted]
|
|
// S: 250 OK
|
|
//
|
|
// We're interested in retrieving the service ID, which is the public
|
|
// name of the service, and the private key if requested.
|
|
replyParams := parseTorReply(reply)
|
|
serviceID, ok := replyParams["ServiceID"]
|
|
if !ok {
|
|
return nil, errors.New("service id not found in reply")
|
|
}
|
|
|
|
// If a new onion service was created, use the new private key for
|
|
// storage.
|
|
newPrivateKey, ok := replyParams["PrivateKey"]
|
|
if ok {
|
|
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 {
|
|
return nil, fmt.Errorf("unable to write private key "+
|
|
"to file: %v", err)
|
|
}
|
|
}
|
|
|
|
c.activeServiceID = serviceID
|
|
log.Debugf("serviceID:%s added to tor controller", serviceID)
|
|
|
|
// Finally, we'll return the onion address composed of the service ID,
|
|
// along with the onion suffix, and the port this onion service can be
|
|
// reached at externally.
|
|
return &OnionAddr{
|
|
OnionService: serviceID + ".onion",
|
|
Port: cfg.VirtualPort,
|
|
PrivateKey: keyParam,
|
|
}, nil
|
|
}
|
|
|
|
// DelOnion tells the Tor daemon to remove an onion service, which satisfies
|
|
// either,
|
|
// - the onion service was created on the same control connection as the
|
|
// "DEL_ONION" command.
|
|
// - the onion service was created using the "Detach" flag.
|
|
func (c *Controller) DelOnion(serviceID string) error {
|
|
log.Debugf("removing serviceID:%s from tor controller", serviceID)
|
|
|
|
cmd := fmt.Sprintf("DEL_ONION %s", serviceID)
|
|
|
|
// Send the command to create the onion service to the Tor server and
|
|
// await its response.
|
|
code, _, err := c.sendCommand(cmd)
|
|
|
|
// Tor replies with "250 OK" on success, or a 512 if there are an
|
|
// invalid number of arguments, or a 552 if it doesn't recognize the
|
|
// ServiceID.
|
|
switch code {
|
|
// Replied 250 OK.
|
|
case success:
|
|
return nil
|
|
|
|
// Replied 512 for invalid arguments. This is most likely that the
|
|
// serviceID is not set(empty string).
|
|
case invalidNumOfArguments:
|
|
return fmt.Errorf("invalid arguments: %w", err)
|
|
|
|
// Replied 552, which means either,
|
|
// - the serviceID is invalid.
|
|
// - the onion service is not owned by the current control connection
|
|
// and,
|
|
// - the onion service is not a detached service.
|
|
// In either case, we will ignore the error and log a warning as there
|
|
// not much we can do from the controller side.
|
|
case serviceIDNotRecognized:
|
|
log.Warnf("removing serviceID:%v not found", serviceID)
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("undefined response code: %v, err: %w",
|
|
code, err)
|
|
}
|
|
}
|