基于HTTP在互联网传输敏感数据的消息摘要、签名与加密方案

2022-06-30 15:21:12 浏览数 (1)

一、关键词

HTTP,HTTPS,AES,SHA-1,MD5,消息摘要,数字签名,数字加密,Java,Servlet,Bouncy Castle

二、名词解释

数字摘要:是将任意长度的消息变成固定长度的短消息,它类似于一个自变量是消息的函数,也就是Hash函数。数字摘要就是采用单项Hash函数将需要加密的明文“摘要”成一串固定长度(128位)的密文这一串密文又称为数字指纹,它有固定的长度,而且不同的明文摘要成密文,其结果总是不同的,而同样的明文其摘要必定一致。

AES:密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。是一种对称加密算法。

SHA-1:安全哈希算法(Secure Hash Algorithm)主要适用于数字签名标准 (Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。 SHA1有如下特性:不可以从消息摘要中复原信息;两个不同的消息不会产生同样的消息摘要。

MD5:Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。

三、项目背景

某合作公司需要通过互联网向我司传递一些用户数据,但是我所在项目组的外网服务器上并无部署https,只能基于http进行数据传输。为了保护双方共同的用户数据,必须对在互联网上传输的信息进行加密处理。

四、方案设计

这里涉及到两个问题,一是采用什么样的远程消息传递框架,二是如何对传输的数据进行加密。

本人平时开发所用的语言主要是Java,对于Jsp/Servlet还比较熟悉,结合去年参加过所在公司的微信公众号开发的经验,设计出了如下方案:

1.在客户端采用构造http post请求,把用户数据加密后放入request body中,并在http参数中放入调用方的签名;

2.服务端接收到请求,提取参数进行签名校验,通过后从request body中提取密文进行解密,然后进行后续处理,最终生成响应返回给客户端。

以下是具体处理的流程图:

在数据加密阶段,基于性能以及效率考虑,采用了Bouncy Castle提供的AES算法,而生成签名则采用了jdk提供的SHA-1,值得注意的是,基于安全考虑,消息密文的消息摘要也被列入到参与数字签名的参数之一。

五、代码实现

1.AES加密工具类:

import org.apache.commons.lang.StringUtils;

import org.apache.log4j.Logger;

import org.bouncycastle.crypto.CipherParameters;

import org.bouncycastle.crypto.engines.AESFastEngine;

import org.bouncycastle.crypto.modes.CBCBlockCipher;

import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;

import org.bouncycastle.crypto.params.KeyParameter;

import org.bouncycastle.crypto.params.ParametersWithIV;

import org.bouncycastle.util.encoders.Hex;

/**

* AES encryption and decryption tool.

*

* @author ben

* @creation 2014年3月20日

*/

public class AESTool {

protected static final Logger log = Logger.getLogger(AESTool.class);

private byte[] initVector = { 0x32, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31,

0x38, 0x27, 0x36, 0x35, 0x33, 0x23, 0x32, 0x31 };

/**

* FIXME For demo only, should rewrite this method in your product environment!

*

* @param appid

* @return

*/

public String findKeyById(String appid) {

// Fake key.

String key = "123456789012345678901234567890~!";

return key;

}

/**

* Encrypt the content with a given key using aes algorithm.

*

* @param content

* @param key

* must contain exactly 32 characters

* @return

* @throws Exception

*/

public String encrypt(String content, String key) throws Exception {

if (key == null) {

throw new IllegalArgumentException("Key cannot be null!");

}

String encrypted = null;

byte[] keyBytes = key.getBytes();

if (keyBytes.length != 32 && keyBytes.length != 24

&& keyBytes.length != 16) {

throw new IllegalArgumentException(

"Key length must be 128/192/256 bits!");

}

byte[] encryptedBytes = null;

encryptedBytes = encrypt(content.getBytes(), keyBytes, initVector);

encrypted = new String(Hex.encode(encryptedBytes));

return encrypted;

}

/**

* Decrypt the content with a given key using aes algorithm.

*

* @param content

* @param key

* must contain exactly 32 characters

* @return

* @throws Exception

*/

public String decrypt(String content, String key) throws Exception {

if (key == null) {

throw new IllegalArgumentException("Key cannot be null!");

}

String decrypted = null;

byte[] encryptedContent = Hex.decode(content);

byte[] keyBytes = key.getBytes();

byte[] decryptedBytes = null;

if (keyBytes.length != 32 && keyBytes.length != 24

&& keyBytes.length != 16) {

throw new IllegalArgumentException(

"Key length must be 128/192/256 bits!");

}

decryptedBytes = decrypt(encryptedContent, keyBytes, initVector);

decrypted = new String(decryptedBytes);

return decrypted;

}

/**

* Encrypt data.

*

* @param plain

* @param key

* @param iv

* @return

* @throws Exception

*/

public byte[] encrypt(byte[] plain, byte[] key, byte[] iv) throws Exception {

PaddedBufferedBlockCipher aes = new PaddedBufferedBlockCipher(

new CBCBlockCipher(new AESFastEngine()));

CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(key),

iv);

aes.init(true, ivAndKey);

return cipherData(aes, plain);

}

/**

* Decrypt data.

*

* @param cipher

* @param key

* @param iv

* @return

* @throws Exception

*/

public byte[] decrypt(byte[] cipher, byte[] key, byte[] iv)

throws Exception {

PaddedBufferedBlockCipher aes = new PaddedBufferedBlockCipher(

new CBCBlockCipher(new AESFastEngine()));

CipherParameters ivAndKey = new ParametersWithIV(new KeyParameter(key),

iv);

aes.init(false, ivAndKey);

return cipherData(aes, cipher);

}

/**

* Encrypt or decrypt data.

*

* @param cipher

* @param data

* @return

* @throws Exception

*/

private byte[] cipherData(PaddedBufferedBlockCipher cipher, byte[] data)

throws Exception {

int minSize = cipher.getOutputSize(data.length);

byte[] outBuf = new byte[minSize];

int length1 = cipher.processBytes(data, 0, data.length, outBuf, 0);

int length2 = cipher.doFinal(outBuf, length1);

int actualLength = length1 length2;

byte[] result = new byte[actualLength];

System.arraycopy(outBuf, 0, result, 0, result.length);

return result;

}

public static void main(String[] args) throws Exception {

AESTool aesTool = new AESTool();

String appid = "canairport001";

String key = aesTool.findKeyById(appid);

String xml = "<root><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name></root>";

String encrypted = aesTool.encrypt(xml, key);

System.out.println("encrypted: n" encrypted);

System.out.println("encrypted length: n" encrypted.length());

String decrypted = aesTool.decrypt(encrypted, key);

System.out.println("decrypted: n" decrypted);

System.out.println("decrypted length: n" decrypted.length());

boolean isSuccessful = StringUtils.equals(decrypted, xml);

System.out.println(isSuccessful);

}

}

