Go客户端加密

OSS客户端加密是在数据上传至OSS之前,由用户在本地对数据进行加密处理,确保只有密钥持有者才能解密数据,增强数据在传输和存储过程中的安全性。

注意事项

  • 本文示例代码以华东1(杭州)的地域IDcn-hangzhou为例,默认使用外网Endpoint,如果您希望通过与OSS同地域的其他阿里云产品访问OSS,请使用内网Endpoint。关于OSS支持的RegionEndpoint的对应关系,请参见OSS地域和访问域名

  • 本文以从环境变量读取访问凭证为例。如何配置访问凭证,请参见配置访问凭证

  • 使用客户端加密功能时,您需要对主密钥的完整性和正确性负责。

  • 在对加密数据进行复制或者迁移时,您需要对加密元数据的完整性和正确性负责。

方法定义

对于主密钥的使用,Go SDK V2目前支持如下两种方式:

  • 使用用户自主管理的主密钥(RSA)

    SDK提供了RSA的默认实现,当主密钥信息由用户提供时,用户需要将主密钥的公钥、私钥信息作为参数传递给SDK。

  • 使用用户自定义的主密钥

    RSA主密钥方式无法满足需求时,用户可以自己实现主密钥的加解密行为,本文将以阿里云KMS 3.0为例,介绍如何自定义主密钥加解密。

使用以上两种加密方式能够有效地避免数据泄漏,保护客户端数据安全。即使数据泄漏,其他人也无法解密得到原始数据。

重要

如果您需要了解OSS客户端加密实现的原理,请参考OSS用户指南中的客户端加密

使用客户端加密,首先您需要实例化加密客户端,然后调用其提供的接口进行操作。您的对象将作为请求的一部分自动加密和解密。

type EncryptionClient struct {
  ...
}

func NewEncryptionClient(c *Client, masterCipher crypto.MasterCipher, optFns ...func(*EncryptionClientOptions)) (eclient *EncryptionClient, err error)

请求参数列表

参数名

类型

说明

c

*Client

非加密客户端实例

masterCipher

crypto.MasterCipher

主密钥实例,用于加密和解密数据密钥

optFns

...func(*EncryptionClientOptions)

(可选)加密客户端配置选项

其中,EncryptionClientOptions选项说明:

参数

类型

说明

MasterCiphers

[]crypto.MasterCipher

主密钥实例组, 用于解密数据密钥。

返回值列表

返回值名

类型

说明

eclient

*EncryptionClient

加密客户端实例, 当 err 为 nil 时有效

err

error

创建加密客户端的状态,当失败时,err 不为 nil

其中,EncryptionClient接口列举如下:

基础接口名

说明

GetObjectMeta

获取对象的部分元信息

HeadObject

获取对象的部元信息

GetObject

下载对象,并自动解密

PutObject

上传对象,并自动加密

InitiateMultipartUpload

初始化一个分片上传事件 和 分片加密上下文(EncryptionMultiPartContext)

UploadPart

初始化一个分片上传事件, 调用该接口上传分片数据,并自动加密。调用该接口时,需要设置 分片加密上下文

CompleteMultipartUpload

在将所有分片数据上传完成后,调用该接口合并成一个文件

AbortMultipartUpload

取消分片上传事件,并删除对应的分片数据

ListParts

列举指定上传事件所属的所有已经上传成功分片

高级接口名

说明

NewDownloader

创建下载管理器实例

NewUploader

创建上传管理器实例

OpenFile

创建ReadOnlyFile实例

辅助接口名

说明

Unwrap

获取非加密客户端实例,可以通过该实例访问其它基础接口

使用RSA主密钥

使用主密钥RSA简单上传和下载Object

使用主密钥RSA简单上传Object示例代码如下:

package main

import (
	"context"
	"flag"
	"log"
	"strings"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/crypto"
)

// 全局变量
var (
	region     string // 存储区域
	bucketName string // 存储空间名称
	objectName string // 对象名称
)

