client/clientutil.go (169 lines of code) (raw):
// Copyright 2021 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 client
import (
"bytes"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/pem"
"fmt"
"io"
"os"
"github.com/GoogleCloudPlatform/stet/client/shares"
configpb "github.com/GoogleCloudPlatform/stet/proto/config_go_proto"
"github.com/google/tink/go/streamingaead/subtle"
"google.golang.org/protobuf/proto"
)
const (
// DEKBytes is the size of the DEK in bytes.
// Parameters for streaming AEAD, required by Tink's subtle API.
aeadHKDFAlg = "SHA256"
aeadSegmentSize = 1048576
aeadFirstSegmentOffset = 0
aeadChunkSize = 128
)
/////////////////////////////////////////
// For AEAD encryption and decryption. //
/////////////////////////////////////////
// AeadEncrypt uses the provided key and AAD to encrypt the plaintext passed in
// via `input`, writing the output to `output`.
func AeadEncrypt(key shares.DEK, input io.Reader, output io.Writer, aad []byte) error {
cipher, err := subtle.NewAESGCMHKDF(key[:], aeadHKDFAlg, int(shares.DEKBytes), aeadSegmentSize, aeadFirstSegmentOffset)
if err != nil {
return fmt.Errorf("unable to create new cipher: %v", err)
}
writer, err := cipher.NewEncryptingWriter(output, aad)
if err != nil {
return fmt.Errorf("unable to create encrypt writer: %v", err)
}
if _, err := io.Copy(writer, input); err != nil {
return fmt.Errorf("failed to encrypt: %v", err)
}
if err := writer.Close(); err != nil {
return fmt.Errorf("error closing writer: %v", err)
}
return nil
}
// AeadDecrypt uses the provided key and AAD to decode the ciphertext passed
// in via `input`, writing the output to `output.
func AeadDecrypt(key shares.DEK, input io.Reader, output io.Writer, aad []byte) error {
cipher, err := subtle.NewAESGCMHKDF(key[:], aeadHKDFAlg, int(shares.DEKBytes), aeadSegmentSize, aeadFirstSegmentOffset)
if err != nil {
return fmt.Errorf("unable to create new cipher: %v", err)
}
reader, err := cipher.NewDecryptingReader(input, aad)
if err != nil {
return fmt.Errorf("unable to create decrypt reader: %v", err)
}
if _, err := io.Copy(output, reader); err != nil {
return fmt.Errorf("failed to decrypt: %w", err)
}
return nil
}
///////////////////////////////////////////////////
// For reading and writing STET-encrypted files. //
///////////////////////////////////////////////////
//
// The v1 file format of a STET-encrypted file is a concatenation of
// a 16 byte STET header, a serialized configpb.Metadata proto, and
// the raw ciphertext bytes, with no padding.
//
// STET Header (16 bytes):
// - "STETENCRYPTED" magic string (13 bytes)
// - file format version (1 byte)
// - serialized metadata length (2 bytes)
//
// Metadata:
// - serialized proto with the length specified in the header
//
// Ciphertext:
// - raw encrypted bytes, extending to the end of the file
// STETMagic is the magic string for a STET encrypted file header ("STETENCRYPTED").
var STETMagic = [13]byte{'S', 'T', 'E', 'T', 'E', 'N', 'C', 'R', 'Y', 'P', 'T', 'E', 'D'}
// STETHeader is the file header for the encrypted STET file format.
type STETHeader struct {
Magic [13]byte // len([]byte(STETMagic)) == 13
Version uint8 // 1 byte
MetadataLen uint16 // 2 bytes
}
// ReadSTETHeader reads a STET encrypted file header from `input`, returning a STETHeader.
func ReadSTETHeader(input io.Reader) (*STETHeader, error) {
var header STETHeader
if err := binary.Read(input, binary.LittleEndian, &header); err != nil {
return nil, fmt.Errorf("failed to read STET encrypted header: %v", err)
}
if !bytes.Equal(header.Magic[:], STETMagic[:]) {
return nil, fmt.Errorf("data is not a known STET encryption format")
}
return &header, nil
}
// WriteSTETHeader writes a STET encrypted file header with the given properties to `output`.
func WriteSTETHeader(output io.Writer, metadataLen int) error {
header := STETHeader{
Magic: STETMagic,
Version: 1,
MetadataLen: uint16(metadataLen),
}
return binary.Write(output, binary.LittleEndian, header)
}
/////////////////////////////////////////////////
// For dealing with RSA keys and fingerprints. //
/////////////////////////////////////////////////
// PublicKeyForRSAFingerprint Iterates through the public keys defined in `keys`, searching for one
// that matches `kek`. If one is found, returns it, otherwise returns nil.
func PublicKeyForRSAFingerprint(kek *configpb.KekInfo, keys *configpb.AsymmetricKeys) (*rsa.PublicKey, error) {
for _, path := range keys.GetPublicKeyFiles() {
keyBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open public key file: %w", err)
}
block, _ := pem.Decode(keyBytes)
if block == nil || block.Type != "PUBLIC KEY" {
return nil, fmt.Errorf("failed to decode PEM block containing public key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse public key from PEM: %v", err)
}
key, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("failed to parse RSA public key: %v", err)
}
// Compute SHA-256 digest of the DER-encoded public key.
sha := sha256.Sum256(block.Bytes)
fingerprint := base64.StdEncoding.EncodeToString(sha[:])
if fingerprint == kek.GetRsaFingerprint() {
return key, nil
}
}
return nil, fmt.Errorf("no RSA public key found for fingerprint: %s", kek.GetRsaFingerprint())
}
// PrivateKeyForRSAFingerprint iterates through the private keys defined in `keys`, searching for
// one that matches `kek`. If one is found, returns it, otherwise returns nil.
func PrivateKeyForRSAFingerprint(kek *configpb.KekInfo, keys *configpb.AsymmetricKeys) (*rsa.PrivateKey, error) {
for _, path := range keys.GetPrivateKeyFiles() {
keyBytes, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to open private key file: %w", err)
}
block, _ := pem.Decode(keyBytes)
if block == nil || block.Type != "RSA PRIVATE KEY" {
return nil, fmt.Errorf("failed to decode PEM block containing RSA private key")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse PKCS1 private key from PEM: %v", err)
}
// Compute SHA-256 digest of the DER-encoded public key.
der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal public key from private key: %w", err)
}
sha := sha256.Sum256(der)
fingerprint := base64.StdEncoding.EncodeToString(sha[:])
if fingerprint == kek.GetRsaFingerprint() {
return key, nil
}
}
return nil, fmt.Errorf("no RSA private key found for fingerprint: %s", kek.GetRsaFingerprint())
}
////////////////////////////////////////////
// For metadata serialization operations. //
////////////////////////////////////////////
// MetadataToAAD processes metadata to use as AAD for AEAD Encryption.
// The serialization scheme is as follows (given n := len(md.shares)):
//
// len(md.shares[0].wrappedShare) || md.shares[0].wrappedShare
// || len(md.shares[0].hash) || md.shares[0].hash
// ...
// || len(md.shares[n-1].wrappedShare) || md.shares[n-1].wrappedShare
// || len(md.shares[n-1].hash) || md.shares[n-1].hash
// || len(md.blobID) || md.blobID
//
// Note that KeyConfig is explicitly omitted from the serialization,
// as its presence is not important to the AAD.
func MetadataToAAD(md *configpb.Metadata) ([]byte, error) {
buf := new(bytes.Buffer)
for _, share := range md.GetShares() {
// Serialize share.wrappedShare
if err := binary.Write(buf, binary.LittleEndian, uint64(len(share.GetShare()))); err != nil {
return nil, fmt.Errorf("unable to serialize length of wrapped share: %v", err)
}
if _, err := buf.Write(share.GetShare()); err != nil {
return nil, fmt.Errorf("unable to serialize wrapped share: %v", err)
}
// Serialize share.hash
if err := binary.Write(buf, binary.LittleEndian, uint64(sha256.Size)); err != nil {
return nil, fmt.Errorf("unable to serialize length of hashed share: %v", err)
}
if _, err := buf.Write(share.GetHash()); err != nil {
return nil, fmt.Errorf("unable to serialize hashed share: %v", err)
}
}
// Serialize blobID.
if err := binary.Write(buf, binary.LittleEndian, uint64(len([]byte(md.GetBlobId())))); err != nil {
return nil, fmt.Errorf("unable to serialize length of blobID: %v", err)
}
if _, err := buf.WriteString(md.GetBlobId()); err != nil {
return nil, fmt.Errorf("unable to serialize blobID: %v", md.GetBlobId())
}
return buf.Bytes(), nil
}
// ReadMetadata parses and returns metadata from the input.
func ReadMetadata(input io.Reader) (*configpb.Metadata, error) {
// Read the STET header from the given `input`.
header, err := ReadSTETHeader(input)
if err != nil {
return nil, fmt.Errorf("failed to read STET encrypted file header: %v", err)
}
// Based on the metadata length in `header`, read metadata from `input`.
metadataBytes := make([]byte, header.MetadataLen)
if _, err := input.Read(metadataBytes); err != nil {
return nil, fmt.Errorf("failed to read encrypted file metadata: %v", err)
}
metadata := &configpb.Metadata{}
if err := proto.Unmarshal(metadataBytes, metadata); err != nil {
return nil, fmt.Errorf("failed to unmarshal metadata proto: %v", err)
}
return metadata, nil
}