/*
* LinShare is an open source filesharing software, part of the LinPKI software
* suite, developed by Linagora.
*
* Copyright (C) 2015 LINAGORA
*
* 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, provided you comply with the Additional Terms applicable for
* LinShare software by Linagora pursuant to Section 7 of the GNU Affero General
* Public License, subsections (b), (c), and (e), pursuant to which you must
* notably (i) retain the display of the “LinShare™” trademark/logo at the top
* of the interface window, the display of the “You are using the Open Source
* and free version of LinShare™, powered by Linagora © 2009–2015. Contribute to
* Linshare R&D by subscribing to an Enterprise offer!” infobox and in the
* e-mails sent with the Program, (ii) retain all hypertext links between
* LinShare and linshare.org, between linagora.com and Linagora, and (iii)
* refrain from infringing Linagora intellectual property rights over its
* trademarks and commercial brands. Other Additional Terms apply, see
* <http://www.linagora.com/licenses/> for more details.
*
* 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 and
* its applicable Additional Terms for LinShare along with this program. If not,
* see <http://www.gnu.org/licenses/> for the GNU Affero General Public License
* version 3 and <http://www.linagora.com/licenses/> for the Additional Terms
* applicable to LinShare software.
*/
package org.linagora.linshare.core.utils;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
*
* PBE with AES
* the encrypted file is salt[16 bytes] + iterations int [4 bytes] + CBC encrypted file
*
*/
public class SymmetricEnciphermentPBEwithAES {
// PBE, AES CONFIG
private final static int ITERATIONS = 20;
private final static int SALT_NUMBER_BITES = 16; //16*8=128 bit
/**
*ALGO_AES can be many conf like
*PBEWITHSHAAND192BITAES-CBC-BC
*PBEWITHSHA256AND128BITAES-CBC-BC
*PBEWITHSHA256AND192BITAES-CBC-BC
*PBEWITHSHAAND128BITAES-CBC-BC
*PBEWITHSHAAND256BITAES-CBC-BC
*PBEWITHSHA256AND256BITAES-CBC-BC etc ...
*/
private final static String SECRETKEYFACTORY_ALGO = "PBEWITHSHA256AND256BITAES-CBC-BC";
private final static String CIPHER_ALGO="AES/CBC/PKCS5Padding";
private byte[] salt;
private int iterations;
private Cipher cipher;
private DataInputStream in;
private OutputStream out;
/**
* can only be Cipher.DECRYPT_MODE or Cipher.ENCRYPT_MODE
* @see javax.crypto.Cipher
*/
private int cipherMode;
static {
Security.addProvider(new BouncyCastleProvider());
}
public SymmetricEnciphermentPBEwithAES(String password,byte[] dataToProcess,int cipherMode) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, IOException{
this(password,new ByteArrayInputStream(dataToProcess),new ByteArrayOutputStream(),cipherMode);
}
public SymmetricEnciphermentPBEwithAES(String password,InputStream is, OutputStream out,int cipherMode) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IOException {
this.in = new DataInputStream(is);
this.out= out;
this.cipherMode=cipherMode;
if(cipherMode==Cipher.DECRYPT_MODE){
//read the salt 16 bytes
salt = new byte[SALT_NUMBER_BITES];
in.read(salt,0,SALT_NUMBER_BITES);
SecretKey secret_key = getSecretKey(password);
//read the iterations
iterations = in.readInt(); //encoded in four bytes
AlgorithmParameterSpec param_spec = getPBEParameterSpec(salt,iterations);
cipher = Cipher.getInstance(CIPHER_ALGO);
cipher.init(Cipher.DECRYPT_MODE, secret_key, param_spec);
}
else if(cipherMode==Cipher.ENCRYPT_MODE){
salt = generateSalt(); // create new salt (IV)
this.iterations=ITERATIONS;
SecretKey secret_key = getSecretKey(password);
AlgorithmParameterSpec param_spec = getPBEParameterSpec(salt,iterations);
cipher = Cipher.getInstance(CIPHER_ALGO);
cipher.init(Cipher.ENCRYPT_MODE, secret_key, param_spec);
}
}
/**
* To create the bytes for the initialization vector IV, we should use java.security.SecureRandom to generate
* a byte array equivalent to the block size for the cipher we are using. Most block ciphers have a block size of 64 bits
*(8 bytes). AES has a variable block size, either 128, 192, or 256 bits, but is typically set to 128-bit (16 bytes).
*/
private static byte[] generateSalt() {
byte[] salt = new byte[SALT_NUMBER_BITES];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
return salt;
}
public static void main(String[] args) throws Exception {
/* File encrypt / decrypt test */
SymmetricEnciphermentPBEwithAES aes = new SymmetricEnciphermentPBEwithAES("12345678",new FileInputStream("/test/test1.pdf"),new FileOutputStream("/test/xxxxx.pdf"),Cipher.ENCRYPT_MODE);
aes.encryptStream();
aes = new SymmetricEnciphermentPBEwithAES("12345678",new FileInputStream("/test/xxxxx.pdf"),new FileOutputStream("/test/decryption.pdf"),Cipher.DECRYPT_MODE);
aes.decryptStream();
/* String in memory encrypt / decrypt test */
SymmetricEnciphermentPBEwithAES aes2 = new SymmetricEnciphermentPBEwithAES("12345678","only a test with cipher when you use a string".getBytes(),Cipher.ENCRYPT_MODE);
byte[] encStr = aes2.encryptString();
System.out.println(new String(encStr));
aes2 = new SymmetricEnciphermentPBEwithAES("12345678",encStr,Cipher.DECRYPT_MODE);
System.out.println(new String(aes2.decryptString()));
}
/**
* PBE specification (pkcs5 standard)
* @param salt
* @return
*/
private static AlgorithmParameterSpec getPBEParameterSpec(byte[] salt, int iterations){
PBEParameterSpec param_spec = new PBEParameterSpec(salt, iterations);
return param_spec;
}
private static SecretKey getSecretKey(String pw) throws NoSuchAlgorithmException, InvalidKeySpecException{
PBEKeySpec key_spec = new PBEKeySpec(pw.toCharArray());
SecretKeyFactory key_factory = SecretKeyFactory.getInstance(SECRETKEYFACTORY_ALGO);
SecretKey secret_key = key_factory.generateSecret(key_spec);
return secret_key;
}
public void encryptStream() throws IOException {
if(cipherMode!=Cipher.ENCRYPT_MODE) throw new IllegalStateException("can not call encrypt, check cipher mode");
out.write(salt,0,SALT_NUMBER_BITES);
byte v[] = new byte[4];
//*** writeInt
v[0] = (byte)(0xff & (iterations >> 24));
v[1] = (byte)(0xff & (iterations >> 16));
v[2] = (byte)(0xff & (iterations >> 8));
v[3] = (byte)(0xff & iterations);
out.write(v);
out.flush();
CipherOutputStream cos = new CipherOutputStream(out, cipher);
// Read from the input and write to the encrypting output stream
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
cos.write(buffer, 0, bytesRead);
}
cos.flush();
cos.close();
out.close();
in.close();
}
public void decryptStream() throws IOException {
if(cipherMode!=Cipher.DECRYPT_MODE) throw new IllegalStateException("can not call decrypt, check cipher mode");
CipherInputStream cis = new CipherInputStream(in, cipher);
// Read from encrypted input and write to output stream
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = cis.read(buffer)) !=-1) {
out.write(buffer, 0, bytesRead);
//System.out.println(bytesRead);
}
out.flush();
out.close();
cis.close();
in.close();
}
public byte[] encryptString() throws IOException {
encryptStream();
return ((ByteArrayOutputStream) out).toByteArray();
}
public byte[] decryptString() throws IOException {
decryptStream();
return ((ByteArrayOutputStream) out).toByteArray();
}
/**
* give a CipherInputStream to decrypt data, if you want to decrypt data yourself
* in must be given in class constructor
* @return
*/
public CipherInputStream getCipherInputStream(){
if(in == null) throw new IllegalStateException("can not give intialised CipherInputStream, check inputstream");
return new CipherInputStream(in, cipher);
}
/**
* give a CipherOutputStream to encrypt data, if you want to encrypt data yourself
* out must be given in class constructor
* @return
*/
public CipherOutputStream getCipherOutputStream(){
if(out == null) throw new IllegalStateException("can not give intialised CipherOutputStream, check outputstream");
return new CipherOutputStream(out, cipher);
}
}