package main

// Bitwarden-compatible crypto for the KNELSecretsManager CLI (Vaultwarden API).
//   - master key: PBKDF2-SHA256(password, email, iterations, 32B) or Argon2id
//   - auth: masterPasswordHash = base64(PBKDF2-SHA256(masterKey, password, 1, 32B))
//   - stretched master key: HKDF-SHA256 expand, info "enc"/"mac" (32B each)
//   - encString "2.iv|ct|mac": AES-256-CBC(encKey32) + HMAC-SHA256(macKey32, iv||ct)
//     ("AesCbc128_HmacSha256_B64" is Bitwarden's legacy misnomer; keys are 32+32 bytes)

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"golang.org/x/crypto/argon2"
	"golang.org/x/crypto/pbkdf2"
)

type EncString struct {
	Type byte
	IV   []byte
	CT   []byte
	MAC  []byte
}

func ParseEncString(s string) (*EncString, error) {
	if s == "" {
		return nil, errors.New("empty encstring")
	}
	parts := strings.SplitN(s, ".", 2)
	if len(parts) != 2 {
		return nil, errors.New("encstring missing type header")
	}
	t, err := strconv.Atoi(parts[0])
	if err != nil {
		return nil, fmt.Errorf("bad encstring type: %w", err)
	}
	es := &EncString{Type: byte(t)}
	switch t {
	case 0: // AesCbc256_B64 (legacy, no mac)
		b, err := base64.StdEncoding.DecodeString(parts[1])
		if err != nil {
			return nil, err
		}
		es.IV, es.CT = b[:16], b[16:]
	case 1, 2: // both handled identically on the wire (enc32+mac32 keys)
		seg := strings.Split(parts[1], "|")
		if len(seg) != 3 {
			return nil, errors.New("encstring needs iv|ct|mac")
		}
		if es.IV, err = base64.StdEncoding.DecodeString(seg[0]); err != nil {
			return nil, err
		}
		if es.CT, err = base64.StdEncoding.DecodeString(seg[1]); err != nil {
			return nil, err
		}
		if es.MAC, err = base64.StdEncoding.DecodeString(seg[2]); err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("unsupported encstring type %d", t)
	}
	return es, nil
}

func (e *EncString) String() string {
	if e.Type == 0 {
		return "0." + base64.StdEncoding.EncodeToString(append(append([]byte{}, e.IV...), e.CT...))
	}
	return fmt.Sprintf("%d.%s|%s|%s", e.Type,
		base64.StdEncoding.EncodeToString(e.IV),
		base64.StdEncoding.EncodeToString(e.CT),
		base64.StdEncoding.EncodeToString(e.MAC))
}

func deriveMasterKey(password, email string, kdfType int, iterations, memory, parallelism uint32) []byte {
	salt := []byte(strings.ToLower(strings.TrimSpace(email)))
	switch kdfType {
	case 1:
		return argon2.IDKey([]byte(password), salt, iterations, memory, uint8(parallelism), 32)
	default:
		return pbkdf2.Key([]byte(password), salt, int(iterations), 32, sha256.New)
	}
}

func masterPasswordHash(masterKey []byte, password string) string {
	return base64.StdEncoding.EncodeToString(pbkdf2.Key(masterKey, []byte(password), 1, 32, sha256.New))
}

// stretchKey expands the 32B master key to a 64B symmetric key (enc 32 | mac 32).
func stretchKey(masterKey []byte) []byte {
	out := make([]byte, 64)
	copy(out[:32], hkdfExpand(masterKey, []byte("enc"), 32))
	copy(out[32:], hkdfExpand(masterKey, []byte("mac"), 32))
	return out
}

func hkdfExpand(key, info []byte, length int) []byte {
	out := make([]byte, 0, length)
	t := []byte{}
	var i byte
	for len(out) < length {
		i++
		h := hmac.New(sha256.New, key)
		h.Write(t)
		h.Write(info)
		h.Write([]byte{i})
		t = h.Sum(nil)
		out = append(out, t...)
	}
	return out[:length]
}

// symDecrypt decrypts a type-1/2 encstring with a 64-byte key (enc32|mac32).
func symDecrypt(key64 []byte, es *EncString) ([]byte, error) {
	if len(key64) != 64 {
		return nil, errors.New("symmetric key must be 64 bytes")
	}
	if es.Type == 0 {
		return aesCBCDecrypt(key64[:32], es.IV, es.CT)
	}
	if es.Type != 1 && es.Type != 2 {
		return nil, fmt.Errorf("unsupported encstring type %d", es.Type)
	}
	mac := hmac.New(sha256.New, key64[32:])
	mac.Write(es.IV)
	mac.Write(es.CT)
	if subtle.ConstantTimeCompare(mac.Sum(nil), es.MAC) != 1 {
		return nil, errors.New("mac mismatch")
	}
	return aesCBCDecrypt(key64[:32], es.IV, es.CT)
}

// symEncrypt encrypts plaintext into a type-2 encstring with a 64-byte key.
func symEncrypt(key64 []byte, plaintext []byte) (string, error) {
	if len(key64) != 64 {
		return "", errors.New("symmetric key must be 64 bytes")
	}
	iv := make([]byte, 16)
	if _, err := rand.Read(iv); err != nil {
		return "", err
	}
	ct, err := aesCBCEncrypt(key64[:32], iv, plaintext)
	if err != nil {
		return "", err
	}
	mac := hmac.New(sha256.New, key64[32:])
	mac.Write(iv)
	mac.Write(ct)
	es := &EncString{Type: 2, IV: iv, CT: ct, MAC: mac.Sum(nil)}
	return es.String(), nil
}

func aesCBCDecrypt(key, iv, ct []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	if len(ct) == 0 || len(ct)%aes.BlockSize != 0 {
		return nil, errors.New("ciphertext not block aligned")
	}
	pt := make([]byte, len(ct))
	cipher.NewCBCDecrypter(block, iv).CryptBlocks(pt, ct)
	return unpad(pt)
}

func aesCBCEncrypt(key, iv, pt []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	ct := make([]byte, len(pad(pt)))
	cipher.NewCBCEncrypter(block, iv).CryptBlocks(ct, pad(pt))
	return ct, nil
}

func unpad(b []byte) ([]byte, error) {
	if len(b) == 0 {
		return nil, errors.New("empty plaintext")
	}
	n := int(b[len(b)-1])
	if n == 0 || n > aes.BlockSize || n > len(b) {
		return nil, errors.New("bad padding")
	}
	return b[:len(b)-n], nil
}

func pad(b []byte) []byte {
	n := aes.BlockSize - len(b)%aes.BlockSize
	out := make([]byte, len(b)+n)
	copy(out, b)
	for i := len(b); i < len(out); i++ {
		out[i] = byte(n)
	}
	return out
}