2.数字签名工具类:

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.ArrayList;

import java.util.Collections;

import java.util.List;

import org.apache.commons.lang.StringUtils;

import org.apache.log4j.Logger;

/**

* @author lixuanbin

* @creation 2013-1-30

*/

public class SignatureUtil {

protected static Logger log = Logger.getLogger(SignatureUtil.class);

private static final char[] hexArray = "0123456789ABCDEF".toCharArray();

private String encryptionAlgorithm = "SHA-1";

public String bytesToHexString(byte[] bytes) {

char[] hexChars = new char[bytes.length * 2];

for (int j = 0; j < bytes.length; j ) {

int v = bytes[j] & 0xFF;

hexChars[j * 2] = hexArray[v >>> 4];

hexChars[j * 2 1] = hexArray[v & 0x0F];

}

return new String(hexChars);

}

public byte[] hexStringToBytes(String s) {

int len = s.length();

byte[] data = new byte[len / 2];

for (int i = 0; i < len; i = 2) {

data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) Character

.digit(s.charAt(i 1), 16));

}

return data;

}

/**

* 使用指定算法生成消息摘要,默认是md5

*

* @param strSrc

* , a string will be encrypted; <br/>

* @param encName

* , the algorithm name will be used, dafault to "MD5"; <br/>

* @return

*/

public String digest(String strSrc, String encName) {

MessageDigest md = null;

String strDes = null;

byte[] bt = strSrc.getBytes();

try {

if (encName == null || encName.equals("")) {

encName = "MD5";

}

md = MessageDigest.getInstance(encName);

md.update(bt);

strDes = bytesToHexString(md.digest()); // to HexString

} catch (NoSuchAlgorithmException e) {

log.error("Invalid algorithm: " encName);

return null;

}

return strDes;

}

/**

* 根据appid、token、lol以及时间戳来生成签名

*

* @param appid

* @param token

* @param lol

* @param millis

* @return

*/

