' localhost:8080/crypto/service.txt
+{AES}ef535bcba73b5921dac557b76d872a65e96c566d2be8ad5ae7281509cb82fe5ab38e152d1319bfe436c89dd5e7e67d7e63041e769ef6272274203b5e48d0f4d0
+```
+
+License
+-------
+
+(see the top-level [LICENSE.txt](../../LICENSE.txt) for full license details)
+
+Collective work: Copyright 2012 The Apache Software Foundation.
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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.
diff --git oak-crypto/impl/pom.xml oak-crypto/impl/pom.xml
new file mode 100644
index 0000000..f516489
--- /dev/null
+++ oak-crypto/impl/pom.xml
@@ -0,0 +1,149 @@
+
+
+
+
+
+ 4.0.0
+
+
+ org.apache.jackrabbit
+ oak-parent
+ 1.4-SNAPSHOT
+ ../../oak-parent/pom.xml
+
+
+ oak-crypto-impl
+ Oak Crypto Impl
+ bundle
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+
+
+ org.apache.felix
+ maven-scr-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+ logback-test.xml
+
+
+
+
+
+
+ maven-failsafe-plugin
+
+
+
+ src/test/resources/logging.properties
+
+
+
+
+
+ org.apache.rat
+ apache-rat-plugin
+
+
+
+
+
+
+
+
+ org.osgi
+ org.osgi.core
+ provided
+
+
+ org.osgi
+ org.osgi.compendium
+ provided
+
+
+ biz.aQute.bnd
+ bndlib
+ provided
+
+
+ org.apache.felix
+ org.apache.felix.scr.annotations
+ provided
+
+
+
+
+ org.apache.jackrabbit
+ oak-crypto-api
+ ${project.version}
+
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ 1.10.19
+ test
+
+
+ org.apache.commons
+ commons-lang3
+ 3.3.2
+ true
+ test
+
+
+
+
diff --git oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/AesCbcPaddingCipher.java oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/AesCbcPaddingCipher.java
new file mode 100644
index 0000000..db6eeee
--- /dev/null
+++ oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/AesCbcPaddingCipher.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.annotation.Nonnull;
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+import static org.apache.jackrabbit.oak.crypto.impl.Util.checkNotNull;
+import static org.apache.jackrabbit.oak.crypto.impl.Util.isEqual;
+
+/**
+ * Cipher supporting symmetric authenticated encryption/decryption using the
+ * {@link Cipher} ciphers available in compliant Java SE 7 platforms.
+ *
+ * Encryption does encrypt-then-mac using {@code AES/CBC/PKCS5Padding}
+ * for encryption and {@code HmacSHA256} for MAC.
+ *
+ * The MAC and Cipher keys are 16 bytes (128 bits) long.
+ */
+class AesCbcPaddingCipher {
+
+ /*
+ * The structure of the ciphertext buffer is as follow
+ *
+ * +---------------+------------- ... -------------+-----------------+
+ * | IV (16 bytes) | Encrypted cleartext (n bytes) | HMAC (32 bytes) |
+ * +---------------+------------- ... -------------+-----------------+
+ * | | | |
+ * | | | |
+ * | |<- - - - - - entLen - - - - - >| |
+ * | | |
+ * |<- - - - - - - - - - payLen - - - - - - - - - >| |
+ * | |
+ * |<- - - - - - - - - - - - - - totLen - - - - - - - - - - - - - - >|
+ *
+ */
+
+ /**
+ * The MAC algorithm.
+ */
+ private static final String MAC_ALGORITHM = "HmacSHA256";
+
+ /**
+ * The length in bytes of the HMACs
+ */
+ private static final int MAC_LENGTH = 32;
+
+ /**
+ * The encryption/decryption cipher.
+ */
+ private static final String CIPHER = "AES/CBC/PKCS5Padding";
+
+ /**
+ * The cipher key algorithm.
+ */
+ private static final String CIPHER_KEY_ALGORITHM = "AES";
+
+ /**
+ * The cipher block length in bytes.
+ */
+ private static final int CIPHER_BLOCK_LENGTH = 16;
+
+ /**
+ * The cipher key size in bits.
+ */
+ private static final int CIPHER_KEY_SIZE = 128;
+
+ /**
+ * The length of the IVs in bytes.
+ */
+ private static final int IV_LENGTH = 16;
+
+ /**
+ * The cipher identifier.
+ */
+ private static final String CIPHER_ID = "AES";
+
+
+ @Nonnull
+ byte[] encrypt(@Nonnull SecureRandom secureRandom,
+ @Nonnull SecretKey macKey,
+ @Nonnull SecretKey cipherKey,
+ @Nonnull byte[] iv,
+ @Nonnull byte[] plaintext)
+ throws GeneralSecurityException {
+
+ checkNotNull(secureRandom);
+ checkNotNull(macKey);
+ checkNotNull(cipherKey);
+ checkNotNull(iv);
+ checkNotNull(plaintext);
+
+ IvParameterSpec ivSpec = new IvParameterSpec(iv);
+ Cipher cipher = getCipher(secureRandom, Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
+
+ int encLen = cipher.getOutputSize(plaintext.length);
+ int payLen = IV_LENGTH + encLen;
+ int totLen = payLen + MAC_LENGTH;
+
+ validateCiphertextLength(totLen, payLen, encLen);
+
+ // create the ciphertext buffer
+ // and copy the IV at the start
+ byte[] ciphertext = Arrays.copyOf(iv, totLen);
+
+ // append the encrypted plaintext
+ // in the ciphertext buffer
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, IV_LENGTH);
+
+ // append the hmac of the payload (IV + encrypted plaintext)
+ // in the ciphertext buffer
+ byte[] hmac = hmac(macKey, ciphertext, payLen);
+ System.arraycopy(hmac, 0, ciphertext, payLen, MAC_LENGTH);
+
+ return ciphertext;
+ }
+
+ @Nonnull
+ byte[] decrypt(@Nonnull SecureRandom secureRandom,
+ @Nonnull SecretKey macKey,
+ @Nonnull SecretKey cipherKey,
+ @Nonnull byte[] ciphertext)
+ throws GeneralSecurityException {
+
+ checkNotNull(secureRandom);
+ checkNotNull(macKey);
+ checkNotNull(cipherKey);
+ checkNotNull(ciphertext);
+
+ int totLen = ciphertext.length;
+ int payLen = totLen - MAC_LENGTH;
+ int encLen = payLen - IV_LENGTH;
+
+ validateCiphertextLength(totLen, payLen, encLen);
+
+ // validate the ciphertext hmac
+ byte[] hmac1 = hmac(macKey, ciphertext, payLen);
+ byte[] hmac2 = Arrays.copyOfRange(ciphertext, payLen, totLen);
+ if (! isEqual(hmac1, hmac2)) {
+ throw new GeneralSecurityException("Altered ciphertext (HMAC verification failed)");
+ }
+
+ IvParameterSpec ivSpec = new IvParameterSpec(ciphertext, 0, CIPHER_BLOCK_LENGTH);
+ Cipher cipher = getCipher(secureRandom, Cipher.DECRYPT_MODE, cipherKey, ivSpec);
+
+ // create a plaintext buffer large
+ // enough to accommodate for padding
+ byte[] plaintext = new byte[encLen];
+
+ // decrypt the cipher text and get the actual
+ // plaintext length (without padding)
+ int plaintextLen = cipher.doFinal(ciphertext, IV_LENGTH, encLen, plaintext, 0);
+
+ // remove the padding and return
+ // the original plaintext
+ return Arrays.copyOf(plaintext, plaintextLen);
+ }
+
+ @Nonnull
+ byte[] generateIv(@Nonnull SecureRandom secureRandom) {
+ checkNotNull(secureRandom);
+ byte[] iv = new byte[CIPHER_BLOCK_LENGTH];
+ secureRandom.nextBytes(iv);
+ return iv;
+ }
+
+ @Nonnull
+ SecretKey generateKey(@Nonnull SecureRandom secureRandom) throws NoSuchAlgorithmException {
+ KeyGenerator gen = KeyGenerator.getInstance(CIPHER_KEY_ALGORITHM);
+ gen.init(CIPHER_KEY_SIZE, secureRandom);
+ return gen.generateKey();
+ }
+
+ @Nonnull
+ String getCipherId() {
+ return CIPHER_ID;
+ }
+
+ //
+
+ @Nonnull
+ private static byte[] hmac(@Nonnull SecretKey macKey,
+ @Nonnull byte[] ciphertext,
+ int payLen)
+ throws GeneralSecurityException {
+ Mac mac = Mac.getInstance(MAC_ALGORITHM);
+ mac.init(macKey);
+ mac.update(ciphertext, 0, payLen);
+ return mac.doFinal();
+ }
+
+ @Nonnull
+ private static Cipher getCipher(@Nonnull SecureRandom secureRandom,
+ int opmode,
+ @Nonnull SecretKey cipherKey,
+ @Nonnull IvParameterSpec ivSpec)
+ throws GeneralSecurityException {
+ Cipher cipher = Cipher.getInstance(CIPHER);
+ cipher.init(opmode, cipherKey, ivSpec, secureRandom);
+ return cipher;
+ }
+
+ private static void validateCiphertextLength(int totLen, int payLen, int encLen) {
+ if (totLen < (IV_LENGTH + MAC_LENGTH) || payLen < IV_LENGTH || encLen < 0) {
+ throw new IllegalArgumentException("Ciphertext is too short");
+ }
+ }
+}
diff --git oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/AutoSeedSecureRandom.java oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/AutoSeedSecureRandom.java
new file mode 100644
index 0000000..8117e4a
--- /dev/null
+++ oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/AutoSeedSecureRandom.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.security.SecureRandom;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link SecureRandom} which provides auto seeding and
+ * reseeding based on the amount of random bytes produced.
+ */
+class AutoSeedSecureRandom extends SecureRandom {
+
+ /**
+ * default logger
+ */
+ private static final Logger log = LoggerFactory.getLogger(AutoSeedSecureRandom.class);
+
+ private final int seedInterval;
+
+ private final int seedLength;
+
+ /**
+ * Keeps track of the number of random bytes generated.
+ */
+ private int counter;
+
+ /**
+ * @param seedInterval the amount of bytes to be produced before reseeding automatically
+ * @param seedLength the length in bytes of the seed to be produced when reseeding
+ */
+ public AutoSeedSecureRandom(int seedInterval, int seedLength) {
+ super();
+ next(8); // force initial auto seeding
+ this.seedInterval = seedInterval;
+ this.seedLength = seedLength;
+ counter = seedInterval;
+ }
+
+ @Override
+ public synchronized void nextBytes(byte[] bytes) {
+ counter -= bytes.length;
+ if (counter <= 0) {
+ log.info("Auto Reseeding SecureRandom");
+ setSeed(generateSeed(seedLength));
+ counter = seedInterval;
+ }
+ super.nextBytes(bytes);
+ }
+
+ synchronized int getCount() {
+ return counter;
+ }
+
+}
diff --git oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/CryptoServletRegistration.java oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/CryptoServletRegistration.java
new file mode 100644
index 0000000..4beda1c
--- /dev/null
+++ oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/CryptoServletRegistration.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.jackrabbit.oak.crypto.CryptoException;
+import org.apache.jackrabbit.oak.crypto.SymmetricCipher;
+import org.osgi.service.http.HttpService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(label = "Apache Jackrabbit Oak Crypto HTTP service",
+ description = "The Apache Oak Crypto HTTP service allows to transform plaintext into ciphertext.")
+@Property(name = "alias", value = "/crypto/service.txt", label = "root path for the Oak Crypto HTTP service", description = "name in the URI namespace at which the resources are registered")
+public class CryptoServletRegistration {
+
+ /*
+ * NOTE: The servlet is available non authenticated.
+ * It would make sense to limit its availability to authenticated administrator users.
+ */
+
+ /**
+ * default logger
+ */
+ private static final Logger log = LoggerFactory.getLogger(CryptoServletRegistration.class);
+
+ @Reference
+ private HttpService httpService;
+
+ @Reference
+ private SymmetricCipher symmetricCipher;
+
+ @Activate
+ public void activate(Map properties) {
+ registerServlet(getAlias(properties));
+ }
+
+ @Deactivate
+ public void deactivate(Map properties) {
+ unregisterServlet(getAlias(properties));
+ }
+
+ private void registerServlet(String alias) {
+ try {
+ Servlet servlet = new CryptoServlet((SymmetricCipherImpl) symmetricCipher);
+ httpService.registerServlet(alias, servlet, null, null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void unregisterServlet(String alias) {
+ httpService.unregister(alias);
+ }
+
+ private String getAlias(Map properties) {
+ return (String) properties.get("alias");
+ }
+
+ private static class CryptoServlet extends HttpServlet {
+
+ final SymmetricCipherImpl symmetricCipher;
+
+ CryptoServlet(SymmetricCipherImpl symmetricCipher) {
+ this.symmetricCipher = symmetricCipher;
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+
+ String datum = request.getParameter("datum");
+ if (datum != null) {
+ try {
+ String encrypted = symmetricCipher.encrypt(datum);
+ response.setContentType("text/plain");
+ response.setStatus(HttpServletResponse.SC_OK);
+ PrintWriter out = response.getWriter();
+ out.write(encrypted);
+ out.flush();
+ } catch (CryptoException e) {
+ throw new ServletException(e);
+ }
+ } else {
+ log.info("Missing request parameter");
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ }
+ }
+
+}
diff --git oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/KeySupplier.java oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/KeySupplier.java
new file mode 100644
index 0000000..865b6ea
--- /dev/null
+++ oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/KeySupplier.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.jackrabbit.oak.crypto.impl.Util.clear;
+import static org.apache.jackrabbit.oak.crypto.impl.Util.closeQuietly;
+
+/**
+ * Stores the keys encrypted (using the AES key wrap algorithm) and a Key encryption Key (KEK).
+ * The KEK is stored on the file system and MUST be strictly protected by access control
+ * in order to prevent its disclosure.
+ */
+final class KeySupplier {
+
+ /**
+ * default logger
+ */
+ private static final Logger log = LoggerFactory.getLogger(KeySupplier.class);
+
+ /**
+ * The cipher for wrapping/unwrapping the keys.
+ */
+ private static final String CIPHER = "AES/ECB/NoPadding";
+
+ /**
+ * The secret keys algorithm.
+ */
+ private static final String KEY_ALGORITHM = "AES";
+
+ /**
+ * The secret keys size in bits.
+ */
+ private static final int KEY_SIZE = 128;
+
+ private final SecureRandom secureRandom;
+
+ private final SecretKey kek;
+
+ private final File root;
+
+ private final File keys;
+
+ public KeySupplier(@Nonnull SecureRandom secureRandom, @Nonnull String rootPath)
+ throws GeneralSecurityException, IOException, ClassNotFoundException {
+ this.secureRandom = secureRandom;
+ root = mkdirs(new File(rootPath));
+ keys = mkdirs(new File(root, "keys"));
+ kek = initKek();
+ log.info("Keys supplied from path {}", keys.getAbsolutePath());
+ log.warn("The path {} MUST be strictly protected by access control", root.getAbsolutePath());
+ }
+
+ @CheckForNull
+ public SecretKey get(@Nonnull String keyId) {
+ File keyFile = new File(keys, keyId);
+ byte[] wrapped = null;
+ try {
+ Cipher cipher = getCipher(Cipher.UNWRAP_MODE);
+ wrapped = read(keyFile);
+ if (wrapped != null) {
+ return (SecretKey) cipher.unwrap(wrapped, KEY_ALGORITHM, Cipher.SECRET_KEY);
+ }
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ } finally {
+ clear(wrapped);
+ }
+ return null;
+ }
+
+ void set(@Nonnull String keyId, @Nonnull SecretKey key)
+ throws GeneralSecurityException, IOException {
+ File keyFile = new File(keys, keyId);
+ Cipher cipher = getCipher(Cipher.WRAP_MODE);
+ byte[] wrappedKey = null;
+ try {
+ wrappedKey = cipher.wrap(key);
+ write(keyFile, wrappedKey);
+ } finally {
+ clear(wrappedKey);
+ }
+ }
+
+ @Nonnull
+ private SecretKey initKek()
+ throws GeneralSecurityException, IOException, ClassNotFoundException {
+ File kekFile = new File(root, "kek.ser");
+ byte[] kek = null;
+ try {
+ kek = read(kekFile);
+ return (kek != null) ? new SecretKeySpec(kek, KEY_ALGORITHM) : setupKek(kekFile);
+ } finally {
+ clear(kek);
+ }
+ }
+
+ @Nonnull
+ private SecretKey setupKek(@Nonnull File kekFile)
+ throws GeneralSecurityException, IOException {
+ KeyGenerator gen = KeyGenerator.getInstance(KEY_ALGORITHM);
+ gen.init(KEY_SIZE, secureRandom);
+ SecretKey kek = gen.generateKey();
+ byte[] kekData = null;
+ try {
+ kekData = kek.getEncoded();
+ write(kekFile, kekData);
+ return kek;
+ } finally {
+ clear(kekData);
+ }
+ }
+
+ @Nonnull
+ private File mkdirs(@Nonnull File folder) {
+ folder.mkdirs();
+ return folder;
+ }
+
+ @Nonnull
+ private Cipher getCipher(int mode)
+ throws GeneralSecurityException {
+ Cipher cipher = Cipher.getInstance(CIPHER);
+ cipher.init(mode, kek, secureRandom);
+ return cipher;
+ }
+
+ private void write(@Nonnull File file, @Nonnull byte[] key)
+ throws IOException {
+ ObjectOutputStream oos = null;
+ try {
+ oos = new ObjectOutputStream(new FileOutputStream(file));
+ oos.writeObject(key);
+ } finally {
+ closeQuietly(oos);
+ }
+ }
+
+ @CheckForNull
+ private byte[] read(@Nonnull File file)
+ throws IOException, ClassNotFoundException {
+ if(file.exists()) {
+ ObjectInputStream ois = null;
+ try {
+ ois = new ObjectInputStream(new FileInputStream(file));
+ return (byte[]) ois.readObject();
+ } finally {
+ closeQuietly(ois);
+ }
+ }
+ return null;
+ }
+}
diff --git oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/SymmetricCipherImpl.java oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/SymmetricCipherImpl.java
new file mode 100644
index 0000000..81f0b3e
--- /dev/null
+++ oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/SymmetricCipherImpl.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.jackrabbit.oak.crypto.CryptoException;
+import org.apache.jackrabbit.oak.crypto.SymmetricCipher;
+import org.osgi.service.component.ComponentContext;
+
+import static org.apache.jackrabbit.oak.crypto.impl.Util.checkNotNull;
+import static org.apache.jackrabbit.oak.crypto.impl.Util.clear;
+
+@Service
+@Component(label = "Apache Jackrabbit Oak Symmetric Cipher",
+ description = "The Apache Oak Symmetric Cipher allows transforming ciphertexts into plaintext.")
+public class SymmetricCipherImpl implements SymmetricCipher {
+
+ /*
+ * The structure of the encrypted buffer is as follow
+ *
+ * +-------------+------- ... -------+--------------+-------------- ... ---------------+
+ * |'{' (1 char) | METADATA (m chars) | '}' (1 char) | Hex encoded Ciphertext (n chars) |
+ * +-------------+------- ... -------+--------------+--------------- ... --------------+
+ */
+
+
+ /**
+ * The seed length when reseeding the #secureRandom secure random.
+ */
+ private static final int SEED_LENGTH = 20;
+
+ /**
+ * The number of bytes generated before reseeding the #secureRandom secure random.
+ */
+ private static final int SEED_INTERVAL = 2^30;
+
+ /**
+ * The charset used to for String encryption.
+ */
+ private static final String CHARSET = "UTF-8";
+
+ /**
+ * The identifier for the key used for cipher operations.
+ */
+ private static final String CIPHER_KEY_ID = "cipherKey";
+
+ /**
+ * The identifier for the key used for the MAC operations.
+ */
+ private static final String MAC_KEY_ID = "macKey";
+
+ /**
+ * The name of the property containing the repository home path.
+ */
+ private static final String PROP_HOME = "repository.home";
+
+ /**
+ * The name of the folder holding the keys.
+ */
+ private static final String KEYS_ROOT_NAME = "keys";
+
+ /**
+ * The list of digits that are used to HEX encode the ciphertext
+ */
+ private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ /**
+ * The secure random generator used for all crypto operations.
+ */
+ private static final SecureRandom SECURE_RANDOM =
+ new AutoSeedSecureRandom(SEED_INTERVAL, SEED_LENGTH);
+
+ private static final AesCbcPaddingCipher sc =
+ new AesCbcPaddingCipher();
+
+ private SecretKey cipherKey;
+
+ private SecretKey macKey;
+
+ @Activate
+ protected void activate(@Nonnull ComponentContext context)
+ throws GeneralSecurityException, IOException, ClassNotFoundException {
+ String homeDir = lookup(context, PROP_HOME);
+ if (homeDir == null) {
+ throw new IllegalArgumentException(String.format("Missing %s configuration", PROP_HOME));
+ }
+
+ File keysRoot = new File(new File(homeDir), KEYS_ROOT_NAME);
+ keysRoot.mkdirs();
+
+ KeySupplier supplier = new KeySupplier(SECURE_RANDOM, keysRoot.getAbsolutePath());
+ cipherKey = getOrCreateKey(supplier, CIPHER_KEY_ID);
+ macKey = getOrCreateKey(supplier, MAC_KEY_ID);
+ }
+
+ @Deactivate
+ protected void deactivate() {
+ cipherKey = null;
+ macKey = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Nonnull
+ @Override
+ public String decrypt(@Nonnull String ciphertext) throws CryptoException {
+ checkNotNull(ciphertext);
+ byte[] ptb = null, ctb = null;
+ try {
+ ctb = getCiphertext(sc.getCipherId(), ciphertext);
+ ptb = sc.decrypt(SECURE_RANDOM, macKey, cipherKey, ctb);
+ return new String(ptb, CHARSET);
+ } catch (GeneralSecurityException e) {
+ throw new CryptoException(e);
+ } catch (UnsupportedEncodingException e) {
+ throw new CryptoException(e);
+ } finally {
+ clear(ptb);
+ clear(ctb);
+ }
+ }
+
+ /**
+ * Transforms a plaintext into a ciphertext using a block cipher.
+ * The ciphertext produced can be transformed back into the plaintext
+ * by invoking the {@link #decrypt(String)} method.
+ *
+ * @param plaintext the plaintext (UTF-8 charset) to be transformed into a ciphertext
+ * @return the produced ciphertext (UTF-8 charset)
+ * @throws CryptoException if an error occurs during the transformation
+ */
+ @Nonnull
+ String encrypt(@Nonnull String plaintext) throws CryptoException {
+ checkNotNull(plaintext);
+ byte[] ptb = null, ctb = null, iv = sc.generateIv(SECURE_RANDOM);
+ try {
+ ptb = plaintext.getBytes(CHARSET);
+ ctb = sc.encrypt(SECURE_RANDOM, macKey, cipherKey, iv, ptb);
+ return toEncrypted(sc.getCipherId(), ctb);
+ } catch (GeneralSecurityException e) {
+ throw new CryptoException(e);
+ } catch (UnsupportedEncodingException e) {
+ throw new CryptoException(e);
+ } finally {
+ clear(ctb);
+ clear(ptb);
+ clear(iv);
+ }
+ }
+
+ @Nonnull
+ private SecretKey getOrCreateKey(@Nonnull KeySupplier supplier, @Nonnull String keyId)
+ throws GeneralSecurityException, IOException {
+ SecretKey secretKey = supplier.get(keyId);
+ return (secretKey != null) ? secretKey : createKey(supplier, keyId);
+ }
+
+ @Nonnull
+ private SecretKey createKey(@Nonnull KeySupplier supplier, @Nonnull String keyId)
+ throws GeneralSecurityException, IOException {
+ SecretKey secretKey = sc.generateKey(SECURE_RANDOM);
+ supplier.set(keyId, secretKey);
+ return secretKey;
+ }
+
+ @Nullable
+ private static String lookup(@Nonnull ComponentContext context, String property) {
+ //Prefer property from BundleContext first
+ if (context.getBundleContext().getProperty(property) != null) {
+ return context.getBundleContext().getProperty(property);
+ }
+ if (context.getProperties().get(property) != null) {
+ return context.getProperties().get(property).toString();
+ }
+ return null;
+ }
+
+ @Nonnull
+ private String toEncrypted(@Nonnull String cipherId, @Nonnull byte[] ciphertext) {
+ char[] res = null;
+ char[] meta = meta(cipherId).toCharArray();
+ try {
+ res = new char[meta.length + ciphertext.length * 2];
+ System.arraycopy(meta, 0, res, 0, meta.length);
+ for (int i = 0; i < ciphertext.length; i++) {
+ int value = ciphertext[i] & 0xFF;
+ res[meta.length + i * 2] = HEX_DIGITS[value >>> 4];
+ res[meta.length + i * 2 + 1] = HEX_DIGITS[value & 0x0F];
+ }
+ return new String(res);
+ } finally {
+ clear(res);
+ }
+ }
+
+ @Nonnull
+ private byte[] getCiphertext(@Nonnull String cipherId, @Nonnull String encrypted) {
+ String meta = meta(cipherId);
+ int metaLen = meta.length();
+ int ciphLen = encrypted.length() - metaLen;
+ if (! isEncrypted(encrypted, meta)) {
+ throw new IllegalArgumentException("Can't decrypt plaintext");
+ }
+ byte[] ciphertext = new byte[ciphLen / 2];
+ for (int i = metaLen ; i < encrypted.length() ; i += 2) {
+ int cid = (i - metaLen) / 2;
+ ciphertext[cid] = (byte) ((Character.digit(encrypted.charAt(i), 16) << 4) +
+ Character.digit(encrypted.charAt(i + 1), 16));
+
+ }
+ return ciphertext;
+ }
+
+ @Nonnull
+ private static String meta(@Nonnull String cipherId) {
+ return '{' + cipherId + '}';
+ }
+
+ private static boolean isEncrypted(@Nonnull String data, @Nonnull String meta) {
+ int length = data.length() - meta.length();
+ return data.startsWith(meta) && length >= 0 && length % 2 == 0;
+ }
+}
diff --git oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/Util.java oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/Util.java
new file mode 100644
index 0000000..ae70318
--- /dev/null
+++ oak-crypto/impl/src/main/java/org/apache/jackrabbit/oak/crypto/impl/Util.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.annotation.Nullable;
+
+final class Util {
+
+ /**
+ * Code from http://codahale.com/a-lesson-in-timing-attacks
+ */
+ static boolean isEqual(byte[] expected, byte[] actual) {
+ if (expected.length != actual.length) {
+ return false;
+ }
+ int result = 0;
+ for (int i = 0; i < expected.length; i++) {
+ result |= expected[i] ^ actual[i];
+ }
+ return result == 0;
+ }
+
+ static void clear(@Nullable byte[] data) {
+ if (data != null) {
+ Arrays.fill(data, (byte) 0);
+ }
+ }
+
+ static void clear(@Nullable char[] data) {
+ if (data != null) {
+ Arrays.fill(data, '\u0000');
+ }
+ }
+
+ static void checkNotNull(@Nullable Object ref) {
+ if (ref == null) {
+ throw new NullPointerException();
+ }
+ }
+
+ static void closeQuietly(@Nullable Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException ignore) {}
+ }
+
+}
diff --git oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/AesCbcPaddingCipherTest.java oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/AesCbcPaddingCipherTest.java
new file mode 100644
index 0000000..22d4049
--- /dev/null
+++ oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/AesCbcPaddingCipherTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Random;
+
+import javax.annotation.Nonnull;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class AesCbcPaddingCipherTest {
+
+ private AesCbcPaddingCipher cipher;
+
+ private SecureRandom random;
+
+ private byte[] iv1;
+
+ private byte[] iv2;
+
+ private SecretKey macKey;
+
+ private SecretKey cipherKey;
+
+ @Before
+ public void before() throws Exception {
+ random = getSecureRandom();
+ cipher = new AesCbcPaddingCipher();
+ iv1 = getRandomBytes(16);
+ iv2 = getRandomBytes(16);
+ cipherKey = new SecretKeySpec(getRandomBytes(16), "AES");
+ macKey = new SecretKeySpec(getRandomBytes(16), "HmacSHA256");
+ }
+
+ @Test
+ public void testEncryption() throws Exception {
+ byte[] plaintext = getRandomBytes(4);
+ byte[] ciphertext = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext);
+ assertNotEquals(plaintext, ciphertext);
+ assertTrue(plaintext.length < ciphertext.length);
+ }
+
+ @Test
+ public void testSizesEncryptionDecryptionLoop() throws Exception {
+ for (int length : Arrays.asList(0, 1, 4, 16, 32, 64, 1024, 1024 * 1024)) {
+ testEncryptDecrypt(getRandomBytes(length));
+ }
+ }
+
+ @Test
+ public void testPaddingEncryptionDecryptionLoop() throws Exception {
+ for (int length = 0 ; length < 1024 ; length++) {
+ testEncryptDecrypt(getRandomBytes(length));
+ }
+ }
+
+ @Test
+ public void testSamePlaintextDifferentCiphertexts() throws Exception {
+ byte[] plaintext = getRandomBytes(1024);
+ byte[] ciphertext1 = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext);
+ byte[] ciphertext2 = cipher.encrypt(random, macKey, cipherKey, iv2, plaintext);
+ assertFalse(Arrays.equals(ciphertext1, ciphertext2));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCiphertextTooSmall() throws Exception {
+ byte[] ciphertext = getRandomBytes(47);
+ cipher.decrypt(random, macKey, cipherKey, ciphertext);
+ }
+
+ @Test
+ public void testGenerateIv() throws Exception {
+ byte[] iv1 = cipher.generateIv(random);
+ byte[] iv2 = cipher.generateIv(random);
+ assertEquals(16, iv1.length);
+ assertEquals(16, iv2.length);
+ assertFalse(Arrays.equals(iv1, iv2));
+ }
+
+ @Test(expected = GeneralSecurityException.class)
+ public void testCiphertextIntegrityIv() throws Exception {
+ byte[] plaintext = getRandomBytes(100);
+ byte[] ciphertext = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext);
+ // change a byte in the IV part of the ciphertext
+ changeByteAtIndex(ciphertext, 1);
+ cipher.decrypt(random, macKey, cipherKey, ciphertext);
+ }
+
+ @Test(expected = GeneralSecurityException.class)
+ public void testCiphertextIntegrity() throws Exception {
+ byte[] plaintext = getRandomBytes(100);
+ byte[] ciphertext = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext);
+ // change a byte in the encrypted data part of the ciphertext
+ changeByteAtIndex(ciphertext, 18);
+ cipher.decrypt(random, macKey, cipherKey, ciphertext);
+ }
+
+ @Test(expected = GeneralSecurityException.class)
+ public void testCiphertextIntegrityHmac() throws Exception {
+ byte[] plaintext = getRandomBytes(100);
+ byte[] ciphertext = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext);
+ // change a byte in the HMAC part of the the ciphertext
+ changeByteAtIndex(ciphertext, ciphertext.length - 2);
+ cipher.decrypt(random, macKey, cipherKey, ciphertext);
+ }
+
+ @Test(expected = GeneralSecurityException.class)
+ public void testCiphertextIntegrityShorter() throws Exception {
+ byte[] plaintext = getRandomBytes(100);
+ byte[] ciphertext = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext);
+ // remove the last byte of the ciphertext
+ cipher.decrypt(random, macKey, cipherKey, Arrays.copyOf(ciphertext, ciphertext.length - 1));
+ }
+
+ private void testEncryptDecrypt(@Nonnull byte[] plaintext1)
+ throws GeneralSecurityException {
+ byte[] ciphertext = cipher.encrypt(random, macKey, cipherKey, iv1, plaintext1);
+ byte[] plaintext2 = cipher.decrypt(random, macKey, cipherKey, ciphertext);
+ assertArrayEquals(plaintext1, plaintext2);
+ }
+
+ @Nonnull
+ private SecureRandom getSecureRandom()
+ throws NoSuchAlgorithmException {
+ return SecureRandom.getInstance("SHA1PRNG");
+ }
+
+ @Nonnull
+ private byte[] getRandomBytes(int length) {
+ byte[] key = new byte[length];
+ new Random().nextBytes(key);
+ return key;
+ }
+
+ private void changeByteAtIndex(@Nonnull byte[] data, int index) {
+ data[index] += 1;
+ }
+
+}
diff --git oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/AutoSeedSecureRandomTest.java oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/AutoSeedSecureRandomTest.java
new file mode 100644
index 0000000..0b0dc52
--- /dev/null
+++ oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/AutoSeedSecureRandomTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class AutoSeedSecureRandomTest {
+
+ @Test
+ public void testAutoReseedSecureRandom() {
+ AutoSeedSecureRandom ass = new AutoSeedSecureRandom(10, 1);
+ assertEquals(10, ass.getCount());
+ ass.nextBytes(new byte[2]);
+ assertEquals(8, ass.getCount());
+ ass.nextInt(); // java int are 4 bytes
+ assertEquals(4, ass.getCount());
+ ass.nextInt();
+ assertEquals(10, ass.getCount());
+ }
+
+ @Test
+ public void testAutoSeedSecureRandom() {
+ AutoSeedSecureRandom ass = new AutoSeedSecureRandom(10, 1);
+ assertNotEquals(ass.nextInt(), ass.nextInt());
+ }
+}
\ No newline at end of file
diff --git oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/KeySupplierTest.java oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/KeySupplierTest.java
new file mode 100644
index 0000000..07a72bc
--- /dev/null
+++ oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/KeySupplierTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+
+import javax.annotation.Nonnull;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class KeySupplierTest {
+
+ private String keyStorePath;
+
+ private SecretKey key1;
+
+ private SecretKey key2;
+
+ private SecureRandom secureRandom;
+
+ @Before
+ public void before() throws Exception {
+ secureRandom = getSecureRandom();
+ keyStorePath = generateTempFilePath();
+ byte[] key1Data = new byte[16];
+ new Random().nextBytes(key1Data);
+ byte[] key2Data = new byte[16];
+ new Random().nextBytes(key2Data);
+ key1 = new SecretKeySpec(key1Data, "AES");
+ key2 = new SecretKeySpec(key2Data, "AES");
+ }
+
+ @Test
+ public void testRetrieveMissingKey() throws Exception {
+ KeySupplier supplier = new KeySupplier(secureRandom, keyStorePath);
+ SecretKey retrieved = supplier.get("keyFromTheMoon");
+ assertNull(retrieved);
+ }
+
+ @Test
+ public void testRetrieveExistingKey() throws Exception {
+ KeySupplier supplier = new KeySupplier(secureRandom, keyStorePath);
+ supplier.set("key1", key1);
+ SecretKey retrieved = supplier.get("key1");
+ assertNotNull(retrieved);
+ assertArrayEquals(key1.getEncoded(), retrieved.getEncoded());
+ }
+
+ @Test
+ public void testWriteExistingKey() throws Exception {
+ KeySupplier supplier = new KeySupplier(secureRandom, keyStorePath);
+ supplier.set("key", key1);
+ supplier.set("key", key2);
+ SecretKey retrieved = supplier.get("key");
+ assertNotNull(retrieved);
+ assertArrayEquals(key2.getEncoded(), retrieved.getEncoded());
+ }
+
+ @Nonnull
+ private String generateTempFilePath() throws IOException {
+ File file = File.createTempFile("root", null);
+ if (file.delete()) {
+ return file.getAbsolutePath();
+ }
+ throw new IllegalStateException("Failed to create the temp keys root path");
+ }
+
+ private SecureRandom getSecureRandom()
+ throws NoSuchAlgorithmException {
+ return SecureRandom.getInstance("SHA1PRNG");
+ }
+}
\ No newline at end of file
diff --git oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/SymmetricCipherImplTest.java oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/SymmetricCipherImplTest.java
new file mode 100644
index 0000000..f4e2083
--- /dev/null
+++ oak-crypto/impl/src/test/java/org/apache/jackrabbit/oak/crypto/impl/SymmetricCipherImplTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.jackrabbit.oak.crypto.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SymmetricCipherImplTest {
+
+ private SymmetricCipherImpl symmetricCipher;
+
+ private String repositoryHome;
+
+ private String data;
+
+ @Before
+ public void before() throws Exception {
+ repositoryHome = generateTempFilePath();
+ symmetricCipher = new SymmetricCipherImpl();
+ symmetricCipher.activate(mockComponentContext(repositoryHome));
+ data = RandomStringUtils.randomAlphanumeric(10);
+ }
+
+ @After
+ public void after() throws Exception {
+ symmetricCipher.deactivate();
+ }
+
+ @Test
+ public void testEncryptRoundTripEmpty() throws Exception {
+ testEncryptDecrypt("");
+ }
+
+ @Test
+ public void testEncryptRoundTripVariousLengths() throws Exception {
+ for (int i = 0 ; i < 1000 ; i++) {
+ testEncryptDecrypt(RandomStringUtils.random(i));
+ }
+ }
+
+ @Test
+ public void testEncryptRoundTripVariousEncoding() throws Exception {
+ String encoded = new String(RandomStringUtils.random(100).getBytes("UTF-8"), "ISO-8859-1");
+ String ciphertext = symmetricCipher.encrypt(encoded);
+ String plaintext2 = symmetricCipher.decrypt(ciphertext);
+ assertEquals(encoded, plaintext2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testDecryptingPlainText() throws Exception {
+ symmetricCipher.decrypt("plaintext");
+ }
+
+ @Test
+ public void testEncryptPersistedRoundTrip() throws Exception {
+ SymmetricCipherImpl symmetricCipher1 = new SymmetricCipherImpl();
+ symmetricCipher1.activate(mockComponentContext(repositoryHome));
+ String ciphertext = symmetricCipher.encrypt(data);
+ String original = symmetricCipher1.decrypt(ciphertext);
+ assertEquals(data, original);
+ }
+
+ @Nonnull
+ private ComponentContext mockComponentContext(@Nonnull String repositoryHome)
+ throws IOException {
+ ComponentContext cc = mock(ComponentContext.class);
+ BundleContext bc = mock(BundleContext.class);
+ when(bc.getProperty("repository.home")).thenReturn(repositoryHome);
+ when(cc.getBundleContext()).thenReturn(bc);
+ return cc;
+ }
+
+ @Nonnull
+ private String generateTempFilePath() throws IOException {
+ File file = File.createTempFile("root", null);
+ if (file.delete()) {
+ return file.getAbsolutePath();
+ }
+ throw new IllegalStateException("Failed to create the temp keys root path");
+ }
+
+ private void testEncryptDecrypt(@Nonnull String plaintext1)
+ throws GeneralSecurityException {
+ String ciphertext = symmetricCipher.encrypt(plaintext1);
+ String plaintext2 = symmetricCipher.decrypt(ciphertext);
+ assertEquals(plaintext1, plaintext2);
+ }
+
+}
\ No newline at end of file
diff --git oak-crypto/impl/src/test/resources/logback-test.xml oak-crypto/impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..1d3e2f6
--- /dev/null
+++ oak-crypto/impl/src/test/resources/logback-test.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+ %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n
+
+
+
+
+ target/unit-tests.log
+
+ %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n
+
+
+
+
+
+
+
+
+
diff --git pom.xml pom.xml
index 88002f5..d2af2ab 100644
--- pom.xml
+++ pom.xml
@@ -38,6 +38,8 @@
oak-parent
oak-commons
+ oak-crypto/api
+ oak-crypto/impl
oak-blob
oak-blob-cloud
oak-core