cmd/hub/crypto/crypto.go (200 lines of code) (raw):
// Copyright (c) 2022 EPAM Systems, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha1"
"errors"
"fmt"
"golang.org/x/crypto/pbkdf2"
"github.com/epam/hubctl/cmd/hub/aws"
"github.com/epam/hubctl/cmd/hub/azure"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/gcp"
"github.com/epam/hubctl/cmd/hub/util"
)
const (
aes256KeySize = 32
// V1 is pbkdf2 password derived key
// V2 is AWS KMS
// V3 is Azure KeyVault
// V4 is GCP KMS
encryptionMarkerByte0 = '\x26'
encryptionV1MarkerByte1 = '\x01'
encryptionV2MarkerByte1 = '\x02'
encryptionV3MarkerByte1 = '\x03'
encryptionV4MarkerByte1 = '\x04'
encryptionV1SaltLen = 8
encryptionNonceLen = 12
encryptionV2EncryptedBlobLen = 184 // encrypted AES256 key and 152 bytes of fixed-size AWS KMS meta
encryptionV3EncryptedBlobLen = 256 // RSA-OAEP-256
encryptionV4EncryptedBlobLen = 113 // encrypted AES256 key and 81 bytes of fixed-size GCP KMS meta
encryptionMacLen = 16
EncryptionV1Overhead = 2 + encryptionV1SaltLen + encryptionNonceLen + encryptionMacLen
EncryptionV2Overhead = 2 + encryptionV2EncryptedBlobLen + encryptionNonceLen + encryptionMacLen
EncryptionV3Overhead = 2 + encryptionV3EncryptedBlobLen + encryptionNonceLen + encryptionMacLen
EncryptionV4Overhead = 2 + encryptionV4EncryptedBlobLen + encryptionNonceLen + encryptionMacLen
helpPassword = "HUB_CRYPTO_PASSWORD='random password'"
helpAwsKms = "HUB_CRYPTO_AWS_KMS_KEY_ARN='arn:aws:kms:...'"
helpAzukeKeyvault = "HUB_CRYPTO_AZURE_KEYVAULT_KEY_ID='https://*.vault.azure.net/keys/...'"
helpGcpKms = "HUB_CRYPTO_GCP_KMS_KEY_NAME='projects/*/locations/*/keyRings/my-key-ring/cryptoKeys/my-key'"
)
var (
encryptionVer byte
encryptionBlob []byte
encryptionKey []byte
)
func IsEncryptedData(data []byte) bool {
return (len(data) > EncryptionV1Overhead || len(data) > EncryptionV2Overhead || len(data) > EncryptionV3Overhead || len(data) > EncryptionV4Overhead) &&
data[0] == encryptionMarkerByte0 &&
(data[1] == encryptionV1MarkerByte1 || data[1] == encryptionV2MarkerByte1 || data[1] == encryptionV3MarkerByte1 || data[1] == encryptionV4MarkerByte1)
}
// for password based key the blob is salt
// for AWS KMS, Azure Key Vault, GCP KMS the blob is encrypted data key
// if no blob is supplied then a new key is requested
// if ver is supplied then it must match envionment setup
func encryptionKeyInit(ver byte, blob []byte) (byte, []byte, []byte, error) {
if ver == encryptionV1MarkerByte1 && config.CryptoPassword == "" {
return 0, nil, nil,
fmt.Errorf("Set %s", helpPassword)
}
if ver == encryptionV2MarkerByte1 && config.CryptoAwsKmsKeyArn == "" {
return 0, nil, nil,
fmt.Errorf("Set %s", helpAwsKms)
}
if ver == encryptionV3MarkerByte1 && config.CryptoAzureKeyVaultKeyId == "" {
return 0, nil, nil,
fmt.Errorf("Set %s", helpAzukeKeyvault)
}
if ver == encryptionV4MarkerByte1 && config.CryptoGcpKmsKeyName == "" {
return 0, nil, nil,
fmt.Errorf("Set %s", helpGcpKms)
}
if config.CryptoPassword != "" && (ver == 0 || ver == encryptionV1MarkerByte1) {
salt := blob
if len(salt) == 0 {
salt = make([]byte, encryptionV1SaltLen)
_, err := rand.Read(salt)
if err != nil {
return 0, nil, nil, err
}
}
key := pbkdf2.Key([]byte(config.CryptoPassword), salt, 4096, aes256KeySize, sha1.New)
return encryptionV1MarkerByte1, salt, key, nil
}
if config.CryptoAwsKmsKeyArn != "" && (ver == 0 || ver == encryptionV2MarkerByte1) {
clearKey, encryptedKey, err := aws.KmsKey(config.CryptoAwsKmsKeyArn, blob)
if err != nil {
return 0, nil, nil, err
}
return encryptionV2MarkerByte1, encryptedKey, clearKey, nil
}
if config.CryptoAzureKeyVaultKeyId != "" && (ver == 0 || ver == encryptionV3MarkerByte1) {
clearKey, encryptedKey, err := azure.KeyvaultKey(config.CryptoAzureKeyVaultKeyId, blob)
if err != nil {
return 0, nil, nil, err
}
return encryptionV3MarkerByte1, encryptedKey, clearKey, nil
}
if config.CryptoGcpKmsKeyName != "" && (ver == 0 || ver == encryptionV4MarkerByte1) {
clearKey, encryptedKey, err := gcp.KmsKey(config.CryptoGcpKmsKeyName, blob)
if err != nil {
return 0, nil, nil, err
}
return encryptionV4MarkerByte1, encryptedKey, clearKey, nil
}
return 0, nil, nil,
fmt.Errorf("Set %s or %s or %s", helpPassword, helpAwsKms, helpAzukeKeyvault)
}
func maybeEncryptionKeyInit() (byte, []byte, []byte, error) {
var err error
if len(encryptionKey) == 0 {
encryptionVer, encryptionBlob, encryptionKey, err = encryptionKeyInit(0, nil)
}
return encryptionVer, encryptionBlob, encryptionKey, err
}
func Encrypt(data []byte) ([]byte, error) {
if len(data) == 0 {
return data, nil
}
ver, blob, key, err := maybeEncryptionKeyInit()
if err != nil {
return nil, err
}
if ver == encryptionV2MarkerByte1 && len(blob) != encryptionV2EncryptedBlobLen {
util.WarnOnce("AWS KMS `CiphertextBlob` size %d doesn't match built-in size %d",
len(blob), encryptionV2EncryptedBlobLen)
}
if ver == encryptionV3MarkerByte1 && len(blob) != encryptionV3EncryptedBlobLen {
util.WarnOnce("Azure Key Vault encrypted key size %d doesn't match built-in size %d",
len(blob), encryptionV3EncryptedBlobLen)
}
if ver == encryptionV4MarkerByte1 && len(blob) != encryptionV4EncryptedBlobLen {
util.WarnOnce("GCP KMS encrypted key size %d doesn't match built-in size %d",
len(blob), encryptionV4EncryptedBlobLen)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if nonceSize != encryptionNonceLen {
util.WarnOnce("Cipher `nonce` size %d doesn't match built-in size %d", nonceSize, encryptionNonceLen)
}
nonce := make([]byte, nonceSize)
_, err = rand.Read(nonce)
if err != nil {
return nil, err
}
ciphertext := gcm.Seal(nil, nonce, data, blob)
buf := bytes.NewBuffer(make([]byte, 0, 2+len(blob)+len(nonce)+len(ciphertext)))
buf.WriteByte(encryptionMarkerByte0)
buf.WriteByte(ver)
buf.Write(blob)
buf.Write(nonce)
buf.Write(ciphertext)
return buf.Bytes(), nil
}
func Decrypt(encrypted []byte) ([]byte, error) {
if len(encrypted) == 0 {
return encrypted, nil
}
if !IsEncryptedData(encrypted) {
return nil, errors.New("Bad ciphertext marker")
}
overhead := EncryptionV1Overhead
blobLen := encryptionV1SaltLen
ver := encrypted[1]
if ver == encryptionV2MarkerByte1 {
overhead = EncryptionV2Overhead
blobLen = encryptionV2EncryptedBlobLen
} else if ver == encryptionV3MarkerByte1 {
overhead = EncryptionV3Overhead
blobLen = encryptionV3EncryptedBlobLen
} else if ver == encryptionV4MarkerByte1 {
overhead = EncryptionV4Overhead
blobLen = encryptionV4EncryptedBlobLen
}
if len(encrypted) < overhead+aes.BlockSize {
return nil, errors.New("Insufficient ciphertext length")
}
encrypted = encrypted[2:]
blob := encrypted[:blobLen]
rest := encrypted[blobLen:]
_, _, key, err := encryptionKeyInit(ver, blob)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if nonceSize != encryptionNonceLen {
util.WarnOnce("Cipher `nonce` size %d doesn't match built-in size %d", nonceSize, encryptionNonceLen)
}
nonce := rest[:nonceSize]
ciphertext := rest[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, blob)
}