No flags found
Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.
e.g., #unittest #integration
#production #enterprise
#frontend #backend
69ddc04
... +0 ...
6ef10ec
Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.
e.g., #unittest #integration
#production #enterprise
#frontend #backend
107 | 107 | } |
|
108 | 108 | ||
109 | 109 | static byte[] parseKey(String key) { |
|
110 | - | try { |
|
111 | - | byte[] bytes = Base64Util.decode(HttpUtils.bytes(key)); |
|
110 | + | byte[] bytes = Base64Util.decode(HttpUtils.bytes(key)); |
|
112 | 111 | ||
113 | - | if (bytes.length == 16) { |
|
114 | - | return bytes; |
|
115 | - | } |
|
116 | - | } |
|
117 | - | catch (Exception e) { |
|
118 | - | //Ignore |
|
112 | + | if (bytes != null && bytes.length == 16) { |
|
113 | + | return bytes; |
|
119 | 114 | } |
|
120 | 115 | return null; |
|
121 | 116 | } |
1 | + | /* |
|
2 | + | * -------------------------------- MIT License -------------------------------- |
|
3 | + | * |
|
4 | + | * Copyright (c) 2021 SNF4J contributors |
|
5 | + | * |
|
6 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 | + | * of this software and associated documentation files (the "Software"), to deal |
|
8 | + | * in the Software without restriction, including without limitation the rights |
|
9 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 | + | * copies of the Software, and to permit persons to whom the Software is |
|
11 | + | * furnished to do so, subject to the following conditions: |
|
12 | + | * |
|
13 | + | * The above copyright notice and this permission notice shall be included in all |
|
14 | + | * copies or substantial portions of the Software. |
|
15 | + | * |
|
16 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
17 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
18 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
19 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
20 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
21 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
22 | + | * SOFTWARE. |
|
23 | + | * |
|
24 | + | * ----------------------------------------------------------------------------- |
|
25 | + | */ |
|
26 | + | package org.snf4j.core.session.ssl; |
|
27 | + | ||
28 | + | import java.io.ByteArrayInputStream; |
|
29 | + | import java.io.Closeable; |
|
30 | + | import java.io.File; |
|
31 | + | import java.io.IOException; |
|
32 | + | import java.io.InputStream; |
|
33 | + | import java.security.KeyException; |
|
34 | + | import java.security.KeyFactory; |
|
35 | + | import java.security.KeyStore; |
|
36 | + | import java.security.PrivateKey; |
|
37 | + | import java.security.Provider; |
|
38 | + | import java.security.SecureRandom; |
|
39 | + | import java.security.cert.CertificateException; |
|
40 | + | import java.security.cert.CertificateFactory; |
|
41 | + | import java.security.cert.X509Certificate; |
|
42 | + | import java.security.spec.PKCS8EncodedKeySpec; |
|
43 | + | import java.util.Arrays; |
|
44 | + | import java.util.List; |
|
45 | + | ||
46 | + | import javax.crypto.Cipher; |
|
47 | + | import javax.crypto.EncryptedPrivateKeyInfo; |
|
48 | + | import javax.crypto.SecretKey; |
|
49 | + | import javax.crypto.SecretKeyFactory; |
|
50 | + | import javax.crypto.spec.PBEKeySpec; |
|
51 | + | import javax.net.ssl.KeyManagerFactory; |
|
52 | + | import javax.net.ssl.SSLContext; |
|
53 | + | import javax.net.ssl.SSLEngine; |
|
54 | + | import javax.net.ssl.SSLSessionContext; |
|
55 | + | import javax.net.ssl.TrustManagerFactory; |
|
56 | + | import javax.security.auth.DestroyFailedException; |
|
57 | + | import javax.security.auth.Destroyable; |
|
58 | + | ||
59 | + | import org.snf4j.core.util.PemUtil; |
|
60 | + | import org.snf4j.core.util.PemUtil.Label; |
|
61 | + | ||
62 | + | /** |
|
63 | + | * A builder for the {@link SSLContext}. |
|
64 | + | * |
|
65 | + | * @author <a href="http://snf4j.org">SNF4J.ORG</a> |
|
66 | + | */ |
|
67 | + | public class SSLContextBuilder implements Destroyable { |
|
68 | + | ||
69 | + | private final static String[] KEY_ALGOS = new String[] {"RSA","DSA","EC"}; |
|
70 | + | ||
71 | + | private final boolean forServer; |
|
72 | + | ||
73 | + | private String protocol = ProtocolDefaults.TLS; |
|
74 | + | ||
75 | + | private Provider provider; |
|
76 | + | ||
77 | + | private String providerName; |
|
78 | + | ||
79 | + | private int sessionCacheSize = -1; |
|
80 | + | ||
81 | + | private int sessionTimeout = -1; |
|
82 | + | ||
83 | + | private TrustManagerFactory trustManager; |
|
84 | + | ||
85 | + | private X509Certificate[] trustCerts; |
|
86 | + | ||
87 | + | private PrivateKey key; |
|
88 | + | ||
89 | + | private char[] password; |
|
90 | + | ||
91 | + | private KeyManagerFactory keyManager; |
|
92 | + | ||
93 | + | private X509Certificate[] keyCerts; |
|
94 | + | ||
95 | + | private SecureRandom secureRandom; |
|
96 | + | ||
97 | + | //SSL engine default |
|
98 | + | ||
99 | + | private String[] protocols; |
|
100 | + | ||
101 | + | private ProtocolFilter protocolFilter; |
|
102 | + | ||
103 | + | private String[] ciphers; |
|
104 | + | ||
105 | + | private CipherFilter cipherFilter; |
|
106 | + | ||
107 | + | private Boolean enableRetransmissions; //JDK9 |
|
108 | + | ||
109 | + | private int maximumPacketSize = -1; //JDK9 |
|
110 | + | ||
111 | + | private Boolean useCiphersOrder; |
|
112 | + | ||
113 | + | private ClientAuth clientAuth = ClientAuth.NONE; |
|
114 | + | ||
115 | + | private SSLContextBuilder(boolean forServer) { |
|
116 | + | this.forServer = forServer; |
|
117 | + | } |
|
118 | + | ||
119 | + | private static SSLContextBuilder forServer() { |
|
120 | + | return new SSLContextBuilder(true); |
|
121 | + | } |
|
122 | + | ||
123 | + | /** |
|
124 | + | * Creates a builder for a client-side {@link SSLContext}. |
|
125 | + | * |
|
126 | + | * @return a builder for a client-side {@link SSLContext} |
|
127 | + | */ |
|
128 | + | public static SSLContextBuilder forClient() { |
|
129 | + | return new SSLContextBuilder(false); |
|
130 | + | } |
|
131 | + | ||
132 | + | /** |
|
133 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
134 | + | * |
|
135 | + | * @param keyFile a file for a PKCS#8 private key in the PEM encoding |
|
136 | + | * @param keyCertsFile a file for an X.509 certificate chain in the PEM encoding |
|
137 | + | * @return a builder for a server-side {@link SSLContext} |
|
138 | + | * @throws IOException if a failure occurred while reading the files |
|
139 | + | * @throws KeyException if a failure occurred while creating the key |
|
140 | + | * @throws CertificateException if a failure occurred while creating the |
|
141 | + | * certificates |
|
142 | + | */ |
|
143 | + | public static SSLContextBuilder forServer(File keyFile, File keyCertsFile) throws IOException, KeyException, CertificateException { |
|
144 | + | return forServer().keyManager(keyFile, keyCertsFile); |
|
145 | + | } |
|
146 | + | ||
147 | + | /** |
|
148 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
149 | + | * |
|
150 | + | * @param keyFile a file for a PKCS#8 private key in the PEM encoding |
|
151 | + | * @param password the password protecting the private key, or {@code null} |
|
152 | + | * if the key is not password-protected |
|
153 | + | * @param keyCertsFile a file for an X.509 certificate chain in the PEM encoding |
|
154 | + | * @return a builder for a server-side {@link SSLContext} |
|
155 | + | * @throws IOException if a failure occurred while reading the files |
|
156 | + | * @throws KeyException if a failure occurred while creating the key |
|
157 | + | * @throws CertificateException if a failure occurred while creating the |
|
158 | + | * certificates |
|
159 | + | */ |
|
160 | + | public static SSLContextBuilder forServer(File keyFile, char[] password, File keyCertsFile) throws IOException, KeyException, CertificateException { |
|
161 | + | return forServer().keyManager(keyFile, password, keyCertsFile); |
|
162 | + | } |
|
163 | + | ||
164 | + | /** |
|
165 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
166 | + | * |
|
167 | + | * @param keyIn an input stream for a PKCS#8 private key in the PEM |
|
168 | + | * encoding |
|
169 | + | * @param keyCertsIn an input stream for an X.509 certificate chain in the PEM |
|
170 | + | * encoding |
|
171 | + | * @return a builder for a server-side {@link SSLContext} |
|
172 | + | * @throws IOException if a failure occurred while reading from the |
|
173 | + | * input streams |
|
174 | + | * @throws KeyException if a failure occurred while creating the key |
|
175 | + | * @throws CertificateException if a failure occurred while creating the |
|
176 | + | * certificates |
|
177 | + | */ |
|
178 | + | public static SSLContextBuilder forServer(InputStream keyIn, InputStream keyCertsIn) throws IOException, KeyException, CertificateException { |
|
179 | + | return forServer().keyManager(keyIn, keyCertsIn); |
|
180 | + | } |
|
181 | + | ||
182 | + | /** |
|
183 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
184 | + | * |
|
185 | + | * @param keyIn an input stream for a PKCS#8 private key in the PEM encoding |
|
186 | + | * @param password the password protecting the private key, or {@code null} if |
|
187 | + | * the key is not password-protected |
|
188 | + | * @param keyCertsIn an input stream for an X.509 certificate chain in the PEM |
|
189 | + | * encoding |
|
190 | + | * @return a builder for a server-side {@link SSLContext} |
|
191 | + | * @throws IOException if a failure occurred while reading from the |
|
192 | + | * input streams |
|
193 | + | * @throws KeyException if a failure occurred while creating the key |
|
194 | + | * @throws CertificateException if a failure occurred while creating the |
|
195 | + | * certificates |
|
196 | + | */ |
|
197 | + | public static SSLContextBuilder forServer(InputStream keyIn, char[] password, InputStream keyCertsIn) throws IOException, KeyException, CertificateException { |
|
198 | + | return forServer().keyManager(keyIn, password, keyCertsIn); |
|
199 | + | } |
|
200 | + | ||
201 | + | /** |
|
202 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
203 | + | * |
|
204 | + | * @param key a PKCS#8 private key |
|
205 | + | * @param keyCerts an X.509 certificate chain |
|
206 | + | * @return a builder for a server-side {@link SSLContext} |
|
207 | + | */ |
|
208 | + | public static SSLContextBuilder forServer(PrivateKey key, X509Certificate... keyCerts) { |
|
209 | + | return forServer().keyManager(key, keyCerts); |
|
210 | + | } |
|
211 | + | ||
212 | + | /** |
|
213 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
214 | + | * |
|
215 | + | * @param key a PKCS#8 private key |
|
216 | + | * @param password the password protecting the private key, or {@code null} if |
|
217 | + | * the key is not password-protected |
|
218 | + | * @param keyCerts an X.509 certificate chain |
|
219 | + | * @return a builder for a server-side {@link SSLContext} |
|
220 | + | */ |
|
221 | + | public static SSLContextBuilder forServer(PrivateKey key, char[] password, X509Certificate... keyCerts) { |
|
222 | + | return forServer().keyManager(key, password, keyCerts); |
|
223 | + | } |
|
224 | + | ||
225 | + | /** |
|
226 | + | * Creates a builder for a server-side {@link SSLContext}. |
|
227 | + | * |
|
228 | + | * @param keyFactory a factory for a private key |
|
229 | + | * @return a builder for a server-side {@link SSLContext} |
|
230 | + | */ |
|
231 | + | public static SSLContextBuilder forServer(KeyManagerFactory keyFactory) { |
|
232 | + | return forServer().keyManager(keyFactory); |
|
233 | + | } |
|
234 | + | ||
235 | + | /** |
|
236 | + | * Tells if the builder if for a server-side {@link SSLContext}. |
|
237 | + | * |
|
238 | + | * @return {@code true} if the builder if for a server-side {@link SSLContext} |
|
239 | + | */ |
|
240 | + | public boolean isForServer() { |
|
241 | + | return forServer; |
|
242 | + | } |
|
243 | + | ||
244 | + | /** |
|
245 | + | * Tells if the builder if for a client-side {@link SSLContext}. |
|
246 | + | * |
|
247 | + | * @return {@code true} if the builder if for a client-side {@link SSLContext} |
|
248 | + | */ |
|
249 | + | public boolean isForClient() { |
|
250 | + | return !forServer; |
|
251 | + | } |
|
252 | + | ||
253 | + | /** |
|
254 | + | * Configures the protocol name of the {@link SSLContext} to be created by this |
|
255 | + | * builder. |
|
256 | + | * |
|
257 | + | * @param protocol the protocol name |
|
258 | + | * @return this builder |
|
259 | + | */ |
|
260 | + | public SSLContextBuilder protocol(String protocol) { |
|
261 | + | this.protocol = protocol; |
|
262 | + | return this; |
|
263 | + | } |
|
264 | + | ||
265 | + | /** |
|
266 | + | * Configures protocol versions to enable, or {@code null} to enable the |
|
267 | + | * recommended protocol versions. |
|
268 | + | * <p> |
|
269 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
270 | + | * returned by the {@link #engineBuilder()} method. |
|
271 | + | * |
|
272 | + | * @param protocols the protocol versions |
|
273 | + | * @return this builder |
|
274 | + | */ |
|
275 | + | public SSLContextBuilder protocols(String... protocols) { |
|
276 | + | this.protocols = protocols == null ? null : protocols.clone(); |
|
277 | + | return this; |
|
278 | + | } |
|
279 | + | ||
280 | + | /** |
|
281 | + | * Configures a filter for protocol versions to enable, or {@code null} to use |
|
282 | + | * the default filter. |
|
283 | + | * <p> |
|
284 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
285 | + | * returned by the {@link #engineBuilder()} method. |
|
286 | + | * |
|
287 | + | * @param filter the protocol filter |
|
288 | + | * @return this builder |
|
289 | + | */ |
|
290 | + | public SSLContextBuilder protocolFilter(ProtocolFilter filter) { |
|
291 | + | protocolFilter = filter; |
|
292 | + | return this; |
|
293 | + | } |
|
294 | + | ||
295 | + | /** |
|
296 | + | * Configures cipher suites to enable, or {@code null} to enable the |
|
297 | + | * recommended cipher suites. |
|
298 | + | * <p> |
|
299 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
300 | + | * returned by the {@link #engineBuilder()} method. |
|
301 | + | * |
|
302 | + | * @param ciphers the cipher suites |
|
303 | + | * @return this builder |
|
304 | + | */ |
|
305 | + | public SSLContextBuilder ciphers(String... ciphers) { |
|
306 | + | this.ciphers = ciphers == null ? null : ciphers.clone(); |
|
307 | + | return this; |
|
308 | + | } |
|
309 | + | ||
310 | + | /** |
|
311 | + | * Configures a filter for cipher suites to enable, or {@code null} to use |
|
312 | + | * the default filter. |
|
313 | + | * <p> |
|
314 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
315 | + | * returned by the {@link #engineBuilder()} method. |
|
316 | + | * |
|
317 | + | * @param filter the cipher filter |
|
318 | + | * @return this builder |
|
319 | + | */ |
|
320 | + | public SSLContextBuilder cipherFilter(CipherFilter filter) { |
|
321 | + | cipherFilter = filter; |
|
322 | + | return this; |
|
323 | + | } |
|
324 | + | ||
325 | + | /** |
|
326 | + | * Configures if DTLS handshake retransmissions should be enabled. |
|
327 | + | * <p> |
|
328 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
329 | + | * returned by the {@link #engineBuilder()} method. |
|
330 | + | * <p> |
|
331 | + | * NOTE: It requires Java 9 or newer. |
|
332 | + | * |
|
333 | + | * @param enable {@code true} to enable DTLS handshake retransmissions. |
|
334 | + | * @return this builder |
|
335 | + | */ |
|
336 | + | public SSLContextBuilder enableRetransmissions(boolean enable) { |
|
337 | + | enableRetransmissions = enable ? Boolean.TRUE : Boolean.FALSE; |
|
338 | + | return this; |
|
339 | + | } |
|
340 | + | ||
341 | + | /** |
|
342 | + | * Configures the maximum expected network packet size. |
|
343 | + | * <p> |
|
344 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
345 | + | * returned by the {@link #engineBuilder()} method. |
|
346 | + | * <p> |
|
347 | + | * NOTE: It requires Java 9 or newer. |
|
348 | + | * |
|
349 | + | * @param maxSize the maximum expected network packet size in bytes, or 0 to use |
|
350 | + | * the default value that is specified by the underlying |
|
351 | + | * implementation. |
|
352 | + | * @return this builder |
|
353 | + | */ |
|
354 | + | public SSLContextBuilder maximumPacketSize(int maxSize) { |
|
355 | + | maximumPacketSize = maxSize; |
|
356 | + | return this; |
|
357 | + | } |
|
358 | + | ||
359 | + | /** |
|
360 | + | * Configures if the local cipher suites preferences should be honored during |
|
361 | + | * SSL/TLS/DTLS handshaking |
|
362 | + | * <p> |
|
363 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
364 | + | * returned by the {@link #engineBuilder()} method. |
|
365 | + | * |
|
366 | + | * @param useOrder {@code true} to honor the local cipher suites preferences |
|
367 | + | * @return this builder |
|
368 | + | */ |
|
369 | + | public SSLContextBuilder useCiphersOrder(boolean useOrder) { |
|
370 | + | useCiphersOrder = useOrder ? Boolean.TRUE : Boolean.FALSE; |
|
371 | + | return this; |
|
372 | + | } |
|
373 | + | ||
374 | + | /** |
|
375 | + | * Configures the client authentication mode for a server-side |
|
376 | + | * {@link SSLEngine}. |
|
377 | + | * <p> |
|
378 | + | * This configuration is used to pre-configure the {@link SSLEngineBuilder} |
|
379 | + | * returned by the {@link #engineBuilder()} method. |
|
380 | + | * |
|
381 | + | * @param clientAuth the client authentication mode. |
|
382 | + | * @return this builder |
|
383 | + | */ |
|
384 | + | public SSLContextBuilder clientAuth(ClientAuth clientAuth) { |
|
385 | + | this.clientAuth = clientAuth; |
|
386 | + | return this; |
|
387 | + | } |
|
388 | + | ||
389 | + | /** |
|
390 | + | * Configures the provide of the {@link SSLContext} to be created by this |
|
391 | + | * builder. |
|
392 | + | * |
|
393 | + | * @param provider the provider |
|
394 | + | * @return this builder |
|
395 | + | */ |
|
396 | + | public SSLContextBuilder provider(Provider provider) { |
|
397 | + | this.provider = provider; |
|
398 | + | providerName = null; |
|
399 | + | return this; |
|
400 | + | } |
|
401 | + | ||
402 | + | /** |
|
403 | + | * Configures the provider name of the {@link SSLContext} to be created by this |
|
404 | + | * builder. |
|
405 | + | * |
|
406 | + | * @param provider the provider name |
|
407 | + | * @return this builder |
|
408 | + | */ |
|
409 | + | public SSLContextBuilder providerName(String provider) { |
|
410 | + | this.providerName = provider; |
|
411 | + | provider = null; |
|
412 | + | return this; |
|
413 | + | } |
|
414 | + | ||
415 | + | /** |
|
416 | + | * Configures the timeout limit for the cached SSL session objects. |
|
417 | + | * |
|
418 | + | * @param timeout the timeout limit in seconds, or 0 to set no limit. |
|
419 | + | * @return this builder |
|
420 | + | */ |
|
421 | + | public SSLContextBuilder sessionTimeout(int timeout) { |
|
422 | + | sessionTimeout = timeout; |
|
423 | + | return this; |
|
424 | + | } |
|
425 | + | ||
426 | + | /** |
|
427 | + | * Configures the size of the cache used for storing the SSL session objects. |
|
428 | + | * |
|
429 | + | * @param size the cache size limit, or 0 to set no limit. |
|
430 | + | * @return this builder |
|
431 | + | */ |
|
432 | + | public SSLContextBuilder sessionCacheSize(int size) { |
|
433 | + | sessionCacheSize = size; |
|
434 | + | return this; |
|
435 | + | } |
|
436 | + | ||
437 | + | private X509Certificate[] createCerts(List<byte[]> certs) throws CertificateException { |
|
438 | + | if (certs == null) { |
|
439 | + | throw new CertificateException("Invalid certificate PEM format"); |
|
440 | + | } |
|
441 | + | if (certs.isEmpty()) { |
|
442 | + | throw new CertificateException("No certificate found"); |
|
443 | + | } |
|
444 | + | ||
445 | + | CertificateFactory cf = CertificateFactory.getInstance("X.509"); |
|
446 | + | int len = certs.size(); |
|
447 | + | X509Certificate[] out = new X509Certificate[len]; |
|
448 | + | ||
449 | + | for (int i=0; i<len; ++i) { |
|
450 | + | InputStream in = new ByteArrayInputStream(certs.get(i)); |
|
451 | + | ||
452 | + | try { |
|
453 | + | out[i] = (X509Certificate) cf.generateCertificate(in); |
|
454 | + | } |
|
455 | + | finally { |
|
456 | + | silentClose(in); |
|
457 | + | } |
|
458 | + | } |
|
459 | + | return out; |
|
460 | + | } |
|
461 | + | ||
462 | + | static void silentClose(Closeable stream) { |
|
463 | + | try { |
|
464 | + | stream.close(); |
|
465 | + | } |
|
466 | + | catch (IOException e) { |
|
467 | + | //Ignore |
|
468 | + | } |
|
469 | + | } |
|
470 | + | ||
471 | + | private X509Certificate[] readCerts(File file) throws IOException, CertificateException { |
|
472 | + | return createCerts(PemUtil.read(Label.CERTIFICATE, file)); |
|
473 | + | } |
|
474 | + | ||
475 | + | private X509Certificate[] readCerts(InputStream in) throws IOException, CertificateException { |
|
476 | + | return createCerts(PemUtil.read(Label.CERTIFICATE, in)); |
|
477 | + | } |
|
478 | + | ||
479 | + | /** |
|
480 | + | * Configures trusted certificates for remote hosts verification. |
|
481 | + | * |
|
482 | + | * @param trustCertsFile a file for X.509 certificates in the PEM encoding |
|
483 | + | * @return this builder |
|
484 | + | * @throws IOException if a failure occurred while reading the files |
|
485 | + | * @throws CertificateException if a failure occurred while creating the |
|
486 | + | */ |
|
487 | + | public SSLContextBuilder trustManager(File trustCertsFile) throws IOException, CertificateException { |
|
488 | + | return trustManager(readCerts(trustCertsFile)); |
|
489 | + | } |
|
490 | + | ||
491 | + | /** |
|
492 | + | * Configures trusted certificates for remote hosts verification. |
|
493 | + | * |
|
494 | + | * @param trustCertsIn an input stream for X.509 certificates in the PEM encoding |
|
495 | + | * @return this builder |
|
496 | + | * @throws IOException if a failure occurred while reading the files |
|
497 | + | * @throws CertificateException if a failure occurred while creating the |
|
498 | + | */ |
|
499 | + | public SSLContextBuilder trustManager(InputStream trustCertsIn) throws IOException, CertificateException { |
|
500 | + | return trustManager(readCerts(trustCertsIn)); |
|
501 | + | } |
|
502 | + | ||
503 | + | /** |
|
504 | + | * Configures trusted certificates for remote hosts verification. |
|
505 | + | * |
|
506 | + | * @param trustCerts X.509 certificates |
|
507 | + | * @return this builder |
|
508 | + | */ |
|
509 | + | public SSLContextBuilder trustManager(X509Certificate... trustCerts) { |
|
510 | + | this.trustCerts = certs(trustCerts, true, "trustCerts"); |
|
511 | + | trustManager = null; |
|
512 | + | return this; |
|
513 | + | } |
|
514 | + | ||
515 | + | /** |
|
516 | + | * Configures trusted certificates for remote hosts verification. |
|
517 | + | * |
|
518 | + | * @param trustFactory a factory for trusted certificates |
|
519 | + | * @return this builder |
|
520 | + | */ |
|
521 | + | public SSLContextBuilder trustManager(TrustManagerFactory trustFactory) { |
|
522 | + | this.trustManager = trustFactory; |
|
523 | + | trustCerts = null; |
|
524 | + | return this; |
|
525 | + | } |
|
526 | + | ||
527 | + | private PrivateKey createKey(List<byte[]> keys, char[] password) throws KeyException { |
|
528 | + | if (keys == null) { |
|
529 | + | throw new KeyException("Invalid private key PEM format"); |
|
530 | + | } |
|
531 | + | if (keys.isEmpty()) { |
|
532 | + | throw new KeyException("No private key found"); |
|
533 | + | } |
|
534 | + | ||
535 | + | PKCS8EncodedKeySpec keySpec = null; |
|
536 | + | byte[] key = keys.get(0); |
|
537 | + | ||
538 | + | if (password == null) { |
|
539 | + | keySpec = new PKCS8EncodedKeySpec(keys.get(0)); |
|
540 | + | } |
|
541 | + | else { |
|
542 | + | try { |
|
543 | + | EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(key); |
|
544 | + | SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); |
|
545 | + | PBEKeySpec pbeKeySpec = new PBEKeySpec(password); |
|
546 | + | SecretKey pbeKey; |
|
547 | + | Cipher cipher; |
|
548 | + | ||
549 | + | try { |
|
550 | + | pbeKey = keyFactory.generateSecret(pbeKeySpec); |
|
551 | + | try { |
|
552 | + | cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); |
|
553 | + | cipher.init(Cipher.DECRYPT_MODE, pbeKey, encryptedPrivateKeyInfo.getAlgParameters()); |
|
554 | + | keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher); |
|
555 | + | } |
|
556 | + | finally { |
|
557 | + | pbeKey.destroy(); |
|
558 | + | } |
|
559 | + | } |
|
560 | + | finally { |
|
561 | + | pbeKeySpec.clearPassword(); |
|
562 | + | } |
|
563 | + | } |
|
564 | + | catch (DestroyFailedException e) { |
|
565 | + | //Ignore |
|
566 | + | } |
|
567 | + | catch (Exception e) { |
|
568 | + | throw new KeyException("Invalid PKCS8 encoding of password-protected private key", e); |
|
569 | + | } |
|
570 | + | } |
|
571 | + | ||
572 | + | Exception exception = null; |
|
573 | + | ||
574 | + | for (int i=0; i < KEY_ALGOS.length; ++i) { |
|
575 | + | try { |
|
576 | + | return KeyFactory.getInstance(KEY_ALGOS[i]).generatePrivate(keySpec); |
|
577 | + | } |
|
578 | + | catch (Exception e) { |
|
579 | + | exception = e; |
|
580 | + | } |
|
581 | + | } |
|
582 | + | throw new KeyException("Generation of private key failed: none of " + Arrays.toString(KEY_ALGOS) + " worked", exception); |
|
583 | + | } |
|
584 | + | ||
585 | + | private PrivateKey readKey(File file, char[] password) throws IOException, KeyException { |
|
586 | + | return createKey(PemUtil.read(password == null ? Label.PRIVATE_KEY : Label.ENCRYPTED_PRIVATE_KEY, file), |
|
587 | + | password); |
|
588 | + | } |
|
589 | + | ||
590 | + | private PrivateKey readKey(InputStream in, char[] password) throws IOException, KeyException { |
|
591 | + | return createKey(PemUtil.read(password == null ? Label.PRIVATE_KEY : Label.ENCRYPTED_PRIVATE_KEY, in), |
|
592 | + | password); |
|
593 | + | } |
|
594 | + | ||
595 | + | /** |
|
596 | + | * Configures a private key with certificate chain for host identification. |
|
597 | + | * |
|
598 | + | * @param keyFile a file for a PKCS#8 private key in the PEM encoding |
|
599 | + | * @param keyCertsFile a file for an X.509 certificate chain in the PEM encoding |
|
600 | + | * @throws IOException if a failure occurred while reading the files |
|
601 | + | * @throws KeyException if a failure occurred while creating the key |
|
602 | + | * @throws CertificateException if a failure occurred while creating the |
|
603 | + | * certificates |
|
604 | + | * @return this builder |
|
605 | + | */ |
|
606 | + | public SSLContextBuilder keyManager(File keyFile, File keyCertsFile) throws IOException, KeyException, CertificateException { |
|
607 | + | return keyManager(keyFile, null, keyCertsFile); |
|
608 | + | } |
|
609 | + | ||
610 | + | /** |
|
611 | + | * Configures a private key with certificate chain for host identification. |
|
612 | + | * |
|
613 | + | * @param keyFile a file for a PKCS#8 private key in the PEM encoding |
|
614 | + | * @param password the password protecting the private key, or {@code null} |
|
615 | + | * if the key is not password-protected |
|
616 | + | * @param keyCertsFile a file for an X.509 certificate chain in the PEM encoding |
|
617 | + | * @throws IOException if a failure occurred while reading the files |
|
618 | + | * @throws KeyException if a failure occurred while creating the key |
|
619 | + | * @throws CertificateException if a failure occurred while creating the |
|
620 | + | * certificates |
|
621 | + | * @return this builder |
|
622 | + | */ |
|
623 | + | public SSLContextBuilder keyManager(File keyFile, char[] password, File keyCertsFile) throws IOException, KeyException, CertificateException { |
|
624 | + | return keyManager(readKey(keyFile, password), password, readCerts(keyCertsFile)); |
|
625 | + | } |
|
626 | + | ||
627 | + | /** |
|
628 | + | * Configures a private key with certificate chain for host identification. |
|
629 | + | * |
|
630 | + | * @param keyIn an input stream for a PKCS#8 private key in the PEM |
|
631 | + | * encoding |
|
632 | + | * @param keyCertsIn an input stream for an X.509 certificate chain in the PEM |
|
633 | + | * encoding |
|
634 | + | * @throws IOException if a failure occurred while reading from the |
|
635 | + | * input streams |
|
636 | + | * @throws KeyException if a failure occurred while creating the key |
|
637 | + | * @throws CertificateException if a failure occurred while creating the |
|
638 | + | * certificates |
|
639 | + | * @return this builder |
|
640 | + | */ |
|
641 | + | public SSLContextBuilder keyManager(InputStream keyIn, InputStream keyCertsIn) throws IOException, KeyException, CertificateException { |
|
642 | + | return keyManager(keyIn, null, keyCertsIn); |
|
643 | + | } |
|
644 | + | ||
645 | + | /** |
|
646 | + | * Configures a private key with certificate chain for host identification. |
|
647 | + | * |
|
648 | + | * @param keyIn an input stream for a PKCS#8 private key in the PEM |
|
649 | + | * encoding |
|
650 | + | * @param password the password protecting the private key, or {@code null} if |
|
651 | + | * the key is not password-protected |
|
652 | + | * @param keyCertsIn an input stream for an X.509 certificate chain in the PEM |
|
653 | + | * encoding |
|
654 | + | * @throws IOException if a failure occurred while reading from the |
|
655 | + | * input streams |
|
656 | + | * @throws KeyException if a failure occurred while creating the key |
|
657 | + | * @throws CertificateException if a failure occurred while creating the |
|
658 | + | * certificates |
|
659 | + | * @return this builder |
|
660 | + | */ |
|
661 | + | public SSLContextBuilder keyManager(InputStream keyIn, char[] password, InputStream keyCertsIn) throws IOException, KeyException, CertificateException { |
|
662 | + | return keyManager(readKey(keyIn, password), password, readCerts(keyCertsIn)); |
|
663 | + | } |
|
664 | + | ||
665 | + | private static X509Certificate[] certs(X509Certificate[] certs, boolean allowEmpty, String name) { |
|
666 | + | if (!allowEmpty) { |
|
667 | + | if (certs == null) { |
|
668 | + | throw new IllegalArgumentException(name + " is null"); |
|
669 | + | } |
|
670 | + | if (certs.length == 0) { |
|
671 | + | throw new IllegalArgumentException(name + " is empty"); |
|
672 | + | } |
|
673 | + | } |
|
674 | + | if (certs != null) { |
|
675 | + | for (X509Certificate cert: certs) { |
|
676 | + | if (cert == null) { |
|
677 | + | throw new IllegalArgumentException(name + " contains null entry"); |
|
678 | + | } |
|
679 | + | } |
|
680 | + | certs = certs.clone(); |
|
681 | + | } |
|
682 | + | return certs; |
|
683 | + | } |
|
684 | + | ||
685 | + | /** |
|
686 | + | * Configures a private key with certificate chain for host identification. |
|
687 | + | * |
|
688 | + | * @param key a PKCS#8 private key |
|
689 | + | * @param keyCerts an X.509 certificate chain |
|
690 | + | * @return this builder |
|
691 | + | */ |
|
692 | + | public SSLContextBuilder keyManager(PrivateKey key, X509Certificate... keyCerts) { |
|
693 | + | return keyManager(key, null, keyCerts); |
|
694 | + | } |
|
695 | + | ||
696 | + | /** |
|
697 | + | * Configures a private key with certificate chain for host identification. |
|
698 | + | * |
|
699 | + | * @param key a PKCS#8 private key |
|
700 | + | * @param password the password protecting the private key, or {@code null} if |
|
701 | + | * the key is not password-protected |
|
702 | + | * @param keyCerts an X.509 certificate chain |
|
703 | + | * @return this builder |
|
704 | + | */ |
|
705 | + | public SSLContextBuilder keyManager(PrivateKey key, char[] password, X509Certificate... keyCerts) { |
|
706 | + | keyCerts = certs(keyCerts, false, "keyCerts"); |
|
707 | + | if (key == null) { |
|
708 | + | throw new IllegalArgumentException("key is null"); |
|
709 | + | } |
|
710 | + | this.key = key; |
|
711 | + | this.password = password == null ? null : password.clone(); |
|
712 | + | this.keyCerts = keyCerts; |
|
713 | + | this.keyManager = null; |
|
714 | + | return this; |
|
715 | + | } |
|
716 | + | ||
717 | + | /** |
|
718 | + | * Configures a private key with certificate chain for host identification. |
|
719 | + | * |
|
720 | + | * @param keyFactory a factory for a private key |
|
721 | + | * @return this builder |
|
722 | + | */ |
|
723 | + | public SSLContextBuilder keyManager(KeyManagerFactory keyFactory) { |
|
724 | + | this.keyManager = keyFactory; |
|
725 | + | silentDestroy(); |
|
726 | + | return this; |
|
727 | + | } |
|
728 | + | ||
729 | + | /** |
|
730 | + | * Configures a secure source of randomness. |
|
731 | + | * |
|
732 | + | * @param random the source of randomness, or {@code null} to use the default |
|
733 | + | * source. |
|
734 | + | * @return this builder |
|
735 | + | */ |
|
736 | + | public SSLContextBuilder secureRandom(SecureRandom random) { |
|
737 | + | this.secureRandom = random; |
|
738 | + | return this; |
|
739 | + | } |
|
740 | + | ||
741 | + | private TrustManagerFactory buildTrustManager() throws Exception { |
|
742 | + | if (trustCerts == null) { |
|
743 | + | return null; |
|
744 | + | } |
|
745 | + | ||
746 | + | KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); |
|
747 | + | ||
748 | + | ks.load(null, null); |
|
749 | + | for (int i=0; i<trustCerts.length; ++i) { |
|
750 | + | ks.setCertificateEntry(Integer.toString(i+1), trustCerts[i]); |
|
751 | + | } |
|
752 | + | ||
753 | + | TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
|
754 | + | ||
755 | + | tmf.init(ks); |
|
756 | + | return tmf; |
|
757 | + | } |
|
758 | + | ||
759 | + | private KeyManagerFactory buildKeyManager() throws Exception { |
|
760 | + | if (key == null) { |
|
761 | + | return null; |
|
762 | + | } |
|
763 | + | ||
764 | + | KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); |
|
765 | + | char[] password = this.password == null ? new char[0] : this.password; |
|
766 | + | ||
767 | + | ks.load(null, null); |
|
768 | + | ks.setKeyEntry("key", key, password, keyCerts); |
|
769 | + | ||
770 | + | KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
|
771 | + | ||
772 | + | kmf.init(ks, password); |
|
773 | + | return kmf; |
|
774 | + | } |
|
775 | + | ||
776 | + | /** |
|
777 | + | * Creates a new {@link SSLEngine} builder pre-configured with the current |
|
778 | + | * configuration settings. The returned builder is constructed with a new |
|
779 | + | * {@link SSLContext} created by calling the {@link #build()} method. |
|
780 | + | * |
|
781 | + | * @return the new {@link SSLEngine} builder |
|
782 | + | * @throws SSLContextCreateException if a failure occurred while building the |
|
783 | + | * {@link SSLContext} instance used to |
|
784 | + | * construct the new {@link SSLEngine} builder |
|
785 | + | */ |
|
786 | + | public SSLEngineBuilder engineBuilder() throws SSLContextCreateException { |
|
787 | + | SSLEngineBuilder builder = new SSLEngineBuilder(build(), forServer); |
|
788 | + | Boolean value; |
|
789 | + | ||
790 | + | builder.protocols(protocols); |
|
791 | + | builder.ciphers(ciphers); |
|
792 | + | builder.clientAuth(clientAuth); |
|
793 | + | builder.cipherFilter(cipherFilter); |
|
794 | + | builder.protocolFilter(protocolFilter); |
|
795 | + | builder.maximumPacketSize(maximumPacketSize); |
|
796 | + | value = enableRetransmissions; |
|
797 | + | if (value != null) { |
|
798 | + | builder.enableRetransmissions(value.booleanValue()); |
|
799 | + | } |
|
800 | + | value = useCiphersOrder; |
|
801 | + | if (value != null) { |
|
802 | + | builder.useCiphersOrder(value.booleanValue()); |
|
803 | + | } |
|
804 | + | return builder; |
|
805 | + | } |
|
806 | + | ||
807 | + | private enum Phase { |
|
808 | + | ||
809 | + | NONE(null), |
|
810 | + | GET_DEFAULT_CTX("Getting of the default SSL context failed"), |
|
811 | + | BUILD_TRUST_MGR_FACTORY("Building of the trust manager factory failed"), |
|
812 | + | BUILD_KEY_MGR_FACTORY("Building of the key manager factory failed"), |
|
813 | + | BUILD_CTX("Building of the SSL context failed"); |
|
814 | + | ||
815 | + | String msg; |
|
816 | + | ||
817 | + | private Phase(String msg) { |
|
818 | + | this.msg = msg; |
|
819 | + | } |
|
820 | + | ||
821 | + | private String exceptionMessage() { |
|
822 | + | return msg; |
|
823 | + | } |
|
824 | + | } |
|
825 | + | ||
826 | + | /** |
|
827 | + | * Builds a new {@link SSLContext} instance based on the current configuration |
|
828 | + | * settings. |
|
829 | + | * |
|
830 | + | * @return the new {@link SSLContext} instance. |
|
831 | + | * @throws SSLContextCreateException if a failure occurred while building the |
|
832 | + | * {@link SSLContext} instance |
|
833 | + | */ |
|
834 | + | public SSLContext build() throws SSLContextCreateException { |
|
835 | + | String protocol = this.protocol; |
|
836 | + | Phase phase = Phase.NONE; |
|
837 | + | ||
838 | + | try { |
|
839 | + | if (protocol == null) { |
|
840 | + | phase = Phase.GET_DEFAULT_CTX; |
|
841 | + | return SSLContext.getDefault(); |
|
842 | + | } |
|
843 | + | ||
844 | + | TrustManagerFactory tmf; |
|
845 | + | KeyManagerFactory kmf; |
|
846 | + | ||
847 | + | phase = Phase.BUILD_TRUST_MGR_FACTORY; |
|
848 | + | tmf = trustManager != null ? trustManager : buildTrustManager(); |
|
849 | + | ||
850 | + | phase = Phase.BUILD_KEY_MGR_FACTORY; |
|
851 | + | kmf = keyManager != null ? keyManager : buildKeyManager(); |
|
852 | + | ||
853 | + | String providerName = this.providerName; |
|
854 | + | Provider provider = this.provider; |
|
855 | + | SSLContext context; |
|
856 | + | SSLSessionContext sessionContext; |
|
857 | + | ||
858 | + | phase = Phase.BUILD_CTX; |
|
859 | + | if (provider != null) { |
|
860 | + | context = SSLContext.getInstance(protocol, provider); |
|
861 | + | } |
|
862 | + | else if (providerName != null) { |
|
863 | + | context = SSLContext.getInstance(protocol, providerName); |
|
864 | + | } |
|
865 | + | else { |
|
866 | + | context = SSLContext.getInstance(protocol); |
|
867 | + | } |
|
868 | + | ||
869 | + | context.init( |
|
870 | + | kmf == null ? null : kmf.getKeyManagers(), |
|
871 | + | tmf == null ? null : tmf.getTrustManagers(), |
|
872 | + | secureRandom); |
|
873 | + | sessionContext = forServer ? context.getServerSessionContext() : context.getClientSessionContext(); |
|
874 | + | ||
875 | + | if (sessionCacheSize >= 0) { |
|
876 | + | sessionContext.setSessionCacheSize(sessionCacheSize); |
|
877 | + | } |
|
878 | + | if (sessionTimeout >= 0) { |
|
879 | + | sessionContext.setSessionTimeout(sessionTimeout); |
|
880 | + | } |
|
881 | + | ||
882 | + | return context; |
|
883 | + | } |
|
884 | + | catch (Exception e) { |
|
885 | + | throw new SSLContextCreateException(phase.exceptionMessage(), e); |
|
886 | + | } |
|
887 | + | } |
|
888 | + | ||
889 | + | private void silentDestroy() { |
|
890 | + | try { |
|
891 | + | destroy(); |
|
892 | + | } |
|
893 | + | catch (DestroyFailedException e) { |
|
894 | + | //Ignore |
|
895 | + | } |
|
896 | + | } |
|
897 | + | ||
898 | + | /** |
|
899 | + | * Destroys sensitive information associated with this builder (i.e. password |
|
900 | + | * and private key). |
|
901 | + | * |
|
902 | + | * @throws DestroyFailedException if the destroy operation failed |
|
903 | + | */ |
|
904 | + | @Override |
|
905 | + | public void destroy() throws DestroyFailedException { |
|
906 | + | if (password != null) { |
|
907 | + | Arrays.fill(password, (char)0); |
|
908 | + | password = null; |
|
909 | + | } |
|
910 | + | try { |
|
911 | + | if (key != null) { |
|
912 | + | key.destroy(); |
|
913 | + | } |
|
914 | + | } |
|
915 | + | finally { |
|
916 | + | key = null; |
|
917 | + | keyCerts = null; |
|
918 | + | } |
|
919 | + | } |
|
920 | + | ||
921 | + | /** |
|
922 | + | * Tells if sensitive information associated with this builder is destroyed |
|
923 | + | * |
|
924 | + | * @return {@code true} if the sensitive information is destroyed |
|
925 | + | */ |
|
926 | + | @Override |
|
927 | + | public boolean isDestroyed() { |
|
928 | + | return key == null; |
|
929 | + | } |
|
930 | + | } |
30 | 30 | ||
31 | 31 | /** |
|
32 | 32 | * A class with Base64 utility functions. |
|
33 | - | * <p> |
|
34 | - | * For JDK8 and above it uses the {@link java.util.Base64 java.util.Base64} |
|
35 | - | * implementation. |
|
36 | 33 | * |
|
37 | 34 | * @author <a href="http://snf4j.org">SNF4J.ORG</a> |
|
38 | 35 | */ |
|
39 | 36 | public final class Base64Util { |
|
40 | 37 | ||
38 | + | private final static byte[] EMPTY = new byte[0]; |
|
39 | + | ||
41 | 40 | private final static byte PAD = (byte) '='; |
|
42 | 41 | ||
43 | 42 | private final static int PAD_INDEX = 64; |
|
44 | 43 | ||
45 | - | private final static byte[] ALPHABET = new byte[('Z' - 'A' + 1) * 2 + 10 + 2 + 1]; |
|
44 | + | private final static char[] ALPHABET = new char[('Z' - 'A' + 1) * 2 + 10 + 2 + 1]; |
|
46 | 45 | ||
47 | - | private final static byte[] DECODING = new byte[256]; |
|
48 | - | ||
49 | - | private final static String JAVA_UTIL_BASE64 = "java.util.Base64"; |
|
50 | - | ||
51 | - | private final static boolean USE_JDK; |
|
46 | + | private final static int[] DECODING = new int[256]; |
|
52 | 47 | ||
53 | 48 | static { |
|
54 | 49 | int i = 0; |
|
55 | 50 | ||
56 | - | for (byte c = 'A'; c <= 'Z'; c++) { |
|
51 | + | for (char c = 'A'; c <= 'Z'; c++) { |
|
57 | 52 | ALPHABET[i++] = c; |
|
58 | 53 | } |
|
59 | - | for (byte c = 'a'; c <= 'z'; c++) { |
|
54 | + | for (char c = 'a'; c <= 'z'; c++) { |
|
60 | 55 | ALPHABET[i++] = c; |
|
61 | 56 | } |
|
62 | - | for (byte c = '0'; c <= '9'; c++) { |
|
57 | + | for (char c = '0'; c <= '9'; c++) { |
|
63 | 58 | ALPHABET[i++] = c; |
|
64 | 59 | } |
|
65 | 60 | ALPHABET[i++] = '+'; |
70 | 65 | for (i = 0; i < ALPHABET.length - 1; ++i) { |
|
71 | 66 | DECODING[ALPHABET[i]] = (byte) i; |
|
72 | 67 | } |
|
73 | - | USE_JDK = isClass(JAVA_UTIL_BASE64); |
|
74 | 68 | } |
|
75 | 69 | ||
76 | 70 | private Base64Util() { |
|
77 | 71 | } |
|
78 | 72 | ||
79 | - | static boolean isClass(String className) { |
|
80 | - | try { |
|
81 | - | Class.forName(className); |
|
82 | - | return true; |
|
83 | - | } catch (Exception e) { |
|
84 | - | return false; |
|
85 | - | } |
|
86 | - | } |
|
87 | - | ||
88 | - | static byte[] encode(byte[] data, boolean useJdk) { |
|
89 | - | if (useJdk) { |
|
90 | - | return java.util.Base64.getEncoder().encode(data); |
|
91 | - | } |
|
92 | - | return encode0(data); |
|
93 | - | } |
|
94 | - | ||
95 | 73 | /** |
|
96 | 74 | * Encodes bytes from the specified byte array into a newly-allocated byte array |
|
97 | 75 | * using the Base64 encoding scheme. |
102 | 80 | * @return A newly-allocated byte array containing the resulting encoded bytes |
|
103 | 81 | */ |
|
104 | 82 | public static byte[] encode(byte[] data) { |
|
105 | - | return encode(data, USE_JDK); |
|
83 | + | return encode(data, 0, data.length); |
|
106 | 84 | } |
|
107 | 85 | ||
108 | 86 | /** |
118 | 96 | * @return A String containing the resulting Base64 encoded characters |
|
119 | 97 | */ |
|
120 | 98 | public static String encode(byte[] data, Charset charset) { |
|
121 | - | return new String(encode(data, USE_JDK), charset); |
|
99 | + | return new String(encode(data, 0, data.length), charset); |
|
122 | 100 | } |
|
123 | 101 | ||
124 | - | static byte[] encode0(byte[] data) { |
|
125 | - | int len = data.length; |
|
126 | - | ||
127 | - | if (len == 0) { |
|
128 | - | return data; |
|
102 | + | /** |
|
103 | + | * Encodes bytes from the specified byte array into a newly-allocated byte array |
|
104 | + | * using the Base64 encoding scheme. |
|
105 | + | * <p> |
|
106 | + | * It uses "The Base 64 Alphabet" as specified in Table 1 of RFC 4648. |
|
107 | + | * |
|
108 | + | * @param data the byte array to encode |
|
109 | + | * @param offset offset within the array of the first byte to be encoded |
|
110 | + | * @param length number of bytes to be encoded |
|
111 | + | * @return A newly-allocated byte array containing the resulting encoded bytes |
|
112 | + | */ |
|
113 | + | public static byte[] encode(byte[] data, int offset, int length) { |
|
114 | + | if (length == 0) { |
|
115 | + | return EMPTY; |
|
129 | 116 | } |
|
130 | 117 | ||
131 | - | byte[] encoded = new byte[(len / 3 + (len % 3 == 0 ? 0 : 1)) * 4]; |
|
132 | - | int c = 0, i = 0; |
|
133 | - | int i1, i2, i3, i4; |
|
134 | - | ||
135 | - | for (; i < len; i += 3) { |
|
136 | - | if (i + 3 > len) { |
|
137 | - | break; |
|
138 | - | } |
|
139 | - | ||
140 | - | i1 = data[i] >>> 2 & 0x3f; |
|
141 | - | i2 = (data[i] << 4 | data[i + 1] >>> 4 & 0x0f) & 0x3f; |
|
142 | - | i3 = (data[i + 1] << 2 | data[i + 2] >>> 6 & 0x03) & 0x3f; |
|
143 | - | i4 = data[i + 2] & 0x3f; |
|
118 | + | int end = (length / 3) * 3 + offset; |
|
119 | + | byte[] encoded = new byte[(length / 3 + (length % 3 == 0 ? 0 : 1)) * 4]; |
|
120 | + | int c = 0, i = offset, v; |
|
144 | 121 | ||
145 | - | encoded[c++] = ALPHABET[i1]; |
|
146 | - | encoded[c++] = ALPHABET[i2]; |
|
147 | - | encoded[c++] = ALPHABET[i3]; |
|
148 | - | encoded[c++] = ALPHABET[i4]; |
|
122 | + | while (i < end) { |
|
123 | + | v = (data[i++] & 0xff) << 16 | (data[i++] & 0xff) << 8 | (data[i++] & 0xff); |
|
124 | + | encoded[c++] = (byte)ALPHABET[(v >>> 18) & 0x3f]; |
|
125 | + | encoded[c++] = (byte)ALPHABET[(v >>> 12) & 0x3f]; |
|
126 | + | encoded[c++] = (byte)ALPHABET[(v >>> 6) & 0x3f]; |
|
127 | + | encoded[c++] = (byte)ALPHABET[v & 0x3f]; |
|
149 | 128 | } |
|
150 | 129 | ||
151 | - | switch (len - i) { |
|
130 | + | int i1, i2, i3, i4; |
|
131 | + | ||
132 | + | switch (length-i+offset) { |
|
152 | 133 | case 1: |
|
153 | 134 | i1 = data[i] >>> 2 & 0x3f; |
|
154 | 135 | i2 = data[i] << 4 & 0x3f; |
167 | 148 | return encoded; |
|
168 | 149 | } |
|
169 | 150 | ||
170 | - | encoded[c++] = ALPHABET[i1]; |
|
171 | - | encoded[c++] = ALPHABET[i2]; |
|
172 | - | encoded[c++] = ALPHABET[i3]; |
|
173 | - | encoded[c++] = ALPHABET[i4]; |
|
151 | + | encoded[c++] = (byte)ALPHABET[i1]; |
|
152 | + | encoded[c++] = (byte)ALPHABET[i2]; |
|
153 | + | encoded[c++] = (byte)ALPHABET[i3]; |
|
154 | + | encoded[c++] = (byte)ALPHABET[i4]; |
|
174 | 155 | return encoded; |
|
175 | 156 | } |
|
176 | 157 | ||
177 | - | static String encode0(byte[] data, Charset charset) { |
|
178 | - | return new String(encode0(data), charset); |
|
179 | - | } |
|
180 | - | ||
181 | - | static byte[] decode(byte[] data, boolean useJdk) { |
|
182 | - | if (useJdk) { |
|
183 | - | return java.util.Base64.getDecoder().decode(data); |
|
184 | - | } |
|
185 | - | ||
186 | - | byte[] encoded = decode0(data); |
|
187 | - | ||
188 | - | if (encoded == null) { |
|
189 | - | throw new IllegalArgumentException("data is not in valid Base64 scheme"); |
|
190 | - | } |
|
191 | - | return encoded; |
|
158 | + | /** |
|
159 | + | * Encodes the specified byte array into a String using the Base64 encoding |
|
160 | + | * scheme. |
|
161 | + | * <p> |
|
162 | + | * It first encodes input bytes into a base64 encoded byte array by calling the |
|
163 | + | * {@link #encode(byte[])} method and then constructs a new String by using the |
|
164 | + | * encoded byte array and the specified charset. |
|
165 | + | * |
|
166 | + | * @param data the byte array to encode |
|
167 | + | * @param offset offset within the array of the first byte to be encoded |
|
168 | + | * @param length number of bytes to be encoded |
|
169 | + | * @param charset the charset used to encode the resulting String |
|
170 | + | * @return A String containing the resulting Base64 encoded characters |
|
171 | + | */ |
|
172 | + | public static String encode(byte[] data, int offset, int length, Charset charset) { |
|
173 | + | return new String(encode(data, offset, length), charset); |
|
192 | 174 | } |
|
193 | - | ||
175 | + | ||
194 | 176 | /** |
|
195 | 177 | * Decodes bytes from the specified byte array into a newly-allocated byte array |
|
196 | 178 | * using the Base64 encoding scheme. |
202 | 184 | * @throws IllegalArgumentException - if the data is not in valid Base64 scheme |
|
203 | 185 | */ |
|
204 | 186 | public static byte[] decode(byte[] data) { |
|
205 | - | return decode(data, USE_JDK); |
|
187 | + | return decode(data, 0, data.length, false); |
|
206 | 188 | } |
|
207 | 189 | ||
190 | + | /** |
|
191 | + | * Decodes bytes from the specified byte array into a newly-allocated byte array |
|
192 | + | * using the Base64 encoding scheme with an option for the MIME format. |
|
193 | + | * <p> |
|
194 | + | * It uses "The Base 64 Alphabet" as specified in Table 1 of RFC 4648. |
|
195 | + | * |
|
196 | + | * @param data the byte array to decode |
|
197 | + | * @param isMime {@code true} if the data is encoded in the MIME format |
|
198 | + | * @return A newly-allocated byte array containing the resulting decoded bytes, |
|
199 | + | * or {@code null} if the data is not in valid Base64 scheme |
|
200 | + | */ |
|
201 | + | public static byte[] decode(byte[] data, boolean isMime) { |
|
202 | + | return decode(data, 0, data.length, isMime); |
|
203 | + | } |
|
204 | + | ||
208 | 205 | /** |
|
209 | 206 | * Decodes a Base64 encoded String into a newly-allocated byte array using the |
|
210 | 207 | * Base64 encoding scheme. |
215 | 212 | * |
|
216 | 213 | * @param data the string to decode |
|
217 | 214 | * @param charset The charset to be used to encode the String |
|
218 | - | * @return A newly-allocated byte array containing the resulting decoded bytes |
|
219 | - | * @throws IllegalArgumentException - if the data is not in valid Base64 scheme |
|
215 | + | * @return A newly-allocated byte array containing the resulting decoded bytes, |
|
216 | + | * or {@code null} if the data is not in valid Base64 scheme |
|
220 | 217 | */ |
|
221 | 218 | public static byte[] decode(String data, Charset charset) { |
|
222 | - | return decode(data.getBytes(charset), USE_JDK); |
|
219 | + | return decode(data.getBytes(charset), false); |
|
223 | 220 | } |
|
224 | 221 | ||
225 | - | static byte[] decode0(byte[] data) { |
|
226 | - | int len = data.length; |
|
222 | + | /** |
|
223 | + | * Decodes a Base64 encoded String into a newly-allocated byte array using the |
|
224 | + | * Base64 encoding scheme with an option for the MIME format. |
|
225 | + | * <p> |
|
226 | + | * It first decodes the Base64 encoded String into a sequence of bytes using the |
|
227 | + | * given charset and then decode the bytes by calling the |
|
228 | + | * {@link #decode(byte[])} method. |
|
229 | + | * |
|
230 | + | * @param data the string to decode |
|
231 | + | * @param isMime {@code true} if the data is encoded in the MIME format |
|
232 | + | * @param charset The charset to be used to encode the String |
|
233 | + | * @return A newly-allocated byte array containing the resulting decoded bytes, |
|
234 | + | * or {@code null} if the data is not in valid Base64 scheme |
|
235 | + | */ |
|
236 | + | public static byte[] decode(String data, Charset charset, boolean isMime) { |
|
237 | + | return decode(data.getBytes(charset), isMime); |
|
238 | + | } |
|
227 | 239 | ||
228 | - | if (len == 0) { |
|
229 | - | return data; |
|
230 | - | } else if (len < 2) { |
|
240 | + | /** |
|
241 | + | * Decodes bytes from the specified byte array into a newly-allocated byte array |
|
242 | + | * using the Base64 encoding scheme with an option for the MIME format. |
|
243 | + | * <p> |
|
244 | + | * It uses "The Base 64 Alphabet" as specified in Table 1 of RFC 4648. |
|
245 | + | * |
|
246 | + | * @param data the byte array to decode |
|
247 | + | * @param offset offset within the array of the first byte to be decoded |
|
248 | + | * @param length number of bytes to be encoded |
|
249 | + | * @param isMime {@code true} if the data is encoded in the MIME format |
|
250 | + | * @return A newly-allocated byte array containing the resulting decoded bytes, |
|
251 | + | * or {@code null} if the data is not in valid Base64 scheme |
|
252 | + | */ |
|
253 | + | public static byte[] decode(byte[] data, int offset, int length, boolean isMime) { |
|
254 | + | int end = offset+length; |
|
255 | + | int origEnd = end; |
|
256 | + | int ignored = 0; |
|
257 | + | ||
258 | + | if (length == 0) { |
|
259 | + | return EMPTY; |
|
260 | + | } else if (length < 2) { |
|
231 | 261 | return null; |
|
232 | 262 | } |
|
233 | 263 | ||
234 | - | if (data[len - 1] == PAD) { |
|
235 | - | --len; |
|
236 | - | if (data[len - 1] == PAD) { |
|
237 | - | --len; |
|
264 | + | if (isMime) { |
|
265 | + | for (int i=offset; i<end; ++i) { |
|
266 | + | byte b = data[i]; |
|
267 | + | ||
268 | + | if (b == PAD) { |
|
269 | + | end = i; |
|
270 | + | length = end - offset; |
|
271 | + | break; |
|
272 | + | } |
|
273 | + | if (DECODING[b & 0xff] == -1) { |
|
274 | + | ++ignored; |
|
275 | + | } |
|
276 | + | } |
|
277 | + | } |
|
278 | + | else if (data[end - 1] == PAD) { |
|
279 | + | --end; |
|
280 | + | --length; |
|
281 | + | if (data[end - 1] == PAD) { |
|
282 | + | --end; |
|
283 | + | --length; |
|
238 | 284 | } |
|
239 | 285 | } |
|
240 | 286 | ||
241 | - | if (len == 0) { |
|
242 | - | return new byte[0]; |
|
287 | + | if (length == 0) { |
|
288 | + | return EMPTY; |
|
243 | 289 | } |
|
244 | 290 | ||
245 | - | int calcLen = (len / 4) * 3; |
|
291 | + | int calcLen = ((length-ignored) / 4) * 3; |
|
246 | 292 | ||
247 | - | switch (len & 0x03) { |
|
293 | + | switch ((length-ignored) & 0x03) { |
|
248 | 294 | case 1: |
|
249 | 295 | return null; |
|
250 | 296 |
259 | 305 | } |
|
260 | 306 | ||
261 | 307 | byte[] decoded = new byte[calcLen]; |
|
262 | - | int d = 0, i = 0; |
|
263 | - | int v, jlen, shift; |
|
264 | - | ||
265 | - | for (; i < len; i += 4) { |
|
266 | - | jlen = len - i; |
|
267 | - | if (jlen > 4) { |
|
268 | - | jlen = 4; |
|
269 | - | shift = 0; |
|
270 | - | } else { |
|
271 | - | shift = (4 - jlen) * 6; |
|
272 | - | } |
|
273 | - | ||
274 | - | v = 0; |
|
275 | - | for (int j = 0; j < jlen; ++j) { |
|
276 | - | byte c = DECODING[data[i + j] & 0xff]; |
|
277 | - | ||
278 | - | if (c == -1) { |
|
279 | - | return null; |
|
308 | + | int d = 0; |
|
309 | + | int v = 0, vcount = 0; |
|
310 | + | ||
311 | + | for (int i=offset; i<end; ++i) { |
|
312 | + | int c = DECODING[data[i] & 0xff]; |
|
313 | + | ||
314 | + | if (c == -1) { |
|
315 | + | if (isMime) { |
|
316 | + | continue; |
|
280 | 317 | } |
|
281 | - | v <<= 6; |
|
282 | - | v |= c; |
|
318 | + | return null; |
|
283 | 319 | } |
|
284 | - | if (shift > 0) { |
|
285 | - | decoded[d++] = (byte) (v >> (16 - shift)); |
|
286 | - | if (shift == 6) { |
|
287 | - | decoded[d++] = (byte) (v >> (8 - shift)); |
|
288 | - | } |
|
289 | - | } else { |
|
320 | + | v <<= 6; |
|
321 | + | v |= c; |
|
322 | + | if (vcount == 3) { |
|
290 | 323 | decoded[d++] = (byte) (v >> 16); |
|
291 | 324 | decoded[d++] = (byte) (v >> 8); |
|
292 | 325 | decoded[d++] = (byte) v; |
|
326 | + | vcount = 0; |
|
327 | + | v = 0; |
|
328 | + | } |
|
329 | + | else { |
|
330 | + | ++vcount; |
|
293 | 331 | } |
|
294 | 332 | } |
|
333 | + | ||
334 | + | if (vcount > 0) { |
|
335 | + | int shift = (4 - vcount) * 6; |
|
295 | 336 | ||
337 | + | decoded[d++] = (byte) (v >> (16 - shift)); |
|
338 | + | if (shift == 6) { |
|
339 | + | decoded[d++] = (byte) (v >> (8 - shift)); |
|
340 | + | } |
|
341 | + | } |
|
342 | + | ||
343 | + | if (isMime && end < origEnd) { |
|
344 | + | for (; end < origEnd; ++end) { |
|
345 | + | byte b = data[end]; |
|
346 | + | ||
347 | + | if (b == PAD) { |
|
348 | + | continue; |
|
349 | + | } |
|
350 | + | if (DECODING[b & 0xff] != -1) { |
|
351 | + | return null; |
|
352 | + | } |
|
353 | + | } |
|
354 | + | } |
|
296 | 355 | return decoded; |
|
297 | 356 | } |
|
298 | 357 | ||
299 | - | static byte[] decode0(String data, Charset charset) { |
|
300 | - | return decode0(data.getBytes(charset)); |
|
301 | - | } |
|
302 | - | ||
303 | 358 | } |
1 | + | /* |
|
2 | + | * -------------------------------- MIT License -------------------------------- |
|
3 | + | * |
|
4 | + | * Copyright (c) 2021 SNF4J contributors |
|
5 | + | * |
|
6 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 | + | * of this software and associated documentation files (the "Software"), to deal |
|
8 | + | * in the Software without restriction, including without limitation the rights |
|
9 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 | + | * copies of the Software, and to permit persons to whom the Software is |
|
11 | + | * furnished to do so, subject to the following conditions: |
|
12 | + | * |
|
13 | + | * The above copyright notice and this permission notice shall be included in all |
|
14 | + | * copies or substantial portions of the Software. |
|
15 | + | * |
|
16 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
17 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
18 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
19 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
20 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
21 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
22 | + | * SOFTWARE. |
|
23 | + | * |
|
24 | + | * ----------------------------------------------------------------------------- |
|
25 | + | */ |
|
26 | + | package org.snf4j.core.session.ssl; |
|
27 | + | ||
28 | + | import java.util.ArrayList; |
|
29 | + | import java.util.HashSet; |
|
30 | + | import java.util.List; |
|
31 | + | import java.util.Set; |
|
32 | + | ||
33 | + | import javax.net.ssl.SSLContext; |
|
34 | + | import javax.net.ssl.SSLEngine; |
|
35 | + | ||
36 | + | class ProtocolDefaults { |
|
37 | + | ||
38 | + | final static String TLS = "TLS"; |
|
39 | + | ||
40 | + | final static String DTLS = "DTLS"; |
|
41 | + | ||
42 | + | static volatile ProtocolDefaults tlsDefaults; |
|
43 | + | ||
44 | + | static volatile ProtocolDefaults dtlsDefaults; |
|
45 | + | ||
46 | + | final static String[] PROTOCOLS = new String[] { |
|
47 | + | "TLSv1.3", |
|
48 | + | "TLSv1.2", |
|
49 | + | "TLSv1.1", |
|
50 | + | "TLSv1", |
|
51 | + | "DTLSv1.2", |
|
52 | + | "DTLSv1.0" |
|
53 | + | }; |
|
54 | + | ||
55 | + | final static String[] CIPHERS = new String[] { |
|
56 | + | "TLS_AES_128_GCM_SHA256", |
|
57 | + | "TLS_AES_256_GCM_SHA384", |
|
58 | + | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", |
|
59 | + | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", |
|
60 | + | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", |
|
61 | + | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", |
|
62 | + | "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", |
|
63 | + | "TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", |
|
64 | + | "TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", |
|
65 | + | "TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256" |
|
66 | + | }; |
|
67 | + | ||
68 | + | private final Set<String> supportedCiphers; |
|
69 | + | ||
70 | + | private final String[] defaultCiphers; |
|
71 | + | ||
72 | + | private final Set<String> supportedProtocols; |
|
73 | + | ||
74 | + | private final String[] defaultProtocols; |
|
75 | + | ||
76 | + | ProtocolDefaults(String protocol) { |
|
77 | + | SSLEngine engine = ProtocolDefaults.defaultEngine(protocol); |
|
78 | + | ||
79 | + | supportedCiphers = ProtocolDefaults.supportedCiphers(engine); |
|
80 | + | defaultCiphers = ProtocolDefaults.defaultCiphers(engine, supportedCiphers); |
|
81 | + | supportedProtocols = ProtocolDefaults.supportedProtocols(engine); |
|
82 | + | defaultProtocols = ProtocolDefaults.defaultProtocols(engine, supportedProtocols); |
|
83 | + | } |
|
84 | + | ||
85 | + | Set<String> supportedCiphers() { |
|
86 | + | return supportedCiphers; |
|
87 | + | } |
|
88 | + | ||
89 | + | String[] defaultCiphers() { |
|
90 | + | return defaultCiphers; |
|
91 | + | } |
|
92 | + | ||
93 | + | Set<String> supportedProtocols() { |
|
94 | + | return supportedProtocols; |
|
95 | + | } |
|
96 | + | ||
97 | + | String[] defaultProtocols() { |
|
98 | + | return defaultProtocols; |
|
99 | + | } |
|
100 | + | ||
101 | + | static ProtocolDefaults instance(SSLEngine engine) { |
|
102 | + | return instance(isDtls(engine)); |
|
103 | + | } |
|
104 | + | ||
105 | + | static ProtocolDefaults instance(boolean dtls) { |
|
106 | + | if (dtls) { |
|
107 | + | if (dtlsDefaults == null) { |
|
108 | + | synchronized (ProtocolDefaults.class) { |
|
109 | + | if (dtlsDefaults == null) { |
|
110 | + | dtlsDefaults = new ProtocolDefaults(DTLS); |
|
111 | + | } |
|
112 | + | } |
|
113 | + | } |
|
114 | + | return dtlsDefaults; |
|
115 | + | } |
|
116 | + | if (tlsDefaults == null) { |
|
117 | + | synchronized (ProtocolDefaults.class) { |
|
118 | + | if (tlsDefaults == null) { |
|
119 | + | tlsDefaults = new ProtocolDefaults(TLS); |
|
120 | + | } |
|
121 | + | } |
|
122 | + | } |
|
123 | + | return tlsDefaults; |
|
124 | + | } |
|
125 | + | ||
126 | + | static boolean isDtls(SSLEngine engine) { |
|
127 | + | String[] protocols = engine.getSupportedProtocols(); |
|
128 | + | ||
129 | + | for (String protocol: protocols) { |
|
130 | + | if (protocol.startsWith(DTLS)) { |
|
131 | + | return true; |
|
132 | + | } |
|
133 | + | } |
|
134 | + | return false; |
|
135 | + | } |
|
136 | + | ||
137 | + | static SSLEngine defaultEngine(String protocol) throws Error { |
|
138 | + | SSLContext context; |
|
139 | + | ||
140 | + | try { |
|
141 | + | context = SSLContext.getInstance(protocol); |
|
142 | + | context.init(null, null, null); |
|
143 | + | } |
|
144 | + | catch (Exception e) { |
|
145 | + | throw new Error("Initialization of SSL context for protocol " + protocol + " failed", e); |
|
146 | + | } |
|
147 | + | return context.createSSLEngine(); |
|
148 | + | } |
|
149 | + | ||
150 | + | static Set<String> supportedCiphers(SSLEngine engine) { |
|
151 | + | String[] supported = engine.getSupportedCipherSuites(); |
|
152 | + | Set<String> ciphers = new HashSet<String>(supported.length); |
|
153 | + | ||
154 | + | for (String cipher: supported) { |
|
155 | + | ciphers.add(cipher); |
|
156 | + | } |
|
157 | + | return ciphers; |
|
158 | + | } |
|
159 | + | ||
160 | + | static String[] defaultCiphers(SSLEngine engine, Set<String> supportedCiphers) { |
|
161 | + | List<String> defaultList = new ArrayList<String>(CIPHERS.length); |
|
162 | + | ||
163 | + | for (String cipher: CIPHERS) { |
|
164 | + | if (supportedCiphers.contains(cipher)) { |
|
165 | + | defaultList.add(cipher); |
|
166 | + | } |
|
167 | + | } |
|
168 | + | if (defaultList.isEmpty()) { |
|
169 | + | String[] ciphers = engine.getEnabledCipherSuites(); |
|
170 | + | for (String cipher: ciphers) { |
|
171 | + | defaultList.add(cipher); |
|
172 | + | } |
|
173 | + | } |
|
174 | + | return defaultList.toArray(new String[defaultList.size()]); |
|
175 | + | } |
|
176 | + | ||
177 | + | static Set<String> supportedProtocols(SSLEngine engine) { |
|
178 | + | String[] supported = engine.getSupportedProtocols(); |
|
179 | + | Set<String> protocols = new HashSet<String>(supported.length); |
|
180 | + | ||
181 | + | for (String protocol: supported) { |
|
182 | + | protocols.add(protocol); |
|
183 | + | } |
|
184 | + | return protocols; |
|
185 | + | } |
|
186 | + | ||
187 | + | static String[] defaultProtocols(SSLEngine engine, Set<String> supportedPtotocols) { |
|
188 | + | List<String> defaultList = new ArrayList<String>(PROTOCOLS.length); |
|
189 | + | ||
190 | + | for (String protocol: PROTOCOLS) { |
|
191 | + | if (supportedPtotocols.contains(protocol)) { |
|
192 | + | defaultList.add(protocol); |
|
193 | + | } |
|
194 | + | } |
|
195 | + | if (defaultList.isEmpty()) { |
|
196 | + | String[] protocols = engine.getEnabledProtocols(); |
|
197 | + | for (String protocol: protocols) { |
|
198 | + | defaultList.add(protocol); |
|
199 | + | } |
|
200 | + | } |
|
201 | + | return defaultList.toArray(new String[defaultList.size()]); |
|
202 | + | } |
|
203 | + | } |
1 | + | /* |
|
2 | + | * -------------------------------- MIT License -------------------------------- |
|
3 | + | * |
|
4 | + | * Copyright (c) 2021 SNF4J contributors |
|
5 | + | * |
|
6 | + | * Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 | + | * of this software and associated documentation files (the "Software"), to deal |
|
8 | + | * in the Software without restriction, including without limitation the rights |
|
9 | + | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 | + | * copies of the Software, and to permit persons to whom the Software is |
|
11 | + | * furnished to do so, subject to the following conditions: |
|
12 | + | * |
|
13 | + | * The above copyright notice and this permission notice shall be included in all |
|
14 | + | * copies or substantial portions of the Software. |
|
15 | + | * |
|
16 | + | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
17 | + | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
18 | + | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
19 | + | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
20 | + | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
21 | + | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
22 | + | * SOFTWARE. |
|
23 | + | * |
|
24 | + | * ----------------------------------------------------------------------------- |
|
25 | + | */ |
|
26 | + | package org.snf4j.core.session.ssl; |
|
27 | + | ||
28 | + | import java.lang.reflect.Method; |
|
29 | + | ||
30 | + | import javax.net.ssl.SSLContext; |
|
31 | + | import javax.net.ssl.SSLEngine; |
|
32 | + | import javax.net.ssl.SSLParameters; |
|
33 | + | ||
34 | + | import org.snf4j.core.session.SSLEngineCreateException; |
|
35 | + | ||
36 | + | /** |
|
37 | + | * A builder for the {@link SSLEngine}. |
|
38 | + | * |
|
39 | + | * @author <a href="http://snf4j.org">SNF4J.ORG</a> |
|
40 | + | */ |
|
41 | + | public class SSLEngineBuilder implements Cloneable { |
|
42 | + | ||
43 | + | private final static Method ENABLE_RETRANSMISSIONS = get("setEnableRetransmissions", boolean.class); |
|
44 | + | ||
45 | + | private final static Method MAXIMUM_PACKET_SIZE = get("setMaximumPacketSize", int.class); |
|
46 | + | ||
47 | + | private final boolean forServer; |
|
48 | + | ||
49 | + | private final SSLContext context; |
|
50 | + | ||
51 | + | private String[] protocols; |
|
52 | + | ||
53 | + | private ProtocolFilter protocolFilter = DefaultCipherProtocolFilters.INSATNCE; |
|
54 | + | ||
55 | + | private String[] ciphers; |
|
56 | + | ||
57 | + | private CipherFilter cipherFilter = DefaultCipherProtocolFilters.INSATNCE; |
|
58 | + | ||
59 | + | private Boolean enableRetransmissions; //JDK9 |
|
60 | + | ||
61 | + | private int maximumPacketSize = -1; //JDK9 |
|
62 | + | ||
63 | + | private Boolean useCiphersOrder; |
|
64 | + | ||
65 | + | private ClientAuth clientAuth = ClientAuth.NONE; |
|
66 | + | ||
67 | + | SSLEngineBuilder(SSLContext context, boolean forServer) { |
|
68 | + | this.context = context; |
|
69 | + | this.forServer = forServer; |
|
70 | + | } |
|
71 | + | ||
72 | + | static Method get(String name, Class<?>... parameterTypes) { |
|
73 | + | try { |
|
74 | + | Method method = SSLParameters.class.getDeclaredMethod(name, parameterTypes); |
|
75 | + | ||
76 | + | method.setAccessible(true); |
|
77 | + | return method; |
|
78 | + | } catch (Exception e) {} |
|
79 | + | return null; |
|
80 | + | } |
|
81 | + | ||
82 | + | static void set(Method method, SSLParameters params, Object... args) throws Exception { |
|
83 | + | if (method != null) { |
|
84 | + | method.invoke(params, args); |
|
85 | + | } |
|
86 | + | } |
|
87 | + | ||
88 | + | /** |
|
89 | + | * Creates a builder for a client-side {@link SSLEngine}. |
|
90 | + | * |
|
91 | + | * @param context the SSL context used by the builder to create |
|
92 | + | * {@link SSLEngine} |
|
93 | + | * @return a builder for a client-side {@link SSLEngine} |
|
94 | + | */ |
|
95 | + | public static SSLEngineBuilder forClient(SSLContext context) { |
|
96 | + | return new SSLEngineBuilder(context, false); |
|
97 | + | } |
|
98 | + | ||
99 | + | /** |
|
100 | + | * Creates a builder for a server-side {@link SSLEngine}. |
|
101 | + | * |
|
102 | + | * @param context the SSL context used by the builder to create |
|
103 | + | * {@link SSLEngine} |
|
104 | + | * @return a builder for a server-side {@link SSLEngine} |
|
105 | + | */ |
|
106 | + | public static SSLEngineBuilder forServer(SSLContext context) { |
|
107 | + | return new SSLEngineBuilder(context, true); |
|
108 | + | } |
|
109 | + | ||
110 | + | /** |
|
111 | + | * Returns the SSL context used by the builder to create {@link SSLEngine} |
|
112 | + | * |
|
113 | + | * @return the SSL context |
|
114 | + | */ |
|
115 | + | public SSLContext context() { |
|
116 | + | return context; |
|
117 | + | } |
|
118 | + | ||
119 | + | /** |
|
120 | + | * Tells if the builder if for a server-side {@link SSLEngine}. |
|
121 | + | * |
|
122 | + | * @return {@code true} if the builder if for a server-side {@link SSLEngine} |
|
123 | + | */ |
|
124 | + | public boolean isForServer() { |
|
125 | + | return forServer; |
|
126 | + | } |
|
127 | + | ||
128 | + | /** |
|
129 | + | * Tells if the builder if for a client-side {@link SSLEngine}. |
|
130 | + | * |
|
131 | + | * @return {@code true} if the builder if for a client-side {@link SSLEngine} |
|
132 | + | */ |
|
133 | + | public boolean isForClient() { |
|
134 | + | return !forServer; |
|
135 | + | } |
|
136 | + | ||
137 | + | /** |
|
138 | + | * Configures protocol versions to enable, or {@code null} to enable the |
|
139 | + | * recommended protocol versions. |
|
140 | + | * |
|
141 | + | * @param protocols the protocol versions |
|
142 | + | * @return this builder |
|
143 | + | */ |
|
144 | + | public SSLEngineBuilder protocols(String... protocols) { |
|
145 | + | this.protocols = protocols == null ? null : protocols.clone(); |
|
146 | + | return this; |
|
147 | + | } |
|
148 | + | ||
149 | + | /** |
|
150 | + | * Configures a filter for protocol versions to enable, or {@code null} to use |
|
151 | + | * the default filter. |
|
152 | + | * |
|
153 | + | * @param filter the protocol filter |
|
154 | + | * @return this builder |
|
155 | + | */ |
|
156 | + | public SSLEngineBuilder protocolFilter(ProtocolFilter filter) { |
|
157 | + | protocolFilter = filter == null ? DefaultCipherProtocolFilters.INSATNCE : filter; |
|
158 | + | return this; |
|
159 | + | } |
|
160 | + | ||
161 | + | /** |
|
162 | + | * Configures cipher suites to enable, or {@code null} to enable the |
|
163 | + | * recommended cipher suites. |
|
164 | + | * |
|
165 | + | * @param ciphers the cipher suites |
|
166 | + | * @return this builder |
|
167 | + | */ |
|
168 | + | public SSLEngineBuilder ciphers(String... ciphers) { |
|
169 | + | this.ciphers = ciphers == null ? null : ciphers.clone(); |
|
170 | + | return this; |
|
171 | + | } |
|
172 | + | ||
173 | + | /** |
|
174 | + | * Configures a filter for cipher suites to enable, or {@code null} to use |
|
175 | + | * the default filter. |
|
176 | + | * |
|
177 | + | * @param filter the cipher filter |
|
178 | + | * @return this builder |
|
179 | + | */ |
|
180 | + | public SSLEngineBuilder cipherFilter(CipherFilter filter) { |
|
181 | + | cipherFilter = filter == null ? DefaultCipherProtocolFilters.INSATNCE : filter; |
|
182 | + | return this; |
|
183 | + | } |
|
184 | + | ||
185 | + | /** |
|
186 | + | * Configures if DTLS handshake retransmissions should be enabled. |
|
187 | + | * <p> |
|
188 | + | * NOTE: It requires Java 9 or newer. |
|
189 | + | * |
|
190 | + | * @param enable {@code true} to enable DTLS handshake retransmissions. |
|
191 | + | * @return this builder |
|
192 | + | */ |
|
193 | + | public SSLEngineBuilder enableRetransmissions(boolean enable) { |
|
194 | + | enableRetransmissions = enable ? Boolean.TRUE : Boolean.FALSE; |
|
195 | + | return this; |
|
196 | + | } |
|
197 | + | ||
198 | + | /** |
|
199 | + | * Configures the maximum expected network packet size. |
|
200 | + | * <p> |
|
201 | + | * NOTE: It requires Java 9 or newer. |
|
202 | + | * |
|
203 | + | * @param maxSize the maximum expected network packet size in bytes, or 0 to use |
|
204 | + | * the default value that is specified by the underlying |
|
205 | + | * implementation. |
|
206 | + | * @return this builder |
|
207 | + | */ |
|
208 | + | public SSLEngineBuilder maximumPacketSize(int maxSize) { |
|
209 | + | maximumPacketSize = maxSize; |
|
210 | + | return this; |
|
211 | + | } |
|
212 | + | ||
213 | + | /** |
|
214 | + | * Configures if the local cipher suites preferences should be honored during |
|
215 | + | * SSL/TLS/DTLS handshaking |
|
216 | + | * |
|
217 | + | * @param useOrder {@code true} to honor the local cipher suites preferences |
|
218 | + | * @return this builder |
|
219 | + | */ |
|
220 | + | public SSLEngineBuilder useCiphersOrder(boolean useOrder) { |
|
221 | + | useCiphersOrder = useOrder ? Boolean.TRUE : Boolean.FALSE; |
|
222 | + | return this; |
|
223 | + | } |
|
224 | + | ||
225 | + | /** |
|
226 | + | * Configures the client authentication mode for a server-side |
|
227 | + | * {@link SSLEngine}. |
|
228 | + | * |
|
229 | + | * @param clientAuth the client authentication mode. |
|
230 | + | * @return this builder |
|
231 | + | */ |
|
232 | + | public SSLEngineBuilder clientAuth(ClientAuth clientAuth) { |
|
233 | + | this.clientAuth = clientAuth; |
|
234 | + | return this; |
|
235 | + | } |
|
236 | + | ||
237 | + | private SSLEngine configure(SSLEngine engine) throws Exception { |
|
238 | + | ProtocolDefaults defaults = ProtocolDefaults.instance(engine); |
|
239 | + | ||
240 | + | engine.setUseClientMode(!forServer); |
|
241 | + | engine.setEnabledProtocols(protocolFilter.filterProtocols( |
|
242 | + | protocols, |
|
243 | + | defaults.defaultProtocols(), |
|
244 | + | defaults.supportedProtocols() |
|
245 | + | )); |
|
246 | + | engine.setEnabledCipherSuites(cipherFilter.filterCiphers( |
|
247 | + | ciphers, |
|
248 | + | defaults.defaultCiphers(), |
|
249 | + | defaults.supportedCiphers() |
|
250 | + | )); |
|
251 | + | ||
252 | + | if (forServer) { |
|
253 | + | switch (clientAuth) { |
|
254 | + | case REQUESTED: |
|
255 | + | engine.setWantClientAuth(true); |
|
256 | + | break; |
|
257 | + | ||
258 | + | case REQUIRED: |
|
259 | + | engine.setNeedClientAuth(true); |
|
260 | + | break; |
|
261 | + | ||
262 | + | default: |
|
263 | + | } |
|
264 | + | } |
|
265 | + | ||
266 | + | if (maximumPacketSize >= 0 || enableRetransmissions != null || useCiphersOrder != null) { |
|
267 | + | SSLParameters params = engine.getSSLParameters(); |
|
268 | + | ||
269 | + | if (useCiphersOrder != null) { |
|
270 | + | params.setUseCipherSuitesOrder(useCiphersOrder); |
|
271 | + | } |
|
272 | + | if (maximumPacketSize >= 0) { |
|
273 | + | set(MAXIMUM_PACKET_SIZE, params, maximumPacketSize); |
|
274 | + | } |
|
275 | + | if (enableRetransmissions != null) { |
|
276 | + | set(ENABLE_RETRANSMISSIONS, params, enableRetransmissions); |
|
277 | + | } |
|
278 | + | engine.setSSLParameters(params); |
|
279 | + | } |
|
280 | + | ||
281 | + | return engine; |
|
282 | + | } |
|
283 | + | ||
284 | + | /** |
|
285 | + | * Builds a new {@link SSLEngine} instance based on the current configuration |
|
286 | + | * settings. |
|
287 | + | * |
|
288 | + | * @return the new {@link SSLEngine} instance. |
|
289 | + | * @throws SSLEngineCreateException if a failure occurred while building the |
|
290 | + | * {@link SSLEngine} instance |
|
291 | + | */ |
|
292 | + | public SSLEngine build() throws SSLEngineCreateException { |
|
293 | + | try { |
|
294 | + | return configure(context.createSSLEngine()); |
|
295 | + | } |
|
296 | + | catch (Exception e) { |
|
297 | + | throw new SSLEngineCreateException("Building of SSL engine failed", e); |
|
298 | + | } |
|
299 | + | } |
|
300 | + | ||
301 | + | /** |
|
302 | + | * Builds a new {@link SSLEngine} instance based on the current configuration |
|
303 | + | * settings and advisory peer information. |
|
304 | + | * |
|
305 | + | * @param peerHost the non-authoritative name of the host |
|
306 | + | * @param peerPort the non-authoritative port |
|
307 | + | * @return the new {@link SSLEngine} instance. |
|
308 | + | * @throws SSLEngineCreateException if a failure occurred while building the |
|
309 | + | * {@link SSLEngine} instance |
|
310 | + | * @see SSLContext#createSSLEngine(String, int) |
|
311 | + | */ |
|
312 | + | public SSLEngine build(String peerHost, int peerPort) throws SSLEngineCreateException { |
|
313 | + | try { |
|
314 | + | return configure(context.createSSLEngine(peerHost, peerPort)); |
|
315 | + | } |
|
316 | + | catch (Exception e) { |
|
317 | + | throw new SSLEngineCreateException("Building of SSL engine with peer information failed", e); |
|
318 | + | } |
|
319 | + | } |
|
320 | + | ||
321 | + | SSLEngineBuilder superClone() throws CloneNotSupportedException { |
|
322 | + | return (SSLEngineBuilder) super.clone(); |
|
323 | + | } |
|
324 | + | ||
325 | + | /** |
|
326 | + | * Generates a new copy of this builder. Subsequent changes to this builder will |
|
327 | + | * not affect the new copy, and vice versa. |
|
328 | + | */ |
|
329 | + | @Override |
|
330 | + | public SSLEngineBuilder clone() { |
|
331 | + | SSLEngineBuilder b; |
|
332 | + | ||
333 | + | try { |
|
334 | + | b = superClone(); |
|
335 | + | } catch (CloneNotSupportedException e) { |
|
336 | + | throw new RuntimeException(e); |
|
337 | + | } |
|
338 | + | b.protocols(protocols); |
|
339 | + | b.ciphers(ciphers); |
|
340 | + | return b; |
|
341 | + | } |
|
342 | + | } |
Learn more Showing 10 files with coverage changes found.
snf4j-core/src/main/java/org/snf4j/core/session/ssl/ClientAuth.java
snf4j-core/src/main/java/org/snf4j/core/session/ssl/SSLContextBuilder.java
snf4j-core/src/main/java/org/snf4j/core/session/ssl/SSLContextCreateException.java
snf4j-core/src/main/java/org/snf4j/core/session/ssl/DefaultCipherProtocolFilters.java
snf4j-core/src/main/java/org/snf4j/core/session/ssl/SupportedCipherProtocolFilters.java
snf4j-core/src/main/java/org/snf4j/core/util/PemUtil.java
snf4j-core/src/main/java/org/snf4j/core/session/ssl/ProtocolDefaults.java
snf4j-core/src/main/java/org/snf4j/core/session/ssl/SSLEngineBuilder.java
snf4j-core/src/main/java/org/snf4j/core/AbstractEngineHandler.java
snf4j-core/src/main/java/org/snf4j/core/session/DefaultSessionConfig.java
Files | Coverage |
---|---|
snf4j-core-log4j2/src/main/java/org/snf4j/core/logger/impl | 96.66% |
snf4j-core-slf4j/src/main/java/org/snf4j/core/logger/impl | 96.66% |
snf4j-core/src/main/java/org/snf4j/core | 0.03% 97.88% |
snf4j-sctp/src/main/java/org/snf4j/core | 98.56% |
snf4j-websocket/src/main/java/org/snf4j/websocket | -0.01% 99.37% |
Project Totals (227 files) | 98.17% |
6ef10ec
69ddc04