View Javadoc

1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.ssl;
18  
19  import io.netty.util.internal.PlatformDependent;
20  import io.netty.util.internal.logging.InternalLogger;
21  import io.netty.util.internal.logging.InternalLoggerFactory;
22  
23  import java.util.HashMap;
24  import java.util.Map;
25  import java.util.concurrent.ConcurrentMap;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  /**
30   * Converts a Java cipher suite string to an OpenSSL cipher suite string and vice versa.
31   *
32   * @see <a href="http://en.wikipedia.org/wiki/Cipher_suite">Wikipedia page about cipher suite</a>
33   */
34  final class CipherSuiteConverter {
35  
36      private static final InternalLogger logger = InternalLoggerFactory.getInstance(CipherSuiteConverter.class);
37  
38      /**
39       * A_B_WITH_C_D, where:
40       *
41       * A - TLS or SSL (protocol)
42       * B - handshake algorithm (key exchange and authentication algorithms to be precise)
43       * C - bulk cipher
44       * D - HMAC algorithm
45       *
46       * This regular expression assumes that:
47       *
48       * 1) A is always TLS or SSL, and
49       * 2) D is always a single word.
50       */
51      private static final Pattern JAVA_CIPHERSUITE_PATTERN =
52              Pattern.compile("^(?:TLS|SSL)_((?:(?!_WITH_).)+)_WITH_(.*)_(.*)$");
53  
54      /**
55       * A-B-C, where:
56       *
57       * A - handshake algorithm (key exchange and authentication algorithms to be precise)
58       * B - bulk cipher
59       * C - HMAC algorithm
60       *
61       * This regular expression assumes that:
62       *
63       * 1) A has some deterministic pattern as shown below, and
64       * 2) C is always a single word
65       */
66      private static final Pattern OPENSSL_CIPHERSUITE_PATTERN =
67              // Be very careful not to break the indentation while editing.
68              Pattern.compile(
69                      "^(?:(" + // BEGIN handshake algorithm
70                          "(?:(?:EXP-)?" +
71                              "(?:" +
72                                  "(?:DHE|EDH|ECDH|ECDHE|SRP|RSA)-(?:DSS|RSA|ECDSA|PSK)|" +
73                                  "(?:ADH|AECDH|KRB5|PSK|SRP)" +
74                              ')' +
75                          ")|" +
76                          "EXP" +
77                      ")-)?" +  // END handshake algorithm
78                      "(.*)-(.*)$");
79  
80      private static final Pattern JAVA_AES_CBC_PATTERN = Pattern.compile("^(AES)_([0-9]+)_CBC$");
81      private static final Pattern JAVA_AES_PATTERN = Pattern.compile("^(AES)_([0-9]+)_(.*)$");
82      private static final Pattern OPENSSL_AES_CBC_PATTERN = Pattern.compile("^(AES)([0-9]+)$");
83      private static final Pattern OPENSSL_AES_PATTERN = Pattern.compile("^(AES)([0-9]+)-(.*)$");
84  
85      /**
86       * Java-to-OpenSSL cipher suite conversion map
87       * Note that the Java cipher suite has the protocol prefix (TLS_, SSL_)
88       */
89      private static final ConcurrentMap<String, String> j2o = PlatformDependent.newConcurrentHashMap();
90  
91      /**
92       * OpenSSL-to-Java cipher suite conversion map.
93       * Note that one OpenSSL cipher suite can be converted to more than one Java cipher suites because
94       * a Java cipher suite has the protocol name prefix (TLS_, SSL_)
95       */
96      private static final ConcurrentMap<String, Map<String, String>> o2j = PlatformDependent.newConcurrentHashMap();
97  
98      /**
99       * Clears the cache for testing purpose.
100      */
101     static void clearCache() {
102         j2o.clear();
103         o2j.clear();
104     }
105 
106     /**
107      * Tests if the specified key-value pair has been cached in Java-to-OpenSSL cache.
108      */
109     static boolean isJ2OCached(String key, String value) {
110         return value.equals(j2o.get(key));
111     }
112 
113     /**
114      * Tests if the specified key-value pair has been cached in OpenSSL-to-Java cache.
115      */
116     static boolean isO2JCached(String key, String protocol, String value) {
117         Map<String, String> p2j = o2j.get(key);
118         if (p2j == null) {
119             return false;
120         } else {
121             return value.equals(p2j.get(protocol));
122         }
123     }
124 
125     /**
126      * Converts the specified Java cipher suites to the colon-separated OpenSSL cipher suite specification.
127      */
128     static String toOpenSsl(Iterable<String> javaCipherSuites) {
129         final StringBuilder buf = new StringBuilder();
130         for (String c: javaCipherSuites) {
131             if (c == null) {
132                 break;
133             }
134 
135             String converted = toOpenSsl(c);
136             if (converted != null) {
137                 c = converted;
138             }
139 
140             buf.append(c);
141             buf.append(':');
142         }
143 
144         if (buf.length() > 0) {
145             buf.setLength(buf.length() - 1);
146             return buf.toString();
147         } else {
148             return "";
149         }
150     }
151 
152     /**
153      * Converts the specified Java cipher suite to its corresponding OpenSSL cipher suite name.
154      *
155      * @return {@code null} if the conversion has failed
156      */
157     static String toOpenSsl(String javaCipherSuite) {
158         String converted = j2o.get(javaCipherSuite);
159         if (converted != null) {
160             return converted;
161         } else {
162             return cacheFromJava(javaCipherSuite);
163         }
164     }
165 
166     private static String cacheFromJava(String javaCipherSuite) {
167         String openSslCipherSuite = toOpenSslUncached(javaCipherSuite);
168         if (openSslCipherSuite == null) {
169             return null;
170         }
171 
172         // Cache the mapping.
173         j2o.putIfAbsent(javaCipherSuite, openSslCipherSuite);
174 
175         // Cache the reverse mapping after stripping the protocol prefix (TLS_ or SSL_)
176         final String javaCipherSuiteSuffix = javaCipherSuite.substring(4);
177         Map<String, String> p2j = new HashMap<String, String>(4);
178         p2j.put("", javaCipherSuiteSuffix);
179         p2j.put("SSL", "SSL_" + javaCipherSuiteSuffix);
180         p2j.put("TLS", "TLS_" + javaCipherSuiteSuffix);
181         o2j.put(openSslCipherSuite, p2j);
182 
183         logger.debug("Cipher suite mapping: {} => {}", javaCipherSuite, openSslCipherSuite);
184 
185         return openSslCipherSuite;
186     }
187 
188     static String toOpenSslUncached(String javaCipherSuite) {
189         Matcher m = JAVA_CIPHERSUITE_PATTERN.matcher(javaCipherSuite);
190         if (!m.matches()) {
191             return null;
192         }
193 
194         String handshakeAlgo = toOpenSslHandshakeAlgo(m.group(1));
195         String bulkCipher = toOpenSslBulkCipher(m.group(2));
196         String hmacAlgo = toOpenSslHmacAlgo(m.group(3));
197         if (handshakeAlgo.isEmpty()) {
198             return bulkCipher + '-' + hmacAlgo;
199         } else if (bulkCipher.contains("CHACHA20")) {
200             return handshakeAlgo + '-' + bulkCipher;
201         } else {
202             return handshakeAlgo + '-' + bulkCipher + '-' + hmacAlgo;
203         }
204     }
205 
206     private static String toOpenSslHandshakeAlgo(String handshakeAlgo) {
207         final boolean export = handshakeAlgo.endsWith("_EXPORT");
208         if (export) {
209             handshakeAlgo = handshakeAlgo.substring(0, handshakeAlgo.length() - 7);
210         }
211 
212         if ("RSA".equals(handshakeAlgo)) {
213             handshakeAlgo = "";
214         } else if (handshakeAlgo.endsWith("_anon")) {
215             handshakeAlgo = 'A' + handshakeAlgo.substring(0, handshakeAlgo.length() - 5);
216         }
217 
218         if (export) {
219             if (handshakeAlgo.isEmpty()) {
220                 handshakeAlgo = "EXP";
221             } else {
222                 handshakeAlgo = "EXP-" + handshakeAlgo;
223             }
224         }
225 
226         return handshakeAlgo.replace('_', '-');
227     }
228 
229     private static String toOpenSslBulkCipher(String bulkCipher) {
230         if (bulkCipher.startsWith("AES_")) {
231             Matcher m = JAVA_AES_CBC_PATTERN.matcher(bulkCipher);
232             if (m.matches()) {
233                 return m.replaceFirst("$1$2");
234             }
235 
236             m = JAVA_AES_PATTERN.matcher(bulkCipher);
237             if (m.matches()) {
238                 return m.replaceFirst("$1$2-$3");
239             }
240         }
241 
242         if ("3DES_EDE_CBC".equals(bulkCipher)) {
243             return "DES-CBC3";
244         }
245 
246         if ("RC4_128".equals(bulkCipher) || "RC4_40".equals(bulkCipher)) {
247             return "RC4";
248         }
249 
250         if ("DES40_CBC".equals(bulkCipher) || "DES_CBC_40".equals(bulkCipher)) {
251             return "DES-CBC";
252         }
253 
254         if ("RC2_CBC_40".equals(bulkCipher)) {
255             return "RC2-CBC";
256         }
257 
258         return bulkCipher.replace('_', '-');
259     }
260 
261     private static String toOpenSslHmacAlgo(String hmacAlgo) {
262         // Java and OpenSSL use the same algorithm names for:
263         //
264         //   * SHA
265         //   * SHA256
266         //   * MD5
267         //
268         return hmacAlgo;
269     }
270 
271     /**
272      * Convert from OpenSSL cipher suite name convention to java cipher suite name convention.
273      * @param openSslCipherSuite An OpenSSL cipher suite name.
274      * @param protocol The cryptographic protocol (i.e. SSL, TLS, ...).
275      * @return The translated cipher suite name according to java conventions. This will not be {@code null}.
276      */
277     static String toJava(String openSslCipherSuite, String protocol) {
278         Map<String, String> p2j = o2j.get(openSslCipherSuite);
279         if (p2j == null) {
280             p2j = cacheFromOpenSsl(openSslCipherSuite);
281             // This may happen if this method is queried when OpenSSL doesn't yet have a cipher setup. It will return
282             // "(NONE)" in this case.
283             if (p2j == null) {
284                 return null;
285             }
286         }
287 
288         String javaCipherSuite = p2j.get(protocol);
289         if (javaCipherSuite == null) {
290             javaCipherSuite = protocol + '_' + p2j.get("");
291         }
292 
293         return javaCipherSuite;
294     }
295 
296     private static Map<String, String> cacheFromOpenSsl(String openSslCipherSuite) {
297         String javaCipherSuiteSuffix = toJavaUncached(openSslCipherSuite);
298         if (javaCipherSuiteSuffix == null) {
299             return null;
300         }
301 
302         final String javaCipherSuiteSsl = "SSL_" + javaCipherSuiteSuffix;
303         final String javaCipherSuiteTls = "TLS_" + javaCipherSuiteSuffix;
304 
305         // Cache the mapping.
306         final Map<String, String> p2j = new HashMap<String, String>(4);
307         p2j.put("", javaCipherSuiteSuffix);
308         p2j.put("SSL", javaCipherSuiteSsl);
309         p2j.put("TLS", javaCipherSuiteTls);
310         o2j.putIfAbsent(openSslCipherSuite, p2j);
311 
312         // Cache the reverse mapping after adding the protocol prefix (TLS_ or SSL_)
313         j2o.putIfAbsent(javaCipherSuiteTls, openSslCipherSuite);
314         j2o.putIfAbsent(javaCipherSuiteSsl, openSslCipherSuite);
315 
316         logger.debug("Cipher suite mapping: {} => {}", javaCipherSuiteTls, openSslCipherSuite);
317         logger.debug("Cipher suite mapping: {} => {}", javaCipherSuiteSsl, openSslCipherSuite);
318 
319         return p2j;
320     }
321 
322     static String toJavaUncached(String openSslCipherSuite) {
323         Matcher m = OPENSSL_CIPHERSUITE_PATTERN.matcher(openSslCipherSuite);
324         if (!m.matches()) {
325             return null;
326         }
327 
328         String handshakeAlgo = m.group(1);
329         final boolean export;
330         if (handshakeAlgo == null) {
331             handshakeAlgo = "";
332             export = false;
333         } else if (handshakeAlgo.startsWith("EXP-")) {
334             handshakeAlgo = handshakeAlgo.substring(4);
335             export = true;
336         } else if ("EXP".equals(handshakeAlgo)) {
337             handshakeAlgo = "";
338             export = true;
339         } else {
340             export = false;
341         }
342 
343         handshakeAlgo = toJavaHandshakeAlgo(handshakeAlgo, export);
344         String bulkCipher = toJavaBulkCipher(m.group(2), export);
345         String hmacAlgo = toJavaHmacAlgo(m.group(3));
346 
347         String javaCipherSuite = handshakeAlgo + "_WITH_" + bulkCipher + '_' + hmacAlgo;
348         // For historical reasons the CHACHA20 ciphers do not follow OpenSSL's custom naming convention and omits the
349         // HMAC algorithm portion of the name. There is currently no way to derive this information because it is
350         // omitted from the OpenSSL cipher name, but they currently all use SHA256 for HMAC [1].
351         // [1] https://www.openssl.org/docs/man1.1.0/apps/ciphers.html
352         return bulkCipher.contains("CHACHA20") ? javaCipherSuite + "_SHA256" : javaCipherSuite;
353     }
354 
355     private static String toJavaHandshakeAlgo(String handshakeAlgo, boolean export) {
356         if (handshakeAlgo.isEmpty()) {
357             handshakeAlgo = "RSA";
358         } else if ("ADH".equals(handshakeAlgo)) {
359             handshakeAlgo = "DH_anon";
360         } else if ("AECDH".equals(handshakeAlgo)) {
361             handshakeAlgo = "ECDH_anon";
362         }
363 
364         handshakeAlgo = handshakeAlgo.replace('-', '_');
365         if (export) {
366             return handshakeAlgo + "_EXPORT";
367         } else {
368             return handshakeAlgo;
369         }
370     }
371 
372     private static String toJavaBulkCipher(String bulkCipher, boolean export) {
373         if (bulkCipher.startsWith("AES")) {
374             Matcher m = OPENSSL_AES_CBC_PATTERN.matcher(bulkCipher);
375             if (m.matches()) {
376                 return m.replaceFirst("$1_$2_CBC");
377             }
378 
379             m = OPENSSL_AES_PATTERN.matcher(bulkCipher);
380             if (m.matches()) {
381                 return m.replaceFirst("$1_$2_$3");
382             }
383         }
384 
385         if ("DES-CBC3".equals(bulkCipher)) {
386             return "3DES_EDE_CBC";
387         }
388 
389         if ("RC4".equals(bulkCipher)) {
390             if (export) {
391                 return "RC4_40";
392             } else {
393                 return "RC4_128";
394             }
395         }
396 
397         if ("DES-CBC".equals(bulkCipher)) {
398             if (export) {
399                 return "DES_CBC_40";
400             } else {
401                 return "DES_CBC";
402             }
403         }
404 
405         if ("RC2-CBC".equals(bulkCipher)) {
406             if (export) {
407                 return "RC2_CBC_40";
408             } else {
409                 return "RC2_CBC";
410             }
411         }
412 
413         return bulkCipher.replace('-', '_');
414     }
415 
416     private static String toJavaHmacAlgo(String hmacAlgo) {
417         // Java and OpenSSL use the same algorithm names for:
418         //
419         //   * SHA
420         //   * SHA256
421         //   * MD5
422         //
423         return hmacAlgo;
424     }
425 
426     private CipherSuiteConverter() { }
427 }