客户端加密是指将数据发送到OSS之前在用户本地进行加密。

免责声明

  • 使用客户端加密功能时,您需要对主密钥的完整性和正确性负责。因您维护不当导致主密钥用错或丢失,从而导致加密数据无法解密所引起的一切损失和后果均由您自行承担。
  • 在对加密数据进行复制或者迁移时,您需要对加密元信息的完整性和正确性负责。因您维护不当导致加密元信息出错或丢失,从而导致加密数据无法解密所引起的一切损失和后果均由您自行承担。

背景信息

使用客户端加密时,会为每个Object生成一个随机数据加密密钥,用该随机数据加密密钥明文对Object的数据进行对称加密。主密钥用于生成随机的数据加密密钥,加密后的内容会当作Object的meta信息保存在服务端。解密时先用主密钥将加密后的随机密钥解密出来,再用解密出来的随机数据加密密钥明文解密Object的数据。主密钥只参与客户端本地计算,不会在网络上进行传输或保存在服务端,以保证主密钥的数据安全。

注意
  • 客户端加密支持分片上传超过5 GB的文件。在使用分片方式上传文件时,需要指定上传文件的总大小和分片大小, 除了最后一个分片外,每个分片的大小要一致,且分片大小目前必须是16的整数倍。
  • 调用客户端加密上传文件后,加密元数据会被保护,无法通过CopyObject修改Object meta信息。

加密方式

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

  • 使用KMS托管用户主密钥

    当使用KMS托管用户主密钥用于客户端数据加密时,需要将KMS用户主密钥ID(即CMK ID)传递给SDK。

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

    主密钥信息由用户提供,需要用户将主密钥的公钥、私钥信息当做参数传递给SDK。

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

加密元信息

参数 描述 是否必需
x-oss-meta-client-side-encryption-key 加密后的密钥。 经过主秘钥加密后再经过base64编码的字符串。
x-oss-meta-client-side-encryption-start 随机产生的用于加密数据的初始值 。经过主秘钥加密后再经过base64编码的字符串。
x-oss-meta-client-side-encryption-cek-alg 数据的加密算法。
x-oss-meta-client-side-encryption-wrap-alg 数据密钥的加密算法。
x-oss-meta-client-side-encryption-matdesc 主秘钥的描述信息。JSON格式。
警告 强烈建议为每个主秘钥都配置描述信息,并保存好主秘钥和描述信息之间的对应关系。否则不支持更换主秘钥进行加密。
x-oss-meta-client-side-encryption-unencrypted-content-length 加密前的数据长度。如未指定content-length则不生成该参数。
x-oss-meta-client-side-encryption-unencrypted-content-md5 加密前数据的MD5。如未指定MD5,则不生成该参数。
x-oss-meta-client-side-encryption-data-size 若加密Multipart文件需要在init_multipart时传入整个大文件的总大小。 是(分片上传)
x-oss-meta-client-side-encryption-part-size 若加密Multipart文件需要在init_multipart时传入分片大小。
说明 目前分片大小必须是16的整数倍。
是(分片上传)

使用主密钥RSA普通上传和下载Object

使用主密钥RSA普通上传和下载Object示例代码如下:

package main

