Thursday, October 24, 2013

Java Cryptography Architecture and common encryption usages

JCA can be really tricky to perform simple, common tasks. After latest usage of java cryptography I'd like to present below the simplest usage of cryptography methods, involving mainly symmetric (AES) and asymmetric (RSA/DSA) encryption plus some helper methods.

I don't use specific JCA provider, but let it choose appropriate one. If you want to select specific provider, you must provide additional parameters, usually to getInstance() methods of various algorithm elements. In examples I use RSA1024 (you might change it to DSA) and AES256.

Note about AES256 - to enable this for your VM you need Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files. With default policy you can only use AES128.

The amount of "things can go wrong" is pretty big, so if you write your own cryptography util, there's a good thing to wrap JCA exception in something that can be easy caught in the user code.

public class EncryptionException extends Exception {
 
 public EncryptionException() {
 }
 
 public EncryptionException(String message) {
  super(message);
 }
 
 public EncryptionException(String message, Throwable cause) {
  super(message, cause);
 }
 
 public EncryptionException(Throwable cause) {
  super(cause);
 }
 
}

Firstly a simple thing, BASE64 encoding wrappers (to be able to switch implementation, what of course will be never used):

public static String encodeBase64(byte[] data) {
 return new BASE64Encoder().encode(data);
}
 
public static byte[] decodeBase64(String data) throws EncryptionException {
 try {
  return new BASE64Decoder().decodeBuffer(data);
 } catch (IOException e) {
  throw new EncryptionException("Error decoding base64", e);
 }
}

How to generate RSA 1024 keys with SecureRandom:

/**
 * Generates new keypair
 */
public static KeyPair generateKeys() throws EncryptionException {
 try {
  KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
  SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
  keyGen.initialize(1024, random);
  return keyGen.generateKeyPair();
 } catch (Exception e) {
  throw new EncryptionException("Error generating keypair", e);
 }
}

How to encrypt with asymmetric key (public or private):

/**
 * Encrypts data with key
 */
public static byte[] encryptAsymmetric(Key key, byte[] data)
throws EncryptionException {
 try {
  Cipher cipher = Cipher.getInstance("RSA");
  cipher.init(Cipher.ENCRYPT_MODE, key);
  return cipher.doFinal(data);
 } catch (Exception e) {
  throw new EncryptionException("Error key encrypting", e);
 }
}

For above, how to decrypt with asymmetric key (public or private, the different one than used for encryption):

/**
 * Decrypts data with key
 */
public static byte[] decryptAsymmetric(Key key, byte[] data)
throws EncryptionException {
 try {
  Cipher cipher = Cipher.getInstance("RSA");
  cipher.init(Cipher.DECRYPT_MODE, key);
  return cipher.doFinal(data);
 } catch (Exception e) {
  throw new EncryptionException("Error key decrypting", e);
 }
}

How to build symmetric key for AES256 encryption:

/**
 * Builds a random secret key for symmetric algorithm
 */
public static Key buildSymmetricKey() throws EncryptionException {
 try {
  KeyGenerator keyGen = KeyGenerator.getInstance("AES");
  keyGen.init(256, SecureRandom.getInstance("SHA1PRNG"));
  return keyGen.generateKey();
 } catch (Exception e) {
  throw new EncryptionException("Error generating secret key", e);
 }
}

The building of the AES key based on user password is a little tricky. You need to have a salt (N-bytes array) that is used to generate a proper AES key (with a proper length). If you need to recover this key in the future, using just the same user password, you need to use exactly the same salt, so it's probably best to hardcode it somewhere and use for further keys generation (random 8 bytes):

private static byte[] SALT = new byte[]{
 (byte) 0xa1, (byte) 0x22, (byte) 0x33, (byte) 0xa4,
 (byte) 0x11, (byte) 0x22, (byte) 0x12, (byte) 0x22};
 
/**
 * Builds a secret key for symmetric algorithm recoverable by password
 */
