Android DUKPT - 3DES
一、DUKPT概述
DUKPT 即Derived Unique Key Per Transaction(每个事务的派生唯一密钥)。ANSI X9.24规范定义的密钥管理体系,主要用于对称密钥加密场景(如MAC、PIN等敏感数据保护)。通过动态生成唯一交易密钥,解决传统固定密钥易被破解的安全风险。
二、组成结构
DUKPT由BDK及KSN组成。
-
基础主密钥(BDK)
-
作为根密钥(Base Derivation Key),通过加密模块生成初始密钥
-
通常为双倍长3DES密钥(如128位或192位
-
-
密钥序列号(KSN)
- 包含三部分:
-
密钥标识(10位:9位基础派生标识 + 1位子密钥标识)
-
设备标识(5位,含二进制位扩展)
-
交易计数器(5位,记录交易次数)
-
-
确保终端密钥唯一性,防止重复
- 包含三部分:
三、秘钥衍生过程
-
根密钥准备
收单机构通过HSM生成双倍长3DES的BDK(如0123456789ABCDEFFEDCBA9876543210),该密钥需满足FIPS 140-2 Level 3以上安全标准。 -
KSN结构解析
KSN由三部分构成:-
设备标识:10位十六进制(如2900080124)包含厂商代码和设备序列号
-
交易计数器:21位二进制(如00021E00001)记录交易次数
-
扩展位:1位二进制用于标识密钥用途
-
-
IPEK生成算法
通过3DES算法对BDK和设备标识进行加密运算 -
动态密钥派生
使用IPEK及KSN进行系列异或运算最终获得当前加密PIN的PEK。
四、代码实现
代码作者 Antoine Averlant
// 基于BDK及KSN生成当前PIN秘钥。
public static byte[] computeKeyFromBDK(byte[] baseDerivationKey, byte[] keySerialNumber) throws Exception {BitSet ksn = toBitSet(keySerialNumber);BitSet bdk = toBitSet(baseDerivationKey);BitSet ipek = getIpek(bdk, ksn);// convert key for returningBitSet key = _getCurrentKey(ipek, ksn);byte[] rkey = toByteArray(key);// secure memoryobliviate(ksn);obliviate(bdk);obliviate(ipek);obliviate(key);return rkey;
}// 基于BDK及KSN生成Ipek
public static BitSet getIpek(BitSet key, BitSet ksn) throws Exception {byte[][] ipek = new byte[2][];BitSet keyRegister = key.get(0, key.length());BitSet data = ksn.get(0, ksn.length());data.clear(59, 80);ipek[0] = encryptTripleDes(toByteArray(keyRegister), toByteArray(data.get(0, 64)));keyRegister.xor(toBitSet(toByteArray("C0C0C0C000000000C0C0C0C000000000")));ipek[1] = encryptTripleDes(toByteArray(keyRegister), toByteArray(data.get(0, 64)));byte[] bipek = concat(ipek[0], ipek[1]);BitSet bsipek = toBitSet(bipek);// secure memoryobliviate(ipek[0]);obliviate(ipek[1]);obliviate(bipek);obliviate(keyRegister);obliviate(data);return bsipek;
}// 基于IPEK及KSN生成PEK
private static BitSet _getCurrentKey(BitSet ipek, BitSet ksn) throws Exception {BitSet key = ipek.get(0, ipek.length());BitSet counter = ksn.get(0, ksn.length());counter.clear(59, ksn.length());for (int i = 59; i < ksn.length(); i++) {if (ksn.get(i)) {counter.set(i);BitSet tmp = _nonReversibleKeyGenerationProcess(key, counter.get(16, 80));// secure memoryobliviate(key);key = tmp;}}key.xor(toBitSet(toByteArray("00000000000000FF00000000000000FF"))); // data encryption variant (To PIN)// key.xor(toBitSet(toByteArray("F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0"))); // data encryption variant// key.xor(toBitSet(toByteArray("3C3C3C3C3C3C3C3C3C3C3C3C3C3C3C3C"))); // data encryption variant// secure memoryobliviate(counter);return key;
}private static BitSet _nonReversibleKeyGenerationProcess(BitSet p_key, BitSet data) throws Exception {BitSet keyreg = p_key.get(0, p_key.length());BitSet reg1 = data.get(0, data.length());// step 1: Crypto Register-1 XORed with the right half of the Key Register goes to Crypto Register-2.BitSet reg2 = reg1.get(0, 64); // reg2 is being used like a temp herereg2.xor(keyreg.get(64, 128)); // and here, too, kind of// step 2: Crypto Register-2 DEA-encrypted using, as the key, the left half of the Key Register goes to Crypto Register-2reg2 = toBitSet(encryptDes(toByteArray(keyreg.get(0, 64)), toByteArray(reg2)));// step 3: Crypto Register-2 XORed with the right half of the Key Register goes to Crypto Register-2reg2.xor(keyreg.get(64, 128));// done messing with reg2// step 4: XOR the Key Register with hexadecimal C0C0 C0C0 0000 0000 C0C0 C0C0 0000 0000keyreg.xor(toBitSet(toByteArray("C0C0C0C000000000C0C0C0C000000000")));// step 5: Crypto Register-1 XORed with the right half of the Key Register goes to Crypto Register-1reg1.xor(keyreg.get(64, 128));// step 6: Crypto Register-1 DEA-encrypted using, as the key, the left half of the Key Register goes to Crypto Register-1reg1 = toBitSet(encryptDes(toByteArray(keyreg.get(0, 64)), toByteArray(reg1)));// step 7: Crypto Register-1 XORed with the right half of the Key Register goes to Crypto Register-1reg1.xor(keyreg.get(64, 128));// donebyte[] reg1b = toByteArray(reg1), reg2b = toByteArray(reg2);byte[] key = concat(reg1b, reg2b);BitSet rkey = toBitSet(key);// secure memoryobliviate(reg1);obliviate(reg2);obliviate(reg1b);obliviate(reg2b);obliviate(key);obliviate(keyreg);return rkey;
}public static byte[] encryptTripleDes(byte[] key, byte[] data) throws Exception {return encryptTripleDes(key, data, true);
}public static byte[] encryptTripleDes(byte[] key, byte[] data, boolean padding) throws Exception {BitSet bskey = toBitSet(key);BitSet k1, k2, k3;if (bskey.length() == 64) {// single lengthk1 = bskey.get(0, 64);k2 = k1;k3 = k1;} else if (bskey.length() == 128) {// double lengthk1 = bskey.get(0, 64);k2 = bskey.get(64, 128);k3 = k1;} else {// triple lengthif (bskey.length() != 192) {throw new InvalidParameterException("Key is not 8/16/24 bytes long.");}k1 = bskey.get(0, 64);k2 = bskey.get(64, 128);k3 = bskey.get(128, 192);}byte[] kb1 = toByteArray(k1), kb2 = toByteArray(k2), kb3 = toByteArray(k3);byte[] key16 = concat(kb1, kb2);byte[] key24 = concat(key16, kb3);IvParameterSpec iv = new IvParameterSpec(new byte[8]);SecretKey encryptKey = SecretKeyFactory.getInstance("DESede").generateSecret(new DESedeKeySpec(key24));Cipher encryptor;if (padding)encryptor = Cipher.getInstance("DESede/CBC/PKCS5Padding");elseencryptor = Cipher.getInstance("DESede/CBC/NoPadding");encryptor.init(Cipher.ENCRYPT_MODE, encryptKey, iv);byte[] bytes = encryptor.doFinal(data);// secure memoryobliviate(k1);obliviate(k2);obliviate(k3);obliviate(kb1);obliviate(kb2);obliviate(kb3);obliviate(key16);obliviate(key24);obliviate(bskey);return bytes;
}public static byte[] MEKQ(byte[] bPEK) throws Exception {BitSet pek = toBitSet(bPEK);pek.xor(toBitSet(toByteArray("000000000000FFFF000000000000FFFF")));byte[] rkey = toByteArray(pek);// secure memoryobliviate(pek);return rkey;
}// 核心MAC计算逻辑 ISO-9797-PART1public static byte[] calculateMac(byte[] keyData, byte[] inputData) throws Exception {// 1. 处理双倍长密钥(16字节 -> 24字节)byte[] fullKey = Arrays.copyOf(keyData, 24);byte[] key1 = new byte[16];System.arraycopy(fullKey, 0, fullKey, 16, 8);System.arraycopy(keyData, 0, key1, 0, 8);System.arraycopy(keyData, 0, key1, 8, 8);// 2. 初始化加密器(CBC模式 + 零向量)Cipher cipher = Cipher.getInstance("DESede/CBC/NoPadding");cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key1, "DESede"), new IvParameterSpec(new byte[8]));// 3. 数据填充(补零至8字节倍数)byte[] paddedData;if (inputData.length % 8 != 0) {paddedData = Arrays.copyOf(inputData, inputData.length + (8 - inputData.length % 8));} else {paddedData = Arrays.copyOf(inputData, inputData.length);}int count = paddedData.length/8;byte[] paddedBlock = new byte[8];byte[] encryptData = new byte[8];//拆分数据--加密--异或for (int i = 0; i < count; i++) {if (i == 0) {System.arraycopy(paddedData, 0, paddedBlock, 0, 8);}encryptData = cipher.doFinal(paddedBlock);if (i + 2 < count) {System.arraycopy(paddedData, (i+1) * 8, paddedBlock, 0, 8);myXor(paddedBlock, encryptData);} else {System.arraycopy(paddedData, (i+1) * 8, paddedBlock, 0, 8);myXor(paddedBlock, encryptData);break;}}cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(fullKey, "DESede"), new IvParameterSpec(new byte[8]));encryptData = cipher.doFinal(paddedBlock);// 4. 执行加密并取前8字节return Arrays.copyOfRange(encryptData, 0, encryptData.length);
}public static void myXor(byte[] a, byte[] b) {if (a.length != b.length)return;for (int i = 0; i < a.length; i++)a[i] ^= b[i];
}/*** Converts a byte array to an extended BitSet.*/public static BitSet toBitSet(byte[] b) {BitSet bs = new BitSet(8 * b.length);for (int i = 0; i < b.length; i++) {for (int j = 0; j < 8; j++) {if ((b[i] & (1L << j)) > 0) {bs.set(8 * i + (7 - j));}}}return bs;}/*** Converts an extended BitSet into a byte.* <p>* Requires that the BitSet be exactly 8 bits long.*/public static byte toByte(BitSet b) {byte value = 0;for (int i = 0; i < b.length(); i++) {if (b.get(i))value = (byte) (value | (1L << 7 - i));}return value;}/*** Converts a BitSet into a byte array.* <p>* Pads to the left with zeroes.*/public static byte[] toByteArray(BitSet b) {int size = (int) Math.ceil(b.length() / 8.0d);byte[] value = new byte[size];for (int i = 0; i < size; i++) {value[i] = toByte(b.get(i * 8, Math.min(b.length(), (i + 1) * 8)));}return value;}/*** Converts a hexadecimal String into a byte array (Big-Endian).** @param s A representation of a hexadecimal number without any leading qualifiers such as "0x" or "x".*/public static byte[] toByteArray(String s) {int len = s.length();byte[] data = new byte[len / 2];for (int i = 0; i < len; i += 2) {data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));}return data;}/*** Converts a byte array into a hexadecimal string (Big-Endian).** @return A representation of a hexadecimal number without any leading qualifiers such as "0x" or "x".*/public static String toHex(byte[] bytes) {BigInteger bi = new BigInteger(1, bytes);return String.format("%0" + (bytes.length << 1) + "X", bi);}/*** Concatenates two byte arrays.** @return The array a concatenated with b. So if r is the returned array, r[0] = a[0] and r[a.length] = b[0].*/public static byte[] concat(byte[] a, byte[] b) {byte[] c = new byte[a.length + b.length];for (int i = 0; i < a.length; i++) {c[i] = a[i];}for (int i = 0; i < b.length; i++) {c[a.length + i] = b[i];}return c;}/*** Overwrites the extended BitSet NUM_OVERWRITES times with random data for security purposes.*/public static void obliviate(BitSet b) {obliviate(b, NUM_OVERWRITES);}/*** Overwrites the byte array NUM_OVERWRITES times with random data for security purposes.*/public static void obliviate(byte[] b) {obliviate(b, NUM_OVERWRITES);}/*** Overwrites the extended BitSet with random data for security purposes.*/public static void obliviate(BitSet b, int n) {java.security.SecureRandom r = new java.security.SecureRandom();for (int i = 0; i < NUM_OVERWRITES; i++) {for (int j = 0; j < b.length(); j++) {b.set(j, r.nextBoolean());}}}/*** Overwrites the byte array with random data for security purposes.*/public static void obliviate(byte[] b, int n) {for (int i = 0; i < n; i++) {b[i] = 0x00;b[i] = 0x01;}java.security.SecureRandom r = new java.security.SecureRandom();for (int i = 0; i < n; i++) {r.nextBytes(b);}}