Skip to content

Instantly share code, notes, and snippets.

@varocarbas
Created May 13, 2023 07:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save varocarbas/c0edd30b4e731780794b25fc9f0e3e5e to your computer and use it in GitHub Desktop.
Save varocarbas/c0edd30b4e731780794b25fc9f0e3e5e to your computer and use it in GitHub Desktop.
Encryption -- accessory_java

Encryption -- accessory_java

Introduction

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.

accessory_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);
}

permalink

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"); }
}

permalink

Basic encryption ideas

  • 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.

Code overview

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); }

permalink

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); 
}

permalink

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); 
}

permalink

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); }
}

permalink

Their algorithms follow these steps:

  1. 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;
}

permalink

  1. Updating the corresponding global Cipher variable and eventually storing all the needed information via update_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;
}

permalink

  1. Generating the final output.

Storage

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; 
}

permalink

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;
}

permalink

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;
}

permalink

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;
}

permalink

For the DB scenario, all the storage/retrieval actions are performed by methods inside the db_crypto class.

Related classes

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;
}

permalink

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;
}

permalink

Sample code

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;
    }
}

permalink

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment