JPPF, java, parallel computing, distributed computing, grid computing, parallel, distributed, cluster, grid, cloud, open source, android, .net
JPPF, java, parallel computing, distributed computing, grid computing, parallel, distributed, cluster, grid, cloud, open source, android, .net
JPPF

The open source
grid computing
solution

 Home   About   Features   Download   Documentation   On Github   Forums 

Transforming and encrypting networked data

From JPPF 6.3 Documentation

Jump to: navigation, search
Main Page > Extending and Customizing JPPF > Transforming and encrypting networked data


In JPPF, most of the network traffic is made of serialized Java objects. By default, these serialized objects are sent over the network without any obfuscation or encryption of any sort. This can be considered risky in highly secured environments. To mitigate this risk, JPPF provides a hook that enables transforming a block of data into another block of data, and transform it back into the orignal data (reverse transformation).

To better understand how this mechanism works, let's first have a high-level overview of how JPPF components send and receive messages over the network. A message in JPPF is composed of a number of blocks of data, each block representing a serialized object (or object graph) and immediately preceded by its own length. A message would look like this:

 L1 
    Block1    
     .....     
 Ln 
    Blockn    

Where:

  • Block1, ..., Blockn are separate blocks of data constituting the message
  • L1, ..., Ln are the lengths of each block of data

The data transformation hook allows developers to transform each block of data. The block lengths are always computed by JPPF. For example if the data transformation used is a form of encryption (and decryption for the reverse operation), then everything except the block lengths will be encrypted.

Related sample: “Data Encryption” sample in the JPPF samples pack

The general workflow to implement and deploy a data transformation is as follows:


Step 1: implement the JPPFDataTransform interface

This interface is defined as follows:

public interface JPPFDataTransform {
  // Transform a block of data into another, transformed one
  // This operation must be such that the result of unwrapping the data of the
  // destination must be the equal to the source data
  void wrap(InputStream source, OutputStream destination) throws Exception;

  // Transform a block of data into another, reverse-transformed one
  // This method is the reverse operation with regards to wrap()
  void unwrap(InputStream source, OutputStream destination) throws Exception;
}

One very important thing to note is that the sequential application of the wrap() and unwrap() methods must return exactly the original data.

Also keep in mind that the data transformation is completely stateless. For instance there is no knowledge of where the data comes from or where it is going.

We will now write a data transformation that encrypts data using the DES cryptographic algorithm, based on a 56 bits symetric secret key. This code is available in the related “Data Encryption” sample of the JPPF samples pack. Note that this example is far from totally secure, since the secret key is actually stored with the source code (and in the resulting jar file). It should normally be in a secure location such as a key store. The packaging in the sample is only for demonstration purposes.

Here is our implementation of JPPFDataTransform:

// Data transform that uses the DES cyptographic algorithm with a 56 bits secret key
public class SecureKeyCipherTransform implements JPPFDataTransform {
  // Secret (symetric) key used for encryption and decryption
  private static SecretKey secretKey = getSecretKey();

  // Encrypt the data using streams
  @Override
  public void wrap(InputStream source, OutputStream dest) throws Exception {
    // create a cipher instance
    Cipher cipher = Cipher.getInstance(Helper.getTransformation());
    // initialize the cipher with the key stored in the secured keystore
    cipher.init(Cipher.WRAP_MODE, getSecretKey());
    // generate a new key that we will use to encrypt the data
    SecretKey key = generateKey();
    // encrypt the new key, using the secret key found in the keystore
    byte[] keyBytes = cipher.wrap(key);
    // now we write the encrypted key before the data
    DataOutputStream dos = new DataOutputStream(dest);
    dos.writeInt(keyBytes.length);
    dos.write(keyBytes);

    // get a new cipher for the actual encryption
    cipher = Cipher.getInstance(Helper.getTransformation());
    // init the cipher in encryption mode
    cipher.init(Cipher.ENCRYPT_MODE, key);
    // obtain a cipher output stream
    CipherOutputStream cos = new CipherOutputStream(dest, cipher);
    // finally, encrypt the data using the new key
    transform(source, cos);
    cos.close();
  }

  // Decrypt the data
  @Override
  public void unwrap(InputStream source, OutputStream dest) throws Exception {
    // start by reading the secret key to use to decrypt the data
    DataInputStream dis = new DataInputStream(source);
    // read the length of the key, then the key itself
    int keyLength = dis.readInt();
    byte[] keyBytes = new byte[keyLength];
    dis.read(keyBytes);
    // decrypt the key using the initial key stored in the keystore
    Cipher cipher = Cipher.getInstance(Helper.getTransformation());
    cipher.init(Cipher.UNWRAP_MODE, getSecretKey());
    SecretKey key = (SecretKey) cipher.unwrap(
      keyBytes, Helper.getAlgorithm(), Cipher.SECRET_KEY);

    // get a new cipher for the actual decryption
    cipher = Cipher.getInstance(Helper.getTransformation());
    cipher.init(Cipher.DECRYPT_MODE, key);
    // obtain a cipher input stream and decrypt the data using the new key
    CipherInputStream cis = new CipherInputStream(source, cipher);
    transform(cis, dest);
    cis.close();
  }

  // Generate a secret key
  private SecretKey generateKey() throws Exception {
    return KeyGenerator.getInstance(Helper.getAlgorithm()).generateKey();
  }

  // Transform the specified input source and write it to the specified destination
  private void transform(InputStream source, OutputStream dest) throws Exception {
    byte[] buffer = new byte[8192];
    int n;
    while ((n = source.read(buffer)) > 0) destination.write(buffer, 0, n);
  }

  // Get the secret key used for encryption/decryption
  private static synchronized SecretKey getSecretKey() {
    if (secretKey == null) {
      try {
        // get the keystore password
        char[] password = Helper.getPassword();
        ClassLoader cl = SecureKeyCipherTransform.class.getClassLoader();
        InputStream is = cl.getResourceAsStream(
          Helper.getKeystoreFolder() + Helper.getKeystoreFilename());
        KeyStore ks = KeyStore.getInstance(Helper.getProvider());
        // load the keystore
        ks.load(is, password);
        // get the secret key from the keystore
        secretKey = (SecretKey) ks.getKey(Helper.getKeyAlias(), password);
      } catch(Exception e) {
        e.printStackTrace();
      }
    }
    return secretKey;
  }
}


Step 2: deploy the data transform implementation

The implementation code and related resources must be deployed in the class path of each and every component on the JPPF grid, including servers, nodes, and client applications. If it is not the case, the results are unpredictable and JPPF will probably stop working altogether. The deployment can be made in the form of a jar file or a class folder, the only constraint being that it must be local to the JVM of each JPPF component.


Step 3: hook the implementation to JPPF

This is done by specifying the property jppf.data.transform.class in the JPPF configuration file of each component:

jppf.data.transform.class = <fully qualified name of implementation class>

In our example it would be:

jppf.data.transform.class = org.jppf.example.dataencryption.SecureKeyCipherTransform
Main Page > Extending and Customizing JPPF > Transforming and encrypting networked data



JPPF Copyright © 2005-2020 JPPF.org Powered by MediaWiki