/**************************************************************************
OmegaT - Computer Assisted Translation (CAT) tool
with fuzzy matching, translation memory, keyword search,
glossaries, and translation leveraging into updated projects.
Copyright (C) 2016 Aaron Madlon-Kay
Home page: http://www.omegat.org/
Support center: http://groups.yahoo.com/group/OmegaT/
This file is part of OmegaT.
OmegaT is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmegaT is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/
package org.omegat.util;
import java.awt.Window;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
import javax.swing.FocusManager;
import org.jasypt.exceptions.AlreadyInitializedException;
import org.jasypt.exceptions.EncryptionInitializationException;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.jasypt.util.text.BasicTextEncryptor;
import org.omegat.core.Core;
import org.omegat.gui.dialogs.PasswordEnterDialogController;
import org.omegat.gui.dialogs.PasswordSetDialogController;
import org.omegat.gui.main.IMainWindow;
import org.omegat.util.gui.UIThreadsUtil;
/**
* A class for storing and retrieving sensitive values such as login
* credentials, API keys, etc., from the program-wide Preferences store.
* <p>
* Stored values are encrypted with a "master password" (=encryption key). If
* this has not yet been supplied to the encryption engine, the user will be
* prompted to create it. Upon creating a master password, a "canary" value is
* saved to preferences; the canary is used to ensure that all values are
* encrypted with the same master password (thus ensuring that the user only
* needs to remember one password).
* <p>
* The user can choose not to set a master password; in this case a master
* password is generated for the user and stored in Preferences in plain text.
* Values stored with the CredentialsManager will still be encrypted, but
* because the master password is readily accessible the actual security is
* greatly diminished. This feature was deemed required for usability, despite
* the drawbacks.
*
* @author Aaron Madlon-Kay
*/
public final class CredentialsManager {
public interface IPasswordPrompt {
Optional<char[]> getExistingPassword(String message);
PasswordSetResult createNewPassword();
}
private static final Logger LOGGER = Logger.getLogger(CredentialsManager.class.getName());
private static final String CREDENTIALS_MANAGER_CANARY = "credentials_manager_canary";
private static final String CREDENTIALS_MASTER_PASSWORD = "credentials_master_password";
private static class SingletonHelper {
private static final CredentialsManager INSTANCE = new CredentialsManager();
}
public static CredentialsManager getInstance() {
return SingletonHelper.INSTANCE;
}
private final IPasswordPrompt prompt;
private BasicTextEncryptor textEncryptor;
private CredentialsManager() {
prompt = new GuiPasswordPrompt();
textEncryptor = new BasicTextEncryptor();
}
/**
* Securely store a key-value pair. If the master password is not stored and
* has not been input, the user will be prompted to input it.
*
* @param key
* The key for the value to store (not encrypted)
* @param value
* The value to store (encrypted)
* @return True if the value was stored successfully; false if otherwise
* (e.g. the user canceled)
*/
public boolean store(String key, String value) {
if (value.isEmpty()) {
clear(key);
return true;
}
Optional<String> encrypted = encrypt(value);
encrypted.ifPresent(ev -> Preferences.setPreference(key, ev));
return encrypted.isPresent();
}
/**
* Check to see if a value has been securely stored for the given key.
* <p>
* If the master password has not been set, this will return false for all
* keys.
*
* @see #isMasterPasswordSet()
*/
public boolean isStored(String key) {
return isMasterPasswordSet() && !Preferences.getPreference(key).isEmpty();
}
synchronized private Optional<String> encrypt(String text) {
while (true) {
try {
return Optional.of(textEncryptor.encrypt(text));
} catch (EncryptionInitializationException e) {
if (!onEncryptionFailed()) {
return Optional.empty();
}
}
}
}
private void setEncryptionKey(char[] password) {
try {
textEncryptor.setPasswordCharArray(password);
} catch (AlreadyInitializedException e) {
textEncryptor = new BasicTextEncryptor();
setEncryptionKey(password);
}
}
private void setMasterPassword(char[] masterPassword) {
setEncryptionKey(masterPassword);
store(CREDENTIALS_MANAGER_CANARY, CREDENTIALS_MANAGER_CANARY);
}
/**
* Check whether or not the master password has been set. This checks only
* for the presence of the canary value.
*/
public boolean isMasterPasswordSet() {
return !Preferences.getPreference(CREDENTIALS_MANAGER_CANARY).isEmpty();
}
/**
* Check whether or not the master password is stored in plain text so the user doesn't need to input it.
* The master password is considered to not be stored if {@link #isMasterPasswordSet()} returns false.
*/
public boolean isMasterPasswordStored() {
return isMasterPasswordSet() && !Preferences.getPreference(CREDENTIALS_MASTER_PASSWORD).isEmpty();
}
/**
* Clear the stored master password (if present) and the canary value.
* Afterwards, any encrypted values will be considered to be not set
* ({@link #isStored(String)} returns false; {@link #retrieve(String)}
* returns {@link Optional#empty()}).
*/
public void clearMasterPassword() {
clear(CREDENTIALS_MANAGER_CANARY);
clear(CREDENTIALS_MASTER_PASSWORD);
synchronized (this) {
textEncryptor = new BasicTextEncryptor();
}
}
/**
* Clear the value for the given key.
*/
public void clear(String key) {
Preferences.setPreference(key, "");
}
/**
* Retrieve the securely stored value for the given key. If the master
* password is not stored and has not been input, the user will be prompted
* to input it.
*
* @param key
* The key for the value to store (not encrypted)
* @return The Optional-wrapped value, which can be empty if the user
* declines to enter the master password or the master password is
* not the correct encryption key for the value
*/
public Optional<String> retrieve(String key) {
String encrypted = Preferences.getPreference(key);
if (encrypted.isEmpty()) {
return Optional.empty();
}
return decrypt(encrypted);
}
private Optional<String> decrypt(String text) {
if (!isMasterPasswordSet()) {
LOGGER.warning("Trying to retrieve encrypted credentials but no master password has been set.");
return Optional.empty();
}
synchronized (this) {
while (true) {
try {
return Optional.of(textEncryptor.decrypt(text));
} catch (EncryptionOperationNotPossibleException e) {
LOGGER.severe(
"Could not decrypt stored credential with supposedly correct master password.");
return Optional.empty();
} catch (EncryptionInitializationException e) {
if (!onDecryptionFailed()) {
return Optional.empty();
}
}
}
}
}
private boolean onEncryptionFailed() {
if (isMasterPasswordSet()) {
if (useStoredMasterPassword()) {
return true;
}
return promptForExistingPassword();
} else {
return promptForCreatingPassword();
}
}
private boolean useStoredMasterPassword() {
String mp = Preferences.getPreference(CREDENTIALS_MASTER_PASSWORD);
if (!mp.isEmpty()) {
setEncryptionKey(mp.toCharArray());
if (checkCanary()) {
return true;
}
}
return false;
}
private boolean promptForCreatingPassword() {
PasswordSetResult result = prompt.createNewPassword();
switch (result.responseType) {
case USE_INPUT:
setMasterPassword(result.password);
Arrays.fill(result.password, '\0');
return true;
case GENERATE_AND_STORE:
String pwd = UUID.randomUUID().toString();
setMasterPassword(pwd.toCharArray());
Preferences.setPreference(CREDENTIALS_MASTER_PASSWORD, pwd);
return true;
case CANCEL:
return false;
}
throw new IllegalArgumentException("Unknown response: " + result.responseType);
}
private boolean onDecryptionFailed() {
if (!isMasterPasswordSet()) {
return false;
}
if (useStoredMasterPassword()) {
return true;
}
return promptForExistingPassword();
}
private boolean promptForExistingPassword() {
String message = OStrings.getString("PASSWORD_ENTER_MESSAGE");
while (true) {
Optional<char[]> result = prompt.getExistingPassword(message);
if (result.isPresent()) {
setEncryptionKey(result.get());
if (checkCanary()) {
return true;
} else {
message = OStrings.getString("PASSWORD_TRY_AGAIN_MESSAGE");
}
} else {
LOGGER.info("User declined to input master password");
return false;
}
}
}
private boolean checkCanary() {
if (!isMasterPasswordSet()) {
return false;
}
try {
String decrypted = textEncryptor.decrypt(Preferences.getPreference(CREDENTIALS_MANAGER_CANARY));
return CREDENTIALS_MANAGER_CANARY.equals(decrypted);
} catch (Exception e) {
return false;
}
}
public enum ResponseType {
USE_INPUT, GENERATE_AND_STORE, CANCEL
}
public static class PasswordSetResult {
public final ResponseType responseType;
public final char[] password;
public PasswordSetResult(ResponseType responseType, char[] password) {
this.responseType = responseType;
this.password = password;
}
}
private static class GuiPasswordPrompt implements IPasswordPrompt {
@Override
public Optional<char[]> getExistingPassword(String message) {
return UIThreadsUtil.returnResultFromSwingThread(() -> {
PasswordEnterDialogController dialog = new PasswordEnterDialogController();
dialog.show(getParentWindow(), message);
return dialog.getResult();
});
}
@Override
public PasswordSetResult createNewPassword() {
return UIThreadsUtil.returnResultFromSwingThread(() -> {
PasswordSetDialogController dialog = new PasswordSetDialogController();
dialog.show(getParentWindow());
return dialog.getResult();
});
}
private Window getParentWindow() {
Window window = FocusManager.getCurrentManager().getActiveWindow();
if (window == null) {
IMainWindow mw = Core.getMainWindow();
if (mw != null) {
window = mw.getApplicationFrame();
}
}
return window;
}
}
}