提交 42af75c5 authored 作者: Thomas Mueller's avatar Thomas Mueller

MVStore: encrypted stores are now supported.

上级 4ee69cc5
...@@ -33,6 +33,8 @@ Change Log ...@@ -33,6 +33,8 @@ Change Log
</li><li>New connection setting "DEFAULT_TABLE_ENGINE" to use a specific </li><li>New connection setting "DEFAULT_TABLE_ENGINE" to use a specific
table engine if none is set explicitly. This is to simplify testing table engine if none is set explicitly. This is to simplify testing
the MVStore table engine. the MVStore table engine.
</li><li>MVStore: encrypted stores are now supported.
Only standardized algorithms are used: PBKDF2, SHA-256, XTS-AES, AES-128.
</li><li>MVStore: improved API thanks to Simo Tripodi. </li><li>MVStore: improved API thanks to Simo Tripodi.
</li><li>MVStore: maps can now be renamed. </li><li>MVStore: maps can now be renamed.
</li><li>MVStore: store the file header also at the end of each chunk, </li><li>MVStore: store the file header also at the end of each chunk,
......
...@@ -43,7 +43,7 @@ But it can be also directly within an application, without using JDBC or SQL. ...@@ -43,7 +43,7 @@ But it can be also directly within an application, without using JDBC or SQL.
</li><li>Transactions (even if they are persisted) can be rolled back. </li><li>Transactions (even if they are persisted) can be rolled back.
</li><li>The tool is very modular. It supports pluggable data types / serialization, </li><li>The tool is very modular. It supports pluggable data types / serialization,
pluggable map implementations (B-tree, R-tree, concurrent B-tree currently), BLOB storage, pluggable map implementations (B-tree, R-tree, concurrent B-tree currently), BLOB storage,
and a file system abstraction to support encryption and compressed read-only files. and a file system abstraction to support encryption and zip files.
</li></ul> </li></ul>
<h2 id="example_code">Example Code</h2> <h2 id="example_code">Example Code</h2>
...@@ -325,6 +325,29 @@ new data is always appended at the end of the file. ...@@ -325,6 +325,29 @@ new data is always appended at the end of the file.
Then, the file can be copied (the file handle is available to the application). Then, the file can be copied (the file handle is available to the application).
</p> </p>
<h3>Encrypted Files</h3>
<p>
Data can be encrypted as follows:
</p>
<pre>
MVStore s = new MVStore.Builder().
fileName(fileName).
encryptionKey("007".toCharArray()).
open();
</pre>
<p>
The same security algorithms are used as modern disk encryption software use.
The password char array is cleared after use,
to reduce the risk that the password is stolen
even if the attacker has access to the main memory.
The password is hashed using the PBKDF2 standard, using the SHA-256 hash algorithm.
The length of the salt is 256 bits, so that an attacker can not use a rainbow table.
The salt is generated using a cryptographically secure random number generator.
The number of PBKDF2 iterations is 10000, so that an attacker can not "brute force" the password.
The file itself is encrypted using the standardized disk encryption mode XTS-AES,
so that only little more than one AES-128 round per block is needed.
</p>
<h3>Tools</h3> <h3>Tools</h3>
<p> <p>
There is a tool (<code>MVStoreTool</code>) to dump the contents of a file. There is a tool (<code>MVStoreTool</code>) to dump the contents of a file.
......
...@@ -512,6 +512,7 @@ public class DataUtils { ...@@ -512,6 +512,7 @@ public class DataUtils {
for (int i = 0, size = s.length(); i < size;) { for (int i = 0, size = s.length(); i < size;) {
int startKey = i; int startKey = i;
i = s.indexOf(':', i); i = s.indexOf(':', i);
checkArgument(i > 0, "Not a map");
String key = s.substring(startKey, i++); String key = s.substring(startKey, i++);
StringBuilder buff = new StringBuilder(); StringBuilder buff = new StringBuilder();
while (i < size) { while (i < size) {
...@@ -608,4 +609,16 @@ public class DataUtils { ...@@ -608,4 +609,16 @@ public class DataUtils {
Constants.VERSION_MINOR + "." + Constants.BUILD_ID + "]"; Constants.VERSION_MINOR + "." + Constants.BUILD_ID + "]";
} }
static byte[] getPasswordBytes(char[] passwordChars) {
// using UTF-16
int len = passwordChars.length;
byte[] password = new byte[len * 2];
for (int i = 0; i < len; i++) {
char c = passwordChars[i];
password[i + i] = (byte) (c >>> 8);
password[i + i + 1] = (byte) c;
}
return password;
}
} }
...@@ -11,6 +11,7 @@ import java.nio.ByteBuffer; ...@@ -11,6 +11,7 @@ import java.nio.ByteBuffer;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.channels.FileLock; import java.nio.channels.FileLock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet; import java.util.BitSet;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
...@@ -23,6 +24,7 @@ import org.h2.mvstore.cache.CacheLongKeyLIRS; ...@@ -23,6 +24,7 @@ import org.h2.mvstore.cache.CacheLongKeyLIRS;
import org.h2.mvstore.cache.FilePathCache; import org.h2.mvstore.cache.FilePathCache;
import org.h2.mvstore.type.StringDataType; import org.h2.mvstore.type.StringDataType;
import org.h2.store.fs.FilePath; import org.h2.store.fs.FilePath;
import org.h2.store.fs.FilePathCrypt2;
import org.h2.store.fs.FileUtils; import org.h2.store.fs.FileUtils;
import org.h2.util.MathUtils; import org.h2.util.MathUtils;
import org.h2.util.New; import org.h2.util.New;
...@@ -41,8 +43,7 @@ H:3,... ...@@ -41,8 +43,7 @@ H:3,...
TODO: TODO:
- file system encryption: check standard - file system encryption
- mvcc with multiple transactions - mvcc with multiple transactions
- update checkstyle - update checkstyle
- automated 'kill process' and 'power failure' test - automated 'kill process' and 'power failure' test
...@@ -115,6 +116,7 @@ public class MVStore { ...@@ -115,6 +116,7 @@ public class MVStore {
private static final int FORMAT_READ = 1; private static final int FORMAT_READ = 1;
private final String fileName; private final String fileName;
private final char[] filePassword;
private int pageSize = 6 * 1024; private int pageSize = 6 * 1024;
...@@ -185,8 +187,10 @@ public class MVStore { ...@@ -185,8 +187,10 @@ public class MVStore {
int mb = s == null ? 16 : Integer.parseInt(s.toString()); int mb = s == null ? 16 : Integer.parseInt(s.toString());
cache = new CacheLongKeyLIRS<Page>( cache = new CacheLongKeyLIRS<Page>(
mb * 1024 * 1024, 2048, 16, mb * 1024 * 1024 / 2048 * 2 / 100); mb * 1024 * 1024, 2048, 16, mb * 1024 * 1024 / 2048 * 2 / 100);
filePassword = (char[]) config.get("encrypt");
} else { } else {
cache = null; cache = null;
filePassword = null;
} }
} }
...@@ -368,12 +372,18 @@ public class MVStore { ...@@ -368,12 +372,18 @@ public class MVStore {
return; return;
} }
FileUtils.createDirectories(FileUtils.getParent(fileName)); FileUtils.createDirectories(FileUtils.getParent(fileName));
try {
if (readOnly) { if (readOnly) {
openFile(); openFile();
} else if (!openFile()) { } else if (!openFile()) {
readOnly = true; readOnly = true;
openFile(); openFile();
} }
} finally {
if (filePassword != null) {
Arrays.fill(filePassword, (char) 0);
}
}
} }
/** /**
...@@ -391,7 +401,12 @@ public class MVStore { ...@@ -391,7 +401,12 @@ public class MVStore {
if (f.exists() && !f.canWrite()) { if (f.exists() && !f.canWrite()) {
readOnly = true; readOnly = true;
} }
file = FilePathCache.wrap(f.open(readOnly ? "r" : "rw")); file = f.open(readOnly ? "r" : "rw");
if (filePassword != null) {
byte[] password = DataUtils.getPasswordBytes(filePassword);
file = new FilePathCrypt2.FileCrypt2(password, file);
}
file = FilePathCache.wrap(file);
if (readOnly) { if (readOnly) {
fileLock = file.tryLock(0, Long.MAX_VALUE, true); fileLock = file.tryLock(0, Long.MAX_VALUE, true);
if (fileLock == null) { if (fileLock == null) {
...@@ -433,7 +448,7 @@ public class MVStore { ...@@ -433,7 +448,7 @@ public class MVStore {
} }
} catch (Exception e) { } catch (Exception e) {
try { try {
close(); close(false);
} catch (Exception e2) { } catch (Exception e2) {
// ignore // ignore
} }
...@@ -443,6 +458,7 @@ public class MVStore { ...@@ -443,6 +458,7 @@ public class MVStore {
return true; return true;
} }
private void readMeta() { private void readMeta() {
Chunk header = readChunkHeader(rootChunkStart); Chunk header = readChunkHeader(rootChunkStart);
lastChunkId = header.id; lastChunkId = header.id;
...@@ -545,10 +561,16 @@ public class MVStore { ...@@ -545,10 +561,16 @@ public class MVStore {
* Close the file. Uncommitted changes are ignored, and all open maps are closed. * Close the file. Uncommitted changes are ignored, and all open maps are closed.
*/ */
public void close() { public void close() {
close(true);
}
private void close(boolean shrinkIfPossible) {
closed = true; closed = true;
if (file != null) { if (file != null) {
try { try {
if (shrinkIfPossible) {
shrinkFileIfPossible(0); shrinkFileIfPossible(0);
}
log("file close"); log("file close");
if (fileLock != null) { if (fileLock != null) {
fileLock.release(); fileLock.release();
...@@ -1416,6 +1438,21 @@ public class MVStore { ...@@ -1416,6 +1438,21 @@ public class MVStore {
return set("fileName", fileName); return set("fileName", fileName);
} }
/**
* Encrypt / decrypt the file using the given password. This method has
* no effect for in-memory stores. The password is passed as a char
* array so that it can be cleared as soon as possible. Please note
* there is still a small risk that password stays in memory (due to
* Java garbage collection). Also, the hashed encryption key is kept in
* memory as long as the file is open.
*
* @param password the password
* @return this
*/
public Builder encryptionKey(char[] password) {
return set("encrypt", password);
}
/** /**
* Open the file in read-only mode. In this case, a shared lock will be * Open the file in read-only mode. In this case, a shared lock will be
* acquired to ensure the file is not concurrently opened in write mode. * acquired to ensure the file is not concurrently opened in write mode.
......
...@@ -77,6 +77,54 @@ public class SHA256 { ...@@ -77,6 +77,54 @@ public class SHA256 {
return getHash(buff, true); return getHash(buff, true);
} }
public static byte[] getHMAC(byte[] key, byte[] message) {
int blockSize = 64;
if (key.length > blockSize) {
key = getHash(key, false);
}
if (key.length < blockSize) {
key = Arrays.copyOf(key, blockSize);
}
byte[] iKeyPadMessage = new byte[blockSize + message.length];
Arrays.fill(iKeyPadMessage, 0, blockSize, (byte) 0x36);
xor(iKeyPadMessage, key, blockSize);
System.arraycopy(message, 0, iKeyPadMessage, blockSize, message.length);
byte[] k = getHash(iKeyPadMessage, false);
byte[] oKeyPad = new byte[blockSize + k.length];
Arrays.fill(oKeyPad, 0, blockSize, (byte) 0x5c);
xor(oKeyPad, key, blockSize);
System.arraycopy(k, 0, oKeyPad, blockSize, k.length);
return getHash(oKeyPad, false);
}
private static void xor(byte[] target, byte[] data, int len) {
for (int i = 0; i < len; i++) {
target[i] ^= data[i];
}
}
public static byte[] getPBKDF2(byte[] password, byte[] salt, int iterations, int len) {
byte[] result = new byte[len];
byte[] last = null;
for (int k = 1, offset = 0; offset < len; k++, offset += 32) {
for (int i = 0; i < iterations; i++) {
byte[] x;
if (i == 0) {
x = Arrays.copyOf(salt, salt.length + 4);
writeInt(x, salt.length, k);
} else {
x = last;
}
last = getHMAC(password, x);
for (int j = 0; j < 32 && j + offset < len; j++) {
result[j + offset] ^= last[j];
}
}
}
Arrays.fill(password, (byte) 0);
return result;
}
/** /**
* Calculate the hash code for the given data. * Calculate the hash code for the given data.
* *
......
...@@ -40,6 +40,7 @@ public class TestMVStore extends TestBase { ...@@ -40,6 +40,7 @@ public class TestMVStore extends TestBase {
public void test() throws Exception { public void test() throws Exception {
FileUtils.deleteRecursive(getBaseDir(), true); FileUtils.deleteRecursive(getBaseDir(), true);
testEncryptedFile();
testFileFormatChange(); testFileFormatChange();
testRecreateMap(); testRecreateMap();
testRenameMapRollback(); testRenameMapRollback();
...@@ -74,6 +75,54 @@ public class TestMVStore extends TestBase { ...@@ -74,6 +75,54 @@ public class TestMVStore extends TestBase {
testSimple(); testSimple();
} }
private void testEncryptedFile() {
String fileName = getBaseDir() + "/testEncryptedFile.h3";
FileUtils.delete(fileName);
MVStore s;
MVMap<Integer, String> m;
char[] passwordChars = "007".toCharArray();
s = new MVStore.Builder().
fileName(fileName).
encryptionKey(passwordChars).
open();
assertEquals(0, passwordChars[0]);
assertEquals(0, passwordChars[1]);
assertEquals(0, passwordChars[2]);
assertTrue(FileUtils.exists(fileName));
m = s.openMap("test");
m.put(1, "Hello");
assertEquals("Hello", m.get(1));
s.store();
s.close();
passwordChars = "008".toCharArray();
try {
s = new MVStore.Builder().
fileName(fileName).
encryptionKey(passwordChars).open();
fail();
} catch (IllegalStateException e) {
assertTrue(e.getCause() != null);
}
assertEquals(0, passwordChars[0]);
assertEquals(0, passwordChars[1]);
assertEquals(0, passwordChars[2]);
passwordChars = "007".toCharArray();
s = new MVStore.Builder().
fileName(fileName).
encryptionKey(passwordChars).open();
assertEquals(0, passwordChars[0]);
assertEquals(0, passwordChars[1]);
assertEquals(0, passwordChars[2]);
m = s.openMap("test");
assertEquals("Hello", m.get(1));
s.close();
FileUtils.delete(fileName);
assertFalse(FileUtils.exists(fileName));
}
private void testFileFormatChange() { private void testFileFormatChange() {
String fileName = getBaseDir() + "/testFileFormatChange.h3"; String fileName = getBaseDir() + "/testFileFormatChange.h3";
FileUtils.delete(fileName); FileUtils.delete(fileName);
......
...@@ -46,9 +46,71 @@ public class TestSecurity extends TestBase { ...@@ -46,9 +46,71 @@ public class TestSecurity extends TestBase {
} }
private void testSHA() { private void testSHA() {
testPBKDF2();
testHMAC();
testOneSHA(); testOneSHA();
} }
private void testPBKDF2() {
// test vectors from StackOverflow (PBKDF2-HMAC-SHA2)
assertEquals(
"120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b",
StringUtils.convertBytesToHex(
SHA256.getPBKDF2(
"password".getBytes(),
"salt".getBytes(), 1, 32)));
assertEquals(
"ae4d0c95af6b46d32d0adff928f06dd02a303f8ef3c251dfd6e2d85a95474c43",
StringUtils.convertBytesToHex(
SHA256.getPBKDF2(
"password".getBytes(),
"salt".getBytes(), 2, 32)));
assertEquals(
"c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a",
StringUtils.convertBytesToHex(
SHA256.getPBKDF2(
"password".getBytes(),
"salt".getBytes(), 4096, 32)));
// take a very long time to calculate
// assertEquals(
// "cf81c66fe8cfc04d1f31ecb65dab4089f7f179e89b3b0bcb17ad10e3ac6eba46",
// StringUtils.convertBytesToHex(
// SHA256.getPBKDF2(
// "password".getBytes(),
// "salt".getBytes(), 16777216, 32)));
assertEquals(
"348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9",
StringUtils.convertBytesToHex(
SHA256.getPBKDF2(
"passwordPASSWORDpassword".getBytes(),
"saltSALTsaltSALTsaltSALTsaltSALTsalt".getBytes(), 4096, 40)));
assertEquals(
"89b69d0516f829893c696226650a8687",
StringUtils.convertBytesToHex(
SHA256.getPBKDF2(
"pass\0word".getBytes(),
"sa\0lt".getBytes(), 4096, 16)));
// the password is filled with zeroes
byte[] password = "Test".getBytes();
SHA256.getPBKDF2(password, "".getBytes(), 1, 16);
assertEquals(new byte[4], password);
}
private void testHMAC() {
// from Wikipedia
assertEquals(
"b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad",
StringUtils.convertBytesToHex(
SHA256.getHMAC(new byte[0], new byte[0])));
assertEquals(
"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8",
StringUtils.convertBytesToHex(
SHA256.getHMAC(
"key".getBytes(),
"The quick brown fox jumps over the lazy dog".getBytes())));
}
private String getHashString(byte[] data) { private String getHashString(byte[] data) {
byte[] result = SHA256.getHash(data, true); byte[] result = SHA256.getHash(data, true);
if (data.length > 0) { if (data.length > 0) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论