客户端加密是指将文件(Object)发送到对象存储OSS之前在本地进行加密,本文介绍客户端加密的方式与流程。

免责声明

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

背景信息

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

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

完整的示例代码请参见GitHub

使用KMS托管用户主密钥

当使用KMS托管用户主密钥用于客户端数据加密时,无需向OSS加密客户端提供任何加密密钥,只需要在上传对象时指定KMS用户主密钥ID(也就是CMK ID)。具体工作原理如下图所示。加密2
  • 加密并上传Object
    1. 获取加密密钥。

      通过使用CMK ID,客户端首先向KMS发送一个请求,申请1个用于加密Object的数据密钥(Data Key)。作为响应,KMS会返回一个随机生成的数据明文密钥(Data Key)以及一个数据密文密钥(Encrypted Data Key)。

    2. 加密数据并上传至OSS。

      本地客户端接收到KMS返回的数据明文密钥以及数据密文密钥后,将使用数据明文密钥进行本地加密,并且将加密后的对象以及数据密文密钥上传至OSS。

  • 下载并解密Object
    1. 下载Object。

      客户端从OSS服务端下载加密的Object以及作为对象元数据存储的数据密文密钥。

    2. 解密Object。

      客户端将数据密文密钥以及CMK ID发送至KMS服务器。作为响应,KMS将使用指定的CMK解密,并且将数据明文密钥返回给本地加密客户端。

说明
  • 本地加密客户端为每一个上传的对象获取一个唯一的数据加密密钥。
  • 为了保证数据的安全性,建议CMK定期轮换或者更新。
  • 您需要维护CMK ID与Object之间的映射关系。

使用用户自主管理密钥

使用用户自主管理密钥时,需要您自主生成并保管加密密钥。当本地客户端加密Object时,由用户自主上传加密密钥(对称加密密钥或者非对称加密密钥)至本地加密客户端。其具体加密过程如下图所示。key3
  • 加密并上传Object
    1. 用户向本地加密客户端提供1个用户主密钥(对称密钥或者非对称密钥)。
    2. 本地加密客户端在本地生成一个一次性的对称密钥,即数据密钥(Data Key)。它将用于加密单个对象(针对每个对象,客户端都会随机生成1个数据密钥)。
    3. 客户端使用数据密钥加密对象,并使用用户提供的主密钥来加密数据密钥。
    4. 客户端将加密的数据密钥(Encrypted Data Key)作为对象元数据的一部分上传至OSS。
  • 下载并解密Object
    1. 客户端从OSS下载加密的对象以及元数据。
    2. 通过使用元数据中的材料,客户端将授权确定使用对应主密钥来解密数据密钥,之后使用解密后的数据密钥来解密对象。
注意
  • OSS本地加密客户端不会将用户主密钥以及未加密的数据发送至OSS。所以,请务必妥善保管加密密钥,如果密钥丢失,将无法解密数据。
  • 数据密钥由本地加密客户端随机生成。

使用阿里云SDK

以下仅列举常见SDK客户端加密的代码示例。关于其他SDK客户端加密的代码示例,请参见SDK简介

import com.aliyun.oss.*;
import com.aliyun.oss.crypto.SimpleRSAEncryptionMaterials;
import com.aliyun.oss.model.OSSObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) throws Throwable {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "yourAccessKeyId";
        String accessKeySecret = "yourAccessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "examplebucket";
        // 填写Object完整路径,例如exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "exampleobject.txt";
        String content = "Hello OSS!";

        // 填写您的RSA私钥字符串,可以使用OpenSSL工具生成。以下为RSA私钥字符串的示例值。
        final String PRIVATE_PKCS1_PEM =
                "-----BEGIN RSA PRIVATE KEY-----\n" +
                "MIICWwIBAAKBgQCokfiAVXXf5ImFzKDw+XO/UByW6mse2QsIgz3ZwBtMNu59fR5z\n" +
                "ttSx+8fB7vR4CN3bTztrP9A6bjoN0FFnhlQ3vNJC5MFO1PByrE/MNd5AAfSVba93\n" +
                "I6sx8NSk5MzUCA4NJzAUqYOEWGtGBcom6kEF6MmR1EKib1Id8hpooY5xaQIDAQAB\n" +
                "AoGAOPUZgkNeEMinrw31U3b2JS5sepG6oDG2CKpPu8OtdZMaAkzEfVTJiVoJpP2Y\n" +
                "nPZiADhFW3e0ZAnak9BPsSsySRaSNmR465cG9tbqpXFKh9Rp/sCPo4Jq2n65yood\n" +
                "JBrnGr6/xhYvNa14sQ6xjjfSgRNBSXD1XXNF4kALwgZyCAECQQDV7t4bTx9FbEs5\n" +
                "36nAxPsPM6aACXaOkv6d9LXI7A0J8Zf42FeBV6RK0q7QG5iNNd1WJHSXIITUizVF\n" +
                "6aX5NnvFAkEAybeXNOwUvYtkgxF4s28s6gn11c5HZw4/a8vZm2tXXK/QfTQrJVXp\n" +
                "VwxmSr0FAajWAlcYN/fGkX1pWA041CKFVQJAG08ozzekeEpAuByTIOaEXgZr5MBQ\n" +
                "gBbHpgZNBl8Lsw9CJSQI15wGfv6yDiLXsH8FyC9TKs+d5Tv4Cvquk0efOQJAd9OC\n" +
                "lCKFs48hdyaiz9yEDsc57PdrvRFepVdj/gpGzD14mVerJbOiOF6aSV19ot27u4on\n" +
                "Td/3aifYs0CveHzFPQJAWb4LCDwqLctfzziG7/S7Z74gyq5qZF4FUElOAZkz123E\n" +
                "yZvADwuz/4aK0od0lX9c4Jp7Mo5vQ4TvdoBnPuGo****\n" +
                "-----END RSA PRIVATE KEY-----";
        // 填写您的RSA公钥字符串,可以使用OpenSSL工具生成。以下为RSA公钥字符串的示例值。
        final String PUBLIC_X509_PEM =
                "-----BEGIN PUBLIC KEY-----\n" +
                "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCokfiAVXXf5ImFzKDw+XO/UByW\n" +
                "6mse2QsIgz3ZwBtMNu59fR5zttSx+8fB7vR4CN3bTztrP9A6bjoN0FFnhlQ3vNJC\n" +
                "5MFO1PByrE/MNd5AAfSVba93I6sx8NSk5MzUCA4NJzAUqYOEWGtGBcom6kEF6MnR\n" +
                "1EKib1Id8hpooY5xaQID****\n" +
                "-----END PUBLIC KEY-----";

        // 创建一个RSA密钥对。
        RSAPrivateKey privateKey = SimpleRSAEncryptionMaterials.getPrivateKeyFromPemPKCS1(PRIVATE_PKCS1_PEM);
        RSAPublicKey publicKey = SimpleRSAEncryptionMaterials.getPublicKeyFromPemX509(PUBLIC_X509_PEM);
        KeyPair keyPair = new KeyPair(publicKey, privateKey);

        // 创建主密钥RSA的描述信息。创建后不允许修改。主密钥描述信息和主密钥一一对应。
        // 如果所有的object都使用相同的主密钥,主密钥描述信息可以为空,但后续不支持更换主密钥。
        // 如果主密钥描述信息为空,解密时无法判断文件使用的是哪个主密钥进行加密。
        // 强烈建议为每个主密钥都配置描述信息,由客户端保存主密钥和描述信息之间的对应关系(服务端不保存两者之间的对应关系)。
        Map<String, String> matDesc = new HashMap<String, String>();
        matDesc.put("desc-key", "desc-value");

        // 创建RSA加密材料。
        SimpleRSAEncryptionMaterials encryptionMaterials = new SimpleRSAEncryptionMaterials(keyPair, matDesc);
        // 如果要下载并解密其他RSA密钥加密的文件,请将其他主密钥及其描述信息添加到加密材料中。
        // encryptionMaterials.addKeyPairDescMaterial(<otherKeyPair>, <otherKeyPairMatDesc>);

        // 创建加密客户端。
        OSSEncryptionClient ossEncryptionClient = new OSSEncryptionClientBuilder().
                build(endpoint, accessKeyId, accessKeySecret, encryptionMaterials);

        try {
            // 加密上传文件。
            ossEncryptionClient.putObject(bucketName, objectName, new ByteArrayInputStream(content.getBytes()));

            // 下载文件时自动解密。
            OSSObject ossObject = ossEncryptionClient.getObject(bucketName, objectName);
            BufferedReader reader = new BufferedReader(new InputStreamReader(ossObject.getObjectContent()));
            StringBuffer buffer = new StringBuffer();
            String line;
            while ((line = reader.readLine()) != null) {
                buffer.append(line);
            }
            reader.close();

            // 查看解密后的内容是否与上传的明文一致。
            System.out.println("Put plain text: " + content);
            System.out.println("Get and decrypted text: " + buffer.toString());
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossEncryptionClient != null) {
                ossEncryptionClient.shutdown();
            }
        }
    }
}
# -*- coding: utf-8 -*-
import os
import oss2
from  oss2.crypto import RsaProvider

# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
auth = oss2.Auth('yourAccessKeyId', 'yourAccessKeySecret')

kms_provider=AliKMSProvider('yourAccessKeyId', 'yourAccessKeySecret', 'yourRegion', 'yourCMKID')
bucket = oss2.CryptoBucket(oss2.Auth('yourAccessKeyId', 'yourAccessKeySecret'), 'yourEndpoint', 'yourBucketName', crypto_provider = kms_provider)

key = 'motto.txt'
content = b'a' * 1024 * 1024
filename = 'download.txt'


# 上传文件。
bucket.put_object(key, content, headers={'content-length': str(1024 * 1024)})

# 下载OSS文件到本地内存。
result = bucket.get_object(key)

# 验证获取到的文件内容跟上传时的文件内容是否一致。
content_got = b''
for chunk in result:
    content_got += chunk
assert content_got == content

# 下载OSS文件到本地文件。
result = bucket.get_object_to_file(key, filename)

# 验证获取到的文件内容跟上传时的文件内容是否一致。
with open(filename, 'rb') as fileobj:
    assert fileobj.read() == content
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))
}
#include <alibabacloud/oss/OssEncryptionClient.h>
using namespace AlibabaCloud::OSS;

int main(void)
{
    /* 初始化OSS账号信息 */
    std::string AccessKeyId = "yourAccessKeyId";
    std::string AccessKeySecret = "yourAccessKeySecret";
    std::string Endpoint = "yourEndpoint";
    std::string BucketName = "yourBucketName";
    std::string ObjectName = "yourObjectName";

    /* 主密钥及描述信息*/
    std::string RSAPublicKey = "your rsa public key";
    std::string RSAPrivateKey = "your rsa private key";
    std::map<std::string, std::string> desc;
    desc["comment"] = "your comment";

    /* 初始化网络等资源 */
    InitializeSdk();
    ClientConfiguration conf;
    CryptoConfiguration cryptoConf;
    auto materials = std::make_shared<SimpleRSAEncryptionMaterials>(RSAPublicKey, RSAPrivateKey, desc);
    OssEncryptionClient client(Endpoint, AccessKeyId, AccessKeySecret, conf, materials, cryptoConf);
    /* 上传文件 */
    auto outcome = client.PutObject(BucketName, ObjectName, "yourLocalFilename");
    if (!outcome.isSuccess()) {
        /* 异常处理 */
        std::cout << "PutObject fail" <<
        ",code:" << outcome.error().Code() <<
        ",message:" << outcome.error().Message() <<
        ",requestId:" << outcome.error().RequestId() << std::endl;
        ShutdownSdk();
        return -1;
    }
    /* 释放网络等资源 */
    ShutdownSdk();
    return 0;
}