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