Skip to content

Commit

Permalink
efs: add encrypted file system store
Browse files Browse the repository at this point in the history
Add a keystore implementation, that wraps a file system store, but encrypts the files.

Signed-off-by: Sascha Wolke <[email protected]>
  • Loading branch information
derSascha committed Oct 30, 2024
1 parent db17a10 commit a689183
Show file tree
Hide file tree
Showing 8 changed files with 573 additions and 0 deletions.
165 changes: 165 additions & 0 deletions internal/keystore/efs/efs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2024 - MinIO, Inc. All rights reserved.
// Use of this source code is governed by the AGPLv3
// license that can be found in the LICENSE file.

// Package efs implements a key-value store that
// stores keys as file names and values as encrypted
// file content.
//
// It wraps a fs store and in addition, encrypt the keys.
package efs

import (
"context"
"errors"
"fmt"
"io"
"os"

"aead.dev/mem"
"github.com/minio/kes"
"github.com/minio/kes/internal/crypto"
"github.com/minio/kes/internal/fips"
"github.com/minio/kes/internal/keystore/fs"
)

// NewStore returns a new Store that reads
// from and writes to the given directory,
// using encryption.
//
// If the directory or any parent directory
// does not exist, NewStore creates them all.
//
// It returns an error if dir exists but is
// not a directory.
func NewStore(keyPath string, keyCipher string, dir string) (*Store, error) {
fsStore, err := fs.NewStore(dir)
if err != nil {
return nil, err
}

key, err := loadMasterKey(keyPath, keyCipher)
if err != nil {
return nil, err
}

return &Store{key: key, fsStore: fsStore}, nil
}

// loadMasterKey reads a secret key from a
// given path.
//
// If the key file does not exist, or contains
// an unexpected amount of bytes, it returns an error.
func loadMasterKey(keyPath string, keyCipher string) (crypto.SecretKey, error) {
file, err := os.Open(keyPath)
if errors.Is(err, os.ErrNotExist) {
return crypto.SecretKey{}, fmt.Errorf("master key not found: '%s'", keyPath)
}
if err != nil {
return crypto.SecretKey{}, err
}
defer file.Close()

const MaxSize = crypto.SecretKeySize + 1
value, err := io.ReadAll(mem.LimitReader(file, MaxSize))
if err != nil {
return crypto.SecretKey{}, err
}
if err = file.Close(); err != nil {
return crypto.SecretKey{}, err
}
if len(value) != crypto.SecretKeySize {
return crypto.SecretKey{}, fmt.Errorf("invalid master key size for '%s'", keyCipher)
}

cipher, err := crypto.ParseSecretKeyType(keyCipher)
if err != nil {
return crypto.SecretKey{}, err
}
if cipher == crypto.ChaCha20 && fips.Enabled {
return crypto.SecretKey{}, fmt.Errorf("master key algorithm '%s' not supported by FIPS 140-2", keyCipher)
}

return crypto.NewSecretKey(cipher, value)
}

// Store is a connection to a directory on
// the filesystem using a secret key to encrypt the files.
//
// It implements the kms.Store interface and
// acts as KMS abstraction over a filesystem.
type Store struct {
key crypto.SecretKey
fsStore *fs.Store
}

func (s *Store) String() string { return "Encrypted Filesystem: " + s.fsStore.Dir() }

// Status returns the current state of the Conn.
//
// In particular, it reports whether the underlying
// filesystem is accessible.
func (s *Store) Status(ctx context.Context) (kes.KeyStoreState, error) {
return s.fsStore.Status(ctx)
}

// Create creates a new file with the given name inside
// the Conn directory if and only if no such file exists.
//
// It returns kes.ErrKeyExists if such a file already exists.
func (s *Store) Create(ctx context.Context, name string, value []byte) error {
context := fmt.Sprintf("name=%s", name)
encryptedValue, err := s.key.Encrypt(value, []byte(context))
if err != nil {
return err
}

return s.fsStore.Create(ctx, name, encryptedValue)
}

// Get reads the content of the named file within the Conn
// directory. It returns kes.ErrKeyNotFound if no such file
// exists.
func (s *Store) Get(ctx context.Context, name string) ([]byte, error) {
encryptedValue, err := s.fsStore.Get(ctx, name)
if err != nil {
return nil, err
}

context := fmt.Sprintf("name=%s", name)
value, err := s.key.Decrypt(encryptedValue, []byte(context))
if err != nil {
return nil, err
}

return value, nil
}

