百度智能小程序 用户数据的签名验证和加解密
用户数据的签名验证和加解密
智能小程序可以通过各种前端接口获取百度提供的开放数据。考虑到开发者服务器也需要获取这些开放数据,百度会对这些数据做签名和加密处理。开发者后台拿到开放数据后可以对数据进行校验签名和解密,来保证数据不被篡改。
开发者后台校验与解密开放数据
接口如果涉及敏感数据,接口的明文内容将不包含这些敏感数据。开发者如需要获取敏感数据,需要对接口返回的加密数据(data)进行对称解密。
解密过程:开发者智能小程序(通过 swan.request )将加密数据发送至自身 Server 进行解密后返回智能小程序。
解密算法说明:
- 对称解密使用的算法为 AES-192-CBC,数据采用 PKCS#7 填充;
- 对称解密的目标密文为 Base64_Decode(data);
- 对称解密秘钥 AESKey = Base64_Decode(session_key),AESKey 是 24 字节;
- 对称解密算法初始向量 为 Base64_Decode(iv),其中 iv 由数据接口返回。
会话密钥 session_key 有效性说明
开发者基于 session_key 请关注下面几个与 session_key 有关的注意事项。
1、 session_key 是具有时效性的,过期的 session_key 将无法使用。开发者在 session_key 失效时,需要通过重新执行登录流程获取有效的 session_key 。
2、 使用 checkSession() 可以校验 Session Key 是否有效,从而避免小程序反复执行登录流程,参考授权流程图中 checkSession() 使用。
3、 智能小程序不会把 session_key 的有效期告知开发者。我们会根据用户使用小程序的行为对 session_key 进行续期。用户越频繁使用小程序, session_key 有效期越长。
注意
- session_key 过期会导致开放数据解密失败。要判断当前用户的授权会话是否仍处于有效期,可调用 swan.checkSession() 方法进行判断,详情参照授权登录流程说明。
解密后内容如下:
内容 | 长度 |
---|---|
随机填充内容 | 16 字节 |
用户数据长度 | 4 字节,大端序无符号 32 位整型 |
用户数据 | 由用户数据长度描述 |
app_key | 与 app_key 长度相同 |
解密示例代码:
PHP 版本:
<?php
/**
* @Author: smartprogram_rd@baidu.com
* Copyright 2018 The BAIDU. All rights reserved.
*
* 百度小程序用户信息加解密示例代码(面向过程版)
* 示例代码未做异常判断,请勿用于生产环境
*/
function test() {
$app_key = 'y2dTfnWfkx2OXttMEMWlGHoB1KzMogm7';
$session_key = '1df09d0a1677dd72b8325aec59576e0c';
$iv = "1df09d0a1677dd72b8325Q==";
$ciphertext = "OpCoJgs7RrVgaMNDixIvaCIyV2SFDBNLivgkVqtzq2GC10egsn+PKmQ/+5q+chT8xzldLUog2haTItyIkKyvzvmXonBQLIMeq54axAu9c3KG8IhpFD6+ymHocmx07ZKi7eED3t0KyIxJgRNSDkFk5RV1ZP2mSWa7ZgCXXcAbP0RsiUcvhcJfrSwlpsm0E1YJzKpYy429xrEEGvK+gfL+Cw==";
$plaintext = decrypt($ciphertext, $iv, $app_key, $session_key);
// 解密结果应该是 '{"openid":"open_id","nickname":"baidu_user","headimgurl":"url of image","sex":1}'
echo $plaintext, PHP_EOL;
}
test();
/**
* 数据解密:低版本使用mcrypt库(PHP < 5.3.0),高版本使用openssl库(PHP >= 5.3.0)。
*
* @param string $ciphertext 待解密数据,返回的内容中的data字段
* @param string $iv 加密向量,返回的内容中的iv字段
* @param string $app_key 创建小程序时生成的app_key
* @param string $session_key 登录的code换得的
* @return string | false
*/
function decrypt($ciphertext, $iv, $app_key, $session_key) {
$session_key = base64_decode($session_key);
$iv = base64_decode($iv);
$ciphertext = base64_decode($ciphertext);
$plaintext = false;
if (function_exists("openssl_decrypt")) {
$plaintext = openssl_decrypt($ciphertext, "AES-192-CBC", $session_key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
} else {
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, null, MCRYPT_MODE_CBC, null);
mcrypt_generic_init($td, $session_key, $iv);
$plaintext = mdecrypt_generic($td, $ciphertext);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
}
if ($plaintext == false) {
return false;
}
// trim pkcs#7 padding
$pad = ord(substr($plaintext, -1));
$pad = ($pad < 1 || $pad > 32) ? 0 : $pad;
$plaintext = substr($plaintext, 0, strlen($plaintext) - $pad);
// trim header
$plaintext = substr($plaintext, 16);
// get content length
$unpack = unpack("Nlen/", substr($plaintext, 0, 4));
// get content
$content = substr($plaintext, 4, $unpack['len']);
// get app_key
$app_key_decode = substr($plaintext, $unpack['len'] + 4);
return $app_key == $app_key_decode ? $content : false;
}
Java 版本:
特别说明:受美国软件出口限制,JDK 默认使用的 AES 算法最高只能支持 128 位。如需要更高位的支持需要从 oracle 官网下载 Java 密码技术扩展(JCE)更换 JAVA_HOME/jre/lib/security 目录下的: local_policy.jar 和 US_export_policy.jar。
下载地址:https://www.oracle.com/technetwork/java/javase/downloads/jce-all-download-5170447.html
/*
* Copyright (C) 2018 Baidu, Inc. All Rights Reserved.
*/
package com.baidu.utils.secruity;
import java.nio.charset.Charset;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
public class Demo {
private static Charset CHARSET = Charset.forName("utf-8");
/**
* 对密文进行解密
*
* @param text 需要解密的密文
*
* @return 解密得到的明文
*
* @throws Exception 异常错误信息
*/
public String decrypt(String text, String sessionKey)
throws Exception {
byte [] aesKey = Base64.decodeBase64(sessionKey + "=");
byte[] original;
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
byte[] encrypted = Base64.decodeBase64(text);
original = cipher.doFinal(encrypted);
} catch (Exception e) {
throw new Exception(e);
}
String xmlContent;
String fromClientId;
try {
// 去除补位字符
byte[] bytes = PKCS7Encoder.decode(original);
// 分离16位随机字符串,网络字节序和ClientId
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = recoverNetworkBytesOrder(networkOrder);
xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
fromClientId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
} catch (Exception e) {
throw new Exception(e);
}
return xmlContent;
}
/**
* 还原4个字节的网络字节序
*
* @param orderBytes 字节码
*
* @return sourceNumber
*/
private int recoverNetworkBytesOrder(byte[] orderBytes) {
int sourceNumber = 0;
int length = 4;
int number = 8;
for (int i = 0; i < length; i++) {
sourceNumber <<= number;
sourceNumber |= orderBytes[i] & 0xff;
}
return sourceNumber;
}
/**
* 加密机密demo
* @param args
*/
public static void main(String[] args) {
String dy = "OpCoJgs7RrVgaMNDixIvaCIyV2SFDBNLivgkVqtzq2GC10egsn+PKmQ/+5q+chT8xzldLUog2haTItyIkKyvzvmXonBQLIMeq54axAu9c3KG8IhpFD6+ymHocmx07ZKi7eED3t0KyIxJgRNSDkFk5RV1ZP2mSWa7ZgCXXcAbP0RsiUcvhcJfrSwlpsm0E1YJzKpYy429xrEEGvK+gfL+Cw==";
String sessionKey = "1df09d0a1677dd72b8325aec59576e0c";
Demo demo = new Demo();
String dd = demo.decrypt(dy, sessionKey);
System.out.println(dd);
}
}
PKCS7Encoder.java 版本:
/*
* Copyright (C) 2018 Baidu, Inc. All Rights Reserved.
*/
package com.baidu.mapp.platform.common.util.secruity;
import java.nio.charset.Charset;
import java.util.Arrays;
public class PKCS7Encoder {
static Charset CHARSET = Charset.forName("utf-8");
static int BLOCK_SIZE = 32;
/**
* 获得对明文进行补位填充的字节.
*
* @param count 需要进行填充补位操作的明文字节个数
*
* @return 补齐用的字节数组
*/
static byte[] encode(int count) {
// 计算需要填充的位数
int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
if (amountToPad == 0) {
amountToPad = BLOCK_SIZE;
}
// 获得补位所用的字符
char padChr = chr(amountToPad);
String tmp = new String();
for (int index = 0; index < amountToPad; index++) {
tmp += padChr;
}
return tmp.getBytes(CHARSET);
}
/**
* 删除解密后明文的补位字符
*
* @param decrypted 解密后的明文
*
* @return 删除补位字符后的明文
*/
static byte[] decode(byte[] decrypted) {
int pad = (int) decrypted[decrypted.length - 1];
if (pad < 1 || pad > 32) {
pad = 0;
}
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
/**
* 将数字转化成ASCII码对应的字符,用于对明文进行补码
*
* @param a 需要转化的数字
*
* @return 转化得到的字符
*/
static char chr(int a) {
byte target = (byte) (a & 0xFF);
return (char) target;
}
}