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}