Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2023-05-18 02:31:36 +02:00
commit f4fd390f69
16 changed files with 673 additions and 213 deletions

View file

@ -4,7 +4,7 @@
Minimum: NC 16 Server, Android 6.0 Marshmallow
## 3.24.0 (February 13, 2022)
## 3.24.0 (February 13, 2023)
- Several performance optimizations by @starypatyk
- Support multi-page document scanning and exporting to PDF

View file

@ -58,7 +58,7 @@ configurations.configureEach {
// semantic versioning for version code
def versionMajor = 3
def versionMinor = 25
def versionMinor = 26
def versionPatch = 0
def versionBuild = 0 // 0-50=Alpha / 51-98=RC / 90-99=stable

View file

@ -28,6 +28,9 @@ import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.nextcloud.test.RandomStringGenerator;
import com.nextcloud.test.RetryTestRule;
import com.owncloud.android.AbstractIT;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.lib.common.utils.Log_OC;
@ -35,7 +38,6 @@ import com.owncloud.android.utils.CsrHelper;
import com.owncloud.android.utils.EncryptionUtils;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -44,7 +46,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -64,7 +65,6 @@ import javax.crypto.BadPaddingException;
import androidx.test.runner.AndroidJUnit4;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.owncloud.android.utils.EncryptionUtils.EncryptedFile;
import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
import static com.owncloud.android.utils.EncryptionUtils.decryptFile;
@ -76,8 +76,10 @@ import static com.owncloud.android.utils.EncryptionUtils.deserializeJSON;
import static com.owncloud.android.utils.EncryptionUtils.encodeBytesToBase64String;
import static com.owncloud.android.utils.EncryptionUtils.encryptFile;
import static com.owncloud.android.utils.EncryptionUtils.encryptFolderMetadata;
import static com.owncloud.android.utils.EncryptionUtils.generateChecksum;
import static com.owncloud.android.utils.EncryptionUtils.generateKey;
import static com.owncloud.android.utils.EncryptionUtils.generateSHA512;
import static com.owncloud.android.utils.EncryptionUtils.isFolderMigrated;
import static com.owncloud.android.utils.EncryptionUtils.ivDelimiter;
import static com.owncloud.android.utils.EncryptionUtils.ivDelimiterOld;
import static com.owncloud.android.utils.EncryptionUtils.ivLength;
@ -88,10 +90,11 @@ import static com.owncloud.android.utils.EncryptionUtils.verifySHA512;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
@RunWith(AndroidJUnit4.class)
public class EncryptionTestIT {
public class EncryptionTestIT extends AbstractIT {
@Rule public RetryTestRule retryTestRule = new RetryTestRule();
private String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
@ -104,44 +107,44 @@ public class EncryptionTestIT {
"SYk2jjjWVSXRNmex+V6+Y/jBRT2mvAgm8J+7LPwFdatE+lz0aZrMRD2gCWYF6Itpda" +
"90OlLkmQPVWWtGTgX2ta2tF5r2iSGzk0IdoL8zw98Q2UzpOcw30KnWtFMxuxWk0mHq" +
"pgp00g80cDWg3+RPbWOhdLp5bflQ36fKDfmjq05cGlIk6unnVyC5HXpvh4d4k2EWlX" +
"rjGsndVBPCjGkZePlLRgDHxT06r+5XdJ+1CBDZgCsmjGz3M8uOHyCfVW0WhB7ynzDT" +
"agVgz0iqpuhAi9sPt6iWWwpAnRw8cQgqEKw9bvKKECgYEA/WPi2PJtL6u/xlysh/H7" +
"A717CId6fPHCMDace39ZNtzUzc0nT5BemlcF0wZ74NeJSur3Q395YzB+eBMLs5p8mA" +
"95wgGvJhM65/J+HX+k9kt6Z556zLMvtG+j1yo4D0VEwm3xahB4SUUP+1kD7dNvo4+8" +
"xeSCyjzNllvYZZC0DrECgYEA7w8pEqhHHn0a+twkPCZJS+gQTB9Rm+FBNGJqB3XpWs" +
"TeLUxYRbVGk0iDve+eeeZ41drxcdyWP+WcL34hnrjgI1Fo4mK88saajpwUIYMy6+qM" +
"LY+jC2NRSBox56eH7nsVYvQQK9eKqv9wbB+PF9SwOIvuETN7fd8mAY02UnoaaU8CgY" +
"BoHRKocXPLkpZJuuppMVQiRUi4SHJbxDo19Tp2w+y0TihiJ1lvp7I3WGpcOt3LlMQk" +
"tEbExSvrRZGxZKH6Og/XqwQsYuTEkEIz679F/5yYVosE6GkskrOXQAfh8Mb3/04xVV" +
"tMaVgDQw0+CWVD4wyL+BNofGwBDNqsXTCdCsfxAQKBgQCDv2EtbRw0y1HRKv21QIxo" +
"ju5cZW4+cDfVPN+eWPdQFOs1H7wOPsc0aGRiiupV2BSEF3O1ApKziEE5U1QH+29bR4" +
"R8L1pemeGX8qCNj5bCubKjcWOz5PpouDcEqimZ3q98p3E6GEHN15UHoaTkx0yO/V8o" +
"j6zhQ9fYRxDHB5ACtQKBgQCOO7TJUO1IaLTjcrwS4oCfJyRnAdz49L1AbVJkIBK0fh" +
"JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o" +
"uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA==";
"rjGsndVBPCjGkZePlLRgDHxT06r+5XdJ+1CBDZgCsmjGz3M8uOHyCfVW0WhB7ynzDT" +
"agVgz0iqpuhAi9sPt6iWWwpAnRw8cQgqEKw9bvKKECgYEA/WPi2PJtL6u/xlysh/H7" +
"A717CId6fPHCMDace39ZNtzUzc0nT5BemlcF0wZ74NeJSur3Q395YzB+eBMLs5p8mA" +
"95wgGvJhM65/J+HX+k9kt6Z556zLMvtG+j1yo4D0VEwm3xahB4SUUP+1kD7dNvo4+8" +
"xeSCyjzNllvYZZC0DrECgYEA7w8pEqhHHn0a+twkPCZJS+gQTB9Rm+FBNGJqB3XpWs" +
"TeLUxYRbVGk0iDve+eeeZ41drxcdyWP+WcL34hnrjgI1Fo4mK88saajpwUIYMy6+qM" +
"LY+jC2NRSBox56eH7nsVYvQQK9eKqv9wbB+PF9SwOIvuETN7fd8mAY02UnoaaU8CgY" +
"BoHRKocXPLkpZJuuppMVQiRUi4SHJbxDo19Tp2w+y0TihiJ1lvp7I3WGpcOt3LlMQk" +
"tEbExSvrRZGxZKH6Og/XqwQsYuTEkEIz679F/5yYVosE6GkskrOXQAfh8Mb3/04xVV" +
"tMaVgDQw0+CWVD4wyL+BNofGwBDNqsXTCdCsfxAQKBgQCDv2EtbRw0y1HRKv21QIxo" +
"ju5cZW4+cDfVPN+eWPdQFOs1H7wOPsc0aGRiiupV2BSEF3O1ApKziEE5U1QH+29bR4" +
"R8L1pemeGX8qCNj5bCubKjcWOz5PpouDcEqimZ3q98p3E6GEHN15UHoaTkx0yO/V8o" +
"j6zhQ9fYRxDHB5ACtQKBgQCOO7TJUO1IaLTjcrwS4oCfJyRnAdz49L1AbVJkIBK0fh" +
"JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o" +
"uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA==";
private String cert = "-----BEGIN CERTIFICATE-----\n" +
"MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
"HhcNMTcwOTI2MTAwNDMwWhcNMzcwOTIxMTAwNDMwWjBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
"ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDsn0JKS/THu328z1IgN0Vz\n" +
"YU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzVGzKFvGfZ03fwFrN7Q8P8R2e8\n" +
"SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7Y0BJX9i/nW/L0L/VaE8CZT\n" +
"AqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCiCC3qV99b0igRJGmmLQaG\n" +
"iAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umyeyy33OQgdUKaTl5zcS3\n" +
"VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoLH2eiIJCi+61ZkSGf\n" +
"AgMBAAGjUDBOMB0GA1UdDgQWBBTFrXz2tk1HivD9rQ75qeoyHrAgIjAfBgNVHSME\n" +
"GDAWgBTFrXz2tk1HivD9rQ75qeoyHrAgIjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\n" +
"DQEBBQUAA4IBAQARQTX21QKO77gAzBszFJ6xVnjfa23YZF26Z4X1KaM8uV8TGzuN\n" +
"JA95XmReeP2iO3r8EWXS9djVCD64m2xx6FOsrUI8HZaw1JErU8mmOaLAe8q9RsOm\n" +
"9Eq37e4vFp2YUEInYUqs87ByUcA4/8g3lEYeIUnRsRsWsA45S3wD7wy07t+KAn7j\n" +
"yMmfxdma6hFfG9iN/egN6QXUAyIPXvUvlUuZ7/BhWBj/3sHMrF9quy9Q2DOI8F3t\n" +
"1wdQrkq4BtStKhciY5AIXz9SqsctFHTv4Lwgtkapoel4izJnO0ZqYTXVe7THwri9\n" +
"H/gua6uJDWH9jk2/CiZDWfsyFuNUuXvDSp05\n" +
"-----END CERTIFICATE-----";
"MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
"HhcNMTcwOTI2MTAwNDMwWhcNMzcwOTIxMTAwNDMwWjBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
"ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDsn0JKS/THu328z1IgN0Vz\n" +
"YU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzVGzKFvGfZ03fwFrN7Q8P8R2e8\n" +
"SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7Y0BJX9i/nW/L0L/VaE8CZT\n" +
"AqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCiCC3qV99b0igRJGmmLQaG\n" +
"iAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umyeyy33OQgdUKaTl5zcS3\n" +
"VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoLH2eiIJCi+61ZkSGf\n" +
"AgMBAAGjUDBOMB0GA1UdDgQWBBTFrXz2tk1HivD9rQ75qeoyHrAgIjAfBgNVHSME\n" +
"GDAWgBTFrXz2tk1HivD9rQ75qeoyHrAgIjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\n" +
"DQEBBQUAA4IBAQARQTX21QKO77gAzBszFJ6xVnjfa23YZF26Z4X1KaM8uV8TGzuN\n" +
"JA95XmReeP2iO3r8EWXS9djVCD64m2xx6FOsrUI8HZaw1JErU8mmOaLAe8q9RsOm\n" +
"9Eq37e4vFp2YUEInYUqs87ByUcA4/8g3lEYeIUnRsRsWsA45S3wD7wy07t+KAn7j\n" +
"yMmfxdma6hFfG9iN/egN6QXUAyIPXvUvlUuZ7/BhWBj/3sHMrF9quy9Q2DOI8F3t\n" +
"1wdQrkq4BtStKhciY5AIXz9SqsctFHTv4Lwgtkapoel4izJnO0ZqYTXVe7THwri9\n" +
"H/gua6uJDWH9jk2/CiZDWfsyFuNUuXvDSp05\n" +
"-----END CERTIFICATE-----";
@Test
public void encryptStringAsymmetric() throws Exception {
@ -288,16 +291,22 @@ public class EncryptionTestIT {
}
/**
* DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt
* -> decrypt -> JSON -> EncryptedFolderMetadata -> DecryptedFolderMetadata
* DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt -> decrypt -> JSON ->
* EncryptedFolderMetadata -> DecryptedFolderMetadata
*/
@Test
public void encryptionMetadata() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1, privateKey);
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
// serialize
String encryptedJson = serializeJSON(encryptedFolderMetadata1);
@ -305,17 +314,84 @@ public class EncryptionTestIT {
// de-serialize
EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
new TypeToken<EncryptedFolderMetadata>() {
});
});
// decrypt
DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2, privateKey);
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
// compare
assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
serializeJSON(decryptedFolderMetadata2)));
}
@Test
public void testChangedMetadataKey() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
// store metadata key
String oldMetadataKey = encryptedFolderMetadata1.getMetadata().getMetadataKey();
// do it again
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
String newMetadataKey = encryptedFolderMetadata2.getMetadata().getMetadataKey();
assertNotEquals(oldMetadataKey, newMetadataKey);
}
@Test
public void testMigrateMetadataKey() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
// reset new metadata key, to mimic old version
encryptedFolderMetadata1.getMetadata().setMetadataKey(null);
String oldMetadataKey = encryptedFolderMetadata1.getMetadata().getMetadataKey();
// do it again
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata(
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
String newMetadataKey = encryptedFolderMetadata2.getMetadata().getMetadataKey();
assertNotEquals(oldMetadataKey, newMetadataKey);
}
@Test
public void testCryptFileWithoutMetadata() throws Exception {
byte[] key = decodeStringToBase64Bytes("WANM0gRv+DhaexIsI0T3Lg==");
@ -333,30 +409,36 @@ public class EncryptionTestIT {
assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r",
"78f42172166f9dc8fd1a7156b1753353",
decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
.getEncrypted().getKey()),
.getEncrypted().getKey()),
decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
.getInitializationVector()),
.getInitializationVector()),
decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
.getAuthenticationTag())));
.getAuthenticationTag())));
// n9WXAIXO2wRY4R8nXwmo
assertTrue(cryptFile("n9WXAIXO2wRY4R8nXwmo",
"825143ed1f21ebb0c3b3c3f005b2f5db",
decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
.getEncrypted().getKey()),
.getEncrypted().getKey()),
decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
.getInitializationVector()),
.getInitializationVector()),
decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
.getAuthenticationTag())));
.getAuthenticationTag())));
}
@Test
public void bigMetadata() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
long folderID = 1;
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1, privateKey);
decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
// serialize
String encryptedJson = serializeJSON(encryptedFolderMetadata1);
@ -368,7 +450,11 @@ public class EncryptionTestIT {
// decrypt
DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2, privateKey);
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
// compare
assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
@ -386,7 +472,11 @@ public class EncryptionTestIT {
addFile(decryptedFolderMetadata1, i);
// encrypt
encryptedFolderMetadata1 = encryptFolderMetadata(decryptedFolderMetadata1, privateKey);
encryptedFolderMetadata1 = encryptFolderMetadata(decryptedFolderMetadata1,
cert,
arbitraryDataProvider,
user,
folderID);
// serialize
encryptedJson = serializeJSON(encryptedFolderMetadata1);
@ -397,7 +487,11 @@ public class EncryptionTestIT {
});
// decrypt
decryptedFolderMetadata2 = decryptFolderMetaData(encryptedFolderMetadata2, privateKey);
decryptedFolderMetadata2 = decryptFolderMetaData(encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
// compare
assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
@ -411,6 +505,8 @@ public class EncryptionTestIT {
@Test
public void filedrop() throws Exception {
DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
long folderID = 1;
// add filedrop
Map<String, DecryptedFolderMetadata.DecryptedFile> filesdrop = new HashMap<>();
@ -433,8 +529,10 @@ public class EncryptionTestIT {
// encrypt
EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
decryptedFolderMetadata1,
privateKey
);
cert,
arbitraryDataProvider,
user,
folderID);
EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, cert);
// serialize
@ -447,7 +545,11 @@ public class EncryptionTestIT {
// decrypt
DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
encryptedFolderMetadata2, privateKey);
encryptedFolderMetadata2,
privateKey,
arbitraryDataProvider,
user,
folderID);
// compare
assertFalse(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
@ -532,6 +634,58 @@ public class EncryptionTestIT {
assertTrue(verifySHA512(hashedToken, token));
}
@Test
public void testExcludeGSON() throws Exception {
DecryptedFolderMetadata metadata = generateFolderMetadata();
String jsonWithKeys = serializeJSON(metadata);
String jsonWithoutKeys = serializeJSON(metadata, true);
assertTrue(jsonWithKeys.contains("metadataKeys"));
assertFalse(jsonWithoutKeys.contains("metadataKeys"));
}
@Test
public void testChecksum() throws Exception {
DecryptedFolderMetadata metadata = new DecryptedFolderMetadata();
String mnemonic = "chimney potato joke science ridge trophy result estate spare vapor much room";
metadata.getFiles().put("n9WXAIXO2wRY4R8nXwmo", new DecryptedFolderMetadata.DecryptedFile());
metadata.getFiles().put("ia7OEEEyXMoRa1QWQk8r", new DecryptedFolderMetadata.DecryptedFile());
String encryptedMetadataKey = "GuFPAULudgD49S4+VDFck3LiqQ8sx4zmbrBtdpCSGcT+T0W0z4F5gYQYPlzTG6WOkdW5LJZK/";
metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
String checksum = generateChecksum(metadata, mnemonic);
String expectedChecksum = "002cefa6493f2efb0192247a34bb1b16d391aefee968fd3d4225c4ec3cd56436";
assertEquals(expectedChecksum, checksum);
// change something
String newMnemonic = mnemonic + "1";
String newChecksum = generateChecksum(metadata, newMnemonic);
assertNotEquals(expectedChecksum, newChecksum);
metadata.getFiles().put("aeb34yXMoRa1QWQk8r", new DecryptedFolderMetadata.DecryptedFile());
newChecksum = generateChecksum(metadata, mnemonic);
assertNotEquals(expectedChecksum, newChecksum);
}
@Test
public void testAddIdToMigratedIds() {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
// delete ids
arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), EncryptionUtils.MIGRATED_FOLDER_IDS);
long id = 1;
EncryptionUtils.addIdToMigratedIds(id, user, arbitraryDataProvider);
assertTrue(isFolderMigrated(id, user, arbitraryDataProvider));
}
// Helper
private boolean compareJsonStrings(String expected, String actual) {
@ -561,15 +715,7 @@ public class EncryptionTestIT {
DecryptedFolderMetadata.Metadata metadata1 = new DecryptedFolderMetadata.Metadata();
metadata1.setMetadataKeys(metadataKeys);
metadata1.setVersion(1);
DecryptedFolderMetadata.Sharing sharing = new DecryptedFolderMetadata.Sharing();
sharing.setSignature("HMACOFRECIPIENTANDNEWESTMETADATAKEY");
HashMap<String, String> recipient = new HashMap<>();
recipient.put("blah@schiessle.org", "PUBLIC KEY");
recipient.put("bjoern@schiessle.org", "PUBLIC KEY");
sharing.setRecipient(recipient);
metadata1.setSharing(sharing);
metadata1.setVersion(1.1);
HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
@ -603,7 +749,7 @@ public class EncryptionTestIT {
}
private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv, byte[] expectedAuthTag)
throws Exception {
throws Exception {
File file = getFile(fileName);
assertEquals(md5, getMD5Sum(file));
@ -629,14 +775,6 @@ public class EncryptionTestIT {
return md5.compareTo(getMD5Sum(decryptedFile)) == 0;
}
private File getFile(String filename) throws IOException {
InputStream inputStream = getInstrumentation().getContext().getAssets().open(filename);
File temp = File.createTempFile("file", "file");
FileUtils.copyInputStreamToFile(inputStream, temp);
return temp;
}
private String getMD5Sum(File file) {
FileInputStream fileInputStream = null;
try {

View file

@ -94,22 +94,13 @@ public final class PushUtils {
try {
messageDigest = MessageDigest.getInstance("SHA-512");
messageDigest.update(pushToken.getBytes());
return bytesToHex(messageDigest.digest());
return EncryptionUtils.bytesToHex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
Log_OC.d(TAG, "SHA-512 algorithm not supported");
}
return "";
}
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();
}
private static int generateRsa2048KeyPair() {
migratePushKeys();
String keyPath = MainApp.getAppContext().getFilesDir().getAbsolutePath() + File.separator +

View file

@ -71,9 +71,11 @@ public class DecryptedFolderMetadata {
}
public static class Metadata {
private Map<Integer, String> metadataKeys; // each keys is encrypted on its own, decrypt on use
private Sharing sharing;
private int version;
transient
private Map<Integer, String> metadataKeys; // outdated with v1.1
private String metadataKey;
private String checksum;
private double version = 1.2;
@Override
public String toString() {
@ -84,11 +86,7 @@ public class DecryptedFolderMetadata {
return this.metadataKeys;
}
public Sharing getSharing() {
return this.sharing;
}
public int getVersion() {
public double getVersion() {
return this.version;
}
@ -96,12 +94,28 @@ public class DecryptedFolderMetadata {
this.metadataKeys = metadataKeys;
}
public void setSharing(Sharing sharing) {
this.sharing = sharing;
public void setVersion(double version) {
this.version = version;
}
public void setVersion(int version) {
this.version = version;
public String getMetadataKey() {
if (metadataKey == null) {
// fallback to old keys array
return metadataKeys.get(0);
}
return metadataKey;
}
public void setMetadataKey(String metadataKey) {
this.metadataKey = metadataKey;
}
public String getChecksum() {
return checksum;
}
public void setChecksum(String checksum) {
this.checksum = checksum;
}
}
@ -117,32 +131,11 @@ public class DecryptedFolderMetadata {
}
}
public static class Sharing {
private Map<String, String> recipient;
private String signature;
public Map<String, String> getRecipient() {
return this.recipient;
}
public String getSignature() {
return this.signature;
}
public void setRecipient(Map<String, String> recipient) {
this.recipient = recipient;
}
public void setSignature(String signature) {
this.signature = signature;
}
}
public static class DecryptedFile {
private Data encrypted;
private String initializationVector;
private String authenticationTag;
private int metadataKey;
transient private int metadataKey;
public Data getEncrypted() {
return this.encrypted;
@ -181,7 +174,7 @@ public class DecryptedFolderMetadata {
private String key;
private String filename;
private String mimetype;
private int version;
transient private double version;
public String getKey() {
return this.key;
@ -195,7 +188,7 @@ public class DecryptedFolderMetadata {
return this.mimetype;
}
public int getVersion() {
public double getVersion() {
return this.version;
}

View file

@ -0,0 +1,31 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2023 Tobias Kaminsky
* Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.datamodel
data class EncryptedFiledrop(
val encrypted: String,
val initializationVector: String,
val authenticationTag: String,
val encryptedKey: String,
val encryptedTag: String,
val encryptedInitializationVector: String
)

View file

@ -30,11 +30,11 @@ public class EncryptedFolderMetadata {
private DecryptedFolderMetadata.Metadata metadata;
private Map<String, EncryptedFile> files;
private Map<String, EncryptedFile> filedrop;
private Map<String, EncryptedFiledrop> filedrop;
public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata,
Map<String, EncryptedFile> files,
Map<String, EncryptedFile> filesdrop) {
Map<String, EncryptedFiledrop> filesdrop) {
this.metadata = metadata;
this.files = files;
this.filedrop = filesdrop;
@ -48,7 +48,7 @@ public class EncryptedFolderMetadata {
return files;
}
public Map<String, EncryptedFile> getFiledrop() {
public Map<String, EncryptedFiledrop> getFiledrop() {
return filedrop;
}
@ -64,7 +64,7 @@ public class EncryptedFolderMetadata {
private String encrypted;
private String initializationVector;
private String authenticationTag;
private int metadataKey;
transient private int metadataKey;
public String getEncrypted() {
return encrypted;
@ -93,9 +93,5 @@ public class EncryptedFolderMetadata {
public void setAuthenticationTag(String authenticationTag) {
this.authenticationTag = authenticationTag;
}
public void setMetadataKey(int metadataKey) {
this.metadataKey = metadataKey;
}
}
}

View file

@ -600,6 +600,9 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
return !TextUtils.isEmpty(getFileName()) && getFileName().charAt(0) == '.';
}
/**
* unique fileId for the file within the instance
*/
@SuppressFBWarnings("STT")
public long getLocalId() {
if (localId > 0) {
@ -648,6 +651,9 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
}
};
/**
* Android's internal ID of the file
*/
public long getFileId() {
return this.fileId;
}

View file

@ -126,7 +126,9 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(parent,
client,
privateKey,
publicKey);
publicKey,
arbitraryDataProvider,
user);
metadataExists = metadataPair.first;
metadata = metadataPair.second;
@ -150,7 +152,10 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
metadata.getFiles().put(encryptedFileName, createDecryptedFile(filename));
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
privateKey);
publicKey,
arbitraryDataProvider,
user,
parent.getLocalId());
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
// upload metadata

View file

@ -426,8 +426,8 @@ public class RefreshFolderOperation extends RemoteOperation {
mStorageManager.removeFolder(
mLocalFolder,
true,
mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)
);
mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath)
);
}
}

View file

@ -47,6 +47,7 @@ import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
@ -99,6 +100,7 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
DecryptedFolderMetadata metadata;
String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
try {
// Lock folder
@ -120,10 +122,14 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
});
serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
});
metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey);
metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata,
privateKey,
arbitraryDataProvider,
user,
parentId);
} else {
throw new RemoteOperationFailedException("No Metadata found!");
}
@ -140,8 +146,12 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
// remove file from metadata
metadata.getFiles().remove(fileName);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
privateKey);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(
metadata,
publicKey,
arbitraryDataProvider,
user,
parentId);
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
// upload metadata
@ -155,14 +165,9 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
// return success
return result;
} catch (NoSuchAlgorithmException |
IOException |
InvalidKeyException |
InvalidAlgorithmParameterException |
NoSuchPaddingException |
BadPaddingException |
IllegalBlockSizeException |
InvalidKeySpecException e) {
} catch (NoSuchAlgorithmException | IOException | InvalidKeyException | InvalidAlgorithmParameterException |
NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException |
CertificateException e) {
result = new RemoteOperationResult(e);
Log_OC.e(TAG, "Remove " + remotePath + ": " + result.getLogMessage(), e);

View file

@ -471,7 +471,9 @@ public class UploadFileOperation extends SyncOperation {
Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(parentFile,
client,
privateKey,
publicKey);
publicKey,
arbitraryDataProvider,
user);
metadataExists = metadataPair.first;
DecryptedFolderMetadata metadata = metadataPair.second;
@ -617,8 +619,19 @@ public class UploadFileOperation extends SyncOperation {
metadata.getFiles().put(encryptedFileName, decryptedFile);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
privateKey);
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
publicKey,
arbitraryDataProvider,
user,
parentFile.getLocalId());
String serializedFolderMetadata;
// check if we need metadataKeys
if (metadata.getMetadata().getMetadataKey() != null) {
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true);
} else {
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
}
// upload metadata
EncryptionUtils.uploadMetadata(parentFile,

View file

@ -33,6 +33,7 @@ import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
@ -51,7 +52,6 @@ import com.nextcloud.android.lib.resources.files.ToggleFileLockRemoteOperation;
import com.nextcloud.android.lib.richWorkspace.RichWorkspaceDirectEditingRemoteOperation;
import com.nextcloud.client.account.User;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.core.Clock;
import com.nextcloud.client.device.DeviceInfo;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.documentscan.AppScanOptionalFeature;
@ -68,6 +68,8 @@ import com.nextcloud.utils.view.FastScrollUtils;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.ArbitraryDataProvider;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.SyncedFolderProvider;
@ -1700,11 +1702,12 @@ public class OCFileListFragment extends ExtendedListFragment implements
String publicKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PUBLIC_KEY);
String privateKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PRIVATE_KEY);
FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
OCFile file = storageManager.getFileByRemoteId(event.remoteId);
if (publicKey.isEmpty() || privateKey.isEmpty()) {
Log_OC.d(TAG, "no public key for " + user.getAccountName());
FileDataStorageManager storageManager = mContainerActivity.getStorageManager();
OCFile file = storageManager.getFileByRemoteId(event.remoteId);
int position = -1;
if (file != null) {
position = mAdapter.getItemPosition(file);
@ -1713,11 +1716,23 @@ public class OCFileListFragment extends ExtendedListFragment implements
dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
dialog.show(getParentFragmentManager(), SETUP_ENCRYPTION_DIALOG_TAG);
} else {
encryptFolder(event.localId, event.remoteId, event.remotePath, event.shouldBeEncrypted);
encryptFolder(file,
event.localId,
event.remoteId,
event.remotePath,
event.shouldBeEncrypted,
publicKey,
privateKey);
}
}
private void encryptFolder(long localId, String remoteId, String remotePath, boolean shouldBeEncrypted) {
private void encryptFolder(OCFile folder,
long localId,
String remoteId,
String remotePath,
boolean shouldBeEncrypted,
String publicKey,
String privateKey) {
try {
User user = accountManager.getUser();
OwnCloudClient client = clientFactory.create(user);
@ -1727,6 +1742,45 @@ public class OCFileListFragment extends ExtendedListFragment implements
.execute(client);
if (remoteOperationResult.isSuccess()) {
// lock folder
String token = EncryptionUtils.lockFolder(folder, client);
// Update metadata
Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(folder,
client,
privateKey,
publicKey,
arbitraryDataProvider,
user);
boolean metadataExists = metadataPair.first;
DecryptedFolderMetadata metadata = metadataPair.second;
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
publicKey,
arbitraryDataProvider,
user,
folder.getLocalId());
String serializedFolderMetadata;
// check if we need metadataKeys
if (metadata.getMetadata().getMetadataKey() != null) {
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true);
} else {
serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
}
// upload metadata
EncryptionUtils.uploadMetadata(folder,
serializedFolderMetadata,
token,
client,
metadataExists);
// unlock folder
EncryptionUtils.unlockFolder(folder, client, token);
mAdapter.setEncryptionAttributeForItemID(remoteId, shouldBeEncrypted);
} else if (remoteOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
Snackbar.make(getRecyclerView(),
@ -1738,8 +1792,8 @@ public class OCFileListFragment extends ExtendedListFragment implements
Snackbar.LENGTH_LONG).show();
}
} catch (ClientFactory.CreationException e) {
Log_OC.e(TAG, "Cannot create client", e);
} catch (Exception e) {
Log_OC.e(TAG, "Error creating encrypted folder", e);
}
}

View file

@ -28,12 +28,14 @@ import android.util.Pair;
import com.google.common.collect.Lists;
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.DecryptedFolderMetadata;
import com.owncloud.android.datamodel.EncryptedFiledrop;
import com.owncloud.android.datamodel.EncryptedFolderMetadata;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.OwnCloudClient;
@ -79,6 +81,7 @@ 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;
@ -103,7 +106,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
* Utils for encryption
*/
public final class EncryptionUtils {
private static String TAG = EncryptionUtils.class.getSimpleName();
private static final String TAG = EncryptionUtils.class.getSimpleName();
public static final String PUBLIC_KEY = "PUBLIC_KEY";
public static final String PRIVATE_KEY = "PRIVATE_KEY";
@ -120,6 +123,8 @@ public final class EncryptionUtils {
private static final String AES = "AES";
private static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
private static final String RSA = "RSA";
@VisibleForTesting
public static final String MIGRATED_FOLDER_IDS = "MIGRATED_FOLDER_IDS";
private EncryptionUtils() {
// utility class -> private constructor
@ -129,12 +134,28 @@ public final class EncryptionUtils {
JSON
*/
public static <T> T deserializeJSON(String json, TypeToken<T> 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> T deserializeJSON(String json, TypeToken<T> type) {
return new Gson().fromJson(json, type.getType());
return deserializeJSON(json, type, false);
}
public static String serializeJSON(Object data, boolean excludeTransient) {
if (excludeTransient) {
return new Gson().toJson(data);
} else {
return new GsonBuilder().excludeFieldsWithModifiers(0).create().toJson(data);
}
}
public static String serializeJSON(Object data) {
return new Gson().toJson(data);
return serializeJSON(data, false);
}
/*
@ -148,19 +169,32 @@ public final class EncryptionUtils {
* @return EncryptedFolderMetadata encrypted folder metadata
*/
public static EncryptedFolderMetadata encryptFolderMetadata(DecryptedFolderMetadata decryptedFolderMetadata,
String privateKey
String publicKey,
ArbitraryDataProvider arbitraryDataProvider,
User user,
long parentId
)
throws NoSuchAlgorithmException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, InvalidKeySpecException {
IllegalBlockSizeException, InvalidKeySpecException, CertificateException {
HashMap<String, EncryptedFolderMetadata.EncryptedFile> files = new HashMap<>();
HashMap<String, EncryptedFolderMetadata.EncryptedFile> filesdrop = new HashMap<>();
HashMap<String, EncryptedFiledrop> filesdrop = new HashMap<>();
EncryptedFolderMetadata encryptedFolderMetadata = new EncryptedFolderMetadata(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<String, DecryptedFolderMetadata.DecryptedFile> entry : decryptedFolderMetadata
.getFiles().entrySet()) {
@ -169,39 +203,64 @@ public final class EncryptionUtils {
EncryptedFolderMetadata.EncryptedFile encryptedFile = new EncryptedFolderMetadata.EncryptedFile();
encryptedFile.setInitializationVector(decryptedFile.getInitializationVector());
encryptedFile.setMetadataKey(decryptedFile.getMetadataKey());
encryptedFile.setAuthenticationTag(decryptedFile.getAuthenticationTag());
byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(EncryptionUtils.decryptStringAsymmetric(
decryptedFolderMetadata.getMetadata().getMetadataKeys().get(encryptedFile.getMetadataKey()),
privateKey));
// encrypt
String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted());
encryptedFile.setEncrypted(EncryptionUtils.encryptStringSymmetric(dataJson, decryptedMetadataKey));
encryptedFile.setEncrypted(EncryptionUtils.encryptStringSymmetric(dataJson, metadataKeyBytes));
files.put(key, encryptedFile);
}
// set checksum
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC);
String checksum = EncryptionUtils.generateChecksum(decryptedFolderMetadata, mnemonic);
encryptedFolderMetadata.getMetadata().setChecksum(checksum);
return encryptedFolderMetadata;
}
/**
* normally done on server only internal test
*/
@VisibleForTesting
public static void encryptFileDropFiles(DecryptedFolderMetadata decryptedFolderMetadata, EncryptedFolderMetadata encryptedFolderMetadata, String cert) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, CertificateException {
final Map<String, EncryptedFolderMetadata.EncryptedFile> filesdrop = encryptedFolderMetadata.getFiledrop();
public static void encryptFileDropFiles(DecryptedFolderMetadata decryptedFolderMetadata,
EncryptedFolderMetadata encryptedFolderMetadata,
String cert) throws NoSuchPaddingException, IllegalBlockSizeException, CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
final Map<String, EncryptedFiledrop> filesdrop = encryptedFolderMetadata.getFiledrop();
for (Map.Entry<String, DecryptedFolderMetadata.DecryptedFile> entry : decryptedFolderMetadata
.getFiledrop().entrySet()) {
String key = entry.getKey();
DecryptedFolderMetadata.DecryptedFile decryptedFile = entry.getValue();
EncryptedFolderMetadata.EncryptedFile encryptedFile = new EncryptedFolderMetadata.EncryptedFile();
encryptedFile.setInitializationVector(decryptedFile.getInitializationVector());
encryptedFile.setMetadataKey(decryptedFile.getMetadataKey());
encryptedFile.setAuthenticationTag(decryptedFile.getAuthenticationTag());
byte[] byt = generateKey();
String metadataKey0 = encodeBytesToBase64String(byt);
String enc = encryptStringAsymmetric(metadataKey0, cert);
// encrypt
String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted());
encryptedFile.setEncrypted(EncryptionUtils.encryptStringAsymmetric(dataJson, cert));
String encJson = encryptStringSymmetric(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);
}
@ -211,7 +270,10 @@ public final class EncryptionUtils {
* decrypt folder metaData with private key
*/
public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetadata encryptedFolderMetadata,
String privateKey)
String privateKey,
ArbitraryDataProvider arbitraryDataProvider,
User user,
long remoteId)
throws NoSuchAlgorithmException, InvalidKeyException,
InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, InvalidKeySpecException {
@ -220,34 +282,18 @@ public final class EncryptionUtils {
DecryptedFolderMetadata decryptedFolderMetadata = new DecryptedFolderMetadata(
encryptedFolderMetadata.getMetadata(), files);
for (Map.Entry<String, EncryptedFolderMetadata.EncryptedFile> entry : encryptedFolderMetadata
.getFiles().entrySet()) {
String key = entry.getKey();
EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue();
byte[] decryptedMetadataKey = null;
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
decryptedFile.setInitializationVector(encryptedFile.getInitializationVector());
decryptedFile.setMetadataKey(encryptedFile.getMetadataKey());
decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
String encryptedMetadataKey = decryptedFolderMetadata.getMetadata().getMetadataKey();
byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(
EncryptionUtils.decryptStringAsymmetric(decryptedFolderMetadata.getMetadata()
.getMetadataKeys().get(encryptedFile.getMetadataKey()),
privateKey));
// decrypt
String dataJson = EncryptionUtils.decryptStringSymmetric(encryptedFile.getEncrypted(), decryptedMetadataKey);
decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(dataJson,
new TypeToken<DecryptedFolderMetadata.Data>() {
}));
files.put(key, decryptedFile);
if (encryptedMetadataKey != null) {
decryptedMetadataKey = decodeStringToBase64Bytes(
decryptStringAsymmetric(encryptedMetadataKey, privateKey));
}
Map<String, EncryptedFolderMetadata.EncryptedFile> fileDrop = encryptedFolderMetadata.getFiledrop();
if (fileDrop != null) {
for (Map.Entry<String, EncryptedFolderMetadata.EncryptedFile> entry : fileDrop.entrySet()) {
if (encryptedFolderMetadata.getFiles() != null) {
for (Map.Entry<String, EncryptedFolderMetadata.EncryptedFile> entry : encryptedFolderMetadata
.getFiles().entrySet()) {
String key = entry.getKey();
EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue();
@ -256,14 +302,66 @@ public final class EncryptionUtils {
decryptedFile.setMetadataKey(encryptedFile.getMetadataKey());
decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
// decrypt
String dataJson = EncryptionUtils.decryptStringAsymmetric(encryptedFile.getEncrypted(), privateKey);
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<DecryptedFolderMetadata.Data>() {
}));
files.put(key, decryptedFile);
}
}
// verify checksum
String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC);
String checksum = EncryptionUtils.generateChecksum(decryptedFolderMetadata, mnemonic);
String decryptedFolderChecksum = decryptedFolderMetadata.getMetadata().getChecksum();
if (TextUtils.isEmpty(decryptedFolderChecksum) &&
isFolderMigrated(remoteId, user, arbitraryDataProvider)) {
throw new IllegalStateException("Possible downgrade attack detected!");
}
if (!TextUtils.isEmpty(decryptedFolderChecksum) && !decryptedFolderChecksum.equals(checksum)) {
throw new IllegalStateException("Wrong checksum!");
}
Map<String, EncryptedFiledrop> fileDrop = encryptedFolderMetadata.getFiledrop();
if (fileDrop != null) {
for (Map.Entry<String, EncryptedFiledrop> 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 = decryptStringSymmetric(
encryptedFile.getEncrypted(),
decodeStringToBase64Bytes(encryptedKey),
decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()),
decodeStringToBase64Bytes(encryptedFile.getEncryptedTag())
);
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
decryptedFile.setInitializationVector(encryptedFile.getInitializationVector());
decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(decryptedData,
new TypeToken<DecryptedFolderMetadata.Data>() {
}));
files.put(key, decryptedFile);
// remove from filedrop
fileDrop.remove(key);
@ -294,6 +392,7 @@ public final class EncryptionUtils {
ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
@ -306,7 +405,10 @@ public final class EncryptionUtils {
}
DecryptedFolderMetadata decryptedFolderMetadata = EncryptionUtils.decryptFolderMetaData(
encryptedFolderMetadata,
privateKey);
privateKey,
arbitraryDataProvider,
user,
folder.getLocalId());
boolean transferredFiledrop = filesDropCountBefore > 0 && decryptedFolderMetadata.getFiles().size() ==
encryptedFolderMetadata.getFiles().size() + filesDropCountBefore;
@ -317,7 +419,10 @@ public final class EncryptionUtils {
// upload metadata
EncryptedFolderMetadata encryptedFolderMetadataNew = encryptFolderMetadata(decryptedFolderMetadata,
privateKey);
publicKey,
arbitraryDataProvider,
user,
folder.getLocalId());
String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataNew);
@ -605,6 +710,36 @@ public final class EncryptionUtils {
return encodedCryptedBytes + delimiter + encodedIV;
}
public static String decryptStringSymmetric(String string,
byte[] encryptionKeyBytes,
byte[] iv,
byte[] authenticationTag)
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);
byte[] bytes = decodeStringToBase64Bytes(string);
// check authentication tag
byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes,
bytes.length - (128 / 8),
bytes.length);
if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
throw new SecurityException("Tag not correct");
}
byte[] encodedBytes = cipher.doFinal(bytes);
return decodeBase64BytesToString(encodedBytes);
}
/**
* Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding Asymmetric encryption, with private
@ -883,10 +1018,15 @@ public final class EncryptionUtils {
public static Pair<Boolean, DecryptedFolderMetadata> retrieveMetadata(OCFile parentFile,
OwnCloudClient client,
String privateKey,
String publicKey) throws UploadException,
String publicKey,
ArbitraryDataProvider arbitraryDataProvider,
User user)
throws UploadException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException,
IllegalBlockSizeException, InvalidKeyException, InvalidKeySpecException, CertificateException {
GetMetadataRemoteOperation getMetadataOperation = new GetMetadataRemoteOperation(parentFile.getLocalId());
long localId = parentFile.getLocalId();
GetMetadataRemoteOperation getMetadataOperation = new GetMetadataRemoteOperation(localId);
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
DecryptedFolderMetadata metadata;
@ -900,7 +1040,11 @@ public final class EncryptionUtils {
serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
});
return new Pair<>(Boolean.TRUE, EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey));
return new Pair<>(Boolean.TRUE, decryptFolderMetaData(encryptedFolderMetadata,
privateKey,
arbitraryDataProvider,
user,
localId));
} else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) {
// new metadata
@ -909,7 +1053,7 @@ public final class EncryptionUtils {
metadata.getMetadata().setMetadataKeys(new HashMap<>());
String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
metadata.getMetadata().getMetadataKeys().put(0, encryptedMetadataKey);
metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
return new Pair<>(Boolean.FALSE, metadata);
} else {
@ -984,4 +1128,83 @@ public final class EncryptionUtils {
file.isFolder() &&
user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.nextcloud_26);
}
public static String generateChecksum(DecryptedFolderMetadata metadata,
String mnemonic) throws NoSuchAlgorithmException {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(mnemonic.replaceAll(" ", ""));
ArrayList<String> keys = new ArrayList<>(metadata.getFiles().keySet());
Collections.sort(keys);
for (String key : keys) {
stringBuilder.append(key);
}
stringBuilder.append(metadata.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<Long> 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<Long> arrayList = gson.fromJson(ids, new TypeToken<List<Long>>() {
}.getType());
if (arrayList == null) {
return false;
}
return arrayList.contains(id);
}
}

View file

@ -158,6 +158,7 @@
<string name="conflict_local_file">fichier local</string>
<string name="conflict_message_description">Si vous sélectionnez les deux versions, le fichier local aura un numéro ajouté à son nom.</string>
<string name="conflict_server_file">Fichier serveur</string>
<string name="contact_backup_title">Sauvegarde des contacts</string>
<string name="contactlist_item_icon">L\'icône de l\'utilisateur pour la liste des contacts</string>
<string name="contactlist_no_permission">Pas de permission donnée, rien n\'a été importé.</string>
<string name="contacts">Contacts</string>
@ -236,6 +237,7 @@ Attention la suppression est irréversible.</string>
<string name="drawer_item_all_files">Tous les fichiers</string>
<string name="drawer_item_favorites">Favoris</string>
<string name="drawer_item_gallery">Médias</string>
<string name="drawer_item_groupfolders">Dossiers de groupes</string>
<string name="drawer_item_home">Accueil</string>
<string name="drawer_item_notifications">Notifications</string>
<string name="drawer_item_on_device">Sur l\'appareil</string>
@ -559,6 +561,7 @@ Attention la suppression est irréversible.</string>
<string name="prefs_category_general">Général</string>
<string name="prefs_category_more">Plus</string>
<string name="prefs_daily_backup_summary">Sauvegarde quotidienne de votre agenda &amp; des contacts</string>
<string name="prefs_daily_contact_backup_summary">Sauvegarde quotidienne de vos contacts</string>
<string name="prefs_davx5_setup_error">Erreur inattendue lors de la configuration de DAVx5 (anciennement connu sous le nom de DAVdroid)</string>
<string name="prefs_e2e_active">Le chiffrement de bout en bout est configuré !</string>
<string name="prefs_e2e_mnemonic">Phrase secrète E2E</string>
@ -798,6 +801,7 @@ Attention la suppression est irréversible.</string>
<string name="thumbnail">Miniature</string>
<string name="thumbnail_for_existing_file_description">Vignette pour fichier existant</string>
<string name="thumbnail_for_new_file_desc">Vignette pour nouveau fichier</string>
<string name="timeout_richDocuments">Le chargement prend plus de temps que prévu</string>
<string name="today">Aujourd\'hui</string>
<string name="trashbin_activity_title">Fichiers supprimés</string>
<string name="trashbin_empty_headline">Aucun fichier supprimé</string>

View file

@ -800,6 +800,7 @@
<string name="thumbnail">Ikon</string>
<string name="thumbnail_for_existing_file_description">Miniatyrbild för befintlig fil</string>
<string name="thumbnail_for_new_file_desc">Miniatyrbild för ny fil</string>
<string name="timeout_richDocuments">Inläsningen tar längre tid än förväntat</string>
<string name="today">Idag</string>
<string name="trashbin_activity_title">Borttagna filer</string>
<string name="trashbin_empty_headline">Inga borttagna filer</string>