UNPKG

rapid-unipay

Version:

UnionPay module

255 lines (226 loc) 6.96 kB
/** * 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, }