package torutil

import (
	"crypto"
	"crypto/rsa"
	"crypto/sha1"
	"crypto/x509"
	"encoding/base32"
	"fmt"
	"strings"

	"git.openprivacy.ca/openprivacy/bine/torutil/ed25519"
	"golang.org/x/crypto/sha3"
)

var serviceIDEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)

// OnionServiceIDFromPrivateKey generates the onion service ID from the given
// private key. This panics if the private key is not a 1024-bit
// crypto/*rsa.PrivateKey or git.openprivacy.ca/openprivacy/bine/torutil/ed25519.KeyPair.
func OnionServiceIDFromPrivateKey(key crypto.PrivateKey) string {
	switch k := key.(type) {
	case *rsa.PrivateKey:
		return OnionServiceIDFromV2PublicKey(&k.PublicKey)
	case ed25519.KeyPair:
		return OnionServiceIDFromV3PublicKey(k.PublicKey())
	}
	panic(fmt.Sprintf("Unrecognized private key type: %T", key))
}

// OnionServiceIDFromPublicKey generates the onion service ID from the given
// public key. This panics if the public key is not a 1024-bit
// crypto/*rsa.PublicKey or git.openprivacy.ca/openprivacy/bine/torutil/ed25519.PublicKey.
func OnionServiceIDFromPublicKey(key crypto.PublicKey) string {
	switch k := key.(type) {
	case *rsa.PublicKey:
		return OnionServiceIDFromV2PublicKey(k)
	case ed25519.PublicKey:
		return OnionServiceIDFromV3PublicKey(k)
	}
	panic(fmt.Sprintf("Unrecognized public key type: %T", key))
}

// OnionServiceIDFromV2PublicKey generates a V2 service ID for the given
// RSA-1024 public key. Panics if not a 1024-bit key.
func OnionServiceIDFromV2PublicKey(key *rsa.PublicKey) string {
	if key.N.BitLen() != 1024 {
		panic("RSA key not 1024 bit")
	}
	h := sha1.New()
	h.Write(x509.MarshalPKCS1PublicKey(key))
	return strings.ToLower(serviceIDEncoding.EncodeToString(h.Sum(nil)[:10]))
}

// OnionServiceIDFromV3PublicKey generates a V3 service ID for the given
// ED25519 public key.
func OnionServiceIDFromV3PublicKey(key ed25519.PublicKey) string {
	checkSum := sha3.Sum256(append(append([]byte(".onion checksum"), key...), 0x03))
	var keyBytes [35]byte
	copy(keyBytes[:], key)
	keyBytes[32] = checkSum[0]
	keyBytes[33] = checkSum[1]
	keyBytes[34] = 0x03
	return strings.ToLower(serviceIDEncoding.EncodeToString(keyBytes[:]))
}

// PublicKeyFromV3OnionServiceID returns a public key for the given service ID
// or an error if the service ID is invalid.
func PublicKeyFromV3OnionServiceID(id string) (ed25519.PublicKey, error) {
	byts, err := serviceIDEncoding.DecodeString(strings.ToUpper(id))
	if err != nil {
		return nil, err
	} else if len(byts) != 35 {
		return nil, fmt.Errorf("Invalid id length")
	} else if byts[34] != 0x03 {
		return nil, fmt.Errorf("Invalid version")
	}
	// Do a checksum check
	key := ed25519.PublicKey(byts[:32])
	checkSum := sha3.Sum256(append(append([]byte(".onion checksum"), key...), 0x03))
	if byts[32] != checkSum[0] || byts[33] != checkSum[1] {
		return nil, fmt.Errorf("Invalid checksum")
	}
	return key, nil
}
