// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package ssss

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"strings"

	"maunium.net/go/mautrix/crypto/utils"
)

// Key represents a SSSS private key and related metadata.
type Key struct {
	ID       string       `json:"-"`
	Key      []byte       `json:"-"`
	Metadata *KeyMetadata `json:"-"`
}

// NewKey generates a new SSSS key, optionally based on the given passphrase.
//
// Errors are only returned if crypto/rand runs out of randomness.
func NewKey(passphrase string) (*Key, error) {
	// We don't support any other algorithms currently.
	keyData := KeyMetadata{Algorithm: AlgorithmAESHMACSHA2}

	var ssssKey []byte
	if len(passphrase) > 0 {
		// There's a passphrase. We need to generate a salt for it, set the metadata
		// and then compute the key using the passphrase and the metadata.
		saltBytes := make([]byte, 24)
		if _, err := rand.Read(saltBytes); err != nil {
			return nil, fmt.Errorf("failed to get random bytes for salt: %w", err)
		}
		keyData.Passphrase = &PassphraseMetadata{
			Algorithm:  PassphraseAlgorithmPBKDF2,
			Iterations: 500000,
			Salt:       base64.StdEncoding.EncodeToString(saltBytes),
			Bits:       256,
		}
		var err error
		ssssKey, err = keyData.Passphrase.GetKey(passphrase)
		if err != nil {
			return nil, fmt.Errorf("failed to get key from passphrase: %w", err)
		}
	} else {
		// No passphrase, just generate a random key
		ssssKey = make([]byte, 32)
		if _, err := rand.Read(ssssKey); err != nil {
			return nil, fmt.Errorf("failed to get random bytes for key: %w", err)
		}
	}

	// Generate a random ID for the key. It's what identifies the key in account data.
	keyIDBytes := make([]byte, 24)
	if _, err := rand.Read(keyIDBytes); err != nil {
		return nil, fmt.Errorf("failed to get random bytes for key ID: %w", err)
	}

	// We store a certain hash in the key metadata so that clients can check if the user entered the correct key.
	var ivBytes [utils.AESCTRIVLength]byte
	if _, err := rand.Read(ivBytes[:]); err != nil {
		return nil, fmt.Errorf("failed to get random bytes for IV: %w", err)
	}
	keyData.IV = base64.StdEncoding.EncodeToString(ivBytes[:])
	keyData.MAC = keyData.calculateHash(ssssKey)

	return &Key{
		Key:      ssssKey,
		ID:       base64.StdEncoding.EncodeToString(keyIDBytes),
		Metadata: &keyData,
	}, nil
}

// RecoveryKey gets the recovery key for this SSSS key.
func (key *Key) RecoveryKey() string {
	return utils.EncodeBase58RecoveryKey(key.Key)
}

// Encrypt encrypts the given data with this key.
func (key *Key) Encrypt(eventType string, data []byte) EncryptedKeyData {
	aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)

	iv := utils.GenA256CTRIV()
	payload := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
	base64.StdEncoding.Encode(payload, data)
	utils.XorA256CTR(payload, aesKey, iv)

	return EncryptedKeyData{
		Ciphertext: base64.StdEncoding.EncodeToString(payload),
		IV:         base64.StdEncoding.EncodeToString(iv[:]),
		MAC:        utils.HMACSHA256B64(payload, hmacKey),
	}
}

// Decrypt decrypts the given encrypted data with this key.
func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error) {
	var ivBytes [utils.AESCTRIVLength]byte
	decodedIV, _ := base64.StdEncoding.DecodeString(data.IV)
	copy(ivBytes[:], decodedIV)

	payload, err := base64.StdEncoding.DecodeString(data.Ciphertext)
	if err != nil {
		return nil, err
	}

	// derive the AES and HMAC keys for the requested event type using the SSSS key
	aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)

	// compare the stored MAC with the one we calculated from the ciphertext
	calcMac := utils.HMACSHA256B64(payload, hmacKey)
	if strings.ReplaceAll(data.MAC, "=", "") != strings.ReplaceAll(calcMac, "=", "") {
		return nil, ErrKeyDataMACMismatch
	}

	utils.XorA256CTR(payload, aesKey, ivBytes)
	decryptedDecoded, err := base64.StdEncoding.DecodeString(string(payload))
	return decryptedDecoded, err
}
