文章目录
设计一个支持敏感数据存储和传输安全的加解密平台
1. 设计背景
在任何一个现代应用系统中,数据管理是核心功能之一。系统需要存储和传输大量的用户数据,这些数据可能涉及个人信息如姓名、手机号码等,甚至包括高度敏感的信息如密码和信用卡号。对这些数据的保护至关重要,因为它们直接关系到用户的隐私和财产安全。如果敏感数据未经加密直接存储在数据库中、记录在日志中,或通过公共网络传输,一旦这些数据遭到泄露,就会带来严重的后果。经济损失是显而易见的,可能导致用户财产损失、公司赔偿巨额损失等。此外,公司还可能面临严重的公关危机,失去客户信任,同时可能面临法律诉讼,导致公司在市场中的信誉和地位一落千丈,甚至可能导致公司无法继续运营。
因此,为了防止上述严重后果的发生,对敏感信息进行加密处理是必不可少的。加密是指将明文数据通过某种算法转换成密文数据,以密文的形式进行存储和传输。这样,即便黑客成功攻击系统,窃取了数据,也只能获得不可读的密文,无法获取到真实的明文内容,从而有效保护了敏感信息的安全。
在实际应用中,当应用程序需要使用这些存储的密文时,可以通过解密过程将密文转换回明文。这一过程使得数据在需要时可以被正常访问和使用,同时在存储和传输过程中保持高度安全。这样,加解密机制不仅保障了数据的安全性,同时也确保了数据的可用性。整个数据安全体系依赖于加密和解密过程的安全性。加密和解密过程通常涉及到几个关键组件:加密算法、加密密钥、解密算法和解密密钥。加密算法是用来将明文转换为密文的规则或方法,加密密钥是参与加密过程的重要参数。相应地,解密算法是将密文还原为明文的规则或方法,解密密钥是参与解密过程的重要参数。
在对称加密中,加密密钥和解密密钥是相同的,这意味着同一个密钥既用于加密也用于解密。使用加密算法时,明文输入与密钥结合,产生密文。相反,使用相同的密钥与解密算法时,密文输入可还原为明文。这种方法具有计算速度快、效率高的优点,但密钥的管理和分发需要特别小心,以确保密钥不被泄露。
加密算法和密钥是数据安全的核心。如果攻击者掌握了这两者,就能轻易将密文还原成明文。因此,单纯依靠加密并不足以确保数据安全,必须确保算法和密钥的安全性。实际开发过程中,解密算法和密钥经常会被硬编码在源代码中并保存在代码仓库中。如果黑客攻破了代码仓库,或者内部人员泄露了源代码,黑客就能获取到解密算法和密钥,从而解密所有敏感数据,这会导致巨大的安全隐患。系统间的数据传输也常常需要加密。例如,与银行进行信用卡信息的加密传输。在这种情况下,双方需要交换密钥,以便能够正确加密和解密数据。这个过程需要确保密钥在传递过程中不会被泄露。如果密钥在交换过程中被泄露,导致重大经济损失,负责密钥保管的人员将难以证明自己的清白。这种责任风险会导致没有人愿意承担密钥保管的职责,从而影响系统的正常运作。
为了解决上述问题,可以设计一个加解密服务系统。这个系统负责统一管理所有的加解密算法和密钥,从而集中控制和保护敏感数据。应用程序不再直接处理加解密算法和密钥,而是通过SDK调用该系统服务的接口进行加解密操作。这样,实际的加解密过程在服务端完成,确保算法和密钥的安全性。
2. 需求分析
日常开发中的加解密程序常见问题
- 密钥(包括非对称加解密证书)保存在源文件或者配置文件中,存储分散而不安全。
- 问题: 密钥通常被硬编码在源代码中或存储在配置文件中,这样的做法使密钥存储分散,且不够安全。攻击者如果能够获取到代码或配置文件,就可以轻松获取到密钥,从而破解加密的数据。
- 影响: 密钥的泄露可能导致系统数据的全面泄露和安全性损害。为了确保密钥的安全,应该避免将密钥暴露在源代码中或配置文件中,采用集中化的密钥管理机制。
- 密钥没有分片交换机制,不能满足高安全级密钥管理和交换的要求。
- 问题: 在缺乏密钥分片和安全交换机制的情况下,密钥在传输过程中可能被截获或泄露。缺乏高安全级别的密钥管理和交换机制,会使密钥的管理和使用变得不够安全。
- 影响: 密钥的传输安全性得不到保障,可能会导致密钥被恶意用户获取。为了提高密钥的安全性,应使用密钥分片和安全交换机制,将密钥安全地传输给授权的系统或人员。
- 密钥缺乏版本管理,不能灵活升级,一旦修改密钥,此前加密的数据就可能无法解密。
- 问题: 如果密钥缺乏版本管理,密钥的更新可能会导致旧密钥加密的数据无法解密。没有版本管理的密钥系统难以应对密钥的更新和维护需求。
- 影响: 密钥的更新可能会导致数据访问困难,特别是在旧数据需要解密时。为了避免这种情况,密钥系统需要具备版本管理功能,支持密钥的平滑过渡和升级。
- 加密解密算法程序不统一,同样算法不同实现,内部系统之间密文不能正确解析。
- 问题: 不同系统或应用可能使用相同的加密算法,但由于实现方式不同,可能会导致密文无法正确解析。加密和解密过程中的不一致会影响数据的正确性和可用性。
- 影响: 不同系统之间的数据交换和兼容性受到影响。为了确保数据的一致性,应确保使用相同算法的统一实现,并对加密解密过程进行标准化。
- 部分加解密算法程序使用了弱加解密算法和弱密钥,存在安全隐患。
- 问题: 使用弱加密算法和密钥会使数据容易被破解,导致系统安全性降低。某些旧算法或不推荐的算法可能不再满足现代安全标准。
- 影响: 数据可能容易被攻击者破解,从而暴露敏感信息。为了提高安全性,必须使用符合当前安全标准的强加密算法和密钥。
解决方案
为了应对以上问题,需要设计一个专门的加解密服务和密钥管理系统。以下是设计目标和核心功能:
作为一个加解密服务系统,旨在解决上述问题,核心功能包括加解密服务,辅助功能包括密钥与算法管理。
- 核心功能 - 加解密服务:
- 提供加密和解密功能,确保敏感数据在存储和传输过程中保持安全。
- 通过服务接口,应用程序只需调用统一的加解密接口进行数据处理,而无需直接处理算法和密钥。
- 辅助功能 - 密钥与算法管理:
- 集中密钥存储: 密钥存储在集中管理的系统中,避免了分散存储带来的安全隐患。
- 密钥分片与交换: 实现密钥的安全分片和交换机制,提高密钥的管理和传输安全性。
- 密钥版本管理: 支持密钥的版本管理和升级,确保旧数据在密钥更新后仍可解密。
- 统一算法实现: 提供标准化的加解密算法实现,确保系统之间的数据兼容性。
- 强加密算法: 使用现代的强加密算法和密钥,避免使用过时或弱算法。
具体来说
- 安全性需求–密钥安全性
- 密钥分片存储:为了保证密钥的安全性,密钥不应该以完整的形式存储在任何单一位置。Venus 系统将密钥至少拆分成两个片段,分别存储在两个不同的物理存储服务器上。这两个存储服务器不仅需要在物理上隔离,还应采用不同的技术架构或平台,以防止单点故障或潜在的安全漏洞影响到整个系统。
- 密钥交换:在需要密钥交换的场景中,例如在系统之间传递密钥时,密钥将被拆分成多个片段。每个负责密钥管理的人员仅能接触到一个密钥片段。只有当所有相关人员都完成密钥片段的交接后,密钥才会被完整地重组并进行交换。这样,即使某个片段被泄露,也不能重构出完整的密钥,确保密钥交换的安全性。
- 可靠性需求–高可用性
- 服务可靠性:加解密服务必须具备高可用性,确保在服务器宕机、网络中断等异常情况下,系统仍能正常进行数据的加解密操作。为此,可以采取以下措施:
- 冗余部署:通过在不同的数据中心或地理位置部署冗余的服务实例,确保服务的持续可用性。
- 故障切换机制:在主要服务实例出现故障时,自动切换到备用实例,以保障业务的连续性。
- 数据备份:定期备份密钥和配置数据,以防止由于数据丢失导致服务中断。
- 性能需求–加解密效率
- 低延迟:加解密操作的时间延迟主要取决于加解密算法的计算过程。系统需要确保:
- 算法加载时间:加解密算法的加载时间应尽可能短,避免因算法加载造成的延迟。
- 密钥获取时间:获取加解密密钥的时间也要尽可能短,以确保密钥从存储中取出并用于加解密操作时不会造成性能瓶颈。
- 优化计算效率:选择性能优化的加解密算法和数据结构,避免不必要的计算开销,确保加解密操作的高效执行。
这些非功能需求的设计和实现将直接影响到系统的稳定性、安全性和用户体验。通过严格的设计和实现,可以保证系统在实际应用中能够满足各种业务需求。
系统主要用例过程和功能
-
开发工程师申请加解密算法和密钥
- 用例描述:开发工程师使用密钥管理功能为自己开发的应用申请加解密算法和密钥。
- 步骤:
- 开发工程师登录系统。
- 在密钥管理控制台中选择“申请加解密算法和密钥”。
- 填写相关信息(如应用名称、需要的算法类型等)。
- 提交申请。
- 系统生成申请记录并通知安全工程师进行审核。
-
安全工程师审核算法和密钥
- 用例描述:安全工程师使用密钥管理功能审核算法和密钥的强度是否满足数据安全要求。
- 步骤:
- 安全工程师登录系统。
- 在密钥管理控制台中查看待审核的算法和密钥申请。
- 根据安全标准审核算法和密钥的强度。
- 审核通过或拒绝并给出反馈意见。
- 系统通知开发工程师审核结果。
-
授权密钥管理者查看密钥片
- 用例描述:经过授权的密钥管理者使用密钥管理功能可以查看密钥的一个分片。
- 步骤:
- 密钥管理者登录系统。
- 在密钥管理控制台中选择“查看密钥片”。
- 系统验证密钥管理者的权限。
- 密钥管理者查看密钥的一个分片。
-
应用程序调用加解密功能
- 用例描述:应用程序调用加解密功能完成数据的加密、解密。
- 步骤:
- 应用程序通过 SDK 调用加解密服务 API。
- 系统根据请求类型(加密或解密)获取对应的算法和密钥。
- 进行数据加解密操作。
- 返回加解密结果给应用程序。
-
加密解密功能和密钥管理功能调用密钥服务
- 用例描述:加密解密功能和密钥管理功能调用密钥服务功能完成密钥的存储和读取。
- 步骤:
- 系统调用密钥服务 API 存储或读取密钥。
- 密钥服务访问安全、可靠的密钥存储系统。
- 完成密钥的存储或读取操作。
-
密钥服务功能访问密钥存储系统
- 用例描述:密钥服务功能访问一个安全、可靠的密钥存储系统读写密钥。
- 步骤:
- 系统通过密钥服务 API 访问密钥存储系统。
- 读取或写入密钥数据。
- 返回操作结果。
系统需求
-
集中、分片密钥存储与管理
- 需求描述:支持密钥的集中存储与分片管理,多存储备份,确保密钥的安全性和易管理性。
- 功能要求:
- 支持密钥的分片存储。
- 提供多种存储备份机制。
- 确保密钥的安全性和可靠性。
-
多角色多权限管理
- 需求描述:密钥申请者、密钥管理者、密钥访问者多角色多权限管理,确保密钥管理与传递的安全。
- 功能要求:
- 支持多角色(开发工程师、安全工程师、密钥管理者)管理。
- 提供细粒度的权限控制。
- 确保密钥管理与传递过程中的安全性。
-
密钥管理控制台
- 需求描述:通过密钥管理控制台完成密钥申请、密钥管理、密钥访问控制等操作,实现便捷的密钥管理。
- 功能要求:
- 提供图形化的密钥管理界面。
- 支持密钥的申请、审核、查看和管理操作。
- 提供操作日志和审计功能。
-
统一加解密服务 API
- 需求描述:提供统一的加解密服务 API,简单接口,统一算法,为内部系统提供一致的加解密算法实现。
- 功能要求:
- 提供统一的加解密接口。
- 支持多种加解密算法。
- 确保接口的简洁性和易用性。
3. 概要设计
应用程序通过调用平台提供的加解密 SDK 服务接口,对信息进行加密和解密。这个 SDK 接口提供了常用的加密和解密算法,并且可以根据需求进行扩展。SDK 加解密服务接口会调用密钥服务器的密钥服务,获取加密和解密所需的密钥,并将这些密钥缓存在本地。
密钥服务器中的密钥来自多个密钥存储服务器。一个密钥被分成多个部分,并存储在不同的存储服务器中,每个服务器都由不同的人负责管理。密钥申请者、密钥管理者和安全审核人员通过密钥管理控制台来管理和更新密钥。每个人各司其职,没有人能够查看到完整的密钥信息。
3.1 部署模型
系统的核心服务器是 Key Server 服务器,它负责提供密钥管理服务。密钥被分片存储在文件服务器(File Store)和数据库(DB)中。
使用加解密服务的应用程序(Application)部署在应用程序服务器(App Server)上,这些应用程序依赖系统提供的 SDK API 来进行数据的加密和解密。SDK 会访问密钥服务器(Key Server)以获取加解密所需的算法代码和密钥。
为了确保安全,密钥被分片存储在文件服务器(Key File Store)和数据库服务器(Key DB)中。因此,Key Server 服务器中部署了密钥管理组件(Key Manager),该组件用于访问数据库中的应用程序密钥元信息(Key Meta Data),以获取密钥分片的存储信息。Key Server 服务器根据这些信息访问 File Store 和 DB,获取密钥分片,并将这些分片拼接成完整的密钥,最终返回给 SDK。
此外,密钥管理控制台(Key Console)提供一个 web 页面,供开发工程师、安全工程师和密钥管理者进行密钥申请、更新、审核和查看等操作。
3.2 加解密调用流程
- 应用程序 App 调用 SDK 对数据进行加密(解密)。
- SDK 检查在本地是否有缓存加解密需要的密钥和加解密算法代码,如果有缓存,就直接使用该算法和密钥进行加解密。
- 如果本地没有缓存密钥和算法,请求远程服务器返回密钥和算法。
- 部署在服务器的 Key Manager 收到请求后,访问数据库,检查该应用配置的密钥和算法 Meta 信息。
- 数据库返回的 Mata 信息中包括了密钥的分片信息和存储位置,Key Manager 访问文件服务器和数据库,获取密钥分片,并将多个分片合并成一个完整密钥,返回给客户端 SDK。
- SDK 收到密钥后,缓存在本地进程内存中,并完成对 App 加解密调用的处理。
通过该设计,我们可以看到,平台对密钥进行分片存储,不同存储服务器由不同运维人员管理。就算需要进行密钥交换,那么参与交换的人员,每个人也只能获得一个密钥分片,无法得到完整的密钥,这样就保证了密钥的安全性。
密钥缓存在 SDK 所在的进程(也就是应用程序 App 所在的进程)中,只有第一次调用时会访问远程的服务器,其他调用只访问本进程缓存。因此加解密的性能只受加解密的数据大小和算法的影响,不受系统服务的性能影响,满足了性能要求。
同时,由于密钥在缓存中,如果服务器临时宕机,或者网络通信中断,也不会影响到应用程序的正常使用,保证了 系统的可靠性。但是如果服务器长时间宕机,那么应用重新启动,本地缓存被清空,就需要重新请求密钥,这时候应用就不可用了。
那么系统如何在这种情况下仍然保证高可用呢?
解决方案就是对系统的服务器、数据库和文件服务器做高可用备份。服务器部署 2-3 台服务器,构建一个小型集群,SDK 通过软负载均衡访问服务器集群,若发现某台服务器宕机,就进行失效转移。同样,数据库和文件服务器也需要做主从备份。
4. 详细设计
详细设计主要关注 SDK 核心类设计。其他的例如数据库结构设计、服务器密钥管理 Console 设计等,这里不做展开。
4.1 密钥领域模型
为了便于 SDK 缓存、管理密钥信息以及 SDK 与系统服务端传输密钥信息,我们需要设计了一个密钥领域模型,如下图:
- 一个应用程序使用的所有密钥信息都记录在 KeyBox 对象中,KeyBox 对象中有一个 keySuitMap 成员变量,这个 map 的 key 是密钥名称,value 是一个 KeySuit 对象。
- KeySuit 类中有一个 keyChainMap 成员变量,这个 map 类的 key 是版本号,value 是一个 KeyChain 对象。系统因为安全性需求,需要支持多版本的密钥。也就是说,对同一类数据的加密密钥过一段时间就会进行版本升级,这样即使密钥泄露,也只会影响一段时间的数据,不会导致所有的数据都被解密。
- KeySuit 类的另一个成员变量 currentVersion 记录当前最新的密钥版本号,也就是当前用来进行数据加密的密钥版本号。而解密的时候,则需要从密文数据中提取出加密密钥版本号(或者由应用程序自己记录密钥版本号,在解密的时候提供给 SDK API),根据这个版本号获取对应的解密密钥。
- 具体每个版本的密钥信息记录在 KeyChain 中,包含了密钥名称 name、密钥版本号 version、加入本地缓存的时间 cache_time、该版本密钥创建的时间 versionTime、对应的加解密算法 algorithm,当然,还有最重要的密钥分片列表 keyChipList,里面按序记录着这个密钥的分片信息。
- KeyChip 记录每个密钥分片,包括分片编号 no,以及分片密钥内容 chip。
4.2 核心服务类设计
应用程序通过调用加解密 API Service 完成数据加解密。
- SDK 的核心类是 Service,应用程序调用该对象的 encrypt 方法进行加密,decrypt 方法进行解密。应用程序需要构造 Data 对象,将加解密数据传给 Service,Service 加解密完成后创建一个新的 Data 对象,将加解密的结果写入该对象并返回。Data 成员变量在后面详细讲解。
- Service 通过 Connector 类连接服务器获取密钥 KeyBox 和算法 Algorithm,并调用 Algorithm 的对应方法完成加解密。
加密的流程:
首先,应用程序 App 创建 Data 对象,并将待加密数据写入该对象。接着,App 调用 Service 的 encrypt 方法进行加密,Service 检查加密需要的密钥和算法是否已经有缓存,如果没有,就调用 Connector 请求服务器,返回密钥和算法。Connector 将根据返回的算法字节码来构造加密算法的实例对象,同时根据返回的密钥构造相关密钥对象,并写入 KeyBox,完成更新。
下一步,Service 会根据更新后的 KeyBox 中的密钥和算法进行加密,并将加密结果写入 VenusData。最后,应用程序 App 从返回的 Data 中获取加密后的数据即可
4.3 加解密数据接口 Data 设计
Data 用于表示加解密操作输入和输出的数据,也就是说,加解密的时候构造 Data 对象调用 Service 对应的方法,加解密完成后返回值还是一个 Data 对象。
Data 用作输入时:
- 属性 bytes 和 text 只要设置一个,即要么处理的是二进制 bytes 数据,要么是 String 数据,如果两个都设置了,会抛出异常。
- 属性 version 可以不设置(即 null),表示操作使用的密钥版本是当前版本。
- 属性 outputWithText 表示输出的 Data 是否处理为 text 类型,缺省值是 true。
- 属性 dataWithVersion 表示加密后的 Data 的 bytes 和 text 中是否包含使用密钥的版本信息,这样在解密的时候可以不指定版本,缺省值是 false。 如果 dataWithVersion 设置为 true,即表示加密后密文内包含版本号,这种情况下,Service 需要在密文头部增加 3 个字节的版本号信息,其中头两个字节为固定的 magic code:0x5E、0x23,第三个字节为版本号(也就是说,密钥版本号只占用一个字节,最多支持 256 个版本)。
Data 用作输出时,会设置属性 keyName(和输入时的值一样)、version、bytes、 outputWithText、dataWithVersion(和输入时的值一样),并根据输入的 outputWithText 决定是否设置 text 属性。