import (
  "bytes"
  "fmt"
  "io/ioutil"
  "os"

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

func main() {
  // 创建OSSClient实例。
  client, err := oss.New("<yourEndpoint>", "<yourAccessKeyId>", "<yourAccessKeySecret>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 创建一个主秘钥的描述信息,创建后不允许修改。主秘钥描述信息和主秘钥一一对应。
  // 如果所有的Object都使用相同的主秘钥,主秘钥描述信息可以为空,但后续不支持更换主秘钥。
  // 如果主秘钥描述信息为空,解密时无法判断使用的是哪个主秘钥。
  // 强烈建议为每个主秘钥都配置主秘钥描述信息(json字符串), 由客户端保存主秘钥和描述信息之间的对应关系(服务端不保存两者之间的对应关系)。

    // 由主秘钥描述信息(json字符串)转换的map。
    materialDesc := make(map[string]string)
  materialDesc["desc"] = "<your master encrypt key material describe information>"

  // 根据主秘钥描述信息创建一个主秘钥对象。
  masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "<your rsa public key>", "<your rsa private key>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 根据主秘钥对象创建一个用于加密的接口, 使用aes ctr模式加密。
  contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher)

  // 获取一个用于客户端加密的已创建bucket。
  // 客户端加密bucket和普通bucket具有相似的用法。
  cryptoBucket, err := osscrypto.GetCryptoBucket(client, "<yourBucketName>", contentProvider)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // put object时自动加密。
  err = cryptoBucket.PutObject("<yourObjectName>", bytes.NewReader([]byte("yourObjectValueByteArrary")))
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // get object时自动解密。
  body, err := cryptoBucket.GetObject("<yourObjectName>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  defer body.Close()

  data, err := ioutil.ReadAll(body)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  fmt.Println("data:", string(data))
}

使用主密钥RSA分片上传Object

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

package main

import (
  "fmt"
  "os"

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

func main() {
  // 创建OSSClient实例
  client, err := oss.New("<yourEndpoint>", "<yourAccessKeyId>", "<yourAccessKeySecret>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 创建一个主秘钥的描述信息,创建后不允许修改。主秘钥描述信息和主秘钥一一对应。
  // 如果所有的Object都使用相同的主秘钥,主秘钥描述信息可以为空,但后续不支持更换主秘钥。
  // 如果主秘钥描述信息为空,解密时无法判断使用的是哪个主秘钥。
  // 强烈建议为每个主秘钥都配置主秘钥描述信息(json字符串),由客户端保存主秘钥和描述信息之间的对应关系(服务端不保存两者之间的对应关系)。

    // 由主秘钥描述信息(json字符串)转换的map。
  materialDesc := make(map[string]string)
  materialDesc["desc"] = "<your master encrypt key material describe information>"

  // 根据主秘钥描述信息创建一个主秘钥对象。
  masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "<your rsa public key>", "<your rsa private key>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 根据主秘钥对象创建一个用于加密的接口,使用aes ctr模式加密。
  contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher)

  // 获取一个用于客户端加密的已创建bucket。
  // 客户端加密bucket和普通bucket具有相似的用法。
  cryptoBucket, err := osscrypto.GetCryptoBucket(client, "<yourBucketName>", contentProvider)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  fileName := "<yourLocalFilePath>"
  fileInfo, err := os.Stat(fileName)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  fileSize := fileInfo.Size()

  // 加密上下文信息。
  var cryptoContext osscrypto.PartCryptoContext
  cryptoContext.DataSize = fileSize

  // 期望的分片数,实际分片数以后续计算出来的为准。
  expectPartCount := int64(10)

  // 目前aes ctr加密分片大小需16个字节对齐。
  cryptoContext.PartSize = (fileSize / expectPartCount / 16) * 16

  imur, err := cryptoBucket.InitiateMultipartUpload("<yourObjectName>", &cryptoContext)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  var partsUpload []oss.UploadPart
  for _, chunk := range chunks {
    part, err := cryptoBucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number), cryptoContext)
    if err != nil {
      fmt.Println("Error:", err)
      os.Exit(-1)
    }
    partsUpload = append(partsUpload, part)
  }

  // Complete
  _, err = cryptoBucket.CompleteMultipartUpload(imur, partsUpload)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
}

解密使用主密钥为不同的RSA加密的Object

解密使用主密钥为不同的RSA加密的Object示例代码如下:

package main

import (
  "bytes"
  "fmt"
  "io/ioutil"
  "os"

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

// 根据主秘钥描述信息查询主秘钥。如果需要解密不同的主秘钥加密的object,需要提供此接口。
type MockRsaManager struct {
}

func (mg *MockRsaManager) GetMasterKey(matDesc map[string]string) ([]string, error) {  
  keyList := []string{"<yourRsaPublicKey", "yourRsaPrivatKey"}
  return keyList, nil
}

// 解密不同主秘钥加密的object。
func main() {
  // 创建OSSClient实例。
  client, err := oss.New("<yourEndpoint>", "<yourAccessKeyId>", "<yourAccessKeySecret>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 创建一个主秘钥的描述信息,创建后不允许修改。主秘钥描述信息和主秘钥一一对应。
  // 如果所有的Object都使用相同的主秘钥,主秘钥描述信息可以为空,但后续不支持更换主秘钥。
  // 如果主秘钥描述信息为空,解密时无法判断使用的是哪个主秘钥。
  // 强烈建议为每个主秘钥都配置主秘钥描述信息(json字符串),由客户端保存主秘钥和描述信息之间的对应关系(服务端不保存两者之间的对应关系)。

    // 由主秘钥描述信息(json字符串)转换的map。
  materialDesc := make(map[string]string)
  materialDesc["desc"] = "<your master encrypt key material describe information>"

  // 根据主秘钥描述信息创建一个主秘钥对象。
  masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "<your rsa public key>", "<your rsa private key>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 根据主秘钥对象创建一个用于加密的接口,使用aes ctr模式加密。
  contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher)

  // 如果需要解密不同主秘钥加密的object,需要提供此接口。
  var mockRsaManager MockRsaManager
  var options []osscrypto.CryptoBucketOption
  options = append(options, osscrypto.SetMasterCipherManager(&mockRsaManager))

  // 获取一个用于客户端加密的已创建bucket。
  // 客户端加密bucket和普通bucket具有相似的用法。
  cryptoBucket, err := osscrypto.GetCryptoBucket(client, "<yourBucketName>", contentProvider, options...)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // put object时自动加密。
  err = cryptoBucket.PutObject("<yourObjectName>", bytes.NewReader([]byte("yourObjectValueByteArrary")))
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // get object时自动解密。
  body, err := cryptoBucket.GetObject("<otherObjectNameEncryptedWithOtherRsa>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  defer body.Close()

  data, err := ioutil.ReadAll(body)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  fmt.Println("data:", string(data))
}

使用主秘钥KMS普通上传和下载Object

使用主秘钥KMS普通上传和下载Object示例代码如下:

package main

import (
  "bytes"
  "fmt"
  "io/ioutil"
  "os"

  kms "github.com/aliyun/alibaba-cloud-sdk-go/services/kms"
  "github.com/aliyun/aliyun-oss-go-sdk/oss"
  "github.com/aliyun/aliyun-oss-go-sdk/oss/crypto"
)

func main() {
  // 创建OSSClient实例。
  client, err := oss.New("<yourEndpoint>", "<yourAccessKeyId>", "<yourAccessKeySecret>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 创建kms client实例。
  kmsClient, err := kms.NewClientWithAccessKey("<yourKmsRegion>", "<yourKmsAccessKeyId>", "<yourKmsAccessKeySecret>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 创建一个主秘钥的描述信息,创建后不允许修改。主秘钥描述信息和主秘钥一一对应。
  // 如果所有的Object都使用相同的主秘钥,主秘钥描述信息可以为空,但后续不支持更换主秘钥。
  // 如果主秘钥描述信息为空,解密时无法判断使用的是哪个主秘钥。
  // 强烈建议为每个主秘钥都配置主秘钥描述信息(json字符串),由客户端保存主秘钥和描述信息之间的对应关系(服务端不保存两者之间的对应关系)。

    // 由主秘钥描述信息(json字符串)转换的map。
  materialDesc := make(map[string]string)
  materialDesc["desc"] = "<your kms encrypt key material describe information>"

  // 根据主秘钥描述信息创建一个主秘钥对象。
  masterkmsCipher, err := osscrypto.CreateMasterAliKms(materialDesc, "<YourKmsId>", kmsClient)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // 根据主秘钥对象创建一个用于加密的接口,使用aes ctr模式加密。
  contentProvider := osscrypto.CreateAesCtrCipher(masterkmsCipher)

  // 获取一个用于客户端加密的已创建bucket。
  // 客户端加密bucket和普通bucket具有相似的用法。
  cryptoBucket, err := osscrypto.GetCryptoBucket(client, "<yourBucketName>", contentProvider)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // put object时自动加密。
  err = cryptoBucket.PutObject("<yourObjectName>", bytes.NewReader([]byte("yourObjectValueByteArrary")))
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }

  // get object时自动解密。
  body, err := cryptoBucket.GetObject("<yourObjectName>")
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  defer body.Close()

  data, err := ioutil.ReadAll(body)
  if err != nil {
    fmt.Println("Error:", err)
    os.Exit(-1)
  }
  fmt.Println("data:", string(data))
}