vault/command/agentproxyshared/cache/cacheboltdb/bolt.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

437 lines
13 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cacheboltdb
import (
"context"
"encoding/binary"
"fmt"
"os"
"path/filepath"
"time"
"github.com/golang/protobuf/proto"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-multierror"
bolt "go.etcd.io/bbolt"
)
const (
// Keep track of schema version for future migrations
storageVersionKey = "version"
storageVersion = "2" // v2 merges auth-lease and secret-lease buckets into one ordered bucket
// DatabaseFileName - filename for the persistent cache file
DatabaseFileName = "vault-agent-cache.db"
// metaBucketName - naming the meta bucket that holds the version and
// bootstrapping keys
metaBucketName = "meta"
// DEPRECATED: secretLeaseType - v1 Bucket/type for leases with secret info
secretLeaseType = "secret-lease"
// DEPRECATED: authLeaseType - v1 Bucket/type for leases with auth info
authLeaseType = "auth-lease"
// TokenType - Bucket/type for auto-auth tokens
TokenType = "token"
// LeaseType - v2 Bucket/type for auth AND secret leases.
//
// This bucket stores keys in the same order they were created using
// auto-incrementing keys and the fact that BoltDB stores keys in byte
// slice order. This means when we iterate through this bucket during
// restore, we will always restore parent tokens before their children,
// allowing us to correctly attach child contexts to their parent's context.
LeaseType = "lease"
// lookupType - v2 Bucket/type to map from a memcachedb index ID to an
// auto-incrementing BoltDB key. Facilitates deletes from the lease
// bucket using an ID instead of the auto-incrementing BoltDB key.
lookupType = "lookup"
// AutoAuthToken - key for the latest auto-auth token
AutoAuthToken = "auto-auth-token"
// RetrievalTokenMaterial is the actual key or token in the key bucket
RetrievalTokenMaterial = "retrieval-token-material"
)
// BoltStorage is a persistent cache using a bolt db. Items are organized with
// the version and bootstrapping items in the "meta" bucket, and tokens, auth
// leases, and secret leases in their own buckets.
type BoltStorage struct {
db *bolt.DB
logger hclog.Logger
wrapper wrapping.Wrapper
aad string
}
// BoltStorageConfig is the collection of input parameters for setting up bolt
// storage
type BoltStorageConfig struct {
Path string
Logger hclog.Logger
Wrapper wrapping.Wrapper
AAD string
}
// NewBoltStorage opens a new bolt db at the specified file path and returns it.
// If the db already exists the buckets will just be created if they don't
// exist.
func NewBoltStorage(config *BoltStorageConfig) (*BoltStorage, error) {
dbPath := filepath.Join(config.Path, DatabaseFileName)
db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, err
}
err = db.Update(func(tx *bolt.Tx) error {
return createBoltSchema(tx, storageVersion)
})
if err != nil {
return nil, err
}
bs := &BoltStorage{
db: db,
logger: config.Logger,
wrapper: config.Wrapper,
aad: config.AAD,
}
return bs, nil
}
func createBoltSchema(tx *bolt.Tx, createVersion string) error {
switch {
case createVersion == "1":
if err := createV1BoltSchema(tx); err != nil {
return err
}
case createVersion == "2":
if err := createV2BoltSchema(tx); err != nil {
return err
}
default:
return fmt.Errorf("schema version %s not supported", createVersion)
}
meta, err := tx.CreateBucketIfNotExists([]byte(metaBucketName))
if err != nil {
return fmt.Errorf("failed to create bucket %s: %w", metaBucketName, err)
}
// Check and set file version in the meta bucket.
version := meta.Get([]byte(storageVersionKey))
switch {
case version == nil:
err = meta.Put([]byte(storageVersionKey), []byte(createVersion))
if err != nil {
return fmt.Errorf("failed to set storage version: %w", err)
}
return nil
case string(version) == createVersion:
return nil
case string(version) == "1" && createVersion == "2":
return migrateFromV1ToV2Schema(tx)
default:
return fmt.Errorf("storage migration from %s to %s not implemented", string(version), createVersion)
}
}
func createV1BoltSchema(tx *bolt.Tx) error {
// Create the buckets for tokens and leases.
for _, bucket := range []string{TokenType, authLeaseType, secretLeaseType} {
if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil {
return fmt.Errorf("failed to create %s bucket: %w", bucket, err)
}
}
return nil
}
func createV2BoltSchema(tx *bolt.Tx) error {
// Create the buckets for tokens and leases.
for _, bucket := range []string{TokenType, LeaseType, lookupType} {
if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil {
return fmt.Errorf("failed to create %s bucket: %w", bucket, err)
}
}
return nil
}
func migrateFromV1ToV2Schema(tx *bolt.Tx) error {
if err := createV2BoltSchema(tx); err != nil {
return err
}
for _, v1BucketType := range []string{authLeaseType, secretLeaseType} {
if bucket := tx.Bucket([]byte(v1BucketType)); bucket != nil {
bucket.ForEach(func(key, value []byte) error {
autoIncKey, err := autoIncrementedLeaseKey(tx, string(key))
if err != nil {
return fmt.Errorf("error migrating %s %q key to auto incremented key: %w", v1BucketType, string(key), err)
}
if err := tx.Bucket([]byte(LeaseType)).Put(autoIncKey, value); err != nil {
return fmt.Errorf("error migrating %s %q from v1 to v2 schema: %w", v1BucketType, string(key), err)
}
return nil
})
if err := tx.DeleteBucket([]byte(v1BucketType)); err != nil {
return fmt.Errorf("failed to clean up %s bucket during v1 to v2 schema migration: %w", v1BucketType, err)
}
}
}
meta, err := tx.CreateBucketIfNotExists([]byte(metaBucketName))
if err != nil {
return fmt.Errorf("failed to create meta bucket: %w", err)
}
if err := meta.Put([]byte(storageVersionKey), []byte(storageVersion)); err != nil {
return fmt.Errorf("failed to update schema from v1 to v2: %w", err)
}
return nil
}
func autoIncrementedLeaseKey(tx *bolt.Tx, id string) ([]byte, error) {
leaseBucket := tx.Bucket([]byte(LeaseType))
keyValue, err := leaseBucket.NextSequence()
if err != nil {
return nil, fmt.Errorf("failed to generate lookup key for id %q: %w", id, err)
}
key := make([]byte, 8)
// MUST be big endian, because keys are ordered by byte slice comparison
// which progressively compares each byte in the slice starting at index 0.
// BigEndian in the range [255-257] looks like this:
// [0 0 0 0 0 0 0 255]
// [0 0 0 0 0 0 1 0]
// [0 0 0 0 0 0 1 1]
// LittleEndian in the same range looks like this:
// [255 0 0 0 0 0 0 0]
// [0 1 0 0 0 0 0 0]
// [1 1 0 0 0 0 0 0]
binary.BigEndian.PutUint64(key, keyValue)
err = tx.Bucket([]byte(lookupType)).Put([]byte(id), key)
if err != nil {
return nil, err
}
return key, nil
}
// Set an index (token or lease) in bolt storage
func (b *BoltStorage) Set(ctx context.Context, id string, plaintext []byte, indexType string) error {
blob, err := b.wrapper.Encrypt(ctx, plaintext, wrapping.WithAad([]byte(b.aad)))
if err != nil {
return fmt.Errorf("error encrypting %s index: %w", indexType, err)
}
protoBlob, err := proto.Marshal(blob)
if err != nil {
return err
}
return b.db.Update(func(tx *bolt.Tx) error {
var key []byte
switch indexType {
case LeaseType:
// If this is a lease type, generate an auto-incrementing key and
// store an ID -> key lookup entry
key, err = autoIncrementedLeaseKey(tx, id)
if err != nil {
return err
}
case TokenType:
// If this is an auto-auth token, also stash it in the meta bucket for
// easy retrieval upon restore
key = []byte(id)
meta := tx.Bucket([]byte(metaBucketName))
if err := meta.Put([]byte(AutoAuthToken), protoBlob); err != nil {
return fmt.Errorf("failed to set latest auto-auth token: %w", err)
}
default:
return fmt.Errorf("called Set for unsupported type %q", indexType)
}
s := tx.Bucket([]byte(indexType))
if s == nil {
return fmt.Errorf("bucket %q not found", indexType)
}
return s.Put(key, protoBlob)
})
}
// Delete an index (token or lease) by key from bolt storage
func (b *BoltStorage) Delete(id string, indexType string) error {
return b.db.Update(func(tx *bolt.Tx) error {
key := []byte(id)
if indexType == LeaseType {
key = tx.Bucket([]byte(lookupType)).Get(key)
if key == nil {
return fmt.Errorf("failed to lookup bolt DB key for id %q", id)
}
err := tx.Bucket([]byte(lookupType)).Delete([]byte(id))
if err != nil {
return fmt.Errorf("failed to delete %q from lookup bucket: %w", id, err)
}
}
bucket := tx.Bucket([]byte(indexType))
if bucket == nil {
return fmt.Errorf("bucket %q not found during delete", indexType)
}
if err := bucket.Delete(key); err != nil {
return fmt.Errorf("failed to delete %q from %q bucket: %w", id, indexType, err)
}
b.logger.Trace("deleted index from bolt db", "id", id)
return nil
})
}
func (b *BoltStorage) decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) {
var blob wrapping.BlobInfo
if err := proto.Unmarshal(ciphertext, &blob); err != nil {
return nil, err
}
return b.wrapper.Decrypt(ctx, &blob, wrapping.WithAad([]byte(b.aad)))
}
// GetByType returns a list of stored items of the specified type
func (b *BoltStorage) GetByType(ctx context.Context, indexType string) ([][]byte, error) {
var returnBytes [][]byte
err := b.db.View(func(tx *bolt.Tx) error {
var errors *multierror.Error
bucket := tx.Bucket([]byte(indexType))
if bucket == nil {
return fmt.Errorf("bucket %q not found", indexType)
}
bucket.ForEach(func(key, ciphertext []byte) error {
plaintext, err := b.decrypt(ctx, ciphertext)
if err != nil {
errors = multierror.Append(errors, fmt.Errorf("error decrypting entry %s: %w", key, err))
return nil
}
returnBytes = append(returnBytes, plaintext)
return nil
})
return errors.ErrorOrNil()
})
return returnBytes, err
}
// GetAutoAuthToken retrieves the latest auto-auth token, and returns nil if non
// exists yet
func (b *BoltStorage) GetAutoAuthToken(ctx context.Context) ([]byte, error) {
var encryptedToken []byte
err := b.db.View(func(tx *bolt.Tx) error {
meta := tx.Bucket([]byte(metaBucketName))
if meta == nil {
return fmt.Errorf("bucket %q not found", metaBucketName)
}
value := meta.Get([]byte(AutoAuthToken))
if value != nil {
encryptedToken = make([]byte, len(value))
copy(encryptedToken, value)
}
return nil
})
if err != nil {
return nil, err
}
if encryptedToken == nil {
return nil, nil
}
plaintext, err := b.decrypt(ctx, encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt auto-auth token: %w", err)
}
return plaintext, nil
}
// GetRetrievalToken retrieves a plaintext token from the KeyBucket, which will
// be used by the key manager to retrieve the encryption key, nil if none set
func (b *BoltStorage) GetRetrievalToken() ([]byte, error) {
var token []byte
err := b.db.View(func(tx *bolt.Tx) error {
metaBucket := tx.Bucket([]byte(metaBucketName))
if metaBucket == nil {
return fmt.Errorf("bucket %q not found", metaBucketName)
}
value := metaBucket.Get([]byte(RetrievalTokenMaterial))
if value != nil {
token = make([]byte, len(value))
copy(token, value)
}
return nil
})
if err != nil {
return nil, err
}
return token, err
}
// StoreRetrievalToken sets plaintext token material in the RetrievalTokenBucket
func (b *BoltStorage) StoreRetrievalToken(token []byte) error {
return b.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(metaBucketName))
if bucket == nil {
return fmt.Errorf("bucket %q not found", metaBucketName)
}
return bucket.Put([]byte(RetrievalTokenMaterial), token)
})
}
// Close the boltdb
func (b *BoltStorage) Close() error {
b.logger.Trace("closing bolt db", "path", b.db.Path())
return b.db.Close()
}
// Clear the boltdb by deleting all the token and lease buckets and recreating
// the schema/layout
func (b *BoltStorage) Clear() error {
return b.db.Update(func(tx *bolt.Tx) error {
for _, name := range []string{TokenType, LeaseType, lookupType} {
b.logger.Trace("deleting bolt bucket", "name", name)
if err := tx.DeleteBucket([]byte(name)); err != nil {
return err
}
}
return createBoltSchema(tx, storageVersion)
})
}
// DBFileExists checks whether the vault agent cache file at `filePath` exists
func DBFileExists(path string) (bool, error) {
checkFile, err := os.OpenFile(filepath.Join(path, DatabaseFileName), os.O_RDWR, 0o600)
defer checkFile.Close()
switch {
case err == nil:
return true, nil
case os.IsNotExist(err):
return false, nil
default:
return false, fmt.Errorf("failed to check if bolt file exists at path %s: %w", path, err)
}
}