/*
*
* * Copyright 2016 Orient Technologies LTD (info(at)orientechnologies.com)
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
* *
* * For more information: http://www.orientdb.com
*
*/
package com.orientechnologies.orient.core.security.symmetrickey;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.common.log.OLogManager;
import com.orientechnologies.common.io.OIOUtils;
import com.orientechnologies.common.parser.OSystemVariableResolver;
import com.orientechnologies.orient.core.exception.OSecurityException;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.OBase64Utils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
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;
/**
* Implements a symmetric key utility class that can create default keys and keys from a String, a file,
* a KeyStore, and from the OSymmetricKeyConfig interface.
*
* Static creation methods are provided for each type:
* OSymmetricKey.fromConfig()
* OSymmetricKey.fromString()
* OSymmetricKey.fromFile()
* OSymmetricKey.fromStream()
* OSymmetricKey.fromKeystore()
*
* The encrypt() methods return a specialized Base64-encoded JSON document with these properties (depending on the cipher transform):
* "algorithm", "transform", "iv", "payload"
*
* The decrypt() and decryptAsString() methods accept the Base64-encoded JSON document.
*
* A symmetric key credential interceptor is provided (OSymmetricKeyCI) as well as several authenticators:
* OSecuritySymmetricKeyAuth, OSystemSymmetricKeyAuth
*
* @author S. Colin Leister
*/
public class OSymmetricKey {
// These are just defaults.
private String seedAlgorithm = "PBKDF2WithHmacSHA1";
private String seedPhrase = UUID.randomUUID().toString();
// Holds the length of the salt byte array.
private int saltLength = 64;
// Holds the default number of iterations used. This may be overridden in the configuration.
private int iteration = 65536;
private String secretKeyAlgorithm = "AES";
private String defaultCipherTransformation = "AES/CBC/PKCS5Padding";
// Holds the size of the key (in bits).
private int keySize = 128;
private SecretKey secretKey;
// Getters
public String getDefaultCipherTransform(final String transform) { return defaultCipherTransformation; }
public int getIteration(int iteration) { return iteration; }
public String getKeyAlgorithm(final String algorithm) { return secretKeyAlgorithm; }
public int getKeySize(int bits) { return keySize; }
public int getSaltLength(int length) { return saltLength; }
public String getSeedAlgorithm(final String algorithm) { return seedAlgorithm; }
public String getSeedPhrase(final String phrase) { return seedPhrase; }
// Setters
public OSymmetricKey setDefaultCipherTransform(final String transform) { defaultCipherTransformation = transform; return this; }
public OSymmetricKey setIteration(int iteration) { this.iteration = iteration; return this; }
public OSymmetricKey setKeyAlgorithm(final String algorithm) { secretKeyAlgorithm = algorithm; return this; }
public OSymmetricKey setKeySize(int bits) { keySize = bits; return this; }
public OSymmetricKey setSaltLength(int length) { saltLength = length; return this; }
public OSymmetricKey setSeedAlgorithm(final String algorithm) { seedAlgorithm = algorithm; return this; }
public OSymmetricKey setSeedPhrase(final String phrase) { seedPhrase = phrase; return this; }
public OSymmetricKey() {
create();
}
/**
* Creates a key based on the algorithm, transformation, and key size specified.
*/
public OSymmetricKey(final String secretKeyAlgorithm, final String cipherTransform, final int keySize) {
this.secretKeyAlgorithm = secretKeyAlgorithm;
this.defaultCipherTransformation = cipherTransform;
this.keySize = keySize;
create();
}
/**
* Uses the specified SecretKey as the private key and sets key algorithm from the SecretKey.
*/
public OSymmetricKey(final SecretKey secretKey) throws OSecurityException {
if(secretKey == null) throw new OSecurityException("OSymmetricKey(SecretKey) secretKey is null");
this.secretKey = secretKey;
this.secretKeyAlgorithm = secretKey.getAlgorithm();
}
/**
* Sets the SecretKey based on the specified algorithm and Base64 key specified.
*/
public OSymmetricKey(final String algorithm, final String base64Key) throws OSecurityException {
this.secretKeyAlgorithm = algorithm;
try {
final byte[] keyBytes = OSymmetricKey.convertFromBase64(base64Key);
this.secretKey = new SecretKeySpec(keyBytes, secretKeyAlgorithm);
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.OSymmetricKey() Exception: " + ex.getMessage());
}
}
protected void create()
{
try {
SecureRandom secureRandom = new SecureRandom();
byte[] salt = secureRandom.generateSeed(saltLength);
KeySpec keySpec = new PBEKeySpec(seedPhrase.toCharArray(), salt, iteration, keySize);
SecretKeyFactory factory = SecretKeyFactory.getInstance(seedAlgorithm);
SecretKey tempKey = factory.generateSecret(keySpec);
secretKey = new SecretKeySpec(tempKey.getEncoded(), secretKeyAlgorithm);
}
catch(Exception ex)
{
throw new OSecurityException("OSymmetricKey.create() Exception: " + ex);
}
}
/**
* Returns the secret key algorithm portion of the cipher transformation.
*/
protected static String separateAlgorithm(final String cipherTransform) {
String [] array = cipherTransform.split("/");
if(array.length > 1) return array[0];
return null;
}
/**
* Creates an OSymmetricKey from an OSymmetricKeyConfig interface.
*/
public static OSymmetricKey fromConfig(final OSymmetricKeyConfig keyConfig) {
if(keyConfig.usesKeyString()) {
return fromString(keyConfig.getKeyAlgorithm(), keyConfig.getKeyString());
}
else
if(keyConfig.usesKeyFile()) {
return fromFile(keyConfig.getKeyAlgorithm(), keyConfig.getKeyFile());
}
else
if(keyConfig.usesKeystore()) {
return fromKeystore(keyConfig.getKeystoreFile(), keyConfig.getKeystorePassword(), keyConfig.getKeystoreKeyAlias(), keyConfig.getKeystoreKeyPassword());
}
else {
throw new OSecurityException("OSymmetricKey(OSymmetricKeyConfig) Invalid configuration");
}
}
/**
* Creates an OSymmetricKey from a Base64 key.
*/
public static OSymmetricKey fromString(final String algorithm, final String base64Key) {
return new OSymmetricKey(algorithm, base64Key);
}
/**
* Creates an OSymmetricKey from a file containing a Base64 key.
*/
public static OSymmetricKey fromFile(final String algorithm, final String path) {
String base64Key = null;
try {
java.io.FileInputStream fis = null;
try {
fis = new java.io.FileInputStream(OSystemVariableResolver.resolveSystemVariables(path));
return fromStream(algorithm, fis);
}
finally {
if(fis != null) fis.close();
}
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.fromFile() Exception: " + ex.getMessage());
}
}
/**
* Creates an OSymmetricKey from an InputStream containing a Base64 key.
*/
public static OSymmetricKey fromStream(final String algorithm, final InputStream is) {
String base64Key = null;
try {
base64Key = OIOUtils.readStreamAsString(is);
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.fromStream() Exception: " + ex.getMessage());
}
return new OSymmetricKey(algorithm, base64Key);
}
/**
* Creates an OSymmetricKey from a Java "JCEKS" KeyStore.
* @param path The location of the KeyStore file.
* @param password The password for the KeyStore. May be null.
* @param keyAlias The alias name of the key to be used from the KeyStore. Required.
* @param keyPassword The password of the key represented by keyAlias. May be null.
*/
public static OSymmetricKey fromKeystore(final String path, final String password, final String keyAlias, final String keyPassword) {
OSymmetricKey sk = null;
try {
KeyStore ks = KeyStore.getInstance("JCEKS"); // JCEKS is required to hold SecretKey entries.
java.io.FileInputStream fis = null;
try {
fis = new java.io.FileInputStream(OSystemVariableResolver.resolveSystemVariables(path));
return fromKeystore(fis, password, keyAlias, keyPassword);
}
finally {
if(fis != null) fis.close();
}
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.fromKeystore() Exception: " + ex.getMessage());
}
}
/**
* Creates an OSymmetricKey from a Java "JCEKS" KeyStore.
* @param is The InputStream used to load the KeyStore.
* @param password The password for the KeyStore. May be null.
* @param keyAlias The alias name of the key to be used from the KeyStore. Required.
* @param keyPassword The password of the key represented by keyAlias. May be null.
*/
public static OSymmetricKey fromKeystore(final InputStream is, final String password, final String keyAlias, final String keyPassword) {
OSymmetricKey sk = null;
try {
KeyStore ks = KeyStore.getInstance("JCEKS"); // JCEKS is required to hold SecretKey entries.
char [] ksPasswdChars = null;
if(password != null) ksPasswdChars = password.toCharArray();
ks.load(is, ksPasswdChars); // ksPasswdChars may be null.
char [] ksKeyPasswdChars = null;
if(keyPassword != null) ksKeyPasswdChars = keyPassword.toCharArray();
KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(ksKeyPasswdChars); // ksKeyPasswdChars may be null.
KeyStore.SecretKeyEntry skEntry = (KeyStore.SecretKeyEntry)ks.getEntry(keyAlias, protParam);
if(skEntry == null) throw new OSecurityException("SecretKeyEntry is null for key alias: " + keyAlias);
SecretKey secretKey = skEntry.getSecretKey();
sk = new OSymmetricKey(secretKey);
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.fromKeystore() Exception: " + ex.getMessage());
}
return sk;
}
/**
* Returns the internal SecretKey as a Base64 String.
*/
public String getBase64Key() {
if(secretKey == null) throw new OSecurityException("OSymmetricKey.getBase64Key() SecretKey is null");
return convertToBase64(secretKey.getEncoded());
}
protected static String convertToBase64(final byte[] bytes) {
String result = null;
try {
result = OBase64Utils.encodeBytes(bytes);
} catch(Exception ex) {
OLogManager.instance().error(null, "convertToBase64() Exception: %s", ex.getMessage());
}
return result;
}
protected static byte[] convertFromBase64(final String base64) {
byte[] result = null;
try
{
if(base64 != null) {
result = OBase64Utils.decode(base64.getBytes("UTF8"));
}
} catch(Exception ex) {
OLogManager.instance().error(null, "convertFromBase64() Exception: %s", ex.getMessage());
}
return result;
}
/**
* This is a convenience method that takes a String argument, encodes it as Base64, then calls encrypt(byte[]).
*
* @param value The String to be encoded to Base64 then encrypted.
*
* @return A Base64-encoded JSON document.
*/
public String encrypt(final String value) {
try {
return encrypt(value.getBytes("UTF8"));
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.encrypt() Exception: " + ex.getMessage());
}
}
/**
* This is a convenience method that takes a String argument, encodes it as Base64, then calls encrypt(byte[]).
*
* @param transform The cipher transformation to use.
* @param value The String to be encoded to Base64 then encrypted.
*
* @return A Base64-encoded JSON document.
*/
public String encrypt(final String transform, final String value) {
try {
return encrypt(transform, value.getBytes("UTF8"));
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.encrypt() Exception: " + ex.getMessage());
}
}
/**
* This method encrypts an array of bytes.
*
* @param bytes The array of bytes to be encrypted.
*
* @return The encrypted bytes as a Base64-encoded JSON document or null if unsuccessful.
*/
public String encrypt(final byte[] bytes) {
return encrypt(defaultCipherTransformation, bytes);
}
/**
* This method encrypts an array of bytes.
*
* @param transform The cipher transformation to use.
* @param bytes The array of bytes to be encrypted.
*
* @return The encrypted bytes as a Base64-encoded JSON document or null if unsuccessful.
*/
public String encrypt(final String transform, final byte[] bytes) {
String encodedJSON = null;
if(secretKey == null) throw new OSecurityException("OSymmetricKey.encrypt() SecretKey is null");
if(transform == null) throw new OSecurityException("OSymmetricKey.encrypt() Cannot determine cipher transformation");
try {
// Throws NoSuchAlgorithmException and NoSuchPaddingException.
Cipher cipher = Cipher.getInstance(transform);
// If the cipher transformation requires an initialization vector then init() will create a random one.
// (Use cipher.getIV() to retrieve the IV, if it exists.)
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// If the cipher does not use an IV, this will be null.
byte[] initVector = cipher.getIV();
// byte[] initVector = encCipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
byte[] encrypted = cipher.doFinal(bytes);
encodedJSON = encodeJSON(encrypted, initVector);
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.encrypt() Exception: " + ex.getMessage());
}
return encodedJSON;
}
protected String encodeJSON(final byte[] encrypted, final byte[] initVector) {
String encodedJSON = null;
String encryptedBase64 = convertToBase64(encrypted);
String initVectorBase64 = null;
if(initVector != null) initVectorBase64 = convertToBase64(initVector);
// Create the JSON document.
StringBuffer sb = new StringBuffer();
sb.append("{");
sb.append("\"algorithm\":\"");
sb.append(secretKeyAlgorithm);
sb.append("\",\"transform\":\"");
sb.append(defaultCipherTransformation);
sb.append("\",\"payload\":\"");
sb.append(encryptedBase64);
sb.append("\"");
if(initVectorBase64 != null) {
sb.append(",\"iv\":\"");
sb.append(initVectorBase64);
sb.append("\"");
}
sb.append("}");
try {
// Convert the JSON document to Base64, for a touch more obfuscation.
encodedJSON = convertToBase64(sb.toString().getBytes("UTF8"));
} catch(Exception ex) {
}
return encodedJSON;
}
/**
* This method decrypts the Base64-encoded JSON document using the specified algorithm and cipher transformation.
*
* @param encodedJSON The Base64-encoded JSON document.
*
* @return The decrypted array of bytes as a UTF8 String or null if not successful.
*
*/
public String decryptAsString(final String encodedJSON) {
try {
byte[] decrypted = decrypt(encodedJSON);
return new String(decrypted, "UTF8");
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.decryptAsString() Exception: " + ex.getMessage());
}
}
/**
* This method decrypts the Base64-encoded JSON document using the specified algorithm and cipher transformation.
*
* @param encodedJSON The Base64-encoded JSON document.
*
* @return The decrypted array of bytes or null if unsuccessful.
*
*/
public byte[] decrypt(final String encodedJSON) {
byte[] result = null;
if(encodedJSON == null) throw new OSecurityException("OSymmetricKey.decrypt(String) encodedJSON is null");
try {
byte[] decoded = convertFromBase64(encodedJSON);
if(decoded == null) throw new OSecurityException("OSymmetricKey.decrypt(String) encodedJSON could not be decoded");
String json = new String(decoded, "UTF8");
// Convert the JSON content to an ODocument to make parsing it easier.
final ODocument doc = new ODocument().fromJSON(json, "noMap");
// Set a default in case the JSON document does not contain an "algorithm" property.
String algorithm = secretKeyAlgorithm;
if(doc.containsField("algorithm")) algorithm = doc.field("algorithm");
// Set a default in case the JSON document does not contain a "transform" property.
String transform = defaultCipherTransformation;
if(doc.containsField("transform")) transform = doc.field("transform");
String payloadBase64 = doc.field("payload");
String ivBase64 = doc.field("iv");
byte[] payload = null;
byte[] iv = null;
if(payloadBase64 != null) payload = convertFromBase64(payloadBase64);
if(ivBase64 != null) iv = convertFromBase64(ivBase64);
// Throws NoSuchAlgorithmException and NoSuchPaddingException.
Cipher cipher = Cipher.getInstance(transform);
if(iv != null)
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
else
cipher.init(Cipher.DECRYPT_MODE, secretKey);
result = cipher.doFinal(payload);
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.decrypt(String) Exception: " + ex.getMessage());
}
return result;
}
/**
* Saves the internal SecretKey to the specified OutputStream as a Base64 String.
*/
public void saveToStream(final OutputStream os) {
if(os == null) throw new OSecurityException("OSymmetricKey.saveToStream() OutputStream is null");
try {
final OutputStreamWriter osw = new OutputStreamWriter(os);
try {
final BufferedWriter writer = new BufferedWriter(osw);
try {
writer.write(getBase64Key());
} finally {
writer.close();
}
} finally {
os.close();
}
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.saveToStream() Exception: " + ex.getMessage());
}
}
/**
* Saves the internal SecretKey as a KeyStore.
*/
public void saveToKeystore(final OutputStream os, final String ksPasswd, final String keyAlias, final String keyPasswd) {
if(os == null) throw new OSecurityException("OSymmetricKey.saveToKeystore() OutputStream is null");
if(ksPasswd == null) throw new OSecurityException("OSymmetricKey.saveToKeystore() Keystore Password is required");
if(keyAlias == null) throw new OSecurityException("OSymmetricKey.saveToKeystore() Key Alias is required");
if(keyPasswd == null) throw new OSecurityException("OSymmetricKey.saveToKeystore() Key Password is required");
try {
KeyStore ks = KeyStore.getInstance("JCEKS");
char [] ksPasswdCA = ksPasswd.toCharArray();
char [] keyPasswdCA = keyPasswd.toCharArray();
// Create a new KeyStore by passing null.
ks.load(null, ksPasswdCA);
KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(keyPasswdCA);
KeyStore.SecretKeyEntry skEntry = new KeyStore.SecretKeyEntry(secretKey);
ks.setEntry(keyAlias, skEntry, protParam);
// Save the KeyStore
ks.store(os, ksPasswdCA);
} catch(Exception ex) {
throw new OSecurityException("OSymmetricKey.saveToKeystore() Exception: " + ex.getMessage());
}
}
}