1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
35
36
37
38 @UnstableApi
39 public final class CipherSuiteConverter {
40
41 private static final InternalLogger logger = InternalLoggerFactory.getInstance(CipherSuiteConverter.class);
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 private static final Pattern JAVA_CIPHERSUITE_PATTERN =
57 Pattern.compile("^(?:TLS|SSL)_((?:(?!_WITH_).)+)_WITH_(.*)_(.*)$");
58
59
60
61
62
63
64
65
66
67
68
69
70
71 private static final Pattern OPENSSL_CIPHERSUITE_PATTERN =
72
73 Pattern.compile(
74 "^(?:(" +
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 ")-)?" +
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
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
109
110
111 private static final ConcurrentMap<String, CachedValue> j2o = PlatformDependent.newConcurrentHashMap();
112
113
114
115
116
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
142
143 static void clearCache() {
144 j2o.clear();
145 o2j.clear();
146 }
147
148
149
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
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
170
171
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
190 j2o.putIfAbsent(javaCipherSuite, CachedValue.of(openSslCipherSuite));
191
192 if (openSslCipherSuite == null) {
193 return null;
194 }
195
196
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
289
290
291
292
293
294 return hmacAlgo;
295 }
296
297
298
299
300
301
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
308
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
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
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
396
397
398
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
465
466
467
468
469
470 return hmacAlgo;
471 }
472
473
474
475
476
477
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 }