This article describes a hassle-free approach to handle encryption in Java. The discussed classes are part of a wider library, accessory_java (main repository), which is meant to help programmers deal with different aspects of programming in Java.
I started developing accessory_java while working on a personal project. It was meant to be a container of generic resources which I could eventually use in other applications. Basically, a library including not just useful methods and variables, but also a solid starting point to face virtually any project in Java. Or, in other words, a way to adapt Java to my programming approach, to help me easily overcome the language peculiarities. That initial code has kept growing and evolving until reaching the current stage, which I consider mature, comprehensive and reliable enough.
Main features of accessory_java:
- Built from scratch, by relying on stable functionalities and with the minimum amount of external dependencies possible. At the moment, there is only one external dependency: the MySQL connector. And it can even be ignored in case of not using the database (DB) resources.
- Including virtually no comments or documentation, but with an overall clear structure and quite rigid rules and conventions which are applied in a very systematic and consistent way. Actually, this article and potential future ones might be gradually compensating the aforementioned lacks.
- Main priorities (ordered): friendliness, safety (e.g., default validity checks everywhere or no uncaught exceptions), efficiency/speed, scalability/adaptability.
- Theoretically technology agnostic, although currently mostly focused on MySQL and Linux.
Before using any of the resources of this library, one of the start
overloads in the accessory._ini class has to be called, as shown in the code below.
String app_name = samples.APP;
boolean includes_legacy = false; //When true, it looks for a package called "legacy" which might
//be missing anyway.
//------
//dbs_setup includes the setup to be used by default in all the DBs.
//It can be populated by using one of the parent_ini_db.get_setup_vals methods or via adding valid
//types.CONFIG_DB_SETUP_[*]-value pairs. Any of those values can be overwritten by the specific setups
//in the _ini_db class, which are included when adding a new DB through the populate_db method called
//from populate_all_dbs.
String db_name = samples.PERFECT_NAME;
String username = samples.PERFECT_USERNAME;
String password = samples.PERFECT_PASSWORD;
//--- These two variables work together: if encrypt is true, user has to be non-null and the DB
//credentials will be retrieved from the corresponding encrypted file.
boolean encrypt = true;
String user = samples.USER; //This is the crypto ID associated with the given encryption information,
//which can be generated by calling the method db.encrypt_credentials.
//---
//If encrypt is true, a set of encrypted credentials located in a specific file is expected.
//These credentials can be generated by calling db.encrypt_credentials/db.update_credentials.
//But that call or other related actions (e.g., changing the directory for credentials) can't
//be performed before _ini.start is called. That is, if delay_encryption is set to false,
//dbs_setup would be updated with previously-generated credentials. Alternatively, delay_encryption
//can be true, dbs_setup would include a meaningless placeholder and the credentials would be updated
//after _ini.start is called.
boolean delay_encryption = true;
HashMap<String, Object> dbs_setup = null;
if (samples.USE_DB)
{
String setup = null; //This variable can be null (i.e., default setup) or equal to the given DB
//(i.e., corresponding types.CONFIG_[*] constant).
String host = null; //A null value indicates that the default host will be used
//(e.g., "localhost" in MySQL).
if (encrypt && !delay_encryption)
{
dbs_setup = parent_ini_db.get_setup_vals(db_name, setup, user, host, encrypt);
}
else dbs_setup = parent_ini_db.get_setup_vals(db_name, setup, username, password, host);
}
//------
_ini.start(app_name, includes_legacy, dbs_setup);
if (encrypt && delay_encryption)
{
paths.update_dir(paths.DIR_CRYPTO, samples.PERFECT_LOCATION);
paths.update_dir(paths.DIR_CREDENTIALS, samples.PERFECT_LOCATION);
db.update_credentials(db_crypto.SOURCE, user, username, password);
}
public abstract class samples
{
public static final String APP = "whatevs";
public static final String ID = "whichevs";
public static final String USER = "whoevs";
public static final String PLACEHOLDER = "wherevs";
//Set this variable to true only if there is a valid DB setup in place
//(i.e., MySQL connector, valid DB name and valid credentials).
public static final boolean USE_DB = false;
//--- I would input my own values where these constants are used if I were you.
public static final String PERFECT_NAME = PLACEHOLDER;
public static final String PERFECT_LOCATION = PLACEHOLDER;
public static final String PERFECT_USERNAME = PLACEHOLDER;
public static final String PERFECT_PASSWORD = PLACEHOLDER;
//---
public static void print_error(String sample_id_) { print_message(sample_id_, null, false); }
public static void print_message(String sample_id_, String message_, boolean is_ok_)
{
String message = sample_id_ + misc.SEPARATOR_CONTENT;
if (is_ok_) message += message_;
else message += "ERROR";
generic.to_screen(message);
}
public static void print_messages(String sample_id_, String[] messages_)
{
String message = sample_id_;
for (String message2: messages_) { message += misc.SEPARATOR_CONTENT + message2; }
generic.to_screen(message);
}
public static void print_end() { generic.to_screen("sample code completed successfully"); }
}
- Symmetric encryption.
- Reliance on the Base64 class for binary-to-text encoding.
- Only storing/retrieving the following encryption information/cipher setup: identification keyword (ID), cipher algorithm (the transformation argument of
Cipher.getInstance
), secret key and initialization vector (IV). - The encryption information is being reset with each new encryption.
- Two storage formats: text files (in a directory which can be changed via
paths.update_dir(paths.DIR_CRYPTO, "dir")
) and a DB table.
There is one main class (crypto
) which profusely uses the remaining accessory_java resources, stored in either more specific (e.g., db_crypto
) or more generic (e.g., strings
) classes. Although crypto is a non-static class, all its public methods are static. Its constructors are private and only expect the most basic information.
private crypto(String in_, String id_) { instantiate(in_, id_, true); }
private crypto(String in_, HashMap<String, Object> params_) { instantiate(in_, params_, false); }
The main configuration of this class, the management of the parameters defining the way in which the encryption/decryption processes will happen, relies on the default accessory_java approach. That is, it is done through the config
class, although simplified via default values and methods in the crypto
class.
public static final String DEFAULT_ID = _ID;
public static final String DEFAULT_STORAGE = CONFIG_STORAGE_FILES;
public static final String DEFAULT_ALGO_CIPHER = "AES/CTR/NoPadding";
public static final String DEFAULT_ALGO_KEY = "AES";
public static final String DEFAULT_FILES_EXTENSION = strings.DEFAULT;
public static final boolean DEFAULT_LOG_INFO = false;
private static final boolean DEFAULT_USE_ID = true;
private static boolean _is_ok_last = false;
private boolean _is_ok = false;
private String _id = strings.DEFAULT;
private String _in = strings.DEFAULT;
private String _out = strings.DEFAULT;
private Cipher _cipher_enc = null;
private Cipher _cipher_dec = null;
private String _algo_cipher = null;
private String _algo_key = DEFAULT_ALGO_KEY;
private SecretKey _key = null;
private byte[] _iv = null;
public String serialise() { return toString(); }
public String toString() { return strings.DEFAULT; }
public boolean is_ok() { return _is_ok; }
public static boolean is_ok_last() { return _is_ok_last; }
public static boolean update_algo_cipher(String algo_cipher_)
{
return
(
strings.is_ok(algo_cipher_) ? config.update_crypto
(
CONFIG_ALGO_CIPHER, algo_cipher_
)
: false
);
}
public static String get_algo_cipher()
{
return (String)config.get_crypto(CONFIG_ALGO_CIPHER);
}
public static String get_algo_cipher_or_default()
{
String output = get_algo_cipher();
return (strings.is_ok(output) ? output : DEFAULT_ALGO_CIPHER);
}
public static boolean update_algo_key(String algo_key_)
{
return
(
strings.is_ok(algo_key_) ? config.update_crypto
(
CONFIG_ALGO_KEY, algo_key_
)
: false
);
}
public static String get_algo_key()
{
return (String)config.get_crypto(CONFIG_ALGO_KEY);
}
public static String get_algo_key_or_default()
{
String output = get_algo_key();
return (strings.is_ok(output) ? output : DEFAULT_ALGO_KEY);
}
public static void store_in_files()
{
config.update_crypto(CONFIG_STORAGE, CONFIG_STORAGE_FILES);
}
public static void store_in_db()
{
config.update_crypto(CONFIG_STORAGE, CONFIG_STORAGE_DB);
}
public static boolean is_stored_in_files()
{
return strings.are_equal
(
_types.check_type
(
(String)config.get_crypto(CONFIG_STORAGE), CONFIG_STORAGE
),
CONFIG_STORAGE_FILES
);
}
public static boolean update_files_extension(String files_extension_)
{
return
(
strings.is_ok(files_extension_) ? config.update_crypto
(
CONFIG_FILES_EXTENSION, files_extension_
)
: false
);
}
public static String get_files_extension()
{
return (String)config.get_crypto(CONFIG_FILES_EXTENSION);
}
public static void update_log_info(boolean log_info_)
{
config.update_crypto(CONFIG_LOG_INFO, log_info_);
}
public static boolean get_log_info()
{
return config.get_crypto_boolean(CONFIG_LOG_INFO);
}
The main public methods allow to encrypt/decrypt strings, text files, arrays and HashMap<String, String>
variables.
public static String[] encrypt_file(String path_, String id_)
{
return encrypt_decrypt_file(path_, id_, true);
}
public static String[] decrypt_file(String path_, String id_)
{
return encrypt_decrypt_file(path_, id_, false);
}
public static String[] encrypt(String[] inputs_, String id_)
{
return encrypt_decrypt(inputs_, id_, true);
}
public static String[] decrypt(String[] inputs_, String id_)
{
return encrypt_decrypt(inputs_, id_, false);
}
public static HashMap<String, String> encrypt
(
HashMap<String, String> inputs_, String id_, boolean keys_too_
)
{ return encrypt_decrypt(inputs_, id_, keys_too_, true); }
public static HashMap<String, String> decrypt
(
HashMap<String, String> inputs_, String id_, boolean keys_too_
)
{ return encrypt_decrypt(inputs_, id_, keys_too_, false); }
public static String encrypt(String input_, String id_)
{
return encrypt_decrypt(input_, id_, true, true);
}
public static String decrypt(String input_, String id_)
{
return encrypt_decrypt(input_, id_, false, true);
}
public static String encrypt(String input_, HashMap<String, Object> params_)
{
return encrypt_decrypt(input_, params_, true, false);
}
public static String decrypt(String input_, HashMap<String, Object> params_)
{
return encrypt_decrypt(input_, params_, false, false);
}
There are two main private methods performing the encryption/decryption actions: encrypt
and decrypt
.
private void encrypt() { encrypt(DEFAULT_USE_ID); }
private void encrypt(boolean use_id_)
{
try
{
if (!encrypt_internal(use_id_)) return;
_out = strings.from_bytes_base64(_cipher_enc.doFinal(_in.getBytes()));
update_is_ok(true);
}
catch (Exception e) { manage_error(ERROR_ENCRYPT, e); }
}
private void decrypt() { decrypt(DEFAULT_USE_ID); }
private void decrypt(boolean use_id_)
{
try
{
if (!decrypt_internal(use_id_)) return;
byte[] temp = strings.to_bytes_base64(_in);
if (temp == null) return;
_out = new String(_cipher_dec.doFinal(temp));
update_is_ok(true);
}
catch (Exception e) { manage_error(ERROR_DECRYPT, e); }
}
Their algorithms follow these steps:
- Checking that all the required input information is valid via the
start_enc_dec
method.
private boolean start_enc_dec(boolean is_enc_, boolean use_id_)
{
update_is_ok(false);
if (!use_id_ || !is_ok_common(_in, _id, false, true)) return !use_id_;
if (is_enc_)
{
_algo_cipher = get_algo_cipher_or_default();
if (!algo_is_ok(_algo_cipher)) return manage_error(ERROR_ALGO_CIPHER);
_algo_key = get_algo_key_or_default();
if (!algo_is_ok(_algo_key)) return manage_error(ERROR_ALGO_KEY);
}
else
{
HashMap<String, Object> items = retrieve(_id);
if (items == null) return manage_error(ERROR_RETRIEVE);
_algo_cipher = get_algo_cipher(items);
if (!algo_is_ok(_algo_cipher)) return manage_error(ERROR_RETRIEVE_ALGO_CIPHER);
_key = get_key(items);
if (!key_is_ok(_key)) return manage_error(ERROR_RETRIEVE_KEY);
_iv = get_iv(items);
if (!iv_is_ok(_iv)) return manage_error(ERROR_RETRIEVE_IV);
}
return true;
}
- Updating the corresponding global
Cipher
variable and eventually storing all the needed information viaupdate_cipher_enc
/update_cipher_dec
.
private boolean update_cipher_enc(boolean use_id_)
{
if (_cipher_enc != null) return true;
boolean is_ok = false;
_key = null;
_iv = null;
try
{
if (use_id_) _key = get_key();
if (!key_is_ok(_key))
{
manage_error(ERROR_KEY);
return is_ok;
}
_cipher_enc = Cipher.getInstance(_algo_cipher);
_cipher_enc.init(Cipher.ENCRYPT_MODE, _key, new SecureRandom());
if (use_id_) _iv = _cipher_enc.getIV();
if (!iv_is_ok(_iv))
{
manage_error(ERROR_IV);
return is_ok;
}
if (store())
{
is_ok = true;
if (get_log_info()) log_encryption_info();
}
}
catch (Exception e) { manage_error(ERROR_ENCRYPT, e); }
return is_ok;
}
private boolean decrypt_internal(boolean use_id_)
{
return (start_enc_dec(false, use_id_) && update_cipher_dec());
}
private boolean update_cipher_dec()
{
if (_cipher_dec != null) return true;
boolean is_ok = false;
try
{
_cipher_dec = Cipher.getInstance(_algo_cipher);
_cipher_dec.init(Cipher.DECRYPT_MODE, _key, new IvParameterSpec(_iv));
is_ok = true;
}
catch (Exception e) { manage_error(ERROR_DECRYPT, e); }
return is_ok;
}
- Generating the final output.
The cipher algorithm is a simple string and, as such, its storage/retrieval is trivial. The other two variables are trickier.
The IV is a byte array and, consequently, when dealing with DBs, it has to be firstly converted to/from a string. This is done by calling to_bytes
and from_bytes
in the strings
class.
private static String from_bytes(byte[] input_, boolean use_base64_)
{
String output = DEFAULT;
if (!arrays.is_ok(input_)) return output;
if (use_base64_) output = Base64.getEncoder().encodeToString(input_);
else output = new String(input_, get_encoding());
return output;
}
private static byte[] to_bytes(String input_, boolean use_base64_)
{
byte[] output = null;
if (!strings.is_ok(input_)) return output;
if (use_base64_)
{
try { output = Base64.getDecoder().decode(input_); }
catch (Exception e) { output = null; }
}
else output = input_.getBytes();
return output;
}
In case of relying on files, the whole process is performed by the specific Java classes used inside the corresponding methods from the io
class: bytes_to_file
and file_to_bytes
.
public static void bytes_to_file(String path_, byte[] vals_)
{
method_start();
if (!strings.is_ok(path_)) return;
if (!arrays.is_ok(vals_))
{
empty_file(path_);
return;
}
try (FileOutputStream stream = new FileOutputStream(new File(path_))) { stream.write(vals_); }
catch (Exception e) { manage_error_io(ERROR_WRITE, e, path_); }
method_end();
}
public static byte[] file_to_bytes(String path_)
{
method_start();
byte[] output = null;
if (!paths.file_exists(path_)) return output;
try { output = Files.readAllBytes(Paths.get(path_)); }
catch (Exception e) { manage_error_io(ERROR_READ, e, path_); }
method_end();
return output;
}
In the case of the secret key, the conversion is from/to an object, also by relying on different io
/strings
methods on account of the storage format. More specifically, the used methods are object_to_file
/file_to_object
(io
) and from_object
/to_object
(strings
).
public static void object_to_file(String path_, Object vals_)
{
method_start();
if (!strings.is_ok(path_)) return;
if (vals_ == null)
{
empty_file(path_);
return;
}
try
(
ObjectOutputStream stream = new ObjectOutputStream
(
new FileOutputStream(path_)
)
)
{ stream.writeObject(vals_); }
catch (Exception e) { manage_error_io(ERROR_WRITE, e, path_); }
method_end();
}
public static Object file_to_object(String path_)
{
method_start();
Object output = null;
if (!paths.file_exists(path_)) return output;
try
(
ObjectInputStream stream = new ObjectInputStream
(
new FileInputStream(path_)
)
)
{ output = stream.readObject(); }
catch (Exception e) { manage_error_io(ERROR_WRITE, e, path_); }
method_end();
return output;
}
public static String from_object(Object input_)
{
String output = DEFAULT;
if (input_ == null) return output;
try (ByteArrayOutputStream array = new ByteArrayOutputStream())
{
try (ObjectOutputStream stream = new ObjectOutputStream(array))
{
stream.writeObject(input_);
output = from_bytes_base64(array.toByteArray());
}
catch (Exception e) { output = DEFAULT; }
}
catch (Exception e) { output = DEFAULT; }
return output;
}
public static Object to_object(String input_)
{
Object output = null;
byte[] bytes = to_bytes_base64(input_);
if (bytes == null) return output;
try
(
ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(bytes))
)
{ output = stream.readObject(); }
catch (Exception e) { output = null; }
return output;
}
For the DB scenario, all the storage/retrieval actions are performed by methods inside the db_crypto
class.
The management of credentials (actions performed by the classes credentials
and db_credentials
) is evidently closely related to encryption. This part adds a new variable on top of the crypto
's ID: the user. To encrypt credentials, you have to provide both an ID (used in crypto
) and a user (used in credentials
).
private static boolean encrypt_username_password_internal
(
String id_, String user_, String username_, String password_, String where_
)
{
boolean output = false;
if
(
!strings.is_ok(username_) || password_ == null ||
(where_.equals(WHERE_DB) && !db.table_exists(db_credentials.SOURCE))
)
{ return output; }
HashMap<String, String> id_user = get_id_user(id_, user_);
String id = id_user.get(ID);
String user = id_user.get(USER);
String encryption_id = get_encryption_id(id, user);
String[] temp = crypto.encrypt(new String[] { username_, password_ }, encryption_id);
if (!arrays.is_ok(temp)) return output;
HashMap<String, String> vals = new HashMap<String, String>();
vals.put(USERNAME, temp[0]);
vals.put(PASSWORD, temp[1]);
if (where_.equals(WHERE_FILE)) output = encrypt_username_password_file_store(id, user, vals);
else if (where_.equals(WHERE_DB)) output = encrypt_username_password_db_store(id, user, vals);
return output;
}
Another encryption-related class is db_info
, which manages a DB table storing key-value pairs which can be encrypted.
private static Object adapt_vals
(
HashMap<String, String> vals_, boolean is_enc_, boolean encrypt_, boolean add_existing_
)
{
Object outputs = null;
boolean encrypt = (encrypt_ && is_enc_);
if (add_existing_)
{
if (encrypt)
{
outputs = adapt_vals_internal
(
crypto.encrypt
(
add_existing(vals_, get_encrypted(true)), ENCRYPTION_ID, true
),
true, outputs
);
outputs = adapt_vals_internal(get_decrypted(), false, outputs);
truncate();
}
else outputs = adapt_vals_internal(vals_, is_enc_, outputs);
}
else
{
outputs = adapt_vals_internal
(
(encrypt ? crypto.encrypt(vals_, ENCRYPTION_ID, true) :
new HashMap<String, String>(vals_)), is_enc_, outputs
);
}
return outputs;
}
public class samples_encryption
{
public static void main(String[] args) { run_encryption(); }
public static void run_encryption()
{
samples_start.run_start();
paths.update_dir(paths.DIR_CRYPTO, samples.PERFECT_LOCATION);
//---
String sample_id = "default";
if (!run_simple(sample_id)) return;
//---
//---
sample_id = "DESede";
crypto.update_algo_cipher("DESede/CBC/PKCS5Padding");
crypto.update_algo_key("DESede");
if (!run_simple(sample_id)) return;
//---
//---
sample_id = "credentials";
paths.update_dir(paths.DIR_CREDENTIALS, samples.PERFECT_LOCATION);
String username = samples.PERFECT_USERNAME;
String password = samples.PERFECT_PASSWORD;
if
(
!credentials.encrypt_username_password_file
(
samples.ID, samples.USER, username, password
)
)
{
samples.print_error(sample_id);
return;
}
HashMap<String, String> temp = credentials.get_username_password_file
(
samples.ID, samples.USER, true
);
if (!arrays.is_ok(temp))
{
samples.print_error(sample_id);
return;
}
String[] messages = new String[]
{
"path: " + credentials.get_path(samples.ID, samples.USER, true),
"decrypted: " + strings.to_string(temp)
};
samples.print_messages(sample_id, messages);
//---
if (samples.USE_DB) run_encryption_db();
samples.print_end();
}
private static void run_encryption_db()
{
//---
String sample_id = "db_crypto";
db.create_table(db_crypto.get_source(), true);
crypto.store_in_db();
if (!run_simple(sample_id)) return;
crypto.store_in_files();
//---
//---
sample_id = "db_info";
db.create_table(db_info.get_source(), true);
HashMap<String, String> input = new HashMap<String, String>();
input.put("enc1", "1");
input.put("enc2", "2");
if (!db_info.add(input, true, true, true))
{
samples.print_error(sample_id);
return;
}
input = new HashMap<String, String>();
input.put("plain1", "1");
input.put("plain2", "2");
if (!db_info.add(input, false, false, false))
{
samples.print_error(sample_id);
return;
}
HashMap<String, String> encrypted = db_info.get_all(false);
if (!arrays.is_ok(encrypted))
{
samples.print_error(sample_id);
return;
}
HashMap<String, String> unencrypted = db_info.get_all(true);
if (!arrays.is_ok(unencrypted))
{
samples.print_error(sample_id);
return;
}
String[] messages = new String[]
{
"encrypted: " + strings.to_string(encrypted),
"unencrypted: " + strings.to_string(unencrypted)
};
samples.print_messages(sample_id, messages);
//---
}
private static boolean run_simple(String sample_id_)
{
boolean is_ok = false;
String encrypted = crypto.encrypt(samples.PLACEHOLDER, samples.ID);
String decrypted = crypto.decrypt(encrypted, samples.ID);
String message = null;
if (samples.PLACEHOLDER.equals(decrypted))
{
is_ok = true;
message = (samples.PLACEHOLDER + misc.SEPARATOR_CONTENT + encrypted);
}
samples.print_message(sample_id_, message, is_ok);
return is_ok;
}
}