From 04cac3354cde9cbbf72cd91cef559a02d0143b20 Mon Sep 17 00:00:00 2001
From: Lyubomir Marinov
Date: Mon, 12 Jul 2010 14:59:01 +0000
Subject: [PATCH] Merges branches/gsoc10/passwdstrg@7435 which represents the
work of Dmitri Melnikov on the "Password storage" GSoC 2010 project into
trunk.
---
build.xml | 26 +-
lib/felix.client.run.properties | 1 +
lib/felix.unit.test.properties | 2 +
resources/languages/resources.properties | 33 ++
.../ConfigurationServiceImpl.java | 53 +-
.../impl/credentialsstorage/AESCrypto.java | 158 +++++
.../CredentialsStorageActivator.java | 114 ++++
.../CredentialsStorageServiceImpl.java | 522 +++++++++++++++++
.../impl/credentialsstorage/Crypto.java | 36 ++
.../credentialsstorage.manifest.mf | 14 +
.../SecurityConfigActivator.java | 154 ++++-
.../masterpassword/ConfigurationPanel.java | 32 +
.../MasterPasswordChangeDialog.java | 343 +++++++++++
.../masterpassword/MasterPasswordPanel.java | 245 ++++++++
.../masterpassword/PasswordQualityMeter.java | 160 +++++
.../masterpassword/SavedPasswordsDialog.java | 552 ++++++++++++++++++
.../masterpassword/SavedPasswordsPanel.java | 56 ++
.../securityconfig/securityconfig.manifest.mf | 17 +-
.../configuration/ConfigurationService.java | 26 +
.../CredentialsStorageService.java | 71 +++
.../credentialsstorage/CryptoException.java | 51 ++
.../protocol/ProtocolProviderFactory.java | 77 ++-
.../protocol/protocol.provider.manifest.mf | 1 +
.../CredentialsStorageServiceLick.java | 58 ++
.../TestCredentialsStorageService.java | 183 ++++++
.../credentialsstorage.slick.manifest.mf | 11 +
26 files changed, 2917 insertions(+), 79 deletions(-)
create mode 100644 src/net/java/sip/communicator/impl/credentialsstorage/AESCrypto.java
create mode 100644 src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageActivator.java
create mode 100644 src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageServiceImpl.java
create mode 100644 src/net/java/sip/communicator/impl/credentialsstorage/Crypto.java
create mode 100644 src/net/java/sip/communicator/impl/credentialsstorage/credentialsstorage.manifest.mf
create mode 100644 src/net/java/sip/communicator/plugin/securityconfig/masterpassword/ConfigurationPanel.java
create mode 100644 src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordChangeDialog.java
create mode 100644 src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordPanel.java
create mode 100644 src/net/java/sip/communicator/plugin/securityconfig/masterpassword/PasswordQualityMeter.java
create mode 100644 src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsDialog.java
create mode 100644 src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsPanel.java
create mode 100644 src/net/java/sip/communicator/service/credentialsstorage/CredentialsStorageService.java
create mode 100644 src/net/java/sip/communicator/service/credentialsstorage/CryptoException.java
create mode 100644 test/net/java/sip/communicator/slick/credentialsstorage/CredentialsStorageServiceLick.java
create mode 100644 test/net/java/sip/communicator/slick/credentialsstorage/TestCredentialsStorageService.java
create mode 100644 test/net/java/sip/communicator/slick/credentialsstorage/credentialsstorage.slick.manifest.mf
diff --git a/build.xml b/build.xml
index 202a0cf97..faa9c5d61 100644
--- a/build.xml
+++ b/build.xml
@@ -913,7 +913,8 @@
bundle-bouncycastle,bundle-plugin-otr,bundle-plugin-iptelaccregwizz,
bundle-contactsource,bundle-plugin-reconnect,
bundle-plugin-securityconfig,bundle-plugin-advancedconfig,
- bundle-plugin-sip2sipaccregwizz"/>
+ bundle-plugin-sip2sipaccregwizz,
+ bundle-credentialsstorage,bundle-credentialsstorage-slick"/>
@@ -1049,6 +1050,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ConfigurationService using
+ * an XML or a .properties file for storing properties. Currently only
+ * String properties are meaningfully saved (we should probably
+ * consider how and whether we should take care of the rest).
*
* @author Emil Ivov
* @author Damian Minkov
* @author Lubomir Marinov
+ * @author Dmitri Melnikov
*/
public class ConfigurationServiceImpl
implements ConfigurationService
{
- private final Logger logger = Logger.getLogger(ConfigurationServiceImpl.class);
+ /**
+ * The Logger used by this ConfigurationServiceImpl
+ * instance for logging output.
+ */
+ private final Logger logger
+ = Logger.getLogger(ConfigurationServiceImpl.class);
private static final String SYS_PROPS_FILE_NAME_PROPERTY
= "net.java.sip.communicator.SYS_PROPS_FILE_NAME";
@@ -365,6 +371,43 @@ public List getPropertyNamesByPrefix(String prefix, boolean exactPrefixM
return resultKeySet;
}
+ /**
+ * Returns a List of Strings containing the property names
+ * that have the specified suffix. A suffix is considered to be everything
+ * after the last dot in the property name.
+ *
+ * For example, imagine a configuration service instance containing two
+ * properties only:
+ *
+ *
+ * net.java.sip.communicator.PROP1=value1
+ * net.java.sip.communicator.service.protocol.PROP1=value2
+ *
+ *
+ * A call to this method with suffix equal to "PROP1" will return
+ * both properties, whereas the call with suffix equal to
+ * "communicator.PROP1" or "PROP2" will return an empty List. Thus,
+ * if the suffix argument contains a dot, nothing will be found.
+ *
+ *
+ * @param suffix the suffix for the property names to be returned
+ * @return a List of Strings containing the property names
+ * which contain the specified suffix
+ */
+ public List getPropertyNamesBySuffix(String suffix)
+ {
+ List resultKeySet = new LinkedList();
+
+ for (String key : store.getPropertyNames())
+ {
+ int ix = key.lastIndexOf('.');
+
+ if ((ix != -1) && suffix.equals(key.substring(ix+1)))
+ resultKeySet.add(key);
+ }
+ return resultKeySet;
+ }
+
/**
* Adds a PropertyChangeListener to the listener list.
*
diff --git a/src/net/java/sip/communicator/impl/credentialsstorage/AESCrypto.java b/src/net/java/sip/communicator/impl/credentialsstorage/AESCrypto.java
new file mode 100644
index 000000000..8fda279c2
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/credentialsstorage/AESCrypto.java
@@ -0,0 +1,158 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.credentialsstorage;
+
+import java.security.*;
+import java.security.spec.*;
+
+import javax.crypto.*;
+import javax.crypto.spec.*;
+
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.util.*;
+
+/**
+ * Performs encryption and decryption of text using AES algorithm.
+ *
+ * @author Dmitri Melnikov
+ */
+public class AESCrypto
+ implements Crypto
+{
+ /**
+ * The algorithm associated with the key.
+ */
+ private static final String KEY_ALGORITHM = "AES";
+
+ /**
+ * AES in ECB mode with padding.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5PADDING";
+
+ /**
+ * Salt used when creating the key.
+ */
+ private static byte[] SALT =
+ { 0x0C, 0x0A, 0x0F, 0x0E, 0x0B, 0x0E, 0x0E, 0x0F };
+
+ /**
+ * Length of the key in bits.
+ */
+ private static int KEY_LENGTH = 256;
+
+ /**
+ * Number of iterations to use when creating the key.
+ */
+ private static int ITERATION_COUNT = 1024;
+
+ /**
+ * Key derived from the master password to use for encryption/decryption.
+ */
+ private Key key;
+
+ /**
+ * Decryption object.
+ */
+ private Cipher decryptCipher;
+
+ /**
+ * Encryption object.
+ */
+ private Cipher encryptCipher;
+
+ /**
+ * Creates the encryption and decryption objects and the key.
+ *
+ * @param masterPassword used to derive the key. Can be null.
+ */
+ public AESCrypto(String masterPassword)
+ {
+ // if the password is empty, we get an exception constructing the key
+ if (masterPassword == null)
+ {
+ // here a default password can be set,
+ // cannot be an empty string
+ masterPassword = " ";
+ }
+
+ try
+ {
+ decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM);
+
+ // Password-Based Key Derivation Function found in PKCS5 v2.0.
+ // This is only available with java 6.
+ SecretKeyFactory factory =
+ SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ // Make a key from the master password
+ KeySpec spec =
+ new PBEKeySpec(masterPassword.toCharArray(), SALT,
+ ITERATION_COUNT, KEY_LENGTH);
+ SecretKey tmp = factory.generateSecret(spec);
+ // Make an algorithm specific key
+ key = new SecretKeySpec(tmp.getEncoded(), KEY_ALGORITHM);
+ }
+ catch (InvalidKeySpecException e)
+ {
+ throw new RuntimeException("Invalid key specification", e);
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new RuntimeException("Algorithm not found", e);
+ }
+ catch (NoSuchPaddingException e)
+ {
+ throw new RuntimeException("Padding not found", e);
+ }
+ }
+
+ /**
+ * Decrypts the cyphertext using the key.
+ *
+ * @param ciphertext base64 encoded encrypted data
+ * @return decrypted data
+ * @throws CryptoException when the ciphertext cannot be decrypted with the
+ * key or on decryption error.
+ */
+ public String decrypt(String ciphertext) throws CryptoException
+ {
+ try
+ {
+ decryptCipher.init(Cipher.DECRYPT_MODE, key);
+ return new String(decryptCipher.doFinal(Base64.decode(ciphertext)));
+ }
+ catch (BadPaddingException e)
+ {
+ throw new CryptoException(CryptoException.WRONG_KEY, e);
+ }
+ catch (Exception e)
+ {
+ throw new CryptoException(CryptoException.DECRYPTION_ERROR, e);
+ }
+ }
+
+ /**
+ * Encrypts the plaintext using the key.
+ *
+ * @param plaintext data to be encrypted
+ * @return base64 encoded encrypted data
+ * @throws CryptoException on encryption error
+ */
+ public String encrypt(String plaintext) throws CryptoException
+ {
+ try
+ {
+ encryptCipher.init(Cipher.ENCRYPT_MODE, key);
+ return new String(Base64.encode(encryptCipher.doFinal(plaintext
+ .getBytes("UTF-8"))));
+ }
+ catch (Exception e)
+ {
+ throw new CryptoException(CryptoException.ENCRYPTION_ERROR, e);
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageActivator.java b/src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageActivator.java
new file mode 100644
index 000000000..35de9bf67
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageActivator.java
@@ -0,0 +1,114 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.credentialsstorage;
+
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.service.resources.*;
+import net.java.sip.communicator.util.*;
+
+import org.osgi.framework.*;
+
+/**
+ * Activator for the @link{CredentialsStorageService}.
+ *
+ * @author Dmitri Melnikov
+ */
+public class CredentialsStorageActivator
+ implements BundleActivator
+{
+ /**
+ * The Logger used by the CredentialsStorageActivator
+ * class and its instances.
+ */
+ private static final Logger logger
+ = Logger.getLogger(CredentialsStorageActivator.class);
+
+ /**
+ * The {@link CredentialsStorageService} implementation.
+ */
+ private CredentialsStorageServiceImpl impl;
+
+ /**
+ * The {@link BundleContext}.
+ */
+ private static BundleContext bundleContext;
+
+ /**
+ * The resources service.
+ */
+ private static ResourceManagementService resourcesService;
+
+ /**
+ * Starts the credentials storage service
+ *
+ * @param bundleContext the BundleContext as provided from the OSGi
+ * framework
+ * @throws Exception if anything goes wrong
+ */
+ public void start(BundleContext bundleContext) throws Exception
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Service Impl: " + getClass().getName() + " [ STARTED ]");
+ }
+
+ CredentialsStorageActivator.bundleContext = bundleContext;
+
+ impl = new CredentialsStorageServiceImpl();
+ impl.start(bundleContext);
+
+ bundleContext.registerService(
+ CredentialsStorageService.class.getName(), impl, null);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Service Impl: " + getClass().getName() + " [REGISTERED]");
+ }
+ }
+
+ /**
+ * Unregisters the credentials storage service.
+ *
+ * @param bundleContext BundleContext
+ * @throws Exception if anything goes wrong
+ */
+ public void stop(BundleContext bundleContext) throws Exception
+ {
+ logger.logEntry();
+ impl.stop();
+ logger
+ .info("The CredentialsStorageService stop method has been called.");
+ }
+
+ /**
+ * Returns an internationalized string corresponding to the given key.
+ *
+ * @param key The key of the string.
+ * @return An internationalized string corresponding to the given key.
+ */
+ public static String getString(String key)
+ {
+ return getResources().getI18NString(key);
+ }
+
+ /**
+ * Returns an instance of {@link ResourceManagementService}.
+ *
+ * @return an instance of {@link ResourceManagementService}.
+ */
+ private static ResourceManagementService getResources()
+ {
+ if (resourcesService == null)
+ {
+ resourcesService
+ = ResourceManagementServiceUtils.getService(bundleContext);
+ }
+ return resourcesService;
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageServiceImpl.java b/src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageServiceImpl.java
new file mode 100644
index 000000000..47a2567fc
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/credentialsstorage/CredentialsStorageServiceImpl.java
@@ -0,0 +1,522 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.credentialsstorage;
+
+import java.util.*;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.service.configuration.*;
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.util.*;
+
+import org.osgi.framework.*;
+
+/**
+ * Implements {@link CredentialsStorageService} to load and store user
+ * credentials from/to the {@link ConfigurationService}.
+ *
+ * @author Dmitri Melnikov
+ */
+public class CredentialsStorageServiceImpl
+ implements CredentialsStorageService
+{
+ /**
+ * The logger for this class.
+ */
+ private final Logger logger =
+ Logger.getLogger(CredentialsStorageServiceImpl.class);
+
+ /**
+ * The name of a property which represents an encrypted password.
+ */
+ public static final String ACCOUNT_ENCRYPTED_PASSWORD =
+ "ENCRYPTED_PASSWORD";
+
+ /**
+ * The name of a property which represents an unencrypted password.
+ */
+ public static final String ACCOUNT_UNENCRYPTED_PASSWORD = "PASSWORD";
+
+ /**
+ * The property in the configuration that we use to verify master password
+ * existence and correctness.
+ */
+ private static final String MASTER_PROP =
+ "net.java.sip.communicator.impl.credentialsstorage.MASTER";
+
+ /**
+ * This value will be encrypted and saved in MASTER_PROP and
+ * will be used to verify the key's correctness.
+ */
+ private static final String MASTER_PROP_VALUE = "true";
+
+ /**
+ * The configuration service.
+ */
+ private ConfigurationService configurationService;
+
+ /**
+ * A {@link Crypto} instance that does the actual encryption and decryption.
+ */
+ private Crypto crypto;
+
+ /**
+ * Initializes the credentials service by fetching the configuration service
+ * reference from the bundle context.
+ *
+ * @param bc bundle context
+ */
+ void start(BundleContext bc)
+ {
+ ServiceReference confServiceReference =
+ bc.getServiceReference(ConfigurationService.class.getName());
+ this.configurationService =
+ (ConfigurationService) bc.getService(confServiceReference);
+ }
+
+ /**
+ * Forget the encryption/decryption key when stopping the service.
+ */
+ void stop()
+ {
+ crypto = null;
+ }
+
+ /**
+ * Stores the password for the specified account. When password is
+ * null the property is cleared.
+ *
+ * Many threads can call this method at the same time, and the
+ * first thread may present the user with the master password prompt and
+ * create a Crypto instance based on the input
+ * (createCrypto method). This instance will be used later by all
+ * other threads.
+ *
+ * @see CredentialsStorageServiceImpl#createCrypto()
+ */
+ public synchronized void storePassword(String accountPrefix, String password)
+ {
+ boolean createdOrExisting = createCrypto();
+ if (createdOrExisting)
+ {
+ String encryptedPassword = null;
+ try
+ {
+ if (password != null)
+ encryptedPassword = crypto.encrypt(password);
+ setEncrypted(accountPrefix, encryptedPassword);
+ }
+ catch (Exception e)
+ {
+ logger.warn("Encryption failed, password not saved", e);
+ }
+ }
+ }
+
+ /**
+ * Loads the password for the specified account.
+ * First check if the password is stored in the configuration unencrypted
+ * and if so, encrypt it and store in the new property. Otherwise, if the
+ * password is stored encrypted, decrypt it with the master password.
+ *
+ * Many threads can call this method at the same time, and the
+ * first thread may present the user with the master password prompt and
+ * create a Crypto instance based on the input
+ * (createCrypto method). This instance will be used later by all
+ * other threads.
+ *
+ * @see CredentialsStorageServiceImpl#createCrypto()
+ */
+ public synchronized String loadPassword(String accountPrefix)
+ {
+ String password = null;
+ if (isStoredUnencrypted(accountPrefix))
+ {
+ password = new String(Base64.decode(getUnencrypted(accountPrefix)));
+ movePasswordProperty(accountPrefix, password);
+ }
+
+ if (password == null && isStoredEncrypted(accountPrefix))
+ {
+ boolean createdOrExisting = createCrypto();
+ if (createdOrExisting)
+ {
+ try
+ {
+ String ciphertext = getEncrypted(accountPrefix);
+ password = crypto.decrypt(ciphertext);
+ }
+ catch (Exception e)
+ {
+ logger.warn("Decryption with master password failed", e);
+ // password stays null
+ }
+ }
+ }
+ return password;
+ }
+
+ /**
+ * Removes the password for the account that starts with the given prefix by
+ * setting its value in the configuration to null.
+ */
+ public void removePassword(String accountPrefix)
+ {
+ setEncrypted(accountPrefix, null);
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Password for '" + accountPrefix + "' removed");
+ }
+ }
+
+ /**
+ * Checks if master password is used to encrypt saved account passwords.
+ *
+ * @return true if used, false if not
+ */
+ public boolean isUsingMasterPassword()
+ {
+ return null != configurationService.getString(MASTER_PROP);
+ }
+
+ /**
+ * Verifies the correctness of the master password.
+ * Since we do not store the MP itself, if MASTER_PROP_VALUE
+ * is equal to the decrypted MASTER_PROP value, then
+ * the MP is considered correct.
+ *
+ * @param master master password
+ * @return true if the password is correct, false otherwise
+ */
+ public boolean verifyMasterPassword(String master)
+ {
+ Crypto localCrypto = new AESCrypto(master);
+ try
+ {
+ // use this value to verify master password correctness
+ String encryptedValue = getEncryptedMasterPropValue();
+ return MASTER_PROP_VALUE
+ .equals(localCrypto.decrypt(encryptedValue));
+ }
+ catch (CryptoException e)
+ {
+ if (e.getErrorCode() == CryptoException.WRONG_KEY)
+ {
+ logger.debug("Incorrect master pass", e);
+ return false;
+ }
+ else
+ {
+ // this should not happen, so just in case it does..
+ throw new RuntimeException("Decryption failed", e);
+ }
+ }
+ }
+
+ /**
+ * Changes the master password from the old to the new one.
+ * Decrypts all encrypted password properties from the configuration
+ * with the oldPassword and encrypts them again with newPassword.
+ *
+ * @param oldPassword old master password
+ * @param newPassword new master password
+ */
+ public boolean changeMasterPassword(String oldPassword, String newPassword)
+ {
+ // get all encrypted account password properties
+ List encryptedAccountProps =
+ configurationService
+ .getPropertyNamesBySuffix(ACCOUNT_ENCRYPTED_PASSWORD);
+
+ // this map stores propName -> password
+ Map passwords = new HashMap();
+ try
+ {
+ // read from the config and decrypt with the old MP..
+ setMasterPassword(oldPassword);
+ for (String propName : encryptedAccountProps)
+ {
+ String propValue = configurationService.getString(propName);
+ if (propValue != null)
+ {
+ String decrypted = crypto.decrypt(propValue);
+ passwords.put(propName, decrypted);
+ }
+ }
+ // ..and encrypt again with the new, write to the config
+ setMasterPassword(newPassword);
+ for (Map.Entry entry : passwords.entrySet())
+ {
+ String encrypted = crypto.encrypt(entry.getValue());
+ configurationService.setProperty(entry.getKey(), encrypted);
+ }
+ // save the verification value, encrypted with the new MP,
+ // or remove it if the newPassword is null (we are unsetting MP)
+ writeVerificationValue(newPassword == null);
+ }
+ catch (CryptoException ce)
+ {
+ logger.debug(ce);
+ crypto = null;
+ passwords = null;
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Sets the master password to the argument value.
+ *
+ * @param master master password
+ */
+ private void setMasterPassword(String master)
+ {
+ crypto = new AESCrypto(master);
+ }
+
+ /**
+ * Asks for master password if needed, encrypts the password, saves it to
+ * the new property and removes the old property.
+ *
+ * @param accountPrefix prefix of the account
+ * @param password unencrypted password
+ */
+ private void movePasswordProperty(String accountPrefix, String password)
+ {
+ boolean createdOrExisting = createCrypto();
+ if (createdOrExisting)
+ {
+ try
+ {
+ String encryptedPassword = crypto.encrypt(password);
+ setEncrypted(accountPrefix, encryptedPassword);
+ setUnencrypted(accountPrefix, null);
+ }
+ catch (CryptoException e)
+ {
+ logger.debug("Encryption failed", e);
+ // properties are not moved
+ }
+ }
+ }
+
+ /**
+ * Writes the verification value to the configuration for later use or
+ * removes it completely depending on the remove flag argument.
+ *
+ * @param remove to remove the verification value or just overwrite it.
+ */
+ private void writeVerificationValue(boolean remove)
+ {
+ if (remove)
+ {
+ configurationService.removeProperty(MASTER_PROP);
+ }
+ else
+ {
+ try
+ {
+ String encryptedValue = crypto.encrypt(MASTER_PROP_VALUE);
+ configurationService.setProperty(MASTER_PROP, encryptedValue);
+ }
+ catch (CryptoException e)
+ {
+ logger.warn("Failed to encrypt and write verification value");
+ }
+ }
+ }
+
+ /**
+ * Creates a Crypto instance only when it's null, either with a user input
+ * master password or with null. If the user decided not to input anything,
+ * the instance is not created.
+ *
+ * @return true if Crypto instance was created, false otherwise
+ */
+ private boolean createCrypto()
+ {
+ if (crypto == null)
+ {
+ logger.debug("Crypto instance is null, creating.");
+ if (isUsingMasterPassword())
+ {
+ String master = showPasswordPrompt();
+ if (master == null)
+ {
+ // user clicked cancel button in the prompt
+ crypto = null;
+ return false;
+ }
+ // at this point the master password must be correct,
+ // so we set the crypto instance to use it
+ setMasterPassword(master);
+ }
+ else
+ {
+ logger.debug("Master password not set");
+
+ // setting the master password to null means
+ // we shall still be using encryption/decryption
+ // but using some default value, not something specified
+ // by the user
+ setMasterPassword(null);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Displays a password prompt to the user in a loop until it is correct or
+ * the user presses the cancel button.
+ *
+ * @return the entered password or null if none was provided.
+ */
+ private String showPasswordPrompt()
+ {
+ String master = null;
+ JPasswordField passwordField = new JPasswordField();
+ String inputMsg =
+ CredentialsStorageActivator
+ .getString("plugin.securityconfig.masterpassword.MP_INPUT");
+ String errorMsg =
+ ""
+ + CredentialsStorageActivator
+ .getString("plugin.securityconfig.masterpassword.MP_VERIFICATION_FAILURE_MSG")
+ + "";
+
+ // Ask for master password until the input is correct or
+ // cancel button is pressed
+ boolean correct = true;
+ do
+ {
+ Object[] msg = null;
+ if (correct)
+ {
+ msg = new Object[]
+ { inputMsg, passwordField };
+ }
+ else
+ {
+ msg = new Object[]
+ { errorMsg, inputMsg, passwordField };
+ }
+ // clear the password field
+ passwordField.setText("");
+
+ if (JOptionPane
+ .showOptionDialog(
+ null,
+ msg,
+ CredentialsStorageActivator
+ .getString("plugin.securityconfig.masterpassword.MP_TITLE"),
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.QUESTION_MESSAGE,
+ null,
+ new String[]
+ {
+ CredentialsStorageActivator.getString("service.gui.OK"),
+ CredentialsStorageActivator
+ .getString("service.gui.CANCEL") },
+ CredentialsStorageActivator.getString("service.gui.OK")) == JOptionPane.YES_OPTION)
+ {
+
+ master = new String(passwordField.getPassword());
+ correct = !master.isEmpty() && verifyMasterPassword(master);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ while (!correct);
+ return master;
+ }
+
+ /**
+ * Retrieves the property for the master password from the configuration
+ * service.
+ *
+ * @return the property for the master password
+ */
+ private String getEncryptedMasterPropValue()
+ {
+ return configurationService.getString(MASTER_PROP);
+ }
+
+ /**
+ * Retrieves the encrypted account password using configuration service.
+ *
+ * @param accountPrefix account prefix
+ * @return the encrypted account password.
+ */
+ private String getEncrypted(String accountPrefix)
+ {
+ return configurationService.getString(accountPrefix + "."
+ + ACCOUNT_ENCRYPTED_PASSWORD);
+ }
+
+ /**
+ * Saves the encrypted account password using configuration service.
+ *
+ * @param accountPrefix account prefix
+ * @param value the encrypted account password.
+ */
+ private void setEncrypted(String accountPrefix, String value)
+ {
+ configurationService.setProperty(accountPrefix + "."
+ + ACCOUNT_ENCRYPTED_PASSWORD, value);
+ }
+
+ /**
+ * Check if encrypted account password is saved in the configuration.
+ *
+ * @return true if saved, false if not
+ */
+ public boolean isStoredEncrypted(String accountPrefix)
+ {
+ return null != configurationService.getString(accountPrefix + "."
+ + ACCOUNT_ENCRYPTED_PASSWORD);
+ }
+
+ /**
+ * Retrieves the unencrypted account password using configuration service.
+ *
+ * @param accountPrefix account prefix
+ * @return the unencrypted account password
+ */
+ private String getUnencrypted(String accountPrefix)
+ {
+ return configurationService.getString(accountPrefix + "."
+ + ACCOUNT_UNENCRYPTED_PASSWORD);
+ }
+
+ /**
+ * Saves the unencrypted account password using configuration service.
+ *
+ * @param accountPrefix account prefix
+ * @param value the unencrypted account password
+ */
+ private void setUnencrypted(String accountPrefix, String value)
+ {
+ configurationService.setProperty(accountPrefix + "."
+ + ACCOUNT_UNENCRYPTED_PASSWORD, value);
+ }
+
+ /**
+ * Check if unencrypted account password is saved in the configuration.
+ *
+ * @param accountPrefix account prefix
+ * @return true if saved, false if not
+ */
+ private boolean isStoredUnencrypted(String accountPrefix)
+ {
+ configurationService.getPropertyNamesByPrefix("", false);
+ return null != configurationService.getString(accountPrefix + "."
+ + ACCOUNT_UNENCRYPTED_PASSWORD);
+ }
+}
diff --git a/src/net/java/sip/communicator/impl/credentialsstorage/Crypto.java b/src/net/java/sip/communicator/impl/credentialsstorage/Crypto.java
new file mode 100644
index 000000000..711616538
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/credentialsstorage/Crypto.java
@@ -0,0 +1,36 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.credentialsstorage;
+
+import net.java.sip.communicator.service.credentialsstorage.*;
+
+/**
+ * Allows to encrypt and decrypt text using a symmetric algorithm.
+ *
+ * @author Dmitri Melnikov
+ */
+public interface Crypto
+{
+ /**
+ * Decrypts the cipher text and returns the result.
+ *
+ * @param ciphertext base64 encoded encrypted data
+ * @return decrypted data
+ * @throws CryptoException when the ciphertext cannot be decrypted with the
+ * key or on decryption error.
+ */
+ public String decrypt(String ciphertext) throws CryptoException;
+
+ /**
+ * Encrypts the plain text and returns the result.
+ *
+ * @param plaintext data to be encrypted
+ * @return base64 encoded encrypted data
+ * @throws CryptoException on encryption error
+ */
+ public String encrypt(String plaintext) throws CryptoException;
+}
diff --git a/src/net/java/sip/communicator/impl/credentialsstorage/credentialsstorage.manifest.mf b/src/net/java/sip/communicator/impl/credentialsstorage/credentialsstorage.manifest.mf
new file mode 100644
index 000000000..d6a7a526b
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/credentialsstorage/credentialsstorage.manifest.mf
@@ -0,0 +1,14 @@
+Bundle-Activator: net.java.sip.communicator.impl.credentialsstorage.CredentialsStorageActivator
+Bundle-Name: Credentials Storage Service Implementation
+Bundle-Description: A bundle that handles credentials
+Bundle-Vendor: sip-communicator.org
+Bundle-Version: 0.0.1
+System-Bundle: yes
+Import-Package: org.osgi.framework,
+ net.java.sip.communicator.service.configuration,
+ net.java.sip.communicator.service.resources,
+ net.java.sip.communicator.util,
+ javax.crypto,
+ javax.crypto.spec,
+ javax.swing
+Export-Package: net.java.sip.communicator.service.credentialsstorage
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/SecurityConfigActivator.java b/src/net/java/sip/communicator/plugin/securityconfig/SecurityConfigActivator.java
index e31497b12..c59badb38 100644
--- a/src/net/java/sip/communicator/plugin/securityconfig/SecurityConfigActivator.java
+++ b/src/net/java/sip/communicator/plugin/securityconfig/SecurityConfigActivator.java
@@ -7,8 +7,8 @@
import java.util.*;
-import net.java.sip.communicator.plugin.otr.*;
import net.java.sip.communicator.service.configuration.*;
+import net.java.sip.communicator.service.credentialsstorage.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.resources.*;
@@ -17,8 +17,8 @@
import org.osgi.framework.*;
/**
- *
* @author Yana Stamcheva
+ * @author Dmitri Melnikov
*/
public class SecurityConfigActivator
implements BundleActivator
@@ -35,19 +35,31 @@ public class SecurityConfigActivator
public static BundleContext bundleContext;
/**
- * The {@link ResourceManagementService} of the {@link OtrActivator}. Can
- * also be obtained from the {@link OtrActivator#bundleContext} on demand,
- * but we add it here for convinience.
+ * The {@link ResourceManagementService} of the
+ * {@link SecurityConfigActivator}. Can also be obtained from the
+ * {@link SecurityConfigActivator#bundleContext} on demand, but we add it
+ * here for convenience.
*/
- private static ResourceManagementService resourceService;
+ private static ResourceManagementService resources;
/**
* The ConfigurationService registered in {@link #bundleContext}
- * and used by the NeomediaActivator instance to read and write
- * configuration properties.
+ * and used by the SecurityConfigActivator instance to read and
+ * write configuration properties.
*/
private static ConfigurationService configurationService;
+ /**
+ * The CredentialsStorageService registered in
+ * {@link #bundleContext}.
+ */
+ private static CredentialsStorageService credentialsStorageService;
+
+ /**
+ * The UIService registered in {@link #bundleContext}.
+ */
+ private static UIService uiService;
+
/**
* Starts this plugin.
* @param bc the BundleContext
@@ -59,15 +71,33 @@ public void start(BundleContext bc) throws Exception
bundleContext = bc;
// Register the configuration form.
- Dictionary properties = new Hashtable();
+ Dictionary properties;
+
+ properties = new Hashtable();
properties.put( ConfigurationForm.FORM_TYPE,
ConfigurationForm.GENERAL_TYPE);
- bundleContext.registerService(ConfigurationForm.class.getName(),
+ bundleContext.registerService(
+ ConfigurationForm.class.getName(),
new LazyConfigurationForm(
"net.java.sip.communicator.plugin.securityconfig.SecurityConfigurationPanel",
getClass().getClassLoader(),
"plugin.securityconfig.ICON",
- "plugin.securityconfig.TITLE", 20), properties);
+ "plugin.securityconfig.TITLE",
+ 20),
+ properties);
+
+ properties = new Hashtable();
+ properties.put( ConfigurationForm.FORM_TYPE,
+ ConfigurationForm.SECURITY_TYPE);
+ bundleContext.registerService(
+ ConfigurationForm.class.getName(),
+ new LazyConfigurationForm(
+ "net.java.sip.communicator.plugin.securityconfig.masterpassword.ConfigurationPanel",
+ getClass().getClassLoader(),
+ null /* iconID */,
+ "plugin.securityconfig.masterpassword.TITLE",
+ 20),
+ properties);
}
/**
@@ -87,19 +117,12 @@ public void stop(BundleContext bc) throws Exception {}
*/
public static ResourceManagementService getResources()
{
- if (resourceService == null)
+ if (resources == null)
{
- ServiceReference resReference
- = bundleContext
- .getServiceReference(
- ResourceManagementService.class.getName());
-
- if (resReference != null)
- resourceService
- = (ResourceManagementService)
- bundleContext.getService(resReference);
+ resources
+ = ResourceManagementServiceUtils.getService(bundleContext);
}
- return resourceService;
+ return resources;
}
/**
@@ -125,6 +148,46 @@ public static ConfigurationService getConfigurationService()
return configurationService;
}
+ /**
+ * Returns the CredentialsStorageService obtained from the bundle
+ * context.
+ * @return the CredentialsStorageService obtained from the bundle
+ * context
+ */
+ public static CredentialsStorageService getCredentialsStorageService()
+ {
+ if (credentialsStorageService == null)
+ {
+ ServiceReference credentialsReference
+ = bundleContext.getServiceReference(
+ CredentialsStorageService.class.getName());
+
+ credentialsStorageService
+ = (CredentialsStorageService)
+ bundleContext.getService(credentialsReference);
+ }
+ return credentialsStorageService;
+ }
+
+ /**
+ * Gets the UIService instance registered in the
+ * BundleContext of the SecurityConfigActivator.
+ *
+ * @return the UIService instance registered in the
+ * BundleContext of the SecurityConfigActivator
+ */
+ public static UIService getUIService()
+ {
+ if (uiService == null)
+ {
+ ServiceReference serviceReference
+ = bundleContext.getServiceReference(UIService.class.getName());
+
+ uiService = (UIService) bundleContext.getService(serviceReference);
+ }
+ return uiService;
+ }
+
/**
* Gets all the available accounts in SIP Communicator.
*
@@ -189,4 +252,51 @@ public static List getAllAccountIDs()
}
return providerFactoriesMap;
}
+
+ /**
+ * Finds all accounts with saved encrypted passwords.
+ *
+ * @return a {@link List} of {@link AccountID} with the saved encrypted password.
+ */
+ public static Map getAccountIDsWithSavedPasswords()
+ {
+ Map, ProtocolProviderFactory> providerFactoriesMap
+ = getProtocolProviderFactories();
+
+ if (providerFactoriesMap == null)
+ return null;
+
+ CredentialsStorageService credentialsStorageService
+ = getCredentialsStorageService();
+ Map accountIDs = new HashMap();
+
+ for (ProtocolProviderFactory providerFactory
+ : providerFactoriesMap.values())
+ {
+ String sourcePackageName
+ = getFactoryImplPackageName(providerFactory);
+ for (AccountID accountID : providerFactory.getRegisteredAccounts())
+ {
+ String accountPrefix
+ = ProtocolProviderFactory.findAccountPrefix(
+ bundleContext,
+ accountID,
+ sourcePackageName);
+ if (credentialsStorageService.isStoredEncrypted(accountPrefix))
+ accountIDs.put(accountID, accountPrefix);
+ }
+ }
+ return accountIDs;
+ }
+
+ /**
+ * @return a String containing the package name of the concrete factory
+ * class that extends the abstract factory.
+ */
+ private static String getFactoryImplPackageName(
+ ProtocolProviderFactory providerFactory)
+ {
+ String className = providerFactory.getClass().getName();
+ return className.substring(0, className.lastIndexOf('.'));
+ }
}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/ConfigurationPanel.java b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/ConfigurationPanel.java
new file mode 100644
index 000000000..5d38d7531
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/ConfigurationPanel.java
@@ -0,0 +1,32 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.securityconfig.masterpassword;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.util.swing.*;
+
+/**
+ * Implements a Swing Component to represent the user interface of the
+ * Passwords ConfigurationForm.
+ *
+ * @author Dmitri Melnikov
+ * @author Lubomir Marinov
+ */
+public class ConfigurationPanel
+ extends TransparentPanel
+{
+ /**
+ * Initializes a new ConfigurationPanel instance.
+ */
+ public ConfigurationPanel()
+ {
+ add(new MasterPasswordPanel());
+ add(Box.createVerticalStrut(10));
+ add(new SavedPasswordsPanel());
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordChangeDialog.java b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordChangeDialog.java
new file mode 100644
index 000000000..05c1a5ea0
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordChangeDialog.java
@@ -0,0 +1,343 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.securityconfig.masterpassword;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+import javax.swing.text.*;
+
+import net.java.sip.communicator.plugin.securityconfig.*;
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.resources.*;
+import net.java.sip.communicator.util.swing.*;
+
+/**
+ * UI dialog to change the master password.
+ *
+ * @author Dmitri Melnikov
+ */
+public class MasterPasswordChangeDialog
+ extends SIPCommDialog
+ implements ActionListener,
+ KeyListener
+{
+ /**
+ * Callback interface. Implementing classes know how to change the master
+ * password from the old to the new one.
+ */
+ interface MasterPasswordExecutable
+ {
+ /**
+ * The actions to execute to change the master password.
+ *
+ * @param masterPassword old master password
+ * @param newMasterPassword new master password
+ * @return true on success, false on failure.
+ */
+ public boolean execute(String masterPassword, String newMasterPassword);
+ }
+
+ /**
+ * Dialog instance of this class.
+ */
+ private static MasterPasswordChangeDialog dialog;
+
+ /**
+ * Password quality meter.
+ */
+ private static PasswordQualityMeter passwordMeter =
+ new PasswordQualityMeter();
+
+ /**
+ * Callback to execute on password change.
+ */
+ private MasterPasswordExecutable callback;
+
+ /**
+ * UI components.
+ */
+ private JTextComponent currentPasswdField;
+ private JPasswordField newPasswordField;
+ private JPasswordField newAgainPasswordField;
+ private JButton okButton;
+ private JButton cancelButton;
+ private JTextArea infoTextArea;
+ private JProgressBar passwordQualityBar;
+ private JPanel textFieldsPanel;
+ private JPanel labelsPanel;
+ private JPanel buttonsPanel;
+ private JPanel qualityPanel;
+ private JPanel mainPanel;
+
+ /**
+ * The ResourceManagementService used by this instance to access
+ * the localized and internationalized resources of the application.
+ */
+ private final ResourceManagementService resources
+ = SecurityConfigActivator.getResources();
+
+ /**
+ * Builds the dialog.
+ */
+ private MasterPasswordChangeDialog()
+ {
+ super(false);
+ initComponents();
+
+ this.setTitle(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_TITLE"));
+ this.setMinimumSize(new Dimension(350, 300));
+ this.setPreferredSize(new Dimension(350, 300));
+ this.setResizable(false);
+
+ this.getContentPane().add(mainPanel);
+
+ this.pack();
+ Toolkit toolkit = Toolkit.getDefaultToolkit();
+ Dimension screenSize = toolkit.getScreenSize();
+
+ int x = (screenSize.width - this.getWidth()) / 2;
+ int y = (screenSize.height - this.getHeight()) / 2;
+
+ this.setLocation(x, y);
+ }
+
+ /**
+ * Initialises the UI components.
+ */
+ private void initComponents()
+ {
+ mainPanel = new TransparentPanel(new BorderLayout(10, 10));
+ mainPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
+
+ // info text
+ infoTextArea = new JTextArea();
+ infoTextArea.setEditable(false);
+ infoTextArea.setOpaque(false);
+ infoTextArea.setLineWrap(true);
+ infoTextArea.setWrapStyleWord(true);
+ infoTextArea.setFont(infoTextArea.getFont().deriveFont(Font.BOLD));
+ infoTextArea.setText(
+ resources.getI18NString("plugin.securityconfig.masterpassword.INFO_TEXT"));
+
+ // label fields
+ labelsPanel = new TransparentPanel(new GridLayout(0, 1, 8, 8));
+
+ labelsPanel.add(
+ new JLabel(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.CURRENT_PASSWORD")));
+ labelsPanel.add(
+ new JLabel(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.ENTER_PASSWORD")));
+ labelsPanel.add(
+ new JLabel(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.REENTER_PASSWORD")));
+
+ // password fields
+ if (!SecurityConfigActivator
+ .getCredentialsStorageService()
+ .isUsingMasterPassword())
+ {
+ currentPasswdField
+ = new JTextField(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_NOT_SET"));
+ currentPasswdField.setEnabled(false);
+ }
+ else
+ {
+ currentPasswdField = new JPasswordField(15);
+ }
+ newPasswordField = new JPasswordField(15);
+ newPasswordField.addKeyListener(this);
+ newAgainPasswordField = new JPasswordField(15);
+ newAgainPasswordField.addKeyListener(this);
+
+ textFieldsPanel = new TransparentPanel(new GridLayout(0, 1, 8, 8));
+ textFieldsPanel.add(currentPasswdField);
+ textFieldsPanel.add(newPasswordField);
+ textFieldsPanel.add(newAgainPasswordField);
+
+ // OK and cancel buttons
+ okButton = new JButton(resources.getI18NString("service.gui.OK"));
+ okButton.addActionListener(this);
+ okButton.setEnabled(false);
+ this.getRootPane().setDefaultButton(okButton);
+ cancelButton
+ = new JButton(resources.getI18NString("service.gui.CANCEL"));
+ cancelButton.addActionListener(this);
+
+ passwordQualityBar =
+ new JProgressBar(0, PasswordQualityMeter.TOTAL_POINTS);
+ passwordQualityBar.setValue(0);
+
+ qualityPanel = new TransparentPanel();
+ qualityPanel.setLayout(new BoxLayout(qualityPanel, BoxLayout.Y_AXIS));
+ qualityPanel.add(
+ new JLabel(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.PASSWORD_QUALITY_METER")));
+ qualityPanel.add(passwordQualityBar);
+ qualityPanel.add(Box.createVerticalStrut(15));
+
+ buttonsPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER));
+ buttonsPanel.add(okButton);
+ buttonsPanel.add(cancelButton);
+ qualityPanel.add(buttonsPanel);
+
+ mainPanel.add(infoTextArea, BorderLayout.NORTH);
+ mainPanel.add(labelsPanel, BorderLayout.WEST);
+ mainPanel.add(textFieldsPanel, BorderLayout.CENTER);
+ mainPanel.add(qualityPanel, BorderLayout.SOUTH);
+ }
+
+ /**
+ * OK and Cancel button event handler.
+ */
+ public void actionPerformed(ActionEvent e)
+ {
+ JButton sourceButton = (JButton) e.getSource();
+ boolean close = false;
+ if (sourceButton.equals(okButton)) // ok button
+ {
+ CredentialsStorageService credentialsStorageService
+ = SecurityConfigActivator.getCredentialsStorageService();
+ String oldMasterPassword = null;
+
+ if (credentialsStorageService.isUsingMasterPassword())
+ {
+ oldMasterPassword =
+ new String(((JPasswordField) currentPasswdField)
+ .getPassword());
+ if (oldMasterPassword.isEmpty())
+ {
+ displayPopupError(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_CURRENT_EMPTY"));
+ return;
+ }
+ boolean verified =
+ credentialsStorageService
+ .verifyMasterPassword(oldMasterPassword);
+ if (!verified)
+ {
+ displayPopupError(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_VERIFICATION_FAILURE_MSG"));
+ return;
+ }
+ }
+ // if the callback executes OK, we close the dialog
+ if (callback != null)
+ {
+ String newPassword = new String(newPasswordField.getPassword());
+ close = callback.execute(oldMasterPassword, newPassword);
+ }
+ }
+ else // cancel button
+ {
+ close = true;
+ }
+
+ if (close)
+ {
+ dialog = null;
+ dispose();
+ }
+ }
+
+ /**
+ * Displays an error pop-up.
+ *
+ * @param message the message to display
+ */
+ protected void displayPopupError(String message)
+ {
+ SecurityConfigActivator
+ .getUIService()
+ .getPopupDialog()
+ .showMessagePopupDialog(
+ message,
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_CHANGE_FAILURE"),
+ PopupDialog.ERROR_MESSAGE);
+ }
+
+ /**
+ * Displays an info pop-up.
+ *
+ * @param message the message to display.
+ */
+ protected void displayPopupInfo(String message)
+ {
+ SecurityConfigActivator
+ .getUIService()
+ .getPopupDialog()
+ .showMessagePopupDialog(
+ message,
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_CHANGE_SUCCESS"),
+ PopupDialog.INFORMATION_MESSAGE);
+ }
+
+ protected void close(boolean isEscaped)
+ {
+ cancelButton.doClick();
+ }
+
+ public void keyReleased(KeyEvent event)
+ {
+ JPasswordField source = (JPasswordField) event.getSource();
+ if (newPasswordField.equals(source)
+ || newAgainPasswordField.equals(source))
+ {
+ String password1 = new String(newPasswordField.getPassword());
+ String password2 = new String(newAgainPasswordField.getPassword());
+ // measure password quality
+ passwordQualityBar
+ .setValue(passwordMeter.assessPassword(password1));
+ // enable OK button if passwords are equal
+ boolean eq = !password1.isEmpty() && password1.equals(password2);
+ okButton.setEnabled(eq);
+ password1 = null;
+ password2 = null;
+ }
+ }
+
+ public void keyPressed(KeyEvent arg0)
+ {
+ }
+
+ public void keyTyped(KeyEvent arg0)
+ {
+ }
+
+ /**
+ * @return dialog instance
+ */
+ public static MasterPasswordChangeDialog getInstance()
+ {
+ if (dialog == null)
+ dialog = new MasterPasswordChangeDialog();
+ return dialog;
+ }
+
+ /**
+ * @param callbackInstance callback instance.
+ */
+ public void setCallback(MasterPasswordExecutable callbackInstance)
+ {
+ this.callback = callbackInstance;
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordPanel.java b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordPanel.java
new file mode 100644
index 000000000..c42f4b70f
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/MasterPasswordPanel.java
@@ -0,0 +1,245 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.securityconfig.masterpassword;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.plugin.securityconfig.*;
+import net.java.sip.communicator.plugin.securityconfig.masterpassword.MasterPasswordChangeDialog.MasterPasswordExecutable;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.resources.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.util.swing.*;
+
+/**
+ * Panel containing the master password checkbox and change button.
+ *
+ * @author Dmitri Melnikov
+ */
+public class MasterPasswordPanel
+ extends TransparentPanel
+ implements ActionListener
+{
+ /**
+ * The logger for this class.
+ */
+ private static final Logger logger
+ = Logger.getLogger(MasterPasswordPanel.class);
+
+ /**
+ * UI components.
+ */
+ private JCheckBox useMasterPasswordCheckBox;
+ private JButton changeMasterPasswordButton;
+
+ /**
+ * The ResourceManagementService used by this instance to access
+ * the localized and internationalized resources of the application.
+ */
+ private final ResourceManagementService resources
+ = SecurityConfigActivator.getResources();
+
+ /**
+ * Builds the panel.
+ */
+ public MasterPasswordPanel()
+ {
+ this.setLayout(new BorderLayout(10, 10));
+ this.setAlignmentX(0.0f);
+
+ initComponents();
+ }
+
+ /**
+ * Initialises the UI components.
+ */
+ private void initComponents()
+ {
+ useMasterPasswordCheckBox
+ = new SIPCommCheckBox(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.USE_MASTER_PASSWORD"));
+ useMasterPasswordCheckBox.addActionListener(this);
+ useMasterPasswordCheckBox.setSelected(
+ SecurityConfigActivator
+ .getCredentialsStorageService()
+ .isUsingMasterPassword());
+ this.add(useMasterPasswordCheckBox, BorderLayout.WEST);
+
+ changeMasterPasswordButton = new JButton();
+ changeMasterPasswordButton.setText(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.CHANGE_MASTER_PASSWORD"));
+ changeMasterPasswordButton.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ showMasterPasswordChangeDialog();
+ }
+ });
+ changeMasterPasswordButton.setEnabled(useMasterPasswordCheckBox
+ .isSelected());
+ this.add(changeMasterPasswordButton, BorderLayout.EAST);
+ }
+
+ public void actionPerformed(ActionEvent e)
+ {
+ boolean isSelected = useMasterPasswordCheckBox.isSelected();
+ // do not change the check box yet
+ useMasterPasswordCheckBox.setSelected(!isSelected);
+ if (isSelected)
+ showMasterPasswordChangeDialog();
+ else
+ {
+ // the checkbox is unselected only when this method finishes ok
+ removeMasterPassword();
+ }
+ }
+
+ /**
+ * Show the dialog to change master password.
+ */
+ private void showMasterPasswordChangeDialog()
+ {
+ MasterPasswordChangeDialog dialog = MasterPasswordChangeDialog.getInstance();
+ dialog.setCallback(new ChangeMasterPasswordCallback());
+ dialog.setVisible(true);
+ }
+
+ /**
+ * Displays a master password prompt to the user, verifies the entered
+ * password and then executes ChangeMasterPasswordCallback.execute
+ * method with null as the new password, thus removing it.
+ */
+ private void removeMasterPassword()
+ {
+ String master = null;
+ JPasswordField passwordField = new JPasswordField();
+ String inputMsg
+ = resources.getI18NString("plugin.securityconfig.masterpassword.MP_INPUT");
+ String errorMsg =
+ ""
+ + resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_VERIFICATION_FAILURE_MSG")
+ + "";
+
+ boolean correct = true;
+ do
+ {
+ Object[] msg = null;
+ if (correct)
+ {
+ msg = new Object[]
+ { inputMsg, passwordField };
+ }
+ else
+ {
+ msg = new Object[]
+ { errorMsg, inputMsg, passwordField };
+ }
+ //clear the password field
+ passwordField.setText("");
+
+ if (JOptionPane.showOptionDialog(
+ null,
+ msg,
+ resources.getI18NString("plugin.securityconfig.masterpassword.MP_TITLE"),
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.QUESTION_MESSAGE,
+ null,
+ new String[]
+ {
+ resources.getI18NString("service.gui.OK"),
+ resources.getI18NString("service.gui.CANCEL")
+ },
+ resources.getI18NString("service.gui.OK"))
+ == JOptionPane.YES_OPTION)
+ {
+ master = new String(passwordField.getPassword());
+ correct =
+ !master.isEmpty()
+ && SecurityConfigActivator
+ .getCredentialsStorageService()
+ .verifyMasterPassword(master);
+ }
+ else
+ return;
+ }
+ while (!correct);
+ // remove the master password by setting it to null
+ new ChangeMasterPasswordCallback().execute(master, null);
+ }
+
+ /**
+ * A callback implementation that changes or removes the master password.
+ * When the new password is null, the master password is removed.
+ */
+ class ChangeMasterPasswordCallback
+ implements MasterPasswordExecutable
+ {
+ public boolean execute(String masterPassword, String newMasterPassword)
+ {
+ boolean remove = newMasterPassword == null;
+ // update all passwords with new master pass
+ boolean changed
+ = SecurityConfigActivator
+ .getCredentialsStorageService()
+ .changeMasterPassword(
+ masterPassword,
+ newMasterPassword);
+ if (!changed)
+ {
+ String titleKey
+ = remove
+ ? "plugin.securityconfig.masterpassword.MP_REMOVE_FAILURE"
+ : "plugin.securityconfig.masterpassword.MP_CHANGE_FAILURE";
+ SecurityConfigActivator
+ .getUIService()
+ .getPopupDialog()
+ .showMessagePopupDialog(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_CHANGE_FAILURE_MSG"),
+ resources.getI18NString(titleKey),
+ PopupDialog.ERROR_MESSAGE);
+ return false;
+ }
+ else
+ {
+ String title = null;
+ String msg = null;
+ if (remove)
+ {
+ title = "plugin.securityconfig.masterpassword.MP_REMOVE_SUCCESS";
+ msg = "plugin.securityconfig.masterpassword.MP_REMOVE_SUCCESS_MSG";
+ //disable the checkbox and change button
+ useMasterPasswordCheckBox.setSelected(false);
+ changeMasterPasswordButton.setEnabled(false);
+ }
+ else
+ {
+ title = "plugin.securityconfig.masterpassword.MP_CHANGE_SUCCESS";
+ msg = "plugin.securityconfig.masterpassword.MP_CHANGE_SUCCESS_MSG";
+ // Enable the checkbox and change button.
+ useMasterPasswordCheckBox.setSelected(true);
+ changeMasterPasswordButton.setEnabled(true);
+ }
+ logger.debug("Master password successfully changed");
+ SecurityConfigActivator
+ .getUIService()
+ .getPopupDialog()
+ .showMessagePopupDialog(
+ resources.getI18NString(msg),
+ resources.getI18NString(title),
+ PopupDialog.INFORMATION_MESSAGE);
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/PasswordQualityMeter.java b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/PasswordQualityMeter.java
new file mode 100644
index 000000000..99adec3c9
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/PasswordQualityMeter.java
@@ -0,0 +1,160 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.securityconfig.masterpassword;
+
+import java.util.regex.*;
+
+/**
+ * Simple password quality meter. The JavaScript version found at
+ * http://www.geekwisdom.com/js/passwordmeter.js was used as a base. Provides a
+ * method to compute the relative score of the password that can be used to
+ * model a progress bar, for example.
+ *
+ * @author Dmitri Melnikov
+ */
+public class PasswordQualityMeter
+{
+ /**
+ * Maximum possible points.
+ */
+ public static final int TOTAL_POINTS = 42;
+
+ /**
+ * Assesses the strength of the password.
+ *
+ * @param pass the password to assess
+ * @return the score for this password between 0 and TOTAL_POINTS
+ */
+ public int assessPassword(String pass)
+ {
+ int score = 0;
+
+ if (pass == null || pass.isEmpty())
+ return score;
+ score += assessLength(pass);
+ score += assessLetters(pass);
+ score += assessNumbers(pass);
+ score += assessSpecials(pass);
+ return score;
+ }
+
+ /**
+ * Assesses password length:
+ * level 0 (3 point): less than 5 characters
+ * level 1 (6 points): between 5 and 7 characters
+ * level 2 (12 points): between 8 and 15 characters
+ * level 3 (18 points): 16 or more characters
+ *
+ * @param pass the password to assess
+ * @return the score based on the length
+ */
+ private int assessLength(String pass)
+ {
+ int len = pass.length();
+
+ if (len < 5)
+ return 3;
+ else if (len >= 5 && len < 8)
+ return 6;
+ else if (len >= 8 && len < 16)
+ return 12;
+ // len >= 16
+ return 18;
+ }
+
+ /**
+ * Assesses letter cases:
+ * level 0 (0 points): no letters
+ * level 1 (5 points): all letters are either lower or upper case
+ * level 2 (7 points): letters are mixed case
+ *
+ * @param pass the password to assess
+ * @return the score based on the letters
+ */
+ private int assessLetters(String pass)
+ {
+ boolean lower = matches(pass, "[a-z]+");
+ boolean upper = matches(pass, "[A-Z]+");
+
+ if (lower && upper)
+ return 7;
+ if (lower || upper)
+ return 5;
+ return 0;
+ }
+
+ /**
+ * Assesses number count:
+ * level 0 (0 points): no numbers exist
+ * level 1 (5 points): one or two number exists
+ * level 1 (7 points): 3 or more numbers exists
+ *
+ * @param pass the password to assess
+ * @return the score based on the numbers
+ */
+ private int assessNumbers(String pass)
+ {
+ int found = countMatches(pass, "\\d");
+
+ if (found < 1)
+ return 0;
+ else if (found >= 1 && found < 3)
+ return 5;
+ return 7;
+ }
+
+ /**
+ * Assesses special character count.
+ * Here special characters are non-word and non-space ones.
+ * level 0 (0 points): no special characters
+ * level 1 (5 points): one special character exists
+ * level 2 (10 points): more than one special character exists
+ *
+ * @param pass the password to assess
+ * @return the score based on special characters
+ */
+ private int assessSpecials(String pass)
+ {
+ int found = countMatches(pass, "[^\\w\\s]");
+
+ if (found < 1)
+ return 0;
+ else if (found <= 1 && found < 2)
+ return 5;
+ return 10;
+ }
+
+ /**
+ * Counts the number of matches of a given pattern in a given string.
+ *
+ * @param str the string to search in
+ * @param pattern the pattern to search for
+ * @return number of matches of patter in str
+ */
+ private int countMatches(String str, String pattern)
+ {
+ Pattern p = Pattern.compile(pattern);
+ Matcher matcher = p.matcher(str);
+ int found = 0;
+
+ while (matcher.find())
+ found++;
+ return found;
+ }
+
+ /**
+ * Wrapper around @link{Pattern} and @link{Matcher} classes.
+ *
+ * @param str the string to search in
+ * @param pattern the pattern to search for
+ * @return true if pattern has been found in str.
+ */
+ private boolean matches(String str, String pattern)
+ {
+ return Pattern.compile(pattern).matcher(str).find();
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsDialog.java b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsDialog.java
new file mode 100644
index 000000000..212fd46ed
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsDialog.java
@@ -0,0 +1,552 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.securityconfig.masterpassword;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.*;
+import java.util.List; // disambiguation
+
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.event.*;
+import javax.swing.table.*;
+
+import net.java.sip.communicator.plugin.securityconfig.*;
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.service.gui.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.resources.*;
+import net.java.sip.communicator.util.*;
+import net.java.sip.communicator.util.swing.*;
+
+/**
+ * The dialog that displays all saved account passwords.
+ *
+ * @author Dmitri Melnikov
+ */
+public class SavedPasswordsDialog
+ extends SIPCommDialog
+{
+ /**
+ * The logger for this class.
+ */
+ private static final Logger logger
+ = Logger.getLogger(SavedPasswordsDialog.class);
+
+ /**
+ * UI components.
+ */
+ private JPanel mainPanel;
+ private JButton closeButton;
+
+ /**
+ * The {@link CredentialsStorageService}.
+ */
+ private static final CredentialsStorageService credentialsStorageService
+ = SecurityConfigActivator.getCredentialsStorageService();
+
+ /**
+ * The ResourceManagementService used by this instance to access
+ * the localized and internationalized resources of the application.
+ */
+ private static final ResourceManagementService resources
+ = SecurityConfigActivator.getResources();
+
+ /**
+ * Instance of this dialog.
+ */
+ private static SavedPasswordsDialog dialog;
+
+ /**
+ * Builds the dialog.
+ */
+ private SavedPasswordsDialog()
+ {
+ super(false);
+ initComponents();
+
+ this.setTitle(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.SAVED_PASSWORDS"));
+ this.setMinimumSize(new Dimension(550, 300));
+ this.setPreferredSize(new Dimension(550, 300));
+ this.setResizable(false);
+
+ this.getContentPane().add(mainPanel);
+
+ this.pack();
+ Toolkit toolkit = Toolkit.getDefaultToolkit();
+ Dimension screenSize = toolkit.getScreenSize();
+
+ int x = (screenSize.width - this.getWidth()) / 2;
+ int y = (screenSize.height - this.getHeight()) / 2;
+
+ this.setLocation(x,y);
+ }
+
+ /**
+ * Initialises the UI components.
+ */
+ private void initComponents()
+ {
+ this.setLayout(new GridBagLayout());
+ mainPanel = new TransparentPanel(new BorderLayout(10, 10));
+
+ GridBagConstraints c = new GridBagConstraints();
+ c.gridy = 0;
+ c.fill = GridBagConstraints.BOTH;
+ c.weightx = 1.0;
+ c.weighty = 1.0;
+ c.insets = new Insets(5, 5, 5, 5);
+ c.anchor = GridBagConstraints.PAGE_START;
+
+ AccountPasswordsPanel accPassPanel = new AccountPasswordsPanel();
+ this.add(accPassPanel, c);
+
+ c.gridy = 1;
+ c.weighty = 0.0;
+ c.fill = GridBagConstraints.NONE;
+ c.anchor = GridBagConstraints.LAST_LINE_END;
+ closeButton
+ = new JButton(resources.getI18NString("service.gui.CLOSE"));
+ closeButton.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ dialog = null;
+ dispose();
+ }
+ });
+ this.add(closeButton, c);
+ }
+
+ protected void close(boolean isEscaped)
+ {
+ closeButton.doClick();
+ }
+
+ /**
+ * @return the {@link SavedPasswordsDialog} instance
+ */
+ public static SavedPasswordsDialog getInstance()
+ {
+ if (dialog == null) {
+ dialog = new SavedPasswordsDialog();
+ }
+ return dialog;
+ }
+
+ /**
+ * Panel containing the accounts table and buttons.
+ */
+ private static class AccountPasswordsPanel
+ extends TransparentPanel
+ {
+
+ /**
+ * The table model for the accounts table.
+ */
+ private class AccountsTableModel
+ extends AbstractTableModel
+ {
+ /**
+ * Index of the first column.
+ */
+ public static final int ACCOUNT_TYPE_INDEX = 0;
+ /**
+ * Index of the second column.
+ */
+ public static final int ACCOUNT_NAME_INDEX = 1;
+ /**
+ * Index of the third column.
+ */
+ public static final int PASSWORD_INDEX = 2;
+
+ /**
+ * List of accounts with saved passwords.
+ */
+ public final List savedPasswordAccounts =
+ new ArrayList();
+ /**
+ * Map that associates an {@link AccountID} with its prefix.
+ */
+ public final Map accountIdPrefixes =
+ new HashMap();
+
+ /**
+ * Loads accounts.
+ */
+ public AccountsTableModel()
+ {
+ accountIdPrefixes.putAll(
+ SecurityConfigActivator
+ .getAccountIDsWithSavedPasswords());
+ savedPasswordAccounts.addAll(new ArrayList(
+ accountIdPrefixes.keySet()));
+ }
+
+ /**
+ * Returns the name for the given column.
+ */
+ public String getColumnName(int column)
+ {
+ String key;
+
+ switch (column)
+ {
+ case ACCOUNT_TYPE_INDEX:
+ key = "plugin.securityconfig.masterpassword.COL_TYPE";
+ break;
+ case ACCOUNT_NAME_INDEX:
+ key = "plugin.securityconfig.masterpassword.COL_NAME";
+ break;
+ case PASSWORD_INDEX:
+ key = "plugin.securityconfig.masterpassword.COL_PASSWORD";
+ break;
+ default:
+ return null;
+ }
+ return resources.getI18NString(key);
+ }
+
+ /**
+ * Returns the value for the given row and column.
+ */
+ public Object getValueAt(int row, int column)
+ {
+ if (row < 0)
+ return null;
+
+ AccountID accountID = savedPasswordAccounts.get(row);
+ switch (column)
+ {
+ case ACCOUNT_TYPE_INDEX:
+ String protocol
+ = accountID
+ .getAccountPropertyString(
+ ProtocolProviderFactory.PROTOCOL);
+ return
+ (protocol == null)
+ ? resources
+ .getI18NString(
+ "plugin.securityconfig.masterpassword.PROTOCOL_UNKNOWN")
+ : protocol;
+ case ACCOUNT_NAME_INDEX:
+ return accountID.getUserID();
+ case PASSWORD_INDEX:
+ String pass =
+ credentialsStorageService
+ .loadPassword(accountIdPrefixes.get(accountID));
+ return
+ (pass == null)
+ ? resources
+ .getI18NString(
+ "plugin.securityconfig.masterpassword.CANNOT_DECRYPT")
+ : pass;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Number of rows in the table.
+ */
+ public int getRowCount()
+ {
+ return savedPasswordAccounts.size();
+ }
+
+ /**
+ * Number of columns depends on whether we are showing passwords or
+ * not.
+ */
+ public int getColumnCount()
+ {
+ return showPasswords ? 3 : 2;
+ }
+ }
+
+ /**
+ * Are we showing the passwords column or not.
+ */
+ private boolean showPasswords = false;
+
+ /**
+ * The button to remove the saved password for the selected account.
+ */
+ private JButton removeButton;
+
+ /**
+ * The button to remove saved passwords for all accounts.
+ */
+ private JButton removeAllButton;
+
+ /**
+ * The button to show the saved passwords for all accounts in plain text.
+ */
+ private JButton showPasswordsButton;
+
+ /**
+ * The table itself.
+ */
+ private JTable accountsTable;
+
+ /**
+ * Builds the panel.
+ */
+ public AccountPasswordsPanel()
+ {
+ this.initComponents();
+ }
+
+ /**
+ * Returns the {@link AccountID} object for the selected row.
+ * @return the selected account
+ */
+ private AccountID getSelectedAccountID()
+ {
+ AccountsTableModel model =
+ (AccountsTableModel) accountsTable.getModel();
+ int index = accountsTable.getSelectedRow();
+ if (index < 0 || index > model.savedPasswordAccounts.size())
+ return null;
+
+ return model.savedPasswordAccounts.get(index);
+ }
+
+ /**
+ * Initializes the table's components.
+ */
+ private void initComponents()
+ {
+ setBorder(
+ BorderFactory.createTitledBorder(
+ BorderFactory.createEtchedBorder(EtchedBorder.LOWERED),
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.STORED_ACCOUNT_PASSWORDS")));
+ this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
+
+ accountsTable = new JTable();
+ accountsTable.setModel(new AccountsTableModel());
+ accountsTable
+ .setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
+ accountsTable.setCellSelectionEnabled(false);
+ accountsTable.setColumnSelectionAllowed(false);
+ accountsTable.setRowSelectionAllowed(true);
+ accountsTable.getColumnModel().getColumn(
+ AccountsTableModel.ACCOUNT_NAME_INDEX).setPreferredWidth(270);
+ accountsTable.getSelectionModel().addListSelectionListener(
+ new ListSelectionListener()
+ {
+ public void valueChanged(ListSelectionEvent e)
+ {
+ if (e.getValueIsAdjusting())
+ return;
+ // activate remove button on select
+ removeButton.setEnabled(true);
+ }
+ });
+
+ JScrollPane pnlAccounts = new JScrollPane(accountsTable);
+ this.add(pnlAccounts);
+
+ JPanel pnlButtons = new TransparentPanel();
+ pnlButtons.setLayout(new BorderLayout());
+ this.add(pnlButtons);
+
+ JPanel leftButtons = new TransparentPanel();
+ pnlButtons.add(leftButtons, BorderLayout.WEST);
+
+ removeButton
+ = new JButton(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.REMOVE_PASSWORD_BUTTON"));
+ // enabled on row selection
+ removeButton.setEnabled(false);
+ removeButton.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent arg0)
+ {
+ AccountID selectedAccountID = getSelectedAccountID();
+ if (selectedAccountID != null)
+ {
+ AccountsTableModel model =
+ (AccountsTableModel) accountsTable.getModel();
+ String accountPrefix = model.accountIdPrefixes.get(selectedAccountID);
+
+ removeSavedPassword(accountPrefix, selectedAccountID);
+ model.savedPasswordAccounts.remove(selectedAccountID);
+ model.accountIdPrefixes.remove(accountPrefix);
+
+ int selectedRow = accountsTable.getSelectedRow();
+ model.fireTableRowsDeleted(selectedRow, selectedRow);
+ }
+ }
+ });
+ leftButtons.add(removeButton);
+
+ removeAllButton
+ = new JButton(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.REMOVE_ALL_PASSWORDS_BUTTON"));
+ removeAllButton.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent arg0)
+ {
+ AccountsTableModel model =
+ (AccountsTableModel) accountsTable.getModel();
+ if (model.savedPasswordAccounts.isEmpty())
+ {
+ return;
+ }
+
+ int answer
+ = SecurityConfigActivator
+ .getUIService()
+ .getPopupDialog()
+ .showConfirmPopupDialog(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.REMOVE_ALL_CONFIRMATION"),
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.REMOVE_ALL_TITLE"),
+ PopupDialog.YES_NO_OPTION);
+
+ if (answer == PopupDialog.YES_OPTION)
+ {
+ for (AccountID accountID : model.savedPasswordAccounts)
+ {
+ String accountPrefix =
+ model.accountIdPrefixes.get(accountID);
+ removeSavedPassword(accountPrefix, accountID);
+ }
+ model.savedPasswordAccounts.clear();
+ model.accountIdPrefixes.clear();
+ model.fireTableDataChanged();
+ }
+ }
+ });
+ leftButtons.add(removeAllButton);
+
+ JPanel rightButtons = new TransparentPanel();
+ pnlButtons.add(rightButtons, BorderLayout.EAST);
+ showPasswordsButton
+ = new JButton(
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.SHOW_PASSWORDS_BUTTON"));
+ showPasswordsButton.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent arg0)
+ {
+ // show the master password input only when it's set and the
+ // passwords column is hidden
+ if (credentialsStorageService.isUsingMasterPassword()
+ && !showPasswords)
+ {
+ showOrHidePasswordsProtected();
+ }
+ else
+ {
+ showOrHidePasswords();
+ }
+ }
+ });
+ rightButtons.add(showPasswordsButton);
+ }
+
+ /**
+ * Removes the password from the storage.
+ *
+ * @param accountPrefix account prefix
+ * @param accountID AccountID object
+ */
+ private void removeSavedPassword(String accountPrefix, AccountID accountID)
+ {
+ credentialsStorageService.removePassword(accountPrefix);
+
+ logger.debug(accountID + " removed");
+ }
+
+ /**
+ * Toggles the passwords column.
+ */
+ private void showOrHidePasswords()
+ {
+ showPasswords = !showPasswords;
+ showPasswordsButton.setText(
+ resources.getI18NString(
+ showPasswords
+ ? "plugin.securityconfig.masterpassword.HIDE_PASSWORDS_BUTTON"
+ : "plugin.securityconfig.masterpassword.SHOW_PASSWORDS_BUTTON"));
+ AccountsTableModel model =
+ (AccountsTableModel) accountsTable.getModel();
+ model.fireTableStructureChanged();
+ }
+
+ /**
+ * Displays a master password prompt to the user, verifies the entered
+ * password and then executes showOrHidePasswords method.
+ */
+ private void showOrHidePasswordsProtected()
+ {
+ String master = null;
+ JPasswordField passwordField = new JPasswordField();
+ String inputMsg
+ = resources.getI18NString("plugin.securityconfig.masterpassword.MP_INPUT");
+ String errorMsg =
+ ""
+ + resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_VERIFICATION_FAILURE_MSG")
+ + "";
+
+ boolean correct = true;
+ do
+ {
+ Object[] msg = null;
+ if (correct)
+ {
+ msg = new Object[]
+ { inputMsg, passwordField };
+ }
+ else
+ {
+ msg = new Object[]
+ { errorMsg, inputMsg, passwordField };
+ }
+ //clear the password field
+ passwordField.setText("");
+
+ if (JOptionPane.showOptionDialog(
+ null,
+ msg,
+ resources.getI18NString(
+ "plugin.securityconfig.masterpassword.MP_TITLE"),
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.QUESTION_MESSAGE,
+ null,
+ new String[]
+ {
+ resources.getI18NString("service.gui.OK"),
+ resources.getI18NString("service.gui.CANCEL")
+ },
+ resources.getI18NString("service.gui.OK"))
+ == JOptionPane.YES_OPTION)
+ {
+ master = new String(passwordField.getPassword());
+ correct =
+ !master.isEmpty()
+ && credentialsStorageService
+ .verifyMasterPassword(master);
+ }
+ else
+ return;
+ }
+ while (!correct);
+ showOrHidePasswords();
+ }
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsPanel.java b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsPanel.java
new file mode 100644
index 000000000..8b3cca215
--- /dev/null
+++ b/src/net/java/sip/communicator/plugin/securityconfig/masterpassword/SavedPasswordsPanel.java
@@ -0,0 +1,56 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.plugin.securityconfig.masterpassword;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+import net.java.sip.communicator.plugin.securityconfig.*;
+import net.java.sip.communicator.util.swing.*;
+
+/**
+ * Panel containing the saved passwords button.
+ *
+ * @author Dmitri Melnikov
+ */
+public class SavedPasswordsPanel
+ extends TransparentPanel
+{
+
+ /**
+ * Builds the panel.
+ */
+ public SavedPasswordsPanel() {
+ this.setLayout(new BorderLayout(10, 10));
+ this.setAlignmentX(0.0f);
+
+ initComponents();
+ }
+
+ /**
+ * Initializes the UI components.
+ */
+ private void initComponents()
+ {
+ JButton savedPasswordsButton = new JButton();
+ savedPasswordsButton.setText(
+ SecurityConfigActivator
+ .getResources()
+ .getI18NString(
+ "plugin.securityconfig.masterpassword.SAVED_PASSWORDS"));
+ savedPasswordsButton.addActionListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ SavedPasswordsDialog.getInstance().setVisible(true);
+ }
+ });
+ this.add(savedPasswordsButton, BorderLayout.EAST);
+ }
+}
diff --git a/src/net/java/sip/communicator/plugin/securityconfig/securityconfig.manifest.mf b/src/net/java/sip/communicator/plugin/securityconfig/securityconfig.manifest.mf
index 0624f0dd6..f23ae03b0 100644
--- a/src/net/java/sip/communicator/plugin/securityconfig/securityconfig.manifest.mf
+++ b/src/net/java/sip/communicator/plugin/securityconfig/securityconfig.manifest.mf
@@ -5,23 +5,26 @@ Bundle-Vendor: sip-communicator.org
Bundle-Version: 0.0.1
System-Bundle: yes
Import-Package: org.osgi.framework,
- net.java.sip.communicator.service.configuration,
+ net.java.sip.communicator.service.configuration,
+ net.java.sip.communicator.service.contactlist,
+ net.java.sip.communicator.service.credentialsstorage,
net.java.sip.communicator.service.gui,
net.java.sip.communicator.service.protocol,
net.java.sip.communicator.service.protocol.event,
net.java.sip.communicator.service.resources,
net.java.sip.communicator.util,
net.java.sip.communicator.util.swing,
- net.java.sip.communicator.service.contactlist,
- gnu.java.zrtp,
+ gnu.java.zrtp,
+ javax.crypto,
+ javax.crypto.interfaces,
+ javax.crypto.spec,
javax.imageio,
javax.swing,
javax.swing.border,
- javax.swing.table,
+ javax.swing.table,
+ javax.swing.text,
+ javax.swing.text.html,
javax.swing.event,
- javax.crypto,
- javax.crypto.interfaces,
- javax.crypto.spec,
org.bouncycastle.crypto,
org.bouncycastle.crypto.generators,
org.bouncycastle.crypto.params,
diff --git a/src/net/java/sip/communicator/service/configuration/ConfigurationService.java b/src/net/java/sip/communicator/service/configuration/ConfigurationService.java
index edd2d7940..c2bc0f818 100644
--- a/src/net/java/sip/communicator/service/configuration/ConfigurationService.java
+++ b/src/net/java/sip/communicator/service/configuration/ConfigurationService.java
@@ -18,6 +18,7 @@
*
* @author Emil Ivov
* @author Lubomir Marinov
+ * @author Dmitri Melnikov
*/
public interface ConfigurationService
{
@@ -161,6 +162,31 @@ public void setProperty(String propertyName,
public List getPropertyNamesByPrefix(String prefix,
boolean exactPrefixMatch);
+ /**
+ * Returns a List of Strings containing the property names
+ * that have the specified suffix. A suffix is considered to be everything
+ * after the last dot in the property name.
+ *
+ * For example, imagine a configuration service instance containing two
+ * properties only:
+ *
+ *
+ * net.java.sip.communicator.PROP1=value1
+ * net.java.sip.communicator.service.protocol.PROP1=value2
+ *
+ *
+ * A call to this method with suffix equal to "PROP1" will return
+ * both properties, whereas the call with suffix equal to
+ * "communicator.PROP1" or "PROP2" will return an empty List. Thus,
+ * if the suffix argument contains a dot, nothing will be found.
+ *
+ *
+ * @param suffix the suffix for the property names to be returned
+ * @return a List of Strings containing the property names
+ * which contain the specified suffix
+ */
+ public List getPropertyNamesBySuffix(String suffix);
+
/**
* Returns the String value of the specified property and null in case no
* property value was mapped against the specified propertyName, or in
diff --git a/src/net/java/sip/communicator/service/credentialsstorage/CredentialsStorageService.java b/src/net/java/sip/communicator/service/credentialsstorage/CredentialsStorageService.java
new file mode 100644
index 000000000..abc5712c4
--- /dev/null
+++ b/src/net/java/sip/communicator/service/credentialsstorage/CredentialsStorageService.java
@@ -0,0 +1,71 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.service.credentialsstorage;
+
+/**
+ * Loads and saves user credentials from/to the persistent storage
+ * (configuration file in the default implementation).
+ *
+ * @author Dmitri Melnikov
+ */
+public interface CredentialsStorageService
+{
+ /**
+ * Store the password for the account that starts with the given prefix.
+ *
+ * @param accountPrefix
+ * @param password
+ */
+ public void storePassword(String accountPrefix, String password);
+
+ /**
+ * Load the password for the account that starts with the given prefix.
+ *
+ * @param accountPrefix
+ * @return
+ */
+ public String loadPassword(String accountPrefix);
+
+ /**
+ * Remove the password for the account that starts with the given prefix.
+ *
+ * @param accountPrefix
+ */
+ public void removePassword(String accountPrefix);
+
+ /**
+ * Checks if master password was set by the user and
+ * it is used to encrypt saved account passwords.
+ *
+ * @return true if used, false if not
+ */
+ public boolean isUsingMasterPassword();
+
+ /**
+ * Changes the old master password to the new one.
+ * For all saved account passwords it decrypts them with the old MP and then
+ * encrypts them with the new MP.
+ * @param oldPassword
+ * @param newPassword
+ * @return true if MP was changed successfully, false otherwise
+ */
+ public boolean changeMasterPassword(String oldPassword, String newPassword);
+
+ /**
+ * Verifies the correctness of the master password.
+ * @param master
+ * @return true if the password is correct, false otherwise
+ */
+ public boolean verifyMasterPassword(String master);
+
+ /**
+ * Checks if the account password that starts with the given prefix is saved in encrypted form.
+ *
+ * @return true if saved, false if not
+ */
+ public boolean isStoredEncrypted(String accountPrefix);
+}
diff --git a/src/net/java/sip/communicator/service/credentialsstorage/CryptoException.java b/src/net/java/sip/communicator/service/credentialsstorage/CryptoException.java
new file mode 100644
index 000000000..b8923f16a
--- /dev/null
+++ b/src/net/java/sip/communicator/service/credentialsstorage/CryptoException.java
@@ -0,0 +1,51 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.service.credentialsstorage;
+
+/**
+ * Exception thrown by the Crypto encrypt/decrypt interface methods.
+ *
+ * @author Dmitri Melnikov
+ */
+public class CryptoException
+ extends Exception
+{
+ private static final long serialVersionUID = -5424208764356198091L;
+
+ /**
+ * Set when encryption fails.
+ */
+ public static final int ENCRYPTION_ERROR = 1;
+
+ /**
+ * Set when decryption fails.
+ */
+ public static final int DECRYPTION_ERROR = 2;
+
+ /**
+ * Set when a decryption fail is caused by the wrong key.
+ */
+ public static final int WRONG_KEY = 3;
+
+ /**
+ * The error code of this exception.
+ */
+ private final int errorCode;
+
+ public CryptoException(int code, Exception ex) {
+ super(ex);
+ this.errorCode = code;
+ }
+
+ /**
+ * @return the error code for the exception.
+ */
+ public int getErrorCode()
+ {
+ return errorCode;
+ }
+}
diff --git a/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java b/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java
index 1d5c21e15..c97a09a08 100644
--- a/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java
+++ b/src/net/java/sip/communicator/service/protocol/ProtocolProviderFactory.java
@@ -8,11 +8,12 @@
import java.util.*;
-import org.osgi.framework.*;
-
import net.java.sip.communicator.service.configuration.*;
+import net.java.sip.communicator.service.credentialsstorage.*;
import net.java.sip.communicator.util.*;
+import org.osgi.framework.*;
+
/**
* The ProtocolProviderFactory is what actually creates instances of a
* ProtocolProviderService implementation. A provider factory would register,
@@ -27,8 +28,13 @@
*/
public abstract class ProtocolProviderFactory
{
- private static final Logger logger =
- Logger.getLogger(ProtocolProviderFactory.class);
+ /**
+ * The Logger used by the ProtocolProviderFactory class
+ * and its instances for logging output.
+ */
+ private static final Logger logger
+ = Logger.getLogger(ProtocolProviderFactory.class);
+
/**
* Then name of a property which represents a password.
*/
@@ -473,12 +479,12 @@ public void storePassword(AccountID accountID, String password)
*
*
* @param bundleContext a currently valid bundle context.
- * @param accountID the AccountID for the account whose password we're
- * storing.
- * @param password the password itself.
+ * @param accountID the AccountID of the account whose password is
+ * to be stored
+ * @param password the password to be stored
*
* @throws IllegalArgumentException if no account corresponding to
- * accountID has been previously stored.
+ * accountID has been previously stored.
*/
protected void storePassword(BundleContext bundleContext,
AccountID accountID,
@@ -486,7 +492,7 @@ protected void storePassword(BundleContext bundleContext,
throws IllegalArgumentException
{
String accountPrefix = findAccountPrefix(
- bundleContext, accountID);
+ bundleContext, accountID, getFactoryImplPackageName());
if (accountPrefix == null)
throw new IllegalArgumentException(
@@ -494,24 +500,14 @@ protected void storePassword(BundleContext bundleContext,
+ accountID.getAccountUniqueID()
+ " in package" + getFactoryImplPackageName());
- //obscure the password
- String mangledPassword = null;
-
- //if password is null then the caller simply wants the current password
- //removed from the cache. make sure they don't get a null pointer
- //instead.
- if(password != null)
- mangledPassword = new String(Base64.encode(password.getBytes()));
-
- //get a reference to the config service and store it.
- ServiceReference confReference
+ ServiceReference credentialsReference
= bundleContext.getServiceReference(
- ConfigurationService.class.getName());
- ConfigurationService configurationService
- = (ConfigurationService) bundleContext.getService(confReference);
+ CredentialsStorageService.class.getName());
+ CredentialsStorageService credentialsService
+ = (CredentialsStorageService)
+ bundleContext.getService(credentialsReference);
- configurationService.setProperty(
- accountPrefix + "." + PASSWORD, mangledPassword);
+ credentialsService.storePassword(accountPrefix, password);
}
/**
@@ -545,27 +541,19 @@ protected String loadPassword(BundleContext bundleContext,
AccountID accountID)
{
String accountPrefix = findAccountPrefix(
- bundleContext, accountID);
+ bundleContext, accountID, getFactoryImplPackageName());
if (accountPrefix == null)
return null;
- //get a reference to the config service and store it.
- ServiceReference confReference
+ ServiceReference credentialsReference
= bundleContext.getServiceReference(
- ConfigurationService.class.getName());
- ConfigurationService configurationService
- = (ConfigurationService) bundleContext.getService(confReference);
-
- //obscure the password
- String mangledPassword
- = configurationService.getString(
- accountPrefix + "." + PASSWORD);
-
- if(mangledPassword == null)
- return null;
+ CredentialsStorageService.class.getName());
+ CredentialsStorageService credentialsService
+ = (CredentialsStorageService)
+ bundleContext.getService(credentialsReference);
- return new String(Base64.decode(mangledPassword));
+ return credentialsService.loadPassword(accountPrefix);
}
/**
@@ -784,15 +772,16 @@ protected boolean removeStoredAccount(AccountID accountID)
* @param bundleContext a currently valid bundle context.
* @param accountID the AccountID of the account whose properties we're
* looking for.
+ * @param a String containing the package name of the concrete factory
+ * class that extends us.
* @return a String indicating the ConfigurationService property name
* prefix under which all account properties are stored or null if no
* account corresponding to the specified id was found.
*/
- protected String findAccountPrefix(BundleContext bundleContext,
- AccountID accountID)
+ public static String findAccountPrefix(BundleContext bundleContext,
+ AccountID accountID,
+ String sourcePackageName)
{
- String sourcePackageName = getFactoryImplPackageName();
-
ServiceReference confReference
= bundleContext.getServiceReference(
ConfigurationService.class.getName());
diff --git a/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf b/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf
index 0a64c5f72..862ba0ca2 100644
--- a/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf
+++ b/src/net/java/sip/communicator/service/protocol/protocol.provider.manifest.mf
@@ -7,6 +7,7 @@ System-Bundle: yes
Import-Package: org.osgi.framework,
net.java.sip.communicator.service.configuration,
net.java.sip.communicator.service.configuration.event,
+ net.java.sip.communicator.service.credentialsstorage,
net.java.sip.communicator.util
Export-Package: net.java.sip.communicator.service.protocol,
net.java.sip.communicator.service.protocol.aimconstants,
diff --git a/test/net/java/sip/communicator/slick/credentialsstorage/CredentialsStorageServiceLick.java b/test/net/java/sip/communicator/slick/credentialsstorage/CredentialsStorageServiceLick.java
new file mode 100644
index 000000000..c47063609
--- /dev/null
+++ b/test/net/java/sip/communicator/slick/credentialsstorage/CredentialsStorageServiceLick.java
@@ -0,0 +1,58 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.slick.credentialsstorage;
+
+import java.util.*;
+
+import junit.framework.*;
+
+import net.java.sip.communicator.service.credentialsstorage.*;
+import net.java.sip.communicator.util.*;
+
+import org.osgi.framework.*;
+
+/**
+ * @author Dmitri Melnikov
+ */
+public class CredentialsStorageServiceLick
+ extends TestSuite
+ implements BundleActivator
+{
+ private Logger logger = Logger.getLogger(getClass().getName());
+
+ protected static CredentialsStorageService credentialsService = null;
+ protected static BundleContext bc = null;
+ public static TestCase tcase = new TestCase(){};
+
+ /**
+ *
+ * @param bundleContext BundleContext
+ * @throws Exception
+ */
+ public void start(BundleContext bundleContext) throws Exception
+ {
+ CredentialsStorageServiceLick.bc = bundleContext;
+ setName("CredentialsStorageServiceLick");
+ Hashtable properties = new Hashtable();
+ properties.put("service.pid", getName());
+
+ addTestSuite(TestCredentialsStorageService.class);
+ bundleContext.registerService(getClass().getName(), this, properties);
+
+ logger.debug("Successfully registered " + getClass().getName());
+ }
+
+ /**
+ * stop
+ *
+ * @param bundlecontext BundleContext
+ * @throws Exception
+ */
+ public void stop(BundleContext bundlecontext) throws Exception
+ {
+ }
+}
diff --git a/test/net/java/sip/communicator/slick/credentialsstorage/TestCredentialsStorageService.java b/test/net/java/sip/communicator/slick/credentialsstorage/TestCredentialsStorageService.java
new file mode 100644
index 000000000..214bd94a1
--- /dev/null
+++ b/test/net/java/sip/communicator/slick/credentialsstorage/TestCredentialsStorageService.java
@@ -0,0 +1,183 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.slick.credentialsstorage;
+
+import junit.framework.*;
+import net.java.sip.communicator.service.credentialsstorage.*;
+
+import org.osgi.framework.*;
+
+/**
+ * Tests for the @link{CredentialsStorageService}.
+ *
+ * @author Dmitri Melnikov
+ */
+public class TestCredentialsStorageService
+ extends TestCase
+{
+ /**
+ * The service we are testing.
+ */
+ private CredentialsStorageService credentialsService = null;
+
+ /**
+ * Prefix for the test account.
+ */
+ private final String accountPrefix = "my.account.prefix";
+
+ /**
+ * Password for the test account.
+ */
+ private final String accountPassword = "pa$$W0rt123.";
+
+ /**
+ * The master password.
+ */
+ private final String masterPassword = "MasterPazz321";
+
+ /**
+ * Another master password.
+ */
+ private final String otherMasterPassword = "123$ecretPSWRD";
+
+
+ /**
+ * Generic JUnit Constructor.
+ *
+ * @param name the name of the test
+ */
+ public TestCredentialsStorageService(String name)
+ {
+ super(name);
+ BundleContext context = CredentialsStorageServiceLick.bc;
+ ServiceReference ref =
+ context.getServiceReference(CredentialsStorageService.class
+ .getName());
+ credentialsService =
+ (CredentialsStorageService) context.getService(ref);
+ }
+
+ /**
+ * Generic JUnit setUp method.
+ * @throws Exception if anything goes wrong.
+ */
+ protected void setUp() throws Exception
+ {
+ // set the master password
+ boolean passSet =
+ credentialsService.changeMasterPassword(null, masterPassword);
+ if (!passSet)
+ {
+ throw new Exception("Failed to set the master password");
+ }
+ credentialsService.storePassword(accountPrefix, accountPassword);
+ super.setUp();
+ }
+
+ /**
+ * Generic JUnit tearDown method.
+ * @throws Exception if anything goes wrong.
+ */
+ protected void tearDown() throws Exception
+ {
+ // remove the password
+ boolean passRemoved =
+ credentialsService.changeMasterPassword(masterPassword, null);
+ if (!passRemoved)
+ {
+ throw new Exception("Failed to remove the master password");
+ }
+ credentialsService.removePassword(accountPrefix);
+ }
+
+ /**
+ * Tests if a master password can be verified.
+ */
+ public void testIsVerified()
+ {
+ // try to verify a wrong password
+ boolean verify1 =
+ credentialsService.verifyMasterPassword(otherMasterPassword);
+ assertFalse("Wrong password cannot be correct", verify1);
+
+ // try to verify a correct password
+ boolean verify2 =
+ credentialsService.verifyMasterPassword(masterPassword);
+ assertTrue("Correct password cannot be wrong", verify2);
+ }
+
+ /**
+ * Tests whether the loaded password is the same as the stored one.
+ */
+ public void testLoadPassword()
+ {
+ String loadedPassword = credentialsService.loadPassword(accountPrefix);
+
+ assertEquals("Loaded and stored passwords do not match", accountPassword,
+ loadedPassword);
+ }
+
+ /**
+ * Tests whether the service knows that we are using a master password.
+ */
+ public void testIsUsingMasterPassword()
+ {
+ boolean isUsing = credentialsService.isUsingMasterPassword();
+
+ assertTrue("Master password is used, true expected", isUsing);
+ }
+
+ /**
+ * Changes the master password to the new value and back again.
+ */
+ public void testChangeMasterPassword()
+ {
+ // change MP to a new value
+ boolean change1 =
+ credentialsService.changeMasterPassword(masterPassword,
+ otherMasterPassword);
+ assertTrue("Changing master password failed", change1);
+
+ // account passwords must remain the same
+ String loadedPassword = credentialsService.loadPassword(accountPrefix);
+ assertEquals("Account passwords must not differ", loadedPassword,
+ accountPassword);
+
+ // change MP back
+ boolean change2 =
+ credentialsService.changeMasterPassword(otherMasterPassword,
+ masterPassword);
+ assertTrue("Changing master password back failed", change2);
+ }
+
+ /**
+ * Test that the service is aware that the account password is stored in an
+ * encrypted form.
+ */
+ public void testIsStoredEncrypted()
+ {
+ boolean storedEncrypted =
+ credentialsService.isStoredEncrypted(accountPrefix);
+ assertTrue("Account password is not stored encrypted", storedEncrypted);
+ }
+
+ /**
+ * Tests whether removing the saved password really removes it.
+ */
+ public void testRemoveSavedPassword()
+ {
+ // remove the saved password
+ credentialsService.removePassword(accountPrefix);
+
+ // try to load the password
+ String loadedPassword = credentialsService.loadPassword(accountPrefix);
+ assertNull("Password was not removed", loadedPassword);
+
+ // save it back again
+ credentialsService.storePassword(accountPrefix, accountPassword);
+ }
+}
diff --git a/test/net/java/sip/communicator/slick/credentialsstorage/credentialsstorage.slick.manifest.mf b/test/net/java/sip/communicator/slick/credentialsstorage/credentialsstorage.slick.manifest.mf
new file mode 100644
index 000000000..c53903d94
--- /dev/null
+++ b/test/net/java/sip/communicator/slick/credentialsstorage/credentialsstorage.slick.manifest.mf
@@ -0,0 +1,11 @@
+Bundle-Activator: net.java.sip.communicator.slick.credentialsstorage.CredentialsStorageServiceLick
+Bundle-Name: Credentials Storage Service Implementation Compatibility Kit
+Bundle-Description: A Service Implementation Compatibility Kit for the Credentials Storage Service
+Bundle-Vendor: sip-communicator.org
+Bundle-Version: 0.0.1
+System-Bundle: yes
+Import-Package: junit.framework,
+ net.java.sip.communicator.service.credentialsstorage,
+ net.java.sip.communicator.util,
+ org.osgi.framework
+Export-Package: net.java.sip.communicator.slick.credentialsstorage