/*
* 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 .
*/
package com.owncloud.android.utils;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Pair;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.nextcloud.client.account.User;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.EncryptedFiledrop;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile;
import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser;
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile;
import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedMetadata;
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.e2ee.GetMetadataRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.LockFileRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.MetadataResponse;
import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation;
import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.UnlockFileV1RemoteOperation;
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation;
import com.owncloud.android.lib.resources.files.model.ServerFileInterface;
import com.owncloud.android.lib.resources.status.E2EVersion;
import com.owncloud.android.lib.resources.status.NextcloudVersion;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.lib.resources.status.Problem;
import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation;
import com.owncloud.android.operations.UploadException;
import com.owncloud.android.utils.theme.CapabilityUtils;
import org.apache.commons.httpclient.HttpStatus;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
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.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.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Utils for encryption
*/
public final class EncryptionUtils {
private static final String TAG = EncryptionUtils.class.getSimpleName();
public static final String PUBLIC_KEY = "PUBLIC_KEY";
public static final String PRIVATE_KEY = "PRIVATE_KEY";
public static final String MNEMONIC = "MNEMONIC";
public static final int ivLength = 16;
public static final int saltLength = 40;
public static final String ivDelimiter = "|"; // not base64 encoded
public static final String ivDelimiterOld = "fA=="; // "|" base64 encoded
private static final char HASH_DELIMITER = '$';
private static final int iterationCount = 1024;
private static final int keyStrength = 256;
private static final String AES_CIPHER = "AES/GCM/NoPadding";
private static final String AES = "AES";
public static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
public static final String RSA = "RSA";
@VisibleForTesting
public static final String MIGRATED_FOLDER_IDS = "MIGRATED_FOLDER_IDS";
private EncryptionUtils() {
// utility class -> private constructor
}
/*
JSON
*/
public static T deserializeJSON(String json, TypeToken type, boolean excludeTransient) {
if (excludeTransient) {
return new Gson().fromJson(json, type.getType());
} else {
return new GsonBuilder().excludeFieldsWithModifiers(0).create().fromJson(json, type.getType());
}
}
public static T deserializeJSON(String json, TypeToken type) {
return deserializeJSON(json, type, false);
}
public static String serializeJSON(Object data, boolean excludeTransient) {
if (excludeTransient) {
return new GsonBuilder()
.disableHtmlEscaping()
.create()
.toJson(data);
} else {
return new GsonBuilder()
.disableHtmlEscaping()
.excludeFieldsWithModifiers(0)
.create()
.toJson(data);
}
}
public static String serializeJSON(Object data) {
return serializeJSON(data, false);
}
/*
METADATA
*/
/**
* Encrypt folder metaData V1
*
* @param decryptedFolderMetadata folder metaData to encrypt
* @return EncryptedFolderMetadataFile encrypted folder metadata
*/
public static EncryptedFolderMetadataFileV1 encryptFolderMetadata(
DecryptedFolderMetadataFileV1 decryptedFolderMetadata,
String publicKey,
long parentId,
User user,
ArbitraryDataProvider arbitraryDataProvider
)
throws NoSuchAlgorithmException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, CertificateException {
HashMap files = new HashMap<>();
HashMap filesdrop = new HashMap<>();
EncryptedFolderMetadataFileV1 encryptedFolderMetadata = new EncryptedFolderMetadataFileV1(decryptedFolderMetadata
.getMetadata(),
files,
filesdrop);
// set new metadata key
byte[] metadataKeyBytes = EncryptionUtils.generateKey();
String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(
EncryptionUtils.encodeBytesToBase64String(metadataKeyBytes),
publicKey);
encryptedFolderMetadata.getMetadata().setMetadataKey(encryptedMetadataKey);
// store that this folder has been migrated
addIdToMigratedIds(parentId, user, arbitraryDataProvider);
// Encrypt each file in "files"
for (Map.Entry entry : decryptedFolderMetadata
.getFiles().entrySet()) {
String key = entry.getKey();
DecryptedFile decryptedFile = entry.getValue();
EncryptedFolderMetadataFileV1.EncryptedFile encryptedFile = new EncryptedFolderMetadataFileV1.EncryptedFile();
encryptedFile.setInitializationVector(decryptedFile.getInitializationVector());
encryptedFile.setAuthenticationTag(decryptedFile.getAuthenticationTag());
// encrypt
String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted());
encryptedFile.setEncrypted(EncryptionUtils.encryptStringSymmetricAsString(dataJson, metadataKeyBytes));
files.put(key, encryptedFile);
}
// set checksum
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim();
String checksum = EncryptionUtils.generateChecksum(decryptedFolderMetadata, mnemonic);
encryptedFolderMetadata.getMetadata().setChecksum(checksum);
return encryptedFolderMetadata;
}
/**
* normally done on server only internal test
*/
@VisibleForTesting
public static void encryptFileDropFiles(DecryptedFolderMetadataFileV1 decryptedFolderMetadata,
EncryptedFolderMetadataFileV1 encryptedFolderMetadata,
String cert) throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, CertificateException,
InvalidAlgorithmParameterException {
final Map filesdrop = encryptedFolderMetadata.getFiledrop();
for (Map.Entry entry : decryptedFolderMetadata
.getFiledrop().entrySet()) {
String key = entry.getKey();
DecryptedFile decryptedFile = entry.getValue();
byte[] byt = generateKey();
String metadataKey0 = encodeBytesToBase64String(byt);
String enc = encryptStringAsymmetric(metadataKey0, cert);
String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted());
String encJson = encryptStringSymmetricAsString(dataJson, byt);
int delimiterPosition = encJson.lastIndexOf(ivDelimiter);
String encryptedInitializationVector = encJson.substring(delimiterPosition + ivDelimiter.length());
String encodedCryptedBytes = encJson.substring(0, delimiterPosition);
byte[] bytes = decodeStringToBase64Bytes(encodedCryptedBytes);
// check authentication tag
byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes,
bytes.length - (128 / 8),
bytes.length);
String encryptedTag = encodeBytesToBase64String(extractedAuthenticationTag);
EncryptedFiledrop encryptedFile = new EncryptedFiledrop(encodedCryptedBytes,
decryptedFile.getInitializationVector(),
decryptedFile.getAuthenticationTag(),
enc,
encryptedTag,
encryptedInitializationVector);
filesdrop.put(key, encryptedFile);
}
}
/*
* decrypt folder metaData V1 with private key
*/
public static DecryptedFolderMetadataFileV1 decryptFolderMetaData(EncryptedFolderMetadataFileV1 encryptedFolderMetadata,
String privateKey,
ArbitraryDataProvider arbitraryDataProvider,
User user,
long remoteId)
throws NoSuchAlgorithmException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, InvalidKeySpecException {
HashMap files = new HashMap<>();
DecryptedFolderMetadataFileV1 decryptedFolderMetadata = new DecryptedFolderMetadataFileV1(
encryptedFolderMetadata.getMetadata(), files);
byte[] decryptedMetadataKey = null;
String encryptedMetadataKey = decryptedFolderMetadata.getMetadata().getMetadataKey();
if (encryptedMetadataKey != null) {
decryptedMetadataKey = decodeStringToBase64Bytes(
decryptStringAsymmetric(encryptedMetadataKey, privateKey));
}
if (encryptedFolderMetadata.getFiles() != null) {
for (Map.Entry entry : encryptedFolderMetadata
.getFiles().entrySet()) {
String key = entry.getKey();
EncryptedFolderMetadataFileV1.EncryptedFile encryptedFile = entry.getValue();
DecryptedFile decryptedFile = new DecryptedFile();
decryptedFile.setInitializationVector(encryptedFile.getInitializationVector());
decryptedFile.setMetadataKey(encryptedFile.getMetadataKey());
decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
if (decryptedMetadataKey == null) {
decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(
decryptStringAsymmetric(decryptedFolderMetadata.getMetadata()
.getMetadataKeys().get(encryptedFile.getMetadataKey()),
privateKey));
}
// decrypt
String dataJson = EncryptionUtils.decryptStringSymmetric(encryptedFile.getEncrypted(), decryptedMetadataKey);
decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(dataJson,
new TypeToken<>() {
}));
files.put(key, decryptedFile);
}
}
// verify checksum
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim();
String checksum = EncryptionUtils.generateChecksum(decryptedFolderMetadata, mnemonic);
String decryptedFolderChecksum = decryptedFolderMetadata.getMetadata().getChecksum();
if (TextUtils.isEmpty(decryptedFolderChecksum) &&
isFolderMigrated(remoteId, user, arbitraryDataProvider)) {
reportE2eError(arbitraryDataProvider, user);
throw new IllegalStateException("Possible downgrade attack detected!");
}
if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) {
reportE2eError(arbitraryDataProvider, user);
throw new IllegalStateException("Wrong checksum!");
}
Map fileDrop = encryptedFolderMetadata.getFiledrop();
if (fileDrop != null) {
for (Map.Entry entry : fileDrop.entrySet()) {
String key = entry.getKey();
EncryptedFiledrop encryptedFile = entry.getValue();
// decrypt key
String encryptedKey = decryptStringAsymmetric(encryptedFile.getEncryptedKey(),
privateKey);
// decrypt encrypted blob with key
String decryptedData = decryptStringSymmetricAsString(
encryptedFile.getEncrypted(),
decodeStringToBase64Bytes(encryptedKey),
decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()),
decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()),
arbitraryDataProvider,
user
);
DecryptedFile decryptedFile = new DecryptedFile();
decryptedFile.setInitializationVector(encryptedFile.getInitializationVector());
decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(decryptedData,
new TypeToken<>() {
}));
files.put(key, decryptedFile);
// remove from filedrop
fileDrop.remove(key);
}
}
return decryptedFolderMetadata;
}
/**
* Download metadata (v1 or v2) for folder and decrypt it
*
* @return decrypted v2 metadata or null
*/
@SuppressFBWarnings("URV")
public static @Nullable
Object
downloadFolderMetadata(OCFile folder,
OwnCloudClient client,
Context context,
User user
) {
RemoteOperationResult getMetadataOperationResult = new GetMetadataRemoteOperation(folder.getLocalId())
.execute(client);
if (!getMetadataOperationResult.isSuccess()) {
return null;
}
OCCapability capability = CapabilityUtils.getCapability(context);
// decrypt metadata
EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata();
E2EVersion version = determinateVersion(serializedEncryptedMetadata);
switch (version) {
case UNKNOWN:
Log_OC.e(TAG, "Unknown e2e state");
return null;
case V1_0, V1_1, V1_2:
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<>() {
});
try {
DecryptedFolderMetadataFileV1 v1 = decryptFolderMetaData(encryptedFolderMetadata,
privateKey,
arbitraryDataProvider,
user,
folder.getLocalId());
if (capability.getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
new EncryptionUtilsV2().migrateV1ToV2andUpload(
v1,
client.getUserId(),
publicKey,
folder,
new FileDataStorageManager(user, context.getContentResolver()),
client,
user,
context
);
} else {
return v1;
}
} catch (Exception e) {
// TODO do not crash, but show meaningful error
Log_OC.e(TAG, "Could not decrypt metadata for " + folder.getDecryptedFileName(), e);
return null;
}
case V2_0:
return encryptionUtilsV2.parseAnyMetadata(getMetadataOperationResult.getResultData(),
user,
client,
context,
folder);
}
return null;
}
public static E2EVersion determinateVersion(String metadata) {
try {
EncryptedFolderMetadataFileV1 v1 = EncryptionUtils.deserializeJSON(
metadata,
new TypeToken<>() {
});
double version = v1.getMetadata().getVersion();
if (version == 1.0) {
return E2EVersion.V1_0;
} else if (version == 1.1) {
return E2EVersion.V1_1;
} else if (version == 1.2) {
return E2EVersion.V1_2;
} else {
throw new IllegalStateException("Unknown version");
}
} catch (Exception e) {
EncryptedFolderMetadataFile v2 = EncryptionUtils.deserializeJSON(
metadata,
new TypeToken<>() {
});
if ("2.0".equals(v2.getVersion()) || "2".equals(v2.getVersion())) {
return E2EVersion.V2_0;
}
}
return E2EVersion.UNKNOWN;
}
/*
BASE 64
*/
@SuppressFBWarnings({"DM", "MDM"})
public static byte[] encodeStringToBase64Bytes(String string) {
try {
return Base64.encode(string.getBytes(), Base64.NO_WRAP);
} catch (Exception e) {
return new byte[0];
}
}
@SuppressFBWarnings({"DM", "MDM"})
public static String decodeBase64BytesToString(byte[] bytes) {
try {
return new String(Base64.decode(bytes, Base64.NO_WRAP));
} catch (Exception e) {
return "";
}
}
@SuppressFBWarnings({"DM", "MDM"})
public static String encodeBytesToBase64String(byte[] bytes) {
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
@SuppressFBWarnings({"DM", "MDM"})
public static String encodeStringToBase64String(String string) {
return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
}
@SuppressFBWarnings({"DM", "MDM"})
public static String decodeBase64StringToString(String string) {
return new String(Base64.decode(string, 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#randomBytes(int)}
* @return encryptedFile with encryptedBytes and authenticationTag
*/
public static EncryptedFile encryptFile(OCFile ocFile, byte[] encryptionKeyBytes, byte[] iv)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException, IOException, InvalidParameterSpecException {
File file = new File(ocFile.getStoragePath());
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, encryptionKeyBytes, iv);
return encryptFile(file, cipher);
}
public static EncryptedFile encryptFile(File file, Cipher cipher) throws IOException, InvalidParameterSpecException {
File encryptedFile = new File(file.getAbsolutePath() + ".enc");
encryptFileWithGivenCipher(file, encryptedFile, cipher);
byte[] authenticationTag = cipher.getParameters().getParameterSpec(GCMParameterSpec.class).getIV();
String authenticationTagString = encodeBytesToBase64String(authenticationTag);
return new EncryptedFile(encryptedFile, authenticationTagString);
}
public static Cipher getCipher(int mode, byte[] encryptionKeyBytes, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
Key key = new SecretKeySpec(encryptionKeyBytes, AES);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(mode, key, spec);
return cipher;
}
public static void encryptFileWithGivenCipher(File inputFile, File encryptedFile, Cipher cipher) throws IOException {
FileInputStream inputStream = new FileInputStream(inputFile);
FileOutputStream fileOutputStream = new FileOutputStream(encryptedFile);
CipherOutputStream outputStream = new CipherOutputStream(fileOutputStream, cipher);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
inputStream.close();
}
// FIXME Decryption is broken
public static byte[] decryptFile(
Cipher cipher,
File file,
byte[] authenticationTag,
ArbitraryDataProvider arbitraryDataProvider,
User user)
throws BadPaddingException, IllegalBlockSizeException, IOException {
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)) {
reportE2eError(arbitraryDataProvider, user);
throw new SecurityException("Tag not correct");
}
return cipher.doFinal(fileBytes);
}
/**
* 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
*/
public static String encryptStringAsymmetric(String string, String cert)
throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException,
CertificateException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
String trimmedCert = cert.replace("-----BEGIN CERTIFICATE-----\n", "")
.replace("-----END CERTIFICATE-----\n", "");
byte[] encodedCert = trimmedCert.getBytes(StandardCharsets.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);
}
public static String encryptStringAsymmetricV2(byte[] bytes, String cert)
throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException,
CertificateException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
String trimmedCert = cert.replace("-----BEGIN CERTIFICATE-----\n", "")
.replace("-----END CERTIFICATE-----\n", "");
byte[] encodedCert = trimmedCert.getBytes(StandardCharsets.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[] cryptedBytes = cipher.doFinal(bytes);
return encodeBytesToBase64String(cryptedBytes);
}
public static String encryptStringAsymmetric(String string, PublicKey publicKey) throws NoSuchPaddingException,
NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
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
*/
public static String decryptStringAsymmetric(String string, String privateKeyString)
throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException,
InvalidKeySpecException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
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);
}
public static byte[] decryptStringAsymmetricAsBytes(String string, String privateKeyString)
throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException,
InvalidKeySpecException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
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);
return cipher.doFinal(bytes);
}
public static byte[] decryptStringAsymmetricV2(String string, String privateKeyString)
throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException,
InvalidKeySpecException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
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;
try {
bytes = decodeStringToBase64Bytes(string);
} catch (Exception e) {
bytes = encodeStringToBase64Bytes(string);
}
return cipher.doFinal(bytes);
}
/**
* Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding Asymmetric encryption, with private
* and public key
*
* @param string string to decrypt
* @param privateKey private key
* @return decrypted string
*/
public static String decryptStringAsymmetric(String string, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(RSA_CIPHER);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bytes = decodeStringToBase64Bytes(string);
byte[] encodedBytes = cipher.doFinal(bytes);
return decodeBase64BytesToString(encodedBytes);
}
/**
* Decrypt string with AES/GCM/NoPadding
*
* @param string string to decrypt
* @param encryptionKeyBytes key from metadata
* @return decrypted string
*/
public static String encryptStringSymmetricAsString(String string, byte[] encryptionKeyBytes)
throws NoSuchPaddingException,
InvalidKeyException,
NoSuchAlgorithmException,
IllegalBlockSizeException,
BadPaddingException,
InvalidAlgorithmParameterException {
EncryptedMetadata metadata = encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiter);
return metadata.getCiphertext();
}
@VisibleForTesting
public static String encryptStringSymmetricAsStringOld(String string, byte[] encryptionKeyBytes)
throws NoSuchPaddingException,
InvalidKeyException,
NoSuchAlgorithmException,
IllegalBlockSizeException,
BadPaddingException,
InvalidAlgorithmParameterException {
EncryptedMetadata metadata = encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiterOld);
return metadata.getCiphertext();
}
// /**
// * Encrypt string with AES/GCM/NoPadding
// *
// * @param string string to encrypt
// * @param encryptionKeyBytes key from metadata
// * @return decrypted string
// */
// private static String encryptStringSymmetric(String string,
// byte[] encryptionKeyBytes,
// String delimiter)
// throws NoSuchAlgorithmException,
// InvalidAlgorithmParameterException,
// NoSuchPaddingException,
// InvalidKeyException,
// BadPaddingException,
// IllegalBlockSizeException {
//
// Cipher cipher = Cipher.getInstance(AES_CIPHER);
// byte[] iv = randomBytes(ivLength);
//
// 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 + delimiter + encodedIV;
// }
public static String decryptStringSymmetricAsString(String string,
byte[] encryptionKeyBytes,
byte[] iv,
byte[] authenticationTag,
ArbitraryDataProvider arbitraryDataProvider,
User user
) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
return decryptStringSymmetricAsString(
decodeStringToBase64Bytes(string),
encryptionKeyBytes,
iv,
authenticationTag,
false,
arbitraryDataProvider,
user);
}
public static String decryptStringSymmetricAsString(String string,
byte[] encryptionKeyBytes,
byte[] iv,
byte[] authenticationTag,
boolean fileDropV2,
ArbitraryDataProvider arbitraryDataProvider,
User user) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
return decryptStringSymmetricAsString(
decodeStringToBase64Bytes(string),
encryptionKeyBytes,
iv,
authenticationTag,
fileDropV2,
arbitraryDataProvider,
user);
}
public static String decryptStringSymmetricAsString(byte[] bytes,
byte[] encryptionKeyBytes,
byte[] iv,
byte[] authenticationTag,
boolean fileDropV2,
ArbitraryDataProvider arbitraryDataProvider,
User user)
throws NoSuchPaddingException,
NoSuchAlgorithmException,
InvalidAlgorithmParameterException,
InvalidKeyException,
IllegalBlockSizeException,
BadPaddingException {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
Key key = new SecretKeySpec(encryptionKeyBytes, AES);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
// check authentication tag
byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes,
bytes.length - (128 / 8),
bytes.length);
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
reportE2eError(arbitraryDataProvider, user);
throw new SecurityException("Tag not correct");
}
byte[] encodedBytes = cipher.doFinal(bytes);
if (fileDropV2) {
return new EncryptionUtilsV2().gZipDecompress(encodedBytes);
} else {
return decodeBase64BytesToString(encodedBytes);
}
}
public static EncryptedMetadata encryptStringSymmetric(
String string,
byte[] encryptionKeyBytes) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
return encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiter);
}
public static EncryptedMetadata encryptStringSymmetric(
String string,
byte[] encryptionKeyBytes,
String delimiter) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException {
byte[] bytes = encodeStringToBase64Bytes(string);
return encryptStringSymmetric(bytes, encryptionKeyBytes, delimiter);
}
/**
* Encrypt string with AES/GCM/NoPadding
*
* @param bytes byte array
* @param encryptionKeyBytes key from metadata
* @return decrypted string
*/
public static EncryptedMetadata encryptStringSymmetric(
byte[] bytes,
byte[] encryptionKeyBytes,
String delimiter)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException,
NoSuchPaddingException,
InvalidKeyException,
BadPaddingException,
IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
byte[] iv = randomBytes(ivLength);
Key key = new SecretKeySpec(encryptionKeyBytes, AES);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] cryptedBytes = cipher.doFinal(bytes);
String encodedCryptedBytes = encodeBytesToBase64String(cryptedBytes);
String encodedIV = encodeBytesToBase64String(iv);
String authenticationTag = encodeBytesToBase64String(Arrays.copyOfRange(cryptedBytes,
cryptedBytes.length - (128 / 8),
cryptedBytes.length));
return new EncryptedMetadata(encodedCryptedBytes + delimiter + encodedIV, encodedIV, authenticationTag);
}
/**
* 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
*/
public static String decryptStringSymmetric(String string, byte[] encryptionKeyBytes)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
String ivString;
int delimiterPosition = string.lastIndexOf(ivDelimiter);
if (delimiterPosition == -1) {
// backward compatibility
delimiterPosition = string.lastIndexOf(ivDelimiterOld);
ivString = string.substring(delimiterPosition + ivDelimiterOld.length());
} else {
ivString = string.substring(delimiterPosition + ivDelimiter.length());
}
String cipherString = string.substring(0, delimiterPosition);
byte[] iv = new IvParameterSpec(decodeStringToBase64Bytes(ivString)).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);
}
/**
* Decrypt string with AES/GCM/NoPadding
*
* @param string string to decrypt
* @param encryptionKeyBytes key from metadata
* @param authenticationTag auth tag to check
* @return decrypted string
*/
public static byte[] decryptStringSymmetric(String string,
byte[] encryptionKeyBytes,
String authenticationTag,
String ivString)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
int delimiterPosition = string.lastIndexOf(ivDelimiter);
String cipherString;
if (delimiterPosition == -1) {
cipherString = string;
} else {
cipherString = string.substring(0, delimiterPosition);
}
byte[] iv = new IvParameterSpec(decodeStringToBase64Bytes(ivString)).getIV();
Key key = new SecretKeySpec(encryptionKeyBytes, AES);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] bytes = decodeStringToBase64Bytes(cipherString);
// check authentication tag
if (authenticationTag != null) {
byte[] authenticationTagBytes = decodeStringToBase64Bytes(authenticationTag);
byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes,
bytes.length - (128 / 8),
bytes.length);
if (!Arrays.equals(extractedAuthenticationTag, authenticationTagBytes)) {
throw new SecurityException("Tag not correct");
}
}
return cipher.doFinal(bytes);
}
/**
* 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,
InvalidKeyException,
BadPaddingException,
IllegalBlockSizeException,
InvalidKeySpecException {
return encryptPrivateKey(privateKey, keyPhrase, ivDelimiter);
}
@VisibleForTesting
public static String encryptPrivateKeyOld(String privateKey, String keyPhrase)
throws NoSuchPaddingException,
NoSuchAlgorithmException,
InvalidKeyException,
BadPaddingException,
IllegalBlockSizeException,
InvalidKeySpecException {
return encryptPrivateKey(privateKey, keyPhrase, ivDelimiterOld);
}
private static String encryptPrivateKey(String privateKey, String keyPhrase, String delimiter)
throws NoSuchPaddingException,
NoSuchAlgorithmException,
InvalidKeyException,
BadPaddingException,
IllegalBlockSizeException,
InvalidKeySpecException {
Cipher cipher = Cipher.getInstance(AES_CIPHER);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] salt = randomBytes(saltLength);
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.getIV();
String encodedIV = encodeBytesToBase64String(iv);
String encodedSalt = encodeBytesToBase64String(salt);
String encodedEncryptedBytes = encodeBytesToBase64String(encrypted);
return encodedEncryptedBytes + delimiter + encodedIV + delimiter + encodedSalt;
}
/**
* 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
*/
@SuppressFBWarnings("UCPM_USE_CHARACTER_PARAMETERIZED_METHOD")
public static String decryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
NoSuchAlgorithmException, InvalidKeyException, BadPaddingException,
IllegalBlockSizeException, InvalidKeySpecException, InvalidAlgorithmParameterException {
String[] strings;
// split up iv, salt
if (privateKey.lastIndexOf(ivDelimiter) == -1) {
// backward compatibility
strings = privateKey.split(ivDelimiterOld);
} else {
strings = privateKey.split("\\" + ivDelimiter);
}
String realPrivateKey = strings[0];
byte[] iv = decodeStringToBase64Bytes(strings[1]);
byte[] salt = decodeStringToBase64Bytes(strings[2]);
Cipher cipher = Cipher.getInstance(AES_CIPHER);
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.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] bytes = decodeStringToBase64Bytes(realPrivateKey);
byte[] decrypted = cipher.doFinal(bytes);
String pemKey = decodeBase64BytesToString(decrypted);
return pemKey.replaceAll("\n", "").replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "");
}
public static String privateKeyToPEM(PrivateKey privateKey) {
String privateKeyString = encodeBytesToBase64String(privateKey.getEncoded());
return "-----BEGIN PRIVATE KEY-----\n" + privateKeyString.replaceAll("(.{65})", "$1\n")
+ "\n-----END PRIVATE KEY-----";
}
public static PrivateKey PEMtoPrivateKey(String pem) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] privateKeyBytes = EncryptionUtils.decodeStringToBase64Bytes(pem);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory kf = KeyFactory.getInstance(EncryptionUtils.RSA);
return kf.generatePrivate(keySpec);
}
/*
Helper
*/
public static ArrayList getRandomWords(int count, Context context) throws IOException {
InputStream ins = context.getResources().openRawResource(R.raw.encryption_key_words);
InputStreamReader inputStreamReader = new InputStreamReader(ins);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
List lines = new ArrayList<>();
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
SecureRandom random = new SecureRandom();
ArrayList outputLines = Lists.newArrayListWithCapacity(count);
for (int i = 0; i < count; i++) {
int randomLine = random.nextInt(lines.size());
outputLines.add(lines.get(randomLine));
}
return outputLines;
}
/**
* Generates private/public key pair, used for asymmetric encryption
*
* @return KeyPair
*/
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA);
keyGen.initialize(2048, new SecureRandom());
return keyGen.generateKeyPair();
}
/**
* Generates key for symmetric encryption
*
* @return byte[] byteArray of key
*/
public static byte[] generateKey() {
KeyGenerator keyGenerator;
try {
keyGenerator = KeyGenerator.getInstance(AES);
keyGenerator.init(128);
return keyGenerator.generateKey().getEncoded();
} catch (NoSuchAlgorithmException e) {
Log_OC.e(TAG, e.getMessage());
}
return null;
}
/**
* Generates key for symmetric encryption
*
* @return String String base64 encoded key
*/
public static String generateKeyString() {
return EncryptionUtils.encodeBytesToBase64String(generateKey());
}
public static byte[] randomBytes(int size) {
SecureRandom random = new SecureRandom();
final byte[] iv = new byte[size];
random.nextBytes(iv);
return iv;
}
/**
* Generate a SHA512 with appended salt
*
* @param token token to be hashed
* @return SHA512 with appended salt, delimiter HASH_DELIMITER
*/
public static String generateSHA512(String token) {
String salt = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.randomBytes(EncryptionUtils.saltLength));
return generateSHA512(token, salt);
}
/**
* Generate a SHA512 with appended salt
*
* @param token token to be hashed
* @return SHA512 with appended salt, delimiter HASH_DELIMITER
*/
public static String generateSHA512(String token, String salt) {
MessageDigest digest;
String hashedToken = "";
byte[] hash;
try {
digest = MessageDigest.getInstance("SHA-512");
digest.update(salt.getBytes());
hash = digest.digest(token.getBytes());
StringBuilder stringBuilder = new StringBuilder();
for (byte hashByte : hash) {
stringBuilder.append(Integer.toString((hashByte & 0xff) + 0x100, 16).substring(1));
}
stringBuilder.append(HASH_DELIMITER).append(salt);
hashedToken = stringBuilder.toString();
} catch (NoSuchAlgorithmException e) {
Log_OC.e(TAG, "Generating SHA512 failed", e);
}
return hashedToken;
}
public static boolean verifySHA512(String hashWithSalt, String compareToken) {
String salt = hashWithSalt.split("\\" + HASH_DELIMITER)[1];
String newHash = generateSHA512(compareToken, salt);
return hashWithSalt.equals(newHash);
}
public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient client) throws UploadException {
return lockFolder(parentFile, client, -1);
}
public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient client, long counter) throws UploadException {
// Lock folder
LockFileRemoteOperation lockFileOperation = new LockFileRemoteOperation(parentFile.getLocalId(),
counter);
RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client);
if (lockFileOperationResult.isSuccess() &&
!TextUtils.isEmpty(lockFileOperationResult.getResultData())) {
return lockFileOperationResult.getResultData();
} else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
throw new UploadException("Forbidden! Please try again later.)");
} else {
throw new UploadException("Could not lock folder");
}
}
/**
* @param parentFile file metadata should be retrieved for
* @return Pair: boolean: true: metadata already exists, false: metadata new created
*/
public static Pair retrieveMetadataV1(OCFile parentFile,
OwnCloudClient client,
String privateKey,
String publicKey,
ArbitraryDataProvider arbitraryDataProvider,
User user)
throws UploadException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, InvalidKeyException, InvalidKeySpecException, CertificateException {
long localId = parentFile.getLocalId();
GetMetadataRemoteOperation getMetadataOperation = new GetMetadataRemoteOperation(localId);
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
DecryptedFolderMetadataFileV1 metadata;
if (getMetadataOperationResult.isSuccess()) {
// decrypt metadata
String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata();
EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<>() {
});
return new Pair<>(Boolean.TRUE, decryptFolderMetaData(encryptedFolderMetadata,
privateKey,
arbitraryDataProvider,
user,
localId));
} else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) {
// TODO extract
// new metadata
metadata = new DecryptedFolderMetadataFileV1();
metadata.setMetadata(new DecryptedMetadata());
metadata.getMetadata().setVersion(Double.parseDouble(E2EVersion.V1_2.getValue()));
metadata.getMetadata().setMetadataKeys(new HashMap<>());
String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
return new Pair<>(Boolean.FALSE, metadata);
} else {
// TODO E2E: error
throw new UploadException("something wrong");
}
}
/**
* @param parentFile file metadata should be retrieved for
* @return Pair: boolean: true: metadata already exists, false: metadata new created
*/
public static Pair retrieveMetadata(OCFile parentFile,
OwnCloudClient client,
String privateKey,
String publicKey,
FileDataStorageManager storageManager,
User user,
Context context,
ArbitraryDataProvider arbitraryDataProvider)
throws UploadException, Throwable,
InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, InvalidKeyException, InvalidKeySpecException, CertificateException {
long localId = parentFile.getLocalId();
GetMetadataRemoteOperation getMetadataOperation = new GetMetadataRemoteOperation(localId);
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
DecryptedFolderMetadataFile metadata;
if (getMetadataOperationResult.isSuccess()) {
// decrypt metadata
String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata();
EncryptedFolderMetadataFile encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<>() {
});
return new Pair<>(Boolean.TRUE,
new EncryptionUtilsV2().decryptFolderMetadataFile(encryptedFolderMetadata,
client.getUserId(),
privateKey,
parentFile,
storageManager,
client,
parentFile.getE2eCounter(),
getMetadataOperationResult.getResultData().getSignature(),
user,
context,
arbitraryDataProvider)
);
} else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND ||
getMetadataOperationResult.getHttpCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
// new metadata
metadata = new DecryptedFolderMetadataFile(new com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata(),
new ArrayList<>(),
new HashMap<>(),
E2EVersion.V2_0.getValue());
metadata.getUsers().add(new DecryptedUser(client.getUserId(), publicKey));
byte[] metadataKey = EncryptionUtils.generateKey();
if (metadataKey == null) {
throw new UploadException("Could not encrypt folder!");
}
metadata.getMetadata().setMetadataKey(metadataKey);
metadata.getMetadata().getKeyChecksums().add(new EncryptionUtilsV2().hashMetadataKey(metadataKey));
return new Pair<>(Boolean.FALSE, metadata);
} else {
reportE2eError(arbitraryDataProvider, user);
throw new UploadException("something wrong");
}
}
public static void uploadMetadata(ServerFileInterface parentFile,
String serializedFolderMetadata,
String token,
OwnCloudClient client,
boolean metadataExists,
E2EVersion version,
String signature,
ArbitraryDataProvider arbitraryDataProvider,
User user) throws UploadException {
RemoteOperationResult uploadMetadataOperationResult;
if (metadataExists) {
// update metadata
if (version == E2EVersion.V2_0) {
uploadMetadataOperationResult = new UpdateMetadataV2RemoteOperation(
parentFile.getRemoteId(),
serializedFolderMetadata,
token,
signature)
.execute(client);
} else {
uploadMetadataOperationResult = new UpdateMetadataRemoteOperation(
parentFile.getLocalId(),
serializedFolderMetadata,
token)
.execute(client);
}
} else {
// store metadata
if (version == E2EVersion.V2_0) {
uploadMetadataOperationResult = new StoreMetadataV2RemoteOperation(
parentFile.getRemoteId(),
serializedFolderMetadata,
token,
signature
)
.execute(client);
} else {
uploadMetadataOperationResult = new StoreMetadataRemoteOperation(
parentFile.getLocalId(),
serializedFolderMetadata
)
.execute(client);
}
}
if (!uploadMetadataOperationResult.isSuccess()) {
reportE2eError(arbitraryDataProvider, user);
throw new UploadException("Storing/updating metadata was not successful");
}
}
public static RemoteOperationResult unlockFolder(ServerFileInterface parentFolder, OwnCloudClient client, String token) {
if (token != null) {
return new UnlockFileRemoteOperation(parentFolder.getLocalId(), token).execute(client);
} else {
return new RemoteOperationResult<>(new Exception("No token available"));
}
}
public static RemoteOperationResult unlockFolderV1(ServerFileInterface parentFolder, OwnCloudClient client, String token) {
if (token != null) {
return new UnlockFileV1RemoteOperation(parentFolder.getLocalId(), token).execute(client);
} else {
return new RemoteOperationResult<>(new Exception("No token available"));
}
}
public static X509Certificate convertCertFromString(String string) throws CertificateException {
String trimmedCert = string.replace("-----BEGIN CERTIFICATE-----\n", "")
.replace("-----END CERTIFICATE-----\n", "");
byte[] encodedCert = trimmedCert.getBytes(StandardCharsets.UTF_8);
byte[] decodedCert = org.apache.commons.codec.binary.Base64.decodeBase64(encodedCert);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
InputStream in = new ByteArrayInputStream(decodedCert);
return (X509Certificate) certFactory.generateCertificate(in);
}
public static RSAPublicKey convertPublicKeyFromString(String string) throws CertificateException {
return (RSAPublicKey) convertCertFromString(string).getPublicKey();
}
public static void removeE2E(ArbitraryDataProvider arbitraryDataProvider, User user) {
// delete stored E2E keys and mnemonic
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), EncryptionUtils.MNEMONIC);
}
public static boolean isMatchingKeys(KeyPair keyPair, String publicKeyString) throws CertificateException {
// check key
RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyPair.getPrivate();
RSAPublicKey publicKey = EncryptionUtils.convertPublicKeyFromString(publicKeyString);
BigInteger modulusPublic = publicKey.getModulus();
BigInteger modulusPrivate = privateKey.getModulus();
return modulusPrivate.compareTo(modulusPublic) == 0;
}
public static boolean supportsSecureFiledrop(OCFile file, User user) {
return file.isEncrypted() &&
file.isFolder() &&
user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.nextcloud_26);
}
public static String generateChecksum(DecryptedFolderMetadataFileV1 metadataFile,
String mnemonic) throws NoSuchAlgorithmException {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(mnemonic.replaceAll(" ", ""));
ArrayList keys = new ArrayList<>(metadataFile.getFiles().keySet());
Collections.sort(keys);
for (String key : keys) {
stringBuilder.append(key);
}
stringBuilder.append(metadataFile.getMetadata().getMetadataKey());
// sha256 hash-sum
return sha256(stringBuilder.toString());
}
/**
* SHA-256 hash of metadata-key
*/
public static String sha256(String string) throws NoSuchAlgorithmException {
byte[] bytes = MessageDigest
.getInstance("SHA-256")
.digest(string.getBytes(StandardCharsets.UTF_8));
return bytesToHex(bytes);
}
public static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte individualByte : bytes) {
result.append(Integer.toString((individualByte & 0xff) + 0x100, 16)
.substring(1));
}
return result.toString();
}
public static void addIdToMigratedIds(long id,
User user,
ArbitraryDataProvider arbitraryDataProvider) {
Gson gson = new Gson();
String ids = arbitraryDataProvider.getValue(user, MIGRATED_FOLDER_IDS);
ArrayList arrayList = gson.fromJson(ids, ArrayList.class);
if (arrayList == null) {
arrayList = new ArrayList<>();
}
if (arrayList.contains(id)) {
// nothing to do here
return;
}
arrayList.add(id);
String json = gson.toJson(arrayList);
arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(),
MIGRATED_FOLDER_IDS,
json);
}
public static boolean isFolderMigrated(long id,
User user,
ArbitraryDataProvider arbitraryDataProvider) {
Gson gson = new Gson();
String ids = arbitraryDataProvider.getValue(user, MIGRATED_FOLDER_IDS);
ArrayList arrayList = gson.fromJson(ids, new TypeToken>() {
}.getType());
if (arrayList == null) {
return false;
}
return arrayList.contains(id);
}
public static void reportE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) {
arbitraryDataProvider.incrementValue(user.getAccountName(), ArbitraryDataProvider.E2E_ERRORS);
if (arbitraryDataProvider.getLongValue(user.getAccountName(),
ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP) == -1L) {
arbitraryDataProvider.storeOrUpdateKeyValue(
user.getAccountName(),
ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP,
System.currentTimeMillis() / 1000
);
}
}
@Nullable
public static Problem readE2eError(ArbitraryDataProvider arbitraryDataProvider, User user) {
int value = arbitraryDataProvider.getIntegerValue(user.getAccountName(),
ArbitraryDataProvider.E2E_ERRORS);
long timestamp = arbitraryDataProvider.getLongValue(user.getAccountName(),
ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP);
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(),
ArbitraryDataProvider.E2E_ERRORS);
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(),
ArbitraryDataProvider.E2E_ERRORS_TIMESTAMP);
if (value > 0 && timestamp > 0) {
return new Problem(SendClientDiagnosticRemoteOperation.E2EE_ERRORS, value, timestamp);
} else {
return null;
}
}
public static String generateUid() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String retrievePublicKeyForUser(User user, Context context) {
return new ArbitraryDataProviderImpl(context).getValue(user, PUBLIC_KEY);
}
public static byte[] generateIV() {
return EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
}
public static String byteToHex(byte[] bytes) {
StringBuilder sbKey = new StringBuilder();
for (byte b : bytes) {
sbKey.append(String.format("%02X ", b));
}
return sbKey.toString();
}
public static void savePublicKey(User currentUser,
String key,
String user,
ArbitraryDataProvider arbitraryDataProvider) {
arbitraryDataProvider.storeOrUpdateKeyValue(currentUser,
ArbitraryDataProvider.PUBLIC_KEY + user,
key);
}
public static String getPublicKey(User currentUser,
String user,
ArbitraryDataProvider arbitraryDataProvider) {
return arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PUBLIC_KEY + user);
}
}