在基于B/S 的業務系統中,如果要設計開發加密解密機制,有幾種設計選型:
- 可以使用現成的HTTPS 架構,后端部署用知名簽名機構生成的證書,
- 可以使用現成的HTTPS 架構,后端部署自簽名的證書,但是用戶需要在瀏覽器中匯入自簽名證書,
- 基于HTTP 傳輸加密的資料,也就是說,雖然使用非加密的傳輸協議,但是資料本身是加密的,
本文說明的加密系統是基于第三個技術選型進行設計,即在使用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;
}
}
需要注意的是:
- 該API要求校驗請求頭
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;
}
}
需要說明的是:
- 加密服務可以隨時更新演算法相關的資訊,如RSA的公鑰和私鑰,AES的密鑰和初始向量,在
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四個加解密函式,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/357074.html
標籤:其他
上一篇:網路編程十宗罪