// Delete deletes the named file within the Conn directory if
// and only if it exists. It returns kes.ErrKeyNotFound if
// no such file exists.
func (s *Store) Delete(ctx context.Context, name string) error {
return s.fsStore.Delete(ctx, name)
}

// List returns a new Iterator over the names of
// all stored keys.
// List returns the first n key names, that start with the given
// prefix, and the next prefix from which the listing should
// continue.
//
// It returns all keys with the prefix if n < 0 and less than n
// names if n is greater than the number of keys with the prefix.
//
// An empty prefix matches any key name. At the end of the listing
// or when there are no (more) keys starting with the prefix, the
// returned prefix is empty
func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) {
return s.fsStore.List(ctx, prefix, n)
}

// Close closes the Store.
func (s *Store) Close() error {
return s.fsStore.Close()
}
3 changes: 3 additions & 0 deletions internal/keystore/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type Store struct {

func (s *Store) String() string { return "Filesystem: " + s.dir }

// Dir returns the directory used on the filesystem.
func (s *Store) Dir() string { return s.dir }

// Status returns the current state of the Conn.
//
// In particular, it reports whether the underlying
Expand Down
23 changes: 23 additions & 0 deletions kesconf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type ymlFile struct {
FS *struct {
Path env[string] `yaml:"path"`
}
EncryptedFS *struct {
MasterKeyPath env[string] `yaml:"masterKeyPath"`
MasterKeyCipher env[string] `yaml:"masterKeyCipher"`
Path env[string] `yaml:"path"`
} `yaml:"encryptedfs"`
KES *struct {
Endpoint []env[string] `yaml:"endpoint"`
Enclave env[string] `yaml:"enclave"`
Expand Down Expand Up @@ -416,6 +421,24 @@ func ymlToKeyStore(y *ymlFile) (KeyStore, error) {
}
}

// Encrypted FS Keystore
if y.KeyStore.EncryptedFS != nil {
if y.KeyStore.EncryptedFS.MasterKeyPath.Value == "" {
return nil, errors.New("kesconf: invalid encryptedfs keystore: no master key path specified")
}
if y.KeyStore.EncryptedFS.MasterKeyCipher.Value == "" {
return nil, errors.New("kesconf: invalid encryptedfs keystore: no master key cipher specified")
}
if y.KeyStore.EncryptedFS.Path.Value == "" {
return nil, errors.New("kesconf: invalid encryptedfs keystore: no path specified")
}
keystore = &EncryptedFSKeyStore{
MasterKeyPath: y.KeyStore.EncryptedFS.MasterKeyPath.Value,
MasterKeyCipher: y.KeyStore.EncryptedFS.MasterKeyCipher.Value,
Path: y.KeyStore.EncryptedFS.Path.Value,
}
}

// Hashicorp Vault Keystore
if y.KeyStore.Vault != nil {
if keystore != nil {
Expand Down
29 changes: 29 additions & 0 deletions kesconf/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ func TestReadServerConfigYAML_FS(t *testing.T) {
}
}

func TestReadServerConfigYAML_EncryptedFS(t *testing.T) {
const (
Filename = "./testdata/efs.yml"
MasterKeyPath = "./kes-master-key"
MasterKeyCipher = "master-key-cipher"
FSPath = "/tmp/keys"
)

config, err := ReadFile(Filename)
if err != nil {
t.Fatalf("Failed to read file '%s': %v", Filename, err)
}

fs, ok := config.KeyStore.(*EncryptedFSKeyStore)
if !ok {
var want *EncryptedFSKeyStore
t.Fatalf("Invalid keystore: got type '%T' - want type '%T'", config.KeyStore, want)
}
if fs.MasterKeyPath != MasterKeyPath {
t.Fatalf("Invalid keystore: got master key path '%s' - want path '%s'", fs.MasterKeyPath, MasterKeyPath)
}
if fs.MasterKeyCipher != MasterKeyCipher {
t.Fatalf("Invalid keystore: got master key cipher '%s' - want cipher '%s'", fs.MasterKeyCipher, MasterKeyCipher)
}
if fs.Path != FSPath {
t.Fatalf("Invalid keystore: got path '%s' - want path '%s'", fs.Path, FSPath)
}
}

func TestReadServerConfigYAML_CustomAPI(t *testing.T) {
const (
Filename = "./testdata/custom-api.yml"
Expand Down
Loading

0 comments on commit a689183

Please sign in to comment.