/* * Copyright 2000-2013 JetBrains s.r.o. * * 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 com.intellij.ide.passwordSafe.impl.providers.masterKey; import com.intellij.ide.passwordSafe.MasterPasswordUnavailableException; import com.intellij.ide.passwordSafe.PasswordSafeException; import com.intellij.ide.passwordSafe.impl.providers.BasePasswordSafeProvider; import com.intellij.ide.passwordSafe.impl.providers.ByteArrayWrapper; import com.intellij.ide.passwordSafe.impl.providers.EncryptionUtil; import com.intellij.ide.passwordSafe.impl.providers.masterKey.windows.WindowsCryptUtils; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import org.jetbrains.annotations.Nullable; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; /** * The password safe that stores information in configuration file encrypted by master password */ public class MasterKeyPasswordSafe extends BasePasswordSafeProvider { /** * The test password key */ private static final String TEST_PASSWORD_KEY = "TEST_PASSWORD:"; /** * The test password value */ private static final String TEST_PASSWORD_VALUE = "test password"; /** * The password database instance */ final PasswordDatabase database; /** * The key to use to encrypt data */ private transient final AtomicReference<byte[]> key = new AtomicReference<byte[]>(); /** * The constructor * * @param database the password database */ public MasterKeyPasswordSafe(PasswordDatabase database) { this.database = database; } /** * @return true if the component is running in the test mode */ protected boolean isTestMode() { return false; } /** * Reset password for the password safe (clears password database). The method is used from plugin's UI. * * @param password the password to set * @param encrypt if the password should be encrypted an stored is master database */ void resetMasterPassword(String password, boolean encrypt) { this.key.set(EncryptionUtil.genPasswordKey(password)); database.clear(); try { storePassword(null, MasterKeyPasswordSafe.class, testKey(password), TEST_PASSWORD_VALUE); if (encrypt) { database.setPasswordInfo(encryptPassword(password)); } else { database.setPasswordInfo(new byte[0]); } } catch (PasswordSafeException e) { throw new IllegalStateException("There should be no problem with password at this point", e); } } /** * Set password to use (used from plugin's UI) * * @param password the password * @return true, if password is a correct one */ boolean setMasterPassword(String password) { byte[] savedKey = this.key.get(); this.key.set(EncryptionUtil.genPasswordKey(password)); String rc; try { rc = getPassword(null, MasterKeyPasswordSafe.class, testKey(password)); } catch (PasswordSafeException e) { throw new IllegalStateException("There should be no problem with password at this point", e); } if (!TEST_PASSWORD_VALUE.equals(rc)) { this.key.set(savedKey); return false; } else { return true; } } /** * Encrypt database with new password * * @param oldPassword the old password * @param newPassword the new password * @param encrypt * @return re-encrypted database */ boolean changeMasterPassword(String oldPassword, String newPassword, boolean encrypt) { if (!setMasterPassword(oldPassword)) { return false; } byte[] oldKey = key.get(); byte[] newKey = EncryptionUtil.genPasswordKey(newPassword); ByteArrayWrapper testKey = new ByteArrayWrapper(EncryptionUtil.dbKey(oldKey, MasterKeyPasswordSafe.class, testKey(oldPassword))); HashMap<ByteArrayWrapper, byte[]> oldDb = new HashMap<ByteArrayWrapper, byte[]>(); database.copyTo(oldDb); HashMap<ByteArrayWrapper, byte[]> newDb = new HashMap<ByteArrayWrapper, byte[]>(); for (Map.Entry<ByteArrayWrapper, byte[]> e : oldDb.entrySet()) { if (testKey.equals(e.getKey())) { continue; } byte[] decryptedKey = EncryptionUtil.decryptKey(oldKey, e.getKey().unwrap()); String decryptedText = EncryptionUtil.decryptText(oldKey, e.getValue()); newDb.put(new ByteArrayWrapper(EncryptionUtil.encryptKey(newKey, decryptedKey)), EncryptionUtil.encryptText(newKey, decryptedText)); } synchronized (database.getDbLock()) { resetMasterPassword(newPassword, encrypt); database.putAll(newDb); } return true; } /** * The test key * * @param password the password for the test key * @return the test key */ private static String testKey(String password) { return TEST_PASSWORD_KEY + password; } /** * {@inheritDoc} */ @Override protected byte[] key(@Nullable final Project project) throws PasswordSafeException { if (!isTestMode() && ApplicationManager.getApplication().isHeadlessEnvironment()) { throw new MasterPasswordUnavailableException("The provider is not available in headless environment"); } if (key.get() == null) { if (isPasswordEncrypted()) { try { String s = decryptPassword(database.getPasswordInfo()); setMasterPassword(s); } catch (PasswordSafeException e) { // ignore exception and ask password } } if (key.get() == null) { final Ref<PasswordSafeException> ex = new Ref<PasswordSafeException>(); ApplicationManager.getApplication().invokeAndWait(new Runnable() { public void run() { if (key.get() == null) { try { if (isTestMode()) { throw new MasterPasswordUnavailableException("Master password must be specified in test mode."); } if (database.isEmpty()) { if (!ResetPasswordDialog.newPassword(project, MasterKeyPasswordSafe.this)) { throw new MasterPasswordUnavailableException("Master password is required to store passwords in the database."); } } else { MasterPasswordDialog.askPassword(project, MasterKeyPasswordSafe.this); } } catch (PasswordSafeException e) { ex.set(e); } catch (Exception e) { //noinspection ThrowableInstanceNeverThrown ex.set(new MasterPasswordUnavailableException("The problem with retrieving the password", e)); } } } }, ModalityState.defaultModalityState()); //noinspection ThrowableResultOfMethodCallIgnored if (ex.get() != null) { throw ex.get(); } } } return this.key.get(); } /** * {@inheritDoc} */ @Override public String getPassword(@Nullable Project project, Class requester, String key) throws PasswordSafeException { if (database.isEmpty()) { return null; } return super.getPassword(project, requester, key); } /** * {@inheritDoc} */ @Override public void removePassword(@Nullable Project project, Class requester, String key) throws PasswordSafeException { if (database.isEmpty()) { return; } super.removePassword(project, requester, key); } /** * {@inheritDoc} */ @Override protected byte[] getEncryptedPassword(byte[] key) { return database.get(key); } /** * {@inheritDoc} */ @Override protected void removeEncryptedPassword(byte[] key) { database.remove(key); } /** * {@inheritDoc} */ @Override protected void storeEncryptedPassword(byte[] key, byte[] encryptedPassword) { database.put(key, encryptedPassword); } /** * {@inheritDoc} */ @Override public boolean isSupported() { return !ApplicationManager.getApplication().isHeadlessEnvironment(); } /** * {@inheritDoc} */ @Override public String getDescription() { return "This provider stores passwords in IDEA config and uses master password to encrypt other passwords. " + "The passwords for the same resources are shared between different projects."; } /** * {@inheritDoc} */ @Override public String getName() { return "Master Key PasswordSafe"; } /** * @return true, if OS protected passwords are supported for the current platform */ @SuppressWarnings({"MethodMayBeStatic"}) public boolean isOsProtectedPasswordSupported() { // TODO extension point needed? return SystemInfo.isWindows; } /** * Encrypt master password * * @param pw the password to encrypt * @return the encrypted password * @throws MasterPasswordUnavailableException * if encryption fails */ private static byte[] encryptPassword(String pw) throws MasterPasswordUnavailableException { assert SystemInfo.isWindows; return WindowsCryptUtils.protect(EncryptionUtil.getUTF8Bytes(pw)); } /** * Decrypt master password * * @param pw the password to decrypt * @return the decrypted password * @throws MasterPasswordUnavailableException * if decryption fails */ private static String decryptPassword(byte[] pw) throws MasterPasswordUnavailableException { assert SystemInfo.isWindows; try { return new String(WindowsCryptUtils.unprotect(pw), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("UTF-8 not available", e); } } /** * @return true, if the password is currently encrypted in the database */ public boolean isPasswordEncrypted() { byte[] i = database.getPasswordInfo(); return i != null && i.length > 0; } /** * @return check if provider database is empty */ public boolean isEmpty() { return database.isEmpty(); } }