开放平台-请求加解密
<h1>4.签名加解密</h1>
<p>在Java中,签名和加解密是常见的安全操作,用于确保数据的完整性和保密性以及解决接口安全上的欺 骗和否认问题。本文档将介绍如何在Java中执行签名和加解密操作。</p>
<h2>4.1 加密器</h2>
<p>我们使用 ECC(椭圆曲线非对称加密) 作为签名以及加解密方式,相比传统RSA加密有更短的密文和签 名,与更高的安全级别以及性能。</p>
<ul>
<li>依赖库:</li>
</ul>
<pre><code> &lt;!-- ECC 加密器 --&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.bouncycastle&lt;/groupId&gt;
&lt;artifactId&gt;bcprov-jdk15on&lt;/artifactId&gt;
&lt;version&gt;1.70&lt;/version&gt; &lt;!-- 选择适合的版本 --&gt;
&lt;/dependency&gt;
&lt;!-- fast json --&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.alibaba&lt;/groupId&gt;
&lt;artifactId&gt;fastjson&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;!-- hutool 工具包 --&gt;
&lt;dependency&gt;
&lt;groupId&gt;cn.hutool&lt;/groupId&gt;
&lt;artifactId&gt;hutool-all&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<h2>4.2 密钥交换</h2>
<table>
<thead>
<tr>
<th style="text-align: left;">秘钥</th>
<th style="text-align: left;">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">sign_secret_key</td>
<td style="text-align: left;"> 客户端 (接口对接方) 私钥</td>
</tr>
<tr>
<td style="text-align: left;">sign_public_key</td>
<td style="text-align: left;">客户端 (接口对接方) 公钥</td>
</tr>
<tr>
<td style="text-align: left;">ivs_public_key</td>
<td style="text-align: left;">服务端 (运之盟) 公钥</td>
</tr>
<tr>
<td style="text-align: left;">ivs_private_key</td>
<td style="text-align: left;">服务端 (运之盟) 私钥</td>
</tr>
</tbody>
</table>
<p><code>密钥交换是一种协议或机制,用于安全地共享密钥,以便在加密通信中使用。</code></p>
<p><code>客户端和服务端各持有一对ECC密钥对, 并且互相向对方发送公钥。</code> </p>
<p><code>客户端私钥(sign_secret_key:)用于生成签名, 服务端使用对应公钥(sign_public_key:)进行验证。</code></p>
<p><code>服务端公钥(ivs_public_key)用于客户端对敏感数据加密(如身份证, 密码等), 服务端使用私钥(ivs_private_key)进行解密。</code></p>
<h3>如何使用ECC生成密钥</h3>
<pre><code> /**
* 生成ECC密钥对。
*/
@SneakyThrows
public static Pair&lt;String, String&gt; generateECCKeyPair() {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(&quot;EC&quot;);
ECGenParameterSpec ecSpec = new ECGenParameterSpec(&quot;secp256r1&quot;);
keyPairGenerator.initialize(ecSpec, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// key :公钥 value : 私钥 // 为了方便持久化密钥, 使用Base64 对密钥进行转换String。
// 当使用真实的密钥时, 需要从 Base64 String 转换回密钥
// 转 base64
return Pair.of(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()), Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
}
/**
* 字符串到私钥
* @param keyStr 关键str
* @return {@link PrivateKey}
* @throws Exception 异常
*/
public static PrivateKey privateKeyFromString(String keyStr) {
byte[] decodedKey = Base64.getDecoder().decode(keyStr);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedKey);
KeyFactory keyFactory = KeyFactory.getInstance(&quot;EC&quot;);
return keyFactory.generatePrivate(spec);
}
/**
* 来自字符串公钥
*
* @param keyStr 关键str
* @return {@link PublicKey}
*/
public static PublicKey publicKeyFromString(String keyStr) {
byte[] decodedKey = Base64.getDecoder().decode(keyStr);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decodedKey);
KeyFactory keyFactory = KeyFactory.getInstance(&quot;EC&quot;);
return keyFactory.generatePublic(spec);
}
</code></pre>
<h2>4.3 签名</h2>
<p>数字签名是一种用于验证数据完整性和来源的技术,通常用于解决接口安全上的。我们可以使用以下步 骤进行ECC签名</p>
<h3>签名请求规则</h3>
<ul>
<li>请求方法: POST</li>
<li>请求头:</li>
</ul>
<p><code>Content-Type : application/json</code></p>
<p><code>X-Nonce : 18位long类型的随机数, 用于生成签名和防止重放攻击。</code></p>
<p><code>X-Timestamp : 当前时间戳(13位,精确到毫秒), 用于生成签名和防止重放攻击。</code></p>
<p><code>X-Signature : 签名</code></p>
<h3>签名生成规则</h3>
<ul>
<li>签名所需要的参数如下:</li>
</ul>
<p><code>data 请求体body, json 类型, 需要将 json 内参数按照字典序( a-z )的方式进行排序后使用其 byte[] 进行生成签名。</code></p>
<p><code>nonce : 18位long类型的随机数, 和 请求头 X-Nonce 保持一致。</code></p>
<p><code>timestamp : 当前时间戳(13位,精确到毫秒), 和 请求头 X-Timestamp 保持一致。</code></p>
<h3>生成签名示例代码如下:</h3>
<pre><code> /**
* 签名
* @param skStr 私钥字符串(sign_secret_key)
* @param data 数据
* @param nonce 现时标志
* @param timestamp 时间戳
* @return {@link String} base64 的签名
* @throws NoSuchAlgorithmException 没有这样算法例外
* @throws InvalidKeyException 无效密钥异常
* @throws SignatureException
*/
@SneakyThrows
public static String sign(String skStr, byte[] data, long nonce, long timestamp) {
// 私钥 base64 String 转真实 私钥
PrivateKey privateKey = privateKeyFromString(skStr);
Signature signature = Signature.getInstance(&quot;SHA256withECDSA&quot;);
signature.initSign(privateKey);
// 将原始数据、时间戳和nonce结合在一起进行签名
signature.update(data);
signature.update(ByteUtil.longToBytes(timestamp));
signature.update(ByteUtil.longToBytes(nonce));
// 使用Base64 将真实签名转换位字符串
return Base64.getEncoder().encodeToString(signature.sign());
}
</code></pre>
<h3>Nonce 生成示例代码如下:</h3>
<pre><code> public static long generateNonce() {
long minimum = 100000000000000000L; // 10^17
long maximum = 999999999999999999L; // Just below 10^18
return minimum + ((long) (new SecureRandom().nextDouble() * (maximum - minimum)));
}</code></pre>
<h3>json 排序示例代码如下(基于 fastjson):</h3>
<pre><code> /**
* 按键排序json
* 不要碰这个代码!谁碰我杀谁
* @param jsonStr json str
* @return {@link String}
*/
public static String sortJsonByKey(String jsonStr) {
Object json = JSON.parse(jsonStr);
Object sortedJson = sortObjectByKey(json);
return JSON.toJSONString(sortedJson);
}
/**
* 递归 按键排序json对象
* 不要碰这个代码!谁碰我杀谁
* @param json json
* @return {@link Object}
*/
private static Object sortObjectByKey(Object json) {
if (json instanceof JSONObject) {
JSONObject jsonObject = (JSONObject) json;
Map&lt;String, Object&gt; sortedMap = new LinkedHashMap&lt;&gt;();
jsonObject.keySet().stream()
.sorted() // 默认的字符串排序,即字典序 (a-z)
.forEach(key -&gt; sortedMap.put(key, sortObjectByKey(jsonObject.get(key))));
return sortedMap;
} else if (json instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) json;
for (int i = 0; i &lt; jsonArray.size(); i++) {
jsonArray.set(i, sortObjectByKey(jsonArray.get(i)));
}
return jsonArray;
}
return json;
}</code></pre>
<h3>签名生成示例:</h3>
<pre><code> public static void main(String[] args) {
// 植入三方ecc 加密库
Security.addProvider(new BouncyCastleProvider());
// 获取验签秘钥
Pair&lt;String, String&gt; keyPairSign = generateECCKeyPair();
System.out.println(&quot;keyPairSign: &quot; + keyPairSign.getKey() + &quot;\n keyPairSignValue :&quot; + keyPairSign.getValue());
long nonce = generateNonce();
long timestamp = System.currentTimeMillis();
System.out.println(&quot;nonce: &quot; + nonce + &quot;\n timestamp : &quot; + timestamp);
String json = &quot;{\n&quot; +
&quot; \&quot;idCardNum\&quot;: \&quot;123123\&quot;,\n&quot; +
&quot; \&quot;password\&quot;: \&quot;123123\&quot;,\n&quot; +
&quot; \&quot;username\&quot;: \&quot;123123\&quot;\n&quot; +
&quot;}&quot;;
String sortJson = sortJsonByKey(json);
System.out.println(sortJson);
String sign = sign(keyPairSign.getValue(),
sortJson.getBytes(), nonce, timestamp);
System.out.println(&quot;sign: &quot; + sign);
}</code></pre>
<h1>5. 敏感数据加解密(包括你方请求参数和我方回调参数)</h1>
<ul>
<li>
<p>加解密用于保护敏感数据的机密性, 也可以一定程度上解决接口安全的窃听和伪造问题。</p>
</li>
<li>
<p>我们可以使用 以下步骤进行ECC加解密:</p>
</li>
<li>客户端使用服务端公钥对请求中的<strong>敏感字段进行加密</strong>,得到新的请求包体,注意是<strong>对敏感字段加密</strong>,而 非整个原始请求。 </li>
</ul>
<p>目前的敏感字段如下:(可能会新增)</p>
<pre><code> idCardNum
phone
password
idCardNo
idCard
qualificationCardNo
bankIdCardNo
legalPersonIdCard
bankPhone
adminPhone
financeContactTel
businessContactTel
userPhone
payeePhone
driverPhone
oldPayPwd
newPayPwd
confirmPayPwd</code></pre>
<h2>5.1 加密方法示例如下:</h2>
<pre><code> @SneakyThrows
public static String encrypt(String pkStr, byte[] plaintext) {
PublicKey publicKey = publicKeyFromString(pkStr);
Cipher cipher = Cipher.getInstance(&quot;ECIES&quot;);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext));
}</code></pre>
<h2>5.2 加密代码示例如下:</h2>
<pre><code> public static void main(String[] args) {
// 植入三方ecc 加密库
Security.addProvider(new BouncyCastleProvider());
// 获取ecc加解密秘钥
Pair&lt;String, String&gt; keyPairCrypto = generateECCKeyPair();
System.out.println(&quot;keyPairCryptoKey : &quot; + keyPairCrypto.getKey() + &quot;\nkeyPairCryptoValue :&quot; + keyPairCrypto.getValue());
String json = &quot;{\n&quot; +
&quot; \&quot;idCardNum\&quot;:\&quot;&quot; + encrypt(keyPairCrypto.getKey(),
&quot;123456&quot;.getBytes()) + &quot;\&quot;,\n&quot; +
&quot; \&quot;password\&quot;:\&quot;&quot; + encrypt(keyPairCrypto.getKey(),
&quot;123456&quot;.getBytes()) + &quot;\&quot;,\n&quot; +
&quot; \&quot;username\&quot;:\&quot;123456\&quot;\n&quot; +
&quot;}&quot;;
System.out.println(json);
}</code></pre>