在基于B/S 的业务系统中,如果要设计开发加密解密机制。有几种设计选型:
本文说明的加密系统是基于第三个技术选型进行设计,即在使用HTTP 协议进行传输,并对传输数据加密。
在该架构中存在一个加密服务,对外提供RESTful的HTTP GET API: /crypto/key
, 该API的响应体中包含AES
对称加密算法的配置信息,同时包含RSA
公钥。AES算法的配置信息在加密服务中使用RSA的私钥进行了加密。加密服务的使用者需要从加密服务/crypto/key
获取算法配置信息。该加密系统支持的场景为:
其架构图如下所示:
其主要步骤有:
1)业务系统A 的前端通过HTTP 请求,从加密服务获取加密算法相关信息,并通过封装的驱动文件
解析加密算法信息。
2)业务系统A 的后端通过HTTP 请求,从加密服务获取加密算法相关信息,并通过封装的驱动文件
解析加密算法信息。
3)业务系统A 前端使用AES 加密业务数据,通过HTTP 请求发送到后端。后端接收到前端的加密数据后,使用AES 解密数据,并进行后续的处理。相反的方向是类似的,后端使用AES 加密业务数据,通过HTTP 传输到前端,前端使用AES 解密接收到的后端加密数据,并进行后续的处理。
4)业务系统B 的前端和后端与加密服务进行的交互以及业务系统B 的前后端间的加密传输,和步骤1,2,3 的说明一样,不再重复阐述。
5)业务系统A 和业务系统B 进行交互时,业务系统A 的后端使用AES 加密数据,通过HTTP 传输到业务系统B 后端,业务系统B 后端使用AES 解密数据,并进行后续处理。反方向的流程是类似的处理。
加密服务是个web服务,其设计不限于某种程序语言。既可以使用基于Spring boot的java程序设计语言开发也可以使用Node.js
平台或其他任何程序设计语言开发。本文使用Spring boot
进行设计开发。
加密服务Spring boot的入口程序如下:
@SpringBootApplication
public class CryptoApplication {
public static void main(String[] args) {
SpringApplication.run(CryptoApplication .class, args);
}
@Bean
public CommandLineRunner init(final CryptoController cryptoController) {
CommandLineRunner commandLineRunner = (String ...strings) -> {
cryptoController.init();
};
return commandLineRunner;
}
}
加密服务controller:
@RestController
public class CryptoController {
@Value("${crypto.pubFile}")
private String publicKeyFile;
@Value("${crypto.prvFile}")
private String privateKeyFile;
@Value("${crypto.aes.mode}")
private String aesMode;
@Value("${crypto.aes.key}")
private String aesKey;
@Value("${crypto.aes.iv}")
private String aesIv;
private String publicKey;
private RSAPrivateKey privateKey;
private byte[] aesKeyBytes;
private byte[] aesIvBytes;
@GetMapping(value = "/crypto/key", produces = "application/json")
public ResponseEntity<CryptoKey> getCryptoKey(@RequestHeader String referer) throws Exception {
Assert.isTrue (StringUtils.hasText(aesMode));
Assert.isTrue (StringUtils.hasText(aesKey));
Assert.isTrue (StringUtils.hasText(aesIv));
Assert.isTrue (StringUtils.hasText(publicKey));
Assert.isTrue (privateKey != null);
boolean refererOk = checkReferer(referer)
if (!refererOk) {
return ResponseEntity.badRequest().build();
}
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
cipher.update("aes".getBytes(StandardCharsets.UTF_8));
byte[] algBytes = cipher.doFinal();
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
cipher.update(aesMode.getBytes(StandardCharsets.UTF_8));
byte[] modeBytes = cipher.doFinal();
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
cipher.update(aesKeyBytes);
byte[] keyBytes = cipher.doFinal();
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
cipher.update(aesIvBytes);
byte[] ivBytes = cipher.doFinal();
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES);
byteBuffer.putLong(System.currentTimeMillis());
cipher.update(byteBuffer.array());
byte[] versionBytes = cipher.doFinal();
CryptoKey cryptoKey = new CryptoKey();
cryptoKey.setK(publicKey);
cryptoKey.setA(Codec.bytesToBase64String(algBytes));
cryptoKey.setM(Codec.bytesToBase64String(modeBytes));
cryptoKey.setP(Codec.bytesToBase64String(keyBytes));
cryptoKey.setV(Codec.bytesToBase64String(ivBytes));
cryptoKey.setT(Codec.bytesToBase64String(versionBytes));
return ResponseEntity.ok(cryptoKey);
}
public void init() {
Assert.isTrue (StringUtils.hasText(publicKeyFile));
Assert.isTrue (StringUtils.hasText(privateKeyFile));
// Load public key.
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(2048);
try (InputStream inputStream = new ClassPathResource(publicKeyFile).getInputStream()) {
FileCopyUtils.copy(inputStream, byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
publicKey = new String(bytes, StandardCharsets.UTF_8);
} catch (Exception e) {
logger.error(e.getMessage());
return;
}
// Load private key.
try (InputStream inputStream = new ClassPathResource(privateKeyFile).getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
PemReader pemReader = new PemReader(inputStreamReader)) {
PemObject pemObject = pemReader.readPemObject();
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(pemObject.getContent());
KeyFactory factory = KeyFactory.getInstance("RSA");
privateKey = (RSAPrivateKey) factory.generatePrivate(pkcs8EncodedKeySpec);
} catch (Exception e) {
logger.error(e.getMessage());
return;
}
aesKeyBytes = Codec.base64StringToBytes(aesKey);
aesIvBytes = Codec.base64StringToBytes(aesIv);
}
private boolean checkReferer(String referer) {
URI uri = URI.create(referer);
String host = uri.getHost();
if ("localhost".equals(host)) {
return false;
}
if ("127.0.0.1".equals(host)) {
return false;
}
return true;
}
}
需要注意的是:
referer
,从而保证请求是来自部署于web服务器中的客户端程序。CryptoKey 是pojo
类,加密服务API的响应体中是该类的JSON格式数据。该类的定义为:
public class CryptoKey {
private String k;
private String a;
private String m;
private String p;
private String v;
private String t;
public String getK() {
return k;
}
public void setK(String k) {
this.k = k;
}
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
public String getM() {
return m;
}
public void setM(String m) {
this.m = m;
}
public String getP() {
return p;
}
public void setP(String p) {
this.p = p;
}
public String getV() {
return v;
}
public void setV(String v) {
this.v = v;
}
public String getT() {
return t;
}
public void setT(String t) {
this.t = t;
}
}
需要说明的是:
CryptoKey
类中存在一个版本字段,加密服务每次更新其算法相关信息,该版本字段也同步更新。该机制可以进一步提高加密服务的数据安全。加密服务的使用者必须确认双方加密算法的版本信息一致,否则无法进行正确的加解密。PEM
格式的RSA公钥和私钥文件。客户端是运行在浏览器环境中的JavaScript程序。客户端驱动文件封装了加解密函数,便于客户端直接使用。该驱动文件的实现为:
export const getAesConfig = function(json) {
let pubKeyPem = json["k"];
let secretKey = json["p"];
let algorithm = json["a"];
let aesMode = json["m"];
let aesIv = json["v"];
let version = json["t"];
let secretKeyPlain = crypto.publicDecrypt(pubKeyPem, Buffer.from(base64.toByteArray(secretKey)));
let aesIvPlain = crypto.publicDecrypt(pubKeyPem, Buffer.from(base64.toByteArray(aesIv)));
let algorithmPlain = crypto.publicDecrypt(pubKeyPem, Buffer.from(base64.toByteArray(algorithm)));
let aesModePlain = crypto.publicDecrypt(pubKeyPem, Buffer.from(base64.toByteArray(aesMode)));
let versionPlain = crypto.publicDecrypt(pubKeyPem, Buffer.from(base64.toByteArray(version)));
return {
algorithmPlain, aesModePlain, secretKeyPlain, aesIvPlain, versionPlain};
}
// Plain text is String type.
export const encrypt = function(plainText, aesModePlain, secretKeyPlain, aesIvPlain) {
let plainTextBytes = aesJs.utils.utf8.toBytes(plainText);
// Hard code to use AES-OFB mode.
let aesOfb = new aesJs.ModeOfOperation.ofb(secretKeyPlain, aesIvPlain);
let cipherTextBytes = aesOfb.encrypt(plainTextBytes);
return base64.fromByteArray(cipherTextBytes);
}
// Cipher text is encrypted data with base64 encoding.
export const decrypt = function(cipherText, aesModePlain, secretKeyPlain, aesIvPlain) {
let cipherTextBytes = base64.toByteArray(cipherText);
// Hard code to use AES-OFB mode.
let aesOfb = new aesJs.ModeOfOperation.ofb(secretKeyPlain, aesIvPlain);
let plainTextBytes = aesOfb.decrypt(cipherTextBytes);
return aesJs.utils.utf8.fromBytes(plainTextBytes);
}
// Password are string type.
export const encryptPassword = function(password, aesModePlain, secretKeyPlain, aesIvPlain) {
let plainData = password + aesModePlain + base64.fromByteArray(secretKeyPlain) + base64.fromByteArray(aesIvPlain);
let cipherData = encrypt(plainData, aesModePlain, secretKeyPlain, aesIvPlain);
password = new MD5().update(cipherData).digest('hex');
let salt = encrypt(Date.now(), aesModePlain, secretKeyPlain, aesIvPlain);
return {
password, salt};
}
需要说明的是:
encryptPassword
,因为密码通常需要某种哈希进行处理,如MD5
, SHA1
等,从而保证其不可逆。下面是一个简单的html
页面,用于说明如何在浏览器客户端环境使用驱动文件封装的相关加解密函数。
DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Crypto driver test.title>
head>
<body>
<script src="crypto-driver-bundle.js">script>
<script>
async function test(){
let serverUrl = "http://ip:port/crypto/key";
let response = await fetch(serverUrl);
if (!response.ok) {
console.error("Fetch failed.");
return;
}
let body = await response.json();
let {
algorithmPlain, aesModePlain, secretKeyPlain, aesIvPlain, versionPlain} = cryptoDriver.getAesConfig(body);
let {
password, salt} = cryptoDriver.encryptPassword("123456", aesModePlain, secretKeyPlain, aesIvPlain);
let plainText = "Hello world.";
let cipherText = cryptoDriver.encrypt(plainText, aesModePlain, secretKeyPlain, aesIvPlain);
let original = cryptoDriver.decrypt(cipherText, aesModePlain, secretKeyPlain, aesIvPlain);
if (plainText === original) {
console.log("Test OK.");
} else {
console.log("Test failed.");
}
}
test();
script>
<p> Crypto driver p>
body>
html>
需要说明的是:
crypto-driver-bundle.js
是使用webpack
打包客户端驱动文件源代码后的生成文件,对外导出了cryptoDriver
对象,其有四个属性,对应加解密相关的四个函数:getAesConfig
,encryptPassword
,encrypt
和decrypt
。因为JavaScript是浏览器客户端的事实标准,因此需要为客户端提供基于JavaScript封装的驱动文件,但是对于服务端,其程序实现可以是Java,Node.js,Python等其他程序设计语言,为便于服务端使用加密服务,也需要为服务端提供封装的驱动文件,其实现逻辑和客户端驱动文件一致,封装getAesConfig
,encryptPassword
,encrypt
和decrypt
四个加解密函数。