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 java.io.IOException;
022import java.io.StringReader;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Objects;
031import java.util.function.ToIntFunction;
032import org.jpos.iso.ISOUtil;
033
034/**
035 * The builder class to create and parse key block structure.
036 */
037public class SecureKeyBlockBuilder {
038
039    /** Size in characters of the key-block version byte. */
040    protected static final int SIZE_KEYBLOCK_VERSION    = 1;
041
042    /** Size in characters of the total key-block length field. */
043    protected static final int SIZE_KEYBLOCK_LENGTH     = 4;
044
045    /** Size in characters of the key-usage attribute. */
046    protected static final int SIZE_KEYUSAGE            = 2;
047
048    /** Size in characters of the key-version field. */
049    protected static final int SIZE_KEY_VERSION         = 2;
050
051    /** Size in characters of the number-of-optional-headers field. */
052    protected static final int SIZE_NUMOFOPTHDR         = 2;
053
054    /** Size in characters of the reserved field. */
055    protected static final int SIZE_RESERVED            = 2;
056
057    /** Size in characters of the fixed key-block header. */
058    protected static final int SIZE_HEADER              = 16;
059
060    /** Size in characters of an optional header's identifier. */
061    protected static final int SIZE_OPTHDR_ID           = 2;
062
063    /** Size in characters of an optional header's length field. */
064    protected static final int SIZE_OPTHDR_LENGTH       = 2;
065
066    /** Size in characters of the MAC trailer when bound with 3-DES. */
067    protected static final int SIZE_HEADER_3DES         = 8;
068
069    /** Size in characters of the MAC trailer when bound with AES. */
070    protected static final int SIZE_HEADER_AES          = 16;
071
072    private final List<Character> versionsWith8CharacterMAC = new ArrayList<>(
073        Arrays.asList(
074             'A' // TR-31:2005 'A' Key block protected using the Key Variant Binding Method
075            ,'B' // TR-31:2010 'B' Key block protected using the Key Derivation Binding Method
076            ,'C' // TR-31:2010 'C' Key block protected using the Key Variant Binding Method
077            ,'0' // Proprietary '0' Key block protected using the 3-DES key
078        )
079    );
080
081    /**
082     * Don't let anyone instantiate this class.
083     */
084    private SecureKeyBlockBuilder() {
085    }
086
087    /**
088     * Returns a new {@code SecureKeyBlockBuilder} instance.
089     *
090     * @return a fresh builder
091     */
092    public static SecureKeyBlockBuilder newBuilder() {
093        return new SecureKeyBlockBuilder();
094    }
095
096    /**
097     * Configure key block versions with 8 digits key block MAC.
098     * <p>
099     * Default 8 digits <i>(4 bytes)</i> key block MAC versions are:
100     * <ul>
101     *   <li>'A' TR-31:2005 Key block protected using the Key Variant Binding Method
102     *   <li>'B' TR-31:2010 Key block protected using the Key Derivation Binding Method
103     *   <li>'C' TR-31:2010 Key block protected using the Key Variant Binding Method
104     *   <li>'0' Proprietary Key block protected using the 3-DES key
105     * </ul>
106     * @param versions the string with versions characters
107     * @return This builder instance
108     */
109    public SecureKeyBlockBuilder with8characterMACVersions(String versions) {
110        Objects.requireNonNull(versions, "The versions with 8 digits MAC cannot be null");
111        versionsWith8CharacterMAC.clear();
112        for (Character ch : versions.toCharArray())
113            versionsWith8CharacterMAC.add(ch);
114
115        return this;
116    }
117
118    /**
119     * Returns the MAC trailer length appropriate for the given key block's version.
120     *
121     * @param skb the key block being inspected
122     * @return {@link #SIZE_HEADER_3DES} for 8-character MAC versions, {@link #SIZE_HEADER_AES} otherwise
123     */
124    protected int getMACLength(SecureKeyBlock skb) {
125        if (versionsWith8CharacterMAC.contains(skb.getKeyBlockVersion()))
126            return SIZE_HEADER_3DES;
127
128        return SIZE_HEADER_AES;
129    }
130
131
132    /**
133     * Reads {@code len} characters from {@code sr}.
134     *
135     * @param sr source reader
136     * @param len number of characters to read
137     * @return the characters read, as a {@code String}
138     * @throws IllegalArgumentException if reading fails
139     */
140    protected static String readString(StringReader sr, int len) {
141        char[] chars = new char[len];
142        try {
143            sr.read(chars);
144        } catch (IOException ex) {
145            throw new IllegalArgumentException("Problem witch reading key block characters", ex);
146        }
147        return String.valueOf(chars);
148    }
149
150    /**
151     * Reads a single character from {@code sr}.
152     *
153     * @param sr source reader
154     * @return the character read
155     * @throws IllegalArgumentException if reading fails
156     */
157    protected static char readChar(StringReader sr) {
158        try {
159            return (char) sr.read();
160        } catch (IOException ex) {
161            throw new IllegalArgumentException("Problem witch reading key block character", ex);
162        }
163    }
164
165    /**
166     * Parses {@code numOfBlocks} optional header blocks from {@code sr} into a map.
167     *
168     * @param sr source reader, positioned at the first optional header
169     * @param numOfBlocks number of optional headers to consume
170     * @return a map from header identifier to header value, in iteration order
171     */
172    protected static Map<String, String> parseOptionalHeader(StringReader sr, int numOfBlocks) {
173        Map<String, String> ret = new LinkedHashMap<>();
174        int cnt = numOfBlocks;
175        String hbi;
176        int len;
177        String hdata;
178        while (cnt-- > 0) {
179            hbi = readString(sr, SIZE_OPTHDR_ID);
180            len = Integer.valueOf(readString(sr, SIZE_OPTHDR_LENGTH), 0x10);
181            hdata = readString(sr, len - SIZE_OPTHDR_ID - SIZE_OPTHDR_LENGTH);
182            ret.put(hbi, hdata);
183        }
184        return ret;
185    }
186
187    /**
188     * Calculates the on-wire length contribution of the supplied optional headers.
189     *
190     * @param optHdrs optional headers
191     * @return total characters required to encode them, including id and length prefixes
192     */
193    protected static int calcOptionalHeaderLength(Map<String, String> optHdrs) {
194        ToIntFunction<String> entryLength = e -> {
195                int l = SIZE_OPTHDR_ID + SIZE_OPTHDR_LENGTH;
196                if (e != null)
197                    l += e.length();
198
199                return l;
200        };
201        Collection<String> c = optHdrs.values();
202        return c.stream().mapToInt(entryLength).sum();
203    }
204
205    /**
206     * Parses a key-block string into a populated {@link SecureKeyBlock}.
207     *
208     * @param data raw key-block characters (header, optional headers, encrypted key, MAC)
209     * @return the parsed key block
210     * @throws IllegalArgumentException if {@code data} is shorter than the fixed header or otherwise malformed
211     */
212    public SecureKeyBlock build(CharSequence data) throws IllegalArgumentException {
213        Objects.requireNonNull(data, "The key block data cannot be null");
214        SecureKeyBlock skb = new SecureKeyBlock();
215        String keyblock = data.toString();
216        if (keyblock.length() < SIZE_HEADER)
217            throw new IllegalArgumentException("The key block data cannot be shorter than 16");
218
219        StringReader sr = new StringReader(data.toString());
220        skb.keyBlockVersion = readChar(sr);
221        skb.keyBlockLength = Integer.valueOf(readString(sr, SIZE_KEYBLOCK_LENGTH));
222        String ku = readString(sr, SIZE_KEYUSAGE);
223        skb.keyUsage = ExtKeyUsage.valueOfByCode(ku);
224        skb.algorithm = Algorithm.valueOfByCode(readChar(sr));
225        skb.modeOfUse = ModeOfUse.valueOfByCode(readChar(sr));
226        skb.keyVersion = readString(sr, SIZE_KEY_VERSION);
227        skb.exportability = Exportability.valueOfByCode(readChar(sr));
228        int numOfBlocks = Integer.valueOf(readString(sr, SIZE_NUMOFOPTHDR));
229        skb.reserved = readString(sr, SIZE_RESERVED);
230        skb.optionalHeaders = parseOptionalHeader(sr, numOfBlocks);
231        int consumed = SIZE_HEADER + calcOptionalHeaderLength(skb.getOptionalHeaders());
232
233        if (skb.getKeyBlockLength() <= consumed)
234            // it can be but it should not occur
235            return skb;
236
237        int remain = skb.getKeyBlockLength() - consumed;
238        int macLen = getMACLength(skb);
239        String keyEnc = readString(sr, remain - macLen);
240        if (!keyEnc.isEmpty())
241            skb.setKeyBytes(ISOUtil.hex2byte(keyEnc));
242
243        String mac = readString(sr, macLen);
244        skb.keyBlockMAC = ISOUtil.hex2byte(mac);
245        return skb;
246    }
247
248    /**
249     * Serializes a {@link SecureKeyBlock} into its on-wire string form.
250     *
251     * @param skb the key block to serialize
252     * @return the encoded key-block string (header + optional headers + encrypted key + MAC)
253     */
254    public String toKeyBlock(SecureKeyBlock skb) {
255        StringBuilder sb = new StringBuilder();
256        sb.append(skb.getKeyBlockVersion());
257        sb.append(String.format("%04d", skb.getKeyBlockLength()));
258        sb.append(skb.getKeyUsage().getCode());
259        sb.append(skb.getAlgorithm().getCode());
260        sb.append(skb.getModeOfUse().getCode());
261        sb.append(skb.getKeyVersion());
262        sb.append(skb.getExportability().getCode());
263
264        Map<String, String> optHdr = skb.getOptionalHeaders();
265        sb.append(String.format("%02d", optHdr.size()));
266        sb.append(skb.getReserved());
267
268        for (Entry<String, String> ent : optHdr.entrySet()) {
269            sb.append(ent.getKey());
270            sb.append(String.format("%02X", ent.getValue().length() + SIZE_OPTHDR_ID + SIZE_OPTHDR_LENGTH));
271            sb.append(ent.getValue());
272        }
273
274        byte[] b = skb.getKeyBytes();
275        if (b != null)
276            sb.append(ISOUtil.hexString(b));
277
278        b = skb.getKeyBlockMAC();
279        if (b != null)
280            sb.append(ISOUtil.hexString(skb.getKeyBlockMAC()));
281
282        return sb.toString();
283    }
284
285}