001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.security; 020 021import org.jpos.iso.ISOUtil; 022import org.jpos.util.Loggeable; 023 024import java.io.PrintStream; 025import java.io.Serializable; 026import java.nio.ByteBuffer; 027import java.util.Objects; 028 029/** 030 * Key Serial Number (also called Key Name in the ANSI X9.24). 031 * Needed for deriving the Transaction Key when DUKPT (Derived Unique Key Per 032 * Transaction) method is used.<br> 033 * Refer to ANSI X9.24 for more information about DUKPT 034 * @author Hani S. Kirollos 035 * @see EncryptedPIN 036 */ 037public class KeySerialNumber implements Serializable, Loggeable { 038 private static final long serialVersionUID = 5588769944206835776L; 039 /** Base key identifier portion of the KSN. */ 040 private long baseId; 041 /** Device identifier portion of the KSN. */ 042 private long deviceId; 043 /** Transaction counter portion of the KSN. */ 044 private int transactionCounter; 045 046 /** 047 * Constructs a key serial number object 048 * @param baseKeyID a HexString representing the BaseKeyID (also called KeySet ID) 049 * @param deviceID a HexString representing the Device ID (also called TRSM ID) 050 * @param transactionCounter a HexString representing the transaction counter 051 */ 052 public KeySerialNumber (String baseKeyID, String deviceID, String transactionCounter) { 053 try { 054 baseKeyID = ISOUtil.padleft(baseKeyID, 10, 'F'); 055 } catch (Exception e) { 056 throw new IllegalArgumentException("Invalid baseKeyID."); 057 } 058 baseId = Long.parseLong(baseKeyID, 16); 059 deviceId = Long.parseLong (deviceID, 16); 060 this.transactionCounter = Integer.parseInt (transactionCounter, 16); 061 } 062 063 /** 064 * Constructs a key serial number object from its binary representation. 065 * @param ksn binary representation of the KSN. 066 */ 067 public KeySerialNumber(byte[] ksn) { 068 Objects.requireNonNull (ksn, "KSN cannot be null"); 069 if (ksn.length < 8 || ksn.length > 10) { 070 throw new IllegalArgumentException("KSN must be 8 to 10 bytes long."); 071 } 072 parseKsn (ksn); 073 } 074 /** 075 * Returns the base key ID as a hexadecimal string padded with leading zeros to a length of 10 characters. 076 * 077 * @return a String representing the base key ID. 078 */ 079 public String getBaseKeyID () { 080 return String.format ("%010X", baseId); 081 } 082 083 /** 084 * Returns the base key ID as an array of bytes. 085 * @return a 5 bytes array representing the base key ID. 086 */ 087 public byte[] getBaseKeyIDBytes () { 088 ByteBuffer buf = ByteBuffer.allocate(8); 089 buf.putLong(baseId); 090 buf.position(3); 091 byte[] lastFive = new byte[5]; 092 buf.get(lastFive); 093 return lastFive; 094 } 095 096 /** 097 * Returns the device ID as a hexadecimal string padded with leading zeros to a length of 6 characters. 098 * @return a String representing the device ID. 099 */ 100 public String getDeviceID () { 101 return String.format ("%05X", deviceId); 102 } 103 104 /** 105 * Returns the deviceID as an array of bytes. 106 * 107 * @return a 3 bytes array representing the deviceID 108 */ 109 public byte[] getDeviceIDBytes () { 110 ByteBuffer buf = ByteBuffer.allocate(8); 111 buf.putLong(deviceId); 112 buf.position(5); 113 byte[] lastThree = new byte[3]; 114 buf.get (lastThree); 115 return lastThree; 116 } 117 118 /** 119 * Returns the transaction counter as a hexadecimal string padded with leading zeros to a length of 6 characters. 120 * 121 * @return a String representing the transaction counter. 122 */ 123 public String getTransactionCounter () { 124 return String.format ("%05X", transactionCounter); 125 } 126 127 /** 128 * Returns the transaction counter as an array of bytes. 129 * 130 * @return a 3 byte array representing the transaction counter. 131 */ 132 public byte[] getTransactionCounterBytes () { 133 ByteBuffer buf = ByteBuffer.allocate(4); 134 buf.putInt(transactionCounter); 135 buf.position(1); 136 byte[] lastThree = new byte[3]; 137 buf.get (lastThree); 138 return lastThree; 139 } 140 141 /** 142 * Constructs a 10-byte Key Serial Number (KSN) array using the base key ID, device ID, and transaction counter. 143 * The method first extracts the last 5 bytes from the base key ID and device ID (shifted and combined with the 144 * transaction counter), and then combines them into a single ByteBuffer of size 10. 145 * 146 * @return A byte array containing the 10-byte Key Serial Number. 147 */ 148 public byte[] getBytes() { 149 ByteBuffer buf = ByteBuffer.allocate(10); 150 buf.put (last5(baseId)); 151 buf.put (last5(deviceId >> 1 << 21 | transactionCounter)); 152 return buf.array(); 153 } 154 155 /** 156 * dumps Key Serial Number 157 * @param p a PrintStream usually supplied by Logger 158 * @param indent indention string, usually suppiled by Logger 159 * @see org.jpos.util.Loggeable 160 */ 161 public void dump (PrintStream p, String indent) { 162 String inner = indent + " "; 163 p.println(indent + "<key-serial-number>"); 164 p.printf ("%s<image>%s</image>%n", inner, ISOUtil.hexString(getBytes())); 165 p.println(inner + "<base-key-id>" + getBaseKeyID() + "</base-key-id>"); 166 p.println(inner + "<device-id>" + getDeviceID() + "</device-id>"); 167 p.println(inner + "<transaction-counter>" + getTransactionCounter() + "</transaction-counter"); 168 p.println(indent + "</key-serial-number>"); 169 } 170 171 @Override 172 public String toString() { 173 return String.format( 174 "KeySerialNumber{base=%X, device=%X, counter=%X}", baseId, deviceId, transactionCounter 175 ); 176 } 177 178 /** 179 * Parses a Key Serial Number (KSN) into its base key ID, device ID, and transaction counter components. 180 * The KSN is first padded to a length of 10 bytes, and then the base key ID, device ID, and transaction counter 181 * are extracted. 182 * The base key id has a fixed length of 5 bytes. 183 * The sequence number has a fixed length of 19 bits. 184 * The transaction counter has a fixed length of 21 bits per ANS X9.24 spec. 185 * 186 * It is important to mention that the device ID is a 19-bit value, which has been shifted one bit to the right 187 * from its original hexadecimal representation. To facilitate readability and manipulation when reconstructing 188 * the KSN byte image, the device ID is maintained in a left-shifted position by one bit. 189 * 190 * @param ksn The input KSN byte array to be parsed. 191 * @throws IllegalArgumentException If the base key ID length is smaller than 0 or greater than 8. 192 */ 193 private void parseKsn(byte[] ksn) { 194 ByteBuffer buf = padleft (ksn, 10, (byte) 0xFF); 195 196 byte[] baseKeyIdBytes = new byte[5]; 197 buf.get(baseKeyIdBytes); 198 baseId = padleft (baseKeyIdBytes, 8, (byte) 0x00).getLong(); 199 200 ByteBuffer sliceCopy = buf.slice().duplicate(); 201 ByteBuffer remaining = ByteBuffer.allocate(8); 202 remaining.position(8 - sliceCopy.remaining()); 203 remaining.put(sliceCopy); 204 remaining.flip(); 205 206 long l = remaining.getLong(); 207 208 int mask = (1 << 21) - 1; 209 transactionCounter = (int) l & mask; 210 deviceId = l >>> 21 << 1; 211 } 212 213 /** 214 * Pads the input byte array with a specified padding byte on the left side to achieve a desired length. 215 * 216 * @param b The input byte array to be padded. 217 * @param len The desired length of the resulting padded byte array. 218 * @param padbyte The byte value used for padding the input byte array. 219 * @return A ByteBuffer containing the padded byte array with the specified length. 220 * @throws IllegalArgumentException If the desired length is smaller than the length of the input byte array. 221 */ 222 private ByteBuffer padleft (byte[] b, int len, byte padbyte) { 223 if (len < b.length) { 224 throw new IllegalArgumentException("Desired length must be greater than or equal to the length of the input byte array."); 225 } 226 ByteBuffer buf = ByteBuffer.allocate(len); 227 for (int i=0; i<len-b.length; i++) 228 buf.put (padbyte); 229 buf.put (b); 230 buf.flip(); 231 return buf; 232 } 233 234 /** 235 * Extracts the last 5 bytes from the 8-byte representation of the given long value. 236 * The method first writes the long value into a ByteBuffer of size 8, and then 237 * creates a new ByteBuffer containing the last 5 bytes of the original buffer. 238 * 239 * @param l The input long value to be converted and sliced. 240 * @return A ByteBuffer containing the last 5 bytes of the 8-byte representation of the input long value. 241 */ 242 private ByteBuffer last5 (long l) { 243 ByteBuffer buf = ByteBuffer.allocate(8); 244 buf.putLong(l); 245 buf.position(3); 246 return buf.slice(); 247 } 248}