From 5492a99e38f52b039b9f252547c5790177548054 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Tue, 16 May 2023 09:09:47 +0200 Subject: [PATCH] E2E v1.2 Signed-off-by: tobiasKaminsky --- CHANGELOG.md | 2 +- .../android/util/EncryptionTestIT.java | 300 ++++++++++++----- .../com/owncloud/android/utils/PushUtils.java | 11 +- .../datamodel/DecryptedFolderMetadata.java | 65 ++-- .../android/datamodel/EncryptedFiledrop.kt | 31 ++ .../datamodel/EncryptedFolderMetadata.java | 12 +- .../owncloud/android/datamodel/OCFile.java | 6 + .../operations/CreateFolderOperation.java | 10 +- .../operations/RefreshFolderOperation.java | 4 +- .../RemoveRemoteEncryptedFileOperation.java | 32 +- .../operations/UploadFileOperation.java | 20 +- .../ui/fragment/OCFileListFragment.java | 69 +++- .../android/utils/EncryptionUtils.java | 304 +++++++++++++++--- 13 files changed, 657 insertions(+), 209 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 852e35262a..ebc537ffa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java index 41046243a9..219edfcbb9 100644 --- a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java +++ b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java @@ -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,12 +90,13 @@ 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" + "IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV" + "GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7" + @@ -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,23 @@ 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, + privateKey, + cert, + arbitraryDataProvider, + user, + folderID); // serialize String encryptedJson = serializeJSON(encryptedFolderMetadata1); @@ -305,17 +315,88 @@ public class EncryptionTestIT { // de-serialize EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson, new TypeToken() { - }); + }); // 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, + privateKey, + cert, + arbitraryDataProvider, + user, + folderID); + + // store metadata key + String oldMetadataKey = encryptedFolderMetadata1.getMetadata().getMetadataKey(); + + // do it again + // encrypt + EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata( + decryptedFolderMetadata1, + privateKey, + 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, + privateKey, + 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, + privateKey, + 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 +414,37 @@ 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, + privateKey, + cert, + arbitraryDataProvider, + user, + folderID); // serialize String encryptedJson = serializeJSON(encryptedFolderMetadata1); @@ -368,7 +456,11 @@ public class EncryptionTestIT { // decrypt DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData( - encryptedFolderMetadata2, privateKey); + encryptedFolderMetadata2, + privateKey, + arbitraryDataProvider, + user, + folderID); // compare assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1), @@ -386,7 +478,12 @@ public class EncryptionTestIT { addFile(decryptedFolderMetadata1, i); // encrypt - encryptedFolderMetadata1 = encryptFolderMetadata(decryptedFolderMetadata1, privateKey); + encryptedFolderMetadata1 = encryptFolderMetadata(decryptedFolderMetadata1, + privateKey, + cert, + arbitraryDataProvider, + user, + folderID); // serialize encryptedJson = serializeJSON(encryptedFolderMetadata1); @@ -397,7 +494,11 @@ public class EncryptionTestIT { }); // decrypt - decryptedFolderMetadata2 = decryptFolderMetaData(encryptedFolderMetadata2, privateKey); + decryptedFolderMetadata2 = decryptFolderMetaData(encryptedFolderMetadata2, + privateKey, + arbitraryDataProvider, + user, + folderID); // compare assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1), @@ -411,6 +512,8 @@ public class EncryptionTestIT { @Test public void filedrop() throws Exception { DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata(); + ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext); + long folderID = 1; // add filedrop Map filesdrop = new HashMap<>(); @@ -433,8 +536,11 @@ public class EncryptionTestIT { // encrypt EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata( decryptedFolderMetadata1, - privateKey - ); + privateKey, + cert, + arbitraryDataProvider, + user, + folderID); EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, cert); // serialize @@ -447,7 +553,11 @@ public class EncryptionTestIT { // decrypt DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData( - encryptedFolderMetadata2, privateKey); + encryptedFolderMetadata2, + privateKey, + arbitraryDataProvider, + user, + folderID); // compare assertFalse(compareJsonStrings(serializeJSON(decryptedFolderMetadata1), @@ -532,6 +642,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 +723,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 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 files = new HashMap<>(); @@ -603,7 +757,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 +783,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 { diff --git a/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java b/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java index 815a6acf02..57ae513486 100644 --- a/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java +++ b/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java @@ -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 + diff --git a/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java b/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java index 49792c450c..4f4aafe19e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java +++ b/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java @@ -71,9 +71,11 @@ public class DecryptedFolderMetadata { } public static class Metadata { - private Map metadataKeys; // each keys is encrypted on its own, decrypt on use - private Sharing sharing; - private int version; + transient + private Map 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 recipient; - private String signature; - - public Map getRecipient() { - return this.recipient; - } - - public String getSignature() { - return this.signature; - } - - public void setRecipient(Map 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; } diff --git a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt b/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt new file mode 100644 index 0000000000..fb21d421d8 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt @@ -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 . + */ +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 +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java b/app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java index 228d9b186e..51c23e1d7f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java +++ b/app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java @@ -30,11 +30,11 @@ public class EncryptedFolderMetadata { private DecryptedFolderMetadata.Metadata metadata; private Map files; - private Map filedrop; + private Map filedrop; public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata, Map files, - Map filesdrop) { + Map filesdrop) { this.metadata = metadata; this.files = files; this.filedrop = filesdrop; @@ -48,7 +48,7 @@ public class EncryptedFolderMetadata { return files; } - public Map getFiledrop() { + public Map 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; - } } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index 0da9cb9048..cfb521a32f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -600,6 +600,9 @@ public class OCFile implements Parcelable, Comparable, 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, ServerFileInterfa } }; + /** + * Android's internal ID of the file + */ public long getFileId() { return this.fileId; } diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index 84cdd64714..1abc8b99f8 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -126,7 +126,9 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper Pair metadataPair = EncryptionUtils.retrieveMetadata(parent, client, privateKey, - publicKey); + publicKey, + arbitraryDataProvider, + user); metadataExists = metadataPair.first; metadata = metadataPair.second; @@ -150,7 +152,11 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper metadata.getFiles().put(encryptedFileName, createDecryptedFile(filename)); EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata, - privateKey); + privateKey, + publicKey, + arbitraryDataProvider, + user, + parent.getLocalId()); String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); // upload metadata diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index b6df79022b..b0a9749c1f 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -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) + ); } } diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java index 3656b8696f..de861082c0 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java @@ -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() { - }); + serializedEncryptedMetadata, new TypeToken() { + }); - metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey); + metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, + privateKey, + arbitraryDataProvider, + user, + parentId); } else { throw new RemoteOperationFailedException("No Metadata found!"); } @@ -140,8 +146,13 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation { // remove file from metadata metadata.getFiles().remove(fileName); - EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata, - privateKey); + EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata( + metadata, + privateKey, + publicKey, + arbitraryDataProvider, + user, + parentId); String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); // upload metadata @@ -155,14 +166,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); diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 98a8e310f1..9152797666 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -471,7 +471,9 @@ public class UploadFileOperation extends SyncOperation { Pair metadataPair = EncryptionUtils.retrieveMetadata(parentFile, client, privateKey, - publicKey); + publicKey, + arbitraryDataProvider, + user); metadataExists = metadataPair.first; DecryptedFolderMetadata metadata = metadataPair.second; @@ -617,8 +619,20 @@ public class UploadFileOperation extends SyncOperation { metadata.getFiles().put(encryptedFileName, decryptedFile); EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata, - privateKey); - String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); + privateKey, + 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, diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index bd7bab22e1..a3fcfca73d 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -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,46 @@ public class OCFileListFragment extends ExtendedListFragment implements .execute(client); if (remoteOperationResult.isSuccess()) { + // lock folder + String token = EncryptionUtils.lockFolder(folder, client); + + // Update metadata + Pair metadataPair = EncryptionUtils.retrieveMetadata(folder, + client, + privateKey, + publicKey, + arbitraryDataProvider, + user); + + boolean metadataExists = metadataPair.first; + DecryptedFolderMetadata metadata = metadataPair.second; + + EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata, + privateKey, + 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 +1793,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); } } diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java index d215d3e542..ad2059a593 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java @@ -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; @@ -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 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 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,33 @@ public final class EncryptionUtils { * @return EncryptedFolderMetadata encrypted folder metadata */ public static EncryptedFolderMetadata encryptFolderMetadata(DecryptedFolderMetadata decryptedFolderMetadata, - String privateKey + String privateKey, + String publicKey, + ArbitraryDataProvider arbitraryDataProvider, + User user, + long parentId ) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException, - IllegalBlockSizeException, InvalidKeySpecException { + IllegalBlockSizeException, InvalidKeySpecException, CertificateException { HashMap files = new HashMap<>(); - HashMap filesdrop = new HashMap<>(); + HashMap 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 entry : decryptedFolderMetadata .getFiles().entrySet()) { @@ -169,39 +204,45 @@ 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; } @VisibleForTesting public static void encryptFileDropFiles(DecryptedFolderMetadata decryptedFolderMetadata, EncryptedFolderMetadata encryptedFolderMetadata, String cert) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, CertificateException { - final Map filesdrop = encryptedFolderMetadata.getFiledrop(); + final Map filesdrop = encryptedFolderMetadata.getFiledrop(); for (Map.Entry 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()); + // TODO + String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted()); + EncryptedFiledrop encryptedFile = new EncryptedFiledrop(dataJson, + decryptedFile.getInitializationVector(), + decryptedFile.getAuthenticationTag(), + "123", + "123", + "123"); +// encryptedFile.setInitializationVector(decryptedFile.getInitializationVector()); +// encryptedFile.setAuthenticationTag(decryptedFile.getAuthenticationTag()); // encrypt - String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted()); - encryptedFile.setEncrypted(EncryptionUtils.encryptStringAsymmetric(dataJson, cert)); + +// encryptedFile.setEncrypted(EncryptionUtils.encryptStringAsymmetric(dataJson, cert)); filesdrop.put(key, encryptedFile); } @@ -211,7 +252,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 +264,18 @@ public final class EncryptionUtils { DecryptedFolderMetadata decryptedFolderMetadata = new DecryptedFolderMetadata( encryptedFolderMetadata.getMetadata(), files); - for (Map.Entry 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() { - })); - - files.put(key, decryptedFile); + if (encryptedMetadataKey != null) { + decryptedMetadataKey = decodeStringToBase64Bytes( + decryptStringAsymmetric(encryptedMetadataKey, privateKey)); } - Map fileDrop = encryptedFolderMetadata.getFiledrop(); - - if (fileDrop != null) { - for (Map.Entry entry : fileDrop.entrySet()) { + if (encryptedFolderMetadata.getFiles() != null) { + for (Map.Entry entry : encryptedFolderMetadata + .getFiles().entrySet()) { String key = entry.getKey(); EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue(); @@ -256,14 +284,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() { })); 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 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 = 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() { + })); + + files.put(key, decryptedFile); // remove from filedrop fileDrop.remove(key); @@ -294,6 +374,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() { @@ -306,7 +387,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 +401,11 @@ public final class EncryptionUtils { // upload metadata EncryptedFolderMetadata encryptedFolderMetadataNew = encryptFolderMetadata(decryptedFolderMetadata, - privateKey); + privateKey, + publicKey, + arbitraryDataProvider, + user, + folder.getLocalId()); String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataNew); @@ -605,6 +693,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,7 +1001,10 @@ public final class EncryptionUtils { public static Pair 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()); @@ -900,7 +1021,11 @@ public final class EncryptionUtils { serializedEncryptedMetadata, new TypeToken() { }); - return new Pair<>(Boolean.TRUE, EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey)); + return new Pair<>(Boolean.TRUE, EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, + privateKey, + arbitraryDataProvider, + user, + parentFile.getLocalId())); } else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) { // new metadata @@ -909,7 +1034,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 +1109,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 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 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); + } }