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    protected static final int SIZE_KEYBLOCK_VERSION    = 1;
040
041    protected static final int SIZE_KEYBLOCK_LENGTH     = 4;
042
043    protected static final int SIZE_KEYUSAGE            = 2;
044
045    protected static final int SIZE_KEY_VERSION         = 2;
046
047    protected static final int SIZE_NUMOFOPTHDR         = 2;
048
049    protected static final int SIZE_RESERVED            = 2;
050
051    protected static final int SIZE_HEADER              = 16;
052
053    protected static final int SIZE_OPTHDR_ID           = 2;
054
055    protected static final int SIZE_OPTHDR_LENGTH       = 2;
056
057    protected static final int SIZE_HEADER_3DES         = 8;
058
059    protected static final int SIZE_HEADER_AES          = 16;
060
061    private final List<Character> versionsWith8CharacterMAC = new ArrayList<>(
062        Arrays.asList(
063             'A' // TR-31:2005 'A' Key block protected using the Key Variant Binding Method
064            ,'B' // TR-31:2010 'B' Key block protected using the Key Derivation Binding Method
065            ,'C' // TR-31:2010 'C' Key block protected using the Key Variant Binding Method
066            ,'0' // Proprietary '0' Key block protected using the 3-DES key
067        )
068    );
069
070    /**
071     * Don't let anyone instantiate this class.
072     */
073    private SecureKeyBlockBuilder() {
074    }
075
076    public static SecureKeyBlockBuilder newBuilder() {
077        return new SecureKeyBlockBuilder();
078    }
079
080    /**
081     * Configure key block versions with 8 digits key block MAC.
082     * <p>
083     * Default 8 digits <i>(4 bytes)</i> key block MAC versions are:
084     * <ul>
085     *   <li>'A' TR-31:2005 Key block protected using the Key Variant Binding Method
086     *   <li>'B' TR-31:2010 Key block protected using the Key Derivation Binding Method
087     *   <li>'C' TR-31:2010 Key block protected using the Key Variant Binding Method
088     *   <li>'0' Proprietary Key block protected using the 3-DES key
089     * </ul>
090     * @param versions the string with versions characters
091     * @return This builder instance
092     */
093    public SecureKeyBlockBuilder with8characterMACVersions(String versions) {
094        Objects.requireNonNull(versions, "The versions with 8 digits MAC cannot be null");
095        versionsWith8CharacterMAC.clear();
096        for (Character ch : versions.toCharArray())
097            versionsWith8CharacterMAC.add(ch);
098
099        return this;
100    }
101
102    protected int getMACLength(SecureKeyBlock skb) {
103        if (versionsWith8CharacterMAC.contains(skb.getKeyBlockVersion()))
104            return SIZE_HEADER_3DES;
105
106        return SIZE_HEADER_AES;
107    }
108
109
110    protected static String readString(StringReader sr, int len) {
111        char[] chars = new char[len];
112        try {
113            sr.read(chars);
114        } catch (IOException ex) {
115            throw new IllegalArgumentException("Problem witch reading key block characters", ex);
116        }
117        return String.valueOf(chars);
118    }
119
120    protected static char readChar(StringReader sr) {
121        try {
122            return (char) sr.read();
123        } catch (IOException ex) {
124            throw new IllegalArgumentException("Problem witch reading key block character", ex);
125        }
126    }
127
128    protected static Map<String, String> parseOptionalHeader(StringReader sr, int numOfBlocks) {
129        Map<String, String> ret = new LinkedHashMap<>();
130        int cnt = numOfBlocks;
131        String hbi;
132        int len;
133        String hdata;
134        while (cnt-- > 0) {
135            hbi = readString(sr, SIZE_OPTHDR_ID);
136            len = Integer.valueOf(readString(sr, SIZE_OPTHDR_LENGTH), 0x10);
137            hdata = readString(sr, len - SIZE_OPTHDR_ID - SIZE_OPTHDR_LENGTH);
138            ret.put(hbi, hdata);
139        }
140        return ret;
141    }
142
143    protected static int calcOptionalHeaderLength(Map<String, String> optHdrs) {
144        ToIntFunction<String> entryLength = e -> {
145                int l = SIZE_OPTHDR_ID + SIZE_OPTHDR_LENGTH;
146                if (e != null)
147                    l += e.length();
148
149                return l;
150        };
151        Collection<String> c = optHdrs.values();
152        return c.stream().mapToInt(entryLength).sum();
153    }
154
155    public SecureKeyBlock build(CharSequence data) throws IllegalArgumentException {
156        Objects.requireNonNull(data, "The key block data cannot be null");
157        SecureKeyBlock skb = new SecureKeyBlock();
158        String keyblock = data.toString();
159        if (keyblock.length() < SIZE_HEADER)
160            throw new IllegalArgumentException("The key block data cannot be shorter than 16");
161
162        StringReader sr = new StringReader(data.toString());
163        skb.keyBlockVersion = readChar(sr);
164        skb.keyBlockLength = Integer.valueOf(readString(sr, SIZE_KEYBLOCK_LENGTH));
165        String ku = readString(sr, SIZE_KEYUSAGE);
166        skb.keyUsage = ExtKeyUsage.valueOfByCode(ku);
167        skb.algorithm = Algorithm.valueOfByCode(readChar(sr));
168        skb.modeOfUse = ModeOfUse.valueOfByCode(readChar(sr));
169        skb.keyVersion = readString(sr, SIZE_KEY_VERSION);
170        skb.exportability = Exportability.valueOfByCode(readChar(sr));
171        int numOfBlocks = Integer.valueOf(readString(sr, SIZE_NUMOFOPTHDR));
172        skb.reserved = readString(sr, SIZE_RESERVED);
173        skb.optionalHeaders = parseOptionalHeader(sr, numOfBlocks);
174        int consumed = SIZE_HEADER + calcOptionalHeaderLength(skb.getOptionalHeaders());
175
176        if (skb.getKeyBlockLength() <= consumed)
177            // it can be but it should not occur
178            return skb;
179
180        int remain = skb.getKeyBlockLength() - consumed;
181        int macLen = getMACLength(skb);
182        String keyEnc = readString(sr, remain - macLen);
183        if (!keyEnc.isEmpty())
184            skb.setKeyBytes(ISOUtil.hex2byte(keyEnc));
185
186        String mac = readString(sr, macLen);
187        skb.keyBlockMAC = ISOUtil.hex2byte(mac);
188        return skb;
189    }
190
191    public String toKeyBlock(SecureKeyBlock skb) {
192        StringBuilder sb = new StringBuilder();
193        sb.append(skb.getKeyBlockVersion());
194        sb.append(String.format("%04d", skb.getKeyBlockLength()));
195        sb.append(skb.getKeyUsage().getCode());
196        sb.append(skb.getAlgorithm().getCode());
197        sb.append(skb.getModeOfUse().getCode());
198        sb.append(skb.getKeyVersion());
199        sb.append(skb.getExportability().getCode());
200
201        Map<String, String> optHdr = skb.getOptionalHeaders();
202        sb.append(String.format("%02d", optHdr.size()));
203        sb.append(skb.getReserved());
204
205        for (Entry<String, String> ent : optHdr.entrySet()) {
206            sb.append(ent.getKey());
207            sb.append(String.format("%02X", ent.getValue().length() + SIZE_OPTHDR_ID + SIZE_OPTHDR_LENGTH));
208            sb.append(ent.getValue());
209        }
210
211        byte[] b = skb.getKeyBytes();
212        if (b != null)
213            sb.append(ISOUtil.hexString(b));
214
215        b = skb.getKeyBlockMAC();
216        if (b != null)
217            sb.append(ISOUtil.hexString(skb.getKeyBlockMAC()));
218
219        return sb.toString();
220    }
221
222}