blob: aa777a85cd76ed95f73ab6bdb18763c17423207f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.equinox.internal.security.storage;
import java.io.*;
import java.net.URL;
import java.security.SecureRandom;
import java.util.*;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.spec.PBEKeySpec;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.core.runtime.jobs.Job;
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.equinox.security.storage.provider.*;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;
import org.eclipse.osgi.util.NLS;
/**
* Root secure preference node. In addition to usual things it stores location, modified
* status, encryption algorithm, and performs save and load.
*/
public class SecurePreferencesRoot extends SecurePreferences implements IStorageConstants {
private static final String VERSION_KEY = "org.eclipse.equinox.security.preferences.version"; //$NON-NLS-1$
private static final String VERSION_VALUE = "1"; //$NON-NLS-1$
/**
* Node path reserved for persisted preferences of the modules.
*/
static final public String PROVIDER_PATH = "org.eclipse.equinox.security.storage.impl"; //$NON-NLS-1$
/**
* Description of the property file - information only
*/
final private static String description = "Equinox secure storage version 1.0"; //$NON-NLS-1$
/**
* The node used by the secure preferences itself
*/
private final static String PROVIDER_NODE = "/org.eclipse.equinox.secure.storage"; //$NON-NLS-1$
/**
* Node used to store password verification tokens
*/
private final static String PASSWORD_VERIFICATION_NODE = PROVIDER_NODE + "/verification"; //$NON-NLS-1$
/**
* Text used to verify password
*/
private final static String PASSWORD_VERIFICATION_SAMPLE = "-> brown fox jumped over lazy dog <-"; //$NON-NLS-1$
/**
* Maximum unsuccessful decryption attempts per operation
*/
static protected final int MAX_ATTEMPTS = 20;
static private ILock lock = Job.getJobManager().newLock();
private URL location;
private long timestamp = 0;
private boolean modified = false;
private JavaEncryption cipher = new JavaEncryption();
private Map passwordCache = new HashMap(5); // cached passwords: module ID -> PasswordExt
public SecurePreferencesRoot(URL location) throws IOException {
super(null, null);
this.location = location;
load();
}
public URL getLocation() {
return location;
}
public JavaEncryption getCipher() {
return cipher;
}
public boolean isModified() {
return modified;
}
public void setModified(boolean modified) {
this.modified = modified;
}
public void load() throws IOException {
if (location == null)
return;
Properties properties = new Properties();
InputStream is = null;
try {
is = StorageUtils.getInputStream(location);
if (is != null) {
properties.load(is);
timestamp = getLastModified();
}
} catch (IllegalArgumentException e) {
String msg = NLS.bind(SecAuthMessages.badStorageURL, location.toString());
AuthPlugin.getDefault().logError(msg, e);
location = null; // don't attempt to use it
return;
} finally {
if (is != null)
is.close();
}
// In future new versions could be added
Object version = properties.get(VERSION_KEY);
if ((version != null) && !VERSION_VALUE.equals(version))
return;
properties.remove(VERSION_KEY);
// Process encryption algorithms
if (properties.containsKey(CIPHER_KEY) && properties.containsKey(KEY_FACTORY_KEY)) {
Object cipherAlgorithm = properties.get(CIPHER_KEY);
Object keyFactoryAlgorithm = properties.get(KEY_FACTORY_KEY);
if ((cipherAlgorithm instanceof String) && (keyFactoryAlgorithm instanceof String))
cipher.setAlgorithms((String) cipherAlgorithm, (String) keyFactoryAlgorithm);
properties.remove(CIPHER_KEY);
properties.remove(KEY_FACTORY_KEY);
}
for (Iterator i = properties.keySet().iterator(); i.hasNext();) {
Object externalKey = i.next();
Object value = properties.get(externalKey);
if (!(externalKey instanceof String))
continue;
if (!(value instanceof String))
continue;
PersistedPath storedPath = new PersistedPath((String) externalKey);
if (storedPath.getKey() == null)
continue;
SecurePreferences node = node(storedPath.getPath());
// don't use regular put() method as that would mark node as dirty
node.internalPut(storedPath.getKey(), (String) value);
}
}
synchronized public void flush() throws IOException {
if (location == null)
return;
if (!modified)
return;
// check if the file has been modified since the last time it was touched
if (timestamp != 0 && (timestamp != getLastModified())) {
IUICallbacks callback = CallbacksProvider.getDefault().getCallback();
if (callback != null) {
Boolean response = callback.ask(SecAuthMessages.fileModifiedMsg);
if (response == null)
AuthPlugin.getDefault().frameworkLogError(SecAuthMessages.fileModifiedNote, FrameworkLogEntry.WARNING, null);
else if (!response.booleanValue())
return; // by default go ahead with save
}
}
Properties properties = new Properties();
properties.put(VERSION_KEY, VERSION_VALUE);
// remember encyption algorithms
String cipherAlgorithm = cipher.getCipherAlgorithm();
if (cipherAlgorithm != null) {
properties.put(CIPHER_KEY, cipherAlgorithm);
properties.put(KEY_FACTORY_KEY, cipher.getKeyFactoryAlgorithm());
}
// save all user properties
flush(properties, null);
// output
OutputStream stream = null;
try {
stream = StorageUtils.getOutputStream(location);
properties.store(stream, description);
modified = false;
} finally {
if (stream != null)
stream.close();
}
timestamp = getLastModified();
}
/**
* Provides password for a new entry using:
* 1) default password, if any
* 2a) if options specify usage of specific module, that module is polled to produce password
* 2b) otherwise, password provider with highest priority is used to produce password
*/
public PasswordExt getPassword(String moduleID, IPreferencesContainer container, boolean encryption) throws StorageException {
if (encryption) { // provides password for a new entry
PasswordExt defaultPassword = getDefaultPassword(container);
if (defaultPassword != null)
return defaultPassword;
moduleID = getDefaultModuleID(container);
} else { // provides password for previously encrypted entry using its specified password provider module
if (moduleID == null)
throw new StorageException(StorageException.NO_SECURE_MODULE, SecAuthMessages.invalidEntryFormat);
if (DEFAULT_PASSWORD_ID.equals(moduleID)) { // was default password used?
PasswordExt defaultPassword = getDefaultPassword(container);
if (defaultPassword != null)
return defaultPassword;
throw new StorageException(StorageException.NO_SECURE_MODULE, SecAuthMessages.noDefaultPassword);
}
}
return getModulePassword(moduleID, container);
}
private PasswordExt getModulePassword(String moduleID, IPreferencesContainer container) throws StorageException {
if (DEFAULT_PASSWORD_ID.equals(moduleID)) // this should never happen but add this check just in case
throw new StorageException(StorageException.NO_PASSWORD, SecAuthMessages.loginNoPassword);
PasswordProviderModuleExt moduleExt = PasswordProviderSelector.getInstance().findStorageModule(moduleID);
String key = moduleExt.getID();
PasswordExt passwordExt = null;
boolean validPassword = false;
boolean setupPasswordRecovery = false;
boolean addedNoPrompt = false;
try {
lock.acquire(); // make sure process of password creation is not re-entered by another thread
// Quick check first: it is cached?
synchronized (passwordCache) {
if (passwordCache.containsKey(key))
return (PasswordExt) passwordCache.get(key);
}
// if this is (a headless run or JUnit) and prompt hint is not set up, set it to "false"
boolean supressPrompts = !CallbacksProvider.getDefault().runningUI() || InternalExchangeUtils.isJUnitApp();
if (supressPrompts && container != null && !container.hasOption(IProviderHints.PROMPT_USER)) {
((SecurePreferencesContainer) container).setOption(IProviderHints.PROMPT_USER, new Boolean(false));
addedNoPrompt = true;
}
// is there password verification string already?
SecurePreferences node = node(PASSWORD_VERIFICATION_NODE);
boolean newPassword = !node.hasKey(key);
int passwordType = newPassword ? PasswordProvider.CREATE_NEW_PASSWORD : 0;
for (int i = 0; i < MAX_ATTEMPTS; i++) {
PBEKeySpec password = moduleExt.getPassword(container, passwordType);
if (password == null)
return null;
passwordExt = new PasswordExt(password, key);
if (newPassword) {
String test = createTestString();
CryptoData encryptedValue = getCipher().encrypt(passwordExt, StorageUtils.getBytes(test));
node.internalPut(key, encryptedValue.toString());
markModified();
setupPasswordRecovery = true;
validPassword = true;
break;
}
// verify password using sample text
String encryptedData = node.internalGet(key);
CryptoData data = new CryptoData(encryptedData);
try {
byte[] decryptedData = getCipher().decrypt(passwordExt, data);
String test = StorageUtils.getString(decryptedData);
if (verifyTestString(test)) {
validPassword = true;
break;
}
} catch (IllegalBlockSizeException e) {
if (!moduleExt.changePassword(e, container))
break;
} catch (BadPaddingException e) {
if (!moduleExt.changePassword(e, container))
break;
}
}
if (validPassword) {
synchronized (passwordCache) {
passwordCache.put(key, passwordExt);
}
}
} finally {
if (addedNoPrompt) {
((SecurePreferencesContainer) container).removeOption(IProviderHints.PROMPT_USER);
addedNoPrompt = false;
}
lock.release();
}
if (!validPassword)
throw new StorageException(StorageException.NO_PASSWORD, SecAuthMessages.loginNoPassword);
if (setupPasswordRecovery)
CallbacksProvider.getDefault().setupChallengeResponse(key, container);
return passwordExt;
}
/**
* Retrieves default password from options, if any
*/
private PasswordExt getDefaultPassword(IPreferencesContainer container) {
if (container.hasOption(IProviderHints.DEFAULT_PASSWORD)) {
Object passwordHint = container.getOption(IProviderHints.DEFAULT_PASSWORD);
if (passwordHint instanceof PBEKeySpec)
return new PasswordExt((PBEKeySpec) passwordHint, DEFAULT_PASSWORD_ID);
}
return null;
}
/**
* Retrieves requested module ID from options, if any
*/
private String getDefaultModuleID(IPreferencesContainer container) {
if (container.hasOption(IProviderHints.REQUIRED_MODULE_ID)) {
Object idHint = container.getOption(IProviderHints.REQUIRED_MODULE_ID);
if (idHint instanceof String)
return (String) idHint;
}
return null;
}
public boolean onChangePassword(IPreferencesContainer container, String moduleID) {
// validation: must have a password module
PasswordProviderModuleExt moduleExt;
try {
moduleExt = PasswordProviderSelector.getInstance().findStorageModule(moduleID);
} catch (StorageException e) {
return false; // no module -> nothing to do
}
// obtain new password first
int passwordType = PasswordProvider.CREATE_NEW_PASSWORD | PasswordProvider.PASSWORD_CHANGE;
PBEKeySpec password = moduleExt.getPassword(container, passwordType);
if (password == null)
return false;
// create verification node
String key = moduleExt.getID();
PasswordExt passwordExt = new PasswordExt(password, key);
CryptoData encryptedValue;
try {
String test = createTestString();
encryptedValue = getCipher().encrypt(passwordExt, StorageUtils.getBytes(test));
} catch (StorageException e) {
String msg = NLS.bind(SecAuthMessages.encryptingError, key, PASSWORD_VERIFICATION_NODE);
AuthPlugin.getDefault().logError(msg, e);
return false;
}
SecurePreferences node = node(PASSWORD_VERIFICATION_NODE);
node.internalPut(key, encryptedValue.toString());
markModified();
try {
flush();
} catch (IOException e) {
String msg = NLS.bind(SecAuthMessages.encryptingError, key, PASSWORD_VERIFICATION_NODE);
AuthPlugin.getDefault().logError(msg, e);
return false;
}
// store password in the memory cache
cachePassword(key, passwordExt);
CallbacksProvider.getDefault().setupChallengeResponse(key, container);
return true;
}
public void cachePassword(String moduleID, PasswordExt passwordExt) {
synchronized (passwordCache) {
passwordCache.put(moduleID, passwordExt);
}
}
public void clearPasswordCache() {
synchronized (passwordCache) {
passwordCache.clear();
}
}
private long getLastModified() {
File file = new File(location.getPath());
return file.lastModified();
}
/**
* Generates random string to be stored for password verification. String format:
* <random1>\t<random2>\t<random2>\t<random1>
*/
private String createTestString() {
SecureRandom rand = new SecureRandom();
rand.setSeed(System.currentTimeMillis());
long num1 = rand.nextInt(10000);
long num2 = rand.nextInt(10000);
StringBuffer tmp = new StringBuffer();
tmp.append(num1);
tmp.append('\t');
tmp.append(num2);
tmp.append('\t');
tmp.append(num2);
tmp.append('\t');
tmp.append(num1);
return tmp.toString();
}
/**
* Checks if the string is the hard-coded original password verification sample
* or a string generated according to the rules in {@link #createTestString()}.
*/
private boolean verifyTestString(String test) {
if (test == null || test.length() == 0)
return false;
// backward compatibility: check if it is the original hard-coded string
if (PASSWORD_VERIFICATION_SAMPLE.equals(test))
return true;
String[] parts = test.split("\t"); //$NON-NLS-1$
if (parts == null || parts.length == 0)
return false;
if (parts.length != 4)
return false;
long num1 = -1;
long num2 = -1;
for (int i = 0; i < 4; i++) {
if (parts[i] == null || parts[i].length() == 0)
return false;
try {
switch (i) {
case 0 :
num1 = Long.decode(parts[i]).longValue();
break;
case 1 :
num2 = Long.decode(parts[i]).longValue();
break;
case 2 : {
long tmp = Long.decode(parts[i]).longValue();
if (tmp != num2)
return false;
break;
}
case 3 : {
long tmp = Long.decode(parts[i]).longValue();
if (tmp != num1)
return false;
break;
}
}
} catch (NumberFormatException e) {
return false;
}
}
return true;
}
}