1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.security; |
7 | |
8 | import java.io.ByteArrayInputStream; |
9 | import java.io.ByteArrayOutputStream; |
10 | import java.io.IOException; |
11 | import java.io.InputStream; |
12 | import java.io.OutputStream; |
13 | import java.net.InetAddress; |
14 | import java.net.InetSocketAddress; |
15 | import java.net.ServerSocket; |
16 | import java.net.Socket; |
17 | import java.security.KeyFactory; |
18 | import java.security.KeyStore; |
19 | import java.security.PrivateKey; |
20 | import java.security.cert.Certificate; |
21 | import java.security.cert.CertificateFactory; |
22 | import java.security.spec.PKCS8EncodedKeySpec; |
23 | import java.util.Arrays; |
24 | import java.util.Collections; |
25 | import java.util.HashSet; |
26 | import java.util.Properties; |
27 | import javax.net.ServerSocketFactory; |
28 | import javax.net.ssl.SSLServerSocket; |
29 | import javax.net.ssl.SSLServerSocketFactory; |
30 | import javax.net.ssl.SSLSocket; |
31 | import javax.net.ssl.SSLSocketFactory; |
32 | |
33 | import org.h2.api.ErrorCode; |
34 | import org.h2.engine.SysProperties; |
35 | import org.h2.message.DbException; |
36 | import org.h2.store.fs.FileUtils; |
37 | import org.h2.util.IOUtils; |
38 | import org.h2.util.StringUtils; |
39 | |
40 | /** |
41 | * A factory to create new block cipher objects. |
42 | */ |
43 | public class CipherFactory { |
44 | |
45 | /** |
46 | * The default password to use for the .h2.keystore file |
47 | */ |
48 | public static final String KEYSTORE_PASSWORD = |
49 | "h2pass"; |
50 | |
51 | private static final String KEYSTORE = |
52 | "~/.h2.keystore"; |
53 | private static final String KEYSTORE_KEY = |
54 | "javax.net.ssl.keyStore"; |
55 | private static final String KEYSTORE_PASSWORD_KEY = |
56 | "javax.net.ssl.keyStorePassword"; |
57 | |
58 | private CipherFactory() { |
59 | // utility class |
60 | } |
61 | |
62 | /** |
63 | * Get a new block cipher object for the given algorithm. |
64 | * |
65 | * @param algorithm the algorithm |
66 | * @return a new cipher object |
67 | */ |
68 | public static BlockCipher getBlockCipher(String algorithm) { |
69 | if ("XTEA".equalsIgnoreCase(algorithm)) { |
70 | return new XTEA(); |
71 | } else if ("AES".equalsIgnoreCase(algorithm)) { |
72 | return new AES(); |
73 | } else if ("FOG".equalsIgnoreCase(algorithm)) { |
74 | return new Fog(); |
75 | } |
76 | throw DbException.get(ErrorCode.UNSUPPORTED_CIPHER, algorithm); |
77 | } |
78 | |
79 | /** |
80 | * Create a secure client socket that is connected to the given address and |
81 | * port. |
82 | * |
83 | * @param address the address to connect to |
84 | * @param port the port |
85 | * @return the socket |
86 | */ |
87 | public static Socket createSocket(InetAddress address, int port) |
88 | throws IOException { |
89 | Socket socket = null; |
90 | setKeystore(); |
91 | SSLSocketFactory f = (SSLSocketFactory) SSLSocketFactory.getDefault(); |
92 | SSLSocket secureSocket = (SSLSocket) f.createSocket(); |
93 | secureSocket.connect(new InetSocketAddress(address, port), |
94 | SysProperties.SOCKET_CONNECT_TIMEOUT); |
95 | secureSocket.setEnabledProtocols( |
96 | disableSSL(secureSocket.getEnabledProtocols())); |
97 | if (SysProperties.ENABLE_ANONYMOUS_TLS) { |
98 | String[] list = enableAnonymous( |
99 | secureSocket.getEnabledCipherSuites(), |
100 | secureSocket.getSupportedCipherSuites()); |
101 | secureSocket.setEnabledCipherSuites(list); |
102 | } |
103 | socket = secureSocket; |
104 | return socket; |
105 | } |
106 | |
107 | /** |
108 | * Create a secure server socket. If a bind address is specified, the socket |
109 | * is only bound to this address. |
110 | * |
111 | * @param port the port to listen on |
112 | * @param bindAddress the address to bind to, or null to bind to all |
113 | * addresses |
114 | * @return the server socket |
115 | */ |
116 | public static ServerSocket createServerSocket(int port, |
117 | InetAddress bindAddress) throws IOException { |
118 | ServerSocket socket = null; |
119 | setKeystore(); |
120 | ServerSocketFactory f = SSLServerSocketFactory.getDefault(); |
121 | SSLServerSocket secureSocket; |
122 | if (bindAddress == null) { |
123 | secureSocket = (SSLServerSocket) f.createServerSocket(port); |
124 | } else { |
125 | secureSocket = (SSLServerSocket) f.createServerSocket(port, 0, bindAddress); |
126 | } |
127 | secureSocket.setEnabledProtocols( |
128 | disableSSL(secureSocket.getEnabledProtocols())); |
129 | if (SysProperties.ENABLE_ANONYMOUS_TLS) { |
130 | String[] list = enableAnonymous( |
131 | secureSocket.getEnabledCipherSuites(), |
132 | secureSocket.getSupportedCipherSuites()); |
133 | secureSocket.setEnabledCipherSuites(list); |
134 | } |
135 | |
136 | socket = secureSocket; |
137 | return socket; |
138 | } |
139 | |
140 | private static byte[] getKeyStoreBytes(KeyStore store, String password) |
141 | throws IOException { |
142 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); |
143 | try { |
144 | store.store(bout, password.toCharArray()); |
145 | } catch (Exception e) { |
146 | throw DbException.convertToIOException(e); |
147 | } |
148 | return bout.toByteArray(); |
149 | } |
150 | |
151 | /** |
152 | * Get the keystore object using the given password. |
153 | * |
154 | * @param password the keystore password |
155 | * @return the keystore |
156 | */ |
157 | public static KeyStore getKeyStore(String password) throws IOException { |
158 | try { |
159 | // The following source code can be re-generated |
160 | // if you have a keystore file. |
161 | // This code is (hopefully) more Java version independent |
162 | // than using keystores directly. See also: |
163 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4887561 |
164 | // (1.4.2 cannot read keystore written with 1.4.1) |
165 | // --- generated code start --- |
166 | |
167 | KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); |
168 | |
169 | store.load(null, password.toCharArray()); |
170 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
171 | store.load(null, password.toCharArray()); |
172 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec( |
173 | StringUtils.convertHexToBytes( |
174 | "30820277020100300d06092a864886f70d010101" + |
175 | "0500048202613082025d02010002818100dc0a13" + |
176 | "c602b7141110eade2f051b54777b060d0f74e6a1" + |
177 | "10f9cce81159f271ebc88d8e8aa1f743b505fc2e" + |
178 | "7dfe38d33b8d3f64d1b363d1af4d877833897954" + |
179 | "cbaec2fa384c22a415498cf306bb07ac09b76b00" + |
180 | "1cd68bf77ea0a628f5101959cf2993a9c23dbee7" + |
181 | "9b19305977f8715ae78d023471194cc900b231ee" + |
182 | "cb0aaea98d02030100010281810099aa4ff4d0a0" + |
183 | "9a5af0bd953cb10c4d08c3d98df565664ac5582e" + |
184 | "494314d5c3c92dddedd5d316a32a206be4ec0846" + |
185 | "16fe57be15e27cad111aa3c21fa79e32258c6ca8" + |
186 | "430afc69eddd52d3b751b37da6b6860910b94653" + |
187 | "192c0db1d02abcfd6ce14c01f238eec7c20bd3bb" + |
188 | "750940004bacba2880349a9494d10e139ecb2355" + |
189 | "d101024100ffdc3defd9c05a2d377ef6019fa62b" + |
190 | "3fbd5b0020a04cc8533bca730e1f6fcf5dfceea1" + |
191 | "b044fbe17d9eababfbc7d955edad6bc60f9be826" + |
192 | "ad2c22ba77d19a9f65024100dc28d43fdbbc9385" + |
193 | "2cc3567093157702bc16f156f709fb7db0d9eec0" + |
194 | "28f41fd0edcd17224c866e66be1744141fb724a1" + |
195 | "0fd741c8a96afdd9141b36d67fff6309024077b1" + |
196 | "cddbde0f69604bdcfe33263fb36ddf24aa3b9922" + |
197 | "327915b890f8a36648295d0139ecdf68c245652c" + |
198 | "4489c6257b58744fbdd961834a4cab201801a3b1" + |
199 | "e52d024100b17142e8991d1b350a0802624759d4" + |
200 | "8ae2b8071a158ff91fabeb6a8f7c328e762143dc" + |
201 | "726b8529f42b1fab6220d1c676fdc27ba5d44e84" + |
202 | "7c72c52064afd351a902407c6e23fe35bcfcd1a6" + |
203 | "62aa82a2aa725fcece311644d5b6e3894853fd4c" + |
204 | "e9fe78218c957b1ff03fc9e5ef8ffeb6bd58235f" + |
205 | "6a215c97d354fdace7e781e4a63e8b")); |
206 | PrivateKey privateKey = keyFactory.generatePrivate(keySpec); |
207 | Certificate[] certs = { CertificateFactory |
208 | .getInstance("X.509") |
209 | .generateCertificate( |
210 | new ByteArrayInputStream( |
211 | StringUtils.convertHexToBytes( |
212 | "3082018b3081f502044295ce6b300d06092a8648" + |
213 | "86f70d0101040500300d310b3009060355040313" + |
214 | "024832301e170d3035303532363133323630335a" + |
215 | "170d3337303933303036353734375a300d310b30" + |
216 | "0906035504031302483230819f300d06092a8648" + |
217 | "86f70d010101050003818d0030818902818100dc" + |
218 | "0a13c602b7141110eade2f051b54777b060d0f74" + |
219 | "e6a110f9cce81159f271ebc88d8e8aa1f743b505" + |
220 | "fc2e7dfe38d33b8d3f64d1b363d1af4d87783389" + |
221 | "7954cbaec2fa384c22a415498cf306bb07ac09b7" + |
222 | "6b001cd68bf77ea0a628f5101959cf2993a9c23d" + |
223 | "bee79b19305977f8715ae78d023471194cc900b2" + |
224 | "31eecb0aaea98d0203010001300d06092a864886" + |
225 | "f70d01010405000381810083f4401a279453701b" + |
226 | "ef9a7681a5b8b24f153f7d18c7c892133d97bd5f" + |
227 | "13736be7505290a445a7d5ceb75522403e509751" + |
228 | "5cd966ded6351ff60d5193de34cd36e5cb04d380" + |
229 | "398e66286f99923fd92296645fd4ada45844d194" + |
230 | "dfd815e6cd57f385c117be982809028bba1116c8" + |
231 | "5740b3d27a55b1a0948bf291ddba44bed337b9"))), }; |
232 | store.setKeyEntry("h2", privateKey, password.toCharArray(), certs); |
233 | // --- generated code end --- |
234 | return store; |
235 | } catch (Exception e) { |
236 | throw DbException.convertToIOException(e); |
237 | } |
238 | } |
239 | |
240 | private static void setKeystore() throws IOException { |
241 | Properties p = System.getProperties(); |
242 | if (p.getProperty(KEYSTORE_KEY) == null) { |
243 | String fileName = KEYSTORE; |
244 | byte[] data = getKeyStoreBytes(getKeyStore( |
245 | KEYSTORE_PASSWORD), KEYSTORE_PASSWORD); |
246 | boolean needWrite = true; |
247 | if (FileUtils.exists(fileName) && FileUtils.size(fileName) == data.length) { |
248 | // don't need to overwrite the file if it did not change |
249 | InputStream fin = FileUtils.newInputStream(fileName); |
250 | byte[] now = IOUtils.readBytesAndClose(fin, 0); |
251 | if (now != null && Arrays.equals(data, now)) { |
252 | needWrite = false; |
253 | } |
254 | } |
255 | if (needWrite) { |
256 | try { |
257 | OutputStream out = FileUtils.newOutputStream(fileName, false); |
258 | out.write(data); |
259 | out.close(); |
260 | } catch (Exception e) { |
261 | throw DbException.convertToIOException(e); |
262 | } |
263 | } |
264 | String absolutePath = FileUtils.toRealPath(fileName); |
265 | System.setProperty(KEYSTORE_KEY, absolutePath); |
266 | } |
267 | if (p.getProperty(KEYSTORE_PASSWORD_KEY) == null) { |
268 | System.setProperty(KEYSTORE_PASSWORD_KEY, KEYSTORE_PASSWORD); |
269 | } |
270 | } |
271 | |
272 | private static String[] enableAnonymous(String[] enabled, String[] supported) { |
273 | HashSet<String> set = new HashSet<String>(); |
274 | Collections.addAll(set, enabled); |
275 | for (String x : supported) { |
276 | if (!x.startsWith("SSL") && |
277 | x.indexOf("_anon_") >= 0 && |
278 | x.indexOf("_AES_") >= 0 && |
279 | x.indexOf("_SHA") >= 0) { |
280 | set.add(x); |
281 | } |
282 | } |
283 | return set.toArray(new String[0]); |
284 | } |
285 | |
286 | private static String[] disableSSL(String[] enabled) { |
287 | HashSet<String> set = new HashSet<String>(); |
288 | for (String x : enabled) { |
289 | if (!x.startsWith("SSL")) { |
290 | set.add(x); |
291 | } |
292 | } |
293 | return set.toArray(new String[0]); |
294 | } |
295 | |
296 | } |