From 6b21e4b6cc1e260e4264ea9e592f5a46ee42d46a Mon Sep 17 00:00:00 2001 From: Pawel Maslej Date: Thu, 1 Aug 2024 12:43:10 +0100 Subject: [PATCH 1/2] feature-1659: Initial draft --- .../com/arcadedb/database/DataEncryption.java | 30 +++++ .../java/com/arcadedb/database/Database.java | 10 ++ .../arcadedb/database/DatabaseInternal.java | 5 + .../database/DefaultDataEncryption.java | 120 ++++++++++++++++++ .../arcadedb/serializer/BinarySerializer.java | 39 ++++-- .../arcadedb/database/DataEncryptionTest.java | 96 ++++++++++++++ .../database/DefaultDataEncryptionTest.java | 64 ++++++++++ 7 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 engine/src/main/java/com/arcadedb/database/DataEncryption.java create mode 100644 engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java create mode 100644 engine/src/test/java/com/arcadedb/database/DataEncryptionTest.java create mode 100644 engine/src/test/java/com/arcadedb/database/DefaultDataEncryptionTest.java diff --git a/engine/src/main/java/com/arcadedb/database/DataEncryption.java b/engine/src/main/java/com/arcadedb/database/DataEncryption.java new file mode 100644 index 0000000000..017a061e65 --- /dev/null +++ b/engine/src/main/java/com/arcadedb/database/DataEncryption.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2024-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2024-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.database; + +/** + * Provides methods to intercept serialization of data allowing encryption and decryption. + * + * @author Pawel Maslej + * @since 27 Jun 2024 + */ +public interface DataEncryption { + public byte[] encrypt(byte[] data); + public byte[] decrypt(byte[] data); +} \ No newline at end of file diff --git a/engine/src/main/java/com/arcadedb/database/Database.java b/engine/src/main/java/com/arcadedb/database/Database.java index 2963c5c1d1..ffd657b095 100644 --- a/engine/src/main/java/com/arcadedb/database/Database.java +++ b/engine/src/main/java/com/arcadedb/database/Database.java @@ -331,4 +331,14 @@ Edge newEdgeByKeys(Vertex sourceVertex, String destinationVertexType, String[] d * @see #isAsyncFlush() */ Database setAsyncFlush(boolean value); + + /** + * Sets data encryption to be used by the database.
+ * THIS MUST BE DONE BEFORE WRITING ANY DATA TO THE DATABASE. + * + * @param encryption implementation of DataEncryption + * + * @see DefaultDataEncryption + */ + void setDataEncryption(DataEncryption encryption); } diff --git a/engine/src/main/java/com/arcadedb/database/DatabaseInternal.java b/engine/src/main/java/com/arcadedb/database/DatabaseInternal.java index d197e9881c..fbf7182885 100644 --- a/engine/src/main/java/com/arcadedb/database/DatabaseInternal.java +++ b/engine/src/main/java/com/arcadedb/database/DatabaseInternal.java @@ -128,4 +128,9 @@ default TransactionContext getTransaction() { * Executes an operation after having locked files. */ RET executeLockingFiles(Collection fileIds, Callable callable); + + @Override + default void setDataEncryption(DataEncryption encryption) { + getSerializer().setDataEncryption(encryption); + } } diff --git a/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java b/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java new file mode 100644 index 0000000000..ec043d8ee4 --- /dev/null +++ b/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2024-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2024-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.database; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Provides configurable default with implementation for data encryption and decryption. + * + * @author Pawel Maslej + * @since 27 Jun 2024 + */ +public class DefaultDataEncryption implements DataEncryption { + public static final int DEFAULT_SALT_ITERATIONS = 65536; + public static final int DEFAULT_KEY_LENGTH = 256; + public static final String DEFAULT_PASSWORD_ALGORITHM = "PBKDF2WithHmacSHA256"; + public static final String DEFAULT_SECRET_KEY_ALGORITHM = "AES"; + + public static final String DEFAULT_ALGORITHM = "AES/GCM/NoPadding"; + public static final int DEFAULT_IV_SIZE = 12; + public static final int DEFAULT_TAG_SIZE = 128; + + private SecretKey secretKey; + private String algorithm; + private int ivSize; + private int tagSize; + + public DefaultDataEncryption(SecretKey secretKey, String algorithm, int ivSize, int tagSize) throws NoSuchAlgorithmException, NoSuchPaddingException { + this.secretKey = secretKey; + this.algorithm = algorithm; + this.ivSize = ivSize; + this.tagSize = tagSize; + Cipher.getInstance(algorithm); // validates before execution + } + + @Override + public byte[] encrypt(byte[] data) { + try { + var ivBytes = generateIv(ivSize); + var cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(tagSize, ivBytes)); + byte[] encryptedData = cipher.doFinal(data); + byte[] ivAndEncryptedData = new byte[ivBytes.length + encryptedData.length]; + System.arraycopy(ivBytes, 0, ivAndEncryptedData, 0, ivBytes.length); + System.arraycopy(encryptedData, 0, ivAndEncryptedData, ivBytes.length, encryptedData.length); + return ivAndEncryptedData; + } catch (Exception e) { + throw new RuntimeException("Error while encrypting data", e); + } + } + + @Override + public byte[] decrypt(byte[] data) { + try { + byte[] ivBytes = new byte[ivSize]; + byte[] encryptedData = new byte[data.length - ivSize]; + System.arraycopy(data, 0, ivBytes, 0, ivSize); + System.arraycopy(data, ivSize, encryptedData, 0, encryptedData.length); + var decipher = Cipher.getInstance(algorithm); + decipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(tagSize, ivBytes)); + return decipher.doFinal(encryptedData); + } catch (Exception e) { + throw new RuntimeException("Error while decrypting data", e); + } + } + + public static DefaultDataEncryption useDefaults(SecretKey secretKey) throws NoSuchAlgorithmException, NoSuchPaddingException { + return new DefaultDataEncryption(secretKey, DEFAULT_ALGORITHM, DEFAULT_IV_SIZE, DEFAULT_TAG_SIZE); + } + + public static SecretKey generateRandomSecretKeyUsingDefaults() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance(DEFAULT_SECRET_KEY_ALGORITHM); + keyGen.init(DEFAULT_KEY_LENGTH); + return keyGen.generateKey(); + } + + public static SecretKey getSecretKeyFromPasswordUsingDefaults(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException { + return getKeyFromPassword(password, salt, DEFAULT_PASSWORD_ALGORITHM, DEFAULT_SECRET_KEY_ALGORITHM, DEFAULT_SALT_ITERATIONS, DEFAULT_KEY_LENGTH); + } + + private static byte[] generateIv(int ivSize) { + final byte[] iv = new byte[ivSize]; + new SecureRandom().nextBytes(iv); + return iv; + } + + public static SecretKey getKeyFromPassword(String password, String salt, String passwordAlgorithm, String secretKeyAlgorithm, int saltIterations, int keyLength) + throws NoSuchAlgorithmException, InvalidKeySpecException { + final SecretKeyFactory factory = SecretKeyFactory.getInstance(passwordAlgorithm); + final KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), saltIterations, keyLength); + return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), secretKeyAlgorithm); + } +} \ No newline at end of file diff --git a/engine/src/main/java/com/arcadedb/serializer/BinarySerializer.java b/engine/src/main/java/com/arcadedb/serializer/BinarySerializer.java index da31de08ba..90281c855c 100644 --- a/engine/src/main/java/com/arcadedb/serializer/BinarySerializer.java +++ b/engine/src/main/java/com/arcadedb/serializer/BinarySerializer.java @@ -22,6 +22,7 @@ import com.arcadedb.GlobalConfiguration; import com.arcadedb.database.BaseRecord; import com.arcadedb.database.Binary; +import com.arcadedb.database.DataEncryption; import com.arcadedb.database.Database; import com.arcadedb.database.DatabaseContext; import com.arcadedb.database.DatabaseInternal; @@ -65,6 +66,7 @@ public class BinarySerializer { private final BinaryComparator comparator = new BinaryComparator(); private Class dateImplementation; private Class dateTimeImplementation; + private DataEncryption dataEncryption; public BinarySerializer(final ContextConfiguration configuration) throws ClassNotFoundException { setDateImplementation(configuration.getValue(GlobalConfiguration.DATE_IMPLEMENTATION)); @@ -332,9 +334,10 @@ else if (properties == 0) return null; } - public void serializeValue(final Database database, final Binary content, final byte type, Object value) { + public void serializeValue(final Database database, final Binary serialized, final byte type, Object value) { if (value == null) return; + Binary content = dataEncryption != null ? new Binary() : serialized; switch (type) { case BinaryTypes.TYPE_NULL: @@ -393,8 +396,8 @@ else if (value instanceof LocalDate) break; case BinaryTypes.TYPE_COMPRESSED_RID: { final RID rid = ((Identifiable) value).getIdentity(); - content.putNumber(rid.getBucketId()); - content.putNumber(rid.getPosition()); + serialized.putNumber(rid.getBucketId()); + serialized.putNumber(rid.getPosition()); break; } case BinaryTypes.TYPE_RID: { @@ -403,8 +406,8 @@ else if (value instanceof LocalDate) value = ((Result) value).getElement().get(); final RID rid = ((Identifiable) value).getIdentity(); - content.putInt(rid.getBucketId()); - content.putLong(rid.getPosition()); + serialized.putInt(rid.getBucketId()); + serialized.putLong(rid.getPosition()); break; } case BinaryTypes.TYPE_UUID: { @@ -563,10 +566,26 @@ else if (value instanceof LocalDate) default: LogManager.instance().log(this, Level.INFO, "Error on serializing value '" + value + "', type not supported"); } + + if (dataEncryption != null) { + switch (type) { + case BinaryTypes.TYPE_NULL: + case BinaryTypes.TYPE_COMPRESSED_RID: + case BinaryTypes.TYPE_RID: + break; + default: + serialized.putBytes(dataEncryption.encrypt(content.toByteArray())); + } + } } - public Object deserializeValue(final Database database, final Binary content, final byte type, + public Object deserializeValue(final Database database, final Binary deserialized, final byte type, final EmbeddedModifier embeddedModifier) { + Binary content = dataEncryption != null && + type != BinaryTypes.TYPE_NULL && + type != BinaryTypes.TYPE_COMPRESSED_RID && + type != BinaryTypes.TYPE_RID ? new Binary(dataEncryption.decrypt(deserialized.getBytes())) : deserialized; + final Object value; switch (type) { case BinaryTypes.TYPE_NULL: @@ -626,10 +645,10 @@ public Object deserializeValue(final Database database, final Binary content, fi value = new BigDecimal(new BigInteger(unscaledValue), scale); break; case BinaryTypes.TYPE_COMPRESSED_RID: - value = new RID(database, (int) content.getNumber(), content.getNumber()); + value = new RID(database, (int) deserialized.getNumber(), deserialized.getNumber()); break; case BinaryTypes.TYPE_RID: - value = new RID(database, content.getInt(), content.getLong()); + value = new RID(database, deserialized.getInt(), deserialized.getLong()); break; case BinaryTypes.TYPE_UUID: value = new UUID(content.getNumber(), content.getNumber()); @@ -799,4 +818,8 @@ private void serializeDateTime(final Binary content, final Object value, final b content.putUnsignedNumber(DateUtils.dateTimeToTimestamp(value, DateUtils.getPrecisionFromBinaryType(type))); } + public void setDataEncryption(final DataEncryption dataEncryption) { + this.dataEncryption = dataEncryption; + } + } diff --git a/engine/src/test/java/com/arcadedb/database/DataEncryptionTest.java b/engine/src/test/java/com/arcadedb/database/DataEncryptionTest.java new file mode 100644 index 0000000000..ccca4fcbec --- /dev/null +++ b/engine/src/test/java/com/arcadedb/database/DataEncryptionTest.java @@ -0,0 +1,96 @@ +/* + * Copyright © 2024-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2024-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.database; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.concurrent.atomic.AtomicReference; + +import javax.crypto.NoSuchPaddingException; + +import org.junit.jupiter.api.Test; + +import com.arcadedb.TestHelper; +import com.arcadedb.graph.Vertex; +import com.arcadedb.graph.Vertex.DIRECTION; + +/** + * @author Pawel Maslej + * @since 1 Jul 2024 + */ +class DataEncryptionTest extends TestHelper { + + String password = "password"; + String salt = "salt"; + + @Test + void dataIsEncrypted() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException { + database.setDataEncryption(DefaultDataEncryption.useDefaults(DefaultDataEncryption.getSecretKeyFromPasswordUsingDefaults(password, salt))); + + database.command("sql", "create vertex type Person"); + database.command("sql", "create property Person.id string"); + database.command("sql", "create index on Person (id) unique"); + database.command("sql", "create property Person.name string"); + database.command("sql", "create edge type Knows"); + + var v1Id = new AtomicReference(null); + var v2Id = new AtomicReference(null); + + database.transaction(() -> { + var v1 = database.newVertex("Person").set("name", "John").save(); + var v2 = database.newVertex("Person").set("name", "Doe").save(); + v1.newEdge("Knows", v2, false, "since", 2024); + verify(v1, v2, true); + v1Id.set(v1.getIdentity()); + v2Id.set(v2.getIdentity()); + }); + verify(v1Id.get(), v2Id.get(), true); + + database.setDataEncryption(null); + verify(v1Id.get(), v2Id.get(), false); + + reopenDatabase(); + verify(v1Id.get(), v2Id.get(), false); + + database.setDataEncryption(DefaultDataEncryption.useDefaults(DefaultDataEncryption.getSecretKeyFromPasswordUsingDefaults(password, salt))); + verify(v1Id.get(), v2Id.get(), true); + } + + private void verify(RID rid1, RID rid2, boolean isEquals) { + database.transaction(() -> { + var p1 = database.lookupByRID(rid1, true).asVertex(); + var p2 = database.lookupByRID(rid2, true).asVertex(); + verify(p1, p2, isEquals); + }); + } + + private void verify(Vertex p1, Vertex p2, boolean isEquals) { + if (isEquals) { + assertEquals("John", p1.get("name")); + assertEquals("Doe", p2.get("name")); + assertEquals(2024, p1.getEdges(DIRECTION.OUT, "Knows").iterator().next().get("since")); + } else { + assertFalse(((String) p1.get("name")).contains("John")); + assertFalse(((String) p2.get("name")).contains("Doe")); + assertFalse(p1.getEdges(DIRECTION.OUT, "Knows").iterator().next().get("since").toString().contains("2024")); + } + } +} \ No newline at end of file diff --git a/engine/src/test/java/com/arcadedb/database/DefaultDataEncryptionTest.java b/engine/src/test/java/com/arcadedb/database/DefaultDataEncryptionTest.java new file mode 100644 index 0000000000..b45e35e70b --- /dev/null +++ b/engine/src/test/java/com/arcadedb/database/DefaultDataEncryptionTest.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2024-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2024-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.database; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * @author Pawel Maslej + * @since 1 Jul 2024 + */ +class DefaultDataEncryptionTest { + static SecretKey key; + + @BeforeAll + public static void beforeAll() throws NoSuchAlgorithmException, InvalidKeySpecException { + String password = "password"; + String salt = "salt"; + key = DefaultDataEncryption.getSecretKeyFromPasswordUsingDefaults(password, salt); + } + + @Test + void testEncryptionOfString() throws NoSuchAlgorithmException, NoSuchPaddingException { + var dde = DefaultDataEncryption.useDefaults(key); + String data = "data"; + byte[] encryptedData = dde.encrypt(data.getBytes()); + String decryptedData = new String(dde.decrypt(encryptedData)); + assertEquals(data, decryptedData); + } + + @Test + void testEncryptionOfDouble() throws NoSuchAlgorithmException, NoSuchPaddingException { + var dde = DefaultDataEncryption.useDefaults(key); + double data = 1000000d; + byte[] encryptedData = dde.encrypt(ByteBuffer.allocate(8).putLong(Double.doubleToLongBits(data)).array()); + var decryptedData = Double.longBitsToDouble(ByteBuffer.wrap(dde.decrypt(encryptedData)).getLong()); + assertEquals(data, decryptedData); + } +} \ No newline at end of file From 71cdf98e9ed9e9829f1355a1aa0c86594f06f006 Mon Sep 17 00:00:00 2001 From: Pawel Maslej <151659533+pawellhasa@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:55:03 +0100 Subject: [PATCH 2/2] feature-1659: Added dedicated exception: EncryptionException --- .../database/DefaultDataEncryption.java | 6 ++-- .../exception/EncryptionException.java | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 engine/src/main/java/com/arcadedb/exception/EncryptionException.java diff --git a/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java b/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java index ec043d8ee4..68b9902aa3 100644 --- a/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java +++ b/engine/src/main/java/com/arcadedb/database/DefaultDataEncryption.java @@ -31,6 +31,8 @@ import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; +import com.arcadedb.exception.EncryptionException; + /** * Provides configurable default with implementation for data encryption and decryption. * @@ -72,7 +74,7 @@ public byte[] encrypt(byte[] data) { System.arraycopy(encryptedData, 0, ivAndEncryptedData, ivBytes.length, encryptedData.length); return ivAndEncryptedData; } catch (Exception e) { - throw new RuntimeException("Error while encrypting data", e); + throw new EncryptionException("Error while encrypting data", e); } } @@ -87,7 +89,7 @@ public byte[] decrypt(byte[] data) { decipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(tagSize, ivBytes)); return decipher.doFinal(encryptedData); } catch (Exception e) { - throw new RuntimeException("Error while decrypting data", e); + throw new EncryptionException("Error while decrypting data", e); } } diff --git a/engine/src/main/java/com/arcadedb/exception/EncryptionException.java b/engine/src/main/java/com/arcadedb/exception/EncryptionException.java new file mode 100644 index 0000000000..91f996b03f --- /dev/null +++ b/engine/src/main/java/com/arcadedb/exception/EncryptionException.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2024-present Arcade Data Ltd (info@arcadedata.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-FileCopyrightText: 2024-present Arcade Data Ltd (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.exception; + +public class EncryptionException extends ArcadeDBException { + public EncryptionException(final String message) { + super(message); + } + + public EncryptionException(final String message, final Throwable cause) { + super(message, cause); + } + + public EncryptionException(final Throwable cause) { + super(cause); + } +} \ No newline at end of file