This Java library offers a small set of classes to perform basic cryptographic operations (e.g. symmetric/asymmetric encryption, key generation, hashing).
The core package, called ca.uqac.lif.crypto
, merely provides interfaces for these concepts (Key
, SymmetricCipher
, HashFunction
, etc.), without being tied to any specific implementation (not even Java's own cryptographic classes). This core package can then be combined with any extension implementing these interfaces using an arbitrary encryption library. For example:
- The
Java
extension provides a package implementing ciphers using Java's internal cryptographic functionalities. - The
Apache
extension implements alternate version of ciphers that rely on the Apache Commons Crypto and Codec libraries instead. - The
Stubs
extension contains ciphers that simulate encryption without actually performing it (more on that later). It can prove useful for development, testing and demonstration purposes. - The
Azrael
extension
Let us first look a examples of the common features one expects from a crypto library. As one shall see, most of these operations in Cryptools reduce to one-liners.
Generate a key for the AES algorithm and encrypt a string, first converted to an array of bytes:
AESKey k = AES.generator.generateKey();
byte[] encrypted = (byte[]) AES.instance.encrypt(k, "Hello world".getBytes());
Print the byte array as a hex string, then decypt it using the key created above:
System.out.println(ByteArray.toHexString(encrypted));
String s = new String(AES.instance.decrypt(k, encrypted));
Generate an RSA key pair and encrypt a string with the public key:
RSAKeyPair p = RSA.generator.generateKeyPair();
byte[] encrypted = (byte[]) RSA.instance.encrypt(p.getPublicKey(), "Hello world".getBytes());
Calculate the SHA-2 hash of a string:
byte[] h = SHA2.instance.getDigest("Hello world".getBytes());
Hash the password for a shadow file, similar to mkpasswd
command (requires the Apache extension):
String h = GnuCrypt.instance.getDigest("thepassword", "$1$aFGiJR0E");
System.out.println(h); // $1$Z58Nnbh2$d.ScTU1V0DUw67QrwhqYL1
The previous operations correspond to standard usage of a cryptography library and are hardly surprising. In the following, we discuss some less common features that are provided by the Cryptools library.
The Stubs extension of Cryptools provides symmetric and asymmetric cipher classes that do not perform any encryption, and only "pretend" that an operation is done on the input. This can prove particularly useful for development and debugging, as it is possible to inspect the contents of the encrypted objects that are being exchanged, and ensure that the correct operations are done using the appropriate keys. For example:
KeyPair p = DummyAsymmetricCipher.generator.generateKeyPair("X", "X");
Object encrypted = DummyAsymmetricCipher.instance.encrypt(p.getPublicKey(), "Hello world");
System.out.println(encrypted);
Here, the output of encrypt
is an instance of EncryptedObject
, which simply encapsulates the input with the key name used for the encryption. Printing this object to the console produces the output:
E[PU_X,Hello world]
which indeed shows that what this object contains is the encryption of "Hello world" with the public key "PU_X". The object can then be "decrypted" in the same way; calling...
String recovered = (String) DummyAsymmetricCipher.instance.decrypt(p.getPrivateKey(), encrypted);
...puts "Hello world" back into the variable.
Although no actual encryption is performed, those stubs still reproduce the properties of the encryption algorithms they replace. For example, trying to decrypt the object using the wrong key is impossible:
// Throws a CryptoException, as we should use the private key to decrypt
String recovered = (String) DummyAsymmetricCipher.instance.decrypt(p.getPublicKey(), encrypted);
// Throws a CryptoException, as we use a key from the wrong key pair
KeyPair p2 = DummyAsymmetricCipher.generator.generateKeyPair("Y", "Y");
String recovered = (String) DummyAsymmetricCipher.instance.decrypt(p2.getPublicKey(), encrypted);
Values can also be sent to a dummy hash function:
Object h = DummyHashFunction.instance.getDigest("Hello world");
The contents of h
is an instance of HashValue
, which again behaves as expected of a hash function:
System.out.println(h);
System.out.println(h.equals(DummyHashFunction.instance.getDigest("foobarbaz")));
prints out:
H(Hello world)
false
That is, the hash of two different inputs produces two objects that are not equal.
These constructs can be nested at will:
Object o = DummyAsymmetricCipher.encrypt(p.getPublicKey(),
DummyHashFunction.instance.getDigest("Hello world"));
System.out.println(o);
produces:
E[PU_X,H(Hello world)]
As mentioned, one of the main purposes of these mock classes is to use them as drop-in replacements of actual algorithms during the testing and development phase of a project.
The encrypt
and decrypt
methods of of SymmetricCipher
and AsymmetricCipher
accept arbitrary objects, and not ony byte arrays. As a matter of fact, some ciphers such as AES or RSA throw a CryptoException
if given anything else. This can make encryption cumbersome, as everything needs to be converted to low-level byte arrays, effectively requiring manual serializtion of objects in order to be encrypted.
The Azrael extension provides the AzraelSymmetricCipher
, which uses the Azrael serialization library to convert objects to/from a serialized form which can be turned into a byte array and then passed to a "regular" cipher. Hence, the AzraelJsonCipher
serializes an object into a JSON string, encrypts the byte contents of this string, and performs the reverse operation on decryption. Thus, given a class like the following:
class C {
public String foo;
public int bar;
public List<Integer> baz;
public C(String foo, int bar, List<Integer> baz) {
this.foo = foo;
this.bar = bar;
this.baz = baz;
}
}
it is possible to perform the following:
C obj = new C("hello", 42, Arrays.asList(3, 1, 4));
AzraelObjectCipher cipher = new AzraelJsonCipher(DES.instance);
byte[] encrypted = (byte[]) cipher.encrypt(k, obj);
This effectively serializes obj
into JSON, and encrypts the byte array with DES. Recovering the object is possible:
C recovered = (C) cipher.decrypt(k, encrypted);
System.out.println(recovered.baz); // Prints [3, 1, 4]
This technique makes it possible to apply an arbitrary symmetric cipher on arbitrary objects (provided they serialize properly with Azrael). The intermediate format to which objects are serialized is irrelevant; JSON was only used as an example.
With Java's security
package, saving and loading keys is cumbersome. One must:
- Instantiate a
KeyStore
object (keeping in mind that there are multiple versions of this object that are only distinguished by the string used to instantiate) - Load an empty key store (?!?)
- Add a name-value entry containing the key to the key store
- Serialize the key store to an
ObjectOutputStream
KeyStore
does provide a few more features (such as password protection of individual entries), but remains a complicated mechanism as far as dumping the contents of a key is concerned. What is more, the key's contents is entangled within the binary serialization of the key store itself, and native object serialization in Java is notoriously flaky (for example, objects cannot always be recovered if using a different JVM version than the one used for serialization).
Cryptools does away with this mechanism; many types of keys provided by the library can be serialized using Azrael. This is the case of all keys of the Java extension (which wraps Java's own keys into Cryptools objects). For instance:
KeyPair p = RSA.generator.generateKeyPair();
String serialized = JsonStringPrinter.toJson(p);
The string s
is a plain JSON object that can be saved wherever (suggestion: use a FileSystem
object from the lif-fs library). Recovering the key pair is also possible:
KeyPair p2 = (KeyPair) JsonStringReader.fromJson(serialized);
PublicKey pu = p2.getPublicKey(); // etc.
This mechanism does not rely on the binary serialization of a KeyStore
object, and is thus exempt from the issues mentioned above.
Cryptools allows users to create their own ciphers. The Examples folder contains a simple example showing how one can implement a transposition cipher in a few lines of code. The same can be done with hash functions and asymmetric encryption.
One can build a "short" version of another hash function producing a byte array. The ShortHashFunction
produces a "simpler" hash function by truncating the digest produced by another hash function:
HashFunction f = new ShortHashFunction(MD5.instance);
System.out.println(ByteArray.toHexString(f.getDigest("Hello world".getBytes())));
produces 5EB63BBB
(the first 8 bytes of the MD5 digest of "Hello world").
Again, the main purpose of such a function is to avoid handling long byte arrays during development and testing.
Cryptools was written by Sylvain Hallé, full professor at Université du Québec à Chicoutimi, Canada. Part of this work has been funded by the Canada Research Chair in Software Specification, Testing and Verification and the Natural Sciences and Engineering Research Council of Canada.