nextcloud-android/src/main/java/com/owncloud/android/utils/EncryptionUtils.java

622 lines
25 KiB
Java

/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2017 Tobias Kaminsky
* Copyright (C) 2017 Nextcloud GmbH.
*
* 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/>.
*/
package com.owncloud.android.utils;
import android.accounts.Account;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.Base64;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.files.GetMetadataOperation;
import org.apache.commons.codec.binary.Hex;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Utils for encryption
*/
public class EncryptionUtils {
private static String TAG = EncryptionUtils.class.getSimpleName();
private static byte[] salt = "$4$YmBjm3hk$Qb74D5IUYwghUmzsMqeNFx5z0/8$".getBytes();
private static String ivDelimiter = "fA=="; // "|" base64 encoded
private static int iterationCount = 1024;
private static int keyStrength = 256;
public static String PUBLIC_KEY = "PUBLIC_KEY";
public static String PRIVATE_KEY = "PRIVATE_KEY";
/*
JSON
*/
public static <T> T deserializeJSON(String json, TypeToken<T> type) {
return new Gson().fromJson(json, type.getType());
}
public static String serializeJSON(Object data) {
return new Gson().toJson(data);
}
/*
METADATA
*/
/**
* Encrypt folder metaData
*
* @param decryptedFolderMetadata folder metaData to encrypt
* @return EncryptedFolderMetadata encrypted folder metadata
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static EncryptedFolderMetadata encryptFolderMetadata(DecryptedFolderMetadata decryptedFolderMetadata,
String privateKey)
throws IOException, NoSuchAlgorithmException, ShortBufferException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
NoSuchProviderException, IllegalBlockSizeException, InvalidKeySpecException, CertificateException {
HashMap<String, EncryptedFolderMetadata.EncryptedFile> files = new HashMap<>();
EncryptedFolderMetadata encryptedFolderMetadata = new EncryptedFolderMetadata(decryptedFolderMetadata.metadata,
files);
// Encrypt each file in "files"
for (Map.Entry<String, DecryptedFolderMetadata.DecryptedFile> entry : decryptedFolderMetadata.files.entrySet()) {
String key = entry.getKey();
DecryptedFolderMetadata.DecryptedFile decryptedFile = entry.getValue();
EncryptedFolderMetadata.EncryptedFile encryptedFile = new EncryptedFolderMetadata.EncryptedFile();
encryptedFile.initializationVector = decryptedFile.initializationVector;
encryptedFile.metadataKey = decryptedFile.metadataKey;
encryptedFile.authenticationTag = decryptedFile.authenticationTag;
byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(EncryptionUtils.decryptStringAsymmetric(
decryptedFolderMetadata.metadata.metadataKeys.get(encryptedFile.metadataKey),
privateKey));
// encrypt
String dataJson = EncryptionUtils.serializeJSON(decryptedFile.encrypted);
encryptedFile.encrypted = EncryptionUtils.encryptStringSymmetric(dataJson, decryptedMetadataKey);
files.put(key, encryptedFile);
}
return encryptedFolderMetadata;
}
/*
* decrypt folder metaData with private key
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetadata encryptedFolderMetadata,
String privateKey)
throws IOException, NoSuchAlgorithmException, ShortBufferException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
NoSuchProviderException, IllegalBlockSizeException, CertificateException, InvalidKeySpecException {
HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
DecryptedFolderMetadata decryptedFolderMetadata = new DecryptedFolderMetadata(encryptedFolderMetadata.metadata,
files);
for (Map.Entry<String, EncryptedFolderMetadata.EncryptedFile> entry : encryptedFolderMetadata.files.entrySet()) {
String key = entry.getKey();
EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue();
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
decryptedFile.initializationVector = encryptedFile.initializationVector;
decryptedFile.metadataKey = encryptedFile.metadataKey;
decryptedFile.authenticationTag = encryptedFile.authenticationTag;
byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(EncryptionUtils.decryptStringAsymmetric(
decryptedFolderMetadata.metadata.metadataKeys.get(encryptedFile.metadataKey),
privateKey));
// decrypt
String dataJson = EncryptionUtils.decryptStringSymmetric(encryptedFile.encrypted, decryptedMetadataKey);
decryptedFile.encrypted = EncryptionUtils.deserializeJSON(dataJson,
new TypeToken<DecryptedFolderMetadata.Data>() {
});
files.put(key, decryptedFile);
}
return decryptedFolderMetadata;
}
/**
* Download metadata for folder and decrypt it
*
* @return decrypted metadata or null
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static @Nullable
DecryptedFolderMetadata downloadFolderMetadata(OCFile folder, OwnCloudClient client,
Context context, Account account) {
GetMetadataOperation getMetadataOperation = new GetMetadataOperation(folder.getLocalId());
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
if (!getMetadataOperationResult.isSuccess()) {
return null;
}
// decrypt metadata
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
String privateKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PRIVATE_KEY);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
});
try {
return EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey);
} catch (Exception e) {
return null;
}
}
/*
BASE 64
*/
public static byte[] encodeStringToBase64Bytes(String string) {
try {
return Base64.encode(string.getBytes(), Base64.NO_WRAP);
} catch (Exception e) {
return new byte[0];
}
}
public static String decodeBase64BytesToString(byte[] bytes) {
try {
return new String(Base64.decode(bytes, Base64.NO_WRAP));
} catch (Exception e) {
return "";
}
}
public static String encodeBytesToBase64String(byte[] bytes) {
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
public static byte[] decodeStringToBase64Bytes(String string) {
return Base64.decode(string, Base64.NO_WRAP);
}
/*
ENCRYPTION
*/
/**
* @param ocFile file do crypt
* @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
* @param iv initialization vector, either from metadata or {@link EncryptionUtils#generateIV()}
* @return encryptedFile with encryptedBytes and authenticationTag
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static EncryptedFile encryptFile(OCFile ocFile, byte[] encryptionKeyBytes, byte[] iv)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
File file = new File(ocFile.getStoragePath());
return encryptFile(file, encryptionKeyBytes, iv);
}
/**
* @param file file do crypt
* @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
* @param iv initialization vector, either from metadata or {@link EncryptionUtils#generateIV()}
* @return encryptedFile with encryptedBytes and authenticationTag
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static EncryptedFile encryptFile(File file, byte[] encryptionKeyBytes, byte[] iv)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
Key key = new SecretKeySpec(encryptionKeyBytes, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
byte[] fileBytes = new byte[(int) randomAccessFile.length()];
randomAccessFile.readFully(fileBytes);
byte[] cryptedBytes = cipher.doFinal(fileBytes);
String authenticationTag = encodeBytesToBase64String(Arrays.copyOfRange(cryptedBytes,
cryptedBytes.length - (128 / 8), cryptedBytes.length));
return new EncryptedFile(cryptedBytes, authenticationTag);
}
/**
* @param file encrypted file
* @param encryptionKeyBytes key from metadata
* @param iv initialization vector from metadata
* @param authenticationTag authenticationTag from metadata
* @return decrypted byte[]
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv, byte[] authenticationTag)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
Key key = new SecretKeySpec(encryptionKeyBytes, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
byte[] fileBytes = new byte[(int) randomAccessFile.length()];
randomAccessFile.readFully(fileBytes);
// check authentication tag
byte[] extractedAuthenticationTag = Arrays.copyOfRange(fileBytes,
fileBytes.length - (128 / 8), fileBytes.length);
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
throw new SecurityException("Tag not correct");
}
return cipher.doFinal(fileBytes);
}
public static class EncryptedFile {
public byte[] encryptedBytes;
public String authenticationTag;
public EncryptedFile(byte[] encryptedBytes, String authenticationTag) {
this.encryptedBytes = encryptedBytes;
this.authenticationTag = authenticationTag;
}
}
/**
* Encrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
* Asymmetric encryption, with private and public key
*
* @param string String to encrypt
* @param cert contains public key in it
* @return encrypted string
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String encryptStringAsymmetric(String string, String cert)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, InvalidKeySpecException,
CertificateException {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
String trimmedCert = cert.replace("-----BEGIN CERTIFICATE-----\n", "")
.replace("-----END CERTIFICATE-----\n", "");
byte[] encodedCert = trimmedCert.getBytes("UTF-8");
byte[] decodedCert = org.apache.commons.codec.binary.Base64.decodeBase64(encodedCert);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
InputStream in = new ByteArrayInputStream(decodedCert);
X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(in);
PublicKey realPublicKey = certificate.getPublicKey();
cipher.init(Cipher.ENCRYPT_MODE, realPublicKey);
byte[] bytes = encodeStringToBase64Bytes(string);
byte[] cryptedBytes = cipher.doFinal(bytes);
return encodeBytesToBase64String(cryptedBytes);
}
/**
* Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
* Asymmetric encryption, with private and public key
*
* @param string string to decrypt
* @param privateKeyString private key
* @return decrypted string
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String decryptStringAsymmetric(String string, String privateKeyString)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, CertificateException,
InvalidKeySpecException {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
byte[] privateKeyBytes = decodeStringToBase64Bytes(privateKeyString);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(keySpec);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bytes = decodeStringToBase64Bytes(string);
byte[] encodedBytes = cipher.doFinal(bytes);
return decodeBase64BytesToString(encodedBytes);
}
/**
* Encrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
* Asymmetric encryption, with private and public key
*
* @param string String to encrypt
* @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
* @return encrypted string
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String encryptStringSymmetric(String string, byte[] encryptionKeyBytes)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, InvalidKeySpecException,
CertificateException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = generateIV();
Key key = new SecretKeySpec(encryptionKeyBytes, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] bytes = encodeStringToBase64Bytes(string);
byte[] cryptedBytes = cipher.doFinal(bytes);
String encodedCryptedBytes = encodeBytesToBase64String(cryptedBytes);
String encodedIV = encodeBytesToBase64String(iv);
return encodedCryptedBytes + ivDelimiter + encodedIV;
}
/**
* Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
* Asymmetric encryption, with private and public key
*
* @param string string to decrypt
* @param encryptionKeyBytes key from metadata
* @return decrypted string
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String decryptStringSymmetric(String string, byte[] encryptionKeyBytes)
throws NoSuchProviderException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, CertificateException,
InvalidKeySpecException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
String[] strings = string.split(ivDelimiter);
String cipherString = strings[0];
byte[] iv = new IvParameterSpec(decodeStringToBase64Bytes(strings[1])).getIV();
Key key = new SecretKeySpec(encryptionKeyBytes, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] bytes = decodeStringToBase64Bytes(cipherString);
byte[] encodedBytes = cipher.doFinal(bytes);
return decodeBase64BytesToString(encodedBytes);
}
/**
* Encrypt private key with symmetric AES encryption, GCM mode mode and no padding
*
* @param privateKey byte64 encoded string representation of private key
* @param keyPhrase key used for encryption, e.g. 12 random words
* {@link EncryptionUtils#getRandomWords(int, Context)}
* @return encrypted string, bytes first encoded base64, IV separated with "|", then to string
*/
public static String encryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException, InvalidKeySpecException, InvalidParameterSpecException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(keyPhrase.toCharArray(), salt, iterationCount, keyStrength);
SecretKey tmp = factory.generateSecret(spec);
SecretKeySpec key = new SecretKeySpec(tmp.getEncoded(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] bytes = encodeStringToBase64Bytes(privateKey);
byte[] encrypted = cipher.doFinal(bytes);
byte[] iv = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
String encodedIV = encodeBytesToBase64String(iv);
String encodedEncryptedBytes = encodeBytesToBase64String(encrypted);
return encodedEncryptedBytes + ivDelimiter + encodedIV;
}
/**
* Decrypt private key with symmetric AES encryption, GCM mode mode and no padding
*
* @param privateKey byte64 encoded string representation of private key, IV separated with "|"
* @param keyPhrase key used for encryption, e.g. 12 random words
* {@link EncryptionUtils#getRandomWords(int, Context)}
* @return decrypted string
*/
public static String decryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException, InvalidKeySpecException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(keyPhrase.toCharArray(), salt, iterationCount, keyStrength);
SecretKey tmp = factory.generateSecret(spec);
SecretKeySpec key = new SecretKeySpec(tmp.getEncoded(), "AES");
// handle private key
String[] strings = privateKey.split(ivDelimiter);
String realPrivateKey = strings[0];
String iv = strings[1];
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(decodeStringToBase64Bytes(iv)));
byte[] bytes = decodeStringToBase64Bytes(realPrivateKey);
byte[] decrypted = cipher.doFinal(bytes);
return decodeBase64BytesToString(decrypted);
}
/*
Helper
*/
public static String getMD5Sum(File file) {
try {
FileInputStream fileInputStream = new FileInputStream(file);
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] bytes = new byte[2048];
int readBytes;
while ((readBytes = fileInputStream.read(bytes)) != -1) {
md5.update(bytes, 0, readBytes);
}
return new String(Hex.encodeHex(md5.digest()));
} catch (Exception e) {
Log_OC.e(TAG, e.getMessage());
}
return "";
}
public static ArrayList<String> getRandomWords(int count, Context context) throws IOException {
InputStream ins = context.getResources().openRawResource(context.getResources()
.getIdentifier("encryption_key_words", "raw", context.getPackageName()));
InputStreamReader inputStreamReader = new InputStreamReader(ins);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
ArrayList<String> lines = new ArrayList<>();
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
SecureRandom random = new SecureRandom();
ArrayList<String> outputLines = new ArrayList<>();
for (int i = 0; i < count; i++) {
int randomLine = (int) (random.nextDouble() * lines.size());
outputLines.add(lines.get(randomLine));
}
return outputLines;
}
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048, new SecureRandom());
return keyGen.generateKeyPair();
}
public static byte[] generateKey() {
KeyGenerator keyGenerator;
try {
keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
return keyGenerator.generateKey().getEncoded();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
public static byte[] generateIV() {
SecureRandom random = new SecureRandom();
final byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
}