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    *   https://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.UnstableApi;
21  import io.netty.util.internal.logging.InternalLogger;
22  import io.netty.util.internal.logging.InternalLoggerFactory;
23  
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.Map;
27  import java.util.concurrent.ConcurrentMap;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  
31  import static java.util.Collections.singletonMap;
32  
33  /**
34   * Converts a Java cipher suite string to an OpenSSL cipher suite string and vice versa.
35   *
36   * @see <a href="https://en.wikipedia.org/wiki/Cipher_suite">Wikipedia page about cipher suite</a>
37   */
38  @UnstableApi
39  public final class CipherSuiteConverter {
40  
41      private static final InternalLogger logger = InternalLoggerFactory.getInstance(CipherSuiteConverter.class);
42  
43      /**
44       * A_B_WITH_C_D, where:
45       *
46       * A - TLS or SSL (protocol)
47       * B - handshake algorithm (key exchange and authentication algorithms to be precise)
48       * C - bulk cipher
49       * D - HMAC algorithm
50       *
51       * This regular expression assumes that:
52       *
53       * 1) A is always TLS or SSL, and
54       * 2) D is always a single word.
55       */
56      private static final Pattern JAVA_CIPHERSUITE_PATTERN =
57              Pattern.compile("^(?:TLS|SSL)_((?:(?!_WITH_).)+)_WITH_(.*)_(.*)$");
58  
59      /**
60       * A-B-C, where:
61       *
62       * A - handshake algorithm (key exchange and authentication algorithms to be precise)
63       * B - bulk cipher
64       * C - HMAC algorithm
65       *
66       * This regular expression assumes that:
67       *
68       * 1) A has some deterministic pattern as shown below, and
69       * 2) C is always a single word
70       */
71      private static final Pattern OPENSSL_CIPHERSUITE_PATTERN =
72              // Be very careful not to break the indentation while editing.
73              Pattern.compile(
74                      "^(?:(" + // BEGIN handshake algorithm
75                          "(?:(?:EXP-)?" +
76                              "(?:" +
77                                  "(?:DHE|EDH|ECDH|ECDHE|SRP|RSA)-(?:DSS|RSA|ECDSA|PSK)|" +
78                                  "(?:ADH|AECDH|KRB5|PSK|SRP)" +
79                              ')' +
80                          ")|" +
81                          "EXP" +
82                      ")-)?" +  // END handshake algorithm
83                      "(.*)-(.*)$");
84  
85      private static final Pattern JAVA_AES_CBC_PATTERN = Pattern.compile("^(AES)_([0-9]+)_CBC$");
86      private static final Pattern JAVA_AES_PATTERN = Pattern.compile("^(AES)_([0-9]+)_(.*)$");
87      private static final Pattern OPENSSL_AES_CBC_PATTERN = Pattern.compile("^(AES)([0-9]+)$");
88      private static final Pattern OPENSSL_AES_PATTERN = Pattern.compile("^(AES)([0-9]+)-(.*)$");
89  
90      /**
91       * Used to store nullable values in a CHM
92       */
93      private static final class CachedValue {
94  
95          private static final CachedValue NULL = new CachedValue(null);
96  
97          static CachedValue of(String value) {
98              return value != null ? new CachedValue(value) : NULL;
99          }
100 
101         final String value;
102         private CachedValue(String value) {
103             this.value = value;
104         }
105     }
106 
107     /**
108      * Java-to-OpenSSL cipher suite conversion map
109      * Note that the Java cipher suite has the protocol prefix (TLS_, SSL_)
110      */
111     private static final ConcurrentMap<String, CachedValue> j2o = PlatformDependent.newConcurrentHashMap();
112 
113     /**
114      * OpenSSL-to-Java cipher suite conversion map.
115      * Note that one OpenSSL cipher suite can be converted to more than one Java cipher suites because
116      * a Java cipher suite has the protocol name prefix (TLS_, SSL_)
117      */
118     private static final ConcurrentMap<String, Map<String, String>> o2j = PlatformDependent.newConcurrentHashMap();
119 
120     private static final Map<String, String> j2oTls13;
121     private static final Map<String, Map<String, String>> o2jTls13;
122 
123     static {
124         Map<String, String> j2oTls13Map = new HashMap<String, String>();
125         j2oTls13Map.put("TLS_AES_128_GCM_SHA256", "AEAD-AES128-GCM-SHA256");
126         j2oTls13Map.put("TLS_AES_256_GCM_SHA384", "AEAD-AES256-GCM-SHA384");
127         j2oTls13Map.put("TLS_CHACHA20_POLY1305_SHA256", "AEAD-CHACHA20-POLY1305-SHA256");
128         j2oTls13 = Collections.unmodifiableMap(j2oTls13Map);
129 
130         Map<String, Map<String, String>> o2jTls13Map = new HashMap<String, Map<String, String>>();
131         o2jTls13Map.put("TLS_AES_128_GCM_SHA256", singletonMap("TLS", "TLS_AES_128_GCM_SHA256"));
132         o2jTls13Map.put("TLS_AES_256_GCM_SHA384", singletonMap("TLS", "TLS_AES_256_GCM_SHA384"));
133         o2jTls13Map.put("TLS_CHACHA20_POLY1305_SHA256", singletonMap("TLS", "TLS_CHACHA20_POLY1305_SHA256"));
134         o2jTls13Map.put("AEAD-AES128-GCM-SHA256", singletonMap("TLS", "TLS_AES_128_GCM_SHA256"));
135         o2jTls13Map.put("AEAD-AES256-GCM-SHA384", singletonMap("TLS", "TLS_AES_256_GCM_SHA384"));
136         o2jTls13Map.put("AEAD-CHACHA20-POLY1305-SHA256", singletonMap("TLS", "TLS_CHACHA20_POLY1305_SHA256"));
137         o2jTls13 = Collections.unmodifiableMap(o2jTls13Map);
138     }
139 
140     /**
141      * Clears the cache for testing purpose.
142      */
143     static void clearCache() {
144         j2o.clear();
145         o2j.clear();
146     }
147 
148     /**
149      * Tests if the specified key-value pair has been cached in Java-to-OpenSSL cache.
150      */
151     static boolean isJ2OCached(String key, String value) {
152         CachedValue cached = j2o.get(key);
153         return cached != null && value.equals(cached.value);
154     }
155 
156     /**
157      * Tests if the specified key-value pair has been cached in OpenSSL-to-Java cache.
158      */
159     static boolean isO2JCached(String key, String protocol, String value) {
160         Map<String, String> p2j = o2j.get(key);
161         if (p2j == null) {
162             return false;
163         } else {
164             return value.equals(p2j.get(protocol));
165         }
166     }
167 
168     /**
169      * Converts the specified Java cipher suite to its corresponding OpenSSL cipher suite name.
170      *
171      * @return {@code null} if the conversion has failed
172      */
173     public static String toOpenSsl(String javaCipherSuite, boolean boringSSL) {
174         CachedValue converted = j2o.get(javaCipherSuite);
175         if (converted != null) {
176             return converted.value;
177         }
178         return cacheFromJava(javaCipherSuite, boringSSL);
179     }
180 
181     private static String cacheFromJava(String javaCipherSuite, boolean boringSSL) {
182         String converted = j2oTls13.get(javaCipherSuite);
183         if (converted != null) {
184             return boringSSL ? converted : javaCipherSuite;
185         }
186 
187         String openSslCipherSuite = toOpenSslUncached(javaCipherSuite, boringSSL);
188 
189         // Cache the mapping.
190         j2o.putIfAbsent(javaCipherSuite, CachedValue.of(openSslCipherSuite));
191 
192         if (openSslCipherSuite == null) {
193             return null;
194         }
195 
196         // Cache the reverse mapping after stripping the protocol prefix (TLS_ or SSL_)
197         final String javaCipherSuiteSuffix = javaCipherSuite.substring(4);
198         Map<String, String> p2j = new HashMap<String, String>(4);
199         p2j.put("", javaCipherSuiteSuffix);
200         p2j.put("SSL", "SSL_" + javaCipherSuiteSuffix);
201         p2j.put("TLS", "TLS_" + javaCipherSuiteSuffix);
202         o2j.put(openSslCipherSuite, p2j);
203 
204         logger.debug("Cipher suite mapping: {} => {}", javaCipherSuite, openSslCipherSuite);
205 
206         return openSslCipherSuite;
207     }
208 
209     static String toOpenSslUncached(String javaCipherSuite, boolean boringSSL) {
210         String converted = j2oTls13.get(javaCipherSuite);
211         if (converted != null) {
212             return boringSSL ? converted : javaCipherSuite;
213         }
214 
215         Matcher m = JAVA_CIPHERSUITE_PATTERN.matcher(javaCipherSuite);
216         if (!m.matches()) {
217             return null;
218         }
219 
220         String handshakeAlgo = toOpenSslHandshakeAlgo(m.group(1));
221         String bulkCipher = toOpenSslBulkCipher(m.group(2));
222         String hmacAlgo = toOpenSslHmacAlgo(m.group(3));
223         if (handshakeAlgo.isEmpty()) {
224             return bulkCipher + '-' + hmacAlgo;
225         } else if (bulkCipher.contains("CHACHA20")) {
226             return handshakeAlgo + '-' + bulkCipher;
227         } else {
228             return handshakeAlgo + '-' + bulkCipher + '-' + hmacAlgo;
229         }
230     }
231 
232     private static String toOpenSslHandshakeAlgo(String handshakeAlgo) {
233         final boolean export = handshakeAlgo.endsWith("_EXPORT");
234         if (export) {
235             handshakeAlgo = handshakeAlgo.substring(0, handshakeAlgo.length() - 7);
236         }
237 
238         if ("RSA".equals(handshakeAlgo)) {
239             handshakeAlgo = "";
240         } else if (handshakeAlgo.endsWith("_anon")) {
241             handshakeAlgo = 'A' + handshakeAlgo.substring(0, handshakeAlgo.length() - 5);
242         }
243 
244         if (export) {
245             if (handshakeAlgo.isEmpty()) {
246                 handshakeAlgo = "EXP";
247             } else {
248                 handshakeAlgo = "EXP-" + handshakeAlgo;
249             }
250         }
251 
252         return handshakeAlgo.replace('_', '-');
253     }
254 
255     private static String toOpenSslBulkCipher(String bulkCipher) {
256         if (bulkCipher.startsWith("AES_")) {
257             Matcher m = JAVA_AES_CBC_PATTERN.matcher(bulkCipher);
258             if (m.matches()) {
259                 return m.replaceFirst("$1$2");
260             }
261 
262             m = JAVA_AES_PATTERN.matcher(bulkCipher);
263             if (m.matches()) {
264                 return m.replaceFirst("$1$2-$3");
265             }
266         }
267 
268         if ("3DES_EDE_CBC".equals(bulkCipher)) {
269             return "DES-CBC3";
270         }
271 
272         if ("RC4_128".equals(bulkCipher) || "RC4_40".equals(bulkCipher)) {
273             return "RC4";
274         }
275 
276         if ("DES40_CBC".equals(bulkCipher) || "DES_CBC_40".equals(bulkCipher)) {
277             return "DES-CBC";
278         }
279 
280         if ("RC2_CBC_40".equals(bulkCipher)) {
281             return "RC2-CBC";
282         }
283 
284         return bulkCipher.replace('_', '-');
285     }
286 
287     private static String toOpenSslHmacAlgo(String hmacAlgo) {
288         // Java and OpenSSL use the same algorithm names for:
289         //
290         //   * SHA
291         //   * SHA256
292         //   * MD5
293         //
294         return hmacAlgo;
295     }
296 
297     /**
298      * Convert from OpenSSL cipher suite name convention to java cipher suite name convention.
299      * @param openSslCipherSuite An OpenSSL cipher suite name.
300      * @param protocol The cryptographic protocol (i.e. SSL, TLS, ...).
301      * @return The translated cipher suite name according to java conventions. This will not be {@code null}.
302      */
303     public static String toJava(String openSslCipherSuite, String protocol) {
304         Map<String, String> p2j = o2j.get(openSslCipherSuite);
305         if (p2j == null) {
306             p2j = cacheFromOpenSsl(openSslCipherSuite);
307             // This may happen if this method is queried when OpenSSL doesn't yet have a cipher setup. It will return
308             // "(NONE)" in this case.
309             if (p2j == null) {
310                 return null;
311             }
312         }
313 
314         String javaCipherSuite = p2j.get(protocol);
315         if (javaCipherSuite == null) {
316             String cipher = p2j.get("");
317             if (cipher == null) {
318                 return null;
319             }
320             javaCipherSuite = protocol + '_' + cipher;
321         }
322 
323         return javaCipherSuite;
324     }
325 
326     private static Map<String, String> cacheFromOpenSsl(String openSslCipherSuite) {
327         Map<String, String> converted = o2jTls13.get(openSslCipherSuite);
328         if (converted != null) {
329             return converted;
330         }
331 
332         String javaCipherSuiteSuffix = toJavaUncached0(openSslCipherSuite, false);
333         if (javaCipherSuiteSuffix == null) {
334             return null;
335         }
336 
337         final String javaCipherSuiteSsl = "SSL_" + javaCipherSuiteSuffix;
338         final String javaCipherSuiteTls = "TLS_" + javaCipherSuiteSuffix;
339 
340         // Cache the mapping.
341         final Map<String, String> p2j = new HashMap<String, String>(4);
342         p2j.put("", javaCipherSuiteSuffix);
343         p2j.put("SSL", javaCipherSuiteSsl);
344         p2j.put("TLS", javaCipherSuiteTls);
345         o2j.putIfAbsent(openSslCipherSuite, p2j);
346 
347         // Cache the reverse mapping after adding the protocol prefix (TLS_ or SSL_)
348         CachedValue cachedValue = CachedValue.of(openSslCipherSuite);
349         j2o.putIfAbsent(javaCipherSuiteTls, cachedValue);
350         j2o.putIfAbsent(javaCipherSuiteSsl, cachedValue);
351 
352         logger.debug("Cipher suite mapping: {} => {}", javaCipherSuiteTls, openSslCipherSuite);
353         logger.debug("Cipher suite mapping: {} => {}", javaCipherSuiteSsl, openSslCipherSuite);
354 
355         return p2j;
356     }
357 
358     static String toJavaUncached(String openSslCipherSuite) {
359         return toJavaUncached0(openSslCipherSuite, true);
360     }
361 
362     private static String toJavaUncached0(String openSslCipherSuite, boolean checkTls13) {
363         if (checkTls13) {
364             Map<String, String> converted = o2jTls13.get(openSslCipherSuite);
365             if (converted != null) {
366                 return converted.get("TLS");
367             }
368         }
369 
370         Matcher m = OPENSSL_CIPHERSUITE_PATTERN.matcher(openSslCipherSuite);
371         if (!m.matches()) {
372             return null;
373         }
374 
375         String handshakeAlgo = m.group(1);
376         final boolean export;
377         if (handshakeAlgo == null) {
378             handshakeAlgo = "";
379             export = false;
380         } else if (handshakeAlgo.startsWith("EXP-")) {
381             handshakeAlgo = handshakeAlgo.substring(4);
382             export = true;
383         } else if ("EXP".equals(handshakeAlgo)) {
384             handshakeAlgo = "";
385             export = true;
386         } else {
387             export = false;
388         }
389 
390         handshakeAlgo = toJavaHandshakeAlgo(handshakeAlgo, export);
391         String bulkCipher = toJavaBulkCipher(m.group(2), export);
392         String hmacAlgo = toJavaHmacAlgo(m.group(3));
393 
394         String javaCipherSuite = handshakeAlgo + "_WITH_" + bulkCipher + '_' + hmacAlgo;
395         // For historical reasons the CHACHA20 ciphers do not follow OpenSSL's custom naming convention and omits the
396         // HMAC algorithm portion of the name. There is currently no way to derive this information because it is
397         // omitted from the OpenSSL cipher name, but they currently all use SHA256 for HMAC [1].
398         // [1] https://www.openssl.org/docs/man1.1.0/apps/ciphers.html
399         return bulkCipher.contains("CHACHA20") ? javaCipherSuite + "_SHA256" : javaCipherSuite;
400     }
401 
402     private static String toJavaHandshakeAlgo(String handshakeAlgo, boolean export) {
403         if (handshakeAlgo.isEmpty()) {
404             handshakeAlgo = "RSA";
405         } else if ("ADH".equals(handshakeAlgo)) {
406             handshakeAlgo = "DH_anon";
407         } else if ("AECDH".equals(handshakeAlgo)) {
408             handshakeAlgo = "ECDH_anon";
409         }
410 
411         handshakeAlgo = handshakeAlgo.replace('-', '_');
412         if (export) {
413             return handshakeAlgo + "_EXPORT";
414         } else {
415             return handshakeAlgo;
416         }
417     }
418 
419     private static String toJavaBulkCipher(String bulkCipher, boolean export) {
420         if (bulkCipher.startsWith("AES")) {
421             Matcher m = OPENSSL_AES_CBC_PATTERN.matcher(bulkCipher);
422             if (m.matches()) {
423                 return m.replaceFirst("$1_$2_CBC");
424             }
425 
426             m = OPENSSL_AES_PATTERN.matcher(bulkCipher);
427             if (m.matches()) {
428                 return m.replaceFirst("$1_$2_$3");
429             }
430         }
431 
432         if ("DES-CBC3".equals(bulkCipher)) {
433             return "3DES_EDE_CBC";
434         }
435 
436         if ("RC4".equals(bulkCipher)) {
437             if (export) {
438                 return "RC4_40";
439             } else {
440                 return "RC4_128";
441             }
442         }
443 
444         if ("DES-CBC".equals(bulkCipher)) {
445             if (export) {
446                 return "DES_CBC_40";
447             } else {
448                 return "DES_CBC";
449             }
450         }
451 
452         if ("RC2-CBC".equals(bulkCipher)) {
453             if (export) {
454                 return "RC2_CBC_40";
455             } else {
456                 return "RC2_CBC";
457             }
458         }
459 
460         return bulkCipher.replace('-', '_');
461     }
462 
463     private static String toJavaHmacAlgo(String hmacAlgo) {
464         // Java and OpenSSL use the same algorithm names for:
465         //
466         //   * SHA
467         //   * SHA256
468         //   * MD5
469         //
470         return hmacAlgo;
471     }
472 
473     /**
474      * Convert the given ciphers if needed to OpenSSL format and append them to the correct {@link StringBuilder}
475      * depending on if its a TLSv1.3 cipher or not. If this methods returns without throwing an exception its
476      * guaranteed that at least one of the {@link StringBuilder}s contain some ciphers that can be used to configure
477      * OpenSSL.
478      */
479     static void convertToCipherStrings(Iterable<String> cipherSuites, StringBuilder cipherBuilder,
480                                        StringBuilder cipherTLSv13Builder, boolean boringSSL) {
481         for (String c: cipherSuites) {
482             if (c == null) {
483                 break;
484             }
485 
486             String converted = toOpenSsl(c, boringSSL);
487             if (converted == null) {
488                 converted = c;
489             }
490 
491             if (!OpenSsl.isCipherSuiteAvailable(converted)) {
492                 throw new IllegalArgumentException("unsupported cipher suite: " + c + '(' + converted + ')');
493             }
494 
495             if (SslUtils.isTLSv13Cipher(converted) || SslUtils.isTLSv13Cipher(c)) {
496                 cipherTLSv13Builder.append(converted);
497                 cipherTLSv13Builder.append(':');
498             } else {
499                 cipherBuilder.append(converted);
500                 cipherBuilder.append(':');
501             }
502         }
503 
504         if (cipherBuilder.length() == 0 && cipherTLSv13Builder.length() == 0) {
505             throw new IllegalArgumentException("empty cipher suites");
506         }
507         if (cipherBuilder.length() > 0) {
508             cipherBuilder.setLength(cipherBuilder.length() - 1);
509         }
510         if (cipherTLSv13Builder.length() > 0) {
511             cipherTLSv13Builder.setLength(cipherTLSv13Builder.length() - 1);
512         }
513     }
514 
515     private CipherSuiteConverter() { }
516 }