v3/client/decrypt_middleware.go (136 lines of code) (raw):

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package client import ( "context" "fmt" "github.com/aws/amazon-s3-encryption-client-go/v3/internal" "github.com/aws/amazon-s3-encryption-client-go/v3/materials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/smithy-go" "github.com/aws/smithy-go/middleware" smithyhttp "github.com/aws/smithy-go/transport/http" "mime" "strings" "unicode/utf16" "unicode/utf8" ) func customS3Decoder(matDesc string) (decoded string, e error) { // Manually decode S3's non-standard "double encoding" // First, mime decode it: decoder := new(mime.WordDecoder) s, err := decoder.DecodeHeader(matDesc) if err != nil { return "", fmt.Errorf("error while decoding material description: %s\n from S3 object metadata: %w", matDesc, err) } var sb strings.Builder skipNext := false var utf8buffer []byte // Iterate over the bytes in the string for i, b := range []byte(s) { r := rune(b) // Check if the rune (code point) is non-US-ASCII if r > 127 && !skipNext { // Non-ASCII characters need special treatment // due to double-encoding. // We are dealing with UTF-16 encoded codepoints // of the original UTF-8 characters. // So, take two bytes at a time... buf := []byte{s[i], s[i+1]} // Get the rune (code point) wrongRune := string(buf) // UTF-16 encode it encd := utf16.Encode([]rune(wrongRune))[0] // Buffer the byte-level representation of the code point // So that it can be UTF-8 encoded later utf8buffer = append(utf8buffer, byte(encd)) skipNext = true } else if r > 127 && skipNext { // only skip once skipNext = false } else { // Decode the binary values as UTF-8 // This recovers the original UTF-8 for len(utf8buffer) > 0 { rb, size := utf8.DecodeRune(utf8buffer) sb.WriteRune(rb) utf8buffer = utf8buffer[size:] } sb.WriteByte(b) } // A more general solution would need to clear the utf8buffer here, // but specifically for material description, // we can assume that the string is JSON, // so the last character is '}' which is valid ASCII. } return sb.String(), nil } // GetObjectAPIClient is a client that implements the GetObject operation type GetObjectAPIClient interface { GetObject(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) (*s3.GetObjectOutput, error) } func (m *decryptMiddleware) addDecryptAPIOptions(options *s3.Options) { options.APIOptions = append(options.APIOptions, m.addDecryptMiddleware, ) } func (m *decryptMiddleware) addDecryptMiddleware(stack *middleware.Stack) error { return stack.Deserialize.Add(m, middleware.Before) } const decryptMiddlewareID = "S3Decrypt" type decryptMiddleware struct { client *S3EncryptionClientV3 input *s3.GetObjectInput } // ID returns the resolver identifier func (m *decryptMiddleware) ID() string { return decryptMiddlewareID } func (m *decryptMiddleware) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) ( out middleware.DeserializeOutput, metadata middleware.Metadata, err error, ) { // call down the stack and get the deserialized result (decrypt middleware runs after the operation deserializer) out, metadata, err = next.HandleDeserialize(ctx, in) if err != nil { return out, metadata, err } httpResp, ok := out.RawResponse.(*smithyhttp.Response) if !ok { return out, metadata, &smithy.DeserializationError{Err: fmt.Errorf("unknown transport type %T", out.RawResponse)} } result, ok := out.Result.(*s3.GetObjectOutput) if !ok { return out, metadata, fmt.Errorf("expected GetObjectOutput; got %v", out) } loadReq := &internal.LoadStrategyRequest{ HTTPResponse: httpResp.Response, Input: m.input, } // decode metadata loadStrat := internal.DefaultLoadStrategy{} objectMetadata, err := loadStrat.Load(ctx, loadReq) if err != nil { return out, metadata, fmt.Errorf("failed to load objectMetadata: bucket=%v; key=%v; err=%w", m.input.Bucket, m.input.Key, err) } // determine the content algorithm from metadata // this is purposefully done before attempting to // decrypt the materials var cekFunc internal.CEKEntry if objectMetadata.CEKAlg == internal.AESGCMNoPadding { cekFunc = internal.NewAESGCMContentCipher } else if strings.Contains(objectMetadata.CEKAlg, "AES/CBC") { if !m.client.Options.EnableLegacyUnauthenticatedModes { return out, metadata, fmt.Errorf("configure client with enable legacy unauthenticated modes set to true to decrypt with %s", objectMetadata.CEKAlg) } cekFunc = internal.NewAESCBCContentCipher } else { return out, metadata, fmt.Errorf("invalid content encryption algorithm found in metadata: %s", objectMetadata.CEKAlg) } cipherKey, err := objectMetadata.GetDecodedKey() if err != nil { return out, metadata, fmt.Errorf("unable to get decoded key for materials: %w", err) } iv, err := objectMetadata.GetDecodedIV() if err != nil { return out, metadata, fmt.Errorf("unable to get decoded IV for materials: %w", err) } matDesc, err := objectMetadata.GetMatDesc() if err != nil { return out, metadata, fmt.Errorf("unable to get Material Description for materials: %w", err) } // S3 server will encode metadata with non-US-ASCII characters // Decode it here to avoid parsing/decryption failure decodedMatDesc, err := customS3Decoder(matDesc) if err != nil { return out, metadata, fmt.Errorf("error while decoding Material Description: %w", err) } decryptMaterialsRequest := materials.DecryptMaterialsRequest{ cipherKey, iv, decodedMatDesc, objectMetadata.KeyringAlg, objectMetadata.CEKAlg, objectMetadata.TagLen, } decryptMaterials, err := m.client.Options.CryptographicMaterialsManager.DecryptMaterials(ctx, decryptMaterialsRequest) if err != nil { return out, metadata, fmt.Errorf("error while decrypting materials: %w", err) } cipher, err := cekFunc(*decryptMaterials) reader, err := cipher.DecryptContents(result.Body) if err != nil { return out, metadata, err } result.Body = reader out.Result = result return out, metadata, err }