// init函数用于初始化命令行参数
func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	// 解析命令行参数
	flag.Parse()

	// 检查bucket名称是否为空
	if len(bucketName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name required")
	}

	// 检查region是否为空
	if len(region) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, region required")
	}

	// 检查对象名称是否为空
	if len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, object name required")
	}

	// 加载默认配置并设置凭证提供者和区域
	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	// 创建OSS客户端
	client := oss.NewClient(cfg)

	// 创建一个主密钥的描述信息,创建后不允许修改。主密钥描述信息和主密钥一一对应。
	// 如果所有的对象都使用相同的主密钥,主密钥描述信息可以为空,但后续不支持更换主密钥。
	// 如果主密钥描述信息为空,解密时无法判断使用的是哪个主密钥。
	// 强烈建议为每个主密钥都配置主密钥描述信息,由客户端保存主密钥和描述信息之间的对应关系。
	materialDesc := make(map[string]string)
	materialDesc["desc"] = "your master encrypt key material describe information"

	// 创建只包含 主密钥 的加密客户端
	// 如果没有下载操作时,可以不传私钥,即设置为 ""
	mc, err := crypto.CreateMasterRsa(materialDesc, "yourRsaPublicKey", "yourRsaPrivateKey")
	if err != nil {
		log.Fatalf("failed to create master rsa %v", err)

	}

	// 创建加密客户端
	eclient, err := oss.NewEncryptionClient(client, mc)
	if err != nil {
		log.Fatalf("failed to create encryption client %v", err)
	}

	// 创建简单上传的请求
	putObjRequest := &oss.PutObjectRequest{
		Bucket: oss.Ptr(bucketName),
		Key:    oss.Ptr(objectName),
		Body:   strings.NewReader("hi, simple put object"),
	}

	// 使用加密客户端上传对象
	putObjRequestResult, err := eclient.PutObject(context.TODO(), putObjRequest)
	if err != nil {
		log.Fatalf("failed to put object with encryption client %v", err)
	}
	log.Printf("put object with encryption client result:%#v\n", putObjRequestResult)

	// 创建简单下载的请求
	getObjRequest := &oss.GetObjectRequest{
		Bucket: oss.Ptr(bucketName),
		Key:    oss.Ptr(objectName),
	}

	// 使用加密客户端下载对象
	getObjRequestResult, err := eclient.GetObject(context.TODO(), getObjRequest)
	if err != nil {
		log.Fatalf("failed to put object with encryption client %v", err)
	}
	log.Printf("put object with encryption client result:%#v\n", getObjRequestResult)

}

使用主密钥RSA分片上传Object

使用主密钥RSA分片上传Object示例代码如下:

package main

import (
	"bufio"
	"context"
	"flag"
	"io"
	"log"
	"math/rand"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/crypto"
)

// 全局变量
var (
	region     string                                                                     // 存储区域
	bucketName string                                                                     // 存储空间名称
	objectName string                                                                     // 对象名称
	letters    = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // 用于生成随机字符串的字符集
)

// init函数用于初始化命令行参数
func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	// 解析命令行参数
	flag.Parse()

	// 检查bucket名称是否为空
	if len(bucketName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name required")
	}

	// 检查region是否为空
	if len(region) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, region required")
	}

	// 检查对象名称是否为空
	if len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, object name required")
	}

	// 加载默认配置并设置凭证提供者和区域
	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	// 创建OSS客户端
	client := oss.NewClient(cfg)

	// 创建一个主密钥的描述信息,创建后不允许修改。主密钥描述信息和主密钥一一对应。
	// 如果所有的对象都使用相同的主密钥,主密钥描述信息可以为空,但后续不支持更换主密钥。
	// 如果主密钥描述信息为空,解密时无法判断使用的是哪个主密钥。
	// 强烈建议为每个主密钥都配置主密钥描述信息,由客户端保存主密钥和描述信息之间的对应关系。
	materialDesc := make(map[string]string)
	materialDesc["desc"] = "your master encrypt key material describe information"

	// 创建只包含 主密钥 的加密客户端
	// 如果没有下载操作时,可以不传私钥,即设置为 ""
	mc, err := crypto.CreateMasterRsa(materialDesc, "yourRsaPublicKey", "yourRsaPrivateKey")
	if err != nil {
		log.Fatalf("failed to create master rsa %v", err)

	}

	// 创建加密客户端
	eclient, err := oss.NewEncryptionClient(client, mc)
	if err != nil {
		log.Fatalf("failed to create encryption client %v", err)
	}

	// 创建初始化分片上传的请求
	initRequest := &oss.InitiateMultipartUploadRequest{
		Bucket: oss.Ptr(bucketName),
		Key:    oss.Ptr(objectName),
	}
	initResult, err := eclient.InitiateMultipartUpload(context.TODO(), initRequest)
	if err != nil {
		log.Fatalf("failed to initiate multi part upload %v", err)
	}

	var wg sync.WaitGroup
	var parts oss.UploadParts
	count := 3
	body := randStr(400000)
	reader := strings.NewReader(body)
	bufReader := bufio.NewReader(reader)
	content, _ := io.ReadAll(bufReader)
	partSize := len(body) / count
	var mu sync.Mutex

	for i := 0; i < count; i++ {
		wg.Add(1)
		go func(partNumber int, partSize int, i int) {
			defer wg.Done()
			partRequest := &oss.UploadPartRequest{
				Bucket:              oss.Ptr(bucketName),                                             // 存储空间名称
				Key:                 oss.Ptr(objectName),                                             // 对象名称
				PartNumber:          int32(partNumber),                                               // 分片编号
				UploadId:            oss.Ptr(*initResult.UploadId),                                   // 上传ID
				Body:                strings.NewReader(string(content[i*partSize : (i+1)*partSize])), // 分片内容
				CSEMultiPartContext: initResult.CSEMultiPartContext,                                  // 多部分上下文
			}
			partResult, err := eclient.UploadPart(context.TODO(), partRequest)
			if err != nil {
				log.Fatalf("failed to upload part %d: %v", partNumber, err)
			}
			part := oss.UploadPart{
				PartNumber: partRequest.PartNumber, // 分片编号
				ETag:       partResult.ETag,        // ETag
			}
			mu.Lock()
			parts = append(parts, part)
			mu.Unlock()
		}(i+1, partSize, i)
	}
	wg.Wait()
	sort.Sort(parts)

	request := &oss.CompleteMultipartUploadRequest{
		Bucket:   oss.Ptr(bucketName),           // 存储空间名称
		Key:      oss.Ptr(objectName),           // 对象名称
		UploadId: oss.Ptr(*initResult.UploadId), // 上传ID
		CompleteMultipartUpload: &oss.CompleteMultipartUpload{
			Parts: parts, // 已上传的分片列表
		},
	}
	result, err := eclient.CompleteMultipartUpload(context.TODO(), request)
	if err != nil {
		log.Fatalf("failed to complete multipart upload %v", err)
	}
	log.Printf("complete multipart upload result:%#v\n", result)
}

