客户端加密

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

免责声明

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

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

使用场景

  • 高度敏感数据:对于包含极高敏感度信息的数据,如个人身份信息(PII)、金融交易记录、医疗健康数据等,用户可能希望在数据离开本地环境之前就对其进行加密处理,确保即使数据在传输过程中被截获,原始数据仍能得到有效保护。

  • 合规要求:某些行业和法规(例如HIPAA、GDPR等)要求对存储在第三方平台上的数据进行严格的加密控制,客户端加密能够满足这些合规性要求,因为密钥由用户自己管理,不通过网络传递,也不由云服务商直接掌握。

  • 更强的自主控制权:企业或者开发者可能希望对加密过程有完全的控制权,包括选择加密算法、管理和轮换密钥。通过客户端加密,可以实现这一目标,确保只有合法授权的用户才能解密和访问数据。

  • 跨区域数据迁移安全性:在将数据从一个地区迁移到另一个地区的过程中,使用客户端加密可以在数据迁移前后保持数据始终处于加密状态,增强了数据在公网传输的安全性。

背景信息

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

重要
  • 客户端加密支持分片上传超过5 GB的文件。在使用分片方式上传文件时,需要指定上传文件的总大小和分片大小, 除了最后一个分片外,其他每个分片的大小要一致,且分片大小目前必须是16的整数倍。

  • 使用客户端加密上传文件后,加密元数据会被保护,无法通过CopyObject修改Object meta信息。

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

完整的示例代码请参见GitHub

使用KMS托管用户主密钥

当使用KMS托管用户主密钥用于客户端数据加密时,无需向OSS加密客户端提供任何加密密钥,只需要在上传Object时指定KMS用户主密钥ID(即CMK ID)。具体工作原理如下图所示。

image
  • 加密并上传Object

    1. 获取加密密钥。

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

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

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

  • 下载并解密Object

    1. 下载Object。

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

    2. 解密Object。

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

说明
  • 客户端会为每一个上传的Object获取一个唯一的数据加密密钥。

  • 为了保证数据的安全性,建议定期轮换或者更新CMK。

  • 您需要维护CMK ID与Object之间的映射关系。

使用用户自主管理密钥

使用用户自主管理密钥时,需要您自主生成并保管加密密钥。当客户端加密Object时,由用户自主上传加密密钥(对称加密密钥或者非对称加密密钥)至客户端。具体加密过程如下图所示。

image
  • 加密并上传Object

    1. 用户向客户端提供1个用户主密钥(对称密钥或者非对称密钥)。

    2. 客户端在本地生成一个一次性的对称密钥,即数据密钥(Data Key)。数据密钥将用于加密单个Object(针对每个Object,客户端都会随机生成1个数据密钥)。

    3. 客户端使用数据密钥加密Object,并使用用户提供的主密钥来加密数据密钥。

    4. 客户端将加密后的Object以及加密的数据密钥(Encrypted Data Key)作为Object元数据的一部分上传至OSS。

  • 下载并解密Object

    1. 客户端从OSS服务端下载加密的Object以及Object元数据。

    2. 通过使用Object元数据中的材料,客户端将授权确定使用对应主密钥来解密数据密钥,之后使用解密后的数据密钥来解密Object。

重要
  • 客户端不会将用户主密钥以及未加密的数据发送至OSS。所以,请务必妥善保管加密密钥,如果密钥丢失,将无法解密数据。

  • 数据密钥由客户端随机生成。

使用阿里云SDK

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

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.*;
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";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写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, credentialsProvider, 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.credentials import EnvironmentVariableCredentialsProvider
from  oss2.crypto import RsaProvider
from oss2.cryptoimportAliKMSProvider

# 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
auth = oss2.ProviderAuth(EnvironmentVariableCredentialsProvider())

kms_provider=AliKMSProvider(auth, 'yourRegion', 'yourCMKID')
bucket = oss2.CryptoBucket(auth, '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() {
	// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
	provider, err := oss.NewEnvironmentVariableCredentialsProvider()
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(-1)
	}
	// 创建OSSClient实例。
	// yourEndpoint填写Bucket对应的Endpoint,以华东1(杭州)为例,填写为https://oss-cn-hangzhou.aliyuncs.com。其它Region请按实际情况填写。
	client, err := oss.New("yourEndpoint", "", "", oss.SetCredentialsProvider(&provider))
	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"

	// 根据主密钥描述信息创建一个主密钥对象。
	// yourRsaPublicKey填写您自主管理的主密钥公钥信息,yourRsaPrivateKey填写您自主管理的主密钥私钥信息。
	masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "yourRsaPublicKey", "yourRsaPrivateKey")
	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)
	}

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

	// GetObject时自动解密。
	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账号信息。*/
    
    /* yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。*/
    std::string Endpoint = "yourEndpoint";
    /* 填写Bucket名称,例如examplebucket。*/
    std::string BucketName = "examplebucket";
    /* 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。*/
    std::string ObjectName = "exampledir/exampleobject.txt";

    /* 主密钥及描述信息。*/
    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;
    /* 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。*/
    auto credentialsProvider = std::make_shared<EnvironmentVariableCredentialsProvider>();
    OssClient client(Endpoint, credentialsProvider, conf);
  
    CryptoConfiguration cryptoConf;
    auto materials = std::make_shared<SimpleRSAEncryptionMaterials>(RSAPublicKey, RSAPrivateKey, desc);
    OssEncryptionClient client(Endpoint, credentialsProvider, 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;
        return -1;
    }
    /* 释放网络等资源。*/
    ShutdownSdk();
    return 0;
}