Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KeySerialNumber overhaul #536

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 151 additions & 70 deletions jpos/src/main/java/org/jpos/security/KeySerialNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

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


/**
Expand All @@ -31,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 @@ -64,87 +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);
}

/**
* Constructs a key serial number object from its hexadecimal representation.
* @param hexKSN hexadecimal representation of the KSN.
* @param idLength length of the base key ID.
* @param deviceLength length of the device ID.
* @param counterLength length of the transaction counter.
* Constructs a key serial number object from its binary representation.
* @param ksn binary representation of the KSN.
*/
public KeySerialNumber(String hexKSN, int idLength, int deviceLength, int counterLength) {
if (hexKSN == null || hexKSN.trim().length() == 0)
throw new IllegalArgumentException("KSN cannot be empty.");
if (idLength + deviceLength + counterLength > hexKSN.length())
throw new IllegalArgumentException("Length spec doesn't match KSN.");
setBaseKeyID(hexKSN.substring(0, idLength));
setDeviceID(hexKSN.substring(idLength, idLength + deviceLength));
setTransactionCounter(hexKSN.substring(idLength + deviceLength, idLength + deviceLength + counterLength));
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);
}

/**
* Constructs a key serial number object from its binary representation.
* @param binKSN binary representation of the KSN.
* @param idLength length of the base key ID.
* @param deviceLength length of the device ID.
* @param counterLength length of the transaction counter.
* 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 KeySerialNumber(byte[] binKSN, int idLength, int deviceLength, int counterLength) {
this(ISOUtil.byte2hex(binKSN).toUpperCase(), idLength, deviceLength, counterLength);
public String getBaseKeyID () {
return String.format ("%010X", baseId);
}

/**
*
* @param baseKeyID a HexString representing the BaseKeyID (also called KeySet ID)
* Returns the base key ID as an array of bytes.
* @return a 5 bytes array representing the base key ID.
*/
public void setBaseKeyID (String baseKeyID) {
this.baseKeyID = baseKeyID;
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 baseKeyID a HexString representing the BaseKeyID (also called KeySet 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 getBaseKeyID () {
return baseKeyID;
public String getDeviceID () {
return String.format ("%06X", deviceId);
}

/**
* Returns the deviceID as an array of bytes.
*
* @param deviceID a HexString representing the Device ID (also called TRSM ID)
* @ return a 3 bytes array representing the deviceID
*/
public void setDeviceID (String deviceID) {
this.deviceID = deviceID;
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 deviceID a HexString representing the Device ID (also called TRSM ID)
* @return a String representing the transaction counter.
*/
public String getDeviceID () {
return deviceID;
public String getTransactionCounter () {
return String.format ("%06X", transactionCounter);
}

/**
* Returns the transaction counter as an array of bytes.
*
* @param transactionCounter a HexString representing the transaction counter
* @ return a 3 byte array representing the transaction counter.
*/
public void setTransactionCounter (String transactionCounter) {
this.transactionCounter = transactionCounter;
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 transactionCounter a HexString representing the transaction counter
* @return A byte array containing the 10-byte Key Serial Number.
*/
public String getTransactionCounter () {
return transactionCounter;
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 @@ -154,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();
}
}
Loading