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.store.fs; |
7 | |
8 | import java.io.EOFException; |
9 | import java.io.IOException; |
10 | import java.io.InputStream; |
11 | import java.io.OutputStream; |
12 | import java.nio.ByteBuffer; |
13 | import java.nio.channels.FileChannel; |
14 | import java.nio.channels.FileLock; |
15 | import java.util.Arrays; |
16 | |
17 | import org.h2.engine.Constants; |
18 | import org.h2.mvstore.DataUtils; |
19 | import org.h2.security.AES; |
20 | import org.h2.security.BlockCipher; |
21 | import org.h2.security.SHA256; |
22 | import org.h2.util.MathUtils; |
23 | |
24 | /** |
25 | * An encrypted file. |
26 | */ |
27 | public class FilePathEncrypt extends FilePathWrapper { |
28 | |
29 | private static final String SCHEME = "encrypt"; |
30 | |
31 | /** |
32 | * Register this file system. |
33 | */ |
34 | public static void register() { |
35 | FilePath.register(new FilePathEncrypt()); |
36 | } |
37 | |
38 | @Override |
39 | public FileChannel open(String mode) throws IOException { |
40 | String[] parsed = parse(name); |
41 | FileChannel file = FileUtils.open(parsed[1], mode); |
42 | byte[] passwordBytes = parsed[0].getBytes(Constants.UTF8); |
43 | return new FileEncrypt(name, passwordBytes, file); |
44 | } |
45 | |
46 | @Override |
47 | public String getScheme() { |
48 | return SCHEME; |
49 | } |
50 | |
51 | @Override |
52 | protected String getPrefix() { |
53 | String[] parsed = parse(name); |
54 | return getScheme() + ":" + parsed[0] + ":"; |
55 | } |
56 | |
57 | @Override |
58 | public FilePath unwrap(String fileName) { |
59 | return FilePath.get(parse(fileName)[1]); |
60 | } |
61 | |
62 | @Override |
63 | public long size() { |
64 | long size = getBase().size() - FileEncrypt.HEADER_LENGTH; |
65 | size = Math.max(0, size); |
66 | if ((size & FileEncrypt.BLOCK_SIZE_MASK) != 0) { |
67 | size -= FileEncrypt.BLOCK_SIZE; |
68 | } |
69 | return size; |
70 | } |
71 | |
72 | @Override |
73 | public OutputStream newOutputStream(boolean append) throws IOException { |
74 | return new FileChannelOutputStream(open("rw"), append); |
75 | } |
76 | |
77 | @Override |
78 | public InputStream newInputStream() throws IOException { |
79 | return new FileChannelInputStream(open("r"), true); |
80 | } |
81 | |
82 | /** |
83 | * Split the file name into algorithm, password, and base file name. |
84 | * |
85 | * @param fileName the file name |
86 | * @return an array with algorithm, password, and base file name |
87 | */ |
88 | private String[] parse(String fileName) { |
89 | if (!fileName.startsWith(getScheme())) { |
90 | throw new IllegalArgumentException(fileName + |
91 | " doesn't start with " + getScheme()); |
92 | } |
93 | fileName = fileName.substring(getScheme().length() + 1); |
94 | int idx = fileName.indexOf(':'); |
95 | String password; |
96 | if (idx < 0) { |
97 | throw new IllegalArgumentException(fileName + |
98 | " doesn't contain encryption algorithm and password"); |
99 | } |
100 | password = fileName.substring(0, idx); |
101 | fileName = fileName.substring(idx + 1); |
102 | return new String[] { password, fileName }; |
103 | } |
104 | |
105 | /** |
106 | * Convert a char array to a byte array, in UTF-16 format. The char array is |
107 | * not cleared after use (this must be done by the caller). |
108 | * |
109 | * @param passwordChars the password characters |
110 | * @return the byte array |
111 | */ |
112 | public static byte[] getPasswordBytes(char[] passwordChars) { |
113 | // using UTF-16 |
114 | int len = passwordChars.length; |
115 | byte[] password = new byte[len * 2]; |
116 | for (int i = 0; i < len; i++) { |
117 | char c = passwordChars[i]; |
118 | password[i + i] = (byte) (c >>> 8); |
119 | password[i + i + 1] = (byte) c; |
120 | } |
121 | return password; |
122 | } |
123 | |
124 | /** |
125 | * An encrypted file with a read cache. |
126 | */ |
127 | public static class FileEncrypt extends FileBase { |
128 | |
129 | /** |
130 | * The block size. |
131 | */ |
132 | static final int BLOCK_SIZE = 4096; |
133 | |
134 | /** |
135 | * The block size bit mask. |
136 | */ |
137 | static final int BLOCK_SIZE_MASK = BLOCK_SIZE - 1; |
138 | |
139 | /** |
140 | * The length of the file header. Using a smaller header is possible, |
141 | * but would mean reads and writes are not aligned to the block size. |
142 | */ |
143 | static final int HEADER_LENGTH = BLOCK_SIZE; |
144 | |
145 | private static final byte[] HEADER = "H2encrypt\n".getBytes(); |
146 | private static final int SALT_POS = HEADER.length; |
147 | |
148 | /** |
149 | * The length of the salt, in bytes. |
150 | */ |
151 | private static final int SALT_LENGTH = 8; |
152 | |
153 | /** |
154 | * The number of iterations. It is relatively low; a higher value would |
155 | * slow down opening files on Android too much. |
156 | */ |
157 | private static final int HASH_ITERATIONS = 10; |
158 | |
159 | private final FileChannel base; |
160 | |
161 | /** |
162 | * The current position within the file, from a user perspective. |
163 | */ |
164 | private long pos; |
165 | |
166 | /** |
167 | * The current file size, from a user perspective. |
168 | */ |
169 | private long size; |
170 | |
171 | private final String name; |
172 | |
173 | private XTS xts; |
174 | |
175 | private byte[] encryptionKey; |
176 | |
177 | public FileEncrypt(String name, byte[] encryptionKey, FileChannel base) { |
178 | // don't do any read or write operations here, because they could |
179 | // fail if the file is locked, and we want to give the caller a |
180 | // chance to lock the file first |
181 | this.name = name; |
182 | this.base = base; |
183 | this.encryptionKey = encryptionKey; |
184 | } |
185 | |
186 | private void init() throws IOException { |
187 | if (xts != null) { |
188 | return; |
189 | } |
190 | this.size = base.size() - HEADER_LENGTH; |
191 | boolean newFile = size < 0; |
192 | byte[] salt; |
193 | if (newFile) { |
194 | byte[] header = Arrays.copyOf(HEADER, BLOCK_SIZE); |
195 | salt = MathUtils.secureRandomBytes(SALT_LENGTH); |
196 | System.arraycopy(salt, 0, header, SALT_POS, salt.length); |
197 | DataUtils.writeFully(base, 0, ByteBuffer.wrap(header)); |
198 | size = 0; |
199 | } else { |
200 | salt = new byte[SALT_LENGTH]; |
201 | DataUtils.readFully(base, SALT_POS, ByteBuffer.wrap(salt)); |
202 | if ((size & BLOCK_SIZE_MASK) != 0) { |
203 | size -= BLOCK_SIZE; |
204 | } |
205 | } |
206 | AES cipher = new AES(); |
207 | cipher.setKey(SHA256.getPBKDF2( |
208 | encryptionKey, salt, HASH_ITERATIONS, 16)); |
209 | encryptionKey = null; |
210 | xts = new XTS(cipher); |
211 | } |
212 | |
213 | @Override |
214 | protected void implCloseChannel() throws IOException { |
215 | base.close(); |
216 | } |
217 | |
218 | @Override |
219 | public FileChannel position(long newPosition) throws IOException { |
220 | this.pos = newPosition; |
221 | return this; |
222 | } |
223 | |
224 | @Override |
225 | public long position() throws IOException { |
226 | return pos; |
227 | } |
228 | |
229 | @Override |
230 | public int read(ByteBuffer dst) throws IOException { |
231 | int len = read(dst, pos); |
232 | if (len > 0) { |
233 | pos += len; |
234 | } |
235 | return len; |
236 | } |
237 | |
238 | @Override |
239 | public int read(ByteBuffer dst, long position) throws IOException { |
240 | int len = dst.remaining(); |
241 | if (len == 0) { |
242 | return 0; |
243 | } |
244 | init(); |
245 | len = (int) Math.min(len, size - position); |
246 | if (position >= size) { |
247 | return -1; |
248 | } else if (position < 0) { |
249 | throw new IllegalArgumentException("pos: " + position); |
250 | } |
251 | if ((position & BLOCK_SIZE_MASK) != 0 || |
252 | (len & BLOCK_SIZE_MASK) != 0) { |
253 | // either the position or the len is unaligned: |
254 | // read aligned, and then truncate |
255 | long p = position / BLOCK_SIZE * BLOCK_SIZE; |
256 | int offset = (int) (position - p); |
257 | int l = (len + offset + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE; |
258 | ByteBuffer temp = ByteBuffer.allocate(l); |
259 | readInternal(temp, p, l); |
260 | temp.flip(); |
261 | temp.limit(offset + len); |
262 | temp.position(offset); |
263 | dst.put(temp); |
264 | return len; |
265 | } |
266 | readInternal(dst, position, len); |
267 | return len; |
268 | } |
269 | |
270 | private void readInternal(ByteBuffer dst, long position, int len) |
271 | throws IOException { |
272 | int x = dst.position(); |
273 | readFully(base, position + HEADER_LENGTH, dst); |
274 | long block = position / BLOCK_SIZE; |
275 | while (len > 0) { |
276 | xts.decrypt(block++, BLOCK_SIZE, dst.array(), dst.arrayOffset() + x); |
277 | x += BLOCK_SIZE; |
278 | len -= BLOCK_SIZE; |
279 | } |
280 | } |
281 | |
282 | private static void readFully(FileChannel file, long pos, ByteBuffer dst) |
283 | throws IOException { |
284 | do { |
285 | int len = file.read(dst, pos); |
286 | if (len < 0) { |
287 | throw new EOFException(); |
288 | } |
289 | pos += len; |
290 | } while (dst.remaining() > 0); |
291 | } |
292 | |
293 | @Override |
294 | public int write(ByteBuffer src, long position) throws IOException { |
295 | init(); |
296 | int len = src.remaining(); |
297 | if ((position & BLOCK_SIZE_MASK) != 0 || |
298 | (len & BLOCK_SIZE_MASK) != 0) { |
299 | // either the position or the len is unaligned: |
300 | // read aligned, and then truncate |
301 | long p = position / BLOCK_SIZE * BLOCK_SIZE; |
302 | int offset = (int) (position - p); |
303 | int l = (len + offset + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE; |
304 | ByteBuffer temp = ByteBuffer.allocate(l); |
305 | int available = (int) (size - p + BLOCK_SIZE - 1) / BLOCK_SIZE * BLOCK_SIZE; |
306 | int readLen = Math.min(l, available); |
307 | if (readLen > 0) { |
308 | readInternal(temp, p, readLen); |
309 | temp.rewind(); |
310 | } |
311 | temp.limit(offset + len); |
312 | temp.position(offset); |
313 | temp.put(src); |
314 | temp.limit(l); |
315 | temp.rewind(); |
316 | writeInternal(temp, p, l); |
317 | long p2 = position + len; |
318 | size = Math.max(size, p2); |
319 | int plus = (int) (size & BLOCK_SIZE_MASK); |
320 | if (plus > 0) { |
321 | temp = ByteBuffer.allocate(plus); |
322 | DataUtils.writeFully(base, p + HEADER_LENGTH + l, temp); |
323 | } |
324 | return len; |
325 | } |
326 | writeInternal(src, position, len); |
327 | long p2 = position + len; |
328 | size = Math.max(size, p2); |
329 | return len; |
330 | } |
331 | |
332 | private void writeInternal(ByteBuffer src, long position, int len) |
333 | throws IOException { |
334 | ByteBuffer crypt = ByteBuffer.allocate(len); |
335 | crypt.put(src); |
336 | crypt.flip(); |
337 | long block = position / BLOCK_SIZE; |
338 | int x = 0, l = len; |
339 | while (l > 0) { |
340 | xts.encrypt(block++, BLOCK_SIZE, crypt.array(), crypt.arrayOffset() + x); |
341 | x += BLOCK_SIZE; |
342 | l -= BLOCK_SIZE; |
343 | } |
344 | writeFully(base, position + HEADER_LENGTH, crypt); |
345 | } |
346 | |
347 | private static void writeFully(FileChannel file, long pos, |
348 | ByteBuffer src) throws IOException { |
349 | int off = 0; |
350 | do { |
351 | int len = file.write(src, pos + off); |
352 | off += len; |
353 | } while (src.remaining() > 0); |
354 | } |
355 | |
356 | @Override |
357 | public int write(ByteBuffer src) throws IOException { |
358 | int len = write(src, pos); |
359 | if (len > 0) { |
360 | pos += len; |
361 | } |
362 | return len; |
363 | } |
364 | |
365 | @Override |
366 | public long size() throws IOException { |
367 | init(); |
368 | return size; |
369 | } |
370 | |
371 | @Override |
372 | public FileChannel truncate(long newSize) throws IOException { |
373 | init(); |
374 | if (newSize > size) { |
375 | return this; |
376 | } |
377 | if (newSize < 0) { |
378 | throw new IllegalArgumentException("newSize: " + newSize); |
379 | } |
380 | int offset = (int) (newSize & BLOCK_SIZE_MASK); |
381 | if (offset > 0) { |
382 | base.truncate(newSize + HEADER_LENGTH + BLOCK_SIZE); |
383 | } else { |
384 | base.truncate(newSize + HEADER_LENGTH); |
385 | } |
386 | this.size = newSize; |
387 | pos = Math.min(pos, size); |
388 | return this; |
389 | } |
390 | |
391 | @Override |
392 | public void force(boolean metaData) throws IOException { |
393 | base.force(metaData); |
394 | } |
395 | |
396 | @Override |
397 | public FileLock tryLock(long position, long size, boolean shared) |
398 | throws IOException { |
399 | return base.tryLock(position, size, shared); |
400 | } |
401 | |
402 | @Override |
403 | public String toString() { |
404 | return name; |
405 | } |
406 | |
407 | } |
408 | |
409 | /** |
410 | * An XTS implementation as described in |
411 | * IEEE P1619 (Standard Architecture for Encrypted Shared Storage Media). |
412 | * See also |
413 | * http://axelkenzo.ru/downloads/1619-2007-NIST-Submission.pdf |
414 | */ |
415 | static class XTS { |
416 | |
417 | /** |
418 | * Galois field feedback. |
419 | */ |
420 | private static final int GF_128_FEEDBACK = 0x87; |
421 | |
422 | /** |
423 | * The AES encryption block size. |
424 | */ |
425 | private static final int CIPHER_BLOCK_SIZE = 16; |
426 | |
427 | private final BlockCipher cipher; |
428 | |
429 | XTS(BlockCipher cipher) { |
430 | this.cipher = cipher; |
431 | } |
432 | |
433 | /** |
434 | * Encrypt the data. |
435 | * |
436 | * @param id the (sector) id |
437 | * @param len the number of bytes |
438 | * @param data the data |
439 | * @param offset the offset within the data |
440 | */ |
441 | void encrypt(long id, int len, byte[] data, int offset) { |
442 | byte[] tweak = initTweak(id); |
443 | int i = 0; |
444 | for (; i + CIPHER_BLOCK_SIZE <= len; i += CIPHER_BLOCK_SIZE) { |
445 | if (i > 0) { |
446 | updateTweak(tweak); |
447 | } |
448 | xorTweak(data, i + offset, tweak); |
449 | cipher.encrypt(data, i + offset, CIPHER_BLOCK_SIZE); |
450 | xorTweak(data, i + offset, tweak); |
451 | } |
452 | if (i < len) { |
453 | updateTweak(tweak); |
454 | swap(data, i + offset, i - CIPHER_BLOCK_SIZE + offset, len - i); |
455 | xorTweak(data, i - CIPHER_BLOCK_SIZE + offset, tweak); |
456 | cipher.encrypt(data, i - CIPHER_BLOCK_SIZE + offset, CIPHER_BLOCK_SIZE); |
457 | xorTweak(data, i - CIPHER_BLOCK_SIZE + offset, tweak); |
458 | } |
459 | } |
460 | |
461 | /** |
462 | * Decrypt the data. |
463 | * |
464 | * @param id the (sector) id |
465 | * @param len the number of bytes |
466 | * @param data the data |
467 | * @param offset the offset within the data |
468 | */ |
469 | void decrypt(long id, int len, byte[] data, int offset) { |
470 | byte[] tweak = initTweak(id), tweakEnd = tweak; |
471 | int i = 0; |
472 | for (; i + CIPHER_BLOCK_SIZE <= len; i += CIPHER_BLOCK_SIZE) { |
473 | if (i > 0) { |
474 | updateTweak(tweak); |
475 | if (i + CIPHER_BLOCK_SIZE + CIPHER_BLOCK_SIZE > len && |
476 | i + CIPHER_BLOCK_SIZE < len) { |
477 | tweakEnd = Arrays.copyOf(tweak, CIPHER_BLOCK_SIZE); |
478 | updateTweak(tweak); |
479 | } |
480 | } |
481 | xorTweak(data, i + offset, tweak); |
482 | cipher.decrypt(data, i + offset, CIPHER_BLOCK_SIZE); |
483 | xorTweak(data, i + offset, tweak); |
484 | } |
485 | if (i < len) { |
486 | swap(data, i, i - CIPHER_BLOCK_SIZE + offset, len - i + offset); |
487 | xorTweak(data, i - CIPHER_BLOCK_SIZE + offset, tweakEnd); |
488 | cipher.decrypt(data, i - CIPHER_BLOCK_SIZE + offset, CIPHER_BLOCK_SIZE); |
489 | xorTweak(data, i - CIPHER_BLOCK_SIZE + offset, tweakEnd); |
490 | } |
491 | } |
492 | |
493 | private byte[] initTweak(long id) { |
494 | byte[] tweak = new byte[CIPHER_BLOCK_SIZE]; |
495 | for (int j = 0; j < CIPHER_BLOCK_SIZE; j++, id >>>= 8) { |
496 | tweak[j] = (byte) (id & 0xff); |
497 | } |
498 | cipher.encrypt(tweak, 0, CIPHER_BLOCK_SIZE); |
499 | return tweak; |
500 | } |
501 | |
502 | private static void xorTweak(byte[] data, int pos, byte[] tweak) { |
503 | for (int i = 0; i < CIPHER_BLOCK_SIZE; i++) { |
504 | data[pos + i] ^= tweak[i]; |
505 | } |
506 | } |
507 | |
508 | private static void updateTweak(byte[] tweak) { |
509 | byte ci = 0, co = 0; |
510 | for (int i = 0; i < CIPHER_BLOCK_SIZE; i++) { |
511 | co = (byte) ((tweak[i] >> 7) & 1); |
512 | tweak[i] = (byte) (((tweak[i] << 1) + ci) & 255); |
513 | ci = co; |
514 | } |
515 | if (co != 0) { |
516 | tweak[0] ^= GF_128_FEEDBACK; |
517 | } |
518 | } |
519 | |
520 | private static void swap(byte[] data, int source, int target, int len) { |
521 | for (int i = 0; i < len; i++) { |
522 | byte temp = data[source + i]; |
523 | data[source + i] = data[target + i]; |
524 | data[target + i] = temp; |
525 | } |
526 | } |
527 | |
528 | } |
529 | |
530 | } |