com.coyotesong.security.pbk.PbkTest.java Source code

Java tutorial

Introduction

Here is the source code for com.coyotesong.security.pbk.PbkTest.java

Source

/*
 * This code was written by Bear Giles <bgiles@coyotesong.com>and he
 * licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Any contributions made by others are licensed to this project under
 * one or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 * Copyright (c) 2013 Bear Giles <bgiles@coyotesong.com>
 */
package com.coyotesong.security.pbk;

import static org.junit.Assert.assertEquals;

import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.ResourceBundle;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.junit.Before;
import org.junit.Test;

/**
 * Class that demonstrates creation of Password-Based Encryption keys using the
 * PBKDF2WithHmacSHA1 algorithm. The class also demostrates the use of a Feistel
 * cipher to merge multiple salts and one possible way to create an IV from a
 * PBEKey and salt.
 * 
 * @author Bear Giles <bgiles@coyotesong.com>
 */
public class PbkTest {
    private static final Provider bc = new BouncyCastleProvider();
    private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(PbkTest.class.getName());
    private SecretKey cipherKey;
    private AlgorithmParameterSpec ivSpec;

    /**
     * Perform Feistel cipher using two 20-byte input buffers. The function F is
     * the SHA1 digest and there is no key. The cipher will not add entropy -
     * you still need to have good input! - but it will increase the costs to an
     * attacker.
     * 
     * A typical source for the inputs are 1) the contents of a file (or the
     * digest of the same) and 2) a hard-coded value.
     * 
     * See: http://en.wikipedia.org/wiki/Feistel_cipher
     * 
     * @param inputs
     * @param rounds
     * @return
     */
    public static byte[][] feistelSha1Hash(byte[][] inputs, int rounds) throws NoSuchAlgorithmException {
        final byte[] left = new byte[20];
        final byte[] right = new byte[20];

        System.arraycopy(inputs[0], 0, left, 0, left.length);
        System.arraycopy(inputs[1], 0, right, 0, right.length);

        final MessageDigest digest = MessageDigest.getInstance("SHA1");
        for (int round = 0; round < rounds; round += 2) {
            final byte[] round1 = digest.digest(right);
            for (int i = 0; i < left.length; i++) {
                left[i] ^= round1[i];
            }
            digest.reset();

            final byte[] round2 = digest.digest(left);
            for (int i = 0; i < right.length; i++) {
                right[i] ^= round2[i];
            }
            digest.reset();
        }

        return new byte[][] { left, right };
    }

    /**
     * Create salt. Two values are provided to support creation of both a cipher
     * key and IV from a single password.
     * 
     * The 'left' salt is pulled from a file outside of the app context. this
     * makes it much harder for a compromised app to obtain or modify this
     * value. You could read it as classloader resource but that's not really
     * different from the properties file used below. Another possibility is to
     * load it from a read-only value in a database, ideally one with a
     * different schema than the rest of the application. (It could even be an
     * in-memory database such as H2 that contains nothing but keying material,
     * again initialized from a file outside of the app context.)
     * 
     * The 'right' salt is pulled from a properties file. It is possible to use
     * a base64-encoded value but administration is a lot easier if we just take
     * an arbitrary string and hash it ourselves. At a minimum it should be a
     * random mix-cased string of at least (120/5 = 24) characters.
     * 
     * The generated salts are equally strong.
     * 
     * Implementation note: since this is for demonstration purposes a static
     * string in used in place of reading an external file.
     */
    public byte[][] createSalt() throws NoSuchAlgorithmException {
        final MessageDigest digest = MessageDigest.getInstance("SHA1");
        final byte[] left = new byte[20]; // fall back to all zeroes
        final byte[] right = new byte[20]; // fall back to all zeroes

        // load value from file or database.
        // note: we use fixed value for demonstration purposes.
        final String leftValue = "this string should be read from file or database";
        if (leftValue != null) {
            System.arraycopy(digest.digest(leftValue.getBytes()), 0, left, 0, left.length);
            digest.reset();
        }

        // load value from resource bundle.
        final String rightValue = BUNDLE.getString("salt");
        if (rightValue != null) {
            System.arraycopy(digest.digest(rightValue.getBytes()), 0, right, 0, right.length);
            digest.reset();
        }

        final byte[][] salt = feistelSha1Hash(new byte[][] { left, right }, 1000);

        return salt;
    }

    /**
     * Create secret key and IV from password.
     * 
     * Implementation note: I've believe I've seen other code that can extract
     * the random bits for the IV directly from the PBEKeySpec but I haven't
     * been able to duplicate it. It might have been a BouncyCastle extension.
     * 
     * @throws Exception
     */
    public void createKeyAndIv(char[] password)
            throws SecurityException, NoSuchAlgorithmException, InvalidKeySpecException {
        final String algorithm = "PBKDF2WithHmacSHA1";
        final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm);
        final int derivedKeyLength = 128;
        final int iterations = 10000;

        // create salt
        final byte[][] salt = feistelSha1Hash(createSalt(), 1000);

        // create cipher key
        final PBEKeySpec cipherSpec = new PBEKeySpec(password, salt[0], iterations, derivedKeyLength);
        cipherKey = factory.generateSecret(cipherSpec);
        cipherSpec.clearPassword();

        // create IV. This is just one of many approaches. You do
        // not want to use the same salt used in creating the PBEKey.
        try {
            final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding", bc);
            cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(salt[1], 0, 16));
            ivSpec = new IvParameterSpec(cipher.doFinal(salt[1], 4, 16));
        } catch (NoSuchPaddingException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (InvalidAlgorithmParameterException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (InvalidKeyException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (BadPaddingException e) {
            throw new SecurityException("unable to create IV", e);
        } catch (IllegalBlockSizeException e) {
            throw new SecurityException("unable to create IV", e);
        }
    }

    /**
     * Obtain password. Architectually we'll want good "separation of concerns"
     * and we should get the cipher key and IV from a separate place than where
     * we use it.
     * 
     * This is a unit test so the password is stored in a properties file. In
     * practice we'll want to get it from JNDI from an appserver, or at least a
     * file outside of the appserver's directory.
     * 
     * @throws Exception
     */
    @Before
    public void setup() throws Exception {
        createKeyAndIv(BUNDLE.getString("password").toCharArray());
    }

    /**
     * Test encryption.
     * 
     * @throws Exception
     */
    @Test
    public void testEncryption() throws Exception {
        String plaintext = BUNDLE.getString("plaintext");

        Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
        cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
        byte[] actual = cipher.doFinal(plaintext.getBytes());
        assertEquals(BUNDLE.getString("ciphertext"), new String(Base64.encode(actual), Charset.forName("UTF-8")));
    }

    /**
     * Test decryption.
     * 
     * @throws Exception
     */
    @Test
    public void testEncryptionAndDecryption() throws Exception {
        String ciphertext = BUNDLE.getString("ciphertext");

        Cipher cipher = Cipher.getInstance(BUNDLE.getString("algorithm"), bc);
        cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
        byte[] actual = cipher.doFinal(Base64.decode(ciphertext));

        assertEquals(BUNDLE.getString("plaintext"), new String(actual, Charset.forName("UTF-8")));
    }
}