package tor
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/textproto"
"strconv"
"strings"
"sync/atomic"
)
const (
success = 250
nonceLen = 32
cookieLen = 32
ProtocolInfoVersion = 1
MinTorVersion = "0.3.3.6"
authSafeCookie = "SAFECOOKIE"
authHashedPassword = "HASHEDPASSWORD"
authNull = "NULL"
)
var (
serverKey = []byte("Tor safe cookie authentication " +
"server-to-controller hash")
controllerKey = []byte("Tor safe cookie authentication " +
"controller-to-server hash")
)
type Controller struct {
started int32
stopped int32
conn *textproto.Conn
controlAddr string
password string
version string
targetIPAddress string
}
func NewController(controlAddr string, targetIPAddress string,
password string) *Controller {
return &Controller{
controlAddr: controlAddr,
targetIPAddress: targetIPAddress,
password: password,
}
}
func (c *Controller) Start() error {
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return nil
}
conn, err := textproto.Dial("tcp", c.controlAddr)
if err != nil {
return fmt.Errorf("unable to connect to Tor server: %v", err)
}
c.conn = conn
return c.authenticate()
}
func (c *Controller) Stop() error {
if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
return nil
}
return c.conn.Close()
}
func (c *Controller) sendCommand(command string) (int, string, error) {
if err := c.conn.Writer.PrintfLine(command); err != nil {
return 0, "", err
}
code, reply, err := c.conn.Reader.ReadResponse(success)
if err != nil {
return code, reply, err
}
return code, reply, nil
}
func parseTorReply(reply string) map[string]string {
params := make(map[string]string)
contents := strings.Split(strings.Replace(reply, "\n", " ", -1), " ")
for _, content := range contents {
keyValue := strings.SplitN(content, "=", 2)
if len(keyValue) != 2 {
continue
}
key := keyValue[0]
value := keyValue[1]
params[key] = value
}
return params
}
func (c *Controller) authenticate() error {
protocolInfo, err := c.protocolInfo()
if err != nil {
return err
}
c.version = protocolInfo.version()
switch {
case c.password != "":
if !protocolInfo.supportsAuthMethod(authHashedPassword) {
return fmt.Errorf("%v authentication method not "+
"supported", authHashedPassword)
}
return c.authenticateViaHashedPassword()
case protocolInfo.supportsAuthMethod(authSafeCookie):
return c.authenticateViaSafeCookie(protocolInfo)
case protocolInfo.supportsAuthMethod(authNull):
return c.authenticateViaNull()
default:
return errors.New("the Tor server must be configured with " +
"NULL, SAFECOOKIE, or HASHEDPASSWORD authentication")
}
}
func (c *Controller) authenticateViaNull() error {
_, _, err := c.sendCommand("AUTHENTICATE")
return err
}
func (c *Controller) authenticateViaHashedPassword() error {
cmd := fmt.Sprintf("AUTHENTICATE \"%s\"", c.password)
_, _, err := c.sendCommand(cmd)
return err
}
func (c *Controller) authenticateViaSafeCookie(info protocolInfo) error {
cookie, err := c.getAuthCookie(info)
if err != nil {
return fmt.Errorf("unable to retrieve authentication cookie: "+
"%v", err)
}
clientNonce := make([]byte, nonceLen)
if _, err := rand.Read(clientNonce); err != nil {
return fmt.Errorf("unable to generate client nonce: %v", err)
}
cmd := fmt.Sprintf("AUTHCHALLENGE SAFECOOKIE %x", clientNonce)
_, reply, err := c.sendCommand(cmd)
if err != nil {
return err
}
replyParams := parseTorReply(reply)
serverHash, ok := replyParams["SERVERHASH"]
if !ok {
return errors.New("server hash not found in reply")
}
decodedServerHash, err := hex.DecodeString(serverHash)
if err != nil {
return fmt.Errorf("unable to decode server hash: %v", err)
}
if len(decodedServerHash) != sha256.Size {
return errors.New("invalid server hash length")
}
serverNonce, ok := replyParams["SERVERNONCE"]
if !ok {
return errors.New("server nonce not found in reply")
}
decodedServerNonce, err := hex.DecodeString(serverNonce)
if err != nil {
return fmt.Errorf("unable to decode server nonce: %v", err)
}
if len(decodedServerNonce) != nonceLen {
return errors.New("invalid server nonce length")
}
hmacMessage := bytes.Join(
[][]byte{cookie, clientNonce, decodedServerNonce}, []byte{},
)
computedServerHash := computeHMAC256(serverKey, hmacMessage)
if !hmac.Equal(computedServerHash, decodedServerHash) {
return fmt.Errorf("expected server hash %x, got %x",
decodedServerHash, computedServerHash)
}
clientHash := computeHMAC256(controllerKey, hmacMessage)
if len(clientHash) != sha256.Size {
return errors.New("invalid client hash length")
}
cmd = fmt.Sprintf("AUTHENTICATE %x", clientHash)
if _, _, err := c.sendCommand(cmd); err != nil {
return err
}
return nil
}
func (c *Controller) getAuthCookie(info protocolInfo) ([]byte, error) {
cookieFilePath, ok := info["COOKIEFILE"]
if !ok {
return nil, errors.New("COOKIEFILE not found in PROTOCOLINFO " +
"reply")
}
cookieFilePath = strings.Trim(cookieFilePath, "\"")
cookie, err := ioutil.ReadFile(cookieFilePath)
if err != nil {
return nil, err
}
if len(cookie) != cookieLen {
return nil, errors.New("invalid authentication cookie length")
}
return cookie, nil
}
func computeHMAC256(key, message []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(message)
return mac.Sum(nil)
}
func supportsV3(version string) error {
parts := strings.Split(version, ".")
if len(parts) != 4 {
return errors.New("version string is not of the format " +
"major.minor.revision.build")
}
build := strings.Split(parts[len(parts)-1], "-")
parts[len(parts)-1] = build[0]
for _, part := range parts {
if _, err := strconv.Atoi(part); err != nil {
return err
}
}
if version < MinTorVersion {
return fmt.Errorf("version %v below minimum version supported "+
"%v", version, MinTorVersion)
}
return nil
}
type protocolInfo map[string]string
func (i protocolInfo) version() string {
version := i["Tor"]
return strings.Trim(version, "\"")
}
func (i protocolInfo) supportsAuthMethod(method string) bool {
methods, ok := i["METHODS"]
if !ok {
return false
}
return strings.Contains(methods, method)
}
func (c *Controller) protocolInfo() (protocolInfo, error) {
cmd := fmt.Sprintf("PROTOCOLINFO %d", ProtocolInfoVersion)
_, reply, err := c.sendCommand(cmd)
if err != nil {
return nil, err
}
return protocolInfo(parseTorReply(reply)), nil
}