Cryptography has always been one of the harder programming subjects. It’s hard (but important) to get right, mathematically complex and we (luckily) don’t need to implement it ourselves very often. 99% of the time battle tested implementations will do, but sometimes you just have to roll your own. Or in my case, just have an irresistible urge to do so.

We’ll go back 2000 years in time and implement the Caesar cipher, one of the oldest and best known symmetric encryption ciphers. Nowadays it’s an extremely unsafe cipher but back in the day it was pretty safe, not in the last place because most people were illiterate anyways. We’ll implement it using JCA and JCE*, specs which provide an SPI (Service Provider Interface) to implement stuff like keystores and hashing/signing/encryption algorithms. What we’ll be doing:

  1. Implement Caesar cipher (extend javax.crypto.CipherSpi).
  2. Implement custom keystore (extend java.security.KeyStoreSpi) to store our Caesar cipher keys.
  3. Implement Security Provider (extend java.security.Provider) to register the cipher and keystore.
  4. Install our custom Security Provider and test it by encrypting and decrypting “Hello World”!

See the java-crypto-provider Github repository for the complete example.

Caesar Cipher

The Caesar cipher was used by Julius Caesar to encrypt messages sent to his troops. It works by shifting all letters in a message by a fixed number of positions. When shifting 1 position, A becomes B, C becomes D, etc. For example, shifting “Hello World” by 1 position gives “Ifmmp Xpsme”. It helps to write a transposition matrix to visualize it. The matrix below is shifting 1 position:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Z A B C D E F G H I J K L M N O P Q R S T U V W X Y

The table below shifts 2 positions:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Y Z A B C D E F G H I J K L M N O P Q R S T U V W X

Our encryption function looks like this:

char encrypt(char character, int shift) {
    var base = findBase(character);
    return base == null ? character : (char) ((character - base + shift) % 26 + base);
}

Character findBase(char character) {
    // For upper/lowercase support
    if (character >= 'a' && character <= 'z') {
        return 'a';
    } else if (character >= 'A' && character <= 'Z') {
        return 'A';
    } else {
        return null;
    }
}

To implement this cipher you extend javax.crypto.CipherSpi:

CaesarCipher.java
package nl.reinkrul.secprov;

import javax.crypto.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.AlgorithmParameterSpec;
import java.util.function.BiFunction;

public class CaesarCipher extends CipherSpi {

    private static final Charset CHARSET = StandardCharsets.UTF_8;
    private CaesarKey key;
    private StringBuilder output;
    private BiFunction<Character, Integer, Character> func;

    @Override
    protected int engineGetOutputSize(int inputLen) {
        return inputLen;
    }

    @Override
    protected byte[] engineGetIV() {
        return new byte[0];
    }

    @Override
    protected AlgorithmParameters engineGetParameters() {
        return null;
    }

    @Override
    protected void engineInit(final int opmode, final Key key, final SecureRandom random) throws InvalidKeyException {
        assertModeSupported(opmode);
        setKey(key);
        if (opmode == Cipher.ENCRYPT_MODE) {
            this.func = CaesarCipher::encrypt;
        } else {
            this.func = CaesarCipher::decrypt;
        }
        output = new StringBuilder();
    }

    static char decrypt(final char character, final int shift) {
        final Character base = findBase(character);
        if (base == null) {
            return character;
        }
        final int val = character - base - shift % 26;
        if (val < 0) {
            return (char)(26 + val + base);
        } else {
            return (char)(val + base);
        }
    }

    static char encrypt(final char character, final int shift) {
        final Character base = findBase(character);
        return base == null ? character : (char) ((character - base + shift) % 26 + base);
    }

    private static Character findBase(final char character) {
        if (character >= 'a' && character <= 'z') {
            return 'a';
        } else if (character >= 'A' && character <= 'Z') {
            return 'A';
        } else {
            return null;
        }
    }

    @Override
    protected void engineInit(final int opmode, final Key key, final AlgorithmParameterSpec params, final SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException {
        engineInit(opmode, key, random);
    }

    @Override
    protected void engineInit(final int opmode, final Key key, final AlgorithmParameters params, final SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException {
        engineInit(opmode, key, random);
    }

    @Override
    protected byte[] engineUpdate(final byte[] input, final int inputOffset, final int inputLen) {
        final String str = new String(input, inputOffset, inputLen, CHARSET);
        for (int i = 0; i < str.length(); i++) {
            output.append(func.apply(str.charAt(i), key.getShift()));
        }
        return output.toString().getBytes(CHARSET);
    }

    @Override
    protected int engineUpdate(final byte[] input, final int inputOffset, final int inputLen, final byte[] output, final int outputOffset) throws ShortBufferException {
        return 0;
    }

    @Override
    protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException {
        return new byte[0];
    }

    @Override
    protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException {
        return 0;
    }

    @Override
    protected int engineGetBlockSize() {
        return 0;
    }

    @Override
    protected void engineSetPadding(final String padding) throws NoSuchPaddingException {
        throw new UnsupportedOperationException();
    }

    @Override
    protected void engineSetMode(final String mode) throws NoSuchAlgorithmException {
        throw new UnsupportedOperationException();
    }

    private void setKey(final Key key) {
        if (key instanceof CaesarKey) {
            this.key = (CaesarKey) key;
        } else {
            throw new IllegalArgumentException("Expected a " + CaesarKey.class.getSimpleName() + " but got " + key.getClass().getName());
        }
    }

    private void assertModeSupported(final int opmode) {
        if (opmode != Cipher.DECRYPT_MODE && opmode != Cipher.ENCRYPT_MODE) {
            throw new UnsupportedOperationException("Unsupported mode: " + opmode);
        }
    }
}

To encrypt/decrypt using the cipher you need a secret key. Implement the interface javax.crypto.SecretKey for this purpose, which simply holds the number of shifts:

CaesarKey.java
package nl.reinkrul.secprov;

import javax.crypto.SecretKey;
import java.nio.ByteBuffer;

public class CaesarKey implements SecretKey {

    private static final int KEY_LENGTH = Integer.SIZE / 8;
    private final int shift;

    public CaesarKey(final byte[] encoded) {
        if (encoded.length != KEY_LENGTH) {
            throw new IllegalArgumentException("Invalid key length.");
        }
        this.shift = ByteBuffer.wrap(encoded).getInt();
        assertShiftValid();
    }

    public CaesarKey(final int shift) {
        this.shift = shift;
        assertShiftValid();
    }

    public String getAlgorithm() {
        return "CAESAR";
    }

    public String getFormat() {
        return "CAESAR";
    }

    public byte[] getEncoded() {
        return ByteBuffer.allocate(KEY_LENGTH).putInt(shift).array();
    }

    int getShift() {
        return shift;
    }

    private void assertShiftValid() {
        if (shift < 1) {
            throw new IllegalArgumentException("shift should be >= 1");
        }
    }
}

Custom Keystore

Next, we’ll need a keystore to store our Caesar cipher keys in. Since keystores are accessed using aliases and our secret keys are just integers, we’ll be persisting them as a property list. Normally you would be encrypting the keystore’s content using the provided password, but for the sake of simplicity we won’t.

To implement a keystore you extends java.security.KeyStoreSpi:

CustomKeyStore.java
package nl.reinkrul.secprov;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.Properties;
import java.util.stream.Collectors;

public class CustomKeyStore extends KeyStoreSpi {

    private final Properties keys = new Properties();

    public Key engineGetKey(final String alias, final char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException {
        final String value = keys.getProperty(alias);
        if (value == null) {
            throw new UnrecoverableKeyException("Unknown key: " + alias);
        }
        try {
            return new CaesarKey(Integer.parseInt(value));
        } catch (final NumberFormatException e) {
            // Exception is ignored.
            throw new UnrecoverableKeyException("Key is invalid: " + alias);
        }
    }

    public Date engineGetCreationDate(final String alias) {
        // Not supported, so return Unix epoch
        return new Date(0);
    }

    public void engineSetKeyEntry(final String alias, final Key key, final char[] password, final Certificate[] chain) throws KeyStoreException {
        if (key instanceof CaesarKey) {
            // TODO
            keys.put(alias, String.valueOf(((CaesarKey) key).getShift()));
        } else {
            throw new KeyStoreException("Key not supported: " + key.getClass().getName());
        }
    }

    public void engineSetKeyEntry(final String alias, final byte[] key, final Certificate[] chain) throws KeyStoreException {
        // TODO
        engineSetKeyEntry(alias, new CaesarKey(key), null, chain);
    }

    public void engineSetCertificateEntry(final String alias, final Certificate cert) throws KeyStoreException {
        throw new KeyStoreException("Certificates are not supported.");
    }

    public void engineDeleteEntry(final String alias) throws KeyStoreException {
        keys.remove(alias);
    }

    public Enumeration<String> engineAliases() {
        return Collections.enumeration(keys.keySet().stream().map(Object::toString).collect(Collectors.toList()));
    }

    public boolean engineContainsAlias(final String alias) {
        return keys.containsKey(alias);
    }

    public int engineSize() {
        return keys.size();
    }

    public boolean engineIsKeyEntry(final String alias) {
        return keys.containsKey(alias);
    }

    public Certificate[] engineGetCertificateChain(final String alias) {
        // Certificates not supported
        return new Certificate[0];
    }

    public Certificate engineGetCertificate(final String alias) {
        // Certificates not supported
        return null;
    }

    public boolean engineIsCertificateEntry(final String alias) {
        // Certificates not supported
        return false;
    }

    public String engineGetCertificateAlias(final Certificate cert) {
        // Certificates not supported
        return null;
    }

    public void engineStore(final OutputStream stream, final char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
        keys.store(stream, null);
    }

    public void engineLoad(final InputStream stream, final char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
        if (stream != null) {
            keys.load(stream);
        }
    }
}

Setting up a Security Provider

When using a cryptographic service a Security Provider is used under the hood, which you can implement by extending java.security.Provider. It defines what keystores and algorithms can be used. Ours looks like this:

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Provider;

public class CustomProvider extends Provider {

    public static final String NAME = "Custom";

    public CustomProvider() {
        super(NAME, "1.0", "Custom Java Security Provider");
        AccessController.doPrivileged((PrivilegedAction<?>) () -> {
            // Install custom keystore
            put("KeyStore.Custom", CustomKeyStore.class.getName());

            // Install Caesar cipher
            put("Cipher.Caesar", CaesarCipher.class.getName());
            return null;
        });
    }
}

We’re registering both a keystore type and a cipher. Since that is a privileged action, we need to wrap it in a AccessController.doPrivileged call in case a Java Security Manager is installed.

Testing the keystore and cipher

Now our keystore and cipher are ready to use, but we still have to register the security provider. There are a few ways to do this, but we’ll do it programmatically**:

Security.addProvider(new CustomProvider());

Then you can use the cipher to encrypt strings:

var key = new CaesarKey(20);
var cipher = Cipher.getInstance("Caesar");
cipher.init(Cipher.ENCRYPT_MODE, key, new SecureRandom());
// Should print: Byffi, Qilfx!
System.out.println(new String(cipher.update("Hello, World!".getBytes())));

CaesarCipherTest.java
package nl.reinkrul.secprov;

import org.junit.BeforeClass;
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import java.nio.charset.StandardCharsets;
import java.security.*;

import static org.junit.Assert.assertEquals;

public class CaesarCipherTest {

    @BeforeClass
    public static void setUp() throws Exception {
        Security.addProvider(new CustomProvider());
    }

    @Test
    public void test() throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException {
        final CaesarKey key = new CaesarKey(20);
        final String cipherText = encrypt(key, "Hello, World!");
        assertEquals("Byffi, Qilfx!", cipherText);
        assertEquals("Hello, World!", decrypt(key, cipherText));
    }

    private String decrypt(final CaesarKey key, final String cipherText) throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException {
        final Cipher cipher = Cipher.getInstance("Caesar");
        cipher.init(Cipher.DECRYPT_MODE, key, new SecureRandom());
        return new String(cipher.update(cipherText.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
    }

    private String encrypt(final CaesarKey key, final String plainText) throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException {
        final var cipher = Cipher.getInstance("Caesar");
        cipher.init(Cipher.ENCRYPT_MODE, key, new SecureRandom());
        return new String(cipher.update(plainText.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
    }

    @Test
    public void testSingleCharacters() {
        // Test lowercase
        test('b', 'a', 1);
        test('c', 'a', 2);
        test('z', 'a', 25);
        test('a', 'a', 26);
        test('b', 'a', 27);

        // Test uppercase
        test('B', 'A', 1);
        test('C', 'A', 2);
        test('Z', 'A', 25);
        test('A', 'A', 26);
        test('B', 'A', 27);

        // Test other characters
        test('\n', '\n', 2);
        test('1', '1', 2);
        test('-', '-', 2);
    }

    private void test(final char cipher, final char plain, final int shift) {
        final char encrypted = CaesarCipher.encrypt(plain, shift);
        assertEquals(cipher, encrypted);
        final char decrypted = CaesarCipher.decrypt(encrypted, shift);
        assertEquals(plain, decrypted);
    }
}

That’s it!

Footnotes

* JCA: Java Cryptography Architecture, everything in java.security. JCE: Java Cryptography Extension, everything in javax.crypto.
** When running in a controlled application server you might have to preconfigure your provider.