// 生成随机字符串
func randStr(n int) string {
	b := make([]rune, n)
	randMarker := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := range b {
		b[i] = letters[randMarker.Intn(len(letters))]
	}
	return string(b)
}

使用自定义主密钥

使用自定义主密钥简单上传和下载Object

SDK提供了RSA默认实现, 当这个方式不满足用户的需求时,用户可以自己实现主密钥的加解密行为。以下示例代码以阿里云KMS 3.0为例,演示如何自定义主密钥加解密进行简单上传和下载Object。

package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"strings"

	kms "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
	kmssdk "github.com/aliyun/alibabacloud-dkms-transfer-go-sdk/sdk"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
	osscrypto "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/crypto"
)

// CreateMasterAliKms3 创建由阿里云 KMS 3.0 实现的主密钥接口
// matDesc 将被转换为 JSON 字符串
func CreateMasterAliKms3(matDesc map[string]string, kmsID string, kmsClient *kmssdk.KmsTransferClient) (osscrypto.MasterCipher, error) {
	var masterCipher MasterAliKms3Cipher
	if kmsID == "" || kmsClient == nil {
		return masterCipher, fmt.Errorf("kmsID is empty or kmsClient is nil")
	}

	var jsonDesc string
	if len(matDesc) > 0 {
		b, err := json.Marshal(matDesc)
		if err != nil {
			return masterCipher, err
		}
		jsonDesc = string(b)
	}

	masterCipher.MatDesc = jsonDesc
	masterCipher.KmsID = kmsID
	masterCipher.KmsClient = kmsClient
	return masterCipher, nil
}

// 阿里云 KMS 主密钥接口
type MasterAliKms3Cipher struct {
	MatDesc   string                    // 密钥描述信息
	KmsID     string                    // KMS 密钥 ID
	KmsClient *kmssdk.KmsTransferClient // KMS 客户端
}

// 获取主密钥的包装算法
func (mrc MasterAliKms3Cipher) GetWrapAlgorithm() string {
	return "KMS/ALICLOUD"
}

// 获取主密钥的描述信息
func (mkms MasterAliKms3Cipher) GetMatDesc() string {
	return mkms.MatDesc
}

// 使用阿里云 KMS 加密数据,主要用于加密对象的对称密钥和 IV
func (mkms MasterAliKms3Cipher) Encrypt(plainData []byte) ([]byte, error) {
	base64Plain := base64.StdEncoding.EncodeToString(plainData)
	request := kms.CreateEncryptRequest()
	request.RpcRequest.Scheme = "https"
	request.RpcRequest.Method = "POST"
	request.RpcRequest.AcceptFormat = "json"

	request.KeyId = mkms.KmsID
	request.Plaintext = base64Plain

	response, err := mkms.KmsClient.Encrypt(request)
	if err != nil {
		return nil, err
	}
	return base64.StdEncoding.DecodeString(response.CiphertextBlob)
}

// 使用阿里云 KMS 解密数据,主要用于解密对象的对称密钥和 IV
func (mkms MasterAliKms3Cipher) Decrypt(cryptoData []byte) ([]byte, error) {
	base64Crypto := base64.StdEncoding.EncodeToString(cryptoData)
	request := kms.CreateDecryptRequest()
	request.RpcRequest.Scheme = "https"
	request.RpcRequest.Method = "POST"
	request.RpcRequest.AcceptFormat = "json"
	request.CiphertextBlob = string(base64Crypto)
	response, err := mkms.KmsClient.Decrypt(request)
	if err != nil {
		return nil, err
	}
	return base64.StdEncoding.DecodeString(response.Plaintext)
}

var (
	region     string // 存储区域
	bucketName string // 存储空间名称
	objectName string // 对象名称
)

// init函数用于初始化命令行参数
func init() {
	flag.StringVar(&region, "region", "", "The region in which the bucket is located.")
	flag.StringVar(&bucketName, "bucket", "", "The name of the bucket.")
	flag.StringVar(&objectName, "object", "", "The name of the object.")
}

func main() {
	// 解析命令行参数
	flag.Parse()

	// 检查存储区域是否为空
	if len(region) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, region required")
	}

	// 检查存储空间名称是否为空
	if len(bucketName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, bucket name required")
	}

	// 检查对象名称是否为空
	if len(objectName) == 0 {
		flag.PrintDefaults()
		log.Fatalf("invalid parameters, object name required")
	}

	// 加载默认配置并设置凭证提供者和区域
	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(credentials.NewEnvironmentVariableCredentialsProvider()).
		WithRegion(region)

	// 创建 OSS 客户端
	client := oss.NewClient(cfg)

	// 创建 KMS 客户端
	kmsRegion := "cn-hangzhou"                // KMS 区域
	kmsAccessKeyId := "access key id"         // KMS 访问密钥 ID
	kmsAccessKeySecret := "access key secret" // KMS 访问密钥秘密
	kmsKeyId := "kms id"                      // KMS 密钥 ID

	kmsClient, err := kmssdk.NewClientWithAccessKey(kmsRegion, kmsAccessKeyId, kmsAccessKeySecret, nil)
	if err != nil {
		log.Fatalf("failed to create kms sdk client %v", err)
	}

	// 创建密钥描述信息
	materialDesc := make(map[string]string)
	materialDesc["desc"] = "your kms encrypt key material describe information"

	// 创建主密钥实例
	masterKmsCipher, err := CreateMasterAliKms3(materialDesc, kmsKeyId, kmsClient)
	if err != nil {
		log.Fatalf("failed to create master AliKms3 %v", err)
	}

	// 创建加密客户端
	eclient, err := oss.NewEncryptionClient(client, masterKmsCipher)
	if err != nil {
		log.Fatalf("failed to create encryption client %v", err)
	}

	// 创建上传对象的请求
	request := &oss.PutObjectRequest{
		Bucket: oss.Ptr(bucketName),         // 存储空间名称
		Key:    oss.Ptr(objectName),         // 对象名称
		Body:   strings.NewReader("hi kms"), // 要上传的数据
	}

	// 执行上传对象的操作
	result, err := eclient.PutObject(context.TODO(), request)
	if err != nil {
		log.Fatalf("failed to put object with encryption client %v", err)
	}
	log.Printf("put object with encryption client result:%#v\n", result)

	// 创建下载对象的请求
	getRequest := &oss.GetObjectRequest{
		Bucket: oss.Ptr(bucketName), // 存储空间名称
		Key:    oss.Ptr(objectName), // 对象名称
	}

	// 执行下载对象的操作
	getResult, err := eclient.GetObject(context.TODO(), getRequest)
	if err != nil {
		log.Fatalf("failed to get object with encryption client %v", err)
	}
	defer getResult.Body.Close()

	// 读取下载的数据
	data, err := io.ReadAll(getResult.Body)
	if err != nil {
		log.Fatalf("failed to read all %v", err)
	}
	log.Printf("get object data:%s\n", data)
}

相关文档

  • 关于OSS客户端加密实现的原理,请参见客户端加密

  • 关于客户端加密的Go SDK操作指南,请参见操作指南

  • 关于使用主密钥RSA简单上传和下载Object的完整代码示例,请参见Github示例

  • 关于使用主密钥KMS普通上传和下载Object的完整代码示例,请参见Github示例