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 private long baseId; 040 private long deviceId; 041 private int transactionCounter; 042 043 /** 044 * Constructs a key serial number object 045 * @param baseKeyID a HexString representing the BaseKeyID (also called KeySet ID) 046 * @param deviceID a HexString representing the Device ID (also called TRSM ID) 047 * @param transactionCounter a HexString representing the transaction counter 048 */ 049 public KeySerialNumber (String baseKeyID, String deviceID, String transactionCounter) { 050 try { 051 baseKeyID = ISOUtil.padleft(baseKeyID, 10, 'F'); 052 } catch (Exception e) { 053 throw new IllegalArgumentException("Invalid baseKeyID."); 054 } 055 baseId = Long.parseLong(baseKeyID, 16); 056 deviceId = Long.parseLong (deviceID, 16); 057 this.transactionCounter = Integer.parseInt (transactionCounter, 16); 058 } 059 060 /** 061 * Constructs a key serial number object from its binary representation. 062 * @param ksn binary representation of the KSN. 063 */ 064 public KeySerialNumber(byte[] ksn) { 065 Objects.requireNonNull (ksn, "KSN cannot be null"); 066 if (ksn.length < 8 || ksn.length > 10) { 067 throw new IllegalArgumentException("KSN must be 8 to 10 bytes long."); 068 } 069 parseKsn (ksn); 070 } 071 /** 072 * Returns the base key ID as a hexadecimal string padded with leading zeros to a length of 10 characters. 073 * 074 * @return a String representing the base key ID. 075 */ 076 public String getBaseKeyID () { 077 return String.format ("%010X", baseId); 078 } 079 080 /** 081 * Returns the base key ID as an array of bytes. 082 * @return a 5 bytes array representing the base key ID. 083 */ 084 public byte[] getBaseKeyIDBytes () { 085 ByteBuffer buf = ByteBuffer.allocate(8); 086 buf.putLong(baseId); 087 buf.position(3); 088 byte[] lastFive = new byte[5]; 089 buf.get(lastFive); 090 return lastFive; 091 } 092 093 /** 094 * Returns the device ID as a hexadecimal string padded with leading zeros to a length of 6 characters. 095 * @return a String representing the device ID. 096 */ 097 public String getDeviceID () { 098 return String.format ("%05X", deviceId); 099 } 100 101 /** 102 * Returns the deviceID as an array of bytes. 103 * 104 * @ return a 3 bytes array representing the deviceID 105 */ 106 public byte[] getDeviceIDBytes () { 107 ByteBuffer buf = ByteBuffer.allocate(8); 108 buf.putLong(deviceId); 109 buf.position(5); 110 byte[] lastThree = new byte[3]; 111 buf.get (lastThree); 112 return lastThree; 113 } 114 115 /** 116 * Returns the transaction counter as a hexadecimal string padded with leading zeros to a length of 6 characters. 117 * 118 * @return a String representing the transaction counter. 119 */ 120 public String getTransactionCounter () { 121 return String.format ("%05X", transactionCounter); 122 } 123 124 /** 125 * Returns the transaction counter as an array of bytes. 126 * 127 * @ return a 3 byte array representing the transaction counter. 128 */ 129 public byte[] getTransactionCounterBytes () { 130 ByteBuffer buf = ByteBuffer.allocate(4); 131 buf.putInt(transactionCounter); 132 buf.position(1); 133 byte[] lastThree = new byte[3]; 134 buf.get (lastThree); 135 return lastThree; 136 } 137 138 /** 139 * Constructs a 10-byte Key Serial Number (KSN) array using the base key ID, device ID, and transaction counter. 140 * The method first extracts the last 5 bytes from the base key ID and device ID (shifted and combined with the 141 * transaction counter), and then combines them into a single ByteBuffer of size 10. 142 * 143 * @return A byte array containing the 10-byte Key Serial Number. 144 */ 145 public byte[] getBytes() { 146 ByteBuffer buf = ByteBuffer.allocate(10); 147 buf.put (last5(baseId)); 148 buf.put (last5(deviceId >> 1 << 21 | transactionCounter)); 149 return buf.array(); 150 } 151 152 /** 153 * dumps Key Serial Number 154 * @param p a PrintStream usually supplied by Logger 155 * @param indent indention string, usually suppiled by Logger 156 * @see org.jpos.util.Loggeable 157 */ 158 public void dump (PrintStream p, String indent) { 159 String inner = indent + " "; 160 p.println(indent + "<key-serial-number>"); 161 p.printf ("%s<image>%s</image>%n", inner, ISOUtil.hexString(getBytes())); 162 p.println(inner + "<base-key-id>" + getBaseKeyID() + "</base-key-id>"); 163 p.println(inner + "<device-id>" + getDeviceID() + "</device-id>"); 164 p.println(inner + "<transaction-counter>" + getTransactionCounter() + "</transaction-counter"); 165 p.println(indent + "</key-serial-number>"); 166 } 167 168 @Override 169 public String toString() { 170 return String.format( 171 "KeySerialNumber{base=%X, device=%X, counter=%X}", baseId, deviceId, transactionCounter 172 ); 173 } 174 175 /** 176 * Parses a Key Serial Number (KSN) into its base key ID, device ID, and transaction counter components. 177 * The KSN is first padded to a length of 10 bytes, and then the base key ID, device ID, and transaction counter 178 * are extracted. 179 * The base key id has a fixed length of 5 bytes. 180 * The sequence number has a fixed length of 19 bits. 181 * The transaction counter has a fixed length of 21 bits per ANS X9.24 spec. 182 * 183 * It is important to mention that the device ID is a 19-bit value, which has been shifted one bit to the right 184 * from its original hexadecimal representation. To facilitate readability and manipulation when reconstructing 185 * the KSN byte image, the device ID is maintained in a left-shifted position by one bit. 186 * 187 * @param ksn The input KSN byte array to be parsed. 188 * @throws IllegalArgumentException If the base key ID length is smaller than 0 or greater than 8. 189 */ 190 private void parseKsn(byte[] ksn) { 191 ByteBuffer buf = padleft (ksn, 10, (byte) 0xFF); 192 193 byte[] baseKeyIdBytes = new byte[5]; 194 buf.get(baseKeyIdBytes); 195 baseId = padleft (baseKeyIdBytes, 8, (byte) 0x00).getLong(); 196 197 ByteBuffer sliceCopy = buf.slice().duplicate(); 198 ByteBuffer remaining = ByteBuffer.allocate(8); 199 remaining.position(8 - sliceCopy.remaining()); 200 remaining.put(sliceCopy); 201 remaining.flip(); 202 203 long l = remaining.getLong(); 204 205 int mask = (1 << 21) - 1; 206 transactionCounter = (int) l & mask; 207 deviceId = l >>> 21 << 1; 208 } 209 210 /** 211 * Pads the input byte array with a specified padding byte on the left side to achieve a desired length. 212 * 213 * @param b The input byte array to be padded. 214 * @param len The desired length of the resulting padded byte array. 215 * @param padbyte The byte value used for padding the input byte array. 216 * @return A ByteBuffer containing the padded byte array with the specified length. 217 * @throws IllegalArgumentException If the desired length is smaller than the length of the input byte array. 218 */ 219 private ByteBuffer padleft (byte[] b, int len, byte padbyte) { 220 if (len < b.length) { 221 throw new IllegalArgumentException("Desired length must be greater than or equal to the length of the input byte array."); 222 } 223 ByteBuffer buf = ByteBuffer.allocate(len); 224 for (int i=0; i<len-b.length; i++) 225 buf.put (padbyte); 226 buf.put (b); 227 buf.flip(); 228 return buf; 229 } 230 231 /** 232 * Extracts the last 5 bytes from the 8-byte representation of the given long value. 233 * The method first writes the long value into a ByteBuffer of size 8, and then 234 * creates a new ByteBuffer containing the last 5 bytes of the original buffer. 235 * 236 * @param l The input long value to be converted and sliced. 237 * @return A ByteBuffer containing the last 5 bytes of the 8-byte representation of the input long value. 238 */ 239 private ByteBuffer last5 (long l) { 240 ByteBuffer buf = ByteBuffer.allocate(8); 241 buf.putLong(l); 242 buf.position(3); 243 return buf.slice(); 244 } 245}