ch.bfh.evoting.alljoyn.MessageEncrypter.java Source code

Java tutorial

Introduction

Here is the source code for ch.bfh.evoting.alljoyn.MessageEncrypter.java

Source

/* 
 * MobiVote
 * 
 *  MobiVote: Mobile application for boardroom voting
 *  Copyright (C) 2014 Bern
 *  University of Applied Sciences (BFH), Research Institute for Security
 *  in the Information Society (RISIS), E-Voting Group (EVG) Quellgasse 21,
 *  CH-2501 Biel, Switzerland
 * 
 *  Licensed under Dual License consisting of:
 *  1. GNU Affero General Public License (AGPL) v3
 *  and
 *  2. Commercial license
 * 
 *
 *  1. This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 *
 *  2. Licensees holding valid commercial licenses for MobiVote may use this file in
 *   accordance with the commercial license agreement provided with the
 *   Software or, alternatively, in accordance with the terms contained in
 *   a written agreement between you and Bern University of Applied Sciences (BFH), 
 *   Research Institute for Security in the Information Society (RISIS), E-Voting Group (EVG)
 *   Quellgasse 21, CH-2501 Biel, Switzerland.
 * 
 *
 *   For further information contact us: http://e-voting.bfh.ch/
 * 
 *
 * Redistributions of files must retain the above copyright notice.
 */
package ch.bfh.evoting.alljoyn;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;

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 javax.crypto.spec.SecretKeySpec;

import android.content.Context;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Base64;
import android.util.Log;

/**
 * Class used to encrypt and decrypt messages
 * @author Philemon von Bergen
 *
 */
public class MessageEncrypter {

    private static final String TAG = MessageEncrypter.class.getSimpleName();

    private SecretKey secretKey;
    private byte[] salt;
    private String password;
    private SecureRandom random;

    private boolean isReady = false;

    private Object saltShortDigest;

    private boolean countDecryptionFailed = true;

    private int numberOfDecryptionFailed = 0;

    private Context context;

    public MessageEncrypter(Context ctx) {
        this.context = ctx;
    }

    /**
     * Method that encrypts data
     * @param data The data which should be encrypted
     * @return The encrypted bytes, null if encryption failed
     * 
     */
    public byte[] encrypt(byte[] data) {
        //Inspired from http://stackoverflow.com/questions/992019/java-256-bit-aes-password-based-encryption

        Cipher cipher;
        try {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            // cipher.getParameters() seems to return null on Android 4.3 (Bug?)
            // Solution implemented from here: 
            // https://code.google.com/p/android/issues/detail?id=58191
            byte[] iv;
            if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
                iv = generateIv();
                cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, secretKey);
                AlgorithmParameters params = cipher.getParameters();
                iv = params.getParameterSpec(IvParameterSpec.class).getIV();
            }

            byte[] cipherText = cipher.doFinal(data);

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            outputStream.write(iv);
            outputStream.write(cipherText);

            return outputStream.toByteArray();
        } catch (NoSuchAlgorithmException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (NoSuchPaddingException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (InvalidKeyException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (InvalidParameterSpecException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (IllegalBlockSizeException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (BadPaddingException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (InvalidAlgorithmParameterException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 
     * Method that decrypts data
     * @param ciphertext byte array to decrypt
     * @return the decrypted string if decryption was successful,
     *         null otherwise
     *         
     */
    public String decrypt(byte[] ciphertext) {
        //Inspired from http://stackoverflow.com/questions/992019/java-256-bit-aes-password-based-encryption

        //iv is same as block size: for AES => 128 bits = 16 bytes
        byte[] iv = Arrays.copyOfRange(ciphertext, 0, 16);
        byte[] cipherText = Arrays.copyOfRange(ciphertext, 16, ciphertext.length);

        Cipher cipher;
        try {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));

            String s = new String(cipher.doFinal(cipherText));

            //Since this decryption was successful, it means we have the correct key, 
            //so we can disable the count of failed decryptions
            countDecryptionFailed = false;

            return s;

        } catch (NoSuchAlgorithmException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (NoSuchPaddingException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (InvalidKeyException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (InvalidAlgorithmParameterException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
            return null;
        } catch (IllegalBlockSizeException e) {
            Log.d(TAG, e.getMessage() + " ");
            countFailedDecryption();
            e.printStackTrace();
            return null;
        } catch (BadPaddingException e) {
            Log.d(TAG, e.getMessage() + " ");
            countFailedDecryption();
            e.printStackTrace();
            return null;
        }
    }

    private void countFailedDecryption() {
        if (countDecryptionFailed) {
            //Rememeber the number of decryption failed
            numberOfDecryptionFailed++;
            //if this number reaches 10 indicate it to the user
            if (numberOfDecryptionFailed == 5) {
                Intent intent = new Intent("probablyWrongDecryptionKeyUsed");
                intent.putExtra("type", "password");
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
            }
        }
    }

    /**
     * Key derivation method from the given password
     * @param password password to derive
     */
    private void derivateKey(char[] password) {
        //Inspired from http://stackoverflow.com/questions/992019/java-256-bit-aes-password-based-encryption
        SecretKeyFactory factory;
        try {
            factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

            //1000 iteration should be enough since the attack has to be done online and
            //salt changes for each group
            KeySpec spec = new PBEKeySpec(password, this.salt, 1000, 256);
            SecretKey tmp = factory.generateSecret(spec);
            secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
            this.isReady = true;
        } catch (NoSuchAlgorithmException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            Log.d(TAG, e.getMessage() + " ");
            e.printStackTrace();
        }

    }

    /**
     * Generate a salt that will be used in the symmetric key derivation
     */
    public void generateSalt() {

        this.salt = SecureRandom.getSeed(8);
        this.derivateKey(password.toCharArray());

        Log.d(TAG, "Salt is " + salt);
    }

    /**
     * Set the salt
     * @param salt the Base64 encoded salt
     */
    public void setSalt(String salt) {

        byte[] tempSalt = Base64.decode(salt, Base64.DEFAULT);

        if (this.saltShortDigest.equals(getSaltShortDigest(tempSalt))) {
            Log.d(TAG, "received salt digest is " + saltShortDigest + " and computed digest from received salt is "
                    + getSaltShortDigest(tempSalt));
            this.salt = tempSalt;
            Log.d(TAG, "Saving salt " + salt);
            this.derivateKey(password.toCharArray());
        } else {
            Intent intent = new Intent("probablyWrongDecryptionKeyUsed");
            intent.putExtra("type", "salt");
            LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
            Log.e(TAG, "Salt is false!");
        }

    }

    /**
     * Get the earlier generated or set salt
     * @return the earlier generated or set salt
     */
    public byte[] getSalt() {
        return salt;
    }

    /**
     * Compute a truncated digest on the salt
     * We only take the three first letters (not chars!) of the Base64 encoded digest, because
     * this truncated digest must be transmitted with the group password (only letters)
     * @param salt the salt from on we want to compute the digest 
     * @return the three first letters of the Base64 encoded digest
     */
    public String getSaltShortDigest(byte[] salt) {
        if (salt == null)
            return "";

        //Compute the digest
        MessageDigest md;
        String saltHash;
        Log.d(TAG, "Computing salt digest");
        try {
            md = MessageDigest.getInstance("SHA-1");
            md.update(salt, 0, salt.length);
            saltHash = Base64.encodeToString(md.digest(), Base64.DEFAULT);
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "Digest of salt could not be computed");
            e.printStackTrace();
            return null;
        }

        //Truncate the digest
        String shortDigest = "";
        int i = 0;
        while (shortDigest.length() < 3) {
            char c = saltHash.charAt(i);
            if (Character.isLetter(c)) {
                shortDigest = shortDigest.concat(String.valueOf(Character.toLowerCase(c)));
            }
            i++;
            if (i >= saltHash.length()) {
                break;
            }
        }
        Log.d(TAG, "Short digest is " + shortDigest);

        return shortDigest;
    }

    /**
     * Set the password used to derivate the symmetric key
     * @param password the password used to derivate the symmetric key
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * Set the truncated digest of the salt that will be received later
     * @param saltShortDigest the truncated digest of the salt that will be received later
     */
    public void setSaltShortDigest(String saltShortDigest) {
        this.saltShortDigest = saltShortDigest;
    }

    /**
     * Reset the state of the message encrypter
     */
    public void reset() {
        this.isReady = false;
        this.salt = null;
        this.secretKey = null;
        this.password = null;
        this.saltShortDigest = null;
        this.countDecryptionFailed = true;
        this.numberOfDecryptionFailed = 0;
    }

    /**
     * Indicate if the symmetric key has been derivated and message encrypter is ready to encrypt/decrypt
     * @return true is message encrypter is ready to encrypt/decrypt, false otherwise
     */
    public boolean isReady() {
        return isReady;
    }

    private byte[] generateIv() {

        random = new SecureRandom();
        byte[] iv = new byte[16];
        random.nextBytes(iv);
        return iv;
    }

}