// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2021 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package secboot

import (
	"bytes"
	"context"
	"errors"

	"golang.org/x/xerrors"

	"github.com/snapcore/secboot/internal/luks2"
	"github.com/snapcore/secboot/internal/luksview"
)

// LUKS2KeyDataReader provides a mechanism to read a KeyData from a LUKS2 token.
//
// XXX: This will eventually be deleted when the legacy activation API has been
// deleted, [StorageContainer] has write support and snapd has migrated to that
// new write support.
type LUKS2KeyDataReader struct {
	name     string
	slot     int
	priority int
	*bytes.Reader
}

// NewLUKS2KeyDataReader is used to read a LUKS2 token containing key data with
// the specified name on the specified LUKS2 container.
//
// XXX: This will eventually be deleted when the legacy activation API has been
// deleted, [StorageContainer] has write support and snapd has migrated to that
// new write support.
func NewLUKS2KeyDataReader(devicePath, name string) (*LUKS2KeyDataReader, error) {
	view, err := newLUKSView(context.TODO(), devicePath)
	if err != nil {
		return nil, xerrors.Errorf("cannot obtain LUKS2 header view: %w", err)
	}

	token, _, exists := view.TokenByName(name)
	if !exists {
		return nil, errors.New("a keyslot with the specified name does not exist")
	}

	kdToken, ok := token.(*luksview.KeyDataToken)
	if !ok {
		return nil, errors.New("named keyslot has the wrong type")
	}

	if kdToken.Data == nil {
		return nil, errors.New("named keyslot does not contain key data yet")
	}

	return &LUKS2KeyDataReader{
		name:     devicePath + ":" + name,
		slot:     token.Keyslots()[0],
		priority: kdToken.Priority,
		Reader:   bytes.NewReader(kdToken.Data)}, nil
}

func (r *LUKS2KeyDataReader) ReadableName() string {
	return r.name
}

// KeyslotID indicates the keyslot ID associated with the token from which this
// KeyData is read.
func (r *LUKS2KeyDataReader) KeyslotID() int {
	return r.slot
}

// Priority indicates the priority of the keyslot associated with the token from
// which this KeyData is read. The default priority is 0 with higher numbers
// indicating a higher priority.
func (r *LUKS2KeyDataReader) Priority() int {
	return r.priority
}

// LUKS2KeyDataWriter provides a mechanism to write a KeyData to a LUKS2 token.
type LUKS2KeyDataWriter struct {
	devicePath string
	id         int
	slot       int
	name       string
	priority   int
	*bytes.Buffer
}

// NewLUKS2KeyDataWriter creates a new LUKS2KeyDataWriter for atomically writing a
// KeyData to a LUKS2 token with the specicied name and priority on the specified
// LUKS2 container.
//
// The container must already contain a token of the correct type with the supplied
// name. The initial token is bootstrapped by InitializeLUKS2Container or
// SetLUKS2ContainerUnlockKey.
func NewLUKS2KeyDataWriter(devicePath, name string) (*LUKS2KeyDataWriter, error) {
	view, err := newLUKSView(context.TODO(), devicePath)
	if err != nil {
		return nil, xerrors.Errorf("cannot obtain LUKS2 header view: %w", err)
	}

	token, id, exists := view.TokenByName(name)
	if !exists {
		return nil, errors.New("a keyslot with the specified name does not exist")
	}

	kdToken, ok := token.(*luksview.KeyDataToken)
	if !ok {
		return nil, errors.New("named keyslot has the wrong type")
	}

	return &LUKS2KeyDataWriter{
		devicePath: devicePath,
		id:         id,
		slot:       token.Keyslots()[0],
		name:       name,
		priority:   kdToken.Priority,
		Buffer:     new(bytes.Buffer)}, nil
}

func (w *LUKS2KeyDataWriter) Commit() error {
	token := &luksview.KeyDataToken{
		TokenBase: luksview.TokenBase{
			TokenKeyslot: w.slot,
			TokenName:    w.name},
		Priority: w.priority,
		Data:     w.Bytes()}

	return luks2ImportToken(w.devicePath, token, &luks2.ImportTokenOptions{Id: w.id, Replace: true})
}

// SetPriority sets the priority for the updated KeyData that is written using
// this writer. It must be called before Commit.
//
// Zero is the default priority, with higher numbers indicating a higher
// priority. A negative priority indicates that the KeyData shouldn't be used
// for activation unless referred to explicitly.
func (w *LUKS2KeyDataWriter) SetPriority(priority int) {
	w.priority = priority
}
