|
|
|
// Minio Cloud Storage, (C) 2015, 2016, 2017, 2018 Minio, Inc.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package crypto
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
vault "github.com/hashicorp/vault/api"
|
|
|
|
"github.com/minio/minio/cmd/logger"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
//ErrKMSAuthLogin is raised when there is a failure authenticating to KMS
|
|
|
|
ErrKMSAuthLogin = errors.New("Vault service did not return auth info")
|
|
|
|
)
|
|
|
|
|
|
|
|
// VaultKey represents vault encryption key-ring.
|
|
|
|
type VaultKey struct {
|
|
|
|
Name string `json:"name"` // The name of the encryption key-ring
|
|
|
|
Version int `json:"version"` // The key version
|
|
|
|
}
|
|
|
|
|
|
|
|
// VaultAuth represents vault authentication type.
|
|
|
|
// Currently the only supported authentication type is AppRole.
|
|
|
|
type VaultAuth struct {
|
|
|
|
Type string `json:"type"` // The authentication type
|
|
|
|
AppRole VaultAppRole `json:"approle"` // The AppRole authentication credentials
|
|
|
|
}
|
|
|
|
|
|
|
|
// VaultAppRole represents vault AppRole authentication credentials
|
|
|
|
type VaultAppRole struct {
|
|
|
|
ID string `json:"id"` // The AppRole access ID
|
|
|
|
Secret string `json:"secret"` // The AppRole secret
|
|
|
|
}
|
|
|
|
|
|
|
|
// VaultConfig represents vault configuration.
|
|
|
|
type VaultConfig struct {
|
|
|
|
Endpoint string `json:"endpoint"` // The vault API endpoint as URL
|
|
|
|
CAPath string `json:"-"` // The path to PEM-encoded certificate files used for mTLS. Currently not used in config file.
|
|
|
|
Auth VaultAuth `json:"auth"` // The vault authentication configuration
|
|
|
|
Key VaultKey `json:"key-id"` // The named key used for key-generation / decryption.
|
|
|
|
Namespace string `json:"-"` // The vault namespace of enterprise vault instances
|
|
|
|
}
|
|
|
|
|
|
|
|
// vaultService represents a connection to a vault KMS.
|
|
|
|
type vaultService struct {
|
|
|
|
config *VaultConfig
|
|
|
|
client *vault.Client
|
|
|
|
leaseDuration time.Duration
|
|
|
|
tokenRenewer *vault.Renewer
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ KMS = (*vaultService)(nil) // compiler check that *vaultService implements KMS
|
|
|
|
|
|
|
|
// empty/default vault configuration used to check whether a particular is empty.
|
|
|
|
var emptyVaultConfig = VaultConfig{}
|
|
|
|
|
|
|
|
// IsEmpty returns true if the vault config struct is an
|
|
|
|
// empty configuration.
|
|
|
|
func (v *VaultConfig) IsEmpty() bool { return *v == emptyVaultConfig }
|
|
|
|
|
|
|
|
// Verify returns a nil error if the vault configuration
|
|
|
|
// is valid. A valid configuration is either empty or
|
|
|
|
// contains valid non-default values.
|
|
|
|
func (v *VaultConfig) Verify() (err error) {
|
|
|
|
if v.IsEmpty() {
|
|
|
|
return // an empty configuration is valid
|
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case v.Endpoint == "":
|
|
|
|
err = errors.New("crypto: missing hashicorp vault endpoint")
|
|
|
|
case strings.ToLower(v.Auth.Type) != "approle":
|
|
|
|
err = fmt.Errorf("crypto: invalid hashicorp vault authentication type: %s is not supported", v.Auth.Type)
|
|
|
|
case v.Auth.AppRole.ID == "":
|
|
|
|
err = errors.New("crypto: missing hashicorp vault AppRole ID")
|
|
|
|
case v.Auth.AppRole.Secret == "":
|
|
|
|
err = errors.New("crypto: missing hashicorp vault AppSecret ID")
|
|
|
|
case v.Key.Name == "":
|
|
|
|
err = errors.New("crypto: missing hashicorp vault key name")
|
|
|
|
case v.Key.Version < 0:
|
|
|
|
err = errors.New("crypto: invalid hashicorp vault key version: The key version must not be negative")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewVault initializes Hashicorp Vault KMS by authenticating
|
|
|
|
// to Vault with the credentials in config and gets a client
|
|
|
|
// token for future api calls.
|
|
|
|
func NewVault(config VaultConfig) (KMS, error) {
|
|
|
|
if config.IsEmpty() {
|
|
|
|
return nil, errors.New("crypto: the hashicorp vault configuration must not be empty")
|
|
|
|
}
|
|
|
|
if err := config.Verify(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
vaultCfg := vault.Config{Address: config.Endpoint}
|
|
|
|
if err := vaultCfg.ConfigureTLS(&vault.TLSConfig{CAPath: config.CAPath}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
client, err := vault.NewClient(&vaultCfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if config.Namespace != "" {
|
|
|
|
client.SetNamespace(config.Namespace)
|
|
|
|
}
|
|
|
|
v := &vaultService{client: client, config: &config}
|
|
|
|
|
|
|
|
if err := v.authenticate(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// reauthenticate() tries to login in 1 minute
|
|
|
|
// intervals until successful.
|
|
|
|
func (v *vaultService) reauthenticate() {
|
|
|
|
retryDelay := 1 * time.Minute
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
if err := v.authenticate(); err != nil {
|
|
|
|
time.Sleep(retryDelay)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// renewer calls vault client's renewer that automatically
|
|
|
|
// renews secret periodically
|
|
|
|
func (v *vaultService) renewer(secret *vault.Secret) {
|
|
|
|
renewer, err := v.client.NewRenewer(&vault.RenewerInput{
|
|
|
|
Secret: secret,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
logger.FatalIf(err, "crypto: hashicorp vault token renewer could not be started")
|
|
|
|
}
|
|
|
|
v.tokenRenewer = renewer
|
|
|
|
go renewer.Renew()
|
|
|
|
defer renewer.Stop()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case err := <-renewer.DoneCh():
|
|
|
|
if err != nil {
|
|
|
|
v.reauthenticate()
|
|
|
|
renewer.Stop()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Renewal is now over
|
|
|
|
case renewal := <-renewer.RenewCh():
|
|
|
|
v.leaseDuration = time.Duration(renewal.Secret.Auth.LeaseDuration)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// authenticate logs the app to vault, and starts the auto renewer
|
|
|
|
// before secret expires
|
|
|
|
func (v *vaultService) authenticate() (err error) {
|
|
|
|
payload := map[string]interface{}{
|
|
|
|
"role_id": v.config.Auth.AppRole.ID,
|
|
|
|
"secret_id": v.config.Auth.AppRole.Secret,
|
|
|
|
}
|
|
|
|
var secret *vault.Secret
|
|
|
|
secret, err = v.client.Logical().Write("auth/approle/login", payload)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if secret.Auth == nil {
|
|
|
|
err = ErrKMSAuthLogin
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v.client.SetToken(secret.Auth.ClientToken)
|
|
|
|
v.leaseDuration = time.Duration(secret.Auth.LeaseDuration)
|
|
|
|
go v.renewer(secret)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateKey returns a new plaintext key, generated by the KMS,
|
|
|
|
// and a sealed version of this plaintext key encrypted using the
|
|
|
|
// named key referenced by keyID. It also binds the generated key
|
|
|
|
// cryptographically to the provided context.
|
|
|
|
func (v *vaultService) GenerateKey(keyID string, ctx Context) (key [32]byte, sealedKey []byte, err error) {
|
|
|
|
var contextStream bytes.Buffer
|
|
|
|
ctx.WriteTo(&contextStream)
|
|
|
|
|
|
|
|
payload := map[string]interface{}{
|
|
|
|
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
|
|
|
}
|
|
|
|
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/datakey/plaintext/%s", keyID), payload)
|
|
|
|
if err != nil {
|
|
|
|
return key, sealedKey, err
|
|
|
|
}
|
|
|
|
sealKey := s.Data["ciphertext"].(string)
|
|
|
|
plainKey, err := base64.StdEncoding.DecodeString(s.Data["plaintext"].(string))
|
|
|
|
if err != nil {
|
|
|
|
return key, sealedKey, err
|
|
|
|
}
|
|
|
|
copy(key[:], []byte(plainKey))
|
|
|
|
return key, []byte(sealKey), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnsealKey returns the decrypted sealedKey as plaintext key.
|
|
|
|
// Therefore it sends the sealedKey to the KMS which decrypts
|
|
|
|
// it using the named key referenced by keyID and responses with
|
|
|
|
// the plaintext key.
|
|
|
|
//
|
|
|
|
// The context must be same context as the one provided while
|
|
|
|
// generating the plaintext key / sealedKey.
|
|
|
|
func (v *vaultService) UnsealKey(keyID string, sealedKey []byte, ctx Context) (key [32]byte, err error) {
|
|
|
|
var contextStream bytes.Buffer
|
|
|
|
ctx.WriteTo(&contextStream)
|
|
|
|
|
|
|
|
payload := map[string]interface{}{
|
|
|
|
"ciphertext": string(sealedKey),
|
|
|
|
"context": base64.StdEncoding.EncodeToString(contextStream.Bytes()),
|
|
|
|
}
|
|
|
|
s, err := v.client.Logical().Write(fmt.Sprintf("/transit/decrypt/%s", keyID), payload)
|
|
|
|
if err != nil {
|
|
|
|
return key, err
|
|
|
|
}
|
|
|
|
base64Key := s.Data["plaintext"].(string)
|
|
|
|
plainKey, err := base64.StdEncoding.DecodeString(base64Key)
|
|
|
|
if err != nil {
|
|
|
|
return key, err
|
|
|
|
}
|
|
|
|
copy(key[:], []byte(plainKey))
|
|
|
|
return key, nil
|
|
|
|
}
|