public String generateSignature(String appid, String token, String lol,

long millis) {

String timestamp = String.valueOf(millis);

String signature = null;

if (StringUtils.isNotBlank(token) && StringUtils.isNotBlank(timestamp)

&& StringUtils.isNotBlank(appid)) {

List<String> srcList = new ArrayList<String>();

srcList.add(timestamp);

srcList.add(appid);

srcList.add(token);

srcList.add(lol);

// 按照字典序逆序拼接参数

Collections.sort(srcList);

Collections.reverse(srcList);

StringBuilder sb = new StringBuilder();

for (int i = 0; i < srcList.size(); i ) {

sb.append(srcList.get(i));

}

signature = digest(sb.toString(), encryptionAlgorithm);

srcList.clear();

srcList = null;

}

return signature;

}

/**

* 验证签名: <br/>

* 1.根据appid获取该渠道的token;<br/>

* 2.根据appid、token、lol以及时间戳计算一次签名;<br/>

* 3.比较传过来的签名以及计算出的签名是否一致;

* @param signature

* @param appid

* @param lol

* @param millis

* @return

*/

public boolean isValid(String signature, String appid, String lol,

long millis) {

String token = findTokenById(appid);

String calculatedSignature = generateSignature(appid, token, lol,

millis);

log.info("calculated signature: n" calculatedSignature);

if (StringUtils.equals(calculatedSignature, signature)) {

return true;

} else {

return false;

}

}

/**

* FIXME For demo only, should be a different string in production.

* @param appid

* @return

*/

public String findTokenById(String appid) {

String token = "#@!1234567890!@#";

return token;

}

public static void main(String[] args) {

SignatureUtil generator = new SignatureUtil();

String xmlString = "<root><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name><name>test</name></root>";

System.out.println(xmlString.getBytes().length);

String digest = generator.digest(xmlString, "MD5");

System.out.println(digest);

System.out.println(digest.getBytes().length);

String appid = "canairport001";

String token = generator.findTokenById(appid);

long millis = System.currentTimeMillis();

String signature = generator.generateSignature(appid, token, digest,

millis);

System.out.println(signature);

boolean isValid = generator.isValid(signature, appid, digest, millis);

System.out.println(isValid);

}

}

3.发送方代码:

import java.io.IOException;

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import java.util.Map.Entry;

import org.apache.commons.lang.StringUtils;

import org.apache.http.HttpEntity;

import org.apache.http.HttpHost;

import org.apache.http.HttpResponse;

import org.apache.http.HttpStatus;

import org.apache.http.auth.AuthScope;

import org.apache.http.auth.UsernamePasswordCredentials;

import org.apache.http.client.ClientProtocolException;

import org.apache.http.client.methods.HttpPost;

import org.apache.http.conn.params.ConnRoutePNames;

import org.apache.http.entity.StringEntity;

import org.apache.http.impl.client.DefaultHttpClient;

import org.apache.http.message.BasicHeader;

import org.apache.http.protocol.HTTP;

import org.apache.http.util.EntityUtils;

import org.apache.log4j.Logger;

/**

* @author ben

* @creation 2014年6月9日

*/

public class HttpclientUtil {

protected static final Logger log = Logger.getLogger(HttpclientUtil.class);

/**

* 根据传入的uri和参数map拼接成实际uri

*

* @param uri

* @param paraMap

* @return

*/

public String buildUri(String uri, Map<String, String> paraMap) {

StringBuilder sb = new StringBuilder();

uri = StringUtils.trim(uri);

uri = StringUtils.removeEnd(uri, "/");

uri = StringUtils.removeEnd(uri, "?");

sb.append(uri);

if (paraMap != null && !paraMap.isEmpty()) {

sb.append("?");

Iterator<Entry<String, String>> iterator = paraMap.entrySet()

.iterator();

while (iterator.hasNext()) {

Map.Entry<String, String> pair = iterator.next();

try {

String keyString = pair.getKey();

String valueString = pair.getValue();

sb.append(keyString);

sb.append("=");

sb.append(valueString);

sb.append("&");

} catch (Exception e) {

log.error(e, e);

}

}

}

return StringUtils.removeEnd(sb.toString(), "&");

}

/**

* Post an xml string to a specific host.

*

* @param targetHost

* @param targetPort

* @param protocol

* @param proxyHost

* @param proxyPort

* @param proxyUser

* @param proxyPassword

* @param uri

* @param paraMap

* @param xml

* @param charset

* @return

* @throws ClientProtocolException

* @throws IOException

*/

public String postXmlString(String targetHost, int targetPort,

String protocol, String proxyHost, int proxyPort, String proxyUser,

String proxyPassword, String uri, Map<String, String> paraMap,

String xml, String charset) throws ClientProtocolException,

IOException {

String result = null;

DefaultHttpClient httpclient = new DefaultHttpClient();

if (StringUtils.isNotBlank(proxyHost) && proxyPort > 0) {

// 设置上网代理

AuthScope authScope = new AuthScope(proxyHost, proxyPort);

if (StringUtils.isNotBlank(proxyUser)

&& StringUtils.isNotBlank(proxyPassword)) {

// 设置上网代理的用户名和密码

UsernamePasswordCredentials upc = new UsernamePasswordCredentials(

proxyUser, proxyPassword);

httpclient.getCredentialsProvider().setCredentials(authScope,

upc);

}

HttpHost proxy = new HttpHost(proxyHost, proxyPort);

httpclient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY,

proxy);

}

HttpHost host = new HttpHost(targetHost, targetPort, protocol);

uri = buildUri(uri, paraMap);

log.info("post uri: " uri);

log.info("post content: " xml);

HttpPost post = new HttpPost(uri);

StringEntity se = new StringEntity(xml,

StringUtils.isNotBlank(charset) ? charset : "utf-8");

se.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,

"application/xml"));

post.setEntity(se);

HttpResponse response = httpclient.execute(host, post);

if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {

HttpEntity entity = response.getEntity();

if (entity != null) {

result = EntityUtils.toString(entity);

log.info("post result: " result);

}

} else {

log.error("post failed, status code: "

response.getStatusLine().getStatusCode());

}

return result;

}

public static void main(String[] args) throws Exception {

AESTool aes = new AESTool();

SignatureUtil signatureUtil = new SignatureUtil();

String appid = "canairport001";

String token = signatureUtil.findTokenById(appid);

String key = aes.findKeyById(appid);

long millis = System.currentTimeMillis();

String xml = "<dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.5</version></dependency>";

xml = aes.encrypt(xml, key);

String lol = signatureUtil.digest(xml, "MD5");

String signature = signatureUtil.generateSignature(appid, token, lol,

millis);

log.info("lol: n" lol);

log.info("signature: n" signature);

String uri = "http://127.0.0.1:8080/demo/psginfo.do";

Map<String, String> paraMap = new HashMap<String, String>();

paraMap.put("s", signature);

paraMap.put("a", appid);

paraMap.put("t", String.valueOf(millis));

paraMap.put("l", lol);

paraMap.put("o", "test");

HttpclientUtil util = new HttpclientUtil();

try {

String result = util.postXmlString("127.0.0.1", 8080, "http", null,

0, null, null, uri, paraMap, xml, "utf-8");

result = aes.decrypt(result, key);

System.out.println(result);

} catch (ClientProtocolException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

}

4.服务端代码:

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.io.UnsupportedEncodingException;

import javax.servlet.ServletException;

import javax.servlet.annotation.WebServlet;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;

import org.apache.log4j.Logger;

import co.speedar.wechat.util.AESTool;

import co.speedar.wechat.util.SignatureUtil;

/**

* Servlet implementation class PsginfoServlet

*/

@WebServlet(urlPatterns = { "/psginfo.do" }, loadOnStartup = 1)

public class PsginfoServlet extends HttpServlet {

protected static final Logger log = Logger.getLogger(PsginfoServlet.class);

private static final long serialVersionUID = 6536688299231165548L;

private SignatureUtil signatureUtil = new SignatureUtil();

private AESTool aes = new AESTool();

/**

* @see HttpServlet#HttpServlet()

*/

public PsginfoServlet() {

super();

}

/**

* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse

* response)

*/

protected void doGet(HttpServletRequest request,

HttpServletResponse response) throws ServletException, IOException {

String echostr = request.getParameter("e");

log.info("echostr before echo: " echostr);

String signature = request.getParameter("s");

String appid = request.getParameter("a");

String timestamp = request.getParameter("t");

String lol = request.getParameter("l");

long millis = Long.valueOf(timestamp);

// Need to check signature in product mode.

if (signatureUtil.isValid(signature, appid, lol, millis)) {

PrintWriter writer = response.getWriter();

log.info("echostr after echo: " echostr);

writer.print(echostr);

writer.flush();

writer.close();

}

}

/**

* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse

* response)

*/

protected void doPost(HttpServletRequest request,

HttpServletResponse response) throws ServletException, IOException {

// Get request parameters.

String signature = request.getParameter("s");

String appid = request.getParameter("a");

String timestamp = request.getParameter("t");

String lol = request.getParameter("l");

String operation = request.getParameter("o");

long millis = Long.valueOf(timestamp);

// Get xml data.

String encoding = StringUtils

.isNotBlank(request.getCharacterEncoding()) ? request

.getCharacterEncoding() : "utf-8";

String requestXmlString = getXmlStringFromHttpRequest(request);

String digest = signatureUtil.digest(requestXmlString, "MD5");

// Check signature and digest.

if (StringUtils.equals(digest, lol)) {

if (signatureUtil.isValid(signature, appid, lol, millis)) {

try {

String key = aes.findKeyById(appid);

requestXmlString = aes.decrypt(requestXmlString, key);

log.info("received xml data:n" requestXmlString);

// 校验xml合法性并执行相应动作

String responseXmlString = doSomeThing(requestXmlString,

operation);

responseXmlString = aes.encrypt(responseXmlString, key);

log.info("responsed xml data:n" responseXmlString);

response.setCharacterEncoding(encoding);

PrintWriter writer = response.getWriter();

writer.print(responseXmlString);

writer.flush();

writer.close();

} catch (Exception e) {

log.error(e, e);

}

} else {

log.error("invalid signature");

}

} else {

log.error("invalid digest.");

}

}

/**

* TODO Write your own business here.

*

* @param xml

* @param operation

* @return

*/

private String doSomeThing(String xml, String operation) {

return "done";

}

/**

* Extract xml string form http request.

*

* @param request

* @return

* @throws IOException

*/

private String getXmlStringFromHttpRequest(HttpServletRequest request) {

String requestXmlString = "";

try {

InputStream inputStream = request.getInputStream();

String encoding = StringUtils.isNotBlank(request

.getCharacterEncoding()) ? request.getCharacterEncoding()

: "utf-8";

requestXmlString = getXmlStringFromInputStream(inputStream,

encoding);

encoding = null;

inputStream.close();

inputStream = null;

} catch (IOException e) {

log.error(e, e);

}

return requestXmlString;

}

/**

* Extract xml string from the inputStream.

*

* @param inputStream

* @param charsetName

* @return

*/

private String getXmlStringFromInputStream(InputStream inputStream,

String charsetName) {

String resultXmlString = "";

String tempString = null;

BufferedReader bufferedReader;

try {

bufferedReader = new BufferedReader(new InputStreamReader(

inputStream, charsetName));

tempString = bufferedReader.readLine();

while (tempString != null) {

resultXmlString = tempString;

tempString = bufferedReader.readLine();

}

tempString = null;

bufferedReader.close();

bufferedReader = null;

} catch (UnsupportedEncodingException e) {

log.error(e, e);

} catch (IOException e) {

log.error(e, e);

}

return StringUtils.trim(resultXmlString);

}

}

5.maven配置:

<dependency>

<groupId>org.bouncycastle</groupId>

<artifactId>bcprov-jdk16</artifactId>

<version>1.46</version>

</dependency>

<dependency>

<groupId>commons-lang</groupId>

<artifactId>commons-lang</artifactId>

<version>2.5</version>

</dependency>

<dependency>

<groupId>org.apache.httpcomponents</groupId>

<artifactId>httpclient</artifactId>

<version>4.2.5</version>

</dependency>

<dependency>

<groupId>org.apache.httpcomponents</groupId>

<artifactId>httpmime</artifactId>

<version>4.2.5</version>

</dependency>

六、结语

在本方案设计实现过程中,消息传递的框架采用的是Java开发者所熟悉的Servlet技术,摘要、签名、加密所采用的算法,以及所依赖的第三方jar也是比较有口碑又大众化的货,对于有类似需要的开发者来说,本方案具有一定的参考意义。远程传递消息框架以及生成签名的环节,主要是模仿了微信公众平台的消息交互方式以及生成签名的思路,而有所创新的一小点是,把消息密文的MD5值也参与到了签名运算中,增加了被仿冒的难度,同时也便于服务方校验消息在传递过程中是否有被第三方所篡改。

基于简化工程配置的考虑,本示例项目中没有使用spring,您可以在您的生产项目中把本示例中的代码改造成春哥的单例业务bean。密钥、token建议别直接写到春哥的context配置文件中,而是写在您的生产容器的环境变量中,防止被窃取。

另外,在本方案中生成签名的参数您可以酌情增减并调换顺序,替换签名所采用的算法,或者根据您的实际需要“个性化”一下您的加密算法,以期达到更好的安全效果。

Last but not the least,在密钥以及token交换的阶段,请采取您所认可的安全有效的方式进行,譬如面对面,微信,qq,微薄私信,电话,短信,邮件(可以参考本人之前写过的一篇文章:http://lixuanbin.iteye.com/blog/1544344)

0 人点赞