/******************************************************************************* * Copyright (c) 2008, 2016 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.IOException; import java.util.*; import java.util.Map.Entry; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import org.eclipse.core.runtime.IPath; 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.InternalExchangeUtils; import org.eclipse.equinox.security.storage.StorageException; import org.eclipse.osgi.util.NLS; public class SecurePreferences { /** * Pseudo-module ID to use when encryption is done with the default password. */ protected final static String DEFAULT_PASSWORD_ID = "org.eclipse.equinox.security.noModule"; //$NON-NLS-1$ private static final String PATH_SEPARATOR = String.valueOf(IPath.SEPARATOR); private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final String FALSE = "false"; //$NON-NLS-1$ private static final String TRUE = "true"; //$NON-NLS-1$ private boolean removed = false; /** * Parent node; null if this is a root node */ final protected SecurePreferences parent; /** * Name of this node */ final private String name; /** * Child nodes; created lazily; might be null */ protected Map children; /** * Values associated with this node; created lazily; might be null */ private Map values; /** * Cache root node to improve performance a bit */ private SecurePreferencesRoot root = null; public SecurePreferences(SecurePreferences parent, String name) { this.parent = parent; this.name = name; } ////////////////////////////////////////////////////////////////////////////////////////// // Navigation public SecurePreferences parent() { checkRemoved(); return parent; } public String name() { checkRemoved(); return name; } public String absolutePath() { checkRemoved(); if (parent == null) return PATH_SEPARATOR; String parentPath = parent.absolutePath(); if (PATH_SEPARATOR.equals(parentPath)) // parent is the root node? return parentPath + name; return parentPath + PATH_SEPARATOR + name; } public SecurePreferences node(String pathName) { checkRemoved(); validatePath(pathName); return navigateToNode(pathName, true); } public boolean nodeExists(String pathName) { checkRemoved(); validatePath(pathName); return (navigateToNode(pathName, false) != null); } public String[] keys() { checkRemoved(); if (values == null) return EMPTY_STRING_ARRAY; Set keys = values.keySet(); int size = keys.size(); String[] result = new String[size]; int pos = 0; for (Iterator i = keys.iterator(); i.hasNext();) { result[pos++] = (String) i.next(); } return result; } public String[] childrenNames() { checkRemoved(); if (children == null) return EMPTY_STRING_ARRAY; Set keys = children.keySet(); int size = keys.size(); String[] result = new String[size]; int pos = 0; for (Iterator i = keys.iterator(); i.hasNext();) { result[pos++] = (String) i.next(); } return result; } protected SecurePreferencesRoot getRoot() { if (root == null) { SecurePreferences result = this; while (result.parent() != null) result = result.parent(); root = (SecurePreferencesRoot) result; } return root; } protected SecurePreferences navigateToNode(String pathName, boolean create) { if (pathName == null || pathName.length() == 0) return this; int pos = pathName.indexOf(IPath.SEPARATOR); if (pos == -1) return getChild(pathName, create); else if (pos == 0) // if path requested is absolute, pass it to the root without "/" return getRoot().navigateToNode(pathName.substring(1), create); else { // if path requested contains segments, isolate top segment and rest String topSegment = pathName.substring(0, pos); String otherSegments = pathName.substring(pos + 1); SecurePreferences child = getChild(topSegment, create); if (child == null && !create) return null; return child.navigateToNode(otherSegments, create); } } synchronized private SecurePreferences getChild(String segment, boolean create) { if (children == null) { if (create) children = new HashMap(5); else return null; } SecurePreferences child = (SecurePreferences) children.get(segment); if (!create || (child != null)) return child; child = new SecurePreferences(this, segment); children.put(segment, child); return child; } ////////////////////////////////////////////////////////////////////////////////////////// // Load and save public void flush() throws IOException { getRoot().flush(); } public void flush(Properties properties, String parentsPath) { String thisNodePath; if (name == null) thisNodePath = null; else if (parentsPath == null) thisNodePath = PATH_SEPARATOR + name; else thisNodePath = parentsPath + PATH_SEPARATOR + name; if (values != null) { for (Iterator it = values.entrySet().iterator(); it.hasNext();) { Entry entry = (Entry) it.next(); String key = (String) entry.getKey(); PersistedPath extenalTag = new PersistedPath(thisNodePath, key); properties.setProperty(extenalTag.toString(), (String) entry.getValue()); } } if (children != null) { for (Iterator i = children.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry) i.next(); SecurePreferences child = (SecurePreferences) entry.getValue(); child.flush(properties, thisNodePath); } } } /////////////////////////////////////////////////////////////////////////////////////////////////////// // Basic put() and get() public void put(String key, String value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { if (key == null) throw new NullPointerException(); checkRemoved(); if (!encrypt || value == null) { CryptoData clearValue = new CryptoData(null, null, StorageUtils.getBytes(value)); internalPut(key, clearValue.toString()); // uses Base64 to encode byte sequences markModified(); return; } PasswordExt passwordExt = getRoot().getPassword(null, container, true); if (passwordExt == null) { boolean storeDecrypted = !CallbacksProvider.getDefault().runningUI() || InternalExchangeUtils.isJUnitApp(); if (storeDecrypted) { // for JUnits and headless runs we store value as clear text and log a error CryptoData clearValue = new CryptoData(null, null, StorageUtils.getBytes(value)); internalPut(key, clearValue.toString()); markModified(); // Make this as visible as possible. Both print out the output and log a error String msg = NLS.bind(SecAuthMessages.storedClearText, key, absolutePath()); System.out.println(msg); AuthPlugin.getDefault().logError(msg, new StorageException(StorageException.NO_PASSWORD, msg)); return; } throw new StorageException(StorageException.NO_PASSWORD, SecAuthMessages.loginNoPassword); } // value must not be null at this point CryptoData encryptedValue = getRoot().getCipher().encrypt(getRoot().getPassword(null, container, true), StorageUtils.getBytes(value)); internalPut(key, encryptedValue.toString()); markModified(); } public String get(String key, String def, SecurePreferencesContainer container) throws StorageException { checkRemoved(); if (!hasKey(key)) return def; String encryptedValue = internalGet(key); if (encryptedValue == null) return null; CryptoData data = new CryptoData(encryptedValue); String moduleID = data.getModuleID(); if (moduleID == null) { // clear-text value, not encrypted if (data.getData() == null) return null; return StorageUtils.getString(data.getData()); } PasswordExt passwordExt = getRoot().getPassword(moduleID, container, false); if (passwordExt == null) throw new StorageException(StorageException.NO_PASSWORD, SecAuthMessages.loginNoPassword); try { byte[] clearText = getRoot().getCipher().decrypt(passwordExt, data); return StorageUtils.getString(clearText); } catch (IllegalBlockSizeException e) { // invalid password? throw new StorageException(StorageException.DECRYPTION_ERROR, e); } catch (BadPaddingException e) { // invalid password? throw new StorageException(StorageException.DECRYPTION_ERROR, e); } } /** * For internal use - retrieve moduleID used to encrypt this value */ public String getModule(String key) { if (!hasKey(key)) return null; String encryptedValue = internalGet(key); if (encryptedValue == null) return null; try { CryptoData data = new CryptoData(encryptedValue); String moduleID = data.getModuleID(); if (DEFAULT_PASSWORD_ID.equals(moduleID)) return null; return moduleID; } catch (StorageException e) { return null; } } synchronized protected void internalPut(String key, String value) { if (values == null) values = new HashMap(5); values.put(key, value); } protected String internalGet(String key) { if (values == null) return null; return (String) values.get(key); } protected void markModified() { getRoot().setModified(true); } synchronized public void clear() { checkRemoved(); if (values != null) values.clear(); markModified(); } synchronized public void remove(String key) { checkRemoved(); if (values != null) { values.remove(key); markModified(); } } public void removeNode() { checkRemoved(); if (parent != null) parent.removeNode(name); markRemoved(); } public void markRemoved() { removed = true; if (children == null) return; for (Iterator i = children.entrySet().iterator(); i.hasNext();) { Map.Entry entry = (Map.Entry) i.next(); SecurePreferences child = (SecurePreferences) entry.getValue(); child.markRemoved(); } } synchronized protected void removeNode(String childName) { if (children == null) return; if (children.remove(childName) != null) markModified(); } private void checkRemoved() { if (removed) throw new IllegalStateException(NLS.bind(SecAuthMessages.removedNode, name)); } private void validatePath(String path) { if (isValid(path)) return; String msg = NLS.bind(SecAuthMessages.invalidNodePath, path); throw new IllegalArgumentException(msg); } /** * In additions to standard Preferences descriptions of paths, the following * conditions apply: * Path can contains ASCII characters between 32 and 126 (alphanumerics and printable * characters). * Path can not contain two or more consecutive forward slashes ('/'). * Path can not end with a trailing forward slash. */ private boolean isValid(String path) { if (path == null || path.length() == 0) return true; char[] chars = path.toCharArray(); boolean lastSlash = false; for (int i = 0; i < chars.length; i++) { if ((chars[i] <= 31) || (chars[i] >= 127)) return false; boolean isSlash = (chars[i] == IPath.SEPARATOR); if (lastSlash && isSlash) return false; lastSlash = isSlash; } return (chars.length > 1) ? (chars[chars.length - 1] != IPath.SEPARATOR) : true; } ///////////////////////////////////////////////////////////////////////////////// // Variations of get() / put() methods adapted to different data types public boolean getBoolean(String key, boolean defaultValue, SecurePreferencesContainer container) throws StorageException { if (!hasKey(key)) return defaultValue; String value = get(key, null, container); return value == null ? defaultValue : TRUE.equalsIgnoreCase(value); } public void putBoolean(String key, boolean value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { put(key, value ? TRUE : FALSE, encrypt, container); } public int getInt(String key, int defaultValue, SecurePreferencesContainer container) throws StorageException { if (!hasKey(key)) return defaultValue; String value = get(key, null, container); if (value == null) return defaultValue; try { return Integer.parseInt(value); } catch (NumberFormatException e) { return defaultValue; } } public void putInt(String key, int value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { put(key, Integer.toString(value), encrypt, container); } public long getLong(String key, long defaultValue, SecurePreferencesContainer container) throws StorageException { if (!hasKey(key)) return defaultValue; String value = get(key, null, container); if (value == null) return defaultValue; try { return Long.parseLong(value); } catch (NumberFormatException e) { return defaultValue; } } public void putLong(String key, long value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { put(key, Long.toString(value), encrypt, container); } public float getFloat(String key, float defaultValue, SecurePreferencesContainer container) throws StorageException { if (!hasKey(key)) return defaultValue; String value = get(key, null, container); if (value == null) return defaultValue; try { return Float.parseFloat(value); } catch (NumberFormatException e) { return defaultValue; } } public void putFloat(String key, float value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { put(key, Float.toString(value), encrypt, container); } public double getDouble(String key, double defaultValue, SecurePreferencesContainer container) throws StorageException { if (!hasKey(key)) return defaultValue; String value = get(key, null, container); if (value == null) return defaultValue; try { return Double.parseDouble(value); } catch (NumberFormatException e) { return defaultValue; } } public void putDouble(String key, double value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { put(key, Double.toString(value), encrypt, container); } public byte[] getByteArray(String key, byte[] defaultValue, SecurePreferencesContainer container) throws StorageException { if (!hasKey(key)) return defaultValue; String value = get(key, null, container); return Base64.decode(value); } public void putByteArray(String key, byte[] value, boolean encrypt, SecurePreferencesContainer container) throws StorageException { put(key, Base64.encode(value), encrypt, container); } protected boolean hasKey(String key) { checkRemoved(); return (values == null) ? false : values.containsKey(key); } public boolean isModified() { return getRoot().isModified(); } public boolean isEncrypted(String key) throws StorageException { checkRemoved(); if (!hasKey(key)) return false; String encryptedValue = internalGet(key); if (encryptedValue == null) return false; CryptoData data = new CryptoData(encryptedValue); String moduleID = data.getModuleID(); return (moduleID != null); } public boolean passwordChanging(SecurePreferencesContainer container, String moduleID) { return getRoot().onChangePassword(container, moduleID); } }