Olaoluwa Osuntokun
4 years ago
committed by
GitHub
4 changed files with 271 additions and 145 deletions
@ -0,0 +1,217 @@
|
||||
package tor |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
) |
||||
|
||||
var ( |
||||
// 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 |
||||
) |
||||
|
||||
// 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(OnionType, []byte) error |
||||
|
||||
// PrivateKey retrieves a stored private key. If it is not found, then
|
||||
// ErrNoPrivateKey should be returned.
|
||||
PrivateKey(OnionType) ([]byte, error) |
||||
|
||||
// DeletePrivateKey securely removes the private key from the store.
|
||||
DeletePrivateKey(OnionType) 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 |
||||
} |
||||
|
||||
// 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) *OnionFile { |
||||
return &OnionFile{ |
||||
privateKeyPath: privateKeyPath, |
||||
privateKeyPerm: privateKeyPerm, |
||||
} |
||||
} |
||||
|
||||
// StorePrivateKey stores the private key at its expected path.
|
||||
func (f *OnionFile) StorePrivateKey(_ OnionType, privateKey []byte) error { |
||||
return ioutil.WriteFile(f.privateKeyPath, privateKey, f.privateKeyPerm) |
||||
} |
||||
|
||||
// PrivateKey retrieves the private key from its expected path. If the file does
|
||||
// not exist, then ErrNoPrivateKey is returned.
|
||||
func (f *OnionFile) PrivateKey(_ OnionType) ([]byte, error) { |
||||
if _, err := os.Stat(f.privateKeyPath); os.IsNotExist(err) { |
||||
return nil, ErrNoPrivateKey |
||||
} |
||||
return ioutil.ReadFile(f.privateKeyPath) |
||||
} |
||||
|
||||
// DeletePrivateKey removes the file containing the private key.
|
||||
func (f *OnionFile) DeletePrivateKey(_ OnionType) 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 |
||||
} |
||||
|
||||
// AddOnion creates an onion service and returns its onion address. Once
|
||||
// created, the new onion service will remain active until the connection
|
||||
// between the controller and the Tor server is closed.
|
||||
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.
|
||||
if cfg.Type == V3 { |
||||
if err := supportsV3(c.version); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
// 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 { |
||||
case V2: |
||||
keyParam = "NEW:RSA1024" |
||||
case V3: |
||||
keyParam = "NEW:ED25519-V3" |
||||
} |
||||
|
||||
if cfg.Store != nil { |
||||
privateKey, err := cfg.Store.PrivateKey(cfg.Type) |
||||
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 nil, 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) |
||||
_, 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 and an onion store was provided,
|
||||
// we'll store its private key to disk in the event that it needs to be
|
||||
// recreated later on.
|
||||
if privateKey, ok := replyParams["PrivateKey"]; cfg.Store != nil && ok { |
||||
err := cfg.Store.StorePrivateKey(cfg.Type, []byte(privateKey)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("unable to write private key "+ |
||||
"to file: %v", err) |
||||
} |
||||
} |
||||
|
||||
// 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, |
||||
}, nil |
||||
} |
@ -0,0 +1,51 @@
|
||||
package tor |
||||
|
||||
import ( |
||||
"bytes" |
||||
"io/ioutil" |
||||
"path/filepath" |
||||
"testing" |
||||
) |
||||
|
||||
// TestOnionFile tests that the OnionFile implementation of the OnionStore
|
||||
// interface behaves as expected.
|
||||
func TestOnionFile(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
tempDir, err := ioutil.TempDir("", "onion_store") |
||||
if err != nil { |
||||
t.Fatalf("unable to create temp dir: %v", err) |
||||
} |
||||
|
||||
privateKey := []byte("hide_me_plz") |
||||
privateKeyPath := filepath.Join(tempDir, "secret") |
||||
|
||||
// Create a new file-based onion store. A private key should not exist
|
||||
// yet.
|
||||
onionFile := NewOnionFile(privateKeyPath, 0600) |
||||
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey { |
||||
t.Fatalf("expected ErrNoPrivateKey, got \"%v\"", err) |
||||
} |
||||
|
||||
// Store the private key and ensure what's stored matches.
|
||||
if err := onionFile.StorePrivateKey(V2, privateKey); err != nil { |
||||
t.Fatalf("unable to store private key: %v", err) |
||||
} |
||||
storePrivateKey, err := onionFile.PrivateKey(V2) |
||||
if err != nil { |
||||
t.Fatalf("unable to retrieve private key: %v", err) |
||||
} |
||||
if !bytes.Equal(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
|
||||
// retrieve it.
|
||||
if err := onionFile.DeletePrivateKey(V2); err != nil { |
||||
t.Fatalf("unable to delete private key: %v", err) |
||||
} |
||||
if _, err := onionFile.PrivateKey(V2); err != ErrNoPrivateKey { |
||||
t.Fatal("found deleted private key") |
||||
} |
||||
} |
Loading…
Reference in new issue