View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.wss4j.common.util;
21  
22  import org.apache.wss4j.common.ext.WSSecurityException;
23  import org.apache.xml.security.algorithms.JCEMapper;
24  import org.apache.xml.security.encryption.XMLCipher;
25  import org.apache.xml.security.signature.XMLSignature;
26  import org.apache.xml.security.utils.JavaUtils;
27  
28  import javax.crypto.Cipher;
29  import javax.crypto.KeyGenerator;
30  import javax.crypto.NoSuchPaddingException;
31  import javax.crypto.SecretKey;
32  import javax.crypto.spec.SecretKeySpec;
33  import java.security.MessageDigest;
34  import java.security.NoSuchAlgorithmException;
35  import java.security.NoSuchProviderException;
36  import java.util.HashMap;
37  import java.util.Map;
38  
39  public final class KeyUtils {
40      private static final org.slf4j.Logger LOG =
41              org.slf4j.LoggerFactory.getLogger(KeyUtils.class);
42      private static final int MAX_SYMMETRIC_KEY_SIZE = 1024;
43      private static final Map<String, Integer> DEFAULT_DERIVED_KEY_LENGTHS = new HashMap<>();
44  
45      public static final String RSA_ECB_OAEPWITH_SHA1_AND_MGF1_PADDING = "RSA/ECB/OAEPWithSHA1AndMGF1Padding";
46  
47      /**
48       * A cached MessageDigest object
49       */
50      private static MessageDigest digest;
51  
52      static {
53          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_NOT_RECOMMENDED_MD5, 128);
54          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_RIPEMD160, 160);
55          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_SHA1, 160);
56          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_SHA224, 224);
57          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_SHA256, 256);
58          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_SHA384, 384);
59          DEFAULT_DERIVED_KEY_LENGTHS.put(XMLSignature.ALGO_ID_MAC_HMAC_SHA512, 512);
60      }
61  
62      private KeyUtils() {
63          // complete
64      }
65  
66      /**
67       * Returns the length of the key in # of bytes. For the HMAC algorithms it guesses a default value that can be used
68       * based on the algorithm.
69       *
70       * @param algorithm the URI of the algorithm. See http://www.w3.org/TR/xmlenc-core1/
71       * @return the key length
72       */
73      public static int getKeyLength(String algorithm) throws WSSecurityException {
74          if (algorithm == null) {
75              return 0;
76          }
77  
78          int size = JCEMapper.getKeyLengthFromURI(algorithm);
79          if (size == 0 && DEFAULT_DERIVED_KEY_LENGTHS.containsKey(algorithm)) {
80              // Use a default derived key length for algorithms such as HMAC-SHA1, if none is specified
81              size = DEFAULT_DERIVED_KEY_LENGTHS.get(algorithm);
82          }
83  
84          return size / 8;
85      }
86  
87      /**
88       * Convert the raw key bytes into a SecretKey object of type algorithm.
89       */
90      public static SecretKey prepareSecretKey(String algorithm, byte[] rawKey) {
91          // Do an additional check on the keysize required by the encryption algorithm
92          int size = 0;
93          try {
94              size = JCEMapper.getKeyLengthFromURI(algorithm) / 8;
95          } catch (Exception e) {
96              // ignore - some unknown (to JCEMapper) encryption algorithm
97              LOG.debug(e.getMessage());
98          }
99          String keyAlgorithm = JCEMapper.getJCEKeyAlgorithmFromURI(algorithm);
100         SecretKeySpec keySpec;
101         if (size > 0 && !algorithm.endsWith("gcm") && !algorithm.contains("hmac-")) {
102             keySpec =
103                 new SecretKeySpec(
104                     rawKey, 0, rawKey.length > size ? size : rawKey.length, keyAlgorithm
105                 );
106         } else if (rawKey.length > MAX_SYMMETRIC_KEY_SIZE) {
107             // Prevent a possible attack where a huge secret key is specified
108             keySpec =
109                 new SecretKeySpec(
110                     rawKey, 0, MAX_SYMMETRIC_KEY_SIZE, keyAlgorithm
111                 );
112         } else {
113             keySpec = new SecretKeySpec(rawKey, keyAlgorithm);
114         }
115         return keySpec;
116     }
117 
118     public static KeyGenerator getKeyGenerator(String algorithm) throws WSSecurityException {
119         try {
120             //
121             // Assume AES as default, so initialize it
122             //
123             String keyAlgorithm = JCEMapper.getJCEKeyAlgorithmFromURI(algorithm);
124             if (keyAlgorithm == null || keyAlgorithm.length() == 0) {
125                 keyAlgorithm = JCEMapper.translateURItoJCEID(algorithm);
126             }
127             KeyGenerator keyGen = KeyGenerator.getInstance(keyAlgorithm);
128             if (algorithm.equalsIgnoreCase(XMLCipher.AES_128)
129                 || algorithm.equalsIgnoreCase(XMLCipher.AES_128_GCM)) {
130                 keyGen.init(128);
131             } else if (algorithm.equalsIgnoreCase(XMLCipher.AES_192)
132                 || algorithm.equalsIgnoreCase(XMLCipher.AES_192_GCM)) {
133                 keyGen.init(192);
134             } else if (algorithm.equalsIgnoreCase(XMLCipher.AES_256)
135                 || algorithm.equalsIgnoreCase(XMLCipher.AES_256_GCM)) {
136                 keyGen.init(256);
137             }
138             return keyGen;
139         } catch (NoSuchAlgorithmException e) {
140             throw new WSSecurityException(
141                 WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, e
142             );
143         }
144     }
145 
146     /**
147      * Translate the "cipherAlgo" URI to a JCE ID, and return a javax.crypto.Cipher instance
148      * of this type.
149      * @param cipherAlgo The cipher in it's WSS URI form,
150      *                   ref. https://www.w3.org/TR/xmlenc-core1/#sec-Algorithms
151      */
152     public static Cipher getCipherInstance(String cipherAlgo)
153         throws WSSecurityException {
154         return getCipherInstance(cipherAlgo, null);
155     }
156 
157     /**
158      * Translate the "cipherAlgo" URI to a JCE ID, and request a javax.crypto.Cipher instance
159      * of this type from the given provider.
160      *
161      * @param cipherAlgo The cipher in it's WSS URI form, ref. https://www.w3.org/TR/xmlenc-core1/#sec-Algorithms
162      * @param provider   The provider which shall instantiate the cipher.
163      */
164     public static Cipher getCipherInstance(String cipherAlgo, String provider)
165             throws WSSecurityException {
166         String keyAlgorithm = JCEMapper.translateURItoJCEID(cipherAlgo);
167         if (keyAlgorithm == null) {
168             throw new WSSecurityException(
169                     WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, "unsupportedKeyTransp",
170                     new Object[]{"No such algorithm: \"" + cipherAlgo + "\""});
171         }
172 
173         if (provider == null) {
174             provider = JCEMapper.getProviderId();
175         } else {
176             JavaUtils.checkRegisterPermission();
177         }
178 
179         try {
180             if (provider == null) {
181                 return Cipher.getInstance(keyAlgorithm);
182             } else {
183                 return Cipher.getInstance(keyAlgorithm, provider);
184             }
185         } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
186             if (XMLCipher.RSA_OAEP.equals(cipherAlgo)) {
187                 // Check to see if an RSA OAEP MGF-1 with SHA-1 algorithm was requested
188                 // Some JCE implementations don't support RSA/ECB/OAEPPadding (e.g. nCipherKM of Thales)
189                 try {
190                     if (provider == null) {
191                         return Cipher.getInstance(RSA_ECB_OAEPWITH_SHA1_AND_MGF1_PADDING);
192                     } else {
193                         return Cipher.getInstance(RSA_ECB_OAEPWITH_SHA1_AND_MGF1_PADDING, provider);
194                     }
195                 } catch (NoSuchProviderException ex1) {
196                     throw new WSSecurityException(
197                         WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, ex1, "unsupportedKeyTransp",
198                         new Object[]{
199                             "No such provider \"" + JCEMapper.getProviderId() + "\" for \""
200                                 + RSA_ECB_OAEPWITH_SHA1_AND_MGF1_PADDING + "\""
201                         });
202                 } catch (NoSuchPaddingException ex1) {
203                     throw new WSSecurityException(
204                         WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, e, "unsupportedKeyTransp",
205                         new Object[]{"No such padding: \"" + RSA_ECB_OAEPWITH_SHA1_AND_MGF1_PADDING + "\""});
206                 } catch (NoSuchAlgorithmException ex1) {
207                     throw new WSSecurityException(
208                         WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, e, "unsupportedKeyTransp",
209                         new Object[]{"No such algorithm: \"" + RSA_ECB_OAEPWITH_SHA1_AND_MGF1_PADDING + "\""});
210                 }
211             } else {
212                 if (e instanceof NoSuchAlgorithmException) {    //NOPMD
213                     throw new WSSecurityException(
214                         WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, e, "unsupportedKeyTransp",
215                         new Object[]{"No such algorithm: \"" + keyAlgorithm + "\""});
216                 } else {
217                     throw new WSSecurityException(
218                         WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, e, "unsupportedKeyTransp",
219                         new Object[]{"No such padding: \"" + keyAlgorithm + "\""});
220                 }
221             }
222         } catch (NoSuchProviderException ex) {
223             throw new WSSecurityException(
224                 WSSecurityException.ErrorCode.UNSUPPORTED_ALGORITHM, ex, "unsupportedKeyTransp",
225                 new Object[]{"No such provider \"" + JCEMapper.getProviderId() + "\" for \"" + keyAlgorithm + "\""});
226         }
227     }
228 
229     /**
230      * Generate a (SHA1) digest of the input bytes. The MessageDigest instance that backs this
231      * method is cached for efficiency.
232      * @param inputBytes the bytes to digest
233      * @return the digest of the input bytes
234      * @throws WSSecurityException
235      */
236     public static synchronized byte[] generateDigest(byte[] inputBytes) throws WSSecurityException {
237         try {
238             if (digest == null) {
239                 digest = MessageDigest.getInstance("SHA-1");
240             }
241             return digest.digest(inputBytes);
242         } catch (Exception e) {
243             throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, e, "empty",
244                                           new Object[] {"Error in generating digest"}
245             );
246         }
247     }
248 }