package tor

import (
	"encoding/base32"
	"errors"
	"filippo.io/edwards25519"
	"git.openprivacy.ca/openprivacy/bine/control"
	"git.openprivacy.ca/openprivacy/bine/tor"
	"golang.org/x/crypto/ed25519"
	"golang.org/x/crypto/sha3"
	"strings"
)

// V3HostnameLength is the length of a Tor V3 Onion Address (without the .onion suffix)
const V3HostnameLength = 56

// Hidden service version
const version = byte(0x03)

// Salt used to create checkdigits
const salt = ".onion checksum"

func getCheckdigits(pub ed25519.PublicKey) []byte {
	// Calculate checksum sha3(".onion checksum" || publicKey || version)
	checkstr := []byte(salt)
	checkstr = append(checkstr, pub...)
	checkstr = append(checkstr, version)
	checksum := sha3.Sum256(checkstr)
	return checksum[:2]
}

// GetTorV3Hostname converts an ed25519 public key to a valid tor onion hostname
func GetTorV3Hostname(pub ed25519.PublicKey) string {
	// Construct onion address base32(publicKey || checkdigits || version)
	checkdigits := getCheckdigits(pub)
	combined := pub[:]
	combined = append(combined, checkdigits...)
	combined = append(combined, version)
	serviceID := base32.StdEncoding.EncodeToString(combined)
	return strings.ToLower(serviceID)
}

// IsValidHostname returns true if the given address is a valid onion v3 address
func IsValidHostname(address string) bool {
	if len(address) == V3HostnameLength {
		data, err := base32.StdEncoding.DecodeString(strings.ToUpper(address))
		if err == nil {
			pubkey := data[0:ed25519.PublicKeySize]

			// Tor won't allow us to connect to a hostname containing a torsion component
			// However because we permit authentication over inbound connections we would like to
			// be extra safe and reject all *invalid* hostnames that contain a torsion component...

			// to do this we need to multiply the point by the order of the group and check that the
			// result is the ed25519 identity element.
			// l = order of the group (minus 1)
			lBytes := []byte{236, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16}
			l, _ := edwards25519.NewScalar().SetCanonicalBytes(lBytes)

			// construct a curve point from the public key
			// if this fails then the hostname is invalid
			p, err := new(edwards25519.Point).SetBytes(pubkey)
			if err != nil {
				return false
			}

			// Calculate l*P (actually (l-1)P + P because of the limitations of the scalar library...
			result := new(edwards25519.Point).ScalarMult(l, p)
			result = new(edwards25519.Point).Add(result, p)
			// The result should be the identity point..assuming the hostname contains no torsion components...
			if result.Equal(edwards25519.NewIdentityPoint()) == 0 {
				return false
			}

			// Finally check that we arrive at the same hostname as the one we were given...
			if GetTorV3Hostname(ed25519.PublicKey(pubkey)) == address {
				return true
			}
		}
	}
	return false
}

// HashedPasswordAuthenticator authenticates to a Tor control port using a hashed password.
// Note: This method is vulnerable to replay attacks by the host system (but so is cookie auth)
type HashedPasswordAuthenticator struct {
	Password string
}

// Authenticate uses the given hashed password to authenticate to the control port
func (h HashedPasswordAuthenticator) Authenticate(controlport *control.Conn) error {
	return controlport.Authenticate(h.Password)
}

// NewHashedPasswordAuthenticator creates a new hashed password authenticator
func NewHashedPasswordAuthenticator(password string) tor.Authenticator {
	return HashedPasswordAuthenticator{Password: password}
}

// NullAuthenticator exists to force always authenticating to a system tor.
type NullAuthenticator struct {
}

// Authenticate on a NullAuthenticator always results in failure.
func (n NullAuthenticator) Authenticate(controlport *control.Conn) error {
	return errors.New("null authenticator provided, this control port is unsafe, start a new tor process instead")
}
