blob: 052d59326e257e50b1c96c04ee77a9317a2a7a14 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Inno-Tec Innovative Technologies GmbH - Fix for Bug 388055
*
*******************************************************************************/
package org.eclipse.equinox.internal.security.storage;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
import java.util.Map.Entry;
import javax.crypto.*;
import javax.crypto.spec.*;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.ConfigurationScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.equinox.internal.security.auth.AuthPlugin;
import org.eclipse.equinox.internal.security.auth.nls.SecAuthMessages;
import org.eclipse.equinox.internal.security.storage.friends.*;
import org.eclipse.equinox.security.storage.StorageException;
import org.eclipse.osgi.util.NLS;
/**
* Note that algorithm detection skips aliases:
* Alg.Alias.Cipher.ABC
* only a few aliases are useful and it will be harder to separate human-readable
* aliases from internal ones.
*
*/
public class JavaEncryption {
private final static String SECRET_KEY_FACTORY = "SecretKeyFactory."; //$NON-NLS-1$
private final static String CIPHER = "Cipher."; //$NON-NLS-1$
private final static String sampleText = "sample text for roundtrip testing"; //$NON-NLS-1$
private final static PasswordExt samplePassword = new PasswordExt(new PBEKeySpec("password1".toCharArray()), "abc"); //$NON-NLS-1$ //$NON-NLS-2$
static private ILock lock = Job.getJobManager().newLock();
static private final int SALT_ITERATIONS = 10;
private String keyFactoryAlgorithm = null;
private String cipherAlgorithm = null;
private boolean initialized = false;
private HashMap<String, String> availableCiphers;
public JavaEncryption() {
// placeholder
}
public String getKeyFactoryAlgorithm() {
return keyFactoryAlgorithm;
}
public String getCipherAlgorithm() {
return cipherAlgorithm;
}
public void setAlgorithms(String cipherAlgorithm, String keyFactoryAlgorithm) {
try {
lock.acquire(); // avoid conflict with init()
this.cipherAlgorithm = cipherAlgorithm;
this.keyFactoryAlgorithm = keyFactoryAlgorithm;
} finally {
lock.release();
}
}
private void init() throws StorageException {
if (initialized)
return;
initialized = true;
try {
lock.acquire(); // avoid multiple simultaneous initializations
IUICallbacks callback = CallbacksProvider.getDefault().getCallback();
if (callback == null)
internalInitialize();
else {
callback.execute(() -> internalInitialize());
}
} finally {
lock.release();
}
}
protected void internalInitialize() throws StorageException {
if (cipherAlgorithm != null && keyFactoryAlgorithm != null) {
if (roundtrip(cipherAlgorithm, keyFactoryAlgorithm))
return;
// this is a bad situation - JVM cipher no longer available. Both log and throw an exception
String msg = NLS.bind(SecAuthMessages.noAlgorithm, cipherAlgorithm);
StorageException e = new StorageException(StorageException.INTERNAL_ERROR, msg);
AuthPlugin.getDefault().logError(msg, e);
throw e;
}
if (cipherAlgorithm == null || keyFactoryAlgorithm == null) {
IEclipsePreferences eclipseNode = ConfigurationScope.INSTANCE.getNode(AuthPlugin.PI_AUTH);
cipherAlgorithm = eclipseNode.get(IStorageConstants.CIPHER_KEY, IStorageConstants.DEFAULT_CIPHER);
keyFactoryAlgorithm = eclipseNode.get(IStorageConstants.KEY_FACTORY_KEY, IStorageConstants.DEFAULT_KEY_FACTORY);
}
if (roundtrip(cipherAlgorithm, keyFactoryAlgorithm))
return;
String unavailableCipher = cipherAlgorithm;
detect();
if (availableCiphers.size() == 0)
throw new StorageException(StorageException.INTERNAL_ERROR, SecAuthMessages.noAlgorithms);
// use first available
cipherAlgorithm = availableCiphers.keySet().iterator().next();
keyFactoryAlgorithm = availableCiphers.get(cipherAlgorithm);
String msg = NLS.bind(SecAuthMessages.usingAlgorithm, unavailableCipher, cipherAlgorithm);
AuthPlugin.getDefault().logMessage(msg);
}
public CryptoData encrypt(PasswordExt passwordExt, byte[] clearText) throws StorageException {
init();
return internalEncrypt(passwordExt, clearText);
}
private CryptoData internalEncrypt(PasswordExt passwordExt, byte[] clearText) throws StorageException {
try {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(keyFactoryAlgorithm);
SecretKey key = keyFactory.generateSecret(passwordExt.getPassword());
byte[] salt = new byte[8];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
PBEParameterSpec entropy = new PBEParameterSpec(salt, SALT_ITERATIONS);
Cipher c = Cipher.getInstance(cipherAlgorithm);
c.init(Cipher.ENCRYPT_MODE, key, entropy);
byte[] iv = c.getIV();
byte[] result = c.doFinal(clearText);
return new CryptoData(passwordExt.getModuleID(), salt, result, iv);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
handle(e, StorageException.ENCRYPTION_ERROR);
return null;
} catch (InvalidKeySpecException | NoSuchPaddingException | NoSuchAlgorithmException e) {
handle(e, StorageException.INTERNAL_ERROR);
return null;
}
}
public byte[] decrypt(PasswordExt passwordExt, CryptoData encryptedData) throws StorageException, IllegalStateException, IllegalBlockSizeException, BadPaddingException {
init();
return internalDecrypt(passwordExt, encryptedData);
}
private byte[] internalDecrypt(PasswordExt passwordExt, CryptoData encryptedData) throws StorageException, IllegalStateException, IllegalBlockSizeException, BadPaddingException {
try {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(keyFactoryAlgorithm);
SecretKey key = keyFactory.generateSecret(passwordExt.getPassword());
IvParameterSpec ivParamSpec = null;
if (encryptedData.getIV() != null) {
ivParamSpec = new IvParameterSpec(encryptedData.getIV());
}
PBEParameterSpec entropy = null;
if (ivParamSpec != null) {
entropy = new PBEParameterSpec(encryptedData.getSalt(), SALT_ITERATIONS, ivParamSpec);
} else {
entropy = new PBEParameterSpec(encryptedData.getSalt(), SALT_ITERATIONS);
}
Cipher c = Cipher.getInstance(cipherAlgorithm);
c.init(Cipher.DECRYPT_MODE, key, entropy);
byte[] result = c.doFinal(encryptedData.getData());
return result;
} catch (InvalidAlgorithmParameterException | InvalidKeyException | InvalidKeySpecException | NoSuchPaddingException | NoSuchAlgorithmException e) {
handle(e, StorageException.INTERNAL_ERROR);
return null;
}
}
private void handle(Exception e, int internalCode) throws StorageException {
if (AuthPlugin.DEBUG_LOGIN_FRAMEWORK)
e.printStackTrace();
StorageException exception = new StorageException(internalCode, e);
throw exception;
}
/////////////////////////////////////////////////////////////////////////////////////
// Algorithm detection
/**
* Result: Map:
* <String>cipher -> <String>keyFactory
*/
public HashMap<String, String> detect() {
IUICallbacks callback = CallbacksProvider.getDefault().getCallback();
if (callback == null)
return internalDetect();
IStorageTask task = () -> internalDetect();
try {
callback.execute(task);
} catch (StorageException e) { // should not happen in this path
AuthPlugin.getDefault().logError(e.getMessage(), e);
}
return availableCiphers;
}
public HashMap<String, String> internalDetect() {
Set<String> ciphers = findProviders(CIPHER);
Set<String> keyFactories = findProviders(SECRET_KEY_FACTORY);
availableCiphers = new HashMap<>(ciphers.size());
for (String cipher : ciphers) {
// check if there is a key factory with the same name
if (keyFactories.contains(cipher)) {
if (roundtrip(cipher, cipher)) {
availableCiphers.put(cipher, cipher);
continue;
}
}
for (String keyFactory : keyFactories) {
if (roundtrip(cipher, keyFactory)) {
availableCiphers.put(cipher, keyFactory);
continue;
}
}
}
return availableCiphers;
}
private Set<String> findProviders(String prefix) {
Provider[] providers = Security.getProviders();
Set<String> algorithms = new HashSet<>();
int prefixLength = prefix.length();
for (int i = 0; i < providers.length; i++) {
for (Iterator<Entry<Object, Object>> j = providers[i].entrySet().iterator(); j.hasNext();) {
Entry<Object, Object> entry = j.next();
Object key = entry.getKey();
if (key == null)
continue;
if (!(key instanceof String))
continue;
String value = (String) key;
if (value.indexOf(' ') != -1) // skips properties like "[Cipher.ABC SupportedPaddings]"
continue;
if (value.startsWith(prefix)) {
String keyFactory = value.substring(prefixLength);
algorithms.add(keyFactory);
}
}
}
return algorithms;
}
private boolean roundtrip(String testCipher, String testKeyFactory) {
boolean storeInitState = initialized;
String storedCipherAlgorithm = cipherAlgorithm;
String storedKeyAlgorithm = keyFactoryAlgorithm;
initialized = true;
try {
cipherAlgorithm = testCipher;
keyFactoryAlgorithm = testKeyFactory;
CryptoData encrypted = internalEncrypt(samplePassword, StorageUtils.getBytes(sampleText));
byte[] roundtripBytes = internalDecrypt(samplePassword, encrypted);
String result = StorageUtils.getString(roundtripBytes);
return sampleText.equals(result);
} catch (Exception e) {
// internal implementation throws both checked and unchecked
// exceptions (without much documentation to go on), so have to use catch-all
return false;
} finally { // reset back
cipherAlgorithm = storedCipherAlgorithm;
keyFactoryAlgorithm = storedKeyAlgorithm;
initialized = storeInitState;
}
}
}