package com.yakivmospan.scytale;
import android.security.KeyPairGeneratorSpec;
import android.support.annotation.NonNull;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
/**
* API to encrypt/decrypt data
*/
public class Crypto extends ErrorHandler {
private static final String UTF_8 = "UTF-8";
private static final String IV_SEPARATOR = "]";
private String mTransformation;
private int mEncryptionBlockSize;
private int mDecryptionBlockSize;
/**
* Initializes Crypto to encrypt/decrypt data with given transformation.
*
* @param transformation is used to encrypt/decrypt data. See {@link Cipher} for more info.
*/
public Crypto(@NonNull String transformation) {
mTransformation = transformation;
}
/**
* Initializes Crypto to encrypt/decrypt data using buffer with provided lengths. This might be useful if you
* want to encrypt/decrypt big amount of data using Block Based Algorithms (such as RSA). By default they can
* proceed only one block of data, not bigger then a size of a key that was used for encryption/decryption.
*
* @param transformation is used to encrypt/decrypt data. See {@link Cipher} for more info.<p>
* @param encryptionBlockSize block size for keys used with this Crypto for encryption. Depends on API level.
* For example: 1024 size RSA/ECB/PKCS1Padding key will equal to (keySize / 8) - 11 == (1024 / 8) - 11 == 117
* but for API 18 it is equal to 245 as there is no possibility to specify key size in {@link
* KeyPairGeneratorSpec} and 2048 key size is always used there. Use {@link Options#ENCRYPTION_BLOCK_SIZE} in
* pair with key created by {@link Store#generateSymmetricKey(String, char[])}<p>
* @param decryptionBlockSize block size for keys used with this Crypto for decryption. Depend on API level. For
* example: 1024 size RSA/ECB/PKCS1Padding key will equal to (keySize / 8) == (1024 / 8) == 128 but on API 18 it
* is equal to 256 as there is no possibility to specify key size in {@link KeyPairGeneratorSpec} and 2048 key
* size is always used there. Use {@link Options#DECRYPTION_BLOCK_SIZE} in pair with key created by {@link
* Store#generateSymmetricKey(String, char[])}
*/
public Crypto(@NonNull String transformation, int encryptionBlockSize, int decryptionBlockSize) {
mTransformation = transformation;
mEncryptionBlockSize = encryptionBlockSize;
mDecryptionBlockSize = decryptionBlockSize;
}
/**
* The same as encrypt(data, key.getPublic(), false);
*
* @return encrypted data in Base64 String or null if any error occur. Doesn't use Initialisation Vectors
*/
public String encrypt(@NonNull String data, @NonNull KeyPair key) {
return encrypt(data, key.getPublic(), false);
}
/**
* The same as encrypt(data, key, true)
*
* @return encrypted data in Base64 String or null if any error occur. Does use Initialisation Vectors
*/
public String encrypt(@NonNull String data, @NonNull SecretKey key) {
return encrypt(data, key, true);
}
/**
* @param useInitialisationVectors specifies when ever IvParameterSpec should be used in encryption
*
* @return encrypted data in Base64 String or null if any error occur. if useInitialisationVectors is true, data
* also contains iv key inside. In this case data will be returned in this format <iv key>]<encrypted data>
*/
public String encrypt(@NonNull String data, @NonNull Key key, boolean useInitialisationVectors) {
String result = "";
try {
Cipher cipher = Cipher.getInstance(mTransformation == null ? key.getAlgorithm() : mTransformation);
cipher.init(Cipher.ENCRYPT_MODE, key);
if (useInitialisationVectors) {
byte[] iv = cipher.getIV();
String ivString = Base64.encodeToString(iv, Base64.DEFAULT);
result = ivString + IV_SEPARATOR;
}
byte[] plainData = data.getBytes(UTF_8);
byte[] decodedData;
if (mEncryptionBlockSize == 0 && mDecryptionBlockSize == 0) {
decodedData = decode(cipher, plainData);
} else {
decodedData = decodeWithBuffer(cipher, plainData, mEncryptionBlockSize);
}
String encodedString = Base64.encodeToString(decodedData, Base64.DEFAULT);
result += encodedString;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException |
IllegalBlockSizeException | IOException e) {
onException(e);
}
return result;
}
/**
* The same as decrypt(data, key.getPrivate(), false)
*
* @param data Base64 encrypted data. Doesn't use Initialisation Vectors
*
* @return decrypted data or null if any error occur
*/
public String decrypt(@NonNull String data, @NonNull KeyPair key) {
return decrypt(data, key.getPrivate(), false);
}
/**
* The same as decrypt(data, key, true)
*
* @param data Base64 encrypted data with iv key. Does use Initialisation Vectors
*
* @return decrypted data or null if any error occur
*/
public String decrypt(@NonNull String data, @NonNull SecretKey key) {
return decrypt(data, key, true);
}
/**
* @param data Base64 encrypted data. If useInitialisationVectors is enabled, data should contain iv key inside.
* In this case data should be in this format <iv key>]<encrypted data>
* @param useInitialisationVectors specifies when ever IvParameterSpec should be used in encryption
*
* @return decrypted data or null if any error occur
*/
public String decrypt(@NonNull String data, @NonNull Key key, boolean useInitialisationVectors) {
String result = null;
try {
String transformation = mTransformation == null ? key.getAlgorithm() : mTransformation;
Cipher cipher = Cipher.getInstance(transformation);
String encodedString;
if (useInitialisationVectors) {
String[] split = data.split(IV_SEPARATOR);
String ivString = split[0];
encodedString = split[1];
IvParameterSpec ivSpec = new IvParameterSpec(Base64.decode(ivString, Base64.DEFAULT));
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
} else {
encodedString = data;
cipher.init(Cipher.DECRYPT_MODE, key);
}
byte[] decodedData;
byte[] encryptedData = Base64.decode(encodedString, Base64.DEFAULT);
if (mEncryptionBlockSize == 0 && mDecryptionBlockSize == 0) {
decodedData = decode(cipher, encryptedData);
} else {
decodedData = decodeWithBuffer(cipher, encryptedData, mDecryptionBlockSize);
}
result = new String(decodedData, UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException | IOException | InvalidAlgorithmParameterException e) {
onException(e);
}
return result;
}
private byte[] decode(@NonNull Cipher cipher, @NonNull byte[] plainData)
throws IOException, IllegalBlockSizeException, BadPaddingException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(baos, cipher);
cipherOutputStream.write(plainData);
cipherOutputStream.close();
return baos.toByteArray();
}
private byte[] decodeWithBuffer(@NonNull Cipher cipher, @NonNull byte[] plainData, int bufferLength)
throws IllegalBlockSizeException, BadPaddingException {
// string initialize 2 buffers.
// scrambled will hold intermediate results
byte[] scrambled;
// toReturn will hold the total result
byte[] toReturn = new byte[0];
// holds the bytes that have to be modified in one step
byte[] buffer = new byte[(plainData.length > bufferLength ? bufferLength : plainData.length)];
for (int i = 0; i < plainData.length; i++) {
if ((i > 0) && (i % bufferLength == 0)) {
//execute the operation
scrambled = cipher.doFinal(buffer);
// add the result to our total result.
toReturn = append(toReturn, scrambled);
// here we calculate the bufferLength of the next buffer required
int newLength = bufferLength;
// if newLength would be longer than remaining bytes in the bytes array we shorten it.
if (i + bufferLength > plainData.length) {
newLength = plainData.length - i;
}
// clean the buffer array
buffer = new byte[newLength];
}
// copy byte into our buffer.
buffer[i % bufferLength] = plainData[i];
}
// this step is needed if we had a trailing buffer. should only happen when encrypting.
// example: we encrypt 110 bytes. 100 bytes per run means we "forgot" the last 10 bytes. they are in the buffer array
scrambled = cipher.doFinal(buffer);
// final step before we can return the modified data.
toReturn = append(toReturn, scrambled);
return toReturn;
}
private byte[] append(byte[] prefix, byte[] suffix) {
byte[] toReturn = new byte[prefix.length + suffix.length];
for (int i = 0; i < prefix.length; i++) {
toReturn[i] = prefix[i];
}
for (int i = 0; i < suffix.length; i++) {
toReturn[i + prefix.length] = suffix[i];
}
return toReturn;
}
}