/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.credentialsstorage; import java.util.*; import net.java.sip.communicator.service.credentialsstorage.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.Base64; // disambiguation import org.jitsi.service.configuration.*; 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 <tt>Logger</tt> used by this <tt>CredentialsStorageServiceImpl</tt> * for logging output. */ 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. Encrypts and moves all passwords to * new properties. * * @param bc bundle context */ void start(BundleContext bc) { configurationService = ServiceUtils.getService(bc, ConfigurationService.class); /* * If a master password is set, the migration of the unencrypted * passwords will have to wait for the UIService to register in * order to be able to ask for the master password. But that is * unreasonably late in the case of no master password. */ if (!isUsingMasterPassword()) moveAllPasswordProperties(); } /** * 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 <tt>Crypto</tt> instance based on the input * (<tt>createCrypto</tt> method). This instance will be used later by all * other threads. * * @param accountPrefix account prefix * @param password the password to store * @return <tt>true</tt> if the specified <tt>password</tt> was successfully * stored; otherwise, <tt>false</tt> * @see CredentialsStorageServiceImpl#storePassword(String, String) */ public synchronized boolean storePassword( String accountPrefix, String password) { if (createCrypto()) { String encryptedPassword = null; try { if (password != null) encryptedPassword = crypto.encrypt(password); setEncrypted(accountPrefix, encryptedPassword); return true; } catch (Exception ex) { logger.error("Encryption failed, password not saved", ex); return false; } } else return false; } /** * Loads the password for the specified account. If the password is stored * encrypted, decrypts 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 * <tt>Crypto</tt> instance based on the input (<tt>createCrypto</tt> * method). This instance will be used later by all other threads. * * @param accountPrefix account prefix * @return the loaded password for the <tt>accountPrefix</tt> * @see CredentialsStorageServiceImpl#createCrypto() */ public synchronized String loadPassword(String accountPrefix) { String password = null; if (isStoredEncrypted(accountPrefix) && createCrypto()) { try { password = crypto.decrypt(getEncrypted(accountPrefix)); } catch (Exception ex) { logger.error("Decryption with master password failed", ex); // 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. * * @param accountPrefix account prefix * @return <tt>true</tt> if the password for the specified * <tt>accountPrefix</tt> was successfully removed; otherwise, * <tt>false</tt> */ public boolean removePassword(String accountPrefix) { setEncrypted(accountPrefix, null); if (logger.isDebugEnabled()) logger.debug("Password for '" + accountPrefix + "' removed"); return true; } /** * 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 {@link #MASTER_PROP_VALUE} * is equal to the decrypted {@link #MASTER_PROP}'s value, then * the MP is considered correct. * * @param master master password * @return <tt>true</tt> if the password is correct; <tt>false</tt>, * otherwise */ public boolean verifyMasterPassword(String master) { Crypto localCrypto = new AESCrypto(master); try { // use this value to verify master password correctness String encryptedValue = getEncryptedMasterPropValue(); boolean correct = MASTER_PROP_VALUE.equals(localCrypto.decrypt(encryptedValue)); if (correct) { // also set the crypto instance to use the correct MP setMasterPassword(master); } return correct; } 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 * @return <tt>true</tt> if master password was changed successfully; * <tt>false</tt>, otherwise */ public boolean changeMasterPassword(String oldPassword, String newPassword) { // get all encrypted account password properties List<String> encryptedAccountProps = configurationService .getPropertyNamesBySuffix(ACCOUNT_ENCRYPTED_PASSWORD); // this map stores propName -> password Map<String, String> passwords = new HashMap<String, String>(); 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<String, String> 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); } /** * Moves all password properties from unencrypted * {@link #ACCOUNT_UNENCRYPTED_PASSWORD} to the corresponding encrypted * {@link #ACCOUNT_ENCRYPTED_PASSWORD}. */ private void moveAllPasswordProperties() { List<String> unencryptedProperties = configurationService.getPropertyNamesBySuffix( ACCOUNT_UNENCRYPTED_PASSWORD); for (String prop : unencryptedProperties) { int idx = prop.lastIndexOf('.'); if (idx != -1) { String prefix = prop.substring(0, idx); String encodedPassword = getUnencrypted(prefix); /* * If the password is stored unencrypted, we have to migrate it, * of course. But if it is also stored encrypted in addition to * being stored unencrypted, the situation starts to look * unclear and it may be better to just not migrate it. */ if ((encodedPassword == null) || (encodedPassword.length() == 0) || isStoredEncrypted(prefix)) { setUnencrypted(prefix, null); } else if (!movePasswordProperty( prefix, new String(Base64.decode(encodedPassword)))) { logger.warn("Failed to move password for prefix " + prefix); } } } } /** * 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 * @return <tt>true</tt> if the specified <tt>password</tt> was successfully * moved; otherwise, <tt>false</tt> */ private boolean movePasswordProperty(String accountPrefix, String password) { if (createCrypto()) { try { setEncrypted(accountPrefix, crypto.encrypt(password)); setUnencrypted(accountPrefix, null); return true; } catch (CryptoException cex) { logger.debug("Encryption failed", cex); } } // properties are not moved return false; } /** * 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 { configurationService.setProperty( MASTER_PROP, crypto.encrypt(MASTER_PROP_VALUE)); } catch (CryptoException cex) { logger.error( "Failed to encrypt and write verification value", cex); } } } /** * 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 <tt>true</tt> if the Crypto instance was created; <tt>false</tt>, * otherwise */ private synchronized boolean createCrypto() { /* * XXX The method #createCrypto() is synchronized in order to not ask * for the master password more than once. Without the synchronization, * it is possible to have the master password prompt shown twice in a * row during application startup when unencypted passwords are to be * migrated with the master password already set and the accounts start * loading. */ 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; } else { /* * At this point the master password must be correct, so we * set the crypto instance to use it */ setMasterPassword(master); } moveAllPasswordProperties(); } 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 (crypto != null); } /** * 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 <tt>null</tt> if none was provided. */ private String showPasswordPrompt() { String master; // Ask for master password until the input is correct or // cancel button is pressed and null returned boolean correct = true; MasterPasswordInputService masterPasswordInputService = CredentialsStorageActivator.getMasterPasswordInputService(); if(masterPasswordInputService == null) { logger.error( "Missing MasterPasswordInputService to show input dialog"); return null; } do { master = masterPasswordInputService.showInputDialog(correct); if (master == null) return null; correct = ((master.length() != 0) && verifyMasterPassword(master)); } 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. * * @param accountPrefix account prefix * @return <tt>true</tt> if saved, <tt>false</tt> if not */ public boolean isStoredEncrypted(String accountPrefix) { return configurationService.getString( accountPrefix + "." + ACCOUNT_ENCRYPTED_PASSWORD) != null; } /** * 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); } }