Basically ciphers are implemented similar to digests. But while digests can be used without any security restrictions, ciphers need to be signed when used with JCA. In more detail your cipher implementation must reside in a signed jar file. To sign you need a code signing certificate from Oracle. Application for such a certificate is free of charge, but will take approximately 2 weeks to be processed. You will not get it for fun either as you have to provide your use case.
For the exact procedure follow Oracles step by step guide. To follow the tutorial below I expect you to have your certificate ready. Otherwise you might do the coding but will not be able to use your provider afterwards.
Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.
Step 1: Create key classes
We wil create a Caesar cipher implementation. You could also do ROT13 with it. As ciphers are about encryption/decryption we need some keys. Our key class will be very simple as we just need to store an offset for shifting our letters later on.
Start by creating a new Java Project named com.example.jce.cipher. Then implement the key class com.example.jce.provider.CaesarKey:
package com.example.jce.cipher; import javax.crypto.SecretKey; public class CaesarKey implements SecretKey { private static final long serialVersionUID = -542084808194990775L; private byte mValue; public CaesarKey(byte value) { // converts any value to [0, 26), even negative ones mValue = (byte) (((value % 26) + 26) % 26); } @Override public String getAlgorithm() { return "Caesar"; } @Override public String getFormat() { return getAlgorithm() + " format"; } @Override public byte[] getEncoded() { return new byte[] { mValue }; } }You may also implement a key generator that creates random keys on demand. This is not mandatory for our example but may be useful sometimes. The generator is implemented in com.example.jce.cipher.CaesarkeyGenerator:
package com.example.jce.cipher; import java.security.InvalidAlgorithmParameterException; import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; import javax.crypto.KeyGeneratorSpi; import javax.crypto.SecretKey; public class CaesarKeyGenerator extends KeyGeneratorSpi { SecureRandom mRandomSource; public void engineInit(SecureRandom sr) { mRandomSource = sr; } public void engineInit(AlgorithmParameterSpec ap, SecureRandom sr) throws InvalidAlgorithmParameterException { throw new InvalidAlgorithmParameterException("No parameters supported in this class"); } public SecretKey engineGenerateKey() { if (mRandomSource == null) mRandomSource = new SecureRandom(); return new CaesarKey((byte) Math.abs(mRandomSource.nextInt())); } @Override protected void engineInit(int arg0, SecureRandom sr) { engineInit(sr); } }Step 2: Implement the cipher
Ciphers are implemented extending CipherSpi (as all JCA extensions extend a WhateverSpi class). Implement com.example.jce.cipher.CaesarCipher as follows:
package com.example.jce.cipher; import java.security.AlgorithmParameters; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.CipherSpi; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.ShortBufferException; public class CaesarCipher extends CipherSpi { private Key mKey; private int mOPMode; @Override protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException { return engineUpdate(input, inputOffset, inputLen); } @Override protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { return engineUpdate(input, inputOffset, inputLen, output, outputOffset); } @Override protected int engineGetBlockSize() { return 1; } @Override protected byte[] engineGetIV() { return null; } @Override protected int engineGetOutputSize(int inputLen) { return 0; } @Override protected AlgorithmParameters engineGetParameters() { return null; } @Override protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { mOPMode = opmode; if (key instanceof CaesarKey) mKey = key; else throw new InvalidKeyException("Caesar cipher needs a Caesar key"); } @Override protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { engineInit(opmode, key, random); } @Override protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { engineInit(opmode, key, random); } @Override protected void engineSetMode(String mode) throws NoSuchAlgorithmException { throw new NoSuchAlgorithmException(); } @Override protected void engineSetPadding(String pading) throws NoSuchPaddingException { throw new NoSuchPaddingException(); } @Override protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { byte[] output = new byte[inputLen]; try { engineUpdate(input, inputOffset, inputLen, output, 0); } catch (ShortBufferException e) { // output buffer is always big enough } return output; } @Override protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException { if (output.length - outputOffset < inputLen) throw new ShortBufferException("Buffer too short to hold encrypted data"); for (int index = inputOffset; index < inputOffset + inputLen; index++) output[outputOffset + index - inputOffset] = twistCharacter(input[index], getKey()); return inputLen; } private byte getKey() { // calculate key depending on encryption/decryption if (mOPMode == Cipher.ENCRYPT_MODE) return mKey.getEncoded()[0]; // decode using inverse key return (byte) (26 - mKey.getEncoded()[0]); } private byte twistCharacter(byte data, byte key) { if (Character.isLetter(data)) { // only operate on characters, leave everything else as-is if (Character.isLowerCase(data)) return (byte) (((data - 'a' + key) % 26) + 'a'); else return (byte) (((data - 'A' + key) % 26) + 'A'); } return data; } @Override protected int engineGetKeySize(Key key) throws InvalidKeyException { if (key instanceof CaesarKey) return 1; throw new InvalidKeyException("Caesar cipher needs a Caesar key"); } }This is a straight forward implementation that will encode characters only. Everything else like digits or punctuation will remain unchanged.
Step 3: Create a JCE provider
As for the digest, we need to usa a provider to register our digest in the JRE. Create a new class com.example.jce.cipher.CaesarProvider:
package com.example.jce.cipher; import java.security.Provider; public class CaesarProvider extends Provider { private static final long serialVersionUID = -67123468605911408L; public CaesarProvider() { super("Caesar Cipher", 1.0, "Caesar cipher v1.0"); put("KeyGenerator.Caesar", CaesarKeyGenerator.class.getName()); put("Cipher.Caesar", CaesarCipher.class.getName()); } }At this point we need the certificate requested from Oracle, because a cipher class needs to be nested in a signed jar file. If you do not have such a certificate your quest ends here...
For all the others: export the project as a JAR file and sign it using jarsigner along with your certificate. The commandline looks something like this:
jarsigner -keystore <store location> -storepass <store master password> <JAR file> <keystore alias>
Step 4: Using our provider
Create a new Java project com.example.jce.cipher.consumer. Create a subfolder libs and store your signed JAR file there. Then add this JAR to the Java Build Path.
Now everything is in place to use our cipher:
public static void main(String[] args) { // adding provider dynamically Security.addProvider(new CaesarProvider()); try { String message = "Hello World"; Cipher c = Cipher.getInstance("Caesar"); SecretKey key = new CaesarKey((byte) 2); System.out.println("Message: " + message); c.init(Cipher.ENCRYPT_MODE, key); byte[] encrypted = c.doFinal(message.getBytes()); System.out.println("Encrypted: " + new String(encrypted)); c.init(Cipher.DECRYPT_MODE, key); byte[] decrypted = c.doFinal(encrypted); System.out.println("Decrypted: " + new String(decrypted)); } catch (Exception e) { e.printStackTrace(); } }
Further reading
If you intend to implement your own stuff you should have a look at these links:
- Java Cryptography Architecture
Overview of the JCA framework (Oracle) - How to implement a provider
Detailed step by step guide how to implement your own provider (Oracle) - Java security
Detailed description of the JCA framework with examples (O' Reilly)
I've followed the steps here, have signed my jar, but keep getting :
ReplyDeletejava.security.NoSuchAlgorithmException: Cannot find any provider supporting Caesar
at javax.crypto.Cipher.getInstance(Cipher.java:535)
Any tips ?
Did you use your code signing certificate you got from oracle? If I remember correctly you get this error when the jar is not signed correctly
DeleteThanks for your reply Christian !. I am using my own key pair for signing, no complaints from jarsigner nor from jce/jca API. on the singing AFAIK. How do I get an official Oracle certificate for testing a provider ?
ReplyDeleteThere is a link in the 2nd paragraph, describing the procedure.
Deleterequest to Oracle is sent
DeleteDarn, even for open source projects it's around $300 for a code signing cert. So a self-signed certificate won't work - would https://letsencrypt.org/ help here ?
ReplyDeleteNo, you need a cert signed by oracle, no one else. You get it for free, click on the link 'Oacle step by step guide' in the tutorial
DeleteGot reply from Oracle - they only give a certificate to enterprise entities. Bummer.
ReplyDeleteWith OpenJDK, providers need not be signed to work. But with the code from this blog, I still get
ReplyDeletejava.security.NoSuchAlgorithmException: Cannot find any provider supporting Caesar.
If I turn on debugging with -Djava.security.debug=all I get an additional stack trace :
ReplyDeletejava.lang.Exception: Call trace
at sun.security.jca.ProviderList.loadAll(ProviderList.java:278)
at sun.security.jca.ProviderList.removeInvalid(ProviderList.java:299)
at sun.security.jca.Providers.getFullProviderList(Providers.java:173)
at java.security.Security.insertProviderAt(Security.java:360)
at java.security.Security.addProvider(Security.java:403)
Any tips before i give up ?