// Copyright (C) 2015 Space Monkey, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +build cgo

package openssl

/*
#include <openssl/ssl.h>
#include <openssl/evp.h>

static int SSL_CTX_set_tlsext_ticket_key_cb_not_a_macro(SSL_CTX *sslctx,
        int (*cb)(SSL *s, unsigned char key_name[16],
                  unsigned char iv[EVP_MAX_IV_LENGTH],
                  EVP_CIPHER_CTX *ctx, HMAC_CTX *hctx, int enc)) {

    return SSL_CTX_set_tlsext_ticket_key_cb(sslctx, cb);
}

extern int ticket_key_cb(SSL *s, unsigned char key_name[16],
        unsigned char iv[EVP_MAX_IV_LENGTH],
        EVP_CIPHER_CTX *cctx, HMAC_CTX *hctx, int enc);
*/
import "C"

import (
	"os"
	"unsafe"
)

const (
	KeyNameSize = 16
)

// TicketCipherCtx describes the cipher that will be used by the ticket store
// for encrypting the tickets. Engine may be nil if no engine is desired.
type TicketCipherCtx struct {
	Cipher *Cipher
	Engine *Engine
}

// TicketDigestCtx describes the digest that will be used by the ticket store
// to authenticate the data. Engine may be nil if no engine is desired.
type TicketDigestCtx struct {
	Digest *Digest
	Engine *Engine
}

// TicketName is an identifier for the key material for a ticket.
type TicketName [KeyNameSize]byte

// TicketKey is the key material for a ticket. If this is lost, forward secrecy
// is lost as it allows decrypting TLS sessions retroactively.
type TicketKey struct {
	Name      TicketName
	CipherKey []byte
	HMACKey   []byte
	IV        []byte
}

// TicketKeyManager is a manager for TicketKeys. It allows one to control the
// lifetime of tickets, causing renewals and expirations for keys that are
// created. Calls to the manager are serialized.
type TicketKeyManager interface {
	// New should create a brand new TicketKey with a new name.
	New() *TicketKey

	// Current should return a key that is still valid.
	Current() *TicketKey

	// Lookup should return a key with the given name, or nil if no name
	// exists.
	Lookup(name TicketName) *TicketKey

	// Expired should return if the key with the given name is expired and
	// should not be used any more.
	Expired(name TicketName) bool

	// ShouldRenew should return if the key is still ok to use for the current
	// session, but we should send a new key for the client.
	ShouldRenew(name TicketName) bool
}

// TicketStore descibes the encryption and authentication methods the tickets
// will use along with a key manager for generating and keeping track of the
// secrets.
type TicketStore struct {
	CipherCtx TicketCipherCtx
	DigestCtx TicketDigestCtx
	Keys      TicketKeyManager
}

func (t *TicketStore) cipherEngine() *C.ENGINE {
	if t.CipherCtx.Engine == nil {
		return nil
	}
	return t.CipherCtx.Engine.e
}

func (t *TicketStore) digestEngine() *C.ENGINE {
	if t.DigestCtx.Engine == nil {
		return nil
	}
	return t.DigestCtx.Engine.e
}

const (
	// instruct to do a handshake
	ticket_resp_requireHandshake = 0
	// crypto context is set up correctly
	ticket_resp_sessionOk = 1
	// crypto context is ok, but the ticket should be reissued
	ticket_resp_renewSession = 2
	// we had a problem that shouldn't fall back to doing a handshake
	ticket_resp_error = -1

	// asked to create session crypto context
	ticket_req_newSession = 1
	// asked to load crypto context for a previous session
	ticket_req_lookupSession = 0
)

//export ticket_key_cb_thunk
func ticket_key_cb_thunk(p unsafe.Pointer, s *C.SSL, key_name *C.uchar,
	iv *C.uchar, cctx *C.EVP_CIPHER_CTX, hctx *C.HMAC_CTX, enc C.int) C.int {

	// no panic's allowed. it's super hard to guarantee any state at this point
	// so just abort everything.
	defer func() {
		if err := recover(); err != nil {
			logger.Critf("openssl: ticket key callback panic'd: %v", err)
			os.Exit(1)
		}
	}()

	ctx := (*Ctx)(p)
	store := ctx.ticket_store
	if store == nil {
		// TODO(jeff): should this be an error condition? it doesn't make sense
		// to be called if we don't have a store I believe, but that's probably
		// not worth aborting the handshake which is what I believe returning
		// an error would do.
		return ticket_resp_requireHandshake
	}

	ctx.ticket_store_mu.Lock()
	defer ctx.ticket_store_mu.Unlock()

	switch enc {
	case ticket_req_newSession:
		key := store.Keys.Current()
		if key == nil {
			key = store.Keys.New()
			if key == nil {
				return ticket_resp_requireHandshake
			}
		}

		C.memcpy(
			unsafe.Pointer(key_name),
			unsafe.Pointer(&key.Name[0]),
			KeyNameSize)
		C.EVP_EncryptInit_ex(
			cctx,
			store.CipherCtx.Cipher.ptr,
			store.cipherEngine(),
			(*C.uchar)(&key.CipherKey[0]),
			(*C.uchar)(&key.IV[0]))
		C.HMAC_Init_ex(
			hctx,
			unsafe.Pointer(&key.HMACKey[0]),
			C.int(len(key.HMACKey)),
			store.DigestCtx.Digest.ptr,
			store.digestEngine())

		return ticket_resp_sessionOk

	case ticket_req_lookupSession:
		var name TicketName
		C.memcpy(
			unsafe.Pointer(&name[0]),
			unsafe.Pointer(key_name),
			KeyNameSize)

		key := store.Keys.Lookup(name)
		if key == nil {
			return ticket_resp_requireHandshake
		}
		if store.Keys.Expired(name) {
			return ticket_resp_requireHandshake
		}

		C.EVP_DecryptInit_ex(
			cctx,
			store.CipherCtx.Cipher.ptr,
			store.cipherEngine(),
			(*C.uchar)(&key.CipherKey[0]),
			(*C.uchar)(&key.IV[0]))
		C.HMAC_Init_ex(
			hctx,
			unsafe.Pointer(&key.HMACKey[0]),
			C.int(len(key.HMACKey)),
			store.DigestCtx.Digest.ptr,
			store.digestEngine())

		if store.Keys.ShouldRenew(name) {
			return ticket_resp_renewSession
		}

		return ticket_resp_sessionOk

	default:
		return ticket_resp_error
	}
}

// SetTicketStore sets the ticket store for the context so that clients can do
// ticket based session resumption. If the store is nil, the
func (c *Ctx) SetTicketStore(store *TicketStore) {
	c.ticket_store = store

	if store == nil {
		C.SSL_CTX_set_tlsext_ticket_key_cb_not_a_macro(c.ctx, nil)
	} else {
		C.SSL_CTX_set_tlsext_ticket_key_cb_not_a_macro(c.ctx,
			(*[0]byte)(C.ticket_key_cb))
	}
}
