package org.springframework.roo.addon.cloud.foundry; import static javax.crypto.Cipher.DECRYPT_MODE; import static javax.crypto.Cipher.ENCRYPT_MODE; import java.io.UnsupportedEncodingException; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.springframework.roo.classpath.preferences.Preferences; import org.springframework.roo.classpath.preferences.PreferencesService; /** * The user's cloud-related preferences. * * @author Andrew Swan * @since 1.2.0 */ public class CloudPreferences { private static final String CHARSET_NAME = "UTF-8"; private static final String CLOUD_FOUNDRY_KEY = "Cloud Foundry Prefs"; private static final String DELIMITER = "|"; private static final String DELIMITER_REGEX = "\\|"; // i.e. a pipe private static final String ROO_KEY = "Roo == Java + Productivity"; private final Preferences preferences; /** * Constructor * * @param preferencesService the service from which to load the preferences * (required) */ public CloudPreferences(final PreferencesService preferencesService) { preferences = preferencesService .getPreferencesFor(CloudFoundrySessionImpl.class); } /** * Clears any stored {@link CloudCredentials}. */ public void clearStoredLoginDetails() { preferences.putByteArray(CLOUD_FOUNDRY_KEY, new byte[0]); preferences.flush(); } /** * Encrypts or decrypts the given input, according to the given * <code>opmode</code> * * @param input the bytes to operate upon (required) * @param opmode the operation to perform, see the {@link Cipher} class for * suitable constants * @return a non-<code>null</code> array */ private byte[] crypt(final byte[] input, final int opmode) { final Cipher cipher = getCipher(opmode); try { return cipher.doFinal(input); } catch (final GeneralSecurityException e) { throw new IllegalStateException(e); } } /** * Decodes the given encoded (but decrypted) preferences string into a set * of {@link CloudCredentials}. * * @param encodedEntries the encoded string to convert (can be blank) * @return a non-<code>null</code> set */ private Set<CloudCredentials> decodeLoginPrefEntries( final String encodedEntries) { if (StringUtils.isBlank(encodedEntries)) { return Collections.emptySet(); } final Set<CloudCredentials> set = new HashSet<CloudCredentials>(); for (final String encodedEntry : encodedEntries.split(DELIMITER_REGEX)) { set.add(CloudCredentials.decode(encodedEntry)); } return set; } /** * Flushes these preferences to the persistent store * * @see Preferences#flush() */ public void flush() { preferences.flush(); } private Cipher getCipher(final int opmode) { try { final DESKeySpec keySpec = new DESKeySpec( ROO_KEY.getBytes(CHARSET_NAME)); final SecretKeyFactory keyFactory = SecretKeyFactory .getInstance("DES"); final SecretKey skey = keyFactory.generateSecret(keySpec); final Cipher cipher = Cipher.getInstance("DES"); cipher.init(opmode, skey); return cipher; } catch (final InvalidKeySpecException e) { throw new IllegalStateException(e); } catch (final NoSuchPaddingException e) { throw new IllegalStateException(e); } catch (final NoSuchAlgorithmException e) { throw new IllegalStateException(e); } catch (final InvalidKeyException e) { throw new IllegalStateException(e); } catch (final UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * Returns any stored {@link CloudCredentials} * * @return a non-<code>null</code> set */ private Set<CloudCredentials> getStoredCredentials() { final byte[] encodedPrefs = preferences.getByteArray(CLOUD_FOUNDRY_KEY); if (encodedPrefs.length == 0) { return Collections.emptySet(); } final byte[] decryptedPrefs = crypt(encodedPrefs, DECRYPT_MODE); try { return decodeLoginPrefEntries(new String(decryptedPrefs, CHARSET_NAME)); } catch (final UnsupportedEncodingException e) { throw new IllegalStateException(e); } } /** * Returns any stored credentials having the given URL. * * @param url the URL to match upon (can be <code>null</code>) * @return a non-<code>null</code> list, empty if the given URL is * <code>null</code> */ public List<CloudCredentials> getStoredCredentialsForUrl(final String url) { final List<CloudCredentials> matches = new ArrayList<CloudCredentials>(); if (url != null) { for (final CloudCredentials cloudCredentials : getStoredCredentials()) { if (url.equals(cloudCredentials.getUrl())) { matches.add(cloudCredentials); } } } return matches; } /** * Returns the email addresses of any stored credentials * * @return a non-<code>null</code> list with no duplicates */ public List<String> getStoredEmails() { final Set<String> storedEmails = new LinkedHashSet<String>(); for (final CloudCredentials storedCredentials : getStoredCredentials()) { storedEmails.add(storedCredentials.getEmail()); } return new ArrayList<String>(storedEmails); } /** * Returns the email addresses of any stored credentials with the given URL * * @param cloudControllerUrl the URL to match on (can be blank) * @return a non-<code>null</code> list with no duplicates; empty if the * given URL is blank */ public List<String> getStoredEmails(final String cloudControllerUrl) { final Set<String> storedEmails = new LinkedHashSet<String>(); if (StringUtils.isNotBlank(cloudControllerUrl)) { for (final CloudCredentials storedCredentials : getStoredCredentials()) { if (cloudControllerUrl.equals(storedCredentials.getUrl())) { storedEmails.add(storedCredentials.getEmail()); } } } return new ArrayList<String>(storedEmails); } /** * Returns the stored password for the given URL and email address * * @param cloudControllerUrl * @param email * @return <code>null</code> if there isn't one */ public String getStoredPassword(final String cloudControllerUrl, final String email) { for (final CloudCredentials storedCredential : getStoredCredentials()) { if (storedCredential.isSameAccount(cloudControllerUrl, email)) { return storedCredential.getPassword(); } } return null; } /** * Returns the URLs of any stored credentials * * @return a non-<code>null</code> list with no duplicates */ public List<String> getStoredUrls() { final Set<String> storedUrls = new LinkedHashSet<String>(); for (final CloudCredentials storedCredentials : getStoredCredentials()) { storedUrls.add(storedCredentials.getUrl()); } return new ArrayList<String>(storedUrls); } /** * Stores the given credentials along with any previously stored ones * * @param newCredentials the credentials to store (required, must be valid) */ public void storeCredentials(final CloudCredentials newCredentials) { Validate.isTrue(newCredentials.isValid(), "Cannot store invalid credentials"); // The credentials to write are the existing valid ones... final Collection<String> entries = new LinkedHashSet<String>(); for (final CloudCredentials storedCredentials : getStoredCredentials()) { if (storedCredentials.isValid()) { entries.add(storedCredentials.encode()); } } // ...plus the given ones entries.add(newCredentials.encode()); // Write them try { final byte[] encodedEntries = StringUtils.join(entries, DELIMITER) .getBytes(CHARSET_NAME); final byte[] encryptedEntries = crypt(encodedEntries, ENCRYPT_MODE); preferences.putByteArray(CLOUD_FOUNDRY_KEY, encryptedEntries); } catch (final UnsupportedEncodingException e) { throw new IllegalStateException(e); } } }