Skip to content

Commit

Permalink
KeySerialNumber overhaul
Browse files Browse the repository at this point in the history
The KeySerialNumber class in its current implementation has several
issues that need to be addressed. The properties baseID, deviceID, and
transactionCounter are currently stored as simple String variables with
limited validation. However, the standard requires that the transaction
counter is just the rightmost 21 bits of the 10-byte Key Serial Number
(KSN). As part of this refactor, the implementation is updated to
comply and enforce this standard.

Instead of keeping three Strings, we now keep two longs (baseId,
deviceId) and an int (transactionCounter).

Also added handy methods to get byte representation of those as well
as the whole image (getBytes()).
  • Loading branch information
ar committed May 9, 2023
1 parent 0c97db5 commit 7a9468a
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 256 deletions.
206 changes: 158 additions & 48 deletions jpos/src/main/java/org/jpos/security/KeySerialNumber.java
Expand Up @@ -18,10 +18,13 @@

package org.jpos.security;

import org.jpos.iso.ISOUtil;
import org.jpos.util.Loggeable;

import java.io.PrintStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.Objects;


/**
Expand All @@ -30,31 +33,13 @@
* Transaction) method is used.<br>
* Refer to ANSI X9.24 for more information about DUKPT
* @author Hani S. Kirollos
* @version $Revision$ $Date$
* @see EncryptedPIN
*/
public class KeySerialNumber
implements Serializable, Loggeable {

private static final long serialVersionUID = -8388775376202253082L;
/**
* baseKeyID a HexString representing the BaseKeyID (also called KeySet ID)
*/
String baseKeyID;
/**
* deviceID a HexString representing the Device ID (also called TRSM ID)
*/
String deviceID;
/**
* transactionCounter a HexString representing the transaction counter
*/
String transactionCounter;

/**
* Constructs a key serial number object
*/
public KeySerialNumber () {
}
public class KeySerialNumber implements Serializable, Loggeable {
private static final long serialVersionUID = 5588769944206835776L;
private long baseId;
private long deviceId;
private int transactionCounter;

/**
* Constructs a key serial number object
Expand All @@ -63,59 +48,108 @@ public KeySerialNumber () {
* @param transactionCounter a HexString representing the transaction counter
*/
public KeySerialNumber (String baseKeyID, String deviceID, String transactionCounter) {
setBaseKeyID(baseKeyID);
setDeviceID(deviceID);
setTransactionCounter(transactionCounter);
try {
baseKeyID = ISOUtil.padleft(baseKeyID, 10, 'F');
} catch (Exception e) {
throw new IllegalArgumentException("Invalid baseKeyID.");
}
baseId = Long.parseLong(baseKeyID, 16);
deviceId = Long.parseLong (deviceID, 16);
this.transactionCounter = Integer.parseInt (transactionCounter, 16);
}

/**
*
* @param baseKeyID a HexString representing the BaseKeyID (also called KeySet ID)
* Constructs a key serial number object from its binary representation.
* @param ksn binary representation of the KSN.
*/
public void setBaseKeyID (String baseKeyID) {
this.baseKeyID = baseKeyID;
public KeySerialNumber(byte[] ksn) {
Objects.requireNonNull (ksn, "KSN cannot be null");
if (ksn.length < 8 || ksn.length > 10) {
throw new IllegalArgumentException("KSN must be 8 to 10 bytes long.");
}
parseKsn (ksn);
}

/**
*
* @return baseKeyID a HexString representing the BaseKeyID (also called KeySet ID)
* Returns the base key ID as a hexadecimal string padded with leading zeros to a length of 10 characters.
*
* @return a String representing the base key ID.
*/
public String getBaseKeyID () {
return baseKeyID;
return String.format ("%010X", baseId);
}

/**
*
* @param deviceID a HexString representing the Device ID (also called TRSM ID)
* Returns the base key ID as an array of bytes.
* @return a 5 bytes array representing the base key ID.
*/
public void setDeviceID (String deviceID) {
this.deviceID = deviceID;
public byte[] getBaseKeyIDBytes () {
ByteBuffer buf = ByteBuffer.allocate(8);
buf.putLong(baseId);
buf.position(3);
byte[] lastFive = new byte[5];
buf.get(lastFive);
return lastFive;
}

/**
*
* @return deviceID a HexString representing the Device ID (also called TRSM ID)
* Returns the device ID as a hexadecimal string padded with leading zeros to a length of 6 characters.
* @return a String representing the device ID.
*/
public String getDeviceID () {
return deviceID;
return String.format ("%06X", deviceId);
}

/**
* Returns the deviceID as an array of bytes.
*
* @param transactionCounter a HexString representing the transaction counter
* @ return a 3 bytes array representing the deviceID
*/
public void setTransactionCounter (String transactionCounter) {
this.transactionCounter = transactionCounter;
public byte[] getDeviceIDBytes () {
ByteBuffer buf = ByteBuffer.allocate(8);
buf.putLong(deviceId);
buf.position(5);
byte[] lastThree = new byte[3];
buf.get (lastThree);
return lastThree;
}

/**
* Returns the transaction counter as a hexadecimal string padded with leading zeros to a length of 6 characters.
*
* @return transactionCounter a HexString representing the transaction counter
* @return a String representing the transaction counter.
*/
public String getTransactionCounter () {
return transactionCounter;
return String.format ("%06X", transactionCounter);
}

/**
* Returns the transaction counter as an array of bytes.
*
* @ return a 3 byte array representing the transaction counter.
*/
public byte[] getTransactionCounterBytes () {
ByteBuffer buf = ByteBuffer.allocate(4);
buf.putInt(transactionCounter);
buf.position(1);
byte[] lastThree = new byte[3];
buf.get (lastThree);
return lastThree;
}

/**
* Constructs a 10-byte Key Serial Number (KSN) array using the base key ID, device ID, and transaction counter.
* The method first extracts the last 5 bytes from the base key ID and device ID (shifted and combined with the
* transaction counter), and then combines them into a single ByteBuffer of size 10.
*
* @return A byte array containing the 10-byte Key Serial Number.
*/
public byte[] getBytes() {
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put (last5(baseId));
buf.put (last5(deviceId >> 1 << 21 | transactionCounter));
return buf.array();
}

/**
* dumps Key Serial Number
* @param p a PrintStream usually supplied by Logger
Expand All @@ -125,12 +159,88 @@ public String getTransactionCounter () {
public void dump (PrintStream p, String indent) {
String inner = indent + " ";
p.println(indent + "<key-serial-number>");
p.printf ("%s<image>%s</image>%n", inner, ISOUtil.hexString(getBytes()));
p.println(inner + "<base-key-id>" + getBaseKeyID() + "</base-key-id>");
p.println(inner + "<device-id>" + getDeviceID() + "</device-id>");
p.println(inner + "<transaction-counter>" + getTransactionCounter() + "</transaction-counter");
p.println(indent + "</key-serial-number>");
}
}

@Override
public String toString() {
return String.format(
"KeySerialNumber{base=%X, device=%X, counter=%X}", baseId, deviceId, transactionCounter
);
}

/**
* Parses a Key Serial Number (KSN) into its base key ID, device ID, and transaction counter components.
* The KSN is first padded to a length of 10 bytes, and then the base key ID, device ID, and transaction counter
* are extracted.
* The base key id has a fixed length of 5 bytes.
* The sequence number has a fixed length of 19 bits.
* The transaction counter has a fixed length of 21 bits per ANS X9.24 spec.
*
* It is important to mention that the device ID is a 19-bit value, which has been shifted one bit to the right
* from its original hexadecimal representation. To facilitate readability and manipulation when reconstructing
* the KSN byte image, the device ID is maintained in a left-shifted position by one bit.
*
* @param ksn The input KSN byte array to be parsed.
* @throws IllegalArgumentException If the base key ID length is smaller than 0 or greater than 8.
*/
private void parseKsn(byte[] ksn) {
ByteBuffer buf = padleft (ksn, 10, (byte) 0xFF);

byte[] baseKeyIdBytes = new byte[5];
buf.get(baseKeyIdBytes);
baseId = padleft (baseKeyIdBytes, 8, (byte) 0x00).getLong();

ByteBuffer sliceCopy = buf.slice().duplicate();
ByteBuffer remaining = ByteBuffer.allocate(8);
remaining.position(8 - sliceCopy.remaining());
remaining.put(sliceCopy);
remaining.flip();

long l = remaining.getLong();

int mask = (1 << 21) - 1;
transactionCounter = (int) l & mask;
deviceId = l >>> 21 << 1;
}

/**
* Pads the input byte array with a specified padding byte on the left side to achieve a desired length.
*
* @param b The input byte array to be padded.
* @param len The desired length of the resulting padded byte array.
* @param padbyte The byte value used for padding the input byte array.
* @return A ByteBuffer containing the padded byte array with the specified length.
* @throws IllegalArgumentException If the desired length is smaller than the length of the input byte array.
*/
private ByteBuffer padleft (byte[] b, int len, byte padbyte) {
if (len < b.length) {
throw new IllegalArgumentException("Desired length must be greater than or equal to the length of the input byte array.");
}
ByteBuffer buf = ByteBuffer.allocate(len);
for (int i=0; i<len-b.length; i++)
buf.put (padbyte);
buf.put (b);
buf.flip();
return buf;
}

/**
* Extracts the last 5 bytes from the 8-byte representation of the given long value.
* The method first writes the long value into a ByteBuffer of size 8, and then
* creates a new ByteBuffer containing the last 5 bytes of the original buffer.
*
* @param l The input long value to be converted and sliced.
* @return A ByteBuffer containing the last 5 bytes of the 8-byte representation of the input long value.
*/
private ByteBuffer last5 (long l) {
ByteBuffer buf = ByteBuffer.allocate(8);
buf.putLong(l);
buf.position(3);
return buf.slice();
}
}
Expand Up @@ -2165,25 +2165,13 @@ private byte[] calculateInitialKey(KeySerialNumber sn, SecureDESKey bdk, boolean
byte[] kl = new byte[8];
byte[] kr = new byte[8];
byte[] kk = decryptFromLMK(bdk).getEncoded();
byte[] ksn = new byte[8];

System.arraycopy(kk, 0, kl, 0, 8);
System.arraycopy(kk, 8, kr, 0, 8);
String paddedKsn;
try
{
paddedKsn = ISOUtil.padleft(
sn.getBaseKeyID() + sn.getDeviceID() + sn.getTransactionCounter(),
20, 'F'
);
}
catch (ISOException e)
{
throw new SMException(e);
}

byte[] ksn = ISOUtil.hex2byte(paddedKsn.substring(0, 16));
System.arraycopy (sn.getBytes(), 0, ksn, 0, ksn.length);
ksn[7] &= 0xE0;

byte[] data = encrypt64(ksn, kl);
data = decrypt64(data, kr);
data = encrypt64(data, kl);
Expand All @@ -2210,7 +2198,7 @@ private byte[] calculateInitialKey(KeySerialNumber sn, SecureDESKey bdk, boolean
public byte[] dataEncrypt (SecureDESKey bdk, byte[] clearText) throws SMException {
try {
byte[] ksnB = jceHandler.generateDESKey ((short) 128).getEncoded();
KeySerialNumber ksn = getKSN (ISOUtil.hexString(ksnB));
KeySerialNumber ksn = getKSN (ksnB);
byte[] derivedKey = calculateDerivedKey (ksn, bdk, true, true);
Key dk = jceHandler.formDESKey ((short) 128, derivedKey);
byte[] cypherText = jceHandler.encryptData (lpack(clearText), dk);
Expand Down Expand Up @@ -2241,7 +2229,7 @@ public byte[] dataDecrypt (SecureDESKey bdk, byte[] cypherText) throws SMExcepti
System.arraycopy (cypherText, 24, encryptedData, 0, encryptedData.length);
System.arraycopy (cypherText, cypherText.length-8, mac, 0, 8);

KeySerialNumber ksn = getKSN (ISOUtil.hexString(ksnB));
KeySerialNumber ksn = getKSN (ksnB);

byte[] derivedKey = calculateDerivedKey (ksn, bdk, true, true);
Key dk = jceHandler.formDESKey ((short) 128, derivedKey);
Expand Down Expand Up @@ -2274,10 +2262,10 @@ private byte[] calculateDerivedKeySDES(KeySerialNumber ksn, SecureDESKey bdk)
new byte[]{(byte) 0xE0, (byte) 0x00, (byte) 0x00};

byte[] curkey = calculateInitialKey(ksn, bdk, false);
byte[] smidr = ISOUtil.hex2byte(
ksn.getBaseKeyID() + ksn.getDeviceID() + ksn.getTransactionCounter()
);
byte[] reg3 = ISOUtil.hex2byte(ksn.getTransactionCounter());
byte[] smidr = new byte[8];
System.arraycopy (ksn.getBytes(), 2, smidr, 0, smidr.length);

byte[] reg3 = ksn.getTransactionCounterBytes();
reg3 = and(reg3, _1FFFFF);
byte[] shiftr = _100000;
byte[] temp;
Expand Down Expand Up @@ -2311,10 +2299,18 @@ private byte[] calculateDerivedKeyTDES(KeySerialNumber ksn, SecureDESKey bdk, bo

byte[] curkey = calculateInitialKey(ksn, bdk, true);

String sn = ksn.getBaseKeyID() + ksn.getDeviceID() + ksn.getTransactionCounter();
if (sn.length() > 16) sn = sn.substring(sn.length()-16);
byte[] smidr = ISOUtil.hex2byte(sn);
byte[] reg3 = ISOUtil.hex2byte(ksn.getTransactionCounter());
byte[] smidr = new byte[8];
System.arraycopy (ksn.getBytes(), 2, smidr, 0, smidr.length);

byte[] ksnImage = ksn.getBytes();
byte[] reg3;
if (dataEncryption && ksnImage[0] != (byte) 0xFF && ksnImage[1] != (byte) 0xFF) {
// jPOS 2.x compatibility mode -
reg3 = new byte[5];
System.arraycopy (ksnImage, 5, reg3, 0, reg3.length);
} else {
reg3 = ksn.getTransactionCounterBytes();
}
reg3 = and(reg3, _1FFFFF);
byte[] shiftr = _100000;
byte[] temp;
Expand All @@ -2324,6 +2320,7 @@ private byte[] calculateDerivedKeyTDES(KeySerialNumber ksn, SecureDESKey bdk, bo
byte[] curkeyL = new byte[8];
byte[] curkeyR = new byte[8];
smidr = and(smidr, _E00000, 5);

do
{
temp = and(shiftr, reg3);
Expand Down Expand Up @@ -2384,13 +2381,10 @@ public SecureDESKey importBDK(String clearComponent1HexString,
clearComponent3HexString);
}

private KeySerialNumber getKSN(String s)
{
return new KeySerialNumber(
s.substring(0, 6),
s.substring(6, 10),
s.substring(10, Math.min(s.length(), 20))
);
private KeySerialNumber getKSN(byte[] b) {
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put(b, 0, 10);
return new KeySerialNumber (buf.array());
}

protected EncryptedPIN translatePINImpl
Expand Down

0 comments on commit 7a9468a

Please sign in to comment.