针对银联生成ARQC,ARPC,还有MAC进行了软加密实现,一般的银行都是进行调用加密机实现,为了方便测试使用自己对其进行了软加密算法实现:
基本帮助类如下:
package com.omini.common.utils; import java.nio.ByteBuffer; import java.util.Arrays; /** * @author sandy * @version $Revision: 1.1 $ 建立日期 2012-9-11 */ public class StringUtils { /** * 抽取字符或是数字 若isNumber传入true则表示抽取数字,false则表示抽取字符 * * @param result * @param isNumber * @return */ public static String extract(String result, boolean isNumber) { if (null == result || result.equals("") || result.length() == 0) { throw new IllegalArgumentException("参数不正确,不能为空!"); } StringBuffer resultBuffer = new StringBuffer(); char[] chars = result.toUpperCase().toCharArray(); for (char c : chars) { boolean flag = Character.isDigit(c); if (isNumber && flag) { resultBuffer.append(c); } if (!flag && !isNumber) { resultBuffer.append(c); } } return resultBuffer.toString(); } /** * 减去10 * * @param input * @return */ public static String divide(String input) { if (null == input || input.equals("") || input.length() == 0) { throw new IllegalArgumentException("参数不正确,不能为空!"); } char[] output = new char[input.length()]; for (int i = 0; i < input.length(); i++) { if (output[i] > 96) { output[i] = (char) (output[i] - 49); } else if (output[i] > 64) { output[i] = (char) (output[i] - 17); } else { output[i] = output[i]; } } return Arrays.toString(output); } /** * 追加字符到指定长度的字符 * * @param srcData * :原数据 * @param alignMode * :对齐方式 * @param paddCharacter * :填补的字符 * @param totalLen * :填充到的长度 * @return */ public static String padding(String srcData, String alignMode, String paddCharacter, int totalLen) { if (srcData == null || null == alignMode || null == paddCharacter || totalLen == 0) { throw new IllegalArgumentException("传入的数据不能为空或0,请检查数据!"); } int paddLen = totalLen - srcData.length(); StringBuffer paddResultBuffer = new StringBuffer(); if (alignMode.equalsIgnoreCase("left")) { for (int i = 0; i < paddLen; i++) { paddResultBuffer.append(paddCharacter); } paddResultBuffer.append(srcData); } else if (alignMode.equalsIgnoreCase("right")) { paddResultBuffer.append(srcData); for (int i = 0; i < paddLen; i++) { paddResultBuffer.append(paddCharacter); } } else { throw new IllegalArgumentException("paddAlign is not left or right,please check !"); } return paddResultBuffer.toString(); } /** * 两个数据进行异或操作 * * @param hexSrcData1 * :32CB95B36D89477C * @param hexSrcData2 * :3030000000000000 * @return */ public static String XOR(String hexSrcData1, String hexSrcData2) { if (hexSrcData1.length() != hexSrcData2.length()) { throw new IllegalArgumentException("异或的两个数据长度不相等,请检查数据!"); } byte[] bytes1 = HexBinary.decode(hexSrcData1); byte[] bytes2 = HexBinary.decode(hexSrcData2); ByteBuffer buffer = ByteBuffer.allocate(bytes2.length); for (int i = 0; i < bytes2.length; i++) { byte temp = (byte) ((int) bytes1[i] ^ (int) bytes2[i]); buffer.put(temp); } return HexBinary.encode(buffer.array()); } /** * 按位取反操作 * * @param hexSrcData * @return */ public static String reversBytes(String hexSrcData) { if (null == hexSrcData || hexSrcData.equals("") || hexSrcData.length() == 0) { throw new IllegalArgumentException("非法的按位取反的数据,请检查数据"); } byte[] srcBytes = HexBinary.decode(hexSrcData); ByteBuffer destBuffer = ByteBuffer.allocate(srcBytes.length); for (int i = 0; i < srcBytes.length; i++) { byte temp = (byte) (~(int) srcBytes[i]); destBuffer.put(temp); } return HexBinary.encode(destBuffer.array()); } }
package com.omini.common.utils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* @author sandy
* @version $Revision: 1.1 $ 建立日期 2012-9-11
*/
public class DESUtils
{
/**
* DES加密数据非填充方式
*
* @param hexKey
* @param hexData
* @param mode
* @return
* @throws Exception
*/
public static String decEncNoPaddingDES(String hexKey, String hexData, int mode) throws Exception
{
SecretKey desKey = new SecretKeySpec(HexBinary.decode(hexKey), "DES/ECB/NoPadding");
Cipher cp = Cipher.getInstance("DES/ECB/NoPadding");
cp.init(mode, desKey);
byte[] bytes = cp.doFinal(HexBinary.decode(hexData));
return HexBinary.encode(bytes);
}
public static String encrypt(String hexKey, String hexData) throws Exception
{
SecretKey desKey = new SecretKeySpec(HexBinary.decode(hexKey), "DES");
Cipher cp = Cipher.getInstance("DES");
cp.init(Cipher.ENCRYPT_MODE, desKey);
byte[] bytes = cp.doFinal(HexBinary.decode(hexData));
return HexBinary.encode(bytes);
}
/**
* 3Des加密非填充
*
* @param hexKey
* @param hexData
* @return
* @throws Exception
*/
public static String encryptDesSede(String hexKey, String hexData) throws Exception
{
SecretKey desKey = new SecretKeySpec(HexBinary.decode(hexKey), "DESede/ECB/NoPadding");
Cipher cp = Cipher.getInstance("DESede/ECB/NoPadding");
cp.init(Cipher.ENCRYPT_MODE, desKey);
byte[] bytes = cp.doFinal(HexBinary.decode(hexData));
return HexBinary.encode(bytes);
}
}
package com.omini.common.utils; /** * @author sandy * @version $Revision: 1.1 $ 建立日期 2012-9-11 */ public class HexBinary { /** * Creates a clone of the given byte array. */ public static byte[] getClone(byte[] pHexBinary) { byte[] result = new byte[pHexBinary.length]; System.arraycopy(pHexBinary, 0, result, 0, pHexBinary.length); return result; } /** * Converts the stringpValue
into an array of hex bytes. */ public static byte[] decode(String pValue) { if ((pValue.length() % 2) != 0) { throw new IllegalArgumentException("A HexBinary string must have even length."); } byte[] result = new byte[pValue.length() / 2]; int j = 0; for (int i = 0; i < pValue.length();) { byte b; char c = pValue.charAt(i++); char d = pValue.charAt(i++); if (c >= '0' && c <= '9') { b = (byte) ((c - '0') << 4); } else if (c >= 'A' && c <= 'F') { b = (byte) ((c - 'A' + 10) << 4); } else if (c >= 'a' && c <= 'f') { b = (byte) ((c - 'a' + 10) << 4); } else { throw new IllegalArgumentException("Invalid hex digit: " + c); } if (d >= '0' && d <= '9') { b += (byte) (d - '0'); } else if (d >= 'A' && d <= 'F') { b += (byte) (d - 'A' + 10); } else if (d >= 'a' && d <= 'f') { b += (byte) (d - 'a' + 10); } else { throw new IllegalArgumentException("Invalid hex digit: " + d); } result[j++] = b; } return result; } /** * Converts the byte arraypHexBinary
into a string. */ public static String encode(byte[] pHexBinary) { StringBuffer result = new StringBuffer(); for (int i = 0; i < pHexBinary.length; i++) { byte b = pHexBinary[i]; byte c = (byte) ((b & 0xf0) >> 4); if (c <= 9) { result.append((char) ('0' + c)); } else { result.append((char) ('A' + c - 10)); } c = (byte) (b & 0x0f); if (c <= 9) { result.append((char) ('0' + c)); } else { result.append((char) ('A' + c - 10)); } } return result.toString(); } public static void main(String[] args) { String a = "12"; System.out.println(encode(a.getBytes())); String ab = "00000000000100000000000001560000000000015611041501112233447C00000603A00002"; System.out.println("::::::::::[" + HexBinary.encode(ab.getBytes()) + "]"); } }
package com.omini.common.utils; import javax.crypto.Cipher; /** * 生成ARQC,ARPC,MAC,CVN,CVN2工具类 * * @author sandy * @version $Revision: 1.1 $ 建立日期 2012-9-11 */ public class UnionUtils { /** * 其实生成ARQC就是调用的生成MAC方法,只不过MAC值是取得的前4个字节的值 * * @param pan * @param panSN * @param hexATC * @param mainKey * @param arqcDataSource * @return * @throws Exception */ public static String generateARQC(String pan, String panSN, String hexATC, String mainKey, String arqcDataSource) throws Exception { if (null == mainKey || mainKey.length() != 32) { throw new IllegalArgumentException("非法的工作主密钥的值"); } if (pan == null || pan.equals("") || pan.length() == 0) { throw new IllegalArgumentException("卡号不能为空,请检查传入的卡号"); } String processKey = generateProcesKey(pan, panSN, hexATC, mainKey); String result = process(processKey, arqcDataSource); if (result.length() != 16) { throw new IllegalArgumentException("返回的mac结果非8字节(16位hex)"); } return result; } /** * 根据ARQC来生成ARPC * * @param hexArqc表示ARQC的值8字节 * @param pan * :表示卡号 * @param panSN * :表示卡序号00或01 * @param hexATC * :交易记数器 * @param mainKey * :工作主密钥 * @param authCode * :授权响应码2个字节 * @return * @throws Exception */ public static String generateARPC(String hexArqc, String pan, String panSN, String hexATC, String mainKey, String authCode) throws Exception { if (null == mainKey || mainKey.length() != 32) { throw new IllegalArgumentException("非法的工作主密钥的值"); } if (pan == null || pan.equals("") || pan.length() == 0) { throw new IllegalArgumentException("卡号不能为空,请检查传入的卡号"); } if (hexArqc == null || hexArqc.equals("") || pan.length() == 0 || hexArqc.length() != 16) { throw new IllegalArgumentException("非法的ARQC数据"); } String processKey = generateProcesKey(pan, panSN, hexATC, mainKey); String paddARC = StringUtils.padding(authCode, "right", "0", 16); String arqcAndARCXORResult = StringUtils.XOR(hexArqc, paddARC); String arpc = DESUtils.encryptDesSede(processKey, arqcAndARCXORResult); System.out.println(arpc); return arpc; } /** * 生成MAC,并获取计算后的前4个字节 命令中需要加密的数据加密以后再计算MAC。MAC使用对称密钥算法计算的, 步骤如下: 步骤1:初始值为8 * 字节全零(此步骤可省略); 步骤2:下列数据按顺序排列得到一个数据块D: ——CLA、INS、P1、P2 和Lc(Lc 的长度包括MAC * 的长度); ——ATC(对于发卡行脚本处理,此ATC 在请求中报文中上送); * ——应用密文(对于发卡行脚本处理,此应用密文通常是ARQC,或AAC,在请求报文中上送); ——命令数据域中的明文或密文数据(如果存在)。 * 步骤3:将上述数据块D 分成8 字节长的数据块D1、D2、D3…最后一块数据块的字节长度为1 到8; 步骤4:如果最后一块数据块的长度为8 * 字节,后面补8 字节数据块:80 00 00 00 00 00 00 00, 执行步骤5; 如果最后一块数据块的长度小于8 * 字节,后面补一个字节80,如果长度到8 字节,执行 步骤5。如果仍然不够8 字节,补00 直到8 字节; 步骤5:用MAC * 过程密钥对数据块进行加密。MAC 过程密钥的生成见C.4; 图C.1 是使用MAC 过程密钥A 和B 生成MAC 的流程图。 步骤6:MAC * 的计算结果为8 字节,从最左边的字节开始取4 字节 * * @param pan * @param panSN * @param hexATC * @param mainKey * @param macDataSource * @return * @throws Exception */ public static String generateMAC(String pan, String panSN, String hexATC, String mainKey, String macDataSource) throws Exception { if (null == mainKey || mainKey.length() != 32) { throw new IllegalArgumentException("非法的工作主密钥的值"); } if (pan == null || pan.equals("") || pan.length() == 0) { throw new IllegalArgumentException("卡号不能为空,请检查传入的卡号"); } String processKey = generateProcesKey(pan, panSN, hexATC, mainKey); String result = process(processKey, macDataSource); if (result.length() != 16) { throw new IllegalArgumentException("返回的mac结果非8字节(16位hex)"); } result = result.substring(0, 8); return result; } /** * 计算CVN时使用二个64位的验证密钥,KeyA和KeyB。 a) 计算CVN 的数据源包括: * 主账号(PAN)、卡失效期和服务代码,从左至右顺序编排。 例如19位PAN、4位卡失效期和3位服务代码组成26个字符CVN数据源。 b) * 将上述数据源扩展成128 位二进制数据(不足128 位右补二进制0)。 c) 将128 位二进制数据分成两个64 位的数据块。最左边的64 * 位为Block1,最右边的64 位为 Block2。 d) 使用KeyA 对Block1 进行加密。 e) 将Block1 * 的加密结果与Block2 进行异或。使用KeyA 对异或结果进行加密。 f) 使用KeyB 对加密结果进行解密。 g) 使用KeyA * 对解密结果进行加密。 h) 从左至右将加密结果中的数字(0-9)抽出,组成一组数字。 i) 从左至右将加密结果中的字符(A-F)抽出,减10 * 后将余数组成一组数字,排列在步骤(8) 的数字之后。 j) 步骤(9)的左边第一组三位数即为CVN 值 * * @param pan * @param invalidDate * @param serviceCode * @param hexKey:验证密钥 * @return * @throws Exception */ public static String generateCVN(String pan, String invalidDate, String serviceCode, String hexKey) throws Exception { if (null == pan || null == invalidDate || null == serviceCode || null == hexKey) { throw new IllegalArgumentException("卡号或是失效日期或服务代码或验证密钥为空!"); } if (hexKey.length() != 32) { throw new IllegalArgumentException("验证密钥长度非32位!"); } String keyA = hexKey.substring(0, 16); String keyB = hexKey.substring(16); StringBuffer cvnDataSource = new StringBuffer(); cvnDataSource.append(pan).append(invalidDate).append(serviceCode); String cvnDS = StringUtils.padding(cvnDataSource.toString(), "right", "0", 32); String blockA = cvnDS.substring(0, 16); String blockB = cvnDS.substring(16); String keyAEncryptBlockAResult = DESUtils.decEncNoPaddingDES(keyA, blockA, Cipher.ENCRYPT_MODE); String xorBlockBResult = StringUtils.XOR(keyAEncryptBlockAResult, blockB); String result = DESUtils.decEncNoPaddingDES(keyA, xorBlockBResult, Cipher.ENCRYPT_MODE); result = DESUtils.decEncNoPaddingDES(keyB, result, Cipher.DECRYPT_MODE); result = DESUtils.decEncNoPaddingDES(keyA, result, Cipher.ENCRYPT_MODE); String numberData = StringUtils.extract(result, true); String characterData = StringUtils.extract(result, false); characterData = StringUtils.divide(characterData); result = numberData + characterData; if (result.length() < 3) { throw new IllegalArgumentException("计算CVN返回的长度小于3位长度不正确"); } result = result.substring(0, 3); return result; } /** * 印刷在签名条的右上方处并放在卡号(后4位)后 * * 生成CVN2其实就是把服务码变成常数000即可 * * @param pan:卡号 * @param invalidDate:失效日期 * @param serviceCode:服务码 * @param hexKey:验证密钥 * @return * @throws Exception */ public static String generateCVN2(String pan, String invalidDate, String serviceCode, String hexKey) throws Exception { return generateCVN(pan,invalidDate,"000",hexKey); } /** * 生成过程密钥 * * @param pan * @param panSN * @param hexATC * @param mainKey * @return * @throws Exception */ private static String generateProcesKey(String pan, String panSN, String hexATC, String mainKey) throws Exception { int cardNoLength = pan.length(); String cardNoRight14 = pan.substring(cardNoLength - 14); // 分散因子 String dispersionFactor = cardNoRight14 + panSN; // 对分散因子取反 String reversDispersionFactor = StringUtils.reversBytes(dispersionFactor); StringBuffer dispersionBuffer = new StringBuffer(); dispersionBuffer.append(dispersionFactor).append(reversDispersionFactor); // 生成子密钥 String subKey = DESUtils.encryptDesSede(mainKey, dispersionBuffer.toString()); String paddATC = StringUtils.padding(hexATC, "left", "0", 16); String reversATC = StringUtils.reversBytes(hexATC); String paddReversATC = StringUtils.padding(reversATC, "left", "0", 16); String mergerATC = paddATC + paddReversATC; // 生成过程密钥 String processKey = DESUtils.encryptDesSede(subKey, mergerATC); return processKey; } /** * 计算MAC处理 * * @param processKey * :过程密钥 * @param macDataSource * :计算MAC的数据源 * @return * @throws Exception */ private static String process(String processKey, String macDataSource) throws Exception { if (null == processKey || processKey.equals("") || processKey.length() != 32) { throw new IllegalArgumentException("过程密钥不能为空或不够32位!"); } String leftKey = processKey.substring(0, 16); String rightKey = processKey.substring(16); // 拆分MAC数据源,每组16位hex(8 byte()) String[] ds = splitData(macDataSource); String des = ""; for (int i = 0; i < ds.length; i++) { if (i == 0) { // 第一次只做DES加密 des = DESUtils.decEncNoPaddingDES(leftKey, ds[i], Cipher.ENCRYPT_MODE).toUpperCase(); } else { // 用上一次 DES加密结果对 第 i 组数据做异或 des = StringUtils.XOR(des, ds[i]); // 对异或后的数据做DES加密 des = DESUtils.decEncNoPaddingDES(leftKey, des, Cipher.ENCRYPT_MODE).toUpperCase(); } } // DES 加密最终结果用processKey后16位解密 des = DESUtils.decEncNoPaddingDES(rightKey, des, Cipher.DECRYPT_MODE).toUpperCase(); // 解密后 再用processKey前16位加密 des = DESUtils.decEncNoPaddingDES(leftKey, des, Cipher.ENCRYPT_MODE).toUpperCase(); return des; } /* * 将hexMacDataSource进行分组 每 16 字符 8byte 一组 */ private static String[] splitData(String hexMacDataSource) { int len = 0; int modValue = hexMacDataSource.length() % 16; if (modValue == 0) { // 补上80000000000000 hexMacDataSource += "80000000000000"; len = hexMacDataSource.length() / 16; } else if (modValue == 14) { // 补上80 hexMacDataSource += "80"; len = hexMacDataSource.length() / 16; } else { hexMacDataSource += "80"; int hexSrcDataLen = hexMacDataSource.length(); int totalLen = hexSrcDataLen + (16 - modValue - 2); hexMacDataSource = StringUtils.padding(hexMacDataSource, "right", "0", totalLen); len = hexMacDataSource.length() / 16; } String[] ds = new String[len]; for (int i = 0; i < ds.length; i++) { if (hexMacDataSource.length() >= 16) { ds[i] = hexMacDataSource.substring(0, 16); hexMacDataSource = hexMacDataSource.substring(16); } else { throw new IllegalArgumentException("填充的数据非法!"); } } return ds; } }
测试代码如下:
package com.omini.common.utils; /** * @author sandy * @version $Revision: 1.1 $ 建立日期 2012-9-11 */ public class UnionUtilsTest { public static void main(String[] args) throws Exception { // UnionUtilsTest.generateMACTest(); UnionUtilsTest.generateCVN(); } public static void generateARPCTest() throws Exception { String arpc = UnionUtils.generateARPC("32CB95B36D89477C", "6214618888000002074", "00", "0008", "00000000000000000000000000000000", "3030"); System.out.println("arpc=" + arpc); } public static void generateMACTest() throws Exception { String result = UnionUtils.generateMAC("6214618888000002074", "00", "0029", "00000000000000000000000000000000", "04DA9F790A00299E99DA1521DAA0A3000000000000"); System.out.println("MAC=" + result); } public static void generateARQCTest() throws Exception { String result = UnionUtils.generateARQC("6214610200000004163", "01", "000B", "77777777777777777777777777777777", "00000001000000000000000001568008000800015610052801112233447C00000B03A03000"); System.out.println("ARQC=" + result); } public static void generateCVN() throws Exception { String result = UnionUtils.generateCVN("6221234567891234", "0712", "111", "0123456789ABCDEFFEDCBA9876543210"); System.out.println("CVN=" + result); } public static void generateCVN2() throws Exception { String result = UnionUtils.generateCVN("6221234567891234", "0712", "000", "0123456789ABCDEFFEDCBA9876543210"); System.out.println("CVN2=" + result); } }