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 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