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.jceadapter;
020
021import java.security.InvalidKeyException;
022import java.security.Key;
023import java.security.NoSuchAlgorithmException;
024import java.security.spec.AlgorithmParameterSpec;
025import java.util.Map;
026import java.util.concurrent.ConcurrentHashMap;
027
028import javax.crypto.Cipher;
029import javax.crypto.KeyGenerator;
030import javax.crypto.Mac;
031import javax.crypto.spec.IvParameterSpec;
032import javax.crypto.spec.SecretKeySpec;
033
034import org.jpos.iso.ISOUtil;
035import org.jpos.security.CipherMode;
036import org.jpos.security.SMAdapter;
037import org.jpos.security.Util;
038
039/**
040 * <p>
041 * Provides some higher level methods that are needed by the JCE Security Module, yet they are generic and can be used elsewhere.
042 * </p>
043 * <p>
044 * It depends on the Java<font size=-1><sup>TM</sup></font> Cryptography Extension (JCE).
045 * </p>
046 * 
047 * @author Hani S. Kirollos
048 * @version $Revision$ $Date$
049 */
050@SuppressWarnings("unchecked")
051public class JCEHandler {
052    static final String ALG_DES = "DES";
053    static final String ALG_TRIPLE_DES = "DESede";
054    static final String DES_MODE_ECB = "ECB";
055    static final String DES_MODE_CBC = "CBC";
056    static final String DES_NO_PADDING = "NoPadding";
057    static final Map<MacEngineKey, Mac> macEngines = new ConcurrentHashMap();
058    /**
059     * The JCE provider
060     */
061
062    /**
063     * Generates a clear DES (DESede) key
064     * 
065     * @param keyLength
066     *            the bit length (key size) of the generated key (LENGTH_DES, LENGTH_DES3_2KEY or LENGTH_DES3_3KEY)
067     * @return generated clear DES (or DESede) key
068     * @exception JCEHandlerException
069     */
070    public Key generateDESKey(short keyLength) throws JCEHandlerException {
071        Key generatedClearKey = null;
072        try {
073            KeyGenerator k1;
074            if (keyLength > SMAdapter.LENGTH_DES) {
075                k1 = KeyGenerator.getInstance(ALG_TRIPLE_DES);
076            } else {
077                k1 = KeyGenerator.getInstance(ALG_DES);
078            }
079            generatedClearKey = k1.generateKey();
080            /*
081             * These 3 steps not only enforce correct parity, but also enforces that when keyLength=128, the third key of the triple
082             * DES key is equal to the first key. This is needed because, JCE doesn't differenciate between Triple DES with 2 keys
083             * and Triple DES with 3 keys
084             */
085            byte[] clearKeyBytes = extractDESKeyMaterial(keyLength, generatedClearKey);
086            Util.adjustDESParity(clearKeyBytes);
087            generatedClearKey = formDESKey(keyLength, clearKeyBytes);
088        } catch (Exception e) {
089            if (e instanceof JCEHandlerException)
090                throw (JCEHandlerException) e;
091            else
092                throw new JCEHandlerException(e);
093        }
094        return generatedClearKey;
095    }
096
097    /**
098     * Encrypts (wraps) a clear DES Key, it also sets odd parity before encryption
099     * 
100     * @param keyLength
101     *            bit length (key size) of the clear DES key (LENGTH_DES, LENGTH_DES3_2KEY or LENGTH_DES3_3KEY)
102     * @param clearDESKey
103     *            DES/Triple-DES key whose format is "RAW" (for a DESede with 2 Keys, keyLength = 128 bits, while DESede key with 3
104     *            keys keyLength = 192 bits)
105     * @param encryptingKey
106     *            can be a key of any type (RSA, DES, DESede...)
107     * @return encrypted DES key
108     * @throws JCEHandlerException
109     */
110    public byte[] encryptDESKey(short keyLength, Key clearDESKey, Key encryptingKey) throws JCEHandlerException {
111        byte[] clearKeyBytes = extractDESKeyMaterial(keyLength, clearDESKey);
112        // enforce correct (odd) parity before encrypting the key
113        Util.adjustDESParity(clearKeyBytes);
114        return doCryptStuff(clearKeyBytes, encryptingKey, Cipher.ENCRYPT_MODE);
115    }
116
117    /**
118     * Extracts the DES/DESede key material
119     * 
120     * @param keyLength
121     *            bit length (key size) of the DES key. (LENGTH_DES, LENGTH_DES3_2KEY or LENGTH_DES3_3KEY)
122     * @param clearDESKey
123     *            DES/Triple-DES key whose format is "RAW"
124     * @return encoded key material
125     * @throws JCEHandlerException
126     */
127    protected byte[] extractDESKeyMaterial(short keyLength, Key clearDESKey) throws JCEHandlerException {
128        String keyAlg = clearDESKey.getAlgorithm();
129        String keyFormat = clearDESKey.getFormat();
130        if (keyFormat.compareTo("RAW") != 0) {
131            throw new JCEHandlerException("Unsupported DES key encoding format: " + keyFormat);
132        }
133        if (!keyAlg.startsWith(ALG_DES)) {
134            throw new JCEHandlerException("Unsupported key algorithm: " + keyAlg);
135        }
136        byte[] clearKeyBytes = clearDESKey.getEncoded();
137        clearKeyBytes = ISOUtil.trim(clearKeyBytes, getBytesLength(keyLength));
138        return clearKeyBytes;
139    }
140
141    /**
142     * Decrypts an encrypted DES/Triple-DES key
143     * 
144     * @param keyLength
145     *            bit length (key size) of the DES key to be decrypted. (LENGTH_DES, LENGTH_DES3_2KEY or LENGTH_DES3_3KEY)
146     * @param encryptedDESKey
147     *            the byte[] representing the encrypted key
148     * @param encryptingKey
149     *            can be of any algorithm (RSA, DES, DESede...)
150     * @param checkParity
151     *            if true, the parity of the key is checked
152     * @return clear DES (DESede) Key
153     * @throws JCEHandlerException
154     *             if checkParity==true and the key does not have correct parity
155     */
156    public Key decryptDESKey(short keyLength, byte[] encryptedDESKey, Key encryptingKey, boolean checkParity)
157            throws JCEHandlerException {
158        byte[] clearKeyBytes = doCryptStuff(encryptedDESKey, encryptingKey, Cipher.DECRYPT_MODE);
159        if (checkParity && !Util.isDESParityAdjusted(clearKeyBytes)) {
160            throw new JCEHandlerException("Parity not adjusted");
161        }
162        return formDESKey(keyLength, clearKeyBytes);
163    }
164
165    /**
166     * Forms the clear DES key given its "RAW" encoded bytes Does the inverse of extractDESKeyMaterial
167     * 
168     * @param keyLength
169     *            bit length (key size) of the DES key. (LENGTH_DES, LENGTH_DES3_2KEY or LENGTH_DES3_3KEY)
170     * @param clearKeyBytes
171     *            the RAW DES/Triple-DES key
172     * @return clear key
173     * @throws JCEHandlerException
174     */
175    protected Key formDESKey(short keyLength, byte[] clearKeyBytes) throws JCEHandlerException {
176        Key key = null;
177        switch (keyLength) {
178        case SMAdapter.LENGTH_DES: {
179            key = new SecretKeySpec(clearKeyBytes, ALG_DES);
180        }
181            break;
182        case SMAdapter.LENGTH_DES3_2KEY: {
183            // make it 3 components to work with JCE
184            clearKeyBytes = ISOUtil.concat(clearKeyBytes, 0, getBytesLength(SMAdapter.LENGTH_DES3_2KEY), clearKeyBytes, 0,
185                    getBytesLength(SMAdapter.LENGTH_DES));
186        }
187        case SMAdapter.LENGTH_DES3_3KEY: {
188            key = new SecretKeySpec(clearKeyBytes, ALG_TRIPLE_DES);
189        }
190        }
191        if (key == null)
192            throw new JCEHandlerException("Unsupported DES key length: " + keyLength + " bits");
193        return key;
194    }
195
196    /**
197     * Encrypts data
198     * 
199     * @param data
200     * @param key
201     * @return encrypted data
202     * @exception JCEHandlerException
203     */
204    public byte[] encryptData(byte[] data, Key key) throws JCEHandlerException {
205        return doCryptStuff(data, key, Cipher.ENCRYPT_MODE);
206    }
207
208    /**
209     * Decrypts data
210     * 
211     * @param encryptedData
212     * @param key
213     * @return clear data
214     * @exception JCEHandlerException
215     */
216    public byte[] decryptData(byte[] encryptedData, Key key) throws JCEHandlerException {
217        return doCryptStuff(encryptedData, key, Cipher.DECRYPT_MODE);
218    }
219
220    /**
221     * Encrypts data
222     * 
223     * @param data
224     * @param key
225     * @param iv 8 bytes initial vector
226     * @return encrypted data
227     * @exception JCEHandlerException
228     */
229    public byte[] encryptDataCBC(byte[] data, Key key, byte[] iv) throws JCEHandlerException {
230        return doCryptStuff(data, key, Cipher.ENCRYPT_MODE, CipherMode.CBC, iv);
231    }
232
233    /**
234     * Decrypts data
235     * 
236     * @param encryptedData
237     * @param key
238     * @param iv 8 bytes initial vector
239     * @return clear data
240     * @exception JCEHandlerException
241     */
242    public byte[] decryptDataCBC(byte[] encryptedData, Key key, byte[] iv) throws JCEHandlerException {
243        return doCryptStuff(encryptedData, key, Cipher.DECRYPT_MODE, CipherMode.CBC, iv);
244    }
245
246    /**
247     * Performs cryptographic DES operations (en/de)cryption) in ECB mode using JCE Cipher.
248     * 
249     * @param data
250     * @param key
251     * @param direction {@link Cipher#ENCRYPT_MODE} or {@link Cipher#DECRYPT_MODE}.
252     * @return result of the cryptographic operations
253     * @throws JCEHandlerException
254     */
255    byte[] doCryptStuff(byte[] data, Key key, int direction) throws JCEHandlerException {
256        return doCryptStuff(data, key, direction, CipherMode.ECB, null);
257    }
258
259    /**
260     * Performs cryptographic operations (en/de)cryption using JCE Cipher.
261     * 
262     * @param data
263     * @param key
264     * @param direction {@link Cipher#ENCRYPT_MODE} or {@link Cipher#DECRYPT_MODE}.
265     * @param cipherMode values specified by {@link CipherMode}.
266     * @param iv 8 bytes initial vector. After operation will contain new iv value.
267     * @return result of the cryptographic operations.
268     * @throws JCEHandlerException
269     */
270    byte[] doCryptStuff(byte[] data, Key key, int direction
271            ,CipherMode cipherMode, byte[] iv) throws JCEHandlerException {
272        byte[] result;
273        String transformation = key.getAlgorithm();
274        if (key.getAlgorithm().startsWith(ALG_DES)) {
275            transformation += "/" + cipherMode.name() + "/" + DES_NO_PADDING;
276        }
277        AlgorithmParameterSpec aps = null;
278        try {
279            Cipher c1 = Cipher.getInstance(transformation);
280            if (cipherMode != CipherMode.ECB)
281                aps = new IvParameterSpec(iv);
282            c1.init(direction, key, aps);
283            result = c1.doFinal(data);
284            if (cipherMode != CipherMode.ECB)
285               System.arraycopy(result, result.length-8, iv, 0, iv.length);
286        } catch (Exception e) {
287            throw new JCEHandlerException(e);
288        }
289        return result;
290    }
291
292    /**
293     * Calculates the length of key in bytes
294     * 
295     * @param keyLength
296     *            bit length (key size) of the DES key. (LENGTH_DES, LENGTH_DES3_2KEY or LENGTH_DES3_3KEY)
297     * @return keyLength/8
298     * @throws JCEHandlerException
299     *             if unknown key length
300     */
301    int getBytesLength(short keyLength) throws JCEHandlerException {
302        int bytesLength = 0;
303        switch (keyLength) {
304        case SMAdapter.LENGTH_DES:
305            bytesLength = 8;
306            break;
307        case SMAdapter.LENGTH_DES3_2KEY:
308            bytesLength = 16;
309            break;
310        case SMAdapter.LENGTH_DES3_3KEY:
311            bytesLength = 24;
312            break;
313        default:
314            throw new JCEHandlerException("Unsupported key length: " + keyLength + " bits");
315        }
316        return bytesLength;
317    }
318
319    /**
320     * Helper method used for create or retrieve MAC algorithm from cache
321     * 
322     * @param engine
323     *            object identyifing MAC algorithm
324     * @return Initialized MAC algotithm
325     * @throws org.jpos.security.jceadapter.JCEHandlerException
326     */
327    Mac assignMACEngine(MacEngineKey engine) throws JCEHandlerException {
328        if (macEngines.containsKey(engine)) {
329            return macEngines.get(engine);
330        }
331        // Initalize new MAC engine and store them in macEngines cache
332        Mac mac = null;
333        try {
334            mac = Mac.getInstance(engine.getMacAlgorithm());
335            mac.init(engine.getMacKey());
336        } catch (NoSuchAlgorithmException e) {
337            throw new JCEHandlerException(e);
338        } catch (InvalidKeyException e) {
339            throw new JCEHandlerException(e);
340        }
341        macEngines.put(engine, mac);
342        return mac;
343    }
344
345    /**
346     * Generates MAC (Message Message Authentication Code) for some data.
347     * 
348     * @param data
349     *            the data to be MACed
350     * @param kd
351     *            the key used for MACing
352     * @param macAlgorithm
353     *            MAC algorithm name suitable for {@link Mac#getInstance}
354     * @return the MAC
355     * @throws org.jpos.security.jceadapter.JCEHandlerException
356     */
357    public byte[] generateMAC(byte[] data, Key kd, String macAlgorithm) throws JCEHandlerException {
358        Mac mac = assignMACEngine(new MacEngineKey(macAlgorithm, kd));
359        synchronized (mac) {
360            mac.reset();
361            return mac.doFinal(data);
362        }
363    }
364
365    /**
366     * Class used for indexing MAC algorithms in cache
367     */
368    protected static class MacEngineKey {
369        private final String macAlgorithm;
370        private final Key macKey;
371
372        protected MacEngineKey(String macAlgorithm, Key macKey) {
373            this.macAlgorithm = macAlgorithm;
374            this.macKey = macKey;
375        }
376
377        public String getMacAlgorithm() {
378            return macAlgorithm;
379        }
380
381        public Key getMacKey() {
382            return macKey;
383        }
384
385        @Override
386        public int hashCode() {
387            final int prime = 31;
388            int result = 1;
389            result = prime * result + (macAlgorithm == null ? 0 : macAlgorithm.hashCode());
390            // Note: class Key does not redefine hashCode - therefore hash on Algorithm only or breaks java equals/hashCode contract
391            return result;
392        }
393
394        @Override
395        public boolean equals(Object obj) {
396            if (this == obj) {
397                return true;
398            }
399            if (obj == null) {
400                return false;
401            }
402            if (getClass() != obj.getClass()) {
403                return false;
404            }
405            MacEngineKey other = (MacEngineKey) obj;
406            if (macAlgorithm == null) {
407                if (other.macAlgorithm != null)
408                    return false;
409            } else if (!macAlgorithm.equals(other.macAlgorithm)) {
410                return false;
411            } else if (macKey != other.macKey) {
412                // Note: class Key does not redefine equals or HashCode - therefore instance equality is all we can do
413                return false;
414            }
415            return true;
416        }
417    }
418}