package au.net.causal.projo.prefs.security;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.jasypt.encryption.pbe.PBEByteEncryptor;
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import au.net.causal.projo.prefs.PreferenceKeyMetadata;
import au.net.causal.projo.prefs.PreferenceNode;
import au.net.causal.projo.prefs.PreferencesException;
/**
* A password source where the master password is stored encrypted in a preference node and a custom encrypter is used to encrypt and descrypt the password.
* When the master password does not exist, it is generated from another password source.
* <p>
*
* Useful for when a native encrypter should be used, but where all other data should be encrypted in a portable way using a master password.
*
* @author prunge
*/
public class StoredEncryptedPasswordSource implements PasswordSource
{
private final PreferenceNode node;
private final String key;
private final PBEByteEncryptor encrypter;
private final Charset passwordEncoding = StandardCharsets.UTF_8;
private final PasswordSource passwordGenerator;
/**
* Creates a <code>StoredEncryptedPasswordSource</code>.
*
* @param node the preference node to store the master password in. Must support storing byte[] data.
* @param key the key to store the master password under.
* @param encrypter the encrypter to use for encrypting and decrypting passwords.
* @param passwordGenerator if a master password does not exist, a new one is generated from this generator.
*
* @throws NullPointerException if any parameter is null.
*/
public StoredEncryptedPasswordSource(PreferenceNode node, String key, PBEByteEncryptor encrypter, PasswordSource passwordGenerator)
{
if (node == null)
throw new NullPointerException("node == null");
if (key == null)
throw new NullPointerException("key == null");
if (encrypter == null)
throw new NullPointerException("encrypter == null");
if (passwordGenerator == null)
throw new NullPointerException("passwordGenerator == null");
this.node = node;
this.key = key;
this.encrypter = encrypter;
this.passwordGenerator = passwordGenerator;
}
@Override
public char[] readPassword(Mode mode)
throws PreferencesException
{
byte[] encryptedPassword = node.getValue(key, new PreferenceKeyMetadata<>(byte[].class));
byte[] passwordBytes;
if (encryptedPassword == null)
{
passwordBytes = generatePassword();
if (passwordBytes == null) //cancelled by user?
return(null);
try
{
encryptedPassword = encrypter.encrypt(passwordBytes);
node.putValue(key, encryptedPassword, new PreferenceKeyMetadata<>(byte[].class));
}
catch (EncryptionOperationNotPossibleException e)
{
throw new PreferencesException("Failed to encrypt password: " + e, e);
}
catch (UserAbortedEnteringPasswordException e)
{
//Could be thrown if the encrypter is a sourced encrypter
//In this case, treat as a cancel
return(null);
}
}
else
{
try
{
passwordBytes = encrypter.decrypt(encryptedPassword);
}
catch (EncryptionOperationNotPossibleException e)
{
throw new PreferencesException("Failed to decrypt password: " + e, e);
}
catch (UserAbortedEnteringPasswordException e)
{
//Could be thrown if the encrypter is a sourced encrypter
//In this case, treat as a cancel
return(null);
}
}
char[] password = bytesToChars(passwordBytes);
return(password);
}
/**
* Generates bytes for a new password by using the password generator.
*
* @return the generated password.
*
* @throws PreferencesException if an error occurs.
*/
protected byte[] generatePassword()
throws PreferencesException
{
char[] newPassword = passwordGenerator.readPassword(Mode.ENCRYPTION);
if (newPassword == null)
return(null);
byte[] newPasswordBytes = charsToBytes(newPassword);
return(newPasswordBytes);
}
private byte[] charsToBytes(char[] c)
{
//TODO technically this is 'insecure' as it creates a string that may be left in memory
//realistically, this doesn't reduce security that much anyway so ignore for now
//but would like to fix later
return(new String(c).getBytes(passwordEncoding));
}
private char[] bytesToChars(byte[] b)
{
//TODO technically this is 'insecure' as it creates a string that may be left in memory
//realistically, this doesn't reduce security that much anyway so ignore for now
//but would like to fix later
return(new String(b, passwordEncoding).toCharArray());
}
}