package com.seafile.seadroid2.crypto;
import android.support.annotation.NonNull;
import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import com.seafile.seadroid2.SeafException;
import org.spongycastle.crypto.PBEParametersGenerator;
import org.spongycastle.crypto.digests.SHA256Digest;
import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* There are a few ways to derive keys, but most of them are not particularly secure.
* To ensure encryption keys are both sufficiently random and hard to brute force, we should use standard PBE key derivation methods.
* Other Seafile platforms, e.g, server side, using PBKDF2WithHmacSHA256 to derive a key/iv pair from the password,
* using AES 256/CBC to encrypt the data.
* <p/>
* Unfortunately, Android SDK doesn`t support PBKDF2WithHmacSHA256, so we use Spongy Castle, which is the stock Bouncy Castle libraries with a couple of small changes to make it work on Android.
* For version 1.47 or higher of SpongyCastle, we can invoke PBKDF2WithHmacSHA256 directly,
* but for versions below 1.47, we could not specify SHA256 digest and it defaulted to SHA1.
* see
* 1. https://rtyley.github.io/spongycastle/
* 2. http://stackoverflow.com/a/15303291/3962551
* 3. https://en.wikipedia.org/wiki/Bouncy_Castle_(cryptography)
*/
public class Crypto {
private static final String TAG = Crypto.class.getSimpleName();
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
private static final String CHAR_SET = "UTF-8";
private static int KEY_LENGTH = 32;
private static int KEY_LENGTH_SHORT = 16;
private static int ITERATION_COUNT = 1000;
// Should generate random salt for each repo
private static byte[] salt = {(byte) 0xda, (byte) 0x90, (byte) 0x45, (byte) 0xc3, (byte) 0x06, (byte) 0xc7, (byte) 0xcc, (byte) 0x26};
static {
// http://stackoverflow.com/questions/6898801/how-to-include-the-spongy-castle-jar-in-android
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
private Crypto() {
}
/**
* When you view an encrypted library, the client needs to verify your password.
* When you create the library, a "magic token" is derived from the library id and password.
* This token is stored with the library on the server side.
* <p/>
* The client use this token to check whether your password is correct before you view the library.
* The magic token is generated by PBKDF2 algorithm with 1000 iterations of SHA256 hash.
*
* @param repoID
* @param password
* @param version
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
private static byte[] generateMagic(String repoID, String password, int version) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException, SeafException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
if (version != 1 && version != 2) {
throw SeafException.unsupportedEncVersion;
}
return deriveKey(repoID + password, version);
}
/**
* Recompute the magic and compare it with the one comes with the repo.
*
* @param repoId
* @param password
* @param version
* @param magic
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws UnsupportedEncodingException
* @throws SeafException
*/
public static void verifyRepoPassword(String repoId, String password, int version, String magic) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException, SeafException, IllegalBlockSizeException, InvalidKeyException, BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException {
final byte[] generateMagic = generateMagic(repoId, password, version);
final byte[] genMagic = toHex(generateMagic).getBytes(CHAR_SET);
final byte[] repoMagic = magic.getBytes(CHAR_SET);
int diff = genMagic.length ^ repoMagic.length;
for (int i = 0; i < genMagic.length && i < repoMagic.length; i++) {
diff |= genMagic[i] ^ repoMagic[i];
}
if (diff != 0) throw SeafException.invalidPassword;
}
/**
* First use PBKDF2 algorithm (1000 iteratioins of SHA256) to derive a key/iv pair from the password,
* then use AES 256/CBC to decrypt the "file key" from randomKey (the "encrypted file key").
* The client only saves the key/iv pair derived from the "file key", which is used to decrypt the data.
*
* @param password
* @param randomKey encrypted file key
* @param version
* @return
* @throws UnsupportedEncodingException
* @throws NoSuchAlgorithmException
*/
public static Pair<String, String> generateKey(@NonNull String password, @NonNull String randomKey, int version) throws UnsupportedEncodingException, NoSuchAlgorithmException {
// derive a key/iv pair from the password
final byte[] key = deriveKey(password, version);
SecretKey derivedKey = new SecretKeySpec(key, "AES");
final byte[] iv = deriveIv(key);
// decrypt the file key from the encrypted file key
final byte[] fileKey = seafileDecrypt(fromHex(randomKey), derivedKey, iv);
// The client only saves the key/iv pair derived from the "file key", which is used to decrypt the data
final String encKey = deriveKey(fileKey, version);
return new Pair<>(encKey, toHex(deriveIv(fromHex(encKey))));
}
/**
* Derive secret key by PBKDF2 algorithm (1000 iterations of SHA256)
*
* @param password
* @param version
* @return
* @throws UnsupportedEncodingException
* @throws NoSuchAlgorithmException
*/
private static byte[] deriveKey(@NonNull String password, int version) throws UnsupportedEncodingException, NoSuchAlgorithmException {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
gen.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password.toCharArray()), salt, ITERATION_COUNT);
return ((KeyParameter) gen.generateDerivedMacParameters(version == 2 ? KEY_LENGTH * 8 : KEY_LENGTH_SHORT * 8)).getKey();
}
/**
* Derive secret key by PBKDF2 algorithm (1000 iterations of SHA256)
*
* @param fileKey
* @param version
* @return
* @throws UnsupportedEncodingException
* @throws NoSuchAlgorithmException
*/
private static String deriveKey(@NonNull byte[] fileKey, int version) throws UnsupportedEncodingException, NoSuchAlgorithmException {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
gen.init(fileKey, salt, ITERATION_COUNT);
byte[] keyBytes = ((KeyParameter) gen.generateDerivedMacParameters(version == 2 ? KEY_LENGTH * 8 : KEY_LENGTH_SHORT * 8)).getKey();
return toHex(keyBytes);
}
/**
* Derive initial vector by PBKDF2 algorithm (10 iterations of SHA256)
*
* @param key
* @return
* @throws UnsupportedEncodingException
*/
private static byte[] deriveIv(@NonNull byte[] key) throws UnsupportedEncodingException {
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
gen.init(key, salt, 10);
return ((KeyParameter) gen.generateDerivedMacParameters(KEY_LENGTH_SHORT * 8)).getKey();
}
/**
* Do the decryption
*
* @param bytes
* @param key
* @param iv
* @return
*/
private static byte[] seafileDecrypt(@NonNull byte[] bytes, @NonNull SecretKey key, @NonNull byte[] iv) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
return cipher.doFinal(bytes);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
Log.e(TAG, "NoSuchAlgorithmException " + e.getMessage());
return null;
} catch (InvalidKeyException e) {
e.printStackTrace();
Log.e(TAG, "InvalidKeyException " + e.getMessage());
return null;
} catch (NoSuchPaddingException e) {
e.printStackTrace();
Log.e(TAG, "NoSuchPaddingException " + e.getMessage());
return null;
} catch (BadPaddingException e) {
e.printStackTrace();
Log.e(TAG, "seafileDecrypt BadPaddingException " + e.getMessage());
return null;
} catch (IllegalBlockSizeException e) {
Log.e(TAG, "IllegalBlockSizeException " + e.getMessage());
e.printStackTrace();
return null;
} catch (InvalidAlgorithmParameterException e) {
Log.e(TAG, "InvalidAlgorithmParameterException " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* Do the encryption
*
* @param plaintext
* @param inputLen
* @param key
* @param iv
* @return
*/
private static byte[] seafileEncrypt(@NonNull byte[] plaintext, int inputLen, @NonNull SecretKey key, @NonNull byte[] iv) {
try {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
IvParameterSpec ivParams = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivParams);
return cipher.doFinal(plaintext, 0, inputLen);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
Log.e(TAG, "NoSuchAlgorithmException " + e.getMessage());
return null;
} catch (InvalidKeyException e) {
e.printStackTrace();
Log.e(TAG, "InvalidKeyException " + e.getMessage());
return null;
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
Log.e(TAG, "InvalidAlgorithmParameterException " + e.getMessage());
return null;
} catch (NoSuchPaddingException e) {
e.printStackTrace();
Log.e(TAG, "NoSuchPaddingException " + e.getMessage());
return null;
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
Log.e(TAG, "IllegalBlockSizeException " + e.getMessage());
return null;
} catch (BadPaddingException e) {
e.printStackTrace();
Log.e(TAG, "seafileEncrypt BadPaddingException " + e.getMessage());
return null;
}
}
/**
* All file data is encrypted by the encKey/encIv with AES 256/CBC.
*
* @param plaintext
* @param encKey
* @param iv
* @return
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static byte[] encrypt(@NonNull byte[] plaintext, @NonNull String encKey, @NonNull String iv) throws NoSuchAlgorithmException, UnsupportedEncodingException {
return encrypt(plaintext, plaintext.length, encKey, iv);
}
/**
* All file data is encrypted by the encKey/encIv with AES 256/CBC.
*
* @param plaintext
* @param inputLen
* @param encKey
* @param iv
* @return
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static byte[] encrypt(@NonNull byte[] plaintext, int inputLen, @NonNull String encKey, @NonNull String iv) throws NoSuchAlgorithmException, UnsupportedEncodingException {
SecretKey secretKey = new SecretKeySpec(fromHex(encKey), "AES");
return seafileEncrypt(plaintext, inputLen, secretKey, fromHex(iv));
}
/**
* All file data is decrypted by the encKey/encIv with AES 256/CBC.
*
* @param plaintext
* @param encKey
* @param iv
* @return
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static byte[] decrypt(@NonNull byte[] plaintext, @NonNull String encKey, @NonNull String iv) throws NoSuchAlgorithmException, UnsupportedEncodingException {
SecretKey realKey = new SecretKeySpec(fromHex(encKey), "AES");
return seafileDecrypt(plaintext, realKey, fromHex(iv));
}
/**
* Convert byte to Hexadecimal
*
* @param buf
* @return
*/
private static String toHex(@NonNull byte[] buf) {
if (buf == null) return "";
String hex = "0123456789abcdef";
StringBuilder result = new StringBuilder(2 * buf.length);
for (int i = 0; i < buf.length; i++) {
result.append(hex.charAt((buf[i] >> 4) & 0x0f)).append(hex.charAt(buf[i] & 0x0f));
}
return result.toString();
}
/**
* Convert Hexadecimal to byte
*
* @param hex
* @return
* @throws NoSuchAlgorithmException
*/
private static byte[] fromHex(@NonNull String hex) throws NoSuchAlgorithmException {
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return bytes;
}
public static String toBase64(byte[] bytes) {
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
public static byte[] fromBase64(String base64) {
return Base64.decode(base64, Base64.NO_WRAP);
}
public static String sha1(@NonNull byte[] cipher) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(cipher, 0, cipher.length);
return toHex(md.digest());
}
}