rapid-unipay
Version:
UnionPay module
255 lines (226 loc) • 6.96 kB
JavaScript
/**
* Copyright (c) 2017 Lucky Byte, Inc.
*/
const debug = require('debug')('unipay');
const winston = require('winston');
const crypto = require('crypto');
/**
* 生成请求报文签名
*
* 参数
* reqt_json: 请求报文,JSON 格式
* privkey: 签名私钥证书,请参考 NodeJS Sign.sign() 说明
* 返回
* 签名数据
*/
const gen_reqt_sign = (reqt_json, privkey) => {
if (!reqt_json || !privkey) {
throw new Error('[utils.gen_reqt_sign]参数无效');
}
let input = [];
Object.keys(reqt_json).sort().forEach((key) => {
if (!reqt_json[key]) {
throw new Error(`请求报文字段[${key}]无效`);
}
if (key != 'signature') {
input.push(`${key}=${reqt_json[key]}`);
}
});
// 默认使用 RSA-SHA1 算法
let sha_alg = 'sha1';
let rsa_sha_alg = 'RSA-SHA1';
// 5.1.0 使用 RSA-SHA256
if (reqt_json.version == '5.1.0') {
sha_alg = 'sha256';
rsa_sha_alg = 'RSA-SHA256';
}
const sha = crypto.createHash(sha_alg);
const shasum = sha.update(input.join('&'), 'utf8').digest('hex');
const rsa_sha = crypto.createSign(rsa_sha_alg);
return rsa_sha.update(shasum, 'utf8').sign(privkey, 'base64');
}
/*
* 验证响应报文签名
*
* 参数
* resp_json: 响应报文,JSON 格式
* cert: 验证签名证书,请参考 NodeJS Verify.verify() 函数说明
* 返回
* 成功返回 true,失败返回 false
*/
const verify_resp_sign = (resp_json, cert) => {
if (!resp_json || !cert) {
throw new Error('[verify_resp_sign]参数无效');
}
if (!resp_json.signature) {
throw new Error('响应报文缺少[signature],不能验证签名');
}
let input = [];
Object.keys(resp_json).sort().forEach((key) => {
if (key != 'signature') {
input.push(`${key}=${resp_json[key]}`);
}
});
// 默认使用 RSA-SHA1 算法
let sha_alg = 'sha1';
let rsa_sha_alg = 'RSA-SHA1';
// 5.1.0 使用 RSA-SHA256
if (resp_json.version == '5.1.0') {
sha_alg = 'sha256';
rsa_sha_alg = 'RSA-SHA256';
}
const sha = crypto.createHash(sha_alg);
const shasum = sha.update(input.join('&'), 'utf8').digest('hex');
const rsa_sha = crypto.createVerify(rsa_sha_alg);
return rsa_sha.update(shasum, 'utf8').verify(cert, resp_json.signature, 'base64');
}
/*
* 解析响应报文
*
* 参数
* resp_data: 银联响应数据,格式为 k=v&k2=v2&...
*
* 返回
* 解析后的 JSON 对象
*/
const parse_resp_data = (resp_data) => {
if (!resp_data) {
throw new Error('[parse_resp_string]参数无效');
}
let resp_json = {}
const array = resp_data.split('&');
for (let i in array) {
// 忽略空字符串,处理输入数据为 'a=b&' 这样的情况
if (!array[i]) {
continue;
}
const index = array[i].indexOf('=');
if (index < 0) {
throw new Error(`响应报文格式错误[${array[i]}]`);
}
const k = array[i].substr(0, index);
const v = array[i].substr(index + 1);
resp_json[k] = v;
}
debug(`解析响应报文[${resp_data}]结果:`, resp_json);
return resp_json;
}
/**
* 构造代付身份认证信息
*
* 参数:
* params: JSON 对象,可包含下面的 keys:
* acct_name: 账户名称
* acct_cert_no: 证件号码
* 返回:
* 身份认证字符串
*/
const build_cust_info = (params) => {
let cust_info = [];
if (params.acct_name) {
cust_info.push(`customerNm=${params.acct_name}`);
}
if (params.acct_cert_no) {
cust_info.push('certifTp=01');
cust_info.push(`certifId=${params.acct_cert_no}`);
}
if (params.smscode) {
cust_info.push(`smsCode=${params.smscode}`);
}
if (params.encrypted_info) {
cust_info.push(`encryptedInfo=${params.encrypted_info}`);
}
const cust_info_str = `{${cust_info.join('&')}}`;
return Buffer.from(cust_info_str, 'utf-8').toString('base64');
}
/**
* 构造身份验证信息中的 encryptedInfo
*/
const build_encrypted_info = (params, enc_cer) => {
let encrypt_info = [];
if (params.cvn) {
encrypt_info.push(`cvn2=${params.cvn}`);
}
if (params.expiry) {
encrypt_info.push(`expired=${params.expiry}`);
}
if (params.mobile) {
encrypt_info.push(`phoneNo=${params.mobile}`);
}
const info_str = encrypt_info.join('&');
if (enc_cer) {
return pubkey_encrypt(enc_cer, info_str);
}
return Buffer.from(info_str, 'utf-8').toString('base64');
}
/**
* 解密银联返回的含子域的数据, 例如 customerInfo 字段
* 返回解析后的 JSON 数据
*/
const parse_subfields = (text) => {
const c = text.replace(/^\s*{/, '').replace(/}\s*$/, '');
const subfields = c.split('&');
let fields = {};
for (let i = 0; i < subfields.length; i++) {
const kv = subfields[i].replace('=', '&').split('&', 2);
fields[kv[0]] = kv[1] || '';
}
return fields;
}
/**
* 加密敏感字段
*/
const pubkey_encrypt = (cert, text) => {
return crypto.publicEncrypt({
key: cert, padding: crypto.constants.RSA_PKCS1_PADDING
}, Buffer.from(text, 'utf-8')).toString('base64');
}
/**
* 解密敏感字段,key 是解密后的私钥, text 是 base64 编码的密文
*/
const privkey_decrypt = (key, text) => {
return crypto.privateDecrypt({
key: key, padding: crypto.constants.RSA_PKCS1_PADDING
}, Buffer.from(text, 'base64')).toString('utf-8');
}
/**
* 生成一个自动提交的 HTML 文件
*/
const build_submit_html = (dest_url, reqt_json, target) => {
let inputs = [];
for (let k in reqt_json) {
const v = reqt_json[k];
inputs.push(`<input type='hidden' name='${k}' value='${v}' />`);
}
return `
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
<meta name="author" content="Lucky Byte, Inc.">
<meta name="target" content="${target || 'None'}" />
</head>
<body>
<form id='form' action='${dest_url}' method='POST'>
${inputs.join('\n')}
</form>
<script type='text/javascript'>
document.getElementById('form').submit();
</script>
<noscript>
非常抱歉,您的浏览器未开启 JavaScript,不能完成支付!
</noscript>
</body>
</html>
`;
}
module.exports = {
gen_reqt_sign : gen_reqt_sign,
verify_resp_sign : verify_resp_sign,
parse_resp_data : parse_resp_data,
build_cust_info : build_cust_info,
build_encrypted_info: build_encrypted_info,
parse_subfields : parse_subfields,
pubkey_encrypt : pubkey_encrypt,
privkey_decrypt : privkey_decrypt,
build_submit_html: build_submit_html,
}