Files
threshold/internal/bsr/bsr.go
T

353 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package bsr
import (
"context"
"errors"
"fmt"
"io"
"github.com/hashicorp/boundary/internal/bsr/internal/checksum"
"github.com/hashicorp/boundary/internal/bsr/internal/is"
"github.com/hashicorp/boundary/internal/bsr/kms"
"github.com/hashicorp/boundary/internal/storage"
"google.golang.org/protobuf/proto"
)
const (
bsrFile = "%s.bsr"
connectionFile = "%s.connection"
channelFile = "%s.channel"
messagesFile = "messages-%s.data"
requestsFile = "requests-%s.data"
bsrPubKeyFile = "bsrKey.pub"
wrappedBsrKeyFile = "wrappedBsrKey"
wrappedPrivKeyFile = "wrappedPrivKey"
pubKeyBsrSignatureFile = "pubKeyBsrSignature.sign"
pubKeySelfSignatureFile = "pubKeySelfSignature.sign"
)
// Session is the top level container in a bsr that contains the files for
// a recorded session.
type Session struct {
*container
multiplexed bool
Meta *SessionMeta
}
// NewSession creates a Session container for a given session id.
func NewSession(ctx context.Context, meta *SessionMeta, f storage.FS, keys *kms.Keys, options ...Option) (*Session, error) {
const op = "bsr.NewSession"
switch {
case is.Nil(meta):
return nil, fmt.Errorf("%s: missing session meta: %w", op, ErrInvalidParameter)
case meta.Id == "":
return nil, fmt.Errorf("%s: missing session id: %w", op, ErrInvalidParameter)
case is.Nil(meta.StaticHost) && is.Nil(meta.DynamicHost):
return nil, fmt.Errorf("%s: missing session host information: %w", op, ErrInvalidParameter)
case !is.Nil(meta.StaticHost) && !is.Nil(meta.DynamicHost):
return nil, fmt.Errorf("%s: meta cannot contain both static and dynamic host information: %w", op, ErrInvalidParameter)
case len(meta.StaticCredentialStore) == 0 && len(meta.VaultCredentialStore) == 0:
return nil, fmt.Errorf("%s: missing credential information: %w", op, ErrInvalidParameter)
case is.Nil(meta.User):
return nil, fmt.Errorf("%s: missing session user: %w", op, ErrInvalidParameter)
case is.Nil(meta.Target):
return nil, fmt.Errorf("%s: missing session target: %w", op, ErrInvalidParameter)
case is.Nil(meta.Worker):
return nil, fmt.Errorf("%s: missing session worker: %w", op, ErrInvalidParameter)
case is.Nil(f):
return nil, fmt.Errorf("%s: missing storage fs: %w", op, ErrInvalidParameter)
case is.Nil(keys):
return nil, fmt.Errorf("%s: missing kms keys: %w", op, ErrInvalidParameter)
}
opts := getOpts(options...)
c, err := f.New(ctx, fmt.Sprintf(bsrFile, meta.Id))
if err != nil {
return nil, err
}
nc, err := newContainer(ctx, sessionContainer, c, keys)
if err != nil {
return nil, err
}
// Sync keys and signatures
err = persistBsrSessionKeys(ctx, keys, nc)
if err != nil {
return nil, errors.Join(err, fmt.Errorf("%s: %w", op, ErrBsrKeyPersistenceFailure))
}
err = meta.writeMeta(ctx, nc)
if err != nil {
return nil, err
}
return &Session{
container: nc,
multiplexed: opts.withSupportsMultiplex,
Meta: meta,
}, nil
}
// persistBsrSessionKeys will marshal, write, and close BSR keys locally before syncing to
// storage. Any error while syncing the key files should result in the termination of
// the session this recording is for.
// The key files synced are:
// * the BSR public key, bsrKey.pub
// * the wrapped BSR key, wrappedBsrKey
// * the wrapped private key, wrappedPrivKey
// * the public key BSR signature, pubKeyBsrSignature.sign
// * the public key self signature, pubKeySelfSignature.sign
func persistBsrSessionKeys(ctx context.Context, keys *kms.Keys, c *container) error {
const op = "bsr.persistBsrSessionKeys"
switch {
case keys.PubKey == nil:
return fmt.Errorf("%s: missing kms pub key: %w", op, ErrInvalidParameter)
case keys.WrappedBsrKey == nil:
return fmt.Errorf("%s: missing kms wrapped BSR key: %w", op, ErrInvalidParameter)
case keys.WrappedPrivKey == nil:
return fmt.Errorf("%s: missing kms wrapped priv key: %w", op, ErrInvalidParameter)
case keys.PubKeyBsrSignature == nil:
return fmt.Errorf("%s: missing kms pub key BSR signature: %w", op, ErrInvalidParameter)
case keys.PubKeySelfSignature == nil:
return fmt.Errorf("%s: missing kms pub key self signature: %w", op, ErrInvalidParameter)
}
keyFiles := map[string]proto.Message{
bsrPubKeyFile: keys.PubKey,
wrappedBsrKeyFile: keys.WrappedBsrKey,
wrappedPrivKeyFile: keys.WrappedPrivKey,
pubKeyBsrSignatureFile: keys.PubKeyBsrSignature,
pubKeySelfSignatureFile: keys.PubKeySelfSignature,
}
for f, k := range keyFiles {
b, err := proto.Marshal(k)
if err != nil {
return fmt.Errorf("%s: failed to marshal data for %s: %w", op, f, err)
}
err = c.syncBsrKey(ctx, f, b)
if err != nil {
return fmt.Errorf("%s: failed syncing bsr key %s: %w", op, f, err)
}
}
return nil
}
// OpenSession retrieves a BSR from storage using the sessionRecordingId and initializes it for reading.
// Encryption keys necessary for checking signed files will be unwrapped using the keyUnwrapFn
// Signature and checksum files will then be verified.
// Fields on the underlying container will be populated so that the returned Session can be used for BSR
// playback and conversion to formats such as asciinema
func OpenSession(ctx context.Context, sessionRecordingId string, f storage.FS, keyUnwrapFn kms.KeyUnwrapCallbackFunc) (*Session, error) {
panic("not implemented")
}
// NewConnection creates a Connection container for a given connection id.
func (s *Session) NewConnection(ctx context.Context, meta *ConnectionMeta) (*Connection, error) {
const op = "bsr.(Session).NewConnection"
switch {
case is.Nil(meta):
return nil, fmt.Errorf("%s: missing connection meta: %w", op, ErrInvalidParameter)
case meta.Id == "":
return nil, fmt.Errorf("%s: missing connection id: %w", op, ErrInvalidParameter)
}
name := fmt.Sprintf(connectionFile, meta.Id)
sc, err := s.container.container.SubContainer(ctx, name)
if err != nil {
return nil, err
}
if _, err := s.WriteMeta(ctx, "connection", name); err != nil {
return nil, err
}
nc, err := newContainer(ctx, connectionContainer, sc, s.keys)
if err != nil {
return nil, err
}
if _, err := nc.WriteMeta(ctx, "id", meta.Id); err != nil {
return nil, err
}
return &Connection{
container: nc,
multiplexed: s.multiplexed,
Meta: meta,
}, nil
}
// Close closes the Session container.
func (s *Session) Close(ctx context.Context) error {
return s.container.close(ctx)
}
// ConnectionMeta contains metadata about a connection in a BSR.
type ConnectionMeta struct {
Id string
}
// Connection is a container in a bsr for a specific connection in a session
// container. It contains the files for the recorded connection.
type Connection struct {
*container
multiplexed bool
Meta *ConnectionMeta
}
// NewChannel creates a Channel container for a given channel id.
func (c *Connection) NewChannel(ctx context.Context, meta *ChannelMeta) (*Channel, error) {
const op = "bsr.(Connection).NewChannel"
if !c.multiplexed {
return nil, fmt.Errorf("%s: connection cannot make channels: %w", op, ErrNotSupported)
}
switch {
case is.Nil(meta):
return nil, fmt.Errorf("%s: missing channel meta: %w", op, ErrInvalidParameter)
case meta.Id == "":
return nil, fmt.Errorf("%s: missing channel id: %w", op, ErrInvalidParameter)
}
name := fmt.Sprintf(channelFile, meta.Id)
sc, err := c.container.container.SubContainer(ctx, name)
if err != nil {
return nil, err
}
if _, err := c.WriteMeta(ctx, "channel", name); err != nil {
return nil, err
}
nc, err := newContainer(ctx, channelContainer, sc, c.keys)
if err != nil {
return nil, err
}
if _, err := nc.WriteMeta(ctx, "id", meta.Id); err != nil {
return nil, err
}
return &Channel{
container: nc,
Meta: meta,
}, nil
}
// NewMessagesWriter creates a writer for recording channel messages.
func (c *Connection) NewMessagesWriter(ctx context.Context, dir Direction) (io.Writer, error) {
const op = "bsr.(Connection).NewMessagesWriter"
switch {
case !ValidDirection(dir):
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
}
messagesName := fmt.Sprintf(messagesFile, dir.String())
_, err := c.container.WriteMeta(ctx, "messages", dir.String())
if err != nil {
return nil, err
}
m, err := c.container.create(ctx, messagesName)
if err != nil {
return nil, err
}
return checksum.NewFile(ctx, m, c.checksums)
}
// NewRequestsWriter creates a writer for recording connection requests.
func (c *Connection) NewRequestsWriter(ctx context.Context, dir Direction) (io.Writer, error) {
const op = "bsr.(Connection).NewRequestsWriter"
switch {
case !ValidDirection(dir):
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
}
requestName := fmt.Sprintf(requestsFile, dir.String())
_, err := c.container.WriteMeta(ctx, "requests", dir.String())
if err != nil {
return nil, err
}
m, err := c.container.create(ctx, requestName)
if err != nil {
return nil, err
}
return checksum.NewFile(ctx, m, c.checksums)
}
// Close closes the Connection container.
func (c *Connection) Close(ctx context.Context) error {
return c.container.close(ctx)
}
// ChannelMeta contains metadata about a channel in a BSR.
type ChannelMeta struct {
Id string
Type string
}
// Channel is a container in a bsr for a specific channel in a session
// container. It contains the files for the recorded channel.
type Channel struct {
*container
Meta *ChannelMeta
}
// Close closes the Channel container.
func (c *Channel) Close(ctx context.Context) error {
return c.container.close(ctx)
}
// NewMessagesWriter creates a writer for recording channel messages.
func (c *Channel) NewMessagesWriter(ctx context.Context, dir Direction) (io.Writer, error) {
const op = "bsr.(Channel).NewMessagesWriter"
switch {
case !ValidDirection(dir):
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
}
messagesName := fmt.Sprintf(messagesFile, dir.String())
_, err := c.container.WriteMeta(ctx, "messages", dir.String())
if err != nil {
return nil, err
}
m, err := c.container.create(ctx, messagesName)
if err != nil {
return nil, err
}
return checksum.NewFile(ctx, m, c.checksums)
}
// NewRequestsWriter creates a writer for recording channel requests.
func (c *Channel) NewRequestsWriter(ctx context.Context, dir Direction) (io.Writer, error) {
const op = "bsr.(Channel).NewRequestsWriter"
switch {
case !ValidDirection(dir):
return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
}
requestName := fmt.Sprintf(requestsFile, dir.String())
_, err := c.container.WriteMeta(ctx, "requests", dir.String())
if err != nil {
return nil, err
}
m, err := c.container.create(ctx, requestName)
if err != nil {
return nil, err
}
return checksum.NewFile(ctx, m, c.checksums)
}