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.tools; |
7 | |
8 | import java.io.IOException; |
9 | import java.io.InputStream; |
10 | import java.io.OutputStream; |
11 | import java.nio.channels.FileChannel; |
12 | import java.sql.SQLException; |
13 | import java.util.ArrayList; |
14 | |
15 | import org.h2.engine.Constants; |
16 | import org.h2.message.DbException; |
17 | import org.h2.security.SHA256; |
18 | import org.h2.store.FileLister; |
19 | import org.h2.store.FileStore; |
20 | import org.h2.store.fs.FileChannelInputStream; |
21 | import org.h2.store.fs.FileChannelOutputStream; |
22 | import org.h2.store.fs.FilePath; |
23 | import org.h2.store.fs.FilePathEncrypt; |
24 | import org.h2.store.fs.FileUtils; |
25 | import org.h2.util.Tool; |
26 | |
27 | /** |
28 | * Allows changing the database file encryption password or algorithm. |
29 | * <br /> |
30 | * This tool can not be used to change a password of a user. |
31 | * The database must be closed before using this tool. |
32 | * @h2.resource |
33 | */ |
34 | public class ChangeFileEncryption extends Tool { |
35 | |
36 | private String directory; |
37 | private String cipherType; |
38 | private byte[] decrypt; |
39 | private byte[] encrypt; |
40 | private byte[] decryptKey; |
41 | private byte[] encryptKey; |
42 | |
43 | /** |
44 | * Options are case sensitive. Supported options are: |
45 | * <table> |
46 | * <tr><td>[-help] or [-?]</td> |
47 | * <td>Print the list of options</td></tr> |
48 | * <tr><td>[-cipher type]</td> |
49 | * <td>The encryption type (AES)</td></tr> |
50 | * <tr><td>[-dir <dir>]</td> |
51 | * <td>The database directory (default: .)</td></tr> |
52 | * <tr><td>[-db <database>]</td> |
53 | * <td>Database name (all databases if not set)</td></tr> |
54 | * <tr><td>[-decrypt <pwd>]</td> |
55 | * <td>The decryption password (if not set: not yet encrypted)</td></tr> |
56 | * <tr><td>[-encrypt <pwd>]</td> |
57 | * <td>The encryption password (if not set: do not encrypt)</td></tr> |
58 | * <tr><td>[-quiet]</td> |
59 | * <td>Do not print progress information</td></tr> |
60 | * </table> |
61 | * @h2.resource |
62 | * |
63 | * @param args the command line arguments |
64 | */ |
65 | public static void main(String... args) throws SQLException { |
66 | new ChangeFileEncryption().runTool(args); |
67 | } |
68 | |
69 | @Override |
70 | public void runTool(String... args) throws SQLException { |
71 | String dir = "."; |
72 | String cipher = null; |
73 | char[] decryptPassword = null; |
74 | char[] encryptPassword = null; |
75 | String db = null; |
76 | boolean quiet = false; |
77 | for (int i = 0; args != null && i < args.length; i++) { |
78 | String arg = args[i]; |
79 | if (arg.equals("-dir")) { |
80 | dir = args[++i]; |
81 | } else if (arg.equals("-cipher")) { |
82 | cipher = args[++i]; |
83 | } else if (arg.equals("-db")) { |
84 | db = args[++i]; |
85 | } else if (arg.equals("-decrypt")) { |
86 | decryptPassword = args[++i].toCharArray(); |
87 | } else if (arg.equals("-encrypt")) { |
88 | encryptPassword = args[++i].toCharArray(); |
89 | } else if (arg.equals("-quiet")) { |
90 | quiet = true; |
91 | } else if (arg.equals("-help") || arg.equals("-?")) { |
92 | showUsage(); |
93 | return; |
94 | } else { |
95 | showUsageAndThrowUnsupportedOption(arg); |
96 | } |
97 | } |
98 | if ((encryptPassword == null && decryptPassword == null) || cipher == null) { |
99 | showUsage(); |
100 | throw new SQLException( |
101 | "Encryption or decryption password not set, or cipher not set"); |
102 | } |
103 | try { |
104 | process(dir, db, cipher, decryptPassword, encryptPassword, quiet); |
105 | } catch (Exception e) { |
106 | throw DbException.toSQLException(e); |
107 | } |
108 | } |
109 | |
110 | /** |
111 | * Get the file encryption key for a given password. The password must be |
112 | * supplied as char arrays and is cleaned in this method. |
113 | * |
114 | * @param password the password as a char array |
115 | * @return the encryption key |
116 | */ |
117 | private static byte[] getFileEncryptionKey(char[] password) { |
118 | if (password == null) { |
119 | return null; |
120 | } |
121 | return SHA256.getKeyPasswordHash("file", password); |
122 | } |
123 | |
124 | /** |
125 | * Changes the password for a database. The passwords must be supplied as |
126 | * char arrays and are cleaned in this method. The database must be closed |
127 | * before calling this method. |
128 | * |
129 | * @param dir the directory (. for the current directory) |
130 | * @param db the database name (null for all databases) |
131 | * @param cipher the cipher (AES) |
132 | * @param decryptPassword the decryption password as a char array |
133 | * @param encryptPassword the encryption password as a char array |
134 | * @param quiet don't print progress information |
135 | */ |
136 | public static void execute(String dir, String db, String cipher, |
137 | char[] decryptPassword, char[] encryptPassword, boolean quiet) |
138 | throws SQLException { |
139 | try { |
140 | new ChangeFileEncryption().process(dir, db, cipher, |
141 | decryptPassword, encryptPassword, quiet); |
142 | } catch (Exception e) { |
143 | throw DbException.toSQLException(e); |
144 | } |
145 | } |
146 | |
147 | private void process(String dir, String db, String cipher, |
148 | char[] decryptPassword, char[] encryptPassword, boolean quiet) |
149 | throws SQLException { |
150 | dir = FileLister.getDir(dir); |
151 | ChangeFileEncryption change = new ChangeFileEncryption(); |
152 | if (encryptPassword != null) { |
153 | for (char c : encryptPassword) { |
154 | if (c == ' ') { |
155 | throw new SQLException("The file password may not contain spaces"); |
156 | } |
157 | } |
158 | change.encryptKey = FilePathEncrypt.getPasswordBytes(encryptPassword); |
159 | change.encrypt = getFileEncryptionKey(encryptPassword); |
160 | } |
161 | if (decryptPassword != null) { |
162 | change.decryptKey = FilePathEncrypt.getPasswordBytes(decryptPassword); |
163 | change.decrypt = getFileEncryptionKey(decryptPassword); |
164 | } |
165 | change.out = out; |
166 | change.directory = dir; |
167 | change.cipherType = cipher; |
168 | |
169 | ArrayList<String> files = FileLister.getDatabaseFiles(dir, db, true); |
170 | FileLister.tryUnlockDatabase(files, "encryption"); |
171 | files = FileLister.getDatabaseFiles(dir, db, false); |
172 | if (files.size() == 0 && !quiet) { |
173 | printNoDatabaseFilesFound(dir, db); |
174 | } |
175 | // first, test only if the file can be renamed |
176 | // (to find errors with locked files early) |
177 | for (String fileName : files) { |
178 | String temp = dir + "/temp.db"; |
179 | FileUtils.delete(temp); |
180 | FileUtils.move(fileName, temp); |
181 | FileUtils.move(temp, fileName); |
182 | } |
183 | // if this worked, the operation will (hopefully) be successful |
184 | // TODO changeFileEncryption: this is a workaround! |
185 | // make the operation atomic (all files or none) |
186 | for (String fileName : files) { |
187 | // don't process a lob directory, just the files in the directory. |
188 | if (!FileUtils.isDirectory(fileName)) { |
189 | change.process(fileName); |
190 | } |
191 | } |
192 | } |
193 | |
194 | private void process(String fileName) { |
195 | if (fileName.endsWith(Constants.SUFFIX_MV_FILE)) { |
196 | try { |
197 | copy(fileName); |
198 | } catch (IOException e) { |
199 | throw DbException.convertIOException(e, |
200 | "Error encrypting / decrypting file " + fileName); |
201 | } |
202 | return; |
203 | } |
204 | FileStore in; |
205 | if (decrypt == null) { |
206 | in = FileStore.open(null, fileName, "r"); |
207 | } else { |
208 | in = FileStore.open(null, fileName, "r", cipherType, decrypt); |
209 | } |
210 | try { |
211 | in.init(); |
212 | copy(fileName, in, encrypt); |
213 | } finally { |
214 | in.closeSilently(); |
215 | } |
216 | } |
217 | |
218 | private void copy(String fileName) throws IOException { |
219 | if (FileUtils.isDirectory(fileName)) { |
220 | return; |
221 | } |
222 | FileChannel fileIn = FilePath.get(fileName).open("r"); |
223 | FileChannel fileOut = null; |
224 | String temp = directory + "/temp.db"; |
225 | try { |
226 | if (decryptKey != null) { |
227 | fileIn = new FilePathEncrypt.FileEncrypt(fileName, decryptKey, fileIn); |
228 | } |
229 | InputStream inStream = new FileChannelInputStream(fileIn, true); |
230 | FileUtils.delete(temp); |
231 | fileOut = FilePath.get(temp).open("rw"); |
232 | if (encryptKey != null) { |
233 | fileOut = new FilePathEncrypt.FileEncrypt(temp, encryptKey, fileOut); |
234 | } |
235 | OutputStream outStream = new FileChannelOutputStream(fileOut, true); |
236 | byte[] buffer = new byte[4 * 1024]; |
237 | long remaining = fileIn.size(); |
238 | long total = remaining; |
239 | long time = System.currentTimeMillis(); |
240 | while (remaining > 0) { |
241 | if (System.currentTimeMillis() - time > 1000) { |
242 | out.println(fileName + ": " + (100 - 100 * remaining / total) + "%"); |
243 | time = System.currentTimeMillis(); |
244 | } |
245 | int len = (int) Math.min(buffer.length, remaining); |
246 | len = inStream.read(buffer, 0, len); |
247 | outStream.write(buffer, 0, len); |
248 | remaining -= len; |
249 | } |
250 | inStream.close(); |
251 | outStream.close(); |
252 | } finally { |
253 | fileIn.close(); |
254 | if (fileOut != null) { |
255 | fileOut.close(); |
256 | } |
257 | } |
258 | FileUtils.delete(fileName); |
259 | FileUtils.move(temp, fileName); |
260 | } |
261 | |
262 | private void copy(String fileName, FileStore in, byte[] key) { |
263 | if (FileUtils.isDirectory(fileName)) { |
264 | return; |
265 | } |
266 | String temp = directory + "/temp.db"; |
267 | FileUtils.delete(temp); |
268 | FileStore fileOut; |
269 | if (key == null) { |
270 | fileOut = FileStore.open(null, temp, "rw"); |
271 | } else { |
272 | fileOut = FileStore.open(null, temp, "rw", cipherType, key); |
273 | } |
274 | fileOut.init(); |
275 | byte[] buffer = new byte[4 * 1024]; |
276 | long remaining = in.length() - FileStore.HEADER_LENGTH; |
277 | long total = remaining; |
278 | in.seek(FileStore.HEADER_LENGTH); |
279 | fileOut.seek(FileStore.HEADER_LENGTH); |
280 | long time = System.currentTimeMillis(); |
281 | while (remaining > 0) { |
282 | if (System.currentTimeMillis() - time > 1000) { |
283 | out.println(fileName + ": " + (100 - 100 * remaining / total) + "%"); |
284 | time = System.currentTimeMillis(); |
285 | } |
286 | int len = (int) Math.min(buffer.length, remaining); |
287 | in.readFully(buffer, 0, len); |
288 | fileOut.write(buffer, 0, len); |
289 | remaining -= len; |
290 | } |
291 | in.close(); |
292 | fileOut.close(); |
293 | FileUtils.delete(fileName); |
294 | FileUtils.move(temp, fileName); |
295 | } |
296 | |
297 | } |