Tuesday, May 24, 2022

Java Tutorial: ResourceBundle

Chapters

Introduction

ResourceBundle enables us to pack and load locale-specific data in a persistent file. By using resource bundles, we can assign a name to our application's elements, such as GUI elements, with multiple-locale.

Also, updating locale-specific data in a resource bundle is much more easier than hardcoding it in our code. Thus, it's recommended to use resource bundles for storing names such as names of GUI elements. resource bundle has a hierarchy.

We define a base name to our base resource. Then, we can create a family of resources. For naming convention, Each bundle name in a resource bundle family, except for the base resource, should copy the name of the base resource with an abbreviation of language code. We may append country code if there's a language code in the name. We may append platform code if the country and language code are present in the name.

User underscore to separate base name and codes. For example:
//base name
MyResources

//sibling name with language code
MyResources_en

//sibling name with language code
//and country code
MyResources_en_US

//sibling name with language code,
//country code and platform code
MyResources_en_US_UNIX
For language code reference, you may look at this article. For country code reference, you may look at this article.

Codes in the file name are case-insensitive
e.g. MyResources_en_uS
However, by convention, lowercase language code; uppercase country and platform codes are preferrable. In a list resource file name, country and platform codes should in uppercase.

Resource bundles store data in the form of case-sensitive key-value pairs. Resource bundle is categorized into two types: PropertyResourceBundle and ListResourceBundle.

PropertyResourceBundle

PropertyResourceBundle is a concrete subclass of ResourceBundle that manages resources for a locale using a set of static strings from a property file. The file extension of a property file is .properties

These are the elements that we can write to our properties file.
# Labels
HiLabel = Hi

! Buttons
abortButton Abort
aboutButton: About
"#" and "!" denotes a comment. Characters after these symbols are ignored. Next, key-value pairs can be separated by whitespace ( ), colon (:) or equal (=) symbols.

API Note:
PropertyResourceBundle can be constructed either from an InputStream or a Reader, which represents a property file. Constructing a PropertyResourceBundle instance from an InputStream requires that the input stream be encoded in UTF-8.

By default, if a MalformedInputException or an UnmappableCharacterException occurs on reading the input stream, then the PropertyResourceBundle instance resets to the state before the exception, re-reads the input stream in ISO-8859-1, and continues reading.

If the system property java.util.PropertyResourceBundle.encoding is set to either "ISO-8859-1" or "UTF-8", the input stream is solely read in that encoding, and throws the exception if it encounters an invalid sequence.

If "ISO-8859-1" is specified, characters that cannot be represented in ISO-8859-1 encoding must be represented by Unicode Escapes as defined in section 3.3 of The Java Language Specification whereas the other constructor which takes a Reader does not have that limitation.

Other encoding values are ignored for this system property. The system property is read and evaluated when initializing this class. Changing or removing the property has no effect after the initialization.

ListResourceBundle

ListResourceBundle is an abstract subclass of ResourceBundle that manages resources for a locale in a convenient and easy to use list. File extension of this resource bundle is .java

This is how we create key-value pairs in this resource bundle.
import java.util.ListResourceBundle;
import java.time.LocalDate;

public class ListResource extends ListResourceBundle{

  @Override
  protected Object[][] getContents(){
    return new Object[][]{
      {"item1", "Toy Car"},
      {"release-date", LocalDate.of(2021, 10, 20)},
      {"tags", new String[]{"Automotive", "Toys"}}
    };
  }
}
Remember that keys are String type and values can be of any type.

Using ResourceBundle

Now we know how to create a resource bundle. Let's try using a resource bundle in our program. First off, let's create a properties files. Create a file and name it "PropertyResources.properties" and write this in the file:
# Labels
HiLabel: Hi

# Buttons
abortButton: Abort
aboutButton: About
We're going to create a family bundle so let's create another bundle and name it "PropertyResource_en_GB.properties" and write this in the file:
# Labels
HiLabel = Hello

# Buttons
okButton Okay
aboutButton: About
Next, let's create a code that will access our bundle.
import java.util.Locale;
import java.util.ResourceBundle;

public class SampleClass{

  public static void main(String[] args){
    ResourceBundle rb =
    ResourceBundle.getBundle
    ("MyResources", Locale.UK);
    //If your resource is in a package
    //("Package-Name.MyResources", Locale.UK);
    
    for(String key : rb.keySet()){
      System.out.println(key + " | " + 
      rb.getString(key));
    }
  }
}

Result
HiLabel | Hello
okButton | Okay
aboutButton | About
This code above also works with list resources which have .java file extension. Remember to compile your list resources first because java look for .class file of your list resources. In the getBundle method, we just need to put the base name of our resource bundle. We don't need to add file extensions or codes like country code.

One of the advantages of list resource to a property resource is that list resource contains values of any type whereas property resource only contain values of String type. We can use handleGetObject(String key) if we want to convert values to Object type that can be cast to other object types. handleGetObject(String key) gets an object for the given key.

handleGetObject(String key) has protected access in ResourceBundle. We need to use ListResourceBundle or PropertiesResourceBundle reference in order to access this method. For example:
ListResourceBundle prb = 
(ListResourceBundle)ResourceBundle.getBundle
("MyResources", Locale.US);
...
prb.handleGetObject(key);
ListResourceBundle has getContents() method. This method returns an array in which each item is a pair of objects in an Object array. Resource bundles can be deployed in different ways such as deploying them together with an application. You can read them in the documentation.

When java looks up for a resource bundle, it looks for a specific bundle with specified locale. If there's no match, java will look for a bundle in the family with a locale that is equivalent to our platform's default locale which is returned by invoking Locale.getDefault.

If there's still no match, java will look for default resource bundle or base resource bundle. If there's still no match, an exception will be thrown. If a property resource and list resource have the same name and in the same package or directory, java will prioritize the list resource.

Inheritance

Resource bundle hierarchy implements inheritance. A bundle with more specific name in the family inherits key-value pairs from a bundle with less specific name. For example, Assume we have three resource bundles in the same family and the base name of the family is "MyResources":
//base 
MyResources.properties
okButton = Ok

//specific
MyResources_en.properties
acceptButton = accept

//more specific
MyResources_en_GB.properties
confirmButton = confirm
When we use MyResources_en_GB.properties in our program, this resource bundle will inherit the values in MyResources.properties and MyResources_en.properties.

However, if the bundle with more specific name has equivalent key-value pairs to its less specific counterparts, the key-value pairs in the bundle with more specific name override the equivalent key-value pairs in the bundle with less specific name.

bundles with equivalent specificity don't inherit key-value pairs. For example:
...
MyResources_en_US.properties
confirmButton = confirm

MyResources_en_GB.properties
commitButton = commit
When we use MyResources_en_GB.properties, it won't inherit the key-value pair in MyResources_en_US.properties. Also, list resource and property resource have different hierarchies. Thus, a list resource can't inherit any key-value pairs from property resource and vice-versa.

ResourceBundle.Control

ResourceBundle.Control defines a set of callback methods that are invoked by the ResourceBundle.getBundle factory methods during the bundle loading process.

In other words, a ResourceBundle.Control collaborates with the factory methods for loading resource bundles. The default implementation of the callback methods provides the information necessary for the factory methods to perform the default behavior.

We can override some methods of ResourceBundle.Control to change some behaviors during bundle loading process. For example, we can override getCandidateLocales method to filter candidate locales.
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;

public class SampleClass{

  public static void main(String[] args){
    ResourceBundle rb = 
    ResourceBundle.getBundle
    ("MyResources", Locale.FRANCE, 
     new CustomResourceControl());
     
    for(String key : rb.keySet()){
      System.out.println(key + " | " + 
      rb.getString(key));
    }
     
  }
}

class CustomResourceControl extends 
ResourceBundle.Control{
  
  @Override
  public List<Locale> 
  getCandidateLocales(String s, Locale locale) {
    if(locale.getCountry().equals("US") || 
       locale.getCountry().equals("UK")){
       return super.getCandidateLocales(s, locale);
    }
    else{
      System.err.println
      ("Warning: Locale should be US or UK.");
      System.err.println("Locale has been "
      +"automatically set to Locale.ROOT");
      return Arrays.asList(Locale.ROOT);
      System.out.println();
    }
  }
}

Result
Warning: Locale should be US or UK.
Locale has been automatically set to Locale.ROOT

Thursday, May 19, 2022

Java Tutorial: Create Loggers using java.util.logging Package

Chapters

Overview

Logging is an essential part of developing an application. It lets us diagnose applications with ease. There are thid-party loggers that we can export to java such as Log4j2. In this tutorial, we're gonna use java.util.logging package to create loggers.

Creating a Logger

Creating a logger is easy. We just need to invoke getLogger method from Logger class. Take a look at this example.
import java.util.logging.*;

public class SampleClass{

  //a Logger instance is preferrable to be referenced
  //to a global variable to prevent garbage collector
  //from collecting the logger's instance
  //in unexpected manner
  static Logger logger =
  Logger.getLogger(SampleClass.class.getName());
  
  public static void main(String[] args){
    
    try{
      int num = 1/0;
    }
    catch(ArithmeticException e){
      logger.log(Level.SEVERE, "Divisor is 0!", e);
    }
    
  }
}

Result
May 17, 2022 4:40:44 PM SampleClass main
SEVERE: Divisor is 0!
java.lang.ArithmeticException: / zero
          at SampleClass.main(SampleClass.java:10)
getLogger(String name) creates a logger with the specified name. If the logger with the specified name is already created, this method gets that logger instead of creating a new one.

Loggers are normally named, using a hierarchical dot-separated namespace. Logger names can be arbitrary strings, but they should normally be based on the package name or class name of the logged component, such as java.net or javax.swing.

log method logs a message. When the message is logged, it will be passed to logger's handler and the handler. What is a handler? handlers process the logged messages and output the messages to a console, file, etc. We will learn handlers later. In the example above, we didn't specify any handler to our logger. Thus, our logger uses the default handler which outputs messages to a console.

In the example above, log(Level level, String msg, Throwable thrown) method logged the date and time where the logging happened, the source class or the class where the log happened, the method name where the log happened, level which we will learn later, the message and the exception that we put in the log method.

log method has other variants: logp and logrb methods. logp or "log precise" method is just like log method but the source class and source method needs to be specified explicitly.

logrb method is just like logp method but this method accepts a ResourceBundle. Resource bundles contain locale-specific objects.

Some methods use MessageFormat style to format parameters in the params or param argument. For example, let's invoke this method: log(Level level, String msg, Object[] params)

logger.log(Level.SEVERE, "Causes: {0}, {1}, {2}", new String[]{"Divisor is 0!", "Illegal Division", "Undefined"});

If you put this code in the example above, the result would be:
May 17, 2022 4:40:44 PM SampleClass main
SEVERE: Causes: Divisor is 0!, Illegal Division, Undefined


If we just casually trying out some things and don't wanna create a logger, we can use the global logger. This logger is designed for testing purposes. To get the global logger, we can invoke Logger.getGlobal() or use GLOBAL_LOGGER_NAME field. For example:
//via method
Logger logger = Logger.getGlobal();

//via constant
Logger logger = 
logger.getLogger(Logger.GLOBAL_LOGGER_NAME);

//via String literal
Logger logger = 
logger.getLogger("global");
"global" is the name the refers to the global logger. In production, it's recommended to create logger per top-level class and add the class names to the loggers' name.

Levels

The Level class defines a set of standard logging levels that can be used to control logging output. These are the levels in descending order:
  • SEVERE (highest value)
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST (lowest value)
To get logger's level, invoke getLevel method from Logger class. To set logger's level, invoke setLevel(Level newLevel) from Logger class. Additionally, two other constants, apart from the constants above, can be used in setLevel method. These are:
  • OFF
  • ALL
Level.OFF means that all logging levels from FINEST to SEVERE are not loggable. Level.ALL is the opposite of Level.OFF. When we set a level to be loggable by invoking setLevel, all levels on top of the level that has been set will be loggable. For example, if I invoke setLevel like this: setLevel(Level.INFO). Then WARNING and SEVERE are also going to be loggable.

Note that loggers and handlers have their own levels. The level of a logger determines if a log can be sent to the handlers. The level of a handler determines if the log that has been received will be exported to a console, file, etc.

If the return value of setLevel method is null, it means that the handler or the logger will use the level of its parent logger. If a level is loggable, the handler or log accepts logs with the specified level. Otherwise, the log will be ignored.

According to the documentation, FINEST, FINER and FINE are used to log messages regarding tracing messages such as debug point or some kind of marker in message form. These levels are pretty optional and can be hidden.

CONFIG is used to log messages regarding configurations, INFO is used to log informational messages, WARNING is used to log potential problem and SEVERE is used to log serious failure such as exceptions. These levels are pretty important and should be watched closely especially the SEVERE level.

Logger Hierarchy

When we create a logger, that logger wil be added to the LogManager namespace. Java registers all created loggers to this namespace. However, there's a logger that can't be registered in the namespace and that logger is an anonymous logger.

One of the disadvantages of a logger that is not registered in the namespace is that that logger won't receive some checks like security checks which are done in the namespace. If you intend to bypass some checks then you may need to use anonymous logger to achieve your specific goal. To create an anonymous logger, invoke getAnonymousLogger method from Logger class.

Now, let's talk about logger hierarchy. When we create a logger, the logger will be automatically become a childen of root logger. Root logger is a logger that is closest to the LogManager. We will learn about LogManager later. The default level of root logger is INFO if the "Default global logging level" attribute in the configuration file of LogManager is not specified.

Root logger was the reason we could print logs on the console. By default, a newly created logger doesn't have a handler. If a logger doesn't have a specified handler, it will pass the log to the handler of its parent logger. Take note that if the logger and its parent have handlers, the log will be passed to the handlers of the logger and its parent.

If we want a log to only be passed to our logger, invoke setUseParentHandlers method and set its argument to false. In this way, the log won't be passed to the parent of our logger.

To gain access to the root logger, invoke Logger.getLogger() with "" argument. For example:

Logger logger = Logger.getLogger("");

Root logger doesn't have a display name. That's why when we invoke getName method in root logger the result is blank. If we want to get the parent logger of a logger, invoke getParent method for Logger class. If the return value of the method is null, the logger doesn't have a parent logger or the logger is the root logger because root logger doesn't have a parent logger.

Take note that even anonymous logger is not part of the logging namespace, its parent is still the root logger. If we want to set a parent logger to another logger, invoke setParent method from Logger class. This example demonstrates getParent and setParent methods.
import java.util.logging.*;

public class SampleClass{
  static Logger logger = 
  Logger.getLogger(SampleClass.class.getName());
  
  public static void main(String[] args){
    
    System.out.println("Name: " + logger.getName());
    System.out.println("Parent: " + 
    logger.getParent());
    System.out.println();
    
    new SampleClass();
  }
  
  SampleClass(){
    new Inner();
  }
  
  class Inner{
    Logger logger =
    Logger.getLogger(Inner.class.getName());
    
    Inner(){
       System.out.println("Name: " + logger.getName());
       System.out.println("Parent: " + logger.getParent());
       System.out.println();
       
       System.out.println("Change Parent...");
       logger.setParent(SampleClass.logger);
       System.out.println("Parent: " + 
       logger.getParent().getName());
    }
  }
}

Result(may vary)
Name: SampleClass
Parent: java.util.logging.LogManager$RootLogger@194...

Name: SampleClass$Inner
Parent: java.util.logging.LogManager$RootLogger@194...

Change Parent...
Parent: SampleClass

If our class is in a package, the return value of getName method would be: package-name.class-name. Take note that the LogManager namespace manages the hierarchy of registered loggers.

Thus, the namespace will be updated if we change the hierarchy of the registered loggers. In the namespace, Loggers are organized into a naming hierarchy based on their dot separated names. Thus "a.b.c" is a child of "a.b", but "a.b1" and a.b2" are peers.

Configuring LogManager

LogManager is used to maintain a set of shared state about Loggers and log services. In every JVM, there is a single global LogManager object that maintains every logger and log service in our application.

We can get that object by invoking LogManager.getLogManager method. The LogManager object is created during class initialization and cannot subsequently be changed. One of the important functionalities of LogManager is that it loads a configuration file. During LogManager initialization, a configuration file sets up our logger framework based on the content of the file.

A configuration file can be in a class file or properties file. To explicitly assign a configuration file to our logger framework, we can type these commands on our terminal/cmd:
load class file
java -Djava.util.logging.config.class="class path"

load properties file
java -Djava.util.logging.config.file="file path"
If a configuration file is not explicity set. Java will use the default logging.properties file. In java 8 and below, this file is located in $JAVA_HOME/jre/lib/ path. In java 9 and above, this file is located in $JAVA_HOME/conf/. $JAVA_HOME or %JAVA_HOME% (Windows) is a system variable that refers to the path where our JDK directory is located.

Now, let's create our own configuration file. First off, let's start with file configuration. Put this text in your custom properties file:

handlers = java.util.logging.ConsoleHandler

#ConsoleHandler initial configuration
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

#initialize new logger
initLogger.level = CONFIG


I'll explain the content of the properties file later. For now, create a file and name it as "MyConfig.properties" and put the file in a directory that you like. Then compile this java code.
import java.util.logging.*;

public class LogTest{

  static Logger rootLogger =
  Logger.getLogger("");
  
  static Logger initLogger =
  Logger.getLogger("initLogger");
  
  static Logger globalLogger =
  Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
  
  public static void main(String[] args){
    System.out.println("Root: " + rootLogger.getLevel());
    System.out.println("User: " + initLogger.getLevel());
    System.out.println("Global: " + globalLogger.getLevel());
  }
}
After that, type this command:

java -Djava.util.logging.config.file=C:\test\logging-config-test\MyConfig.properties LogTest

The result would be:
Root: INFO
User: CONFIG
Global: null
Take note that this command is also valid:

java LogTest -Djava.util.logging.config.file=C:\test\logging-config-test\MyConfig.properties

However, our config file won't be loaded once our program runs. We need to load the config file first before running the program. Thus, order of commands matters.

Java will use the default logging.properties if our config file can't be found.java.util.logging.config.file system property doesn't look at classpath. That's why we use absolute path to refer to our custom config file.

Next, let's talk about the properties in a config file. First off, the "#" symbol means a comment. LogManager ignores characters that are after this symbol. "handlers" is a LogManager property that assigns a handler to the root logger. If you want to assign a handler to a specific logger apart from root logger, we use this property "<logger>.handlers".

For example, initLogger.handlers = java.util.logging.ConsoleHandler
Additionally, we can add multiple handlers in a logger. We separate the handlers using comma(,). For example:
handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler
If you have a custom handler
handlers = MyPackage.MyHandler, java.util.logging.FileHandler

To initialize a new logger at startup, we just need to set one of the attributes of the new logger and java will create that logger for us. In the example above, I just write "initLogger.level = CONFIG" property and java creates a logger named "initLogger" and sets its level to CONFIG.

We can also set some properties of root and global logger in the config file. For example, if we want to change the level of the root logger, write this property: ".level = FINE". For global logger write this: "global.level = FINEST". Root and global loggers are automatically created by the LogManager.

In the discussion above, we now know that we can use "handlers" and "level" attributes to modify the handlers and levels of loggers. More information about properties of loggers can be found in the LogManager documentation.

"java.util.logging.ConsoleHandler" system property refers to the ConsoleHandler. We can initialize some attributes of ConsoleHandler in the config file such as level, filter, formatter and encoding. Take note that each instance of ConsoleHandler will copy the values of the properties assigned to it in the config file during their initialization.

If we have custom handler, we can initialize some of their attributes in the config file. For example:
//if your custom handler is in a package
MyPackage.MyHandler.level = FINE

//if your custom handler is not in a package
MyHandler.level = FINE
In this way, each instance of MyHandler will have a FINE level. If the specified attribute of your custom handler in the config file doesn't match any attribute in the implementation of your custom handler, the property that specified the attribute is invalid and will be ignored.

For example, your custom handler is a ConsoleHandler and you specify "MyHandler.maxLocks" property. In this case, "maxLocks" attribute is gonna be ignored. "maxLocks" is an attribute of FileHandler. For more information about properties of ConsoleHandler can be found in this documentation.

There are different types of handlers which we will learn in the next topic. We will also discuss formatters in another topic. Next, let's create a config file using class file. Write and compile this code:
import java.util.logging.*;

public class LoggerProperties{

  public LoggerProperties(){
    
    try{
      //use 
      //System.setProperty(String key, String value) method
      //to configure system property programatically
      //
      System.setProperty
      ("java.util.logging.ConsoleHandler.level","INFO");
    
      //initialize a logger
      Logger.getLogger("initLogger").
      setLevel(Level.CONFIG);
      
      //set global logger level
      Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).
      setLevel(Level.FINE);
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
After that, compile LogTest in the previous example and then type this command:

java -Djava.util.logging.config.class=LoggerProperties LogTest

And the result would be:
Root: INFO
User: CONFIG
Global: FINE
Use System.setProperty(String key, String value) to configure system properties. System properties are properties that are internally used by java. Unlike "java.util.logging.config.file" property, "java.util.logging.config.class" looks at classpath. That's why we can use a relative path relative to our program. Also, it's not required to include the .class file extension in the command.

There's another way to configure LogManager. We can use readConfiguration method in conjunction with "java.util.logging.config.file" property to directly load properties file in the classpath at startup. First off, compile this code:
import java.util.logging.*;
import java.io.InputStream;

public class LoggerProperties{

  public LoggerProperties(){
    
    //make variables final so they
    //can't be changed.
    //Thus, more security
    try{
      final LogManager lm = 
      LogManager.getLogManager();
      try(
      final InputStream is = 
      this.getClass().getResourceAsStream
      ("MyConfig.properties")){
        lm.readConfiguration(is);
      }
    }
    catch(Exception e){
      e.printStackTrace();
    }
    
  }
}
After that, compile LogTest in the previous example and then type this command:

java -Djava.util.logging.config.class=LoggerProperties LogTest

Assuming MyConfig.properties has this content:
global.level = FINEST

#initialize new logger
initLogger.level = CONFIG
The result would be:
Root: INFO
User: CONFIG
Global: FINEST
Custom Handler

There are different types of handlers. All handlers implement Handler class. Handler class has two direct subclasses: MemoryHandler and StreamHandler.

In this tutorial, we're gonna focus on StreamHandler. StreamHandler has three direct subclasses: ConsoleHandler, FileHandler and SocketHandler. In this tutorial, we're going to focus on ConsoleHandler and FileHandler

First off, let's create a custom ConsoleHandler. Write and compile this code:
import java.util.logging.*;
import java.io.IOException;

public class SampleClass{
  
  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args){
    ConsoleHandler ch = 
    new CustomConsoleHandler();
    
    //add handler
    logger.addHandler(ch);
    
    //setLevel
    logger.setLevel(Level.FINE);
    ch.setLevel(Level.INFO);
    
    //this wil ensure that the log sent
    //to our logger won't be sent to 
    //its parent which is the root logger.
    //
    //If we don't do this, our console
    //will receive two identical messages.
    if(logger.getUseParentHandlers())
      logger.setUseParentHandlers(false);
    
    System.out.println("Logger Level: " + 
    logger.getLevel());
    System.out.println("Handler Level: " + 
    ch.getLevel());
    System.out.println();
    logger.log(Level.FINE, "This is fine!");
    
  }
}

class CustomConsoleHandler extends ConsoleHandler{

  @Override
  public void publish(LogRecord record){
    super.publish(logRecord(record));
  }
  
  private LogRecord logRecord(LogRecord log){
    if(log.getLevel() == Level.FINE){
      Level newLvl = Level.INFO;
      String message = 
      "\nOriginal Level: " + 
      log.getLevel() + "\n" +
      "New level: " + 
      newLvl + "\n" +
      "Message: " + "\n" +
      log.getMessage();
      
      
      LogRecord newLog = 
      new LogRecord(newLvl, message);
      return newLog;
    }
    else return log;
  }
}

Result
Logger Level: FINE
Handler Level: INFO

May 19, 2022 5:20:41 PM SampleClass main
INFO:
Original Level: FINE
New Level: INFO
Message:
This is fine!
We use addHandler method add handlers to our logger. Once our loggers creates a log, each handler in our logger will output the log. LogRecord contains information about a log that is logged by a logger. Once all information about the log is wrapped to a LogRecord, the LogRecord will be sent to the handlers.

Now, let's try a custom FileHandler. Write and compile this code:
import java.util.logging.*;
import java.io.IOException;

public class SampleClass{

  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args)
                       throws IOException{
    FileHandler ch = 
    new CustomFileHandler();
    
    //add handler
    logger.addHandler(ch);
    
    //setLevel
    logger.setLevel(Level.FINE);
    ch.setLevel(Level.INFO);
    
    logger.log(Level.INFO, "Seems like trouble!");
    
  }
  
}

class CustomFileHandler extends FileHandler{

  CustomFileHandler() throws IOException{}
  
  @Override
  public void publish(LogRecord record){
    super.publish(logRecord(record));
  }
  
  private LogRecord logRecord(LogRecord log){
    if(log.getLevel() == Level.INFO){
      Level newLvl = Level.WARNING;
      String message =
      "\nOriginal Level: " +
      log.getLevel() + "\n" +
      "New level: " + 
      newLvl + "\n" +
      "Message: " + "\n" +
      log.getMessage();
      
      LogRecord newLog = 
      new LogRecord(newLvl, message);
      return newLog;
    }
    else return log;
  }
  
}

Result(On console)
May 19, 2022 7:37:57 PM SampleClass main
INFO: Seems like trouble!

Result(in file)
<?xml version="1.0" encoding="windows-1252" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2022-05-19T11:37:57.256883500Z</date>
  <millis>1652960277256</millis>
  <nanos>883500</nanos>
  <sequence>1</sequence>
  <level>WARNING</level>
  <class>SampleClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>
Original Level: INFO
New level: WARNING
Message: 
Seems like trouble!</message>
</record>
</log>
If you have been noticed, the date&time in the file and console are different. In my opinion, java.util.logging.SimpleTimeFormatter and java.util.logging.XMLFormatter use different timezones.

In the example above, I use the default logging.properties configuration file. Let's examine some properties of FileHandler used in the configuration file.

<handler-name>.pattern specifies a pattern for generating the output file name. The default value of this property is "%h/java%u.log". "%h" denotes "user.home" system property in linux. In windows, it denotes "C:\Users\%USERNAME%" system property.

"%u" denotes a unique number. This unique number is used to resolve conflicts between log files. This ensures that each generated log files during runtime is unique to one another.

<handler-name>.formatter specifies the name of a Formatter class to use. The default value of this property is "java.util.logging.XMLFormatter". That's why the result in the example above in file is in XML format. More information about FileHandler properties can be found in the documentation.

If you just want to instantiate a handler, just use their default constructors. For example:
//Instantiate default ConsoleHandler
ConsoleHandler ch = new ConsoleHandler();

//Instantiate default FileHandler
FileHandler fh = new FileHandler();

Custom Formatter

Java provided two formatters that are used by default. java.util.logging.SimpleFormatter system property is used by ConsoleHandler as its default formatter. java.util.logging.XMLFormatter system property is used by FileHandler as its default formatter. If those formatters are not enough, we can override java.util.logging.Formatter to create our custom formatter.

Write and compile this code:
import java.util.logging.*;
import java.util.Locale;
import java.time.format.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

public class SampleClass{
  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args){
    ConsoleHandler ch = new ConsoleHandler();
    
    //use set
    ch.setFormatter(new CustomFormatter());
    
    logger.addHandler(ch);
    logger.setUseParentHandlers(false);
    
    try{
      int num = 1/0;
    }
    catch(ArithmeticException e){
      logger.log(Level.SEVERE,
      "There's a problem here!", e);
    }
    
  }
}

class CustomFormatter extends Formatter{

  @Override
  public String format(LogRecord record){
    LocalDateTime ldt = 
    LocalDateTime.ofInstant(Instant.ofEpochMilli(
    record.getMillis()), ZoneId.of("Asia/Taipei"));
    
    String output = 
    "Date: " + 
    ldt.format(DateTimeFormatter.
    ofPattern("MM/dd/uu hh:mm:ss", 
               Locale.US))+ "\n" +
    "Thread ID: " + 
    record.getLongThreadID() + "\n" +
    "Source Class: " + 
    record.getSourceClassName() + "\n" +
    "Source Method: " + 
    record.getSourceMethodName() + "\n" +
    "Logger Name: " + 
    record.getLoggerName() + "\n" +
    "Log Level: " +
    record.getLevel() + "\n" +
    "Log Message: " +
    record.getMessage() + "\n";
    
    Throwable throwable = record.getThrown();
    if(throwable != null)
      output = output.concat
      ("Exception\n" + throwable);
    
    return output;
  }
}

Result
Date: 05/19/22 10:17:03
Thread ID: 1
Source Class: SampleClass
Source Method: main
Logger Name: MyLogger
Log Level: SEVERE
Log Message: There's a problem here!
Exception
java.lang.ArithmeticException: / by zero
Take note that if you change your formatter, you need to recreate some of the formatting of the default formatter if you need them. For example, log, logp and logrb methods with param1 or params parameters won't follow MessageFormat formatting if we use our custom formatter. To enable that feature to our formatter, we need to recreate it.

Custom Filter

By default, loggers and handlers don't have filters. However, we can create our custom filter that filters loggers and handlers. This example demonstrates adding filter to a logger.
import java.util.logging.*;

public class SampleClass{

  static Logger logger =
  Logger.getLogger("MyLogger");
  
  public static void main(String[] args){
    logger.setFilter(new MyFilter());
    
    logger.log(Level.INFO, "Good Line1");
    logger.log(Level.INFO, "Bad Line2");
    logger.log(Level.INFO, "Bad Line3");
    logger.log(Level.INFO, "Good Line4");
    logger.log(Level.INFO, "Bad Line5");
  }
}

class MyFilter implements Filter{
  
  @Override
  public boolean isLoggable(LogRecord record){
    return record.getMessage().startsWith("Good");
  }
}

Result
May 20, 2022 12:14:05 AM SampleClass main
INFO: Good Line1
May 20, 2022 12:14:05 AM SampleClass main
INFO: Good Line4
Take note that a filter can be applied to a logger, handler or both. If a filter is applied to a logger, logs are gonna be filtered before sending them to a handler. If a filter is applied to a handler, received logs are gonna be filtered before sending them to a console, file, etc.

Log Entry and Return of Methods

There are two convenient methods that we can use to log method entry and return. These methods are entering and exiting methods from Logger class. These methods have two overloaded forms. This example demonstrates their overloaded forms with two parameters.
import java.util.logging.*;

public class SampleClass{
  private static final String className = 
  SampleClass.class.getName();

  static Logger logger = 
  Logger.getLogger
  (className);
  
  public static void main(String[] args){
    ConsoleHandler ch = new ConsoleHandler();
    logger.setLevel(Level.FINER);
    ch.setLevel(Level.FINER);
    logger.addHandler(ch);
    
    ClassA ca = new ClassA();
    logger.entering(className, "main");
    ca.classAMeth();
    logger.exiting(className, "main");
  }
}

class ClassA{
  private final String className = 
  ClassA.class.getName();
  
  Logger logger = 
  Logger.getLogger(className);
  
  ClassA(){
    ConsoleHandler ch = new ConsoleHandler();
    logger.setLevel(Level.FINER);
    ch.setLevel(Level.FINER);
    logger.addHandler(ch);
  }
  
  void classAMeth(){
    logger.entering(className, "classAMeth");
    System.out.println("Class A Method!");
    logger.exiting(className, "classAMeth");
  }
}

Result
May 20, 2022 1:22:42 AM SampleClass main
FINER: ENTRY
May 20, 2022 1:22:42 AM SampleClass classAMeth
FINER: ENTRY
Class A Method!
May 20, 2022 1:22:42 AM SampleClass classAMeth
FINER: RETURN
May 20, 2022 1:22:42 AM SampleClass main
FINER: RETURN
In the example above, I logged the pre-entry and entry of execution in classAMeth; pre-return and return of execution from classAMeth and main. Take note that the logs of these two methods are loggable in FINER level. Thus, your logger and handler must be in FINER level or level lower than FINER.

Other forms of these two methods can be found in the documentation.

Monday, May 16, 2022

Java Tutorial: Timer and TimerTask

Chapters

Overview

Timer is used to scheule tasks that are provided by TimerTask.In java 1.5 and above, it's preferrable to use ScheduledThreadPoolExecutor class. Java provides an easy-to-use instance of ScheduledThreadPoolExecutor which can be instantiated by invoking newScheduledThreadPool() method.

In java below 1.5, concurrent package was not introduced. Therefore, ScheduledThreadPoolExecutor isn't available in those versions.

Run Timer After a Certain Delay

We can use Timer to perform a task after a certain delay. Take a look at this example.
import java.util.Timer;
import java.util.TimerTask;

public class SampleClass{
  private static final long initTime = 
  System.currentTimeMillis();
  private static volatile boolean endMain = false;
  
  public static void main(String[] args) {
  
    TimerTask myTask = new TimerTask(){
      
      @Override
      public void run(){
        System.out.println("Elapsed Time");
        System.out.println(System.currentTimeMillis() - 
        initTime);
        endMain = true;
      }
    };
    Timer timer = new Timer("MyTimer", true);
    
    System.out.println("Task starts after 2 seconds...");
    long delay = 2000L; //2 seconds
    timer.schedule(myTask, delay);
    
    //block main thread while the scheduled task is not
    //complete yet
    while(!endMain);
    
  }
}

Result(may vary)
Task starts after 2 seconds...
Elapsed Time
2000
First off, TimerTask implements Runnable interface and tasks are run via run() method. Thus, we need to override run() method and write our specified task inside it. Timer(String name, boolean isDaemon) creates a new timer whose associated thread has the specified name, and may be specified to run as a daemon thread.

schedule(TimerTask task, long delay) creates a one-shot schedule for task in TimerTask after a specified delay. Each instance of Timer is associated with a single background thread that executes tasks scheduled by the timer. If you want to set your Timer with a delay based on date & time, use schedule(TimerTask task, Date time).

Schedule Timer with a Fixed Delay or Fixed Rate

If we want to schedule tasks at fixed-delay, we use schedule(TimerTask task, long delay, long period) constructor. for initial delay based on date & time, use schedule(TimerTask task, Date firstTime, long period) constructor. I explained the concepts of fixed delay and fixed rate in this article.

This example demonstrates Timer with fixed-delay.
import java.util.Timer;
import java.util.TimerTask;

public class SampleClass{
  private static final long initTime = 
  System.currentTimeMillis();
  private static volatile boolean endMain = false;
  
  public static void main(String[] args) {
  
    TimerTask myTask = new TimerTask(){
      private int num = 0;
      
      @Override
      public void run(){
        System.out.println("Elapsed Time");
        System.out.println(System.currentTimeMillis() - 
        initTime);
        ++num;
        if(num == 5)
          endMain = true;
      }
    };
    Timer timer = new Timer("MyTimer", true);
    
    System.out.println("Task starts after 2 seconds...");
    long delay = 2000L; //2 seconds
    timer.schedule(myTask, delay, 1000L);
    
    //block main thread while the scheduled task is not
    //complete yet
    while(!endMain);
    
  }
}

Result(may vary)
Elapsed Time
2000
Elapsed Time
3000
Elapsed Time
4000
Elapsed Time
5000
Elapsed Time
6000
If you want to schedule your timer at fixed rate, you can use one of these two methods:
scheduleAtFixedRate(TimerTask task, long delay, long period)
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

Also, Timer and TimerTask can be cancelled. cancel method in TimerTask cancels all running tasks of this TimerTask and Timer class can't reschedule the task anymore.

cancel method in Timer class cancels the timer. This means that we can't schedule any task in the timer anymore. cancel method in Timer class may not terminate its background thread right after the moment it was cancelled.

Monday, May 9, 2022

Java Tutorial: currentTimeMillis() and nanoTime()

Chapters

currentTimeMillis() and nanoTime()

currentTimeMillis() returns the current time of our machine in milliseconds. The measured time starts from midnight, January 1, 1970 UTC (epoch time) up to the current date in our machine. Take note that the granularity of the value depends on the underlying operating system and may be larger. For example, many operating systems measure time in units of tens of milliseconds.

nanoTime() returns the current time of our machine in nanoseconds. The measured time starts from midnight, January 1, 1970 UTC (epoch time) up to the current date/time in our machine.

Take note that the method provides nanosecond precision, but not necessarily nanosecond resolution. This means that the returned value is in nanosecond digits but the returned value itself maybe larger than a nanosecond. The resolution of nanoTime() is at least as good as that of currentTimeMillis().

For high precision timers, use nanoTime(). Otherwise, currentTimeMillis() should suffice. These two methods can be found in the System class.

This example demonstrates currentTimeMillis().
public class SampleClass{

  public static void main(String[] args){
      long currentTime = System.currentTimeMillis();
      MyTimer timer = new MyTimer(
      currentTime, currentTime + 3000L);
      timer.start();
  }
}

class MyTimer extends Thread{
  long time = 0L;
  long initTime = 0L;
  
  MyTimer(long initTime, long time){
    this.initTime = initTime;
    this.time = time;
    
    System.out.println("Starting Time");
    System.out.println(initTime);
    System.out.println("Target Time");
    System.out.println(time);
    System.out.println();
  }
  
  @Override
  public void run(){
    long timeMillis = initTime;
    long targetTime = time;
    
    if(targetTime < timeMillis){
      System.out.println("Target time "+
      "is less than current time!");
      return;
    }
    
    while(timeMillis < targetTime){
      System.out.println("Remaining Time");
      System.out.println((targetTime - timeMillis) + 
      " milliseconds.");
      System.out.println(
      ((targetTime - timeMillis)/1000) + //convert millis to seconds
      " seconds.");
      System.out.println();
      
      try{ Thread.sleep(900L); }
      catch(InterruptedException e){
        e.printStackTrace();
      }
      timeMillis = System.currentTimeMillis();
    }
    System.out.println("\nDone!");
  }
}

Result(may vary)
Starting Time
1652089057223
Target Time
1652089060223

Remaining Time
3000 milliseconds.
3 seconds.

Remaining Time
2085 milliseconds.
2 seconds.

Remaining Time
1183 milliseconds.
1 seconds.

Remaining Time
280 milliseconds.
0 seconds.

Done!
In MyTimer constructor, initTime parameter is the starting time in our timer. We use System.currentTimeMillis() to get the current time once our program runs and use that as the starting time. time parameter is our target time, we use the current time plus additional milliseconds to get our desired target time.

In the example above, the distance between the starting time and target time is 3000 millis. Now, try to use nanoTime() instead of currentTimeMillis() in the example above.

currentTimeMillis() and nanoTime() have many uses. Another use of them is creating a basic performance test. Take a look at this example.
import java.util.Random;

public class SampleClass{

  public static void main(String[] args){
    Random random = new Random();
    
    System.out.println("StartTime");
    long startTime = System.nanoTime();
    System.out.println(startTime);
    
    double num = 0;
    for(int i = 0; i < 1000000; i++)
      num = i/random.nextInt();
    
    System.out.println("EndTime");
    long endTime = System.nanoTime();
    System.out.println(endTime);
    System.out.println();
    
    System.out.println("Elapsed Time");
    double elapsed = endTime - startTime;
    System.out.println(elapsed + " nanoseconds");
    //convert nano to millis
    //1000 nanosec = 1 microsec
    //1000 microsec = 1 millisec
    System.out.println(elapsed/1000/1000 + " milliseconds");
  }
}

Result(may vary)
1.3453649e7 nanoseconds
13.45364899 milliseconds

Sunday, May 8, 2022

Java Tutorial: Base64 Class

Chapters

Overview

Base64 class consists exclusively of static methods for obtaining encoders and decoders for the Base64 encoding scheme. Base64 encodes binary data into 6-bit Base64 characters.

Base64 is useful when we want to transmit binary data over channels that only support text content. For example, Base64 is also widely used for sending e-mail attachments because SMTP, the widely used electronic mail protocol, can only transport 7-bit ASCII characters.

Base64 is widely used in World Wide Web. One of its usage is embedding image files in HTML. There are the types of Base64 that java supports.
  • Basic
  • URL and Filename safe
  • MIME
The length of Base64 characters is 6 bits. A 65-character subset of US-ASCII is used, enabling 6 bits to be represented per printable character. Base64 characters can be found in this table.

Base64 encoding length should be multiple of four. If this is not met, the encoder will add "=" as padding to compensate for the lack of characters. If you don't want the encoder to add padding, invoke withoutPadding method first before encoding binary data. For example:

String encoded = Base64.getEncoder().withoutPadding().encodeToString(input.getBytes("UTF-8"));


Basic Encoder

Basic encoder just encodes data as-is. Basic decoder rejects any characters that are not part of Base64 characters. This example demonstrates encoding data to Base64 and decoding it back to its original format.
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SampleClass{

  public static void main(String[] args) throws
                     UnsupportedEncodingException{
    
    String input = "\u00e9zebra!!";
    System.out.println("Original");
    System.out.println(input);
    System.out.println();
    
    String encoded = Base64.getEncoder().
    encodeToString(input.getBytes("UTF-8"));
    System.out.println("Encoded");
    System.out.println(encoded);
    System.out.println();
    
    byte[] decodedBytes = 
    Base64.getDecoder().decode(encoded);
    String decodedStr = 
    new String(decodedBytes, "UTF-8");
    System.out.println("Decoded");
    System.out.println(decodedStr);
    System.out.println();
    
  }
}

Result
Original
ézebra!!

Encoded
w616ZWJyYSEh

Decoded
ézebra!!
Base64.getEncoder returns a Base64.Encoder instance using basic type encoder. Base64.getDecoder returns a Base64.Decoder instance using basic type decoder.

URL and Filename-safe

URL and Filename-safe Base64 encoder encodes data with URL-friendly and filename-friendly characters. In this encoder, "+" is replaced by "-" and "/" is replaced by "_". Those characters that are replaced have special meanings to URLs and filesystems. Those characters with special meanings may mess up the encoded data. Take a look at this table to know more about the differences between Base64 encoders.

This example demonstrates encoding URL using Base64.
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class SampleClass{

  public static void main(String[] args) throws
                     UnsupportedEncodingException{
    
    String input = "https://www.google.com/";
    System.out.println("Original");
    System.out.println(input);
    System.out.println();
    
    String encoded = Base64.getUrlEncoder().
    encodeToString(input.getBytes("UTF-8"));
    System.out.println("Encoded");
    System.out.println(encoded);
    System.out.println();
    
    byte[] decodedBytes = 
    Base64.getUrlDecoder().decode(encoded);
    String decodedStr = 
    new String(decodedBytes, "UTF-8");
    System.out.println("Decoded");
    System.out.println(decodedStr);
    System.out.println();
    
  }
}

Result
Original
https://www.google.com/

Encoded
aHR0cHM6Ly93d3cuZ29...

Decoded
https://www.google.com/

Base64.getUrlEncoder returns a Base64.Encoder instance using URL and filename-safe type encoder. Base64.getUrlDecoder returns a Base64.Decoder instance using URL and filename-safe type decoder.

MIME

Multipurpose Internet Mail Extensions(MIME) is an Internet standard that extends the format of email messages to support text in character sets other than ASCII, as well as attachments of audio, video, images, and application programs.

Base64 alphabet is supported by MIME character set. Thus, Base64 MIME encoder is MIME-friendly. This example demonstrates encoding using Base64 MIME encoder.
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;

public class SampleClass{

  public static void main(String[] args) throws
                     UnsupportedEncodingException,
                     IOException{
    
    File file = new File("files/image.png");
    byte[] fileBytes = 
    Files.readAllBytes(file.toPath());
    
    System.out.println("Original");
    for(byte b : fileBytes)
      System.out.print(b + " ");
    System.out.println();
    
    String encoded = Base64.getMimeEncoder().
    encodeToString(fileBytes);
    System.out.println("Encoded");
    System.out.println(encoded);
    System.out.println();
    
    byte[] decodedBytes = 
    Base64.getMimeDecoder().decode(encoded);
    String decodedStr = 
    new String(decodedBytes, "UTF-8");
    System.out.println("Decoded");
    for(byte b : fileBytes)
      System.out.print(b + " ");
  }
}

Result
Original
-119 80 78 71 13 10 26...
Encoded
iVBORw0Kggo...

Decoded
-119 80 78 71 13 10 26...
Base64.getMimeEncoder returns a Base64.Encoder instance using MIME type encoder. Base64.getMimeDecoder returns a Base64.Decoder instance using MIME type decoder. The length of lines in the output is no longer than 76 characters. Also, each line end with CR+LF(\r\n).

If you want to change the character per length and line separators, invoke getMimeEncoder(int lineLength, byte[] lineSeparator). In the example above, the encoded bytes of the image file can be used in HTML. For example:
<div>
  <p>Taken from wikpedia</p>
  <img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA
    AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
        9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
</div>

Friday, May 6, 2022

Java Tutorial: Cryptography

Chapters

Introduction

In computer programming, Cryptography is a way of hiding data(passwords, messages, etc.) from third parties(adversaries) by securing the communication process between two entities. Cryptographic functions can be found in two java librarties: Java Cryptography Architecture(JCA) and Java Cryptography Extension(JCE).

JCA is tightly integrated with Java core API and provides basic cryptographic functions whereas JCE provides advanced cryptographic functions. Classes belonging to JCA and JCE are called engines. JCA engines are located at java.security package and JCE engines are located at javax.crypto packages. In the past, JCE is separated from JCA due to US export policies. Over time, This export control has been relaxed and in the present JCE becomes part of Java SE.

JCA and JCE provide functionalities. The actual implementations of those functions are provided by providers. Java has default providers we can use right away. Invoke getProviders() method from Security class in order to see all available providers in your java platform. The order of the providers in the array is their preference order.

The method returns Provider objects that hold information about cryptography providers. If you want to add new provider to your platform, invoke addProvider method from Security class.

For example:
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;

public class ProviderExample{

  public static void main(String[] args){
    Security.addProvider(new BouncyCastleProvider());
  }
}
BouncyCastle is one of the popular cryptography providers that can be used in java. It was developed by an Australian charitable organization thus, US law restrictions do not apply to this provider. In the course of this tutorial, we're going to use the default providers provided by java.

Another way of using a third-party provider is to register it in the java.security file. In JDK8 and lower, the file is located in the directory where you installed java at jre\lib\security\java.security. Place the provider JAR in lib\ext

In JDK9 and above, the file is located in the directory where you installed java at conf\security\java.security. Place the provider JAR in the classpath.

Once you open java.security file, look at this section:
...
#
# List of providers and their preference orders (see above):
#
security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
security.provider.4=SunJSSE
...
Then, add your third-party provider in the list:
...
#
# List of providers and their preference orders (see above):
#
security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
security.provider.4=SunJSSE
security.provider.5=third-party-provider-name
...
In this tutorial, I'll be discussing Hashing, Symmetric Encryption, Asymmetric Encryption and Digital Signature. Java can also create digital certificate but I won't cover cerfiticates in this tutorial.

Hashing

Hashing or cryptographic hash function is a process of mapping data("message") with arbitrary size to an array of bits with fixed size (also called "hash value", "hash", "message digest").

Hashing is a one-way function. It means that the output is unfeasible to invert or reverse. Also, hashing must be deterministic. It means that messages that are the same have equivalent hash. On the other hand, a slight change to a message creates a different hash for the message.

This example demonstrates hashing in java.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     UnsupportedEncodingException{
    MessageDigest digestInstance = 
    MessageDigest.getInstance("SHA-256");
    String message1 = "Secret Message!";
    String message2 = 
    "The big brown fox jumps over the lazy doge";
    
    byte[] input = message1.getBytes("UTF-8");
    byte[] hash = digestInstance.digest(input);
    
    System.out.println("Message1: " + message1);
    System.out.println("Hash Value: " + 
    new BigInteger(1, hash).toString(16));
    System.out.println("Hash Length");
    System.out.println(hash.length + " bytes");
    System.out.println((hash.length*8) + " bits");
    
    input = message2.getBytes("UTF-8");
    hash = digestInstance.digest(input);
    
    System.out.println();
    System.out.println("Message2: " + message2);
    System.out.println("Hash Value: " + 
    new BigInteger(1, hash).toString(16));
    System.out.println("Hash Length");
    System.out.println(hash.length + " bytes");
    System.out.println((hash.length*8) + " bits");
  }
}

Result
Message1: Secret Message!
Hash Value: 1bf7aa187ccdf...
Hash Length
32 bytes
256 bits

Message2: The big brown fox jumps over the lazy doge
Hash Value: ec74b616322364f...
Hash Length
32 bytes
256 bits
MessageDigest provides cryptographic hash function with cryptographic hash algorithm such as SHA-256. Take a look at this article to know the supported cryptographic hash algorithms of MessageDigest.

SHA(Secure Hash Algorithm)-256 is a cryptographic hash algorithm that produces 256-bit hash value. This algorithm produces more bits than the old SHA algorithms such as SHA-0 and SHA-1 which produce 160-bit hash value. Thus SHA-256 is more secure than its old counterparts. If you want more secure algorithms, use SHA3 algorithms.

However, SHA-2 algorithms such as SHA-256, SHA-384, SHA-512 and SHA-224 are better than SHA3 algorithms in terms of performance. MD2 and MD5 are old and less secure than SHA-2 and SHA3 algorithms. Thus, it's not recommended to be used to secure data.

Although, MD5 is still used for checking the integrity of a file. Take note that files consist of bytes. Thus, we can get those bytes and put them in MessageDigest instance. MessageDigest.getInstance method returns a MessageDigest object with the specified hash algorithm. getBytes(String charsetName) method returns the byte structure of a java object or file in array form. The bytes are formatted based on charsetName.

digest method completes the hash computation by performing final operations such as padding. The digest is reset after this call is made. Padding is an additional bit added to the hash value if the input bits are less than the bits that a hash algorithm generates. That's why, In the example above, even Message1 is less than 256 bits, the hash value is still 256 bits.

We can use hash values to ensure that the expected message that the received message of a receiver is equivalent to the sent message of the sender. Take a look at this example
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;
import java.util.Arrays;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     UnsupportedEncodingException{
    /*sender*/
    MessageDigest senderDigest = 
    MessageDigest.getInstance("SHA-256");
    String message = "My Message!";
    
    byte[] senderInput = message.getBytes("UTF-8");
    byte[] senderHash = 
    senderDigest.digest(senderInput);
    System.out.println("Sender Hash Value: " +
    new BigInteger(1, senderHash).toString(16));
    
    /*receiver*/
    MessageDigest receiverDigest = 
    MessageDigest.getInstance("SHA-256");
    String expectedMsg = "My Message!";
    
    byte[] receiverInput = 
    expectedMsg.getBytes("UTF-8");
    byte[] receiverHash = 
    receiverDigest.digest(receiverInput);
    
    System.out.println("Receiver Hash Value: " +
    new BigInteger(1, receiverHash).toString(16));
    System.out.println("Sender and Receiver" +
    " hash values are equal?");
    System.out.println(
    Arrays.equals(senderHash, receiverHash));
  }
}

Result
Sender Hash Value: d2f02a685b80a087de...
Receiver Hash Value: d2f02a685b80a087de...
Sender and Receiver hash values are equal?
true
The example above is analogous to password hashing. When we create an application, we take the hash value of the password instead of the password itself and store the hash value in the database. This implementation hides passwords from employees and prying eyes.

Hashing has vulnerabilities such as brute-force search and rainbow-table. Although, those vulnerabilities may take a very long time breaking hash algorithms. We're talking about years or decades.

To defend against these vulnerabilities, we need to add a salt. To put it simply, a salt is just a random data added to the input. The salt is stored along with the password hash. During verification, the stored salt is included in the hash process along with the input. Then, the generated hash will be compared with the stored hash.

If you want the hash of multiple data, we use the update method. Take a look at this example.
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.math.BigInteger;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     UnsupportedEncodingException{
    MessageDigest digest = 
    MessageDigest.getInstance("SHA-512");
    String message1 = "Message1";
    String message2 = "Message2";
    
    digest.update(message1.getBytes("UTF-8"));
    digest.update(message2.getBytes("UTF-8"));
    byte[] hash = digest.digest();
    
    System.out.println("Hash value: " + 
    new BigInteger(1, hash).toString(16));
    System.out.println("Hash Length");
    System.out.println(hash.length + " bytes");
    System.out.println((hash.length*8) + " bits");
  }
}

Result
Hash value: dae036f171ffd578ff...
Hash length
64 bytes
512 bits
Symmetric Encryption

Symmetric encryption uses a single key for encrypting and decrypting data. An unencrypted data is called "cleartext" or "plaintext". The encrypted data is called "ciphertext". This example demonstrates symmetric encryption in java.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.BadPaddingException;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     InvalidKeyException,
                     IllegalBlockSizeException,
                     NoSuchPaddingException,
                     BadPaddingException,
                     UnsupportedEncodingException{
    KeyGenerator keygen = 
    KeyGenerator.getInstance("AES");
    
    keygen.init(192);
    SecretKey key = keygen.generateKey();
    System.out.println("Generated Key");
    System.out.println(
    new BigInteger(1, key.getEncoded()).toString(16));
    
    byte[] input = 
    "The big brown fox jumps over a lazy dog."
    .getBytes("UTF-8");
    System.out.println("Plain text");
    System.out.println(new String(input));
    
    Cipher cipher = 
    Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key);
    byte[] encryptedData = cipher.doFinal(input);
    System.out.println("Ciphertext");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    
    cipher.init(Cipher.DECRYPT_MODE, key);
    byte[] decryptedData = 
    cipher.doFinal(encryptedData);
    System.out.println("Decrypted Data");
    System.out.println(new String(decryptedData));
  }
}

Result(may vary)
Generated Key
57197a794884a...
Plain Text
The big brown fox jumps over a lazy dog.
Ciphertext
2968b36c3699c5a12843...
Decrypted Data
The big brown fox jumps over a lazy dog.
KeyGenerator provides the functionality of a secret (symmetric) key generator. KeyGenerator objects are reusable, i.e., after a key has been generated, the same KeyGenerator object can be re-used to generate further keys. KeyGenerator.getInstance(String algorithm) method returns a KeyGenerator object with the specified algorithm for generating a key.

Available algrorithms for KeyGenerator can be found in this article. Take note that DES(Data Encryption Standard) is discouraged to be used because it's old and not secure anymore.

init(int keysize) method initializes this key generator for a certain keysize using the provider's SecureRandom. AES(Advanced Encryption Standard) supports 128/192/256 bits keysize. AES uses Substitution–permutation network.

To put it simply, AES scrambles bits in multiple rounds. Key size of AES determines the number of round that is going to be used. 128-bit key size implements 10 rounds. 192-bit implements 12 rounds. 256-bit implements 14 rounds. A key with higher key size is more secure. However, key with higher key size takes too long to encrypt/decrypt data. Thus, degrading our application's performance. For standard applications, at the time I write this tutorial, 128-bit key size should suffice.

generateKey method returns SecretKey compatible with the specified algorithm in KeyGenerator object. A key is a string of characters used within an encryption algorithm for altering data so that it appears random.

Cipher provides the functionality of a cryptographic cipher for encryption and decryption. We use Cipher class to encrypt and decrypt data. getInstance(String transformation) returns a Cipher object that implements the specified transformation.

A transformation is a string that describes the operation (or set of operations) to be performed on the given input, to produce some output. A transformation always includes the name of a cryptographic algorithm (e.g., AES), and may be followed by a feedback mode and padding scheme.

A transformation string can be one of these:
  • "algorithm/mode/padding"
  • "algorithm"
In the latter case, provider-specific default values for the mode and padding scheme are used. Before we discuss the mode in a transformation, we need to know two types of Ciphers: Stream Cipher and Block Cipher.

Stream Cipher encrypts the digits (typically bytes), or letters (in substitution ciphers) of a message one at a time. An example is ChaCha20. Substitution ciphers are well-known ciphers, but can be easily decrypted using a frequency table.

Block Cipher takes a number of bits and encrypt them as a single unit, padding the plaintext so that it is a multiple of the block size. The Advanced Encryption Standard (AES) algorithm, approved by NIST in December 2001, uses 128-bit blocks. To put it simply, Block cipher divides the number of bits of plaintext into blocks based on block size. Each block size of blocks is equal.

Now, mode or block cipher mode determines how blocks are encrypted. Some modes(such as CFB and OFB) can encypt and decrypt blocks that are smaller than the actual cipher's block size. There are different types of modes and I'm not gonna explain all of them in this tutorial. You can read this article if you're interested to learn more about them.

ECB(Electronic Code Book) is the simplest cipher block mode because unlike other modes, this mode directly encrypt and decrypt blocks without additional procedures. However, this mode is less secure than others. This mode is bad at hiding data patterns and susceptible to replay attacks since this mode encrypts and decrypts blocks the same way.

padding is any of a number of distinct practices which all include adding data to the beginning, middle, or end of a message prior to encryption. In classical cryptography, padding may include adding nonsense phrases to a message to obscure the fact that many messages end in predictable ways.

PKCS5Padding is commonly used padding type that is provided by default providers. In PKCS5Padding, Padding is in whole bytes. The value of each added byte is the number of bytes that are added, i.e. N bytes, each of value N are added. The number of bytes added will depend on the block boundary to which the message needs to be extended.

The padding will be one of:
01
02 02
03 03 03
04 04 04 04
05 05 05 05 05
06 06 06 06 06 06
etc.
Example: In the following example the block size is 8 bytes and padding is required for 4 bytes.
... | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |
In java, we can use "NoPadding" in the string transformation if we're going to provide our own padding. Otherwise, we need to use one of the padding types available to our platform.

More information about padding types can be found in this article. More information about PKCS(Public Key Cryptography Standards) can be found in this article. Take note that ISO10126Padding is not encouraged to be used anymore because it's widthrawn in 2007.

init(int opmode, Key key) initializes a Cipher object with specified operation and key. The cipher is initialized for one of the following four operations: encryption, decryption, key wrapping or key unwrapping, depending on the value of opmode. This method form uses the provider's SecureRandom if the Cipher object needs random bytes.

Let's focus on encrypting and decrypting data. To initialize a Cipher object in "encrypt" mode, put Cipher.ENCRYPT_MODE in the argument list with the selected key. To initialize a Cipher object in "decrypt" mode, put Cipher.DECRYPT_MODE in the argument list with selected key.

doFinal(byte[] input) executes the operation of the Cipher object that invokes this method. If you want to encrypt multi-part data, use update(byte[] input) method per data and then invoke doFinal() method to finalize the input and execute the selected operation.

Next, I'm gonna demonstrate another cipher block mode which is the CBC(Cipher Block Chaining). This mode is more secure than ECB and is commonly used. In CBC mode, each block of plaintext is XORed with the previous ciphertext block before being encrypted. This way, each ciphertext block depends on all plaintext blocks processed up to that point. To make each message unique, an initialization vector(IV) must be used in the first block.

This example demonstrates CBC mode.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.BadPaddingException;
import javax.crypto.spec.IvParameterSpec;

public class SampleClass{

  public static void main(String[] args) throws
                     NoSuchAlgorithmException,
                     InvalidAlgorithmParameterException,
                     InvalidKeyException,
                     IllegalBlockSizeException,
                     NoSuchPaddingException,
                     BadPaddingException,
                     UnsupportedEncodingException{
                     
    KeyGenerator keygen = 
    KeyGenerator.getInstance("AES");
    
    keygen.init(192);
    SecretKey key = keygen.generateKey();
    System.out.println("Generated Key");
    System.out.println(
    new BigInteger(1, key.getEncoded()).toString(16));
    
    SecureRandom sr = 
    SecureRandom.getInstance("SHA1PRNG");
    byte[] randBytes = new byte[16];
    sr.nextBytes(randBytes);
    IvParameterSpec iv = 
    new IvParameterSpec(randBytes);
    System.out.println("IV");
    System.out.println(
    new BigInteger(1, iv.getIV()).toString(16));
    
    byte[] input = 
    "The big brown fox jumps over a lazy dog."
    .getBytes("UTF-8");
    System.out.println("Plain text");
    System.out.println(new String(input));
    
    Cipher cipher = 
    Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    byte[] encryptedData = cipher.doFinal(input);
    System.out.println("Ciphertext");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    
    cipher.init(Cipher.DECRYPT_MODE, key, iv);
    byte[] decryptedData = 
    cipher.doFinal(encryptedData);
    System.out.println("Decrypted Data");
    System.out.println(new String(decryptedData));
  }
}

Result(may vary)
Generated Key
50bfcd0d400bc...
IV
afd7ece30...
Plain Text
The big brown fox jumps over a lazy dog.
Ciphertext
dcae499df38120f91...
Decrypted Data
he big brown fox jumps over a lazy dog.
SecureRandom.getInstance(String algorithm) returns a SecureRandom object that implements the specified Random Number Generator (RNG) algorithm. nextBytes(byte[] bytes) method generates random bytes and put them in the bytes array.

IvParameterSpec specifies an initialization vector (IV). Examples which use IVs are ciphers in feedback mode, e.g., DES in CBC mode and RSA ciphers with OAEP encoding operation. getIV method returns the initialization vector (IV). Take note that The IV size depends on the cryptographic primitive used; for block ciphers it is generally the cipher's block-size. In the example above, the IV size is 16 bytes or 128 bits, which is equal to AES block size.

init(int opmode, Key key, AlgorithmParameterSpec params) Initializes this cipher with a key and a set of algorithm parameters. We use this constructor if the algorithm in our Cipher object requires a parameter. Algorithm-specific parameters in java are represented by AlgorithmParameterSpec and its implementing classes and known subinterfaces.

In the example above, the Cipher object uses CBC(Cipher Block Chaining) and CBC requires an IV(initialization vector) parameter. To fulfill that requirement, we need to instantiate a IvParameterSpec object.

From the documentation:
If the cipher requires any algorithm parameters and params is null, the underlying cipher implementation is supposed to generate the required parameters itself (using provider-specific default or random values) if it is being initialized for encryption or key wrapping, and raise an InvalidAlgorithmParameterException if it is being initialized for decryption or key unwrapping. The generated parameters can be retrieved using getParameters or getIV (if the parameter is an IV).
Take note that we can encrypt files using symmetric encryption. Take a look at this example.
...
byte[] input = java.nio.file.Files.
readAllBytes(
new java.io.File("fileTest.zip").
toPath());

Cipher cipher =
Cipher.getInstance("AES/CBC/PKCS5Padding");

cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encryptedData = cipher.doFinal(input);
System.out.println("File Encrypted!");

cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decryptedData =
cipher.doFinal(encryptedData);

try(FileWriter fw =
    new FileWriter("decryptedFile.zip")){
    for(byte b : decryptedData)
      fw.write(b);
}
System.out.println("File Decrypted!");
...
Asymmetric Encryption

Asymmetric Encryption or Public-key cryptography uses a pair of keys to encrypt/decrypt data. In Asymmetric Encryption, there are two types of keys: Public Key and Private Key. Public key is used for encrypting data and private key is used for decrypting data. For example, In a sender-receiver scenario, Assuming we're the receiver, we share our public key to the sender and the sender uses our public key to encrypt the message(data) that the sender wanna send to us.

Then, once we receive the encrypted data, we decrypt it using our private key. Remember that we only share your public keys and we keep our private keys "private". To put it simply, don't share your private key with anyone except you. As you can see, Asymmetric encryption is more secure than symmetric encryption.

Even the attackers intercepted your public key, they couldn't use the public key to decrypt encrypted messages. However, asymmetric encryption is still vulnerable to some kind of attacks. The good news is that those attacks are hard to implement and don't break asymmetric encryption easily. Read this article to know more about the weaknesses of asymmetric encryption.

Another disadvantage of this encryption is that this encryption can encrypt data with limited length. Typically, asymmetric encryption is slower than symmetric encryption. There are different types of algorithms that implement asymmetric encryption. In this tutorial, I'm gonna use the RSA(Rivest–Shamir–Adleman). This article lists algorithms that supports asymmetric encryption.

This example demonstrates asymmetric cryptography using RSA algorithm.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.PrivateKey;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;


public class SampleClass{

  public static void main(String[] args) throws
                     BadPaddingException,
                     IllegalBlockSizeException,
                     InvalidKeyException,
                     NoSuchAlgorithmException,
                     NoSuchPaddingException,
                     UnsupportedEncodingException
                     {
    KeyPairGenerator keygen = 
    KeyPairGenerator.getInstance("RSA");
    keygen.initialize(2048);
    
    KeyPair pair = keygen.generateKeyPair();
    PrivateKey privKey = pair.getPrivate();
    PublicKey pubKey = pair.getPublic();
    
    byte[] privKeyBytes = privKey.getEncoded();
    System.out.println("Private Key");
    System.out.println(
    new BigInteger(1, privKeyBytes).toString(16));
    System.out.println("Key Size");
    System.out.println(privKeyBytes.length + " bytes");
    System.out.println(
    (privKeyBytes.length*8) + " bits");
    System.out.println();
    
    byte[] pubKeyBytes = pubKey.getEncoded();
    System.out.println("Public Key");
    System.out.println(
    new BigInteger(1, pubKeyBytes).toString(16));
    System.out.println("Key Size");
    System.out.println(pubKeyBytes.length + " bytes");
    System.out.println(
    (pubKeyBytes.length*8) + " bits");
    System.out.println();
    
    String input = "I love programming!";
    System.out.println("Input");
    System.out.println(input);
    System.out.println();
    
    Cipher cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.ENCRYPT_MODE, pubKey);
    
    byte[] inputBytes = input.getBytes("UTF-8");
    byte[] encryptedData = cipher.doFinal(inputBytes);
    System.out.println("Encrypted Data");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    System.out.println();
    
    cipher.init(Cipher.DECRYPT_MODE, privKey);
    byte[] decryptedData = cipher.doFinal(encryptedData);
    System.out.println("Decrypted Data");
    System.out.println(new String(decryptedData, "UTF-8"));
  }
}

Result(may vary)
Private Key
308204bc020100300d06902a...
Key Size
1216 bytes
9728 bits

Public Key
30820122300d0692a...
Key Size
294 bytes
2352 bits

Input
I love programming!

Encrypted Data
3d6c5729ec927abae5bd...

Decrypted Data
I love programming!
KeyPairGenerator is used to generate pairs of public and private keys. A Key pair generator for a particular algorithm creates a public/private key pair that can be used with this algorithm. It also associates algorithm-specific parameters with each of the generated keys.

RSA is a block cipher and it requires a padding parameter. Fortunately, java automatically provided a padding parameter for RSA when we invoke KeyPairGenerator.getInstance(String algorithm) with "RSA" as our asymmetric encryption algorithm. This article lists asymmetric algorithms that are available to java by default.

initialize(int keysize) Initializes the key pair generator for a certain keysize using a default algorithm-specific parameter set and the SecureRandom implementation of the highest-priority installed provider as the source of randomness. (If none of the installed providers supply an implementation of SecureRandom, a system-provided source of randomness is used.)

RSA supports 512, 1024, 2048 and 4096 bits key size. Key size in this context is not the actual key size of both public and private keys. Key size in this context is used to determine the length(size) of public and private keys. The higher the key size, the longer the length of public and private keys. Keys with higher length are more secure.

However, keys with higher size take time to encrypt/decrypt data. For standard applications, at the time I write this tutorial, 2048 key size is widely used because it has a good balance between security and performance. KeyPair is a simple holder for a key pair (a public key and a private key). It does not enforce any security, and should be treated like a PrivateKey when initialized because the instance of this class holds a private key.

PrivateKey is used to group (and provide type safety for) all private key interfaces. PublicKey is an interface that contains no methods or constants. It merely serves to group (and provide type safety for) all public key interfaces.

generateKeyPair() generates a pair of key(public and private) based on the selected algorithm and key size of KeyPairGenerator instance. getPrivate() retuns a PrivateKey instance with the generated private key stored in KeyPairGenerator instance. getPublic retuns a PublicKey instance with the generated public key stored in KeyPairGenerator instance.

I didn't explain some classes and methods that I've used in the example above because I already explained some of them in previous topics. I assumed you've read previous topics before reading this topic. If you're already familiar with java and its cryptography API, you don't need to read the previous topics.

In the example above, Notice that private key is longer than public key. In RSA, private key stores more information than public key and some of those information are confidential.

Take note that RSA data encryption has length limit. For example, using my java platform's default RSA provider, If I increase the byte length in input variable in the example above:
...
//repeat() means, repeat this string by the
//specified amount in the argument. In this example,
//the String is repeated 50 times.
String input = "I love programming!".repeat(50);
...
I'll get this exception:
...
Exception in thread "main" javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes.
...


The limitation length is based on the key size we put in initialize(int keysize). The limitation length increases/decreasees as key size gets larger/smaller. If we want to encrypt large data, symmetric encrypt is more preferrable to be used. Some of the usage of asymmetric encryption are:
  • Encrypting/Decrypting symmetric keys
  • Encrypting/Decrypting hashes
You may ask yourself on how to verify if a public key that you receive comes from the real sender you're expecting. This is where digital certificate comes in. Digital certificate, to put it simply, is used verify the identity of a public key. Digital certificate is not part of the scope of this tutorial.
Digital Signature

Digital signatures are used for verifying the integrity of data. Digital signature ensures that the data we receive from a sender is not tampered. Digital signature is simply just a hash of the data that is going to be sent and the hash is encrypted. This example demonstrates asymmetric digital signature.
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import javax.crypto.Cipher;

public class SampleClass{

  public static void main(String[] args) throws
                     InvalidKeyException,
                     NoSuchAlgorithmException,
                     SignatureException,
                     UnsupportedEncodingException{
  
    /*Sender*/
    
    KeyPairGenerator keygen = 
    KeyPairGenerator.getInstance("RSA");
    keygen.initialize(2048);
    
    KeyPair pair = keygen.generateKeyPair();
    PrivateKey privKey = pair.getPrivate();
    PublicKey pubKey = pair.getPublic();
    
    String input = "I love programming!";
    
    Signature signatureAlg = 
    Signature.getInstance("SHA256WithRSA");
    signatureAlg.initSign(privKey);
    signatureAlg.update(input.getBytes("UTF-8"));
    byte[] signBytes = signatureAlg.sign();
    
    /*Receiver*/
    
    Signature verificationAlg =
    Signature.getInstance("SHA256WithRSA");
    verificationAlg.initVerify(pubKey);
    verificationAlg.update(input.getBytes("UTF-8"));
    System.out.println("Is data not tampered? " +
    verificationAlg.verify(signBytes));
  }
}

Result
Is data not tampered? true
In the example above, the sender gets the hash of the data and encrypts the hash using his/her private key. Then, the sender sends his/her public key, signature(encrypted hash) and the data to the receiver. Now, the receiver uses the received public key to decrypt the received hash. Then, we get the hash of the received data and compare it to the decrypted hash.

The process that I've explained can be simplified in java. We use Signature to hash and encrypt/decrypt it. Signature.getInstance(String algorithm) returns a Signature object that implements the specified signature algorithm.

"SHA256WithRSA" means SHA-256 is the hash algorithm and RSA is the encryption/decryption algorithm. More available algorithms that are supported by java can be found in this article.

initSign(PrivateKey privateKey) initializes a Signature instance for signing with PrivateKey argument. update(byte[] data) updates the data to be signed or verified, using the specified array of bytes. sign() creates a digital signature with the specified algorithm and data.

In other words, sign() gets the hash of the data, encrypts the hash and return the encrypted hash. initVerify(PublicKey publicKey) initializes a Signature instance for verification with PublicKey argument. verify(byte[] signature) verifies the specified signature.

In other words, verify(byte[] signature) decrypts the encrypted hash (signature), gets the hash of the received data and compare the decrypted hash with the hash of the received data. If both hashes match, the received data is not tampered. In the example above, I only encrypt the hash of the data that is sent to the receiver and not the data itself.

If you only want to check the integrity of your data and now worried about it to be intercepted, You don't need to encrypt the data itself. Otherwise, encrypt the data. I skipped encrypting the data itself in order to focus on digital signature.

Message Authentication Code(MAC)

Message Authentication Code is a symmetric digital signature. MAC uses a single key to create and verify a digital signature. This is also called as keyed hashing because it secures the hash of data by generating the hash with a key. One of the advantages of MAC over asymmetric digital signature is that MAC is much faster than most algorithms used for asymmetric digital signature.

However, asymmetric digital signature is commonly used than MAC because it offers more security. MAC can be cipher-based(CMAC & GMAC) or hash-based(HMAC). In this tutorial, we're going to focus on HMAC. Cipher-based MACs are tricky to implement. It's better to start with HMAC first.

This example demonstrate HMAC.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;

public class SampleClass{

  public static void main(String[] args) throws
                       NoSuchAlgorithmException,
                       InvalidKeyException,
                       UnsupportedEncodingException{
    
    /*Sender*/
    
    KeyGenerator keygen = KeyGenerator.getInstance("HmacSHA256");
    keygen.init(256);
    SecretKey key = keygen.generateKey();
    
    Mac senderMac = Mac.getInstance("HmacSHA256");
    senderMac.init(key);
    
    String input = "A secret between you and me.";
    senderMac.update(input.getBytes("UTF-8"));
    byte[] senderComputedMac = senderMac.doFinal();
    
    /*Receiver*/
    
    Mac receiverMac = Mac.getInstance("HmacSHA256");
    receiverMac.init(key);
    receiverMac.update(input.getBytes("UTF-8"));
    byte[] receiverComputedMac = receiverMac.doFinal();
    
    String senderMacStr = 
    new BigInteger(1, senderComputedMac).toString(16);
    String receiverMacStr = 
    new BigInteger(1, receiverComputedMac).toString(16);
    
    System.out.println("Sender MAC");
    System.out.println(senderMacStr);
    System.out.println();
    
    System.out.println("Receiver MAC");
    System.out.println(receiverMacStr);
    System.out.println();
    
    System.out.println("Is data not tampered?");
    System.out.println(
    senderMacStr.equals(receiverMacStr));
  }
}

Result

Sender MAC
5df66b8ef0a885bd2...

Receiver MAC
5df66b8ef0a885bd2...

Is data not tampered?
true
In the example above, the sender generates a key, generates MAC by generating a hash from the data with the key and send the key and MAC over secure channel. The data can be sent over unsecure channel if the sender is not worried about the data getting intercepted. Otherwise, encrypt the data first before sending it to the unsecure channel. Next, once the key, MAC and data has been sent to the receiver, the receiver generates a new MAC by generating a hash from the received data with the received key.

If the received MAC and the generated MAC of the receiver are the same, it means that the data came from the sender who owns the received key and it's not tampered. Otherwise, the data may be tampered, the sender gave a wrong key or the received key is not the key that generated the received MAC.

Key size is 256 bits because at the time I write this, 256 bits keysize should suffice for standard applications. If you want more security at the expense of performance, you can increase the key size. The algorithm used is "HmacSHA256" or generated HMAC using SHA-256 algorithm. More supported Mac algorithm by java can be found in this article.

The algorithm used in the KeyGenerator and Mac instances must be the same. Take note that HmacMD5 and HmacSHA1 are discouraged to be used because they're not secure anymore. Use other algorithms like HmacSHA2 such as HmacSHA256 and HmacSHA512; HmacSHA3 algorithms.

If you want more security at the expense of performance, use hash algorithm with higher block size like HmacSHA512 or use the new SHA family which is the SHA3 family. SHA2 and SHA3 have different algorithms. SHA2 is vulnerable to length extension attack whereas SHA3 is not.

Mac class provides the functionality of a "Message Authentication Code" (MAC) algorithm.

Key Wrapping

Key wrapping is a process of encrypting a key using another key. This example demonstrates wrapping/unwrapping symmetric key using asymmetric keys.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;
import javax.crypto.*;

public class SampleClass{

  public static void main(String[] args) throws
                     BadPaddingException,
                     IllegalBlockSizeException,
                     InvalidKeyException,
                     NoSuchAlgorithmException,
                     NoSuchPaddingException,
                     UnsupportedEncodingException{
    KeyPairGenerator keyPairGen = 
    KeyPairGenerator.getInstance("RSA");
    keyPairGen.initialize(2048);
    
    KeyPair pair = keyPairGen.generateKeyPair();
    PrivateKey privKey = pair.getPrivate();
    PublicKey pubKey = pair.getPublic();
    
    KeyGenerator keygen =
    KeyGenerator.getInstance("AES");
    keygen.init(192);
    
    SecretKey secretKey = keygen.generateKey();
    
    String input = "Send this message!";
    System.out.println("input: " + input);
    
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    
    byte[] encryptedData = 
    cipher.doFinal(input.getBytes("UTF-8"));
    System.out.println("Encrypted Data");
    System.out.println(
    new BigInteger(1, encryptedData).toString(16));
    
    cipher = Cipher.getInstance("RSA");
    cipher.init(Cipher.WRAP_MODE, pubKey);
    byte[] encryptedKey = 
    cipher.wrap(secretKey);
    
    cipher.init(Cipher.UNWRAP_MODE, privKey);
    SecretKey decryptedKey = 
    (SecretKey)cipher.unwrap(encryptedKey, "AES", 
    Cipher.SECRET_KEY);
    
    cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, decryptedKey);
    byte[] decryptedData = cipher.doFinal(encryptedData);
    
    System.out.println("Decrypted Data: " + 
    new String(decryptedData, "UTF-8"));
  }
}

Result
input: Send this message!
Encrypted Data
118729c524039308c...
Decrypted Data: Send this message!
In the example above, The sender wraps the data that he is going to send using AES which is a symmetric encryption algorithm. Then, the generated key for the encrypted data is encrypted using RSA algorithm which is an asymmetric encryption algorithm. He uses the public key of the receiver to encrypt the key of the encrypted data.

The sender sends the encrypted key and data to the receiver. Then, the receiver unwraps(decrypts) the wrapped(encrypted) key using his private key. Then, he uses the unwrapped key to decrypt the data. We can use the example above if we want to securely transfer a large encrypted data to a receiver. The asymmetric keys encrypt/decrypt the symmetric key of the large data and the symmetric key encrypts the large data.

To wrap a key, invoke init(int opmode, Key key) method of Cipher class. opmode parameter is the operation mode. We put Cipher.WRAP_MODE if we're going to wrap a key. key parameter is the key that we're going to use to wrap a key. Once init is invoked. Invoke wrap(Key key) of Cipher class to wrap the key. key parameter is the key that we're going to wrap.

Next, to unwrap the key, we invoke init again but this time the operation mode is Cipher.UNWRAP_MODE and the key is the private key of the receiver. Once init is invoked, invoke unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm, int wrappedKeyType) method.

This method unwraps a wrapped key. wrappedKey parameter is the bytes of the encrypted key. wrappedKeyAlgorithm is the associated algorithm of the encrypted key. wrappedKeyType is the key type of the encrypted key. There are three types of keys in Cipher class:
Cipher.PRIVATE_KEY
Cipher.PUBLIC_KEY
These keys are keys for asymmetric encryption.
Cipher.SECRET_KEY
This key is a key for symmetric encryption.

Storing Keys

There are two ways of storing keys. The simplest way of storing keys is to write keys to a persistent file. Make sure that you store your keys in a safe place and don't share your key with untrusted people.

Write the key hash of your key to a persistent file if you're planning to use your keys to other programming platforms. If you're planning to use your keys only in java, you can serialize the keys. SecretKey, PublicKey and PrivateKey are serializable.

This example demonstrates storing key hash in a file and recreating the key in java.
import java.io.*;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

public class SampleClass{

  public static void main(String[] args) throws
                     IOException,
                     NoSuchAlgorithmException{
    File secretKeyFile = 
    new File("key/my-secret.key");
    
    KeyGenerator keygen =
    KeyGenerator.getInstance("AES");
    keygen.init(192);
    
    SecretKey secretKey = keygen.generateKey();
    
    //write the keys to a persistent file
    if(!secretKeyFile.exists()){
         try(BufferedWriter bw = 
             new BufferedWriter(
             new FileWriter(secretKeyFile))){
             String key =  
             new BigInteger(1, secretKey.getEncoded())
             .toString(16);
             bw.write(key, 0, key.length());
             System.out.println("Key written to file");
             System.out.println(key);
             
             //let gc remove the key as soon as possible
             //After use, don't let the key persist with
             //memory for too long.
             key = null;
         }
     }
     else{
       System.out.println("File/s already exist!");
       return;
     }
    
    //read the keys from the persistent file
    try(FileReader fr = new FileReader(secretKeyFile)){
      StringBuilder sb = new StringBuilder();
      
      int i = 0;
      while((i = fr.read()) != -1)
        sb.append((char)i);
      
      //convert the key hash to array of bytes
      //in order to recreate the secret key using
      //SecretKeySpec class
      SecretKey keyFromFile = 
      new SecretKeySpec(
      HexFormat.of().parseHex(sb.toString()),
      "AES");
      sb = null;
      
      System.out.println("key read from file");
      System.out.println(
      new BigInteger(1, 
      keyFromFile.getEncoded()).toString(16));
    }
    
  }
}

Result
Key written to file
73f6040f39f1...
Key read from file
73f6040f39f1...
HexFormat is a utility class that we can use to parse hex strings. In the example above, we're writing a plain key hash in a persistent file. If you want to obscure key hash, you can use Base64 in java.

HexFormat is introduced in java 17. If you're using java with version lower than 17, HexFormat is not available. This dicussion has a solution for parsing key hashes that works in java with version lower than 17.

If you're planning to use your keys on other platforms and you want to obscure them using Base64, make sure that the platform can decode Base64 characters. SecretKeySpec can be used to construct a SecretKey from a byte array, without having to go through a (provider-based) SecretKeyFactory.

SecretKeySpec is only useful for raw secret keys that can be represented as a byte array and have no key parameters associated with them, e.g., DES or Triple DES keys.

Another way of storing keys is to create a KeyStore. KeyStore can store keys and certificates. In this tutorial, I'm gonna demonstrates on how to store a key for symmetric encryption.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.File;
import java.math.BigInteger;
import java.security.cert.CertificateException;
import java.security.NoSuchAlgorithmException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.KeyStore.PasswordProtection;
import java.security.UnrecoverableKeyException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

public class SampleClass{

  public static void main(String[] args) throws
                          IOException,
                          NoSuchAlgorithmException,
                          CertificateException,
                          KeyStoreException,
                          UnrecoverableKeyException{
    
    KeyGenerator keygen =
    KeyGenerator.getInstance("AES");
    keygen.init(192);
    
    SecretKey secretKey = keygen.generateKey();
    
    KeyStore keystore = KeyStore.getInstance("JCEKS");
    //This is unsecure. Don't hardcode passwords.
    //It's better to put your KeyStore password
    //in a persistent file. Before that, obscure
    //or encrypt your password first.
    char[] pass = "password".toCharArray();
    
    //initialize Keystore
    keystore.load(null, pass);
    
    File source = new File("keystore/my-keystore.jks");
    if(source.exists()){
       System.out.println("KeyStore already exists!");
       return;
    }
    
    //write KeyStore object to a file
    try(FileOutputStream fos = new 
        FileOutputStream(source)){
        keystore.store(fos, pass);
    }
    
    //Initialize KeyStore from the given
    //input stream
    try(FileInputStream fis = new 
        FileInputStream(source)){
        keystore.load(fis, pass);
    }
    
    //add entry to KeyStore
    KeyStore.SecretKeyEntry keyEntry =
    new KeyStore.SecretKeyEntry(secretKey);
    KeyStore.ProtectionParameter entryPass =
    new KeyStore.PasswordProtection(pass);
    keystore.setEntry("secret-key", keyEntry, entryPass);
    
    SecretKey ksSecretKey = 
    (SecretKey)keystore.getKey("secret-key", pass);
    
    System.out.println("Generated Key");
    System.out.println(
    new BigInteger(1, secretKey.getEncoded()).toString(16));
    System.out.println();
    
    System.out.println("Key in KeyStore");
    System.out.println(
    new BigInteger(1, ksSecretKey.getEncoded()).toString(16));
  }
}

Result
Generated Key
e71094e7294563d771...

Key in KeyStore
e71094e7294563d771...
getInstance(String type) returns KeyStore object with the specified type. JCEKS is similar to JKS(Java KeyStore). Although, JCEKS supports symmetric keys whereas JKS only supports asymmetric keys. In terms of security, JCEKS has stronger key protection.

load(InputStream stream, char[] password) Loads the KeyStore contents from the given input stream to the KeyStore intance. If stream argument is null, this method initializes KeyStore with no contents and associates the given password to it. store(OutputStream stream, char[] password) writes the KeyStore object to a persistent file via output stream and protect the file's integrity with the given password.

KeyStore.SecretKeyEntry creates an entry for a secret key.KeyStore.ProtectionParameter is used to protect the contents of a key store. KeyStore.PasswordProtection is used to wrap password that can be implemented in key store.

setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam) saves a KeyStore entry to a KeyStore. alias parameter is the name associated with the entry. We can use alias if we want to select the entry associated with the alias. entry parameter is the entry that we wanna store in the KeyStore. protParam parameter is a protection of KeyStore wrapped into KeyStore.ProtectionParameter.

getKey(String alias, char[] password) retrives a key with the associated alias and the password that we put in the entry of the key. Storing symmetric key in KeyStore is simple. However, Some keys or certificates are kinda tricky to be stored in KeyStore. Another way of create a KeyStore is using the keytool command in java. keytool is not in the scope of this tutorial.

Salted Password

Salted password is a password that is mixed with cryptographic salt. Cryptographic salt or salt for short, is random data that is used as an additional input to a one-way function that hashes data. In other words, we mix salt and a data, password in our case, and then generate a key from the password with mixed salt.

In this tutorial we're gonna be using PBKDF2. bcrypt and scrypt are some of the popular alternatives to PBKDF2 but java doesn't support those algorithm by default. If you wanna use those algorithms, you need a third-party vendor that implements those algorithms.

This example demonstrates PBKDF2.
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class SampleClass{
  public static void main(String[] args)
                throws UnsupportedEncodingException,
                       NoSuchAlgorithmException,
                       InvalidKeySpecException{
    /*Encrypting*/
    String pass = "password";
    byte[] salt = new byte[16];
    SecureRandom sr =
    SecureRandom.getInstance("SHA1PRNG");
    sr.nextBytes(salt);
    int iterations = 10000;
    int keyLength = 512;
    
    SecretKeyFactory keyFactory =
    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
    
    PBEKeySpec spec =
    new PBEKeySpec(pass.toCharArray(), salt,
                   iterations, keyLength);
    SecretKey key = keyFactory.generateSecret(spec);
    byte[] generatedBytes = key.getEncoded();
    String storedHash = 
    new BigInteger(1, generatedBytes).toString(16);
    System.out.println("Stored Hash");
    System.out.println(storedHash);
    System.out.println();
    
    /*Verification*/
    keyFactory = 
    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
    spec = new PBEKeySpec(pass.toCharArray(), salt,
                          iterations, keyLength);
    key = keyFactory.generateSecret(spec);
    generatedBytes = key.getEncoded();
    String inputHash =
    new BigInteger(1, generatedBytes).toString(16);
    System.out.println("Input Hash");
    System.out.println(inputHash);
    System.out.println();
    
    System.out.println("Stored and input hashes equal?");
    System.out.println(storedHash.equals(inputHash));
  }
}

Result
Stored Hash
b2f36d5f92ac1f...

Input Hash
b2f36d5f92ac1f...

Stored and input hashes equal?
true
In the example above, SecureRandom is instantiated with "SHA1PRNG" algorithm. There are other algorithms that are available and they can be found here. "SHA1PRNG" is commonly used PRNG(Pseudo-Random Number Generator) algorithm due to simplicity of SHA-1 algorithm.

When generating hash or cryptographic key, SHA-1 is discouraged to be used because it's unsecure. Since we just want to generate random numbers that can be mixed with our password, "SHA1PRNG" is alright to be used. Next, we set iterations variable to 10,000 and keyLength variable to 512.

PBEKeySpec class can create a key specification with the specification of password-based encryption(PBE). PBKDF2 is a password-based encryption. Thus, The key that is created by PBEKeySpec is compatible with PBKDF2.

PBEKeySpec(char[] password, byte[] salt, int iterationCount, int keyLength) generates a key spec with the specified password mixed with salt, iteration and key length. password parameter is the password that is gonna be mixed with salt. salt parameter is the random bytes that is gonna be mixed with password. Standard salt size for PBKDF2 is at least 64 bits or 8 bytes.

In the example above, I use 16 bytes or 128 bits recommended by The US National Institute of Standards and Technology. Salt is a way to reduce the ability of attackers to break a password using pre-computed hashes such as rainbow tables.

IterationCount parameter is the number of repetitive use of pseudorandom for password along with the salt. This process makes password cracking much more difficult. This process is also known as key stretching.

Organizations have different standards when it comes to the number of iteration count. For example, Apple reportedly used 2000 for iOS 3, and 10000 for iOS 4. OWASP recommended to use 310000 iterations for PBKDF2-HMAC-SHA256 and 120000 for PBKDF2-HMAC-SHA512.

If you want more security from password cracking, you can increase the itearation count anytime. Just remember that the higher the iteration count, the slower the key generation. If computers get faster in the future, attackers can crack passwords fast. To counter that problem, we can just increase the iteration count.

keyLength is the length of the key. Long keys are harder to break. Thus, adding more security to our passwords. For stanrdard applications, length of 512 bits should suffice.

Once we generate a key spec for PBKDF2. We need to convert the key spec into a cryptographic key. To do that, we need to get an instance of SecretKeyFactory. Key factories are used to convert keys (opaque cryptographic keys of type Key) into key specifications (transparent representations of the underlying key material), and vice versa.

SecretKeyFactory.getInstance(String algorithm) returns a SecretKeyFactory object that converts secret keys of the specified algorithm. Available algorithms for SecretKeyFactory provided by java can be found here.

In the example above, I use "PBKDF2WithHmacSHA512" algorithm. This algorithm uses PBKDF2 with HMAC with SHA512. generateSecret(KeySpec keySpec) generates a SecretKey object from the provided key specification. We use SecretKey as a key type of PBKDF2 because PBKDF2 with HMAC is a symmetric encryption.

Once we successfully generated a SecretKey for our password with salt, we need to get its hash in hexadecimal form. To do that, we need to get its byte structure by invoking getEncoded() method and convert the bytes to hexadecimal using BigInteger.

We can use formatHex(byte[] bytes) in HexFormat class to convert array of bytes to hex. However, HexFormat is introduced in java 17. Java versions older than 17 can't use HexFormat.

Now, We need to store the hash of the key in hexadecimal form, salt, iteration count and length of the key because during verification, we need those values. Now, assume that the user is gonna login to our program.

To verify that the password he/she sends to our program is the same as the password he/she sent to our program, we need to create a key with the stored salt, iteration count and key length. Then, get the hash of the new key and compare the new hash to the stored hash. If they're both the same then the user can access his/her account.