// A wrapper that implements an interface to satisfy SQLCIPHER_CRYPTO_CUSTOM interface using standard
// go cryptography.
// Most things in this file should be self-explanitory, a few high level notes for future maintainers:
// - check go_crypto.c for the related interfaces we are attempting to satisfy and for initialization code
// - we rely on sqlcipher to manage all allocations for output buffers, as such we simple copy results out
package sqlite3

import "C"
import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/sha512"
	"hash"
	"unsafe"

	"crypto/pbkdf2"
	"crypto/sha1"
)

//export gocrypto_add_random
func gocrypto_add_random(_ unsafe.Pointer, _buffer unsafe.Pointer, _size C.int) C.int {
	// ignore any extra randomness we are given, we are using a go's crypto rng
	return 0
}

//export gocrypto_random
func gocrypto_random(_ unsafe.Pointer, buffer *C.char, length C.int) C.int {
	bufferSafe := C.GoBytes(unsafe.Pointer(buffer), length)
	_, err := rand.Read(([]byte)(bufferSafe))
	if err != nil {
		return 1
	}
	return 0
}

// in the sqlcipher codebase only 3 algorithms are defined
// with 0 = sha1, 1 = sha256, and 2 = sha512
// this is consistent between hmac and pbkdf2
// as such we panic if we are given an invalid mode
func selectHash(algo int) func() hash.Hash {
	var hash func() hash.Hash
	switch algo {
	case 0:
		hash = sha1.New
	case 1:
		hash = sha256.New
	case 2:
		hash = sha512.New
	default:
		panic("unknown hash function mode")
	}
	return hash
}

//export gocrypto_hmac
func gocrypto_hmac(_ unsafe.Pointer, algo C.int,
	hmac_key unsafe.Pointer, key_sz C.int,
	in unsafe.Pointer, in_sz C.int,
	in2 unsafe.Pointer, in2_sz C.int,
	out unsafe.Pointer) C.int {

	keyBytes := C.GoBytes(hmac_key, key_sz)
	inBytes := C.GoBytes(in, in_sz)
	in2Bytes := C.GoBytes(in2, in2_sz)

	h := hmac.New(selectHash(int(algo)), keyBytes)
	h.Write(inBytes)
	if in2_sz != 0 {
		h.Write(in2Bytes)
	}
	hmacSum := h.Sum(nil)
	outBytes := unsafe.Slice((*byte)(out), len(hmacSum))
	copy(outBytes, hmacSum)
	return 0
}

//export gocrypto_kdf
func gocrypto_kdf(_ unsafe.Pointer, algo C.int,
	pass *C.char, pass_sz C.int,
	salt unsafe.Pointer, salt_sz C.int,
	workFactor C.int, key_sz C.int,
	key unsafe.Pointer) C.int {

	passBytes := C.GoStringN(pass, pass_sz)
	saltBytes := C.GoBytes(salt, salt_sz)
	if keyBytes, err := pbkdf2.Key(selectHash(int(algo)), passBytes, saltBytes, int(workFactor), int(key_sz)); err != nil {
		return 1
	} else {
		outBytes := unsafe.Slice((*byte)(key), key_sz)
		copy(outBytes, keyBytes)
	}

	return 0
}

//export gocrypto_cipher
func gocrypto_cipher(_ unsafe.Pointer, mode C.int,
	key unsafe.Pointer, key_sz C.int,
	iv unsafe.Pointer,
	in unsafe.Pointer, in_sz C.int,
	out unsafe.Pointer) C.int {

	keyBytes := C.GoBytes(key, key_sz)
	inBytes := C.GoBytes(in, in_sz)

	block, err := aes.NewCipher(keyBytes)
	if err != nil {
		return 1
	}
	outBytes := unsafe.Slice((*byte)(out), in_sz)
	ivBytes := unsafe.Slice((*byte)(iv), aes.BlockSize)
	if int(mode) == 1 {
		// encrypt
		enc := cipher.NewCBCEncrypter(block, ivBytes)
		enc.CryptBlocks(outBytes, inBytes)
	} else {
		// decrypt
		enc := cipher.NewCBCDecrypter(block, ivBytes)
		enc.CryptBlocks(outBytes, inBytes)
	}
	return 0
}

//export gocrypto_get_cipher
func gocrypto_get_cipher(_ unsafe.Pointer) *C.char {
	return C.CString("aes-256-cbc")
}

//export gocrypto_get_key_sz
func gocrypto_get_key_sz(_ unsafe.Pointer) C.int {
	return 32
}

//export gocrypto_get_iv_sz
func gocrypto_get_iv_sz(_ unsafe.Pointer) C.int {
	return aes.BlockSize
}

//export gocrypto_get_block_sz
func gocrypto_get_block_sz(_ unsafe.Pointer) C.int {
	return aes.BlockSize
}

//export gocrypto_get_hmac_sz
func gocrypto_get_hmac_sz(_ unsafe.Pointer, algo int) C.int {
	return C.int(selectHash(algo)().Size())
}

//export gocrypto_ctx_init
func gocrypto_ctx_init(_ *unsafe.Pointer) C.int {
	// we have nothing to initialze
	return 0
}

//export gocrypto_ctx_free
func gocrypto_ctx_free(_ *unsafe.Pointer) C.int {
	// we have nothing to free
	return 0
}

//export gocrypto_fips_status
func gocrypto_fips_status(_ unsafe.Pointer) C.int {
	return 0
}

//export gocrypto_get_provider_version
func gocrypto_get_provider_version(_ unsafe.Pointer) *C.char {
	return C.CString("1.0")
}
