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