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}