fakekms/symmetric_rpcs.go (337 lines of code) (raw):
// Copyright 2022 Google LLC
//
// 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 fakekms
import (
"context"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"io"
"cloud.google.com/go/kms/apiv1/kmspb"
)
// maxPlaintextSize is the maximum plaintext size accepted by Cloud KMS.
const maxPlaintextSize = 64 * 1024
// maxCiphertextSize is the maximum ciphertext size accepted by Cloud KMS.
const maxCiphertextSize = 10 * maxPlaintextSize
// maxIvSize is the maximum initialization vector size accepted by Cloud KMS.
const maxIvSize = 16
// defaultTagLength is the authentication tag length used by Cloud KMS.
const defaultTagLength = 16
// RawEncrypt fakes a Cloud KMS API function.
func (f *fakeKMS) RawEncrypt(ctx context.Context, req *kmspb.RawEncryptRequest) (*kmspb.RawEncryptResponse, error) {
if err := allowlist("name", "plaintext", "plaintext_crc32c", "additional_authenticated_data", "additional_authenticated_data_crc32c", "initialization_vector", "initialization_vector_crc32c").check(req); err != nil {
return nil, err
}
name, err := parseCryptoKeyVersionName(req.Name)
if err != nil {
return nil, err
}
ckv, err := f.cryptoKeyVersion(name)
if err != nil {
return nil, err
}
if ckv.pb.State != kmspb.CryptoKeyVersion_ENABLED {
return nil, errFailedPrecondition("key version %s is not enabled", name)
}
def, _ := algorithmDef(ckv.pb.Algorithm)
if def.Purpose != kmspb.CryptoKey_RAW_ENCRYPT_DECRYPT {
return nil, errFailedPrecondition("keys with algorithm %s may not be used for raw encryption",
nameForValue(kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm_name, int32(ckv.pb.Algorithm)))
}
if req.Plaintext == nil {
return nil, errInvalidArgument("plaintext is empty")
}
var plaintext []byte = req.Plaintext
if len(plaintext) > maxPlaintextSize {
return nil, errInvalidArgument("len(plaintext)=%d, want len(plaintext)<=%d", len(plaintext), maxPlaintextSize)
}
plaintextChecksum := crc32c(plaintext)
if req.PlaintextCrc32C != nil && plaintextChecksum.Value != req.PlaintextCrc32C.Value {
return nil, errInvalidArgument("invalid plaintext checksum")
}
var nonce []byte
// Validate request fields.
switch ckv.pb.Algorithm {
case kmspb.CryptoKeyVersion_AES_128_GCM, kmspb.CryptoKeyVersion_AES_256_GCM:
if req.InitializationVector != nil {
return nil, errInvalidArgument("cannot specify iv when using AES-GCM")
}
nonce = make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
aadChecksum := crc32c(req.AdditionalAuthenticatedData)
if req.AdditionalAuthenticatedData != nil &&
req.AdditionalAuthenticatedDataCrc32C != nil &&
aadChecksum.Value != req.AdditionalAuthenticatedDataCrc32C.Value {
return nil, errInvalidArgument("invalid aad checksum")
}
case kmspb.CryptoKeyVersion_AES_128_CTR,
kmspb.CryptoKeyVersion_AES_256_CTR,
kmspb.CryptoKeyVersion_AES_128_CBC,
kmspb.CryptoKeyVersion_AES_256_CBC:
if req.AdditionalAuthenticatedData != nil {
return nil, errInvalidArgument("cannot specify aad when using %s",
nameForValue(kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm_name, int32(ckv.pb.Algorithm)))
}
if req.InitializationVector != nil {
if len(req.InitializationVector) > maxIvSize {
return nil, errInvalidArgument("len(iv)=%d, want len(iv)<=%d", len(req.InitializationVector), maxIvSize)
}
nonce = req.InitializationVector
} else {
nonce = make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
}
default:
return nil, errInternal("unhandled algorithm")
}
var key []byte
var ok bool
if key, ok = ckv.keyMaterial.([]byte); !ok {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, errInternal("AES cipher creation failed: %v", err)
}
var ciphertext []byte
var tagLen int32
switch ckv.pb.Algorithm {
case kmspb.CryptoKeyVersion_AES_128_GCM, kmspb.CryptoKeyVersion_AES_256_GCM:
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errInternal("GCM cipher creation failed: %v", err)
}
ciphertext = aesgcm.Seal(nil, nonce, plaintext, req.AdditionalAuthenticatedData)
tagLen = defaultTagLength
case kmspb.CryptoKeyVersion_AES_128_CTR, kmspb.CryptoKeyVersion_AES_256_CTR:
ciphertext = make([]byte, len(plaintext))
aesctr := cipher.NewCTR(block, nonce)
aesctr.XORKeyStream(ciphertext, plaintext)
case kmspb.CryptoKeyVersion_AES_128_CBC, kmspb.CryptoKeyVersion_AES_256_CBC:
if len(plaintext)%block.BlockSize() != 0 {
return nil, errInvalidArgument("len(plaintext)=%d, want len(plaintext) mod %d == 0", len(plaintext), block.BlockSize())
}
ciphertext = make([]byte, len(plaintext))
aescbc := cipher.NewCBCEncrypter(block, nonce)
aescbc.CryptBlocks(ciphertext, plaintext)
default:
return nil, errInternal("unhandled algorithm")
}
return &kmspb.RawEncryptResponse{
Name: req.Name,
Ciphertext: ciphertext,
CiphertextCrc32C: crc32c(ciphertext),
InitializationVector: nonce,
InitializationVectorCrc32C: crc32c(nonce),
TagLength: tagLen,
VerifiedPlaintextCrc32C: req.PlaintextCrc32C != nil,
VerifiedAdditionalAuthenticatedDataCrc32C: req.AdditionalAuthenticatedDataCrc32C != nil,
VerifiedInitializationVectorCrc32C: req.InitializationVectorCrc32C != nil,
ProtectionLevel: ckv.pb.ProtectionLevel,
}, nil
}
// RawDecrypt fakes a Cloud KMS API function.
func (f *fakeKMS) RawDecrypt(ctx context.Context, req *kmspb.RawDecryptRequest) (*kmspb.RawDecryptResponse, error) {
if err := allowlist("name", "ciphertext", "ciphertext_crc32c", "additional_authenticated_data", "additional_authenticated_data_crc32c", "initialization_vector", "initialization_vector_crc32c").check(req); err != nil {
return nil, err
}
name, err := parseCryptoKeyVersionName(req.Name)
if err != nil {
return nil, err
}
ckv, err := f.cryptoKeyVersion(name)
if err != nil {
return nil, err
}
if ckv.pb.State != kmspb.CryptoKeyVersion_ENABLED {
return nil, errFailedPrecondition("key version %s is not enabled", name)
}
def, _ := algorithmDef(ckv.pb.Algorithm)
if def.Purpose != kmspb.CryptoKey_RAW_ENCRYPT_DECRYPT {
return nil, errFailedPrecondition("keys with algorithm %s may not be used for raw decryption",
nameForValue(kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm_name, int32(ckv.pb.Algorithm)))
}
if req.Ciphertext == nil {
return nil, errInvalidArgument("ciphertext is empty")
}
var ciphertext []byte = req.Ciphertext
if len(ciphertext) > maxCiphertextSize {
return nil, errInvalidArgument("len(ciphertext)=%d, want len(ciphertext)<=%d", len(ciphertext), maxCiphertextSize)
}
ciphertextChecksum := crc32c(ciphertext)
if req.CiphertextCrc32C != nil && ciphertextChecksum.Value != req.CiphertextCrc32C.Value {
return nil, errInvalidArgument("invalid ciphertext checksum")
}
if len(req.InitializationVector) > 16 {
return nil, errInvalidArgument("len(initialization_vector)=%d, want len(initialization_vector)<=%d", len(req.InitializationVector), maxIvSize)
}
ivChecksum := crc32c(req.InitializationVector)
if req.InitializationVector != nil && req.InitializationVectorCrc32C != nil && ivChecksum.Value != req.InitializationVectorCrc32C.Value {
return nil, errInvalidArgument("invalid iv checksum")
}
if req.AdditionalAuthenticatedData != nil {
switch ckv.pb.Algorithm {
case kmspb.CryptoKeyVersion_AES_128_GCM, kmspb.CryptoKeyVersion_AES_256_GCM:
break
default:
return nil, errInvalidArgument("cannot specify aad when using %s",
nameForValue(kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm_name, int32(ckv.pb.Algorithm)))
}
}
aadChecksum := crc32c(req.AdditionalAuthenticatedData)
if req.AdditionalAuthenticatedData != nil && req.AdditionalAuthenticatedDataCrc32C != nil && aadChecksum.Value != req.AdditionalAuthenticatedDataCrc32C.Value {
return nil, errInvalidArgument("invalid aad checksum")
}
var key []byte
var ok bool
if key, ok = ckv.keyMaterial.([]byte); !ok {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, errInternal("AES cipher creation failed: %v", err)
}
var plaintext []byte
switch ckv.pb.Algorithm {
case kmspb.CryptoKeyVersion_AES_128_GCM, kmspb.CryptoKeyVersion_AES_256_GCM:
if len(ciphertext) < defaultTagLength {
return nil, errInvalidArgument("len(ciphertext)=%d, want len(ciphertext) > %d", len(ciphertext), defaultTagLength)
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errInternal("GCM cipher creation failed: %v", err)
}
plaintext, err = aesgcm.Open(nil, req.InitializationVector, ciphertext, req.AdditionalAuthenticatedData)
if err != nil {
return nil, errInternal("decryption failed: %v", err)
}
case kmspb.CryptoKeyVersion_AES_128_CTR, kmspb.CryptoKeyVersion_AES_256_CTR:
plaintext = make([]byte, len(ciphertext))
aesctr := cipher.NewCTR(block, req.InitializationVector)
aesctr.XORKeyStream(plaintext, ciphertext)
case kmspb.CryptoKeyVersion_AES_128_CBC, kmspb.CryptoKeyVersion_AES_256_CBC:
plaintext = make([]byte, len(ciphertext))
aescbc := cipher.NewCBCDecrypter(block, req.InitializationVector)
aescbc.CryptBlocks(plaintext, ciphertext)
default:
return nil, errInternal("unhandled algorithm")
}
return &kmspb.RawDecryptResponse{
Plaintext: plaintext,
PlaintextCrc32C: crc32c(plaintext),
VerifiedCiphertextCrc32C: req.CiphertextCrc32C != nil,
VerifiedAdditionalAuthenticatedDataCrc32C: req.AdditionalAuthenticatedDataCrc32C != nil,
VerifiedInitializationVectorCrc32C: req.InitializationVectorCrc32C != nil,
ProtectionLevel: ckv.pb.ProtectionLevel,
}, nil
}
// MacSign fakes a Cloud KMS API function.
func (f *fakeKMS) MacSign(ctx context.Context, req *kmspb.MacSignRequest) (*kmspb.MacSignResponse, error) {
if err := allowlist("name", "data", "data_crc32c").check(req); err != nil {
return nil, err
}
name, err := parseCryptoKeyVersionName(req.Name)
if err != nil {
return nil, err
}
ckv, err := f.cryptoKeyVersion(name)
if err != nil {
return nil, err
}
if ckv.pb.State != kmspb.CryptoKeyVersion_ENABLED {
return nil, errFailedPrecondition("key version %s is not enabled", name)
}
def, _ := algorithmDef(ckv.pb.Algorithm)
if def.Purpose != kmspb.CryptoKey_MAC {
return nil, errFailedPrecondition("keys with algorithm %s may not be used for MAC signing",
nameForValue(kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm_name, int32(ckv.pb.Algorithm)))
}
hash := def.Opts.(crypto.Hash)
if req.Data == nil {
return nil, errInvalidArgument("data is empty")
}
var data []byte = req.Data
if len(data) > 64*1024 {
return nil, errInvalidArgument("len(data)=%d, want len(data)<=%d", len(data), 64*1024)
}
dataChecksum := crc32c(data)
if req.DataCrc32C != nil && dataChecksum.Value != req.DataCrc32C.Value {
return nil, errInvalidArgument("invalid data checksum")
}
var key []byte
var ok bool
if key, ok = ckv.keyMaterial.([]byte); !ok {
return nil, err
}
mac := hmac.New(hash.New, key)
_, err = mac.Write(data)
if err != nil {
return nil, errInternal("MAC signing failed: %v", err)
}
macTag := mac.Sum(nil)
return &kmspb.MacSignResponse{
Name: req.Name,
Mac: macTag,
MacCrc32C: crc32c(macTag),
VerifiedDataCrc32C: req.DataCrc32C != nil,
ProtectionLevel: ckv.pb.ProtectionLevel,
}, nil
}
// MacVerify fakes a Cloud KMS API function.
func (f *fakeKMS) MacVerify(ctx context.Context, req *kmspb.MacVerifyRequest) (*kmspb.MacVerifyResponse, error) {
if err := allowlist("name", "data", "data_crc32c", "mac", "mac_crc32c").check(req); err != nil {
return nil, err
}
name, err := parseCryptoKeyVersionName(req.Name)
if err != nil {
return nil, err
}
ckv, err := f.cryptoKeyVersion(name)
if err != nil {
return nil, err
}
if ckv.pb.State != kmspb.CryptoKeyVersion_ENABLED {
return nil, errFailedPrecondition("key version %s is not enabled", name)
}
def, _ := algorithmDef(ckv.pb.Algorithm)
if def.Purpose != kmspb.CryptoKey_MAC {
return nil, errFailedPrecondition("keys with algorithm %s may not be used for MAC verification",
nameForValue(kmspb.CryptoKeyVersion_CryptoKeyVersionAlgorithm_name, int32(ckv.pb.Algorithm)))
}
if req.Data == nil {
return nil, errInvalidArgument("data is empty")
}
var data []byte = req.Data
if len(data) > 64*1024 {
return nil, errInvalidArgument("len(data)=%d, want len(data)<=%d", len(data), 64*1024)
}
dataChecksum := crc32c(data)
if req.DataCrc32C != nil && dataChecksum.Value != req.DataCrc32C.Value {
return nil, errInvalidArgument("invalid data checksum")
}
hash := def.Opts.(crypto.Hash)
if req.Mac == nil {
return nil, errInvalidArgument("mac is empty")
}
if len(req.Mac) != hash.Size() {
return nil, errInvalidArgument("len(mac)=%d, want %d", len(req.Mac), hash.Size())
}
macChecksum := crc32c(req.Mac)
if req.MacCrc32C != nil && macChecksum.Value != req.MacCrc32C.Value {
return nil, errInvalidArgument("invalid mac checksum")
}
var key []byte
var ok bool
if key, ok = ckv.keyMaterial.([]byte); !ok {
return nil, err
}
mac := hmac.New(hash.New, key)
_, err = mac.Write(data)
if err != nil {
return nil, errInternal("MAC signing failed: %v", err)
}
macTag := mac.Sum(nil)
return &kmspb.MacVerifyResponse{
Name: req.Name,
Success: hmac.Equal(req.Mac, macTag),
VerifiedSuccessIntegrity: hmac.Equal(req.Mac, macTag),
VerifiedDataCrc32C: req.DataCrc32C != nil,
VerifiedMacCrc32C: req.MacCrc32C != nil,
ProtectionLevel: ckv.pb.ProtectionLevel,
}, nil
}