github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/baseccl/encryption_spec.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  package baseccl
    10  
    11  import (
    12  	"bytes"
    13  	"fmt"
    14  	"path/filepath"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/base"
    19  	"github.com/cockroachdb/cockroach/pkg/ccl/cliccl/cliflagsccl"
    20  	"github.com/cockroachdb/cockroach/pkg/util/protoutil"
    21  	"github.com/cockroachdb/errors"
    22  	"github.com/spf13/pflag"
    23  )
    24  
    25  // DefaultRotationPeriod is the rotation period used if not specified.
    26  const DefaultRotationPeriod = time.Hour * 24 * 7 // 1 week, give or take time changes.
    27  
    28  // Special value of key paths to mean "no encryption". We do not accept empty fields.
    29  const plaintextFieldValue = "plain"
    30  
    31  // StoreEncryptionSpec contains the details that can be specified in the cli via
    32  // the --enterprise-encryption flag.
    33  type StoreEncryptionSpec struct {
    34  	Path           string
    35  	KeyPath        string
    36  	OldKeyPath     string
    37  	RotationPeriod time.Duration
    38  }
    39  
    40  // Convert to a serialized EncryptionOptions protobuf.
    41  func (es StoreEncryptionSpec) toEncryptionOptions() ([]byte, error) {
    42  	opts := EncryptionOptions{
    43  		KeySource: EncryptionKeySource_KeyFiles,
    44  		KeyFiles: &EncryptionKeyFiles{
    45  			CurrentKey: es.KeyPath,
    46  			OldKey:     es.OldKeyPath,
    47  		},
    48  		DataKeyRotationPeriod: int64(es.RotationPeriod / time.Second),
    49  	}
    50  
    51  	return protoutil.Marshal(&opts)
    52  }
    53  
    54  // String returns a fully parsable version of the encryption spec.
    55  func (es StoreEncryptionSpec) String() string {
    56  	// All fields are set.
    57  	return fmt.Sprintf("path=%s,key=%s,old-key=%s,rotation-period=%s",
    58  		es.Path, es.KeyPath, es.OldKeyPath, es.RotationPeriod)
    59  }
    60  
    61  // NewStoreEncryptionSpec parses the string passed in and returns a new
    62  // StoreEncryptionSpec if parsing succeeds.
    63  // TODO(mberhault): we should share the parsing code with the StoreSpec.
    64  func NewStoreEncryptionSpec(value string) (StoreEncryptionSpec, error) {
    65  	const pathField = "path"
    66  	var es StoreEncryptionSpec
    67  	es.RotationPeriod = DefaultRotationPeriod
    68  
    69  	used := make(map[string]struct{})
    70  	for _, split := range strings.Split(value, ",") {
    71  		if len(split) == 0 {
    72  			continue
    73  		}
    74  		subSplits := strings.SplitN(split, "=", 2)
    75  		if len(subSplits) == 1 {
    76  			return StoreEncryptionSpec{}, fmt.Errorf("field not in the form <key>=<value>: %s", split)
    77  		}
    78  		field := strings.ToLower(subSplits[0])
    79  		value := subSplits[1]
    80  		if _, ok := used[field]; ok {
    81  			return StoreEncryptionSpec{}, fmt.Errorf("%s field was used twice in encryption definition", field)
    82  		}
    83  		used[field] = struct{}{}
    84  
    85  		if len(field) == 0 {
    86  			return StoreEncryptionSpec{}, fmt.Errorf("empty field")
    87  		}
    88  		if len(value) == 0 {
    89  			return StoreEncryptionSpec{}, fmt.Errorf("no value specified for %s", field)
    90  		}
    91  
    92  		switch field {
    93  		case pathField:
    94  			var err error
    95  			es.Path, err = base.GetAbsoluteStorePath(pathField, value)
    96  			if err != nil {
    97  				return StoreEncryptionSpec{}, err
    98  			}
    99  		case "key":
   100  			if value == plaintextFieldValue {
   101  				es.KeyPath = plaintextFieldValue
   102  			} else {
   103  				var err error
   104  				es.KeyPath, err = base.GetAbsoluteStorePath("key", value)
   105  				if err != nil {
   106  					return StoreEncryptionSpec{}, err
   107  				}
   108  			}
   109  		case "old-key":
   110  			if value == plaintextFieldValue {
   111  				es.OldKeyPath = plaintextFieldValue
   112  			} else {
   113  				var err error
   114  				es.OldKeyPath, err = base.GetAbsoluteStorePath("old-key", value)
   115  				if err != nil {
   116  					return StoreEncryptionSpec{}, err
   117  				}
   118  			}
   119  		case "rotation-period":
   120  			var err error
   121  			es.RotationPeriod, err = time.ParseDuration(value)
   122  			if err != nil {
   123  				return StoreEncryptionSpec{}, errors.Wrapf(err, "could not parse rotation-duration value: %s", value)
   124  			}
   125  		default:
   126  			return StoreEncryptionSpec{}, fmt.Errorf("%s is not a valid enterprise-encryption field", field)
   127  		}
   128  	}
   129  
   130  	// Check that all fields are set.
   131  	if es.Path == "" {
   132  		return StoreEncryptionSpec{}, fmt.Errorf("no path specified")
   133  	}
   134  	if es.KeyPath == "" {
   135  		return StoreEncryptionSpec{}, fmt.Errorf("no key specified")
   136  	}
   137  	if es.OldKeyPath == "" {
   138  		return StoreEncryptionSpec{}, fmt.Errorf("no old-key specified")
   139  	}
   140  
   141  	return es, nil
   142  }
   143  
   144  // StoreEncryptionSpecList contains a slice of StoreEncryptionSpecs that implements pflag's value
   145  // interface.
   146  type StoreEncryptionSpecList struct {
   147  	Specs []StoreEncryptionSpec
   148  }
   149  
   150  var _ pflag.Value = &StoreEncryptionSpecList{}
   151  
   152  // String returns a string representation of all the StoreEncryptionSpecs. This is part
   153  // of pflag's value interface.
   154  func (encl StoreEncryptionSpecList) String() string {
   155  	var buffer bytes.Buffer
   156  	for _, ss := range encl.Specs {
   157  		fmt.Fprintf(&buffer, "--%s=%s ", cliflagsccl.EnterpriseEncryption.Name, ss)
   158  	}
   159  	// Trim the extra space from the end if it exists.
   160  	if l := buffer.Len(); l > 0 {
   161  		buffer.Truncate(l - 1)
   162  	}
   163  	return buffer.String()
   164  }
   165  
   166  // Type returns the underlying type in string form. This is part of pflag's
   167  // value interface.
   168  func (encl *StoreEncryptionSpecList) Type() string {
   169  	return "StoreEncryptionSpec"
   170  }
   171  
   172  // Set adds a new value to the StoreEncryptionSpecValue. It is the important part of
   173  // pflag's value interface.
   174  func (encl *StoreEncryptionSpecList) Set(value string) error {
   175  	spec, err := NewStoreEncryptionSpec(value)
   176  	if err != nil {
   177  		return err
   178  	}
   179  	if encl.Specs == nil {
   180  		encl.Specs = []StoreEncryptionSpec{spec}
   181  	} else {
   182  		encl.Specs = append(encl.Specs, spec)
   183  	}
   184  	return nil
   185  }
   186  
   187  // PopulateStoreSpecWithEncryption iterates through the StoreEncryptionSpecList and looks
   188  // for matching paths in the StoreSpecList.
   189  // Any unmatched StoreEncryptionSpec causes an error.
   190  // Matching stores have a few encryption-related fields set.
   191  func PopulateStoreSpecWithEncryption(
   192  	storeSpecs base.StoreSpecList, encryptionSpecs StoreEncryptionSpecList,
   193  ) error {
   194  	for _, es := range encryptionSpecs.Specs {
   195  		var found bool
   196  		for i := range storeSpecs.Specs {
   197  			if storeSpecs.Specs[i].Path != es.Path {
   198  				continue
   199  			}
   200  
   201  			// Found a matching path.
   202  			if storeSpecs.Specs[i].UseFileRegistry {
   203  				return fmt.Errorf("store with path %s already has an encryption setting",
   204  					storeSpecs.Specs[i].Path)
   205  			}
   206  
   207  			// Tell the store we absolutely need the file registry.
   208  			storeSpecs.Specs[i].UseFileRegistry = true
   209  			opts, err := es.toEncryptionOptions()
   210  			if err != nil {
   211  				return err
   212  			}
   213  			storeSpecs.Specs[i].ExtraOptions = opts
   214  			found = true
   215  			break
   216  		}
   217  		if !found {
   218  			return fmt.Errorf("no store with path %s found for encryption setting: %v", es.Path, es)
   219  		}
   220  	}
   221  	return nil
   222  }
   223  
   224  // EncryptionOptionsForStore takes a store directory and returns its ExtraOptions
   225  // if a matching entry if found in the StoreEncryptionSpecList.
   226  // The caller should appropriately set UseFileRegistry on a non-nil result.
   227  func EncryptionOptionsForStore(
   228  	dir string, encryptionSpecs StoreEncryptionSpecList,
   229  ) ([]byte, error) {
   230  	// We need an absolute path, but the input may have come in relative.
   231  	path, err := filepath.Abs(dir)
   232  	if err != nil {
   233  		return nil, errors.Wrapf(err, "could not find absolute path for %s ", dir)
   234  	}
   235  
   236  	for _, es := range encryptionSpecs.Specs {
   237  		if es.Path == path {
   238  			return es.toEncryptionOptions()
   239  		}
   240  	}
   241  
   242  	return nil, nil
   243  }