blob: aa061fa8521e5b923d3decfb3bd09373eec72d1b [file] [log] [blame]
/*
* Copyright 2000-2014 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.concurrency.AsyncFutureFactory;
import com.intellij.concurrency.AsyncFutureResult;
import com.intellij.ide.passwordSafe.MasterPasswordUnavailableException;
import com.intellij.ide.passwordSafe.PasswordSafeException;
import com.intellij.ide.passwordSafe.impl.PasswordSafeTimed;
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.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
/**
* The password safe that stores information in configuration file encrypted by master password
*/
public class MasterKeyPasswordSafe extends BasePasswordSafeProvider {
private static final String TEST_PASSWORD_KEY = "TEST_PASSWORD:";
private static final String TEST_PASSWORD_VALUE = "test password";
private final PasswordDatabase myDatabase;
private transient final PasswordSafeTimed<Ref<Object>> myKey = new PasswordSafeTimed<Ref<Object>>() {
protected Ref<Object> compute() {
return Ref.create();
}
@Override
protected int getMinutesToLive() {
return Registry.intValue("passwordSafe.masterPassword.ttl");
}
};
public MasterKeyPasswordSafe(PasswordDatabase database) {
this.myDatabase = database;
}
/**
* 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) {
myKey.get().set(EncryptionUtil.genPasswordKey(password));
myDatabase.clear();
try {
storePassword(null, MasterKeyPasswordSafe.class, testKey(password), TEST_PASSWORD_VALUE);
if (encrypt) {
myDatabase.setPasswordInfo(encryptPassword(password));
}
else {
myDatabase.setPasswordInfo(ArrayUtil.EMPTY_BYTE_ARRAY);
}
}
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) {
Object savedKey = myKey.get().get();
myKey.get().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)) {
myKey.get().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 = (byte[])myKey.get().get(); // set right in the previous call
byte[] newKey = EncryptionUtil.genPasswordKey(newPassword);
ByteArrayWrapper testKey = new ByteArrayWrapper(EncryptionUtil.dbKey(oldKey, MasterKeyPasswordSafe.class, testKey(oldPassword)));
HashMap<ByteArrayWrapper, byte[]> oldDb = new HashMap<ByteArrayWrapper, byte[]>();
myDatabase.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 (myDatabase.getDbLock()) {
resetMasterPassword(newPassword, encrypt);
myDatabase.putAll(newDb);
}
return true;
}
private static String testKey(String password) {
return TEST_PASSWORD_KEY + password;
}
@NotNull
@Override
protected byte[] key(@Nullable final Project project, @NotNull final Class requestor) throws PasswordSafeException {
Object key = myKey.get().get();
if (key instanceof byte[]) return (byte[])key;
if (key instanceof PasswordSafeException && ((PasswordSafeException)key).justHappened()) throw (PasswordSafeException)key;
if (isPasswordEncrypted()) {
try {
setMasterPassword(decryptPassword(myDatabase.getPasswordInfo()));
key = myKey.get().get();
if (key instanceof byte[]) return (byte[])key;
}
catch (PasswordSafeException e) {
// ignore exception and ask password
}
}
if (ApplicationManager.getApplication().isHeadlessEnvironment()) {
throw new MasterPasswordUnavailableException("The provider is not available in headless environment");
}
key = invokeAndWait(new ThrowableComputable<Object, PasswordSafeException>() {
@Override
public Object compute() throws PasswordSafeException {
Object key = myKey.get().get();
if (key instanceof byte[] || key instanceof PasswordSafeException && ((PasswordSafeException)key).justHappened()) {
return key;
}
try {
if (myDatabase.isEmpty()) {
if (!MasterPasswordDialog.resetMasterPasswordDialog(project, MasterKeyPasswordSafe.this, requestor).showAndGet()) {
throw new MasterPasswordUnavailableException("Master password is required to store passwords in the database.");
}
}
else {
MasterPasswordDialog.askPassword(project, MasterKeyPasswordSafe.this, requestor);
}
}
catch (PasswordSafeException e) {
myKey.get().set(e);
throw e;
}
return myKey.get().get();
}
}, project == null ? Conditions.alwaysFalse() : project.getDisposed());
if (key instanceof byte[]) return (byte[])key;
if (key instanceof PasswordSafeException) throw (PasswordSafeException)key;
throw new AssertionError();
}
private static final Object ourEDTLock = new Object();
public <T, E extends Throwable> T invokeAndWait(@NotNull final ThrowableComputable<T, E> computable, @NotNull final Condition<?> expired) throws E {
if (ApplicationManager.getApplication().isDispatchThread()) {
return computable.compute();
}
final AsyncFutureResult<Object> future = AsyncFutureFactory.getInstance().createAsyncFutureResult();
final ExpirableRunnable runnable = new ExpirableRunnable() {
@Override
public boolean isExpired() {
boolean b = expired.value(null);
if (b) future.setException(new ProcessCanceledException());
return b;
}
@Override
public void run() {
try {
future.set(computable.compute());
}
catch (Throwable e) {
future.setException(e);
}
}
};
ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator();
synchronized (ourEDTLock) {
if (indicator != null && indicator.isModal()) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (!runnable.isExpired()) {
runnable.run();
}
}
});
}
else {
IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(runnable);
}
}
try {
return (T)future.get();
}
catch (InterruptedException e) {
throw new ProcessCanceledException(e);
}
catch (ExecutionException e) {
throw (E) e.getCause();
}
}
@Override
public String getPassword(@Nullable Project project, @NotNull Class requestor, String key) throws PasswordSafeException {
if (myDatabase.isEmpty()) {
return null;
}
return super.getPassword(project, requestor, key);
}
@Override
public void removePassword(@Nullable Project project, @NotNull Class requester, String key) throws PasswordSafeException {
if (myDatabase.isEmpty()) {
return;
}
super.removePassword(project, requester, key);
}
@Override
protected byte[] getEncryptedPassword(byte[] key) {
return myDatabase.get(key);
}
@Override
protected void removeEncryptedPassword(byte[] key) {
myDatabase.remove(key);
}
@Override
protected void storeEncryptedPassword(byte[] key, byte[] encryptedPassword) {
myDatabase.put(key, encryptedPassword);
}
@Override
public boolean isSupported() {
return !ApplicationManager.getApplication().isHeadlessEnvironment();
}
@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.";
}
@Override
public String getName() {
return "Master Key PasswordSafe";
}
public boolean isMasterPasswordEnabled() {
return setMasterPassword("");
}
@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 {
if (!SystemInfo.isWindows) throw new AssertionError("Windows OS expected");
return new String(WindowsCryptUtils.unprotect(pw), CharsetToolkit.UTF8_CHARSET);
}
public boolean isPasswordEncrypted() {
if (!isOsProtectedPasswordSupported()) return false;
byte[] info = myDatabase.getPasswordInfo();
return info != null && info.length > 0;
}
public boolean isEmpty() {
return myDatabase.isEmpty();
}
}