Cryptography Toolkit for JXTA Technology

 

William Yeager

Senior Software Architect

Project JXTA

 


 

Introduction:

With the first release of JXTA Technology we have implemented a basic set of security algorithms using a modified version of the javacard.security 2.1 platform APIs. The documentation for the latter API's can be downloaded at:

http://www.javasoft.com/products/javacard/javacard21.html

We have chosen the javacard API's for reasons of simplicity, and the desire to be able to do multiple platform implementations. This includes small, mobile, wireless devices such as PDA's and cellular telephones.

Our current implementation supports the following algorithms:

RSA with PKCS#1 padding [rfc2313], RC4, SHA-1, MD5 and a SHA-1 based pseudo-random number generator. The project is recent, and "still under construction." Changes are to be expected.

Given this basic cryptography foundation that we've put in place, we have the following as work in progress:

  1. To create secure JXTA pipes which will enable privacy for all code and data (CODAT) in JXTA messages,
  2. To insure CODAT authenticity, and integrity,
  3. To permit transactions between peers that cannot be repudiated.

 

(1) will be an implementation of the IETF Transport Layer Security (TLS) [rfc2246] using the JXTA Technology protocols. Thus, one will be able to have, for example, intra-peer group privacy for chat sessions, instant-p2p, and content transfer. While insuring that the content that is sent is also the content that is received, and that the sender is authentic; (2) is a requirement for the exchange of code between peer group members, and to guarantee that local CODAT has not been surreptitiously modified. With the addition of (3) a financial peer group can be created with digital signatures in place to guarantee the non-deniability of transactions.

What follows is a high level overview of the implementation's APIs with examples of how to use each of the above algorithms. This is not intended to replace Java[tm] documentation, and can be viewed as an introduction to help one understand the organization of the code itself.

 

 

The KeyBuilder:

Where Key is the base interface for all keys, the KeyBuilder class creates Key objects. So far we have the SecretKey, RSAPrivateKey, and RSAPublicKey extensions of Key. As an implementation of Key we have SecretKey which is used by symmetric key cipher implementations like RC4. RSAKey implements both RSAPrivateKey and RSAPublicKey.

Since KeyBuilder is a mini Key Factory, it defines both key types and key sizes. Permissible types and sizes at this time are:

// Our known key types

public static final byte TYPE_RSA_PUBLIC = 1;

public static final byte TYPE_RSA_PRIVATE = 2;

public static final byte TYPE_RSA = (TYPE_RSA_PUBLIC+TYPE_RSA_PRIVATE);

public static final byte TYPE_DES = 4;

public static final byte TYPE_RC4 = 8;

// Our known key sizes RC4 and RSA sizes are for export

// as defined in rfc2246.

public static final short LENGTH_RC4 = 128;

public static final short LENGTH_RSA_MIN = 384;

public static final short LENGTH_RSA_512 = 512;

Examples of calls are:

RSAKey rKey = (RSAKey)KeyBuilder.buildKey(KeyBuilder.TYPE_RSA,

KeyBuilder.LENGTH_RSA_512,

false);

SecretKey sKey = (SecretKey)KeyBuilder.buildKey(KeyBuilder.TYPE_RC4,

KeyBuilder.LENGTH_RC4,

false);

 

The JxtaCrypto Suite:

The security algorithms have, rather than the traditional factory, what we chose to call a suite. With the former one can write modules and load them without modification of the factory code. To add new security algorithms to the suite, one must modify the JxtaCryptoSuite class itself. This approach has been chosen to keep the code base small, and portable across multiple devices as mentioned above. Clearly, on a mobile phone, given the flash memory, processing, and bandwidth limitations, it is preferable to install a single, small jar containing only the required algorithms. And in this case to have multiple jars stored on a server from which one can select and install the jar implementing the desired security algorithm profile. Such scenarios must be possible with our security code.

An application can request a suite using predefined algorithm profiles. Each profile has members, and one can alternatively chose to request a suite of individual members. As of the writing of this document we have 3 profiles. These are defined as follows:

PROFILE_RSA_RC4_SHA1

PROFILE_RSA_RC4_MD5

PROFILE_PROFILE_RSA_RC4_SHA1_MD5

If one also wishes to augment the suite with either a digital signature, or message authentication algorithm (MAC), then additional profiles are supported. First for signatures we have:

ALG_RSA_SHA1_PKCS1

ALG_RSA_MD5_PKCS1

And for MACS:

ALG_RC4_SHA1

ALG_RC4_MD5

Thus, with the following code,

RSAKey rKey = (RSAKey)KeyBuilder.buildKey(KeyBuilder.TYPE_RSA,

KeyBuilder.LENGTH_RSA_512,

false);

JxtaCrypto suite = new JxtaCryptoSuite(JxtaCrypto.PROFILE_RSA_RC4_SHA1,

rKey,

Signature.ALG_RC4_SHA1,

MAC.ALG_RC4_SHA1);

PublicKeyAlgorithm pkey = suite.getJxtaPublicKeyAlgorithm(); // RSA algorithm

Cipher cipher = suite.getJxtaCipher(); // RC4

Hash sha1 = suite.getJxtaHash(Hash.ALG_SHA1); // SHA1 digest

Signature sig = suite.getJxtaSignature(); // Digital signature

MAC msha1 = suite.getJxtaMAC(); // Message authentication code

one creates a suite for the desired profiles. One may also select single members of any given suite:

 

JxtaCrypto suite = new JxtaCryptoSuite(JxtaCrypto.MEMBER_SHA1+ JxtaCrypto.MEMBER_MD5,

Null, (byte) 0, (byte) 0);

Hash sha1 = suite.getJxtaHash(Hash.ALG_SHA1);

Hash md5 = suite.getJxtaHash(Hash.ALG_MD5);

Finally, one can circumvent the suite itself if desired. For example:

Hash sha1 = new SHA1Hash();

 

 

 

 

 

Using Keys:

As mentioned above, SecretKey is used for symmetric cipher algorithms like RC4 and DES. The following example illustrates the use of SecretKey with RC4. Here, we assume we have a JxtaCrptoSuite object suite instantiated with the appropriate profile. The exception code is not included here. For examples see the Test*.java programs in the security project’s source implementation tree.

SecretKey k1 = (SecretKey)KeyBuilder.buildKey(KeyBuilder.TYPE_RC4,

KeyBuilder.LENGTH_RC4,

false);

// get our cipher algorithm

Cipher rc4 = suite.getJxtaCipher();

// ourKey is a byte array containing the key.

// setKey will guarantee that the key size constraints set

// in KeyBuilder are followed.

k1.setKey(ourKey, 0);

rc4.init(k1, Cipher.MODE_ENCRYPT);

Note that the caller knows the secret key beforehand. With RSA it is a bit more complicated because both the public and private keys must be initially calculated as is seen below:

// Get the RSAKey and RSA public key algorithm

RSAKey rKey = (RSAKey)KeyBuilder.buildKey(KeyBuilder.TYPE_RSA,

KeyBuilder.LENGTH_RSA_512,

False);

// Instantiate the suite passing rKey as a parameter

JxtaCrypto suite = new JxtaCryptoSuite(JxtaCrypto.profile, rKey, (byte)0, (byte)0);

// Get an the rsa public key algorithm

RSA rsa = suite.getJxtaPublicKeyAlgorithm();

// First we set our public key. Order is important because the

// private key uses public key calculated values.

rsa.setPublicKey();

rsa.setPrivateKey();

// Now we get these keys for later use. What is done with them

// is system and application dependent.

RSAPublickeyData rPublicD = (RSAPublickeyData)rsa.getPublickey();

RSAPrivatekeyData rPrivateD = (RSAPrivatekeyData)rsa.getRrivatekey();

At this point we have initialized the RSAKey, acquired copies of the keys for later use, and for public key distribution.

 

 

Using Cipher Algorithms:

The cipher algorithms are an implementation of the Cipher class as found in the javacard.security API's. The use is straightforward as per the RC4 example we began above in the KeyBuilder section. After setting the secret key and initializing the ciper for encrpyting there are two method calls that one can use. The first is

/**

* Call update only if one has multiple buffers to encrypt/decrypt

* and all of the data cannot be processed in one buffer.

*/

public int update(byte[] inBuf, int inOff, int inLen, byte[] outBuf, int outOff)

throws CryptoException

and the second

/**

* Call here if you only have one buffer of data, or it's the last buffer.

*/

public int doFinal(byte[] inBuf, int inOff, int inLen, byte[] outBuf, int outOff)

throws CryptoException

Thus, if inBuf contains all "inLen" bytes of the required data to be encrypted:

// Set encryption mode, then encrypt.

// The size of outbuf must be at least as large as the total number of

// input bytes.

rc4.init(k1, Cipher.MODE_ENCRYPT);

int rvalue = rc4.doFinal(inBuf, 0, inLen, outBuf, 0);

OutBuf will contain rvalue encrypted bytes in the case of RC4. To decrypt OutBuf one does the following:

// Set decryption mode, then decrypt

// Here, inBuf must be at least as large as outBuf.

rc4.init(k1, Cipher.MODE_DECRYPT);

short rvalue = rc4.doFinal(outBuf, (short)0, (short)outBuf.length,

inBuf, (short)0);

 

 

 

The RSA Public Key Algorithm:

The RSA public key algorithm is a big integer implementation, and the private key calculation uses the Chinese Remainder Theorem for efficiency. To generate primes we have extended the Class Random with our own SHA-1 based, pseudo-random number generator (see the next section). For prime generation we use a certainty of 80%.

RSA is for encrypting and decrypting small amounts of data, and the encrypted data size is always the size of the modulus. This is guaranteed by PKCS#1 padding of data. The padding is 11 bytes and thus, for the 512-bit export key size, which is the default, only 53 bytes of data are permitted. This is sufficient for TLS, which requires 48 bytes of key material. And, it fulfills the export constraints for the underlying symmetric key, digests, and digital signature algorithms.

To continue with the above example in KeyBuilder, let rPublicD and rPrivateD be our public and private key data, respectively. Then

byte[] X = new byte[rsa.getMaxInputDataBlockLength()];

// Let's fill data with a pattern of "10101010 ... 1010"

for (int i = 0; i < X.length; i++) X[i] = (byte)0xCC;

// At this point we at can use either the public key or

// private key to encrypt, and then decrypt conversely.

rsa.setPrivateKey(rPrivateD);

byte[] Y = rsa.Algorithm(X, 0, X.length, KeyBuilder.TYPE_RSA_PRIVATE,

true); // true if encrypting

The recipient of the public key understands in the context of the algorithm that the received CODAT is to be decrypted with the public key of the sender, which is in the recipient’s possession.

// Now decrypt with the public key

rsa.setPublicKey(rPublicD);

byte[] Z = rsa.Algorithm(Y, 0, Y.length, KeyBuilder.TYPE_RSA_PUBLIC,

false); // false if decrypting

While we cannot publish official timing numbers here, our tests show that on multiple platforms the initial key calculations take a minimum of 350 milliseconds and at most 2.5 seconds, while the encrypt/decrypt calculations average few milliseconds.

 

Digital Signatures:

As mentioned in the section on the JxtaCryptoSuite, we support two signature profiles. They again are:

ALG_RSA_SHA_PKCS1

ALG_RSA_MD5_PKCS1

Using either of the two above hash algorithms along with RSA, the following code shows how to both sign a sequence of bytes with the RSA private key, and then verify the signature for the same sequence of bytes:

RSAKey rKey = (RSAKey)KeyBuilder.buildKey(KeyBuilder.TYPE_RSA,

KeyBuilder.LENGTH_RSA_512,

false);

JxtaCrypto suite = new JxtaCryptoSuite(JxtaCrypto.MEMBER_RSA + JxtaCrypto.MEMBER_SHA1,

rKey,

Signature.ALG_RC4_SHA1,

(byte) 0);

Signature sig = suite.getJxtaSignature(); // Digital signature

PublicKeyAlgorithm rsa = suite.getJxtaPublicKeyAlgorithm();

 

Assume we have already created the RSA public and private keys, and that the public key has been appropriately distributed. As the owner of the private key we do the following to sign a sequence of bytes:

rsa.setPrivateKey(rPrivateD); // set the private key

sig.init(Signature.MODE_SIGN); // initialize signing mode

Next we can either call update with successive buffers of data to be hashed noting that the final buffer of data must be reserved for the call to sig.sign(..) below:

sig.update(inbuf, offset, length);

Or having all of the data, or the final buffer of data in inbuf, one then signs the sequence of data bytes:

byte[] sigBuf = new byte[sig.getLength()]; // signature data buffer

sig.sign(inbuf, inOffset, inLength, sigBuf, 0); // sign the digest

InOffset is the offset of the data in inBuf, and inLength is the number of data bytes in the buffer. The signature is returned in sigBuf starting at byte 0.

Later, the data along with the signature returned from the call to sig.sign(...) can be verified by any one who possesses the public key.

Similarly, sig.update(..) may have been called. And either the final buffer or the entire sequence of bytes of length inLength is in the buffer inBuf beginning at offset inOffset. Also, sigBuf contains the signature to be verfied, sigOffset, is the offset of the initial byte of the signature in sigBuf, and sigLength its length. Then,

rsa.setPublicKey(rPublicD); // set the public key

sig.init(Signature.MODE_VERIFY); // set verify mode

verify(inbuf, inOffset, inLength, sigBuf, sigOffset, sigLength);

will verify the signature. If verify returns true, then the signature is valid.

 

 

 

 

The Hash or Digest Algorithms:

Our hash algorithms are implementations of the javacard.security MessageDigest interface. We use "Hash" rather than MessageDigest. Given,

JxtaCrypto suite = new JxtaCryptoSuite(JxtaCrypto.MEMBER_SHA1+

JxtaCrypto.MEMBER_MD5, null);

Hash sha1 = suite.getJxtaHash(Hash.ALG_SHA1);

Hash md5 = suite.getJxtaHash(Hash.ALG_MD5);

Then, as with the cipher algorithms, there are both update and doFinal methods with the same caveats. The following is an excerpt from TestDigest.java:

// build a 1000 byte buffer to hash 1,000,000 a's

byte[] digest = new byte[sha1.getDigestLength()];

byte[] data = new byte[1000];

int i;

for (i = 0; i < 1000; ++i) data[i] = (byte)'a';

try {

for (i = 0; i < 999; ++i) {

sha1.update(data, 0, data.length);

}

// last buffer full

sha1.doFinal(data, 0, data.length, digest, 0);

} catch (ArrayIndexOutOfBoundsException cex) {

System.exit(1);

}

An identical test is done with the md5 object.

 

 

Random Number Generation:

We have written a pseudo-random byte-stream generator, JRandom, that uses SHA-1 to generate random bits. As an initial seed we use the first 160 bits of PI [rfc2412], and to add unpredictability, a churn method is called that uses the current time in milliseconds along with a counter, which are XOR'd with eight bits of the currently available free memory of the JVM.

JRandom extends java.util.Random, and overrides the latter's public methods:

public synchronized void setSeed(long lseed);

protected synchronized int next(int bits);

public void nextBytes(byte buff[]);

public int nextInt();

public long nextLong();

We use JRandom to generate random, large BigInteger primes in our

RSA code.

Examples are as follows:

// Instantiate the class

JRandom prng = new JRandom();

// 64 random bytes

byte[] rbits = new byte[64];

prng.nextBytes(rbits);

// A long

long l = prng.nextLong();

// An int

int i = prng.nextInt();

// A BigInteger of 512 bits with a certainty of 80.

// (Probability of primality > 1 - 1/2**80)

BigInteger p = new BigInteger(512, 80, (Random)prng);