一、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 returning
BitSet key = _getCurrentKey(ipek, ksn);
byte[] rkey = toByteArray(key);
// secure memory
obliviate(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 memory
obliviate(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 memory
obliviate(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 memory
obliviate(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 here
reg2.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-2
reg2 = 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-2
reg2.xor(keyreg.get(64, 128));
// done messing with reg2
// step 4: XOR the Key Register with hexadecimal C0C0 C0C0 0000 0000 C0C0 C0C0 0000 0000
keyreg.xor(toBitSet(toByteArray("C0C0C0C000000000C0C0C0C000000000")));
// step 5: Crypto Register-1 XORed with the right half of the Key Register goes to Crypto Register-1
reg1.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-1
reg1 = 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-1
reg1.xor(keyreg.get(64, 128));
// done
byte[] reg1b = toByteArray(reg1), reg2b = toByteArray(reg2);
byte[] key = concat(reg1b, reg2b);
BitSet rkey = toBitSet(key);
// secure memory
obliviate(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 length
k1 = bskey.get(0, 64);
k2 = k1;
k3 = k1;
} else if (bskey.length() == 128) {
// double length
k1 = bskey.get(0, 64);
k2 = bskey.get(64, 128);
k3 = k1;
} else {
// triple length
if (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");
else
encryptor = Cipher.getInstance("DESede/CBC/NoPadding");
encryptor.init(Cipher.ENCRYPT_MODE, encryptKey, iv);
byte[] bytes = encryptor.doFinal(data);
// secure memory
obliviate(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 memory
obliviate(pek);
return rkey;
}
// 核心MAC计算逻辑 ISO-9797-PART1
public 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);
}
}