diff --git oak-crypto/api/README.md oak-crypto/api/README.md new file mode 100644 index 0000000..688b2e2 --- /dev/null +++ oak-crypto/api/README.md @@ -0,0 +1,26 @@ +Oak Crypto API +============== + +Module providing the interfaces for cryptography related features. + +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/api/pom.xml oak-crypto/api/pom.xml new file mode 100644 index 0000000..0c5da03 --- /dev/null +++ oak-crypto/api/pom.xml @@ -0,0 +1,73 @@ + + + + + + 4.0.0 + + + org.apache.jackrabbit + oak-parent + 1.4-SNAPSHOT + ../../oak-parent/pom.xml + + + oak-crypto-api + Oak Crypto API + bundle + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.jackrabbit.oak.crypto + + + + + + org.apache.felix + maven-scr-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + + + biz.aQute.bnd + bndlib + provided + + + org.apache.felix + org.apache.felix.scr.annotations + provided + + + com.google.code.findbugs + jsr305 + + + diff --git oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/CryptoException.java oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/CryptoException.java new file mode 100644 index 0000000..7715799 --- /dev/null +++ oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/CryptoException.java @@ -0,0 +1,45 @@ +/* + * 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; + +import java.security.GeneralSecurityException; + +/** + * The {@code CryptoException} exception indicates a crypto related errors + * occurring in the Crypto API. + */ +public class CryptoException extends GeneralSecurityException { + + private static final long serialVersionUID = -4589040130474684024L; + + public CryptoException() { + super(); + } + + public CryptoException(String message) { + super(message); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + + public CryptoException(Throwable cause) { + super(cause); + } + +} diff --git oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/SymmetricCipher.java oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/SymmetricCipher.java new file mode 100644 index 0000000..3e08a5f --- /dev/null +++ oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/SymmetricCipher.java @@ -0,0 +1,41 @@ +/* + * 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; + +import javax.annotation.Nonnull; + +import aQute.bnd.annotation.ProviderType; + +/** + * The {@code SymmetricCipher} API provides methods related to symmetric cryptography features. + */ +@ProviderType +public interface SymmetricCipher { + + /** + * Transforms a ciphertext into the plaintext, using a block cipher. + * + * @param ciphertext the ciphertext (UTF-8 charset) to be transformed back into plaintext + * @return the original plaintext (UTF-8 charset) + * @throws CryptoException if an error occurs during the transformation + */ + @Nonnull + String decrypt(@Nonnull String ciphertext) throws CryptoException; + +} + + diff --git oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/package-info.java oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/package-info.java new file mode 100644 index 0000000..c0a9e7f --- /dev/null +++ oak-crypto/api/src/main/java/org/apache/jackrabbit/oak/crypto/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +@Version("1.0") +@Export(optional = "provide:=true") +package org.apache.jackrabbit.oak.crypto; + +import aQute.bnd.annotation.Version; +import aQute.bnd.annotation.Export; \ No newline at end of file diff --git oak-crypto/impl/README.md oak-crypto/impl/README.md new file mode 100644 index 0000000..21aa96b --- /dev/null +++ oak-crypto/impl/README.md @@ -0,0 +1,51 @@ +Oak Crypto Impl +=============== + +Module which provides an implementation of the [Oak Crypto APIs](../api) based +on the default cryptographic services provided by the JDK/JRE. + +The implementation aims at being portable on platforms compliant with Java SE 7 and SE 8. +To this end, the implementation leverages exclusively the security algorithm which are 'required' as part of the JCA +[specification](https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl). + +## Features + +### Authenticated Encryption/Decryption + +The module supports authenticated encryption cipher using the algorithms ```AES/CBC/PKCS5Padding``` and ```HmacSHA256```. + +Cipher keys are stored encrypted (using the ```AES/ECB/NoPadding``` key wrap algorithm) in the filesytem 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. + +### Crypto Servlet + +The module contains a simple Crypto Servlet which exposes the encryption feature as a HTTP resource. +Encryption can be invoked as follow: + +``` +tmaret-osx$ curl --data 'datum=' 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