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}