亚马逊AWS官方博客

AWS KMS 实现跨租户的安全数据加密

1 技术背景

在大量saas 平台客户需求实践中,有非常多的User担心敏感数据如何保存,如何安全隔离敏感数据。本文主要针对saas平台敏感数据加密需求,利用AWS KMS 服务,结合用户场景构建数据安全加密方案。

 

1.1 问题描述

  • 对客户数据里的指定数据加密(如:姓名,电话,地址等);
  • 密码与加解密权限要有严格的管理方案(隔离租户与用户间的使用权限);
  • 隔离除系统管理员外其他开发和运维用户访问用户数据;
  • 记录CMK 访问记录,用户可以审计data key使用情况;
  • 用户可以拥有全部的CMK管理权限(可以随时修改CMK使用策略);
  • 采用行业认证的公开加密算法和加密流程加密数据(信封加密)。

1.2 系统架构

  • AWS cognito 实现saas平台认证机制集成;
  • 通过AWS KMS 服务实现主密钥的安全管理;
  • AWS encryption SDK 实现信封加密;
  • IAM Policy 实现系统层的安全隔离;
  • Cloud trail 实现操作审计;

 

1.3 数据加密解密流程

 

 

2 KMS 配置

2.1 创建KMS CMK

 

 

2.2 创建别名

 

2.3 配置CMK 及管理权限

安全考虑CMK管理和数据加密权限是分开管理

 

防止管理员误删除CMK

 

2.4 配置用户使用用户(方案中选定角色,与下面Cognito 角色一致)

 

2.5 编辑KMS 策略(只允许指定的租户可以使用CMK)

{
    "Id": "key-consolepolicy",
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Enable IAM User Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws-cn:iam::accountid:root"
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "Allow use of the key",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws-cn:iam::accountid:role/Cognito_testAuth_Role"
            },
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "*",
            "Condition": {
                "StringLike": {
                    "cognito-identity.amazonaws.com:amr":
                    "[\"authenticated\",\"cognito Developer provider name\",\"cognito Developer provider name:cn-north-1:????????????????????????????????????:租户ID:*\"]"
                }
            }
        }
    ]
}

 

3 配置 Cognito identity pool 及IAM 角色

3.1 创建Identity pool 及 IAM role

 

3.2 配置 Developer provider name

 

3.3 创建IAM 角色

利用identity pool 生成的 身份证书管理CMK(权限已在 CMK 管理中指定),删除其他的权限

4 实现 Cognito Developer provider

/* 
 *  
 *  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *  
 *  Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
 *  in compliance with the License. A copy of the License is located at
 *  
 *  http://aws.amazon.com/apache2.0
 *  
 *  or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *  specific language governing permissions and limitations under the License.
 *   
 */

package com.amazon.saas.idp;

import java.util.Collections;
import java.util.Map;

import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentity;
import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentityClientBuilder;
import com.amazonaws.services.cognitoidentity.model.GetCredentialsForIdentityRequest;
import com.amazonaws.services.cognitoidentity.model.GetCredentialsForIdentityResult;
import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenForDeveloperIdentityRequest;
import com.amazonaws.services.cognitoidentity.model.GetOpenIdTokenForDeveloperIdentityResult;

public class CognitoDeveloperIdentityProvider {
    private String region = "cn-north-1";
    //cognito 对于 sts 的 provider id 中国区选择 "cognito-identity---cn-north-1.amazonaws.com.rproxy.goskope.com.cn"
    private String cognitoProviderId= "cognito-identity---cn-north-1.amazonaws.com.rproxy.goskope.com.cn"
  
    // 前面配置的 Developer Identity Provider Name
    private String developerIdentityProviderName = "developerIdentityProviderName";
    // indentity pool id 如 cn-north-1:cc310c44-de78-4661-8e6e-8cc21d974058
    
    private String identityPoolId="cn-north-1:cc310c44-de78-4661-8e6e-8cc21d974058";

    private AmazonCognitoIdentity amazonCognitoIdentity;
    // 构建 AmazonCognitoIdentityClient
    public void init() {
        AmazonCognitoIdentityClientBuilder cidBuilder = AmazonCognitoIdentityClientBuilder.standard();
        cidBuilder.setRegion(region);
        cidBuilder.setCredentials(DefaultAWSCredentialsProviderChain.getInstance());
        amazonCognitoIdentity = cidBuilder.build();
    }
    //获取 cognito OpenIdToken
    public GetOpenIdTokenForDeveloperIdentityResult getOpenIdTokenFromCognito(String tenantid, String userid) {
        GetOpenIdTokenForDeveloperIdentityRequest request = new GetOpenIdTokenForDeveloperIdentityRequest();
        request.setIdentityPoolId(identityPoolId);
        Map<String, String> logins = Collections.singletonMap(developerIdentityProviderName, tenantid + ":" + userid);
        request.setLogins(logins);
        GetOpenIdTokenForDeveloperIdentityResult result = amazonCognitoIdentity
                .getOpenIdTokenForDeveloperIdentity(request);
        return result;
    }
    //获取 aws credentials 用于调用 KMS
    public GetCredentialsForIdentityResult getCredentialsForIdentity(String identityid, String token, TenantUserInfo userinfo) {
        GetCredentialsForIdentityRequest request = new GetCredentialsForIdentityRequest();
        request.setIdentityId(identityid);
        request.setLogins(Collections.singletonMap(cognitoProviderId, token));
        GetCredentialsForIdentityResult result = amazonCognitoIdentity.getCredentialsForIdentity(request);
        return  result;
    }

}

 

5 实现数据加密解密

5.1 AWS 证书与data key 缓存

/* 
 *  
 *    Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *    
 *    Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
 *    in compliance with the License. A copy of the License is located at
 *    
 *    http://aws.amazon.com/apache2.0
 *    
 *    or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *    specific language governing permissions and limitations under the License.
 *   
 */

package com.amazon.saas.tes;

import java.util.concurrent.TimeUnit;

import com.amazon.saas.tes.TenantCryptoMaterialsManagerHolder.Cachekey;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.MasterKeyProvider;
import com.amazonaws.encryptionsdk.caching.CachingCryptoMaterialsManager;
import com.amazonaws.encryptionsdk.caching.CryptoMaterialsCache;
import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCache;
import com.amazonaws.encryptionsdk.kms.KmsMasterKey;
import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
public class TenantCryptoMaterialsManagerHolder extends CacheLoader<Cachekey, CryptoMaterialsManager> {
    @Getter
    @Setter
    @AllArgsConstructor
    public static class Cachekey {
        private String tenantID;
        private String userid;
    }
    @Data
    public static class Config {
        private String keyArn;
        private int maxCacheSize;
        private int maxEntryAge;
        private int credentialsDuration;
    
    }
    private LoadingCache<Cachekey, CryptoMaterialsManager> cache;
    private Config config;
    public void init() {
        //配置缓存参数
        config=new Config();
        cache = CacheBuilder.newBuilder().maximumSize(10000)
                .expireAfterWrite(config.getCredentialsDuration(), TimeUnit.MINUTES).build(this);
    }

    public CryptoMaterialsManager createMaterialsManager(Cachekey key) {
        Credentials credentialsForIdentity = cognitoDeveloperIdentityProviderClient;
       
       
        MasterKeyProvider<KmsMasterKey> keyProvider = KmsMasterKeyProvider.builder()
                .withKeysForEncryption(config.getKeyArn())
                .withCredentials(new BasicSessionCredentials(credentialsForIdentity.getAccessKeyId(),
                        credentialsForIdentity.getSecretKey(), credentialsForIdentity.getSessionToken()))
                .build();

        
        int MAX_CACHE_SIZE = config.getMaxCacheSize();
        CryptoMaterialsCache cache = new LocalCryptoMaterialsCache(MAX_CACHE_SIZE);
       
        int MAX_ENTRY_AGE_SECONDS = config.getMaxEntryAge();
        int MAX_ENTRY_MSGS = config.getMaxCacheSize();

        
        CryptoMaterialsManager cachingCmm = CachingCryptoMaterialsManager.newBuilder()
                .withMasterKeyProvider(keyProvider).withCache(cache).withMaxAge(MAX_ENTRY_AGE_SECONDS, TimeUnit.SECONDS)
                .withMessageUseLimit(MAX_ENTRY_MSGS).build();
        return cachingCmm;
    }

    public CryptoMaterialsManager getMaterialsManager(TenantUserInfo userinfo) {
         return cache.get(new Cachekey(userinfo.getTenantID(), userinfo.getUserid()));
    }

    @Override
    public CryptoMaterialsManager load(Cachekey key) throws Exception {
        return createMaterialsManager(key);
    }

}

 

5.2 数据加密解密

/* 
 *  
 *    Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *    
 *    Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
 *    in compliance with the License. A copy of the License is located at
 *    
 *    http://aws.amazon.com/apache2.0
 *    
 *    or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 *    specific language governing permissions and limitations under the License.
 *   
 */

package com.amazon.saas.tes;

import java.util.Collections;

import com.amazonaws.encryptionsdk.AwsCrypto;
import com.amazonaws.encryptionsdk.CryptoMaterialsManager;
import com.amazonaws.encryptionsdk.CryptoResult;

import org.springframework.beans.factory.annotation.Autowired;

public class EncDecService {
    private final AwsCrypto crypto = new AwsCrypto();
    @Autowired
    TenantCryptoMaterialsManagerHolder tenantCryptoMaterialsManagerHolder;

    public TESEncryptedMessage encrypt(String plaintext, String authorizationHeader, TenantUserInfo userinfo) {
        CryptoMaterialsManager cmm = tenantCryptoMaterialsManagerHolder.getMaterialsManager(authorizationHeader,
                userinfo);
        String message = crypto.encryptString(cmm, plaintext,
                Collections.singletonMap("tenantid.userid", userinfo.getTenantID() + "." + userinfo.getUserid()))
                .getResult();
        TESEncryptedMessage re = new TESEncryptedMessage();
        re.setEncyptedMsg(message);
        return re;
    }

    public TesPlainText dectypt(String encryptMessage, String authorizationHeader, TenantUserInfo userinfo) {
        CryptoMaterialsManager cmm = tenantCryptoMaterialsManagerHolder.getMaterialsManager(authorizationHeader,
                userinfo);
        CryptoResult<String, ?> decryptResult = crypto.decryptString(cmm, encryptMessage);
        TesPlainText re = new TesPlainText();
        re.setText(decryptResult.getResult());
        return re;

    }

 

 

本篇作者

任耀洲

AWS解决方案架构师,负责企业客户应用在AWS的架构咨询和 设计。在微服务架构设计、数据库等领域有丰富的经验