public static Key buildSymmetricKey(String password)
throws EncryptionException {
 try {
  SecretKeyFactory factory =
   SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
  KeySpec spec = new PBEKeySpec(password.toCharArray(), SALT, 256,
   256);
  SecretKey tmp = factory.generateSecret(spec);
  return new SecretKeySpec(tmp.getEncoded(), "AES");
 } catch (Exception e) {
  throw new EncryptionException("Error encoding secret key", e);
 }
}

Now, how to encrypt the arbitrary length data block with AES:

/**
 * Encrypts data with symmetric algorithm and password
 */
public static byte[] encryptSymmetric(Key key, byte[] data)
throws EncryptionException {
 try {
  Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  cipher.init(Cipher.ENCRYPT_MODE, key);
  return cipher.doFinal(data);
 } catch (Exception e) {
  throw new EncryptionException("Error symmetric encrypting", e);
 }
}

Note, that I use ECB as block cipher mode of operation. This is not the safest one, better would be CBC (refer wikipedia). But if you need to recover your data only by AES key, you can't do this. To recover from CBC you need to store your CBC initialization vector together with the password. So, I use ECB for this simple example, to make all it working only with keys.

The decryption for above symmetric encryption is similar:

/**
 * Decrypts data with symmetric algorithm and password
 */
public static byte[] decryptSymmetric(Key key, byte[] data)
throws EncryptionException {
 try {
  Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  cipher.init(Cipher.DECRYPT_MODE, key);
  return cipher.doFinal(data);
 } catch (Exception e) {
  throw new EncryptionException("Error symmetric descrypting", e);
 }
}

This is all about working encryption/decryption. Now about storing the keys in db. If you'd like to use BASE64 encoding or even to hold everything in BLOB-s, methods below will be useful.

Converting the key to BASE64 is easy:

/**
 * Converts key to base64 encoded string
 */
public static String keyToString(Key key) {
 return encodeBase64(key.getEncoded());
}

But recovering the key from BASE64 or byte[] is another tricky part:

/**
 * Converts base64 encoded string to assymetric key
 *
 * @param publicKey if true returns public key, private key otherwise
 */
public static Key asymmetricKeyFromString(String s, boolean publicKey)
throws EncryptionException {
 return asymmetricKeyFromBytes(decodeBase64(s), publicKey);
}
 
/**
 * Converts bytes to assymetric key
 *
 * @param publicKey if true returns public key, private key otherwise
 */
public static Key asymmetricKeyFromBytes(byte[] bytes, boolean publicKey)
throws EncryptionException {
 try {
  if (publicKey) {
   return KeyFactory.getInstance("RSA").generatePublic(
    new X509EncodedKeySpec(bytes));
  } else {
   return KeyFactory.getInstance("RSA").generatePrivate(
    new PKCS8EncodedKeySpec(bytes));
  }
 } catch (Exception e) {
  throw new EncryptionException("Can't decode assymetric key", e);
 }
}

The same for symmetric key looks much easier:

/**
 * Converts base64 encoded string to symmetric key
 */
public static Key symmetricKeyFromString(String s) throws
EncryptionException {
 return symmetricKeyFromBytes(decodeBase64(s));
}
 
/**
 * Converts bytes to symmetric key
 */
public static Key symmetricKeyFromBytes(byte[] bytes)
throws EncryptionException {
 return new SecretKeySpec(bytes, "AES");
}

Sometimes you also need to convert your private/public keys to PEM format for exporting. Without bouncycastle you need your own method for this:

public static String getPem(Key key) {
 StringBuilder sb = new StringBuilder();
 if (key instanceof PrivateKey || key instanceof PublicKey)
  sb.append(String.format("-----BEGIN %s %s KEY-----\n", "RSA",
   key instanceof PublicKey ? "PUBLIC" : "PRIVATE"));
 else
  sb.append("-----BEGIN KEY-----");
 sb.append(encodeBase64(key.getEncoded()));
 if (key instanceof PrivateKey || key instanceof PublicKey)
  sb.append(String.format("\n-----END %s %s KEY-----", "RSA",
   key instanceof PublicKey ? "PUBLIC" : "PRIVATE"));
 else
  sb.append("\n-----END KEY-----");
 return sb.toString();
}

And this was just last example of simple Java cryptography API.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.