1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
34
35
36
37 @UnstableApi
38 public final class CipherSuiteConverter {
39
40 private static final InternalLogger logger = InternalLoggerFactory.getInstance(CipherSuiteConverter.class);
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 private static final Pattern JAVA_CIPHERSUITE_PATTERN =
56 Pattern.compile("^(?:TLS|SSL)_((?:(?!_WITH_).)+)_WITH_(.*)_(.*)$");
57
58
59
60
61
62
63
64
65
66
67
68
69
70 private static final Pattern OPENSSL_CIPHERSUITE_PATTERN =
71
72 Pattern.compile(
73 "^(?:(" +
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 ")-)?" +
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
91
92
93 private static final ConcurrentMap<String, String> j2o = new ConcurrentHashMap<>();
94
95
96
97
98
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
124
125 static void clearCache() {
126 j2o.clear();
127 o2j.clear();
128 }
129
130
131
132
133 static boolean isJ2OCached(String key, String value) {
134 return value.equals(j2o.get(key));
135 }
136
137
138
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
151
152
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
174 j2o.putIfAbsent(javaCipherSuite, openSslCipherSuite);
175
176
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
269
270
271
272
273
274 return hmacAlgo;
275 }
276
277
278
279
280
281
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
288
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
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
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
375
376
377
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
444
445
446
447
448
449 return hmacAlgo;
450 }
451
452
453
454
455
456
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 }