blob: cd4b327963687d43ca7d521738d6383430811883 [file] [log] [blame]
/*
* 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.druid.crypto;
import com.google.common.base.Preconditions;
import org.apache.druid.java.util.common.StringUtils;
import javax.annotation.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
/**
* Utility class for symmetric key encryption (i.e. same secret is used for encryption and decryption) of byte[]
* using javax.crypto package.
*
* To learn about possible algorithms supported and their names,
* See https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
*/
public class CryptoService
{
// Based on Javadocs on SecureRandom, It is threadsafe as well.
private static final SecureRandom SECURE_RANDOM_INSTANCE = new SecureRandom();
// User provided secret phrase used for encrypting data
private final char[] passPhrase;
// Variables for algorithm used to generate a SecretKey based on user provided passPhrase
private final String secretKeyFactoryAlg;
private final int saltSize;
private final int iterationCount;
private final int keyLength;
// Cipher algorithm information
private final String cipherAlgName;
private final String cipherAlgMode;
private final String cipherAlgPadding;
// transformation = "cipherAlgName/cipherAlgMode/cipherAlgPadding" used in Cipher.getInstance(transformation)
private final String transformation;
public CryptoService(
String passPhrase,
@Nullable String cipherAlgName,
@Nullable String cipherAlgMode,
@Nullable String cipherAlgPadding,
@Nullable String secretKeyFactoryAlg,
@Nullable Integer saltSize,
@Nullable Integer iterationCount,
@Nullable Integer keyLength
)
{
Preconditions.checkArgument(
passPhrase != null && !passPhrase.isEmpty(),
"null/empty passPhrase"
);
this.passPhrase = passPhrase.toCharArray();
this.cipherAlgName = cipherAlgName == null ? "AES" : cipherAlgName;
this.cipherAlgMode = cipherAlgMode == null ? "CBC" : cipherAlgMode;
this.cipherAlgPadding = cipherAlgPadding == null ? "PKCS5Padding" : cipherAlgPadding;
this.transformation = StringUtils.format("%s/%s/%s", this.cipherAlgName, this.cipherAlgMode, this.cipherAlgPadding);
this.secretKeyFactoryAlg = secretKeyFactoryAlg == null ? "PBKDF2WithHmacSHA256" : secretKeyFactoryAlg;
this.saltSize = saltSize == null ? 8 : saltSize;
this.iterationCount = iterationCount == null ? 65536 : iterationCount;
this.keyLength = keyLength == null ? 128 : keyLength;
// encrypt/decrypt a test string to ensure all params are valid
String testString = "duh! !! !!!";
Preconditions.checkState(
testString.equals(StringUtils.fromUtf8(decrypt(encrypt(StringUtils.toUtf8(testString))))),
"decrypt(encrypt(testString)) failed"
);
}
public byte[] encrypt(byte[] plain)
{
try {
byte[] salt = new byte[saltSize];
SECURE_RANDOM_INSTANCE.nextBytes(salt);
SecretKey tmp = getKeyFromPassword(passPhrase, salt);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), cipherAlgName);
// error-prone warns if the transformation is not a compile-time constant
// since it cannot check it for insecure combinations.
@SuppressWarnings("InsecureCryptoUsage")
Cipher ecipher = Cipher.getInstance(transformation);
ecipher.init(Cipher.ENCRYPT_MODE, secret);
return new EncryptedData(
salt,
ecipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV(),
ecipher.doFinal(plain)
).toByteAray();
}
catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidParameterSpecException | IllegalBlockSizeException | BadPaddingException ex) {
throw new RuntimeException(ex);
}
}
public byte[] decrypt(byte[] data)
{
try {
EncryptedData encryptedData = EncryptedData.fromByteArray(data);
SecretKey tmp = getKeyFromPassword(passPhrase, encryptedData.getSalt());
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), cipherAlgName);
// error-prone warns if the transformation is not a compile-time constant
// since it cannot check it for insecure combinations.
@SuppressWarnings("InsecureCryptoUsage")
Cipher dcipher = Cipher.getInstance(transformation);
dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(encryptedData.getIv()));
return dcipher.doFinal(encryptedData.getCipher());
}
catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) {
throw new RuntimeException(ex);
}
}
private SecretKey getKeyFromPassword(char[] passPhrase, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException
{
SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKeyFactoryAlg);
KeySpec spec = new PBEKeySpec(passPhrase, salt, iterationCount, keyLength);
return factory.generateSecret(spec);
}
private static class EncryptedData
{
private final byte[] salt;
private final byte[] iv;
private final byte[] cipher;
public EncryptedData(byte[] salt, byte[] iv, byte[] cipher)
{
this.salt = salt;
this.iv = iv;
this.cipher = cipher;
}
public byte[] getSalt()
{
return salt;
}
public byte[] getIv()
{
return iv;
}
public byte[] getCipher()
{
return cipher;
}
public byte[] toByteAray()
{
int headerLength = 12;
ByteBuffer bb = ByteBuffer.allocate(salt.length + iv.length + cipher.length + headerLength);
bb.putInt(salt.length)
.putInt(iv.length)
.putInt(cipher.length)
.put(salt)
.put(iv)
.put(cipher);
bb.flip();
return bb.array();
}
public static EncryptedData fromByteArray(byte[] array)
{
ByteBuffer bb = ByteBuffer.wrap(array);
int saltSize = bb.getInt();
int ivSize = bb.getInt();
int cipherSize = bb.getInt();
byte[] salt = new byte[saltSize];
bb.get(salt);
byte[] iv = new byte[ivSize];
bb.get(iv);
byte[] cipher = new byte[cipherSize];
bb.get(cipher);
return new EncryptedData(salt, iv, cipher);
}
}
}