Bläddra i källkod

extract tests

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 2 år sedan
förälder
incheckning
218fbd7815
71 ändrade filer med 5962 tillägg och 1596 borttagningar
  1. 3 0
      .idea/inspectionProfiles/ktlint.xml
  2. 1 1
      app/build.gradle
  3. 1 0
      app/lint.xml
  4. 1191 0
      app/schemas/com.nextcloud.client.database.NextcloudDatabase/77.json
  5. 32 0
      app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java
  6. 0 730
      app/src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java
  7. 27 14
      app/src/androidTest/java/com/owncloud/android/AbstractIT.java
  8. 8 10
      app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
  9. 2 3
      app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt
  10. 1 0
      app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt
  11. 204 105
      app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java
  12. 157 0
      app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt
  13. 47 0
      app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt
  14. 665 0
      app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt
  15. 2 1
      app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt
  16. 2 0
      app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt
  17. 3 1
      app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt
  18. 8 0
      app/src/main/java/com/nextcloud/client/di/AppModule.java
  19. 2 0
      app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt
  20. 7 0
      app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java
  21. 3 3
      app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java
  22. 15 0
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  23. 12 0
      app/src/main/java/com/owncloud/android/datamodel/OCFile.java
  24. 62 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java
  25. 62 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java
  26. 73 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java
  27. 74 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java
  28. 37 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java
  29. 46 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java
  30. 24 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt
  31. 16 12
      app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java
  32. 30 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt
  33. 33 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt
  34. 59 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt
  35. 28 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt
  36. 29 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt
  37. 28 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt
  38. 32 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt
  39. 29 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt
  40. 29 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt
  41. 28 0
      app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt
  42. 4 1
      app/src/main/java/com/owncloud/android/db/ProviderMeta.java
  43. 25 3
      app/src/main/java/com/owncloud/android/files/FileMenuFilter.java
  44. 228 35
      app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java
  45. 148 54
      app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java
  46. 45 12
      app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java
  47. 118 25
      app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  48. 3 2
      app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java
  49. 46 88
      app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java
  50. 45 14
      app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java
  51. 71 2
      app/src/main/java/com/owncloud/android/operations/UnshareOperation.java
  52. 190 100
      app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  53. 34 0
      app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt
  54. 16 5
      app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java
  55. 11 2
      app/src/main/java/com/owncloud/android/services/OperationsService.java
  56. 11 1
      app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  57. 2 2
      app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
  58. 2 1
      app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java
  59. 13 10
      app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java
  60. 19 6
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  61. 4 2
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
  62. 25 20
      app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt
  63. 30 6
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  64. 43 14
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java
  65. 38 5
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt
  66. 47 42
      app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  67. 14 5
      app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  68. 0 82
      app/src/main/java/com/owncloud/android/utils/CsrHelper.java
  69. 499 177
      app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java
  70. 1116 0
      app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt
  71. 3 0
      app/src/main/res/values/strings.xml

+ 3 - 0
.idea/inspectionProfiles/ktlint.xml

@@ -1,6 +1,9 @@
 <component name="InspectionProjectProfileManager">
   <profile version="1.0">
     <option name="myName" value="ktlint" />
+    <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.nio.channels.FileChannel,position" />
+    </inspection_tool>
     <inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
     <inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
   </profile>

+ 1 - 1
app/build.gradle

@@ -266,7 +266,7 @@ dependencies {
     implementation 'org.greenrobot:eventbus:3.3.1'
     implementation 'com.googlecode.ez-vcard:ez-vcard:0.12.0'
     implementation 'org.lukhnos:nnio:0.2'
-    implementation 'org.bouncycastle:bcpkix-jdk15to18:1.72'
+    implementation 'org.bouncycastle:bcpkix-jdk18on:1.75'
     implementation 'com.google.code.gson:gson:2.10.1'
     implementation 'com.github.nextcloud-deps:sectioned-recyclerview:0.6.1'
     implementation 'com.github.chrisbanes:PhotoView:2.3.0'

+ 1 - 0
app/lint.xml

@@ -48,6 +48,7 @@
     <issue id="TrustAllX509TrustManager">
         <ignore path="**/bouncycastle/est/jcajce/*.class" />
         <ignore path="**/bcpkix-jdk15to18-1.72.jar" />
+        <ignore path="**/bcpkix-jdk18on-1.75.jar" />
     </issue>
 
     <issue id="RestrictedApi" severity="error">

+ 1191 - 0
app/schemas/com.nextcloud.client.database.NextcloudDatabase/77.json

@@ -0,0 +1,1191 @@
+{
+    "formatVersion": 1,
+    "database": {
+        "version": 77,
+        "identityHash": "a3c1d02f306c6613a9a0d392b6cfa7f8",
+        "entities": [
+            {
+                "tableName": "arbitrary_data",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "cloudId",
+                        "columnName": "cloud_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "key",
+                        "columnName": "key",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "value",
+                        "columnName": "value",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "capabilities",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountName",
+                        "columnName": "account",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionMajor",
+                        "columnName": "version_mayor",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionMinor",
+                        "columnName": "version_minor",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionMicro",
+                        "columnName": "version_micro",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionString",
+                        "columnName": "version_string",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "versionEditor",
+                        "columnName": "version_edition",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "extendedSupport",
+                        "columnName": "extended_support",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "corePollinterval",
+                        "columnName": "core_pollinterval",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingApiEnabled",
+                        "columnName": "sharing_api_enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicEnabled",
+                        "columnName": "sharing_public_enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicPasswordEnforced",
+                        "columnName": "sharing_public_password_enforced",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicExpireDateEnabled",
+                        "columnName": "sharing_public_expire_date_enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicExpireDateDays",
+                        "columnName": "sharing_public_expire_date_days",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicExpireDateEnforced",
+                        "columnName": "sharing_public_expire_date_enforced",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicSendMail",
+                        "columnName": "sharing_public_send_mail",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicUpload",
+                        "columnName": "sharing_public_upload",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingUserSendMail",
+                        "columnName": "sharing_user_send_mail",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingResharing",
+                        "columnName": "sharing_resharing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingFederationOutgoing",
+                        "columnName": "sharing_federation_outgoing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingFederationIncoming",
+                        "columnName": "sharing_federation_incoming",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesBigfilechunking",
+                        "columnName": "files_bigfilechunking",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesUndelete",
+                        "columnName": "files_undelete",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesVersioning",
+                        "columnName": "files_versioning",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "externalLinks",
+                        "columnName": "external_links",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverName",
+                        "columnName": "server_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverColor",
+                        "columnName": "server_color",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverTextColor",
+                        "columnName": "server_text_color",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverElementColor",
+                        "columnName": "server_element_color",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverSlogan",
+                        "columnName": "server_slogan",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverLogo",
+                        "columnName": "server_logo",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverBackgroundUrl",
+                        "columnName": "background_url",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "endToEndEncryption",
+                        "columnName": "end_to_end_encryption",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "endToEndEncryptionKeysExist",
+                        "columnName": "end_to_end_encryption_keys_exist",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "endToEndEncryptionApiVersion",
+                        "columnName": "end_to_end_encryption_api_version",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "activity",
+                        "columnName": "activity",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverBackgroundDefault",
+                        "columnName": "background_default",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "serverBackgroundPlain",
+                        "columnName": "background_plain",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocument",
+                        "columnName": "richdocument",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentMimetypeList",
+                        "columnName": "richdocument_mimetype_list",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentDirectEditing",
+                        "columnName": "richdocument_direct_editing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentTemplates",
+                        "columnName": "richdocument_direct_templates",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentOptionalMimetypeList",
+                        "columnName": "richdocument_optional_mimetype_list",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharingPublicAskForOptionalPassword",
+                        "columnName": "sharing_public_ask_for_optional_password",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richdocumentProductName",
+                        "columnName": "richdocument_product_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "directEditingEtag",
+                        "columnName": "direct_editing_etag",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "userStatus",
+                        "columnName": "user_status",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "userStatusSupportsEmoji",
+                        "columnName": "user_status_supports_emoji",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etag",
+                        "columnName": "etag",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "filesLockingVersion",
+                        "columnName": "files_locking_version",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "groupfolders",
+                        "columnName": "groupfolders",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "dropAccount",
+                        "columnName": "drop_account",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "securityGuard",
+                        "columnName": "security_guard",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "external_links",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "iconUrl",
+                        "columnName": "icon_url",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "language",
+                        "columnName": "language",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "type",
+                        "columnName": "type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "name",
+                        "columnName": "name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "url",
+                        "columnName": "url",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "redirect",
+                        "columnName": "redirect",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "filelist",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "name",
+                        "columnName": "filename",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "encryptedName",
+                        "columnName": "encrypted_filename",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "path",
+                        "columnName": "path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "pathDecrypted",
+                        "columnName": "path_decrypted",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "parent",
+                        "columnName": "parent",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "creation",
+                        "columnName": "created",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "modified",
+                        "columnName": "modified",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "contentType",
+                        "columnName": "content_type",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "contentLength",
+                        "columnName": "content_length",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "storagePath",
+                        "columnName": "media_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountOwner",
+                        "columnName": "file_owner",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lastSyncDate",
+                        "columnName": "last_sync_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lastSyncDateForData",
+                        "columnName": "last_sync_date_for_data",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "modifiedAtLastSyncForData",
+                        "columnName": "modified_at_last_sync_for_data",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etag",
+                        "columnName": "etag",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etagOnServer",
+                        "columnName": "etag_on_server",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharedViaLink",
+                        "columnName": "share_by_link",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "permissions",
+                        "columnName": "permissions",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "remoteId",
+                        "columnName": "remote_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localId",
+                        "columnName": "local_id",
+                        "affinity": "INTEGER",
+                        "notNull": true,
+                        "defaultValue": "-1"
+                    },
+                    {
+                        "fieldPath": "updateThumbnail",
+                        "columnName": "update_thumbnail",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isDownloading",
+                        "columnName": "is_downloading",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "favorite",
+                        "columnName": "favorite",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hidden",
+                        "columnName": "hidden",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isEncrypted",
+                        "columnName": "is_encrypted",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "etagInConflict",
+                        "columnName": "etag_in_conflict",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharedWithSharee",
+                        "columnName": "shared_via_users",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "mountType",
+                        "columnName": "mount_type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hasPreview",
+                        "columnName": "has_preview",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "unreadCommentsCount",
+                        "columnName": "unread_comments_count",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "ownerId",
+                        "columnName": "owner_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "ownerDisplayName",
+                        "columnName": "owner_display_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "note",
+                        "columnName": "note",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharees",
+                        "columnName": "sharees",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "richWorkspace",
+                        "columnName": "rich_workspace",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "metadataSize",
+                        "columnName": "metadata_size",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "metadataLivePhoto",
+                        "columnName": "metadata_live_photo",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "locked",
+                        "columnName": "locked",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockType",
+                        "columnName": "lock_type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockOwner",
+                        "columnName": "lock_owner",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockOwnerDisplayName",
+                        "columnName": "lock_owner_display_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockOwnerEditor",
+                        "columnName": "lock_owner_editor",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockTimestamp",
+                        "columnName": "lock_timestamp",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockTimeout",
+                        "columnName": "lock_timeout",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lockToken",
+                        "columnName": "lock_token",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "tags",
+                        "columnName": "tags",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "metadataGPS",
+                        "columnName": "metadata_gps",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "e2eCounter",
+                        "columnName": "e2e_counter",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "filesystem",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localPath",
+                        "columnName": "local_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileIsFolder",
+                        "columnName": "is_folder",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileFoundRecently",
+                        "columnName": "found_at",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileSentForUpload",
+                        "columnName": "upload_triggered",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "syncedFolderId",
+                        "columnName": "syncedfolder_id",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "crc32",
+                        "columnName": "crc32",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileModified",
+                        "columnName": "modified_at",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "ocshares",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` INTEGER, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileSource",
+                        "columnName": "file_source",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "itemSource",
+                        "columnName": "item_source",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareType",
+                        "columnName": "share_type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareWith",
+                        "columnName": "shate_with",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "path",
+                        "columnName": "path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "permissions",
+                        "columnName": "permissions",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "sharedDate",
+                        "columnName": "shared_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "expirationDate",
+                        "columnName": "expiration_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "token",
+                        "columnName": "token",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareWithDisplayName",
+                        "columnName": "shared_with_display_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isDirectory",
+                        "columnName": "is_directory",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "userId",
+                        "columnName": "user_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "idRemoteShared",
+                        "columnName": "id_remote_shared",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountOwner",
+                        "columnName": "owner_share",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isPasswordProtected",
+                        "columnName": "is_password_protected",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "note",
+                        "columnName": "note",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hideDownload",
+                        "columnName": "hide_download",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareLink",
+                        "columnName": "share_link",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "shareLabel",
+                        "columnName": "share_label",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "synced_folders",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localPath",
+                        "columnName": "local_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "remotePath",
+                        "columnName": "remote_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "wifiOnly",
+                        "columnName": "wifi_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "chargingOnly",
+                        "columnName": "charging_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "existing",
+                        "columnName": "existing",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "enabled",
+                        "columnName": "enabled",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "enabledTimestampMs",
+                        "columnName": "enabled_timestamp_ms",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "subfolderByDate",
+                        "columnName": "subfolder_by_date",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "account",
+                        "columnName": "account",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "uploadAction",
+                        "columnName": "upload_option",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "nameCollisionPolicy",
+                        "columnName": "name_collision_policy",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "type",
+                        "columnName": "type",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "hidden",
+                        "columnName": "hidden",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "subFolderRule",
+                        "columnName": "sub_folder_rule",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "list_of_uploads",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localPath",
+                        "columnName": "local_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "remotePath",
+                        "columnName": "remote_path",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "accountName",
+                        "columnName": "account_name",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "fileSize",
+                        "columnName": "file_size",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "status",
+                        "columnName": "status",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "localBehaviour",
+                        "columnName": "local_behaviour",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "uploadTime",
+                        "columnName": "upload_time",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "nameCollisionPolicy",
+                        "columnName": "name_collision_policy",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isCreateRemoteFolder",
+                        "columnName": "is_create_remote_folder",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "uploadEndTimestamp",
+                        "columnName": "upload_end_timestamp",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "lastResult",
+                        "columnName": "last_result",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isWhileChargingOnly",
+                        "columnName": "is_while_charging_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "isWifiOnly",
+                        "columnName": "is_wifi_only",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "createdBy",
+                        "columnName": "created_by",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "folderUnlockToken",
+                        "columnName": "folder_unlock_token",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            },
+            {
+                "tableName": "virtual",
+                "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)",
+                "fields": [
+                    {
+                        "fieldPath": "id",
+                        "columnName": "_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "type",
+                        "columnName": "type",
+                        "affinity": "TEXT",
+                        "notNull": false
+                    },
+                    {
+                        "fieldPath": "ocFileId",
+                        "columnName": "ocfile_id",
+                        "affinity": "INTEGER",
+                        "notNull": false
+                    }
+                ],
+                "primaryKey": {
+                    "autoGenerate": true,
+                    "columnNames": [
+                        "_id"
+                    ]
+                },
+                "indices": [],
+                "foreignKeys": []
+            }
+        ],
+        "views": [],
+        "setupQueries": [
+            "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+            "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a3c1d02f306c6613a9a0d392b6cfa7f8')"
+        ]
+    }
+}

+ 32 - 0
app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java

@@ -0,0 +1,32 @@
+/*
+ *
+ * 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.nextcloud.client;
+
+public enum EndToEndAction {
+    CREATE_FOLDER,
+    GO_INTO_FOLDER,
+    GO_UP,
+    UPLOAD_FILE,
+    DOWNLOAD_FILE,
+    DELETE_FILE,
+}

+ 0 - 730
app/src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java

@@ -1,730 +0,0 @@
-/*
- *
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2020 Tobias Kaminsky
- * Copyright (C) 2020 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.nextcloud.client;
-
-import android.accounts.AccountManager;
-
-import com.nextcloud.test.RandomStringGenerator;
-import com.nextcloud.test.RetryTestRule;
-import com.owncloud.android.AbstractOnServerIT;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.db.OCUpload;
-import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.lib.common.accounts.AccountUtils;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.ocs.responses.PrivateKey;
-import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
-import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
-import com.owncloud.android.lib.resources.status.OCCapability;
-import com.owncloud.android.lib.resources.status.OwnCloudVersion;
-import com.owncloud.android.lib.resources.users.DeletePrivateKeyOperation;
-import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation;
-import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation;
-import com.owncloud.android.lib.resources.users.GetPublicKeyOperation;
-import com.owncloud.android.lib.resources.users.SendCSROperation;
-import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation;
-import com.owncloud.android.operations.DownloadFileOperation;
-import com.owncloud.android.operations.GetCapabilitiesOperation;
-import com.owncloud.android.operations.RemoveFileOperation;
-import com.owncloud.android.utils.CsrHelper;
-import com.owncloud.android.utils.EncryptionUtils;
-import com.owncloud.android.utils.FileStorageUtils;
-
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.File;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.interfaces.RSAPrivateCrtKey;
-import java.security.interfaces.RSAPublicKey;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Random;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import static com.owncloud.android.lib.resources.status.OwnCloudVersion.nextcloud_19;
-import static junit.framework.TestCase.assertEquals;
-import static junit.framework.TestCase.assertFalse;
-import static junit.framework.TestCase.assertNotNull;
-import static junit.framework.TestCase.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-@RunWith(AndroidJUnit4.class)
-public class EndToEndRandomIT extends AbstractOnServerIT {
-    public enum Action {
-        CREATE_FOLDER,
-        GO_INTO_FOLDER,
-        GO_UP,
-        UPLOAD_FILE,
-        DOWNLOAD_FILE,
-        DELETE_FILE,
-    }
-
-    private static ArbitraryDataProvider arbitraryDataProvider;
-
-    private OCFile currentFolder;
-    private int actionCount = 20;
-    private String rootEncFolder = "/e/";
-
-    @Rule
-    public RetryTestRule retryTestRule = new RetryTestRule();
-
-    @BeforeClass
-    public static void initClass() throws Exception {
-        arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
-        createKeys();
-    }
-
-    @Before
-    public void before() throws IOException {
-        OCCapability capability = getStorageManager().getCapability(account.name);
-
-        if (capability.getVersion().equals(new OwnCloudVersion("0.0.0"))) {
-            // fetch new one
-            assertTrue(new GetCapabilitiesOperation(getStorageManager())
-                           .execute(client)
-                           .isSuccess());
-        }
-        // tests only for NC19+
-        assumeTrue(getStorageManager()
-                       .getCapability(account.name)
-                       .getVersion()
-                       .isNewerOrEqual(nextcloud_19)
-                  );
-
-        // make sure that every file is available, even after tests that remove source file
-        createDummyFiles();
-    }
-
-    @Test
-    public void run() throws Exception {
-        init();
-
-        for (int i = 0; i < actionCount; i++) {
-            Action nextAction = Action.values()[new Random().nextInt(Action.values().length)];
-
-            switch (nextAction) {
-                case CREATE_FOLDER:
-                    createFolder(i);
-                    break;
-
-                case GO_INTO_FOLDER:
-                    goIntoFolder(i);
-                    break;
-
-                case GO_UP:
-                    goUp(i);
-                    break;
-
-                case UPLOAD_FILE:
-                    uploadFile(i);
-                    break;
-
-                case DOWNLOAD_FILE:
-                    downloadFile(i);
-                    break;
-
-                case DELETE_FILE:
-                    deleteFile(i);
-                    break;
-
-                default:
-                    Log_OC.d(this, "[" + i + "/" + actionCount + "]" + " Unknown action: " + nextAction);
-                    break;
-            }
-        }
-    }
-
-    @Test
-    public void uploadOneFile() throws Exception {
-        init();
-
-        uploadFile(0);
-    }
-
-    @Test
-    public void createFolder() throws Exception {
-        init();
-
-        currentFolder = createFolder(0);
-        assertNotNull(currentFolder);
-    }
-
-    @Test
-    public void createSubFolders() throws Exception {
-        init();
-
-        currentFolder = createFolder(0);
-        assertNotNull(currentFolder);
-
-        currentFolder = createFolder(1);
-        assertNotNull(currentFolder);
-
-        currentFolder = createFolder(2);
-        assertNotNull(currentFolder);
-    }
-
-    @Test
-    public void createSubFoldersWithFiles() throws Exception {
-        init();
-
-        currentFolder = createFolder(0);
-        assertNotNull(currentFolder);
-
-        uploadFile(1);
-        uploadFile(1);
-        uploadFile(2);
-
-        currentFolder = createFolder(1);
-        assertNotNull(currentFolder);
-        uploadFile(11);
-        uploadFile(12);
-        uploadFile(13);
-
-        currentFolder = createFolder(2);
-        assertNotNull(currentFolder);
-
-        uploadFile(21);
-        uploadFile(22);
-        uploadFile(23);
-    }
-
-    @Test
-    public void pseudoRandom() throws Exception {
-        init();
-
-        uploadFile(1);
-        createFolder(2);
-        goIntoFolder(3);
-        goUp(4);
-        createFolder(5);
-        uploadFile(6);
-        goUp(7);
-        goIntoFolder(8);
-        goIntoFolder(9);
-        uploadFile(10);
-    }
-
-    @Test
-    public void deleteFile() throws Exception {
-        init();
-
-        uploadFile(1);
-        deleteFile(1);
-    }
-
-    @Test
-    public void deleteFolder() throws Exception {
-        init();
-
-        // create folder, go into it
-        OCFile createdFolder = createFolder(0);
-        assertNotNull(createdFolder);
-        currentFolder = createdFolder;
-
-        uploadFile(1);
-        goUp(1);
-
-        // delete folder
-        assertTrue(new RemoveFileOperation(createdFolder,
-                                           false,
-                                           user,
-                                           false,
-                                           targetContext,
-                                           getStorageManager())
-                       .execute(client)
-                       .isSuccess());
-    }
-
-    @Test
-    public void downloadFile() throws Exception {
-        init();
-
-        uploadFile(1);
-        downloadFile(1);
-    }
-
-    private void init() throws Exception {
-        // create folder
-        createFolder(rootEncFolder);
-        OCFile encFolder = createFolder(rootEncFolder + RandomStringGenerator.make(5) + "/");
-
-        // encrypt it
-        assertTrue(new ToggleEncryptionRemoteOperation(encFolder.getLocalId(),
-                                                       encFolder.getRemotePath(),
-                                                       true)
-                       .execute(client).isSuccess());
-        encFolder.setEncrypted(true);
-        getStorageManager().saveFolder(encFolder, new ArrayList<>(), new ArrayList<>());
-
-        useExistingKeys();
-
-        rootEncFolder = encFolder.getDecryptedRemotePath();
-        currentFolder = encFolder;
-    }
-
-    private OCFile createFolder(int i) {
-        String path = currentFolder.getDecryptedRemotePath() + RandomStringGenerator.make(5) + "/";
-        Log_OC.d(this, "[" + i + "/" + actionCount + "] " + "Create folder: " + path);
-
-        return createFolder(path);
-    }
-
-    private void goIntoFolder(int i) {
-        ArrayList<OCFile> folders = new ArrayList<>();
-        for (OCFile file : getStorageManager().getFolderContent(currentFolder, false)) {
-            if (file.isFolder()) {
-                folders.add(file);
-            }
-        }
-
-        if (folders.isEmpty()) {
-            Log_OC.d(this, "[" + i + "/" + actionCount + "] " + "Go into folder: No folders");
-            return;
-        }
-
-        currentFolder = folders.get(new Random().nextInt(folders.size()));
-        Log_OC.d(this,
-                 "[" + i + "/" + actionCount + "] " + "Go into folder: " + currentFolder.getDecryptedRemotePath());
-    }
-
-    private void goUp(int i) {
-        if (currentFolder.getRemotePath().equals(rootEncFolder)) {
-            Log_OC.d(this,
-                     "[" + i + "/" + actionCount + "] " + "Go up to folder: " + currentFolder.getDecryptedRemotePath());
-            return;
-        }
-
-        currentFolder = getStorageManager().getFileById(currentFolder.getParentId());
-        if (currentFolder == null) {
-            throw new RuntimeException("Current folder is null");
-        }
-
-        Log_OC.d(this,
-                 "[" + i + "/" + actionCount + "] " + "Go up to folder: " + currentFolder.getDecryptedRemotePath());
-    }
-
-    private void uploadFile(int i) throws IOException {
-        String fileName = RandomStringGenerator.make(5) + ".txt";
-
-        File file;
-        if (new Random().nextBoolean()) {
-            file = createFile(fileName, new Random().nextInt(50000));
-        } else {
-            file = createFile(fileName, 500000 + new Random().nextInt(50000));
-        }
-
-        String remotePath = currentFolder.getRemotePath() + fileName;
-
-        Log_OC.d(this,
-                 "[" + i + "/" + actionCount + "] " +
-                     "Upload file to: " + currentFolder.getDecryptedRemotePath() + fileName);
-
-        OCUpload ocUpload = new OCUpload(file.getAbsolutePath(),
-                                         remotePath,
-                                         account.name);
-        uploadOCUpload(ocUpload);
-        shortSleep();
-
-        OCFile parentFolder = getStorageManager()
-            .getFileByEncryptedRemotePath(new File(ocUpload.getRemotePath()).getParent() + "/");
-        String uploadedFileName = new File(ocUpload.getRemotePath()).getName();
-
-        String decryptedPath = parentFolder.getDecryptedRemotePath() + uploadedFileName;
-
-        OCFile uploadedFile = getStorageManager().getFileByDecryptedRemotePath(decryptedPath);
-        verifyStoragePath(uploadedFile);
-
-        // verify storage path
-        refreshFolder(currentFolder.getRemotePath());
-        uploadedFile = getStorageManager().getFileByDecryptedRemotePath(decryptedPath);
-        verifyStoragePath(uploadedFile);
-
-        // verify that encrypted file is on server
-        assertTrue(new ReadFileRemoteOperation(currentFolder.getRemotePath() + uploadedFile.getEncryptedFileName())
-                       .execute(client)
-                       .isSuccess());
-
-        // verify that unencrypted file is not on server
-        assertFalse(new ReadFileRemoteOperation(currentFolder.getDecryptedRemotePath() + fileName)
-                        .execute(client)
-                        .isSuccess());
-    }
-
-    private void downloadFile(int i) {
-        ArrayList<OCFile> files = new ArrayList<>();
-        for (OCFile file : getStorageManager().getFolderContent(currentFolder, false)) {
-            if (!file.isFolder()) {
-                files.add(file);
-            }
-        }
-
-        if (files.isEmpty()) {
-            Log_OC.d(this, "[" + i + "/" + actionCount + "] No files in: " + currentFolder.getDecryptedRemotePath());
-            return;
-        }
-
-        OCFile fileToDownload = files.get(new Random().nextInt(files.size()));
-        assertNotNull(fileToDownload.getRemoteId());
-
-        Log_OC.d(this,
-                 "[" + i + "/" + actionCount + "] " + "Download file: " +
-                     currentFolder.getDecryptedRemotePath() + fileToDownload.getDecryptedFileName());
-
-        assertTrue(new DownloadFileOperation(user, fileToDownload, targetContext)
-                       .execute(client)
-                       .isSuccess());
-
-        assertTrue(new File(fileToDownload.getStoragePath()).exists());
-        verifyStoragePath(fileToDownload);
-    }
-
-    @Test
-    public void testUploadWithCopy() throws Exception {
-        init();
-
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
-                                         currentFolder.getRemotePath() + "nonEmpty.txt",
-                                         account.name);
-
-        uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_COPY);
-
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
-        OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
-                                                                                      "nonEmpty.txt");
-
-        assertTrue(originalFile.exists());
-        assertTrue(new File(uploadedFile.getStoragePath()).exists());
-    }
-
-    @Test
-    public void testUploadWithMove() throws Exception {
-        init();
-
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
-                                         currentFolder.getRemotePath() + "nonEmpty.txt",
-                                         account.name);
-
-        uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_MOVE);
-
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
-        OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
-                                                                                      "nonEmpty.txt");
-
-        assertFalse(originalFile.exists());
-        assertTrue(new File(uploadedFile.getStoragePath()).exists());
-    }
-
-    @Test
-    public void testUploadWithForget() throws Exception {
-        init();
-
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
-                                         currentFolder.getRemotePath() + "nonEmpty.txt",
-                                         account.name);
-
-        uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_FORGET);
-
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
-        OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
-                                                                                      "nonEmpty.txt");
-
-        assertTrue(originalFile.exists());
-        assertFalse(new File(uploadedFile.getStoragePath()).exists());
-    }
-
-    @Test
-    public void testUploadWithDelete() throws Exception {
-        init();
-
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
-                                         currentFolder.getRemotePath() + "nonEmpty.txt",
-                                         account.name);
-
-        uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_DELETE);
-
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
-        OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
-                                                                                      "nonEmpty.txt");
-
-        assertFalse(originalFile.exists());
-        assertFalse(new File(uploadedFile.getStoragePath()).exists());
-    }
-
-    @Test
-    public void testCheckCSR() throws Exception {
-        deleteKeys();
-
-        // Create public/private key pair
-        KeyPair keyPair = EncryptionUtils.generateKeyPair();
-
-        // create CSR
-        AccountManager accountManager = AccountManager.get(targetContext);
-        String userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID);
-        String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId);
-
-        SendCSROperation operation = new SendCSROperation(urlEncoded);
-        RemoteOperationResult result = operation.execute(account, targetContext);
-
-        assertTrue(result.isSuccess());
-        String publicKeyString = (String) result.getData().get(0);
-
-        // check key
-        RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyPair.getPrivate();
-        RSAPublicKey publicKey = EncryptionUtils.convertPublicKeyFromString(publicKeyString);
-
-        BigInteger modulusPublic = publicKey.getModulus();
-        BigInteger modulusPrivate = privateKey.getModulus();
-
-        assertEquals(modulusPrivate, modulusPublic);
-
-        createKeys();
-    }
-
-    private void deleteFile(int i) {
-        ArrayList<OCFile> files = new ArrayList<>();
-        for (OCFile file : getStorageManager().getFolderContent(currentFolder, false)) {
-            if (!file.isFolder()) {
-                files.add(file);
-            }
-        }
-
-        if (files.isEmpty()) {
-            Log_OC.d(this, "[" + i + "/" + actionCount + "] No files in: " + currentFolder.getDecryptedRemotePath());
-            return;
-        }
-
-        OCFile fileToDelete = files.get(new Random().nextInt(files.size()));
-        assertNotNull(fileToDelete.getRemoteId());
-
-        Log_OC.d(this,
-                 "[" + i + "/" + actionCount + "] " +
-                     "Delete file: " + currentFolder.getDecryptedRemotePath() + fileToDelete.getDecryptedFileName());
-
-        assertTrue(new RemoveFileOperation(fileToDelete,
-                                           false,
-                                           user,
-                                           false,
-                                           targetContext,
-                                           getStorageManager())
-                       .execute(client)
-                       .isSuccess());
-    }
-
-    @Test
-    public void reInit() throws Exception {
-        // create folder
-        OCFile encFolder = createFolder(rootEncFolder);
-
-        // encrypt it
-        assertTrue(new ToggleEncryptionRemoteOperation(encFolder.getLocalId(),
-                                                       encFolder.getRemotePath(),
-                                                       true)
-                       .execute(client).isSuccess());
-        encFolder.setEncrypted(true);
-        getStorageManager().saveFolder(encFolder, new ArrayList<>(), new ArrayList<>());
-
-
-        // delete keys
-        arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PRIVATE_KEY);
-        arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PUBLIC_KEY);
-        arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.MNEMONIC);
-
-        useExistingKeys();
-    }
-
-    private void useExistingKeys() throws Exception {
-        // download them from server
-        GetPublicKeyOperation publicKeyOperation = new GetPublicKeyOperation();
-        RemoteOperationResult<String> publicKeyResult = publicKeyOperation.execute(account, targetContext);
-
-        assertTrue("Result code:" + publicKeyResult.getHttpCode(), publicKeyResult.isSuccess());
-
-        String publicKeyFromServer = publicKeyResult.getResultData();
-        arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
-                                                    EncryptionUtils.PUBLIC_KEY,
-                                                    publicKeyFromServer);
-
-        RemoteOperationResult<PrivateKey> privateKeyResult = new GetPrivateKeyOperation().execute(account,
-                                                                                                  targetContext);
-        assertTrue(privateKeyResult.isSuccess());
-
-        PrivateKey privateKey = privateKeyResult.getResultData();
-
-        String mnemonic = generateMnemonicString();
-        String decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(privateKey.getKey(), mnemonic);
-
-        arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
-                                                    EncryptionUtils.PRIVATE_KEY, decryptedPrivateKey);
-
-        Log_OC.d(this, "Private key successfully decrypted and stored");
-
-        arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.MNEMONIC, mnemonic);
-    }
-
-    /*
-    TODO do not c&p code
-     */
-    private static void createKeys() throws Exception {
-        deleteKeys();
-
-        String publicKeyString;
-
-        // Create public/private key pair
-        KeyPair keyPair = EncryptionUtils.generateKeyPair();
-
-        // create CSR
-        AccountManager accountManager = AccountManager.get(targetContext);
-        String userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID);
-        String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId);
-
-        SendCSROperation operation = new SendCSROperation(urlEncoded);
-        RemoteOperationResult result = operation.execute(account, targetContext);
-
-        if (result.isSuccess()) {
-            publicKeyString = (String) result.getData().get(0);
-
-            // check key
-            RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey) keyPair.getPrivate();
-            RSAPublicKey publicKey = EncryptionUtils.convertPublicKeyFromString(publicKeyString);
-
-            BigInteger modulusPublic = publicKey.getModulus();
-            BigInteger modulusPrivate = privateKey.getModulus();
-
-            if (modulusPrivate.compareTo(modulusPublic) != 0) {
-                throw new RuntimeException("Wrong CSR returned");
-            }
-        } else {
-            throw new Exception("failed to send CSR", result.getException());
-        }
-
-        java.security.PrivateKey privateKey = keyPair.getPrivate();
-        String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.getEncoded());
-        String privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey);
-        String encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(privatePemKeyString,
-                                                                       generateMnemonicString());
-
-        // upload encryptedPrivateKey
-        StorePrivateKeyOperation storePrivateKeyOperation = new StorePrivateKeyOperation(encryptedPrivateKey);
-        RemoteOperationResult storePrivateKeyResult = storePrivateKeyOperation.execute(account, targetContext);
-
-        if (storePrivateKeyResult.isSuccess()) {
-            arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PRIVATE_KEY,
-                                                        privateKeyString);
-            arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PUBLIC_KEY, publicKeyString);
-            arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.MNEMONIC,
-                                                        generateMnemonicString());
-        } else {
-            throw new RuntimeException("Error uploading private key!");
-        }
-    }
-
-    private static void deleteKeys() {
-        RemoteOperationResult<PrivateKey> privateKeyRemoteOperationResult = new GetPrivateKeyOperation().execute(client);
-        RemoteOperationResult<String> publicKeyRemoteOperationResult = new GetPublicKeyOperation().execute(client);
-
-        if (privateKeyRemoteOperationResult.isSuccess() || publicKeyRemoteOperationResult.isSuccess()) {
-            // delete keys
-            assertTrue(new DeletePrivateKeyOperation().execute(client).isSuccess());
-            assertTrue(new DeletePublicKeyOperation().execute(client).isSuccess());
-
-            arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PRIVATE_KEY);
-            arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PUBLIC_KEY);
-            arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.MNEMONIC);
-        }
-    }
-
-    private static String generateMnemonicString() {
-        return "1 2 3 4 5 6";
-    }
-
-    public void after() {
-        // remove all encrypted files
-        OCFile root = fileDataStorageManager.getFileByDecryptedRemotePath("/");
-        removeFolder(root);
-
-//        List<OCFile> files = fileDataStorageManager.getFolderContent(root, false);
-//
-//        for (OCFile child : files) {
-//            removeFolder(child);
-//        }
-
-        assertEquals(0, fileDataStorageManager.getFolderContent(root, false).size());
-
-        super.after();
-    }
-
-    private void removeFolder(OCFile folder) {
-        Log_OC.d(this, "Start removing content of folder: " + folder.getDecryptedRemotePath());
-
-        List<OCFile> children = fileDataStorageManager.getFolderContent(folder, false);
-
-        // remove children
-        for (OCFile child : children) {
-            if (child.isFolder()) {
-                removeFolder(child);
-
-                // remove folder
-                Log_OC.d(this, "Remove folder: " + child.getDecryptedRemotePath());
-                if (!folder.isEncrypted() && child.isEncrypted()) {
-                    assertTrue(new ToggleEncryptionRemoteOperation(child.getLocalId(),
-                                                                   child.getRemotePath(),
-                                                                   false)
-                                   .execute(client)
-                                   .isSuccess());
-
-                    OCFile f = getStorageManager().getFileByEncryptedRemotePath(child.getRemotePath());
-                    f.setEncrypted(false);
-                    getStorageManager().saveFile(f);
-
-                    child.setEncrypted(false);
-                }
-            } else {
-                Log_OC.d(this, "Remove file: " + child.getDecryptedRemotePath());
-            }
-
-            assertTrue(new RemoveFileOperation(child, false, user, false, targetContext, getStorageManager())
-                           .execute(client)
-                           .isSuccess()
-                      );
-        }
-
-        Log_OC.d(this, "Finished removing content of folder: " + folder.getDecryptedRemotePath());
-    }
-
-    private void verifyStoragePath(OCFile file) {
-        assertEquals(FileStorageUtils.getSavePath(account.name) +
-                         currentFolder.getDecryptedRemotePath() +
-                         file.getDecryptedFileName(),
-                     file.getStoragePath());
-    }
-}

+ 27 - 14
app/src/androidTest/java/com/owncloud/android/AbstractIT.java

@@ -28,6 +28,9 @@ import com.nextcloud.client.preferences.DarkMode;
 import com.nextcloud.common.NextcloudClient;
 import com.nextcloud.java.util.Optional;
 import com.nextcloud.test.GrantStoragePermissionRule;
+import com.nextcloud.test.RandomStringGenerator;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
@@ -38,6 +41,7 @@ import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientFactory;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
 import com.owncloud.android.lib.resources.status.CapabilityBooleanType;
 import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation;
 import com.owncloud.android.lib.resources.status.OCCapability;
@@ -46,8 +50,6 @@ import com.owncloud.android.operations.CreateFolderOperation;
 import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.utils.FileStorageUtils;
 
-import junit.framework.TestCase;
-
 import org.apache.commons.io.FileUtils;
 import org.junit.After;
 import org.junit.Before;
@@ -100,6 +102,8 @@ public abstract class AbstractIT {
     protected FileDataStorageManager fileDataStorageManager =
         new FileDataStorageManager(user, targetContext.getContentResolver());
 
+    protected ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
+
     @BeforeClass
     public static void beforeAll() {
         try {
@@ -118,14 +122,11 @@ public abstract class AbstractIT {
 
             client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext);
             nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext);
-        } catch (OperationCanceledException e) {
-            e.printStackTrace();
-        } catch (AuthenticatorException e) {
-            e.printStackTrace();
-        } catch (IOException e) {
-            e.printStackTrace();
-        } catch (AccountUtils.AccountNotFoundException e) {
-            e.printStackTrace();
+        } catch (OperationCanceledException |
+                 IOException |
+                 AccountUtils.AccountNotFoundException |
+                 AuthenticatorException e) {
+            throw new RuntimeException("Error setting up clients", e);
         }
 
         Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments();
@@ -334,11 +335,15 @@ public abstract class AbstractIT {
     }
 
     public OCFile createFolder(String remotePath) {
-        TestCase.assertTrue(new CreateFolderOperation(remotePath, user, targetContext, getStorageManager())
-                                .execute(client)
-                                .isSuccess());
+        RemoteOperationResult check = new ExistenceCheckRemoteOperation(remotePath, false).execute(client);
 
-        return getStorageManager().getFileByDecryptedRemotePath(remotePath);
+        if (!check.isSuccess()) {
+            assertTrue(new CreateFolderOperation(remotePath, user, targetContext, getStorageManager())
+                           .execute(client)
+                           .isSuccess());
+        }
+
+        return getStorageManager().getFileByDecryptedRemotePath(remotePath.endsWith("/") ? remotePath : remotePath + "/");
     }
 
     public void uploadFile(File file, String remotePath) {
@@ -473,6 +478,14 @@ public abstract class AbstractIT {
         return AccountManager.get(targetContext).getUserData(user.toPlatformAccount(), KEY_USER_ID);
     }
 
+    public String getRandomName() {
+        return getRandomName(5);
+    }
+
+    public String getRandomName(int length) {
+        return RandomStringGenerator.make(length);
+    }
+
     protected static User getUser(Account account) {
         Optional<User> optionalUser = UserAccountManagerImpl.fromContext(targetContext).getUser(account.name);
         return optionalUser.orElseThrow(IllegalAccessError::new);

+ 8 - 10
app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java

@@ -94,21 +94,19 @@ public abstract class AbstractOnServerIT extends AbstractIT {
             user = optionalUser.orElseThrow(IllegalAccessError::new);
 
             client = OwnCloudClientFactory.createOwnCloudClient(account, targetContext);
+            nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, targetContext);
 
             createDummyFiles();
 
             waitForServer(client, baseUrl);
 
-            deleteAllFilesOnServer(); // makes sure that no file/folder is in root
+            // deleteAllFilesOnServer(); // makes sure that no file/folder is in root
 
-        } catch (OperationCanceledException e) {
-            e.printStackTrace();
-        } catch (AuthenticatorException e) {
-            e.printStackTrace();
-        } catch (IOException e) {
-            e.printStackTrace();
-        } catch (AccountUtils.AccountNotFoundException e) {
-            e.printStackTrace();
+        } catch (OperationCanceledException |
+                 IOException |
+                 AccountUtils.AccountNotFoundException |
+                 AuthenticatorException e) {
+            throw new RuntimeException("Error setting up clients", e);
         }
     }
 
@@ -144,7 +142,7 @@ public abstract class AbstractOnServerIT extends AbstractIT {
                     removeResult = new RemoveFileRemoteOperation(remoteFile.getRemotePath())
                         .execute(client)
                         .isSuccess();
-                    
+
                     if (removeResult) {
                         break;
                     }

+ 2 - 3
app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt

@@ -26,12 +26,11 @@ import org.junit.Assert.assertEquals
 import org.junit.Test
 
 class ArbitraryDataProviderIT : AbstractIT() {
-    private val arbitraryDataProvider = ArbitraryDataProviderImpl(targetContext)
 
     @Test
-    fun testNull() {
+    fun testEmpty() {
         val key = "DUMMY_KEY"
-        arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, null)
+        arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, key, "")
 
         assertEquals("", arbitraryDataProvider.getValue(user.accountName, key))
     }

+ 1 - 0
app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt

@@ -84,6 +84,7 @@ class FileDetailSharingFragmentIT : AbstractIT() {
             remoteId = "00000001"
             parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId
             permissions = OCFile.PERMISSION_CAN_RESHARE
+            fileDataStorageManager.saveFile(this)
         }
 
         folder = OCFile("/test").apply {

+ 204 - 105
app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java

@@ -31,16 +31,20 @@ 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.datamodel.e2e.v1.decrypted.Data;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.Encrypted;
+import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile;
+import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.utils.CsrHelper;
+import com.owncloud.android.lib.resources.e2ee.CsrHelper;
 import com.owncloud.android.utils.EncryptionUtils;
 
 import org.apache.commons.codec.binary.Hex;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.runner.RunWith;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -63,9 +67,6 @@ import java.util.Set;
 
 import javax.crypto.BadPaddingException;
 
-import androidx.test.runner.AndroidJUnit4;
-
-import static com.owncloud.android.utils.EncryptionUtils.EncryptedFile;
 import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
 import static com.owncloud.android.utils.EncryptionUtils.decryptFile;
 import static com.owncloud.android.utils.EncryptionUtils.decryptFolderMetaData;
@@ -93,11 +94,12 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNull;
 
-@RunWith(AndroidJUnit4.class)
 public class EncryptionTestIT extends AbstractIT {
     @Rule public RetryTestRule retryTestRule = new RetryTestRule();
 
-    private String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
+    ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
+
+    public static final String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
         "IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV" +
         "GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7" +
         "Y0BJX9i/nW/L0L/VaE8CZTAqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCi" +
@@ -123,7 +125,7 @@ public class EncryptionTestIT extends AbstractIT {
         "JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o" +
         "uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA==";
 
-    private String cert = "-----BEGIN CERTIFICATE-----\n" +
+    public static final String publicKey = "-----BEGIN CERTIFICATE-----\n" +
         "MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu\n" +
         "bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
         "dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
@@ -151,7 +153,7 @@ public class EncryptionTestIT extends AbstractIT {
         byte[] key1 = generateKey();
         String base64encodedKey = encodeBytesToBase64String(key1);
 
-        String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey, cert);
+        String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey, publicKey);
         String decryptedString = decryptStringAsymmetric(encryptedString, privateKey);
 
         byte[] key2 = decodeStringToBase64Bytes(decryptedString);
@@ -207,9 +209,9 @@ public class EncryptionTestIT extends AbstractIT {
 
             String encryptedString;
             if (new Random().nextBoolean()) {
-                encryptedString = EncryptionUtils.encryptStringSymmetric(privateKey, key);
+                encryptedString = EncryptionUtils.encryptStringSymmetricAsString(privateKey, key);
             } else {
-                encryptedString = EncryptionUtils.encryptStringSymmetricOld(privateKey, key);
+                encryptedString = EncryptionUtils.encryptStringSymmetricAsStringOld(privateKey, key);
 
                 if (encryptedString.indexOf(ivDelimiterOld) != encryptedString.lastIndexOf(ivDelimiterOld)) {
                     Log_OC.d("EncryptionTestIT", "skip due to duplicated iv (old system) -> ignoring");
@@ -230,7 +232,7 @@ public class EncryptionTestIT extends AbstractIT {
         for (int i = 0; i < max; i++) {
             Log_OC.d("EncryptionTestIT", i + " of " + max);
 
-            String encryptedString = EncryptionUtils.encryptStringSymmetric(privateKey, key);
+            String encryptedString = EncryptionUtils.encryptStringSymmetricAsString(privateKey, key);
 
             int delimiterPosition = encryptedString.indexOf(ivDelimiter);
             if (delimiterPosition == -1) {
@@ -286,43 +288,42 @@ public class EncryptionTestIT extends AbstractIT {
         keyGen.initialize(2048, new SecureRandom());
         KeyPair keyPair = keyGen.generateKeyPair();
 
-        assertFalse(CsrHelper.generateCsrPemEncodedString(keyPair, "").isEmpty());
+        assertFalse(new CsrHelper().generateCsrPemEncodedString(keyPair, "").isEmpty());
         assertFalse(encodeBytesToBase64String(keyPair.getPublic().getEncoded()).isEmpty());
     }
 
+
     /**
-     * DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt -> decrypt -> JSON ->
-     * EncryptedFolderMetadata -> DecryptedFolderMetadata
+     * DecryptedFolderMetadataFile -> EncryptedFolderMetadataFile -> JSON -> encrypt -> decrypt -> JSON ->
+     * EncryptedFolderMetadataFile -> DecryptedFolderMetadataFile
      */
     @Test
-    public void encryptionMetadata() throws Exception {
-        DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
-        long folderID = 1;
+    public void encryptionMetadataV1() throws Exception {
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
 
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            1,
             user,
-            folderID);
+            arbitraryDataProvider);
 
         // serialize
         String encryptedJson = serializeJSON(encryptedFolderMetadata1);
 
         // de-serialize
-        EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
-                                                                           new TypeToken<EncryptedFolderMetadata>() {
-                                                                           });
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
+                                                                                 new TypeToken<>() {
+                                                                                 });
 
         // decrypt
-        DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
             encryptedFolderMetadata2,
             privateKey,
             arbitraryDataProvider,
             user,
-            folderID);
+            1);
 
         // compare
         assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
@@ -331,29 +332,28 @@ public class EncryptionTestIT extends AbstractIT {
 
     @Test
     public void testChangedMetadataKey() throws Exception {
-        DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
         long folderID = 1;
 
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            folderID,
             user,
-            folderID);
+            arbitraryDataProvider);
 
         // store metadata key
         String oldMetadataKey = encryptedFolderMetadata1.getMetadata().getMetadataKey();
 
         // do it again
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            folderID,
             user,
-            folderID);
+            arbitraryDataProvider);
 
         String newMetadataKey = encryptedFolderMetadata2.getMetadata().getMetadataKey();
 
@@ -362,17 +362,16 @@ public class EncryptionTestIT extends AbstractIT {
 
     @Test
     public void testMigrateMetadataKey() throws Exception {
-        DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
         long folderID = 1;
 
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            folderID,
             user,
-            folderID);
+            arbitraryDataProvider);
 
         // reset new metadata key, to mimic old version
         encryptedFolderMetadata1.getMetadata().setMetadataKey(null);
@@ -380,12 +379,12 @@ public class EncryptionTestIT extends AbstractIT {
 
         // do it again
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata2 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            folderID,
             user,
-            folderID);
+            arbitraryDataProvider);
 
         String newMetadataKey = encryptedFolderMetadata2.getMetadata().getMetadataKey();
 
@@ -403,7 +402,7 @@ public class EncryptionTestIT extends AbstractIT {
 
     @Test
     public void cryptFileWithMetadata() throws Exception {
-        DecryptedFolderMetadata metadata = generateFolderMetadata();
+        DecryptedFolderMetadataFileV1 metadata = generateFolderMetadataV1_1();
 
         // n9WXAIXO2wRY4R8nXwmo
         assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r",
@@ -428,28 +427,27 @@ public class EncryptionTestIT extends AbstractIT {
 
     @Test
     public void bigMetadata() throws Exception {
-        DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
         long folderID = 1;
 
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            folderID,
             user,
-            folderID);
+            arbitraryDataProvider);
 
         // serialize
         String encryptedJson = serializeJSON(encryptedFolderMetadata1);
 
         // de-serialize
-        EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
-                                                                           new TypeToken<EncryptedFolderMetadata>() {
-                                                                           });
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
+                                                                                 new TypeToken<>() {
+                                                                                 });
 
         // decrypt
-        DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
             encryptedFolderMetadata2,
             privateKey,
             arbitraryDataProvider,
@@ -473,17 +471,17 @@ public class EncryptionTestIT extends AbstractIT {
 
             // encrypt
             encryptedFolderMetadata1 = encryptFolderMetadata(decryptedFolderMetadata1,
-                                                             cert,
-                                                             arbitraryDataProvider,
+                                                             publicKey,
+                                                             folderID,
                                                              user,
-                                                             folderID);
+                                                             arbitraryDataProvider);
 
             // serialize
             encryptedJson = serializeJSON(encryptedFolderMetadata1);
 
             // de-serialize
             encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
-                                                       new TypeToken<EncryptedFolderMetadata>() {
+                                                       new TypeToken<>() {
                                                        });
 
             // decrypt
@@ -502,21 +500,97 @@ public class EncryptionTestIT extends AbstractIT {
         }
     }
 
+    @Test
+    public void bigMetadata2() throws Exception {
+        long folderID = 1;
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
+
+        // encrypt
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
+            decryptedFolderMetadata1,
+            publicKey,
+            folderID,
+            user,
+            arbitraryDataProvider);
+
+        // serialize
+        String encryptedJson = serializeJSON(encryptedFolderMetadata1);
+
+        // de-serialize
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
+                                                                                 new TypeToken<EncryptedFolderMetadataFileV1>() {
+                                                                                 });
+
+        // decrypt
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
+            encryptedFolderMetadata2,
+            privateKey,
+            arbitraryDataProvider,
+            user,
+            folderID);
+
+        // compare
+        assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
+                                      serializeJSON(decryptedFolderMetadata2)));
+
+        // prefill with 500
+        for (int i = 0; i < 500; i++) {
+            addFile(decryptedFolderMetadata1, i);
+        }
+
+        int max = 505;
+        for (int i = 500; i < max; i++) {
+            Log_OC.d(this, "Big metadata: " + i + " of " + max);
+
+            addFile(decryptedFolderMetadata1, i);
+
+            // encrypt
+            encryptedFolderMetadata1 = encryptFolderMetadata(
+                decryptedFolderMetadata1,
+                publicKey,
+                folderID,
+                user,
+                arbitraryDataProvider);
+
+            // serialize
+            encryptedJson = serializeJSON(encryptedFolderMetadata1);
+
+            // de-serialize
+            encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
+                                                       new TypeToken<>() {
+                                                       });
+
+            // decrypt
+            decryptedFolderMetadata2 = decryptFolderMetaData(
+                encryptedFolderMetadata2,
+                privateKey,
+                arbitraryDataProvider,
+                user,
+                folderID);
+
+            // compare
+            assertTrue(compareJsonStrings(serializeJSON(decryptedFolderMetadata1),
+                                          serializeJSON(decryptedFolderMetadata2)));
+
+            assertEquals(i + 3, decryptedFolderMetadata1.getFiles().size());
+            assertEquals(i + 3, decryptedFolderMetadata2.getFiles().size());
+        }
+    }
+
     @Test
     public void filedrop() throws Exception {
-        DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata1 = generateFolderMetadataV1_1();
         long folderID = 1;
 
         // add filedrop
-        Map<String, DecryptedFolderMetadata.DecryptedFile> filesdrop = new HashMap<>();
+        Map<String, DecryptedFile> filesdrop = new HashMap<>();
 
-        DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
+        Data data = new Data();
         data.setKey("9dfzbIYDt28zTyZfbcll+g==");
         data.setFilename("test2.txt");
         data.setVersion(1);
 
-        DecryptedFolderMetadata.DecryptedFile file = new DecryptedFolderMetadata.DecryptedFile();
+        DecryptedFile file = new DecryptedFile();
         file.setInitializationVector("hnJLF8uhDvDoFK4ajuvwrg==");
         file.setEncrypted(data);
         file.setMetadataKey(0);
@@ -527,24 +601,24 @@ public class EncryptionTestIT extends AbstractIT {
         decryptedFolderMetadata1.setFiledrop(filesdrop);
 
         // encrypt
-        EncryptedFolderMetadata encryptedFolderMetadata1 = encryptFolderMetadata(
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata1 = encryptFolderMetadata(
             decryptedFolderMetadata1,
-            cert,
-            arbitraryDataProvider,
+            publicKey,
+            folderID,
             user,
-            folderID);
-        EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, cert);
+            arbitraryDataProvider);
+        EncryptionUtils.encryptFileDropFiles(decryptedFolderMetadata1, encryptedFolderMetadata1, publicKey);
 
         // serialize
         String encryptedJson = serializeJSON(encryptedFolderMetadata1);
 
         // de-serialize
-        EncryptedFolderMetadata encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
-                                                                           new TypeToken<EncryptedFolderMetadata>() {
-                                                                           });
+        EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson,
+                                                                                 new TypeToken<>() {
+                                                                                 });
 
         // decrypt
-        DecryptedFolderMetadata decryptedFolderMetadata2 = decryptFolderMetaData(
+        DecryptedFolderMetadataFileV1 decryptedFolderMetadata2 = decryptFolderMetaData(
             encryptedFolderMetadata2,
             privateKey,
             arbitraryDataProvider,
@@ -562,19 +636,19 @@ public class EncryptionTestIT extends AbstractIT {
         assertNull(decryptedFolderMetadata2.getFiledrop());
     }
 
-    private void addFile(DecryptedFolderMetadata decryptedFolderMetadata, int counter) {
+    private void addFile(DecryptedFolderMetadataFileV1 decryptedFolderMetadata, int counter) {
         // Add new file
         // Always generate new
         byte[] key = generateKey();
         byte[] iv = randomBytes(ivLength);
         byte[] authTag = randomBytes((128 / 8));
 
-        DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
+        Data data = new Data();
         data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
         data.setFilename(counter + ".txt");
         data.setVersion(1);
 
-        DecryptedFolderMetadata.DecryptedFile file = new DecryptedFolderMetadata.DecryptedFile();
+        DecryptedFile file = new DecryptedFile();
         file.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv));
         file.setEncrypted(data);
         file.setMetadataKey(0);
@@ -636,7 +710,7 @@ public class EncryptionTestIT extends AbstractIT {
 
     @Test
     public void testExcludeGSON() throws Exception {
-        DecryptedFolderMetadata metadata = generateFolderMetadata();
+        DecryptedFolderMetadataFileV1 metadata = generateFolderMetadataV1_1();
 
         String jsonWithKeys = serializeJSON(metadata);
         String jsonWithoutKeys = serializeJSON(metadata, true);
@@ -644,14 +718,28 @@ public class EncryptionTestIT extends AbstractIT {
         assertTrue(jsonWithKeys.contains("metadataKeys"));
         assertFalse(jsonWithoutKeys.contains("metadataKeys"));
     }
+    
+    @Test
+    public void testEqualsSign() {
+        assertEquals("\"===\"", serializeJSON("==="));
+    }
+    
+    @Test
+    public void testBase64() {
+        String originalString = "randomstring123";
+
+        String encodedString = EncryptionUtils.encodeStringToBase64String(originalString);
+        String compare = EncryptionUtils.decodeBase64StringToString(encodedString);
+        assertEquals(originalString, compare);
+    }
 
     @Test
     public void testChecksum() throws Exception {
-        DecryptedFolderMetadata metadata = new DecryptedFolderMetadata();
+        DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1();
         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());
+        metadata.getFiles().put("n9WXAIXO2wRY4R8nXwmo", new DecryptedFile());
+        metadata.getFiles().put("ia7OEEEyXMoRa1QWQk8r", new DecryptedFile());
 
         String encryptedMetadataKey = "GuFPAULudgD49S4+VDFck3LiqQ8sx4zmbrBtdpCSGcT+T0W0z4F5gYQYPlzTG6WOkdW5LJZK/";
         metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
@@ -667,7 +755,7 @@ public class EncryptionTestIT extends AbstractIT {
         String newChecksum = generateChecksum(metadata, newMnemonic);
         assertNotEquals(expectedChecksum, newChecksum);
 
-        metadata.getFiles().put("aeb34yXMoRa1QWQk8r", new DecryptedFolderMetadata.DecryptedFile());
+        metadata.getFiles().put("aeb34yXMoRa1QWQk8r", new DecryptedFile());
 
         newChecksum = generateChecksum(metadata, mnemonic);
         assertNotEquals(expectedChecksum, newChecksum);
@@ -675,8 +763,6 @@ public class EncryptionTestIT extends AbstractIT {
 
     @Test
     public void testAddIdToMigratedIds() {
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(targetContext);
-
         // delete ids
         arbitraryDataProvider.deleteKeyForAccount(user.getAccountName(), EncryptionUtils.MIGRATED_FOLDER_IDS);
 
@@ -685,10 +771,22 @@ public class EncryptionTestIT extends AbstractIT {
 
         assertTrue(isFolderMigrated(id, user, arbitraryDataProvider));
     }
+    
+    // TODO E2E: more tests 
+    
+    // more tests
+    // migrate v1 -> v2
+    // migrate v1 -> v2 with filedrop
+    
+    // migrate v1 -> v1.1
+    // migrate v1 -> v1.1 with filedrop
+    
+    // migrate v1.1 -> v2
+    // migrate v1.1 -> v2 with filedrop
 
 
     // Helper
-    private boolean compareJsonStrings(String expected, String actual) {
+    public static boolean compareJsonStrings(String expected, String actual) {
         JsonParser parser = new JsonParser();
         JsonElement o1 = parser.parse(expected);
         JsonElement o2 = parser.parse(actual);
@@ -702,29 +800,29 @@ public class EncryptionTestIT extends AbstractIT {
         }
     }
 
-    private DecryptedFolderMetadata generateFolderMetadata() throws Exception {
+    private DecryptedFolderMetadataFileV1 generateFolderMetadataV1_1() throws Exception {
         String metadataKey0 = encodeBytesToBase64String(generateKey());
         String metadataKey1 = encodeBytesToBase64String(generateKey());
         String metadataKey2 = encodeBytesToBase64String(generateKey());
         HashMap<Integer, String> metadataKeys = new HashMap<>();
-        metadataKeys.put(0, EncryptionUtils.encryptStringAsymmetric(metadataKey0, cert));
-        metadataKeys.put(1, EncryptionUtils.encryptStringAsymmetric(metadataKey1, cert));
-        metadataKeys.put(2, EncryptionUtils.encryptStringAsymmetric(metadataKey2, cert));
-        DecryptedFolderMetadata.Encrypted encrypted = new DecryptedFolderMetadata.Encrypted();
+        metadataKeys.put(0, EncryptionUtils.encryptStringAsymmetric(metadataKey0, publicKey));
+        metadataKeys.put(1, EncryptionUtils.encryptStringAsymmetric(metadataKey1, publicKey));
+        metadataKeys.put(2, EncryptionUtils.encryptStringAsymmetric(metadataKey2, publicKey));
+        Encrypted encrypted = new Encrypted();
         encrypted.setMetadataKeys(metadataKeys);
 
-        DecryptedFolderMetadata.Metadata metadata1 = new DecryptedFolderMetadata.Metadata();
+        DecryptedMetadata metadata1 = new DecryptedMetadata();
         metadata1.setMetadataKeys(metadataKeys);
-        metadata1.setVersion(1.1);
+        metadata1.setVersion(1);
 
-        HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
+        HashMap<String, DecryptedFile> files = new HashMap<>();
 
-        DecryptedFolderMetadata.Data data1 = new DecryptedFolderMetadata.Data();
+        Data data1 = new Data();
         data1.setKey("WANM0gRv+DhaexIsI0T3Lg==");
         data1.setFilename("test.txt");
         data1.setVersion(1);
 
-        DecryptedFolderMetadata.DecryptedFile file1 = new DecryptedFolderMetadata.DecryptedFile();
+        DecryptedFile file1 = new DecryptedFile();
         file1.setInitializationVector("gKm3n+mJzeY26q4OfuZEqg==");
         file1.setEncrypted(data1);
         file1.setMetadataKey(0);
@@ -732,12 +830,12 @@ public class EncryptionTestIT extends AbstractIT {
 
         files.put("ia7OEEEyXMoRa1QWQk8r", file1);
 
-        DecryptedFolderMetadata.Data data2 = new DecryptedFolderMetadata.Data();
+        Data data2 = new Data();
         data2.setKey("9dfzbIYDt28zTyZfbcll+g==");
         data2.setFilename("test2.txt");
         data2.setVersion(1);
 
-        DecryptedFolderMetadata.DecryptedFile file2 = new DecryptedFolderMetadata.DecryptedFile();
+        DecryptedFile file2 = new DecryptedFile();
         file2.setInitializationVector("hnJLF8uhDvDoFK4ajuvwrg==");
         file2.setEncrypted(data2);
         file2.setMetadataKey(0);
@@ -745,9 +843,10 @@ public class EncryptionTestIT extends AbstractIT {
 
         files.put("n9WXAIXO2wRY4R8nXwmo", file2);
 
-        return new DecryptedFolderMetadata(metadata1, files);
+        return new DecryptedFolderMetadataFileV1(metadata1, files);
     }
 
+
     private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv, byte[] expectedAuthTag)
         throws Exception {
         File file = getFile(fileName);
@@ -757,10 +856,10 @@ public class EncryptionTestIT extends AbstractIT {
 
         File encryptedTempFile = File.createTempFile("file", "tmp");
         FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
-        fileOutputStream.write(encryptedFile.encryptedBytes);
+        fileOutputStream.write(encryptedFile.getEncryptedBytes());
         fileOutputStream.close();
 
-        byte[] authenticationTag = decodeStringToBase64Bytes(encryptedFile.authenticationTag);
+        byte[] authenticationTag = decodeStringToBase64Bytes(encryptedFile.getAuthenticationTag());
 
         // verify authentication tag
         assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));

+ 157 - 0
app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt

@@ -0,0 +1,157 @@
+/*
+ *
+ * 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.utils
+
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser
+import com.owncloud.android.lib.resources.status.E2EVersion
+
+class EncryptionTestUtils {
+    val t1PrivateKey =
+        "MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC1p8eYMFwGoi7geYzEwNbePRLL5LRhorAecFG3zkpLBwSi/QHkU4" +
+            "u4uSegEbHgOfe73eKVOFdfFpw8wd5cvtY+4CzbX8bu+yrC+tFGcJ25/4VQ78Bl4MI0SvOmxDwuZNrg9SWgs9RwialKOsfCEyz0" +
+            "SS8RstGNt5KZKn1e8z7V9X/eORPmOQ5KcIXHlMbAY3m4erBSKvhRZdqy+Dbnc0rZeZaKkoIMJH1OYfVto/ek12iIKF2YStPVzo" +
+            "TgNsFelPDxeA/lltgf6qDVRD+ELydEncPIJwcv52D8ZitoEyEOfjDZW+rvvE02g1ZD1xPkDLpwltAsFCglCKvKBAWuhthFAgMB" +
+            "AAECgf8BN1MLcq+6m8C1tzNvN/UDd9c0rUpexM6D5eC4O+6B7YGidEqIhHVIzUj0e2HUgpRBbURxsvF1FWdIT2gu7dnULtOGWQ" +
+            "xNujJ0kGwXfAnqxh/rACDFb5TS3sJawEExC5yJw14bCEbE/0uBF5uiTU/U9AV7PKHlqAKsS2RtcwPNceB8zDu0hh/Mb/uS7274" +
+            "TsxUllx0WzGZrozO1K6AlOete9rXmmpghpFTNVhxgf0pxe3hrK+tZGSL9di+Wft9eCvSbdG/FzeXgwVqmGtWU7kSB7FqstEEJO" +
+            "4VpOSyEfcXGHTHwdZjrhBUuAcjWE8E0mCKa8htRE52czb3C0f7ZYkCgYEA5eH3vmHEgQjXzSSEtbmDLRq9X9SB7pIAIXHj2UuE" +
+            "OTkLUJ/7xLTHqt82jqZaZzns1RZIJXKZjH85CswQp/py2/qD240KvA/N+ELZaciaV+Wg+m4+iHdi0DyPkaKaBtFG1nsR2GbVWO" +
+            "1OsaTUZTG4D7RCUErU6XVmNPQKSk5uRA0CgYEAykskpX3KKuWq5nxV4vwgPmxz+uAfCtaGhcPEUg764SR+n0ODAvGiEJU7B0Q2" +
+            "oX621pDOQeRfFufiMWfD8ByhErs1HFCmW69YPlR8qamfc8tHG5UM+r3bb49sDEYU4qr1Ji5Zzs4XgfmToKLbWdzzhaW6YxqO7N" +
+            "ntIIh2169PPxkCgYBF2TAWl8xGTLKNcYAlW1XBObO6z24fWBtUDi/mEWz+mheXCtVMAoX8pFAGbgNgBBiy8k8/mZ+QMgPaBQE2" +
+            "mQGXV3oDFsrhM4go298Fpl9HP8126lJz0pqinRQecyKL2cDFYKWedDh1Cb30ehnTGZVMqD/R97rTqMlCY7hQtZ4JbQKBgEXpLD" +
+            "QJQeoLT0GybJgyXA5WuspT1EaRlxH5cwqM5MUUMLJnyYol6cVjXXAIcfzj5tpGVxHMk9Q9tR0v6DY+HqhzjEpJ0QRUl+GKnz6f" +
+            "QVzqPpvYqhCptoFahpPDUIp5XJmiYSUoclVX5F4aikYHJx3kBYMkdYqDUgDxSGkHzBJZAoGAHV44xgTW02dgeB5GfDJVWCJKAU" +
+            "GsYOFuUehKUBXSJ0929hdP0sjOQDJN3DEDISzmgdWX5NyLJxEYgFWNivpePjWCWzOzyua3nPSpvxPIUB7xh27gjT91glj1hEmy" +
+            "sCd7+9yoMPiCXR7iigRycxegI/Krd39QzISSk9O0finfytU="
+
+    val t1PublicKey = """-----BEGIN CERTIFICATE-----
+MIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe
+Fw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S
+y+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs
+21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv
+EbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW
+ipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D
+yCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID
+AQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw
+FoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG
+9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O
+AvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv
+q61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+
+kHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk
+4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw
+t9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg==
+-----END CERTIFICATE-----"""
+
+    val johnPrivateKey =
+        """MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDuPcSvlhqElPQsCdJEuGmptGj4TUBWe33yu+ncOYR8Ec3M0H4NL0gE
+            |ORJJcz9i18ByLpNzDy6NUGOtlf9YSat/zKdAfFiZJolKc/y4BPfTr8xx5ml2mu4Rz39LXRru+nnhluV3g1h2Z9LvWhUVUqAztz9W2H
+            |H6uC7jx+7HNtYC9VgsVzHjuHPQMlOePPZlr9Hry5enF/Psn24RdiKqwCz8WhsOwtmW5PdHLLBVHAoF53URnFR4sgmLLGlS2GEZ8hvx
+            |vdV/2NmhRWLebmCZziyklAe9gCR9lgfN32tqzyMG7VptBHFy7YJidWjpjSZPGEqFBL+fmCO/cTGJAXfCn9djAgMBAAECggEAV2QBCg
+            |edopShHKZdoyeiWsX621o7B341LR0RI99VYc2GGGNCWcPGPwZQVvEXh0JtLXU4UTR4dw3OApbLG6+qYS7JCzaRqVwhcFYrlbT804Hh
+            |FMbYWNFsEsxyfUqh3peyrbWUZsqfYI+lKHd61F+CtHW7nje3V6jISnXEeP78cgioKOX8gsCG8DEWsmaLrQz0PyMwdhucRfa8Bm6qeX
+            |NY+wCMg8lyH/+OLlyCZTdkaWbTBBD5UXGbZly8iX17McmsYhdjFyx1l0NQnVMAYjOpXXEkeEixZpSfm3GYxmdaQqZFkpbI/FbQF0yD
+            |7hLrGwiRTDcyPUz+QypUv8CZxpXbgQKBgQD3btuYmb+BpPZjryfa3worv/3XQCTs08V0TX3mDxHVQL95TgP+L8/Z/brxIMBNpwG1wk
+            |iCWLYLer68+qioMTohuzeUx7hRKcoHa9ezW8m7m9AcPmAnzNticPYv835BQjEu/avU98rwIDihsYgxcjU3L7/P2ajVgUDigQxmE3gO
+            |OwKBgQD2fXBLwch0P5g2GCyOPYvSgyF/umS7mcyUVTE4WOoJNDf8Q+Bx1dA2yAKQaoVghqW4uCfOAo/rERvGAYZ7fm7nFwx1jZ8ToT
+            |dKZwybIFPjF/zkfuZLajYxVOPnzuQrsXnjcGg/ltMKZg3NqnGQGnD1S3eOhZ+dIOBmb7+jSO4A+QKBgASqnpGeNLJpPgxbPVEva62v
+            |jUYF+6xLwimTXJB+MEPpWLMc+Y5NsInX8zKg/393atzWsS9kJOrKgdZmk8+4PfRs53ty2NMPCrRhIExNqtxS7/XYZ0/Y2TpeDwaQfQ
+            |0WBn9wYVE+6yDkOq0x//OOx9ommGN/I2QDcAnVjTpPm7AJAoGAYT8cDsdlTnfIlY70BSpC/8q8bKgdFeaXz+3MfW6W5wqzC9O7uS2h
+            |9/rxCAj+lhaJS1dcXOql3Rfi3Tu80vwOxR1SzQ4StKvmJHSDhLA8aFwOahemxBojR1M2lz4IxzQ94n12o5/dozygNYQJSdEkv6IGiT
+            |QuxM8zuTZdZQ5g2AECgYAujetfkwgVW7/gumpMKytoY0VuTzF4Y/XZfqBMVIiPIuUl57JbDzrcx6YVXX3PavxNWmBLBmMq3SHMbdva
+            |H7LnU/8rvkT8xRVLg/w/bRJc3Lb3oUjrdhkUQUYDoOfMoFA+ceZ2L6bnSXwm86KKV+xoXWpxAoL4AvdNrMhoWw3+yg=="""
+            .trimMargin()
+
+    val johnPublicKey = """-----BEGIN CERTIFICATE-----
+MIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJERTEb
+MBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx
+EjAQBgNVBAoMCU5leHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0
+NTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRFMRswGQYDVQQIDBJCYWRl
+bi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4
+dGNsb3VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E1AVnt98rvp3DmEfBHNzNB+DS9IBDkS
+SXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc9/
+S10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT
+njz2Za/R68uXpxfz7J9uEXYiqsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp
+UthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1abQRxcu2CYnVo
+6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB
+tv70fTGkXWGAueDp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw
+DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAyVtq9XAvW7nxSW/8
+hp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi
+6dg/7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB
+Mz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid
+vigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8B8mh
+UtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P
+nDO4ew==
+-----END CERTIFICATE-----"""
+
+    @Throws(java.lang.Exception::class)
+    fun generateFolderMetadataV2(userId: String, cert: String): DecryptedFolderMetadataFile {
+        val metadata = DecryptedMetadata().apply {
+            metadataKey = EncryptionUtils.generateKey()
+            keyChecksums.add(EncryptionUtilsV2().hashMetadataKey(metadataKey))
+        }
+
+        val file1 = DecryptedFile(
+            "image1.png",
+            "image/png",
+            "gKm3n+mJzeY26q4OfuZEqg==",
+            "PboI9tqHHX3QeAA22PIu4w==",
+            "WANM0gRv+DhaexIsI0T3Lg=="
+        )
+
+        val file2 = DecryptedFile(
+            "image2.png",
+            "image/png",
+            "hnJLF8uhDvDoFK4ajuvwrg==",
+            "qOQZdu5soFO77Y7y4rAOVA==",
+            "9dfzbIYDt28zTyZfbcll+g=="
+        )
+
+        val users = mutableListOf(
+            DecryptedUser(userId, cert)
+        )
+
+        // val filedrop = mutableMapOf(
+        //     Pair(
+        //         "eie8iaeiaes8e87td6",
+        //         DecryptedFile(
+        //             "test2.txt",
+        //             "txt/plain",
+        //             "hnJLF8uhDvDoFK4ajuvwrg==",
+        //             "qOQZdu5soFO77Y7y4rAOVA==",
+        //             "9dfzbIYDt28zTyZfbcll+g=="
+        //         )
+        //     )
+        // )
+
+        metadata.files["ia7OEEEyXMoRa1QWQk8r"] = file1
+        metadata.files["n9WXAIXO2wRY4R8nXwmo"] = file2
+
+        return DecryptedFolderMetadataFile(metadata, users, mutableMapOf(), E2EVersion.V2_0.value)
+    }
+}

+ 47 - 0
app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt

@@ -0,0 +1,47 @@
+/*
+ *
+ * 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.utils
+
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
+import com.owncloud.android.lib.resources.e2ee.CsrHelper
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class EncryptionUtilsIT : AbstractIT() {
+    @Throws(
+        java.security.NoSuchAlgorithmException::class,
+        java.io.IOException::class,
+        org.bouncycastle.operator.OperatorCreationException::class
+    )
+    @Test
+    fun saveAndRestorePublicKey() {
+        val arbitraryDataProvider = ArbitraryDataProviderImpl(targetContext)
+        val keyPair = EncryptionUtils.generateKeyPair()
+        val e2eUser = "e2e-user"
+        val key = CsrHelper().generateCsrPemEncodedString(keyPair, e2eUser)
+
+        EncryptionUtils.savePublicKey(user, key, e2eUser, arbitraryDataProvider)
+
+        assertEquals(key, EncryptionUtils.getPublicKey(user, e2eUser, arbitraryDataProvider))
+    }
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 665 - 0
app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt


+ 2 - 1
app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt

@@ -69,7 +69,8 @@ import com.owncloud.android.db.ProviderMeta
         AutoMigration(from = 72, to = 73),
         AutoMigration(from = 73, to = 74, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
         AutoMigration(from = 74, to = 75),
-        AutoMigration(from = 75, to = 76)
+        AutoMigration(from = 75, to = 76),
+        AutoMigration(from = 76, to = 77)
     ],
     exportSchema = true
 )

+ 2 - 0
app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt

@@ -98,6 +98,8 @@ data class CapabilityEntity(
     val endToEndEncryption: Int?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST)
     val endToEndEncryptionKeysExist: Int?,
+    @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION)
+    val endToEndEncryptionApiVersion: String?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_ACTIVITY)
     val activity: Int?,
     @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT)

+ 3 - 1
app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt

@@ -128,5 +128,7 @@ data class FileEntity(
     @ColumnInfo(name = ProviderTableMeta.FILE_TAGS)
     val tags: String?,
     @ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS)
-    val metadataGPS: String?
+    val metadataGPS: String?,
+    @ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER)
+    val e2eCounter: Long?
 )

+ 8 - 0
app/src/main/java/com/nextcloud/client/di/AppModule.java

@@ -54,6 +54,7 @@ import com.nextcloud.client.notifications.AppNotificationManager;
 import com.nextcloud.client.notifications.AppNotificationManagerImpl;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.utils.Throttler;
+import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
 import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
@@ -261,4 +262,11 @@ class AppModule {
     PassCodeManager passCodeManager(AppPreferences preferences, Clock clock) {
         return new PassCodeManager(preferences, clock);
     }
+
+    @Provides
+    @Singleton
+    UsersAndGroupsSearchConfig userAndGroupSearchConfig() {
+        return new UsersAndGroupsSearchConfig();
+    }
+
 }

+ 2 - 0
app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt

@@ -32,6 +32,7 @@ interface ArbitraryDataProvider {
     fun incrementValue(accountName: String, key: String)
     fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: Boolean)
     fun storeOrUpdateKeyValue(accountName: String, key: String, newValue: String)
+    fun storeOrUpdateKeyValue(user: User, key: String, newValue: String)
 
     fun getLongValue(accountName: String, key: String): Long
     fun getLongValue(user: User, key: String): Long
@@ -45,6 +46,7 @@ interface ArbitraryDataProvider {
         const val DIRECT_EDITING = "DIRECT_EDITING"
         const val DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG"
         const val PREDEFINED_STATUS = "PREDEFINED_STATUS"
+        const val PUBLIC_KEY = "PUBLIC_KEY_"
         const val E2E_ERRORS = "E2E_ERRORS"
         const val E2E_ERRORS_TIMESTAMP = "E2E_ERRORS_TIMESTAMP"
     }

+ 7 - 0
app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java

@@ -91,6 +91,13 @@ public class ArbitraryDataProviderImpl implements ArbitraryDataProvider {
         }
     }
 
+    @Override
+    public void storeOrUpdateKeyValue(@NonNull User user,
+                                      @NonNull String key,
+                                      @NonNull String newValue) {
+        storeOrUpdateKeyValue(user.getAccountName(), key, newValue);
+    }
+
     @Override
     public long getLongValue(@NonNull String accountName, @NonNull String key) {
         String value = getValue(accountName, key);

+ 3 - 3
app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java → app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java

@@ -29,18 +29,18 @@ import androidx.annotation.VisibleForTesting;
 /**
  * Decrypted class representation of metadata json of folder metadata.
  */
-public class DecryptedFolderMetadata {
+public class DecryptedFolderMetadataOld {
     private Metadata metadata;
     private Map<String, DecryptedFile> files;
 
     private Map<String, DecryptedFile> filedrop;
 
-    public DecryptedFolderMetadata() {
+    public DecryptedFolderMetadataOld() {
         this.metadata = new Metadata();
         this.files = new HashMap<>();
     }
 
-    public DecryptedFolderMetadata(Metadata metadata, Map<String, DecryptedFile> files) {
+    public DecryptedFolderMetadataOld(Metadata metadata, Map<String, DecryptedFile> files) {
         this.metadata = metadata;
         this.files = files;
     }

+ 15 - 0
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -58,6 +58,7 @@ import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.shares.ShareeUser;
 import com.owncloud.android.lib.resources.status.CapabilityBooleanType;
+import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.operations.RemoteOperationFailedException;
 import com.owncloud.android.utils.FileStorageUtils;
@@ -556,6 +557,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_METADATA_SIZE, gson.toJson(file.getImageDimension()));
         cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation()));
         cv.put(ProviderTableMeta.FILE_METADATA_LIVE_PHOTO, file.getLinkedFileIdForLivePhoto());
+        cv.put(ProviderTableMeta.FILE_E2E_COUNTER, file.getE2eCounter());
 
         return cv;
     }
@@ -988,6 +990,7 @@ public class FileDataStorageManager {
         ocFile.setLockToken(fileEntity.getLockToken());
         ocFile.setLivePhoto(fileEntity.getMetadataLivePhoto());
         ocFile.setHidden(nullToZero(fileEntity.getHidden()) == 1);
+        ocFile.setE2eCounter(fileEntity.getE2eCounter());
 
         String sharees = fileEntity.getSharees();
         // Surprisingly JSON deserialization causes significant overhead.
@@ -1974,6 +1977,8 @@ public class FileDataStorageManager {
                           capability.getEndToEndEncryption().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST,
                           capability.getEndToEndEncryptionKeysExist().getValue());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION,
+                          capability.getEndToEndEncryptionApiVersion().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT,
                           capability.getServerBackgroundDefault().getValue());
         contentValues.put(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_PLAIN,
@@ -2127,6 +2132,16 @@ public class FileDataStorageManager {
                 getBoolean(cursor,
                            ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST)
                                                      );
+
+            String e2eVersionString = getString(cursor, ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION);
+            E2EVersion e2EVersion;
+            if (e2eVersionString == null) {
+                e2EVersion = E2EVersion.UNKNOWN;
+            } else {
+                e2EVersion = E2EVersion.fromValue(e2eVersionString);
+            }
+            capability.setEndToEndEncryptionApiVersion(e2EVersion);
+
             capability.setServerBackgroundDefault(
                 getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_DEFAULT));
             capability.setServerBackgroundPlain(getBoolean(cursor,

+ 12 - 0
app/src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -121,6 +121,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
     private String lockToken;
     @Nullable
     private ImageDimension imageDimension;
+    private long e2eCounter = -1;
     @Nullable
     private GeoLocation geolocation;
     private List<String> tags = new ArrayList<>();
@@ -1056,4 +1057,15 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         this.tags = tags;
     }
 
+    public long getE2eCounter() {
+        return e2eCounter;
+    }
+
+    public void setE2eCounter(@Nullable Long e2eCounter) {
+        if (e2eCounter == null) {
+            this.e2eCounter = -1;
+        } else {
+            this.e2eCounter = e2eCounter;
+        }
+    }
 }

+ 62 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java

@@ -0,0 +1,62 @@
+/*
+ *
+ * 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.e2e.v1.decrypted;
+
+public class Data {
+    private String filename;
+    private String mimetype;
+    private String key;
+    private double version;
+
+    public String getKey() {
+        return this.key;
+    }
+
+    public String getFilename() {
+        return this.filename;
+    }
+
+    public String getMimetype() {
+        return this.mimetype;
+    }
+
+    public double getVersion() {
+        return this.version;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public void setFilename(String filename) {
+        this.filename = filename;
+    }
+
+    public void setMimetype(String mimetype) {
+        this.mimetype = mimetype;
+    }
+
+    public void setVersion(double version) {
+        this.version = version;
+    }
+}

+ 62 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java

@@ -0,0 +1,62 @@
+/*
+ *
+ * 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.e2e.v1.decrypted;
+
+public class DecryptedFile {
+    private Data encrypted;
+    private String initializationVector;
+    private String authenticationTag;
+    private int metadataKey;
+
+    public Data getEncrypted() {
+        return this.encrypted;
+    }
+
+    public String getInitializationVector() {
+        return this.initializationVector;
+    }
+
+    public String getAuthenticationTag() {
+        return this.authenticationTag;
+    }
+
+    public int getMetadataKey() {
+        return this.metadataKey;
+    }
+
+    public void setEncrypted(Data encrypted) {
+        this.encrypted = encrypted;
+    }
+
+    public void setInitializationVector(String initializationVector) {
+        this.initializationVector = initializationVector;
+    }
+
+    public void setAuthenticationTag(String authenticationTag) {
+        this.authenticationTag = authenticationTag;
+    }
+
+    public void setMetadataKey(int metadataKey) {
+        this.metadataKey = metadataKey;
+    }
+}

+ 73 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java

@@ -0,0 +1,73 @@
+/*
+ *
+ * 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.e2e.v1.decrypted;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Decrypted class representation of metadata json of folder metadata.
+ */
+public class DecryptedFolderMetadataFileV1 {
+    private DecryptedMetadata metadata;
+    private Map<String, DecryptedFile> files;
+    private Map<String, DecryptedFile> filedrop;
+
+    public DecryptedFolderMetadataFileV1() {
+        this.metadata = new DecryptedMetadata();
+        this.files = new HashMap<>();
+    }
+
+    public DecryptedFolderMetadataFileV1(DecryptedMetadata metadata, Map<String, DecryptedFile> files) {
+        this.metadata = metadata;
+        this.files = files;
+    }
+
+    public DecryptedMetadata getMetadata() {
+        return this.metadata;
+    }
+
+    public Map<String, DecryptedFile> getFiles() {
+        return this.files;
+    }
+
+    public void setMetadata(DecryptedMetadata metadata) {
+        this.metadata = metadata;
+    }
+
+    public void setFiles(Map<String, DecryptedFile> files) {
+        this.files = files;
+    }
+
+    @VisibleForTesting
+    public void setFiledrop(Map<String, DecryptedFile> filedrop) {
+        this.filedrop = filedrop;
+    }
+
+    public Map<String, DecryptedFile> getFiledrop() {
+        return filedrop;
+    }
+
+}

+ 74 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java

@@ -0,0 +1,74 @@
+/*
+ *
+ * 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.e2e.v1.decrypted;
+
+import java.util.Map;
+
+public class DecryptedMetadata {
+    transient
+    private Map<Integer, String> metadataKeys; // outdated with v1.1
+    private String metadataKey;
+    private String checksum;
+    private double version;
+
+    @Override
+    public String toString() {
+        return String.valueOf(version);
+    }
+
+    public Map<Integer, String> getMetadataKeys() {
+        return this.metadataKeys;
+    }
+
+    public String getMetadataKey() {
+        if (metadataKey == null) {
+            // fallback to old keys array
+            return metadataKeys.get(0);
+        }
+        return metadataKey;
+    }
+
+    public double getVersion() {
+        return this.version;
+    }
+
+    public void setMetadataKeys(Map<Integer, String> metadataKeys) {
+        this.metadataKeys = metadataKeys;
+    }
+
+    public void setMetadataKey(String metadataKey) {
+        this.metadataKey = metadataKey;
+    }
+
+    public void setVersion(double version) {
+        this.version = version;
+    }
+
+    public String getChecksum() {
+        return checksum;
+    }
+
+    public void setChecksum(String checksum) {
+        this.checksum = checksum;
+    }
+}

+ 37 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java

@@ -0,0 +1,37 @@
+/*
+ *
+ * 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.e2e.v1.decrypted;
+
+import java.util.Map;
+
+public class Encrypted {
+    private Map<Integer, String> metadataKeys;
+
+    public Map<Integer, String> getMetadataKeys() {
+        return this.metadataKeys;
+    }
+
+    public void setMetadataKeys(Map<Integer, String> metadataKeys) {
+        this.metadataKeys = metadataKeys;
+    }
+}

+ 46 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java

@@ -0,0 +1,46 @@
+/*
+ *
+ * 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.e2e.v1.decrypted;
+
+import java.util.Map;
+
+public 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;
+    }
+}

+ 24 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt

@@ -0,0 +1,24 @@
+/*
+ *
+ * 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.e2e.v1.encrypted
+
+class EncryptedFile(var encryptedBytes: ByteArray, var authenticationTag: String)

+ 16 - 12
app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java → app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java

@@ -1,14 +1,15 @@
 /*
+ *
  * Nextcloud Android client application
  *
  * @author Tobias Kaminsky
- * Copyright (C) 2017 Tobias Kaminsky
- * Copyright (C) 2017 Nextcloud GmbH.
+ * 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.
+ * (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
@@ -16,31 +17,34 @@
  * GNU Affero General Public License for more details.
  *
  * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
-package com.owncloud.android.datamodel;
+package com.owncloud.android.datamodel.e2e.v1.encrypted;
+
+import com.owncloud.android.datamodel.EncryptedFiledrop;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
 
 import java.util.Map;
 
 /**
  * Encrypted class representation of metadata json of folder metadata
  */
-public class EncryptedFolderMetadata {
-    private DecryptedFolderMetadata.Metadata metadata;
+public class EncryptedFolderMetadataFileV1 {
+    private DecryptedMetadata metadata;
     private Map<String, EncryptedFile> files;
 
     private Map<String, EncryptedFiledrop> filedrop;
 
-    public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata,
-                                   Map<String, EncryptedFile> files,
-                                   Map<String, EncryptedFiledrop> filesdrop) {
+    public EncryptedFolderMetadataFileV1(DecryptedMetadata metadata,
+                                         Map<String, EncryptedFile> files,
+                                         Map<String, EncryptedFiledrop> filesdrop) {
         this.metadata = metadata;
         this.files = files;
         this.filedrop = filesdrop;
     }
 
-    public DecryptedFolderMetadata.Metadata getMetadata() {
+    public DecryptedMetadata getMetadata() {
         return this.metadata;
     }
 
@@ -52,7 +56,7 @@ public class EncryptedFolderMetadata {
         return filedrop;
     }
 
-    public void setMetadata(DecryptedFolderMetadata.Metadata metadata) {
+    public void setMetadata(DecryptedMetadata metadata) {
         this.metadata = metadata;
     }
 

+ 30 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt

@@ -0,0 +1,30 @@
+/*
+ *
+ * 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.e2e.v2.decrypted
+
+data class DecryptedFile(
+    var filename: String,
+    val mimetype: String,
+    val nonce: String,
+    val authenticationTag: String,
+    val key: String
+)

+ 33 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt

@@ -0,0 +1,33 @@
+/*
+ *
+ * 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.e2e.v2.decrypted
+
+/**
+ * Decrypted class representation of metadata json of folder metadata.
+ */
+data class DecryptedFolderMetadataFile(
+    val metadata: DecryptedMetadata,
+    var users: MutableList<DecryptedUser> = mutableListOf(),
+    @Transient
+    val filedrop: MutableMap<String, DecryptedFile> = HashMap(),
+    val version: String = "2.0"
+)

+ 59 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt

@@ -0,0 +1,59 @@
+/*
+ *
+ * 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.e2e.v2.decrypted
+
+import com.owncloud.android.utils.EncryptionUtils
+
+data class DecryptedMetadata(
+    val keyChecksums: MutableList<String> = mutableListOf(),
+    val deleted: Boolean = false,
+    var counter: Long = 0,
+    val folders: MutableMap<String, String> = mutableMapOf(),
+    val files: MutableMap<String, DecryptedFile> = mutableMapOf(),
+    @Transient
+    var metadataKey: ByteArray = EncryptionUtils.generateKey()
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as DecryptedMetadata
+
+        if (keyChecksums != other.keyChecksums) return false
+        if (deleted != other.deleted) return false
+        if (counter != other.counter) return false
+        if (folders != other.folders) return false
+        if (files != other.files) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = keyChecksums.hashCode()
+        result = 31 * result + deleted.hashCode()
+        result = 31 * result + counter.hashCode()
+        result = 31 * result + folders.hashCode()
+        result = 31 * result + files.hashCode()
+        return result
+    }
+}

+ 28 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt

@@ -0,0 +1,28 @@
+/*
+ *
+ * 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.e2e.v2.decrypted
+
+data class DecryptedUser(
+    val userId: String,
+    val certificate: String
+)

+ 29 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * 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.e2e.v2.encrypted
+
+data class EncryptedFiledrop(
+    val ciphertext: String,
+    val nonce: String,
+    val authenticationTag: String,
+    val users: List<EncryptedFiledropUser>
+)

+ 28 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt

@@ -0,0 +1,28 @@
+/*
+ *
+ * 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.e2e.v2.encrypted
+
+data class EncryptedFiledropUser(
+    val userId: String,
+    val encryptedFiledropKey: String
+)

+ 32 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt

@@ -0,0 +1,32 @@
+/*
+ *
+ * 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.e2e.v2.encrypted
+
+/**
+ * Decrypted class representation of metadata json of folder metadata.
+ */
+data class EncryptedFolderMetadataFile(
+    val metadata: EncryptedMetadata,
+    val users: List<EncryptedUser>,
+    val filedrop: MutableMap<String, EncryptedFiledrop>?,
+    val version: String = "2.0"
+)

+ 29 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * 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.e2e.v2.encrypted
+
+data class EncryptedMetadata(
+    val ciphertext: String,
+    val nonce: String,
+    val authenticationTag: String
+)

+ 29 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * 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.e2e.v2.encrypted
+
+data class EncryptedUser(
+    val userId: String,
+    val certificate: String,
+    val encryptedMetadataKey: String
+)

+ 28 - 0
app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt

@@ -0,0 +1,28 @@
+/*
+ *
+ * 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.e2e.v2.encrypted
+
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile
+
+class FiledropData {
+    private val files: Map<String, DecryptedFile> = mutableMapOf()
+}

+ 4 - 1
app/src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -35,7 +35,7 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 76;
+    public static final int DB_VERSION = 77;
 
     private ProviderMeta() {
         // No instance
@@ -129,6 +129,7 @@ public class ProviderMeta {
         public static final String FILE_LOCK_TIMEOUT = "lock_timeout";
         public static final String FILE_LOCK_TOKEN = "lock_token";
         public static final String FILE_TAGS = "tags";
+        public static final String FILE_E2E_COUNTER = "e2e_counter";
 
         public static final List<String> FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList(
                 _ID,
@@ -178,6 +179,7 @@ public class ProviderMeta {
                 FILE_LOCK_TOKEN,
                 FILE_METADATA_SIZE,
                 FILE_METADATA_LIVE_PHOTO,
+                FILE_E2E_COUNTER,
                 FILE_TAGS,
                 FILE_METADATA_GPS));
         public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc";
@@ -248,6 +250,7 @@ public class ProviderMeta {
         public static final String CAPABILITIES_SERVER_BACKGROUND_PLAIN = "background_plain";
         public static final String CAPABILITIES_END_TO_END_ENCRYPTION = "end_to_end_encryption";
         public static final String CAPABILITIES_END_TO_END_ENCRYPTION_KEYS_EXIST = "end_to_end_encryption_keys_exist";
+        public static final String CAPABILITIES_END_TO_END_ENCRYPTION_API_VERSION = "end_to_end_encryption_api_version";
         public static final String CAPABILITIES_ACTIVITY = "activity";
         public static final String CAPABILITIES_RICHDOCUMENT = "richdocument";
         public static final String CAPABILITIES_RICHDOCUMENT_MIMETYPE_LIST = "richdocument_mimetype_list";

+ 25 - 3
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -186,7 +186,7 @@ public class FileMenuFilter {
 
 
     private void filterShareFile(List<Integer> toHide, OCCapability capability) {
-        if (!isSingleSelection() || containsEncryptedFile() ||
+        if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() ||
             (!isShareViaLinkAllowed() && !isShareWithUsersAllowed()) ||
             !isShareApiEnabled(capability) || !files.iterator().next().canReshare()) {
             toHide.add(R.id.action_send_share_file);
@@ -220,7 +220,11 @@ public class FileMenuFilter {
     }
 
     private void filterLock(List<Integer> toHide, boolean fileLockingEnabled) {
-        if (files.isEmpty() || !isSingleSelection() || !fileLockingEnabled) {
+        if (files.isEmpty() ||
+            !isSingleSelection() ||
+            !fileLockingEnabled ||
+            containsEncryptedFile() ||
+            containsEncryptedFolder()) {
             toHide.add(R.id.action_lock_file);
         } else {
             OCFile file = files.iterator().next();
@@ -340,7 +344,7 @@ public class FileMenuFilter {
 
     private void filterRemove(List<Integer> toHide, boolean synchronizing) {
         if (files.isEmpty() || synchronizing || containsLockedFile()
-            || containsEncryptedFolder() || containsEncryptedFile()) {
+            || containsEncryptedFolder() || isFolderAndContainsEncryptedFile()) {
             toHide.add(R.id.action_remove_file);
         }
     }
@@ -485,6 +489,24 @@ public class FileMenuFilter {
         return isSingleSelection() && (MimeTypeUtil.isVideo(file) || MimeTypeUtil.isAudio(file));
     }
 
+    private boolean isFolderAndContainsEncryptedFile() {
+        for (OCFile file : files) {
+            if (!file.isFolder()) {
+                continue;
+            }
+            if (file.isFolder()) {
+                List<OCFile> children = storageManager.getFolderContent(file, false);
+                for (OCFile child : children) {
+                    if (child.isEncrypted()) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+
     private boolean containsEncryptedFile() {
         for (OCFile file : files) {
             if (!file.isFolder() && file.isEncrypted()) {

+ 228 - 35
app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java

@@ -27,10 +27,13 @@ import android.util.Pair;
 import com.nextcloud.client.account.User;
 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.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.Data;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
@@ -40,8 +43,10 @@ import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
 import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation;
 import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation;
 import com.owncloud.android.lib.resources.files.model.RemoteFile;
+import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.operations.common.SyncOperation;
 import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.EncryptionUtilsV2;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeType;
 
@@ -54,8 +59,8 @@ import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
 import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
 
 /**
- * Access to remote operation performing the creation of a new folder in the ownCloud server.
- * Save the new folder in Database.
+ * Access to remote operation performing the creation of a new folder in the ownCloud server. Save the new folder in
+ * Database.
  */
 public class CreateFolderOperation extends SyncOperation implements OnRemoteOperationListener {
 
@@ -100,20 +105,28 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
         boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(parent, getStorageManager());
 
         if (encryptedAncestor) {
-            return encryptedCreate(parent, client);
+            E2EVersion e2EVersion = getStorageManager().getCapability(user).getEndToEndEncryptionApiVersion();
+            if (e2EVersion == E2EVersion.V1_0 ||
+                e2EVersion == E2EVersion.V1_1 ||
+                e2EVersion == E2EVersion.V1_2) {
+                return encryptedCreateV1(parent, client);
+            } else if (e2EVersion == E2EVersion.V2_0) {
+                return encryptedCreateV2(parent, client);
+            }
+            return new RemoteOperationResult(new IllegalStateException("E2E not supported"));
         } else {
             return normalCreate(client);
         }
     }
 
-    private RemoteOperationResult encryptedCreate(OCFile parent, OwnCloudClient client) {
+    private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient client) {
         ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
         String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
         String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
 
         String token = null;
         Boolean metadataExists;
-        DecryptedFolderMetadata metadata;
+        DecryptedFolderMetadataFileV1 metadata;
         String encryptedRemotePath = null;
 
         String filename = new File(remotePath).getName();
@@ -123,12 +136,13 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
             token = EncryptionUtils.lockFolder(parent, client);
 
             // get metadata
-            Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(parent,
-                                                                                                   client,
-                                                                                                   privateKey,
-                                                                                                   publicKey,
-                                                                                                   arbitraryDataProvider,
-                                                                                                   user);
+            Pair<Boolean, DecryptedFolderMetadataFileV1> metadataPair = EncryptionUtils.retrieveMetadataV1(parent,
+                                                                                                           client,
+                                                                                                           privateKey,
+                                                                                                           publicKey,
+                                                                                                           arbitraryDataProvider,
+                                                                                                           user
+                                                                                                          );
 
             metadataExists = metadataPair.first;
             metadata = metadataPair.second;
@@ -142,20 +156,21 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
             String encryptedFileName = createRandomFileName(metadata);
             encryptedRemotePath = parent.getRemotePath() + encryptedFileName;
 
-            RemoteOperationResult result = new CreateFolderRemoteOperation(encryptedRemotePath,
-                                                                           true,
-                                                                           token)
+            RemoteOperationResult<String> result = new CreateFolderRemoteOperation(encryptedRemotePath,
+                                                                                   true,
+                                                                                   token)
                 .execute(client);
 
             if (result.isSuccess()) {
                 // update metadata
                 metadata.getFiles().put(encryptedFileName, createDecryptedFile(filename));
 
-                EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
-                                                                                                        publicKey,
-                                                                                                        arbitraryDataProvider,
-                                                                                                        user,
-                                                                                                        parent.getLocalId());
+                EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
+                                                                                                              publicKey,
+                                                                                                              parent.getLocalId(),
+                                                                                                              user,
+                                                                                                              arbitraryDataProvider
+                                                                                                             );
                 String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
 
                 // upload metadata
@@ -164,17 +179,19 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
                                                token,
                                                client,
                                                metadataExists,
+                                               E2EVersion.V1_2,
+                                               "",
                                                arbitraryDataProvider,
                                                user);
 
                 // unlock folder
                 if (token != null) {
-                    RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token);
+                    RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolderV1(parent, client, token);
 
                     if (unlockFolderResult.isSuccess()) {
                         token = null;
                     } else {
-                        // TODO do better
+                        // TODO E2E: do better
                         throw new RuntimeException("Could not unlock folder!");
                     }
                 }
@@ -202,6 +219,146 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
 
             return result;
         } catch (Exception e) {
+            if (!EncryptionUtils.unlockFolderV1(parent, client, token).isSuccess()) {
+                throw new RuntimeException("Could not clean up after failing folder creation!", e);
+            }
+
+            // remove folder
+            if (encryptedRemotePath != null) {
+                RemoteOperationResult removeResult = new RemoveRemoteEncryptedFileOperation(encryptedRemotePath,
+                                                                                            user,
+                                                                                            context,
+                                                                                            filename,
+                                                                                            parent,
+                                                                                            true
+                ).execute(client);
+
+                if (!removeResult.isSuccess()) {
+                    throw new RuntimeException("Could not clean up after failing folder creation!");
+                }
+            }
+
+            // TODO E2E: do better
+            return new RemoteOperationResult(e);
+        } finally {
+            // unlock folder
+            if (token != null) {
+                RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolderV1(parent, client, token);
+
+                if (!unlockFolderResult.isSuccess()) {
+                    // TODO E2E: do better
+                    throw new RuntimeException("Could not unlock folder!");
+                }
+            }
+        }
+    }
+
+    private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient client) {
+        String token = null;
+        Boolean metadataExists;
+        DecryptedFolderMetadataFile metadata;
+        String encryptedRemotePath = null;
+
+        String filename = new File(remotePath).getName();
+
+        try {
+            // lock folder
+            token = EncryptionUtils.lockFolder(parent, client);
+
+            // get metadata
+            EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
+            kotlin.Pair<Boolean, DecryptedFolderMetadataFile> metadataPair = encryptionUtilsV2.retrieveMetadata(parent,
+                                                                                                                client,
+                                                                                                                user,
+                                                                                                                context);
+
+            metadataExists = metadataPair.getFirst();
+            metadata = metadataPair.getSecond();
+
+            // check if filename already exists
+            if (isFileExisting(metadata, filename)) {
+                return new RemoteOperationResult(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS);
+            }
+
+            // generate new random file name, check if it exists in metadata
+            String encryptedFileName = createRandomFileName(metadata);
+            encryptedRemotePath = parent.getRemotePath() + encryptedFileName;
+
+            RemoteOperationResult<String> result = new CreateFolderRemoteOperation(encryptedRemotePath,
+                                                                                   true,
+                                                                                   token)
+                .execute(client);
+
+            String remoteId = result.getResultData();
+
+            if (result.isSuccess()) {
+                DecryptedFolderMetadataFile subFolderMetadata = encryptionUtilsV2.createDecryptedFolderMetadataFile();
+
+                // upload metadata
+                encryptionUtilsV2.serializeAndUploadMetadata(remoteId,
+                                                             subFolderMetadata,
+                                                             token,
+                                                             client,
+                                                             false,
+                                                             context,
+                                                             user,
+                                                             parent,
+                                                             getStorageManager());
+            }
+
+            if (result.isSuccess()) {
+                // update metadata
+                DecryptedFolderMetadataFile updatedMetadataFile = encryptionUtilsV2.addFolderToMetadata(encryptedFileName,
+                                                                                                        filename,
+                                                                                                        metadata,
+                                                                                                        parent,
+                                                                                                        getStorageManager());
+
+                // upload metadata
+                encryptionUtilsV2.serializeAndUploadMetadata(parent,
+                                                             updatedMetadataFile,
+                                                             token,
+                                                             client,
+                                                             metadataExists,
+                                                             context,
+                                                             user,
+                                                             getStorageManager());
+
+                // unlock folder
+                RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token);
+
+                if (unlockFolderResult.isSuccess()) {
+                    token = null;
+                } else {
+                    // TODO E2E: do better
+                    throw new RuntimeException("Could not unlock folder!");
+                }
+
+                RemoteOperationResult remoteFolderOperationResult = new ReadFolderRemoteOperation(encryptedRemotePath)
+                    .execute(client);
+
+                createdRemoteFolder = (RemoteFile) remoteFolderOperationResult.getData().get(0);
+                OCFile newDir = createRemoteFolderOcFile(parent, filename, createdRemoteFolder);
+                getStorageManager().saveFile(newDir);
+
+                RemoteOperationResult encryptionOperationResult = new ToggleEncryptionRemoteOperation(
+                    newDir.getLocalId(),
+                    newDir.getRemotePath(),
+                    true)
+                    .execute(client);
+
+                if (!encryptionOperationResult.isSuccess()) {
+                    throw new RuntimeException("Error creating encrypted subfolder!");
+                }
+            } else {
+                // revert to sane state in case of any error
+                Log_OC.e(TAG, remotePath + " hasn't been created");
+            }
+
+            return result;
+        } catch (Exception e) {
+            // TODO remove folder
+
             if (!EncryptionUtils.unlockFolder(parent, client, token).isSuccess()) {
                 throw new RuntimeException("Could not clean up after failing folder creation!", e);
             }
@@ -209,17 +366,18 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
             // remove folder
             if (encryptedRemotePath != null) {
                 RemoteOperationResult removeResult = new RemoveRemoteEncryptedFileOperation(encryptedRemotePath,
-                                                                                            parent.getLocalId(),
                                                                                             user,
                                                                                             context,
-                                                                                            filename).execute(client);
+                                                                                            filename,
+                                                                                            parent,
+                                                                                            true).execute(client);
 
                 if (!removeResult.isSuccess()) {
                     throw new RuntimeException("Could not clean up after failing folder creation!");
                 }
             }
 
-            // TODO do better
+            // TODO E2E: do better
             return new RemoteOperationResult(e);
         } finally {
             // unlock folder
@@ -227,21 +385,30 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
                 RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(parent, client, token);
 
                 if (!unlockFolderResult.isSuccess()) {
-                    // TODO do better
+                    // TODO E2E: do better
                     throw new RuntimeException("Could not unlock folder!");
                 }
             }
         }
     }
 
-    private boolean isFileExisting(DecryptedFolderMetadata metadata, String filename) {
-        for (String key : metadata.getFiles().keySet()) {
-            DecryptedFolderMetadata.DecryptedFile file = metadata.getFiles().get(key);
+    private boolean isFileExisting(DecryptedFolderMetadataFileV1 metadata, String filename) {
+        for (com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile file : metadata.getFiles().values()) {
+            if (filename.equalsIgnoreCase(file.getEncrypted().getFilename())) {
+                return true;
+            }
+        }
 
-            if (file != null && filename.equalsIgnoreCase(file.getEncrypted().getFilename())) {
+        return false;
+    }
+
+    private boolean isFileExisting(DecryptedFolderMetadataFile metadata, String filename) {
+        for (DecryptedFile file : metadata.getMetadata().getFiles().values()) {
+            if (filename.equalsIgnoreCase(file.getFilename())) {
                 return true;
             }
         }
+
         return false;
     }
 
@@ -261,15 +428,16 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
     }
 
     @NonNull
-    private DecryptedFolderMetadata.DecryptedFile createDecryptedFile(String filename) {
+    private com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile createDecryptedFile(String filename) {
         // Key, always generate new one
         byte[] key = EncryptionUtils.generateKey();
 
         // IV, always generate new one
         byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
 
-        DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
-        DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
+        com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile =
+            new com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile();
+        Data data = new Data();
         data.setFilename(filename);
         data.setMimetype(MimeType.WEBDAV_FOLDER);
         data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
@@ -281,7 +449,32 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
     }
 
     @NonNull
-    private String createRandomFileName(DecryptedFolderMetadata metadata) {
+    private DecryptedFile createDecryptedFolder(String filename) {
+        // Key, always generate new one
+        byte[] key = EncryptionUtils.generateKey();
+
+        // IV, always generate new one
+        byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
+
+        return new DecryptedFile(filename,
+                                 MimeType.WEBDAV_FOLDER,
+                                 EncryptionUtils.encodeBytesToBase64String(iv),
+                                 "",
+                                 EncryptionUtils.encodeBytesToBase64String(key));
+    }
+
+    @NonNull
+    private String createRandomFileName(DecryptedFolderMetadataFile metadata) {
+        String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
+
+        while (metadata.getMetadata().getFiles().get(encryptedFileName) != null) {
+            encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
+        }
+        return encryptedFileName;
+    }
+
+    @NonNull
+    private String createRandomFileName(DecryptedFolderMetadataFileV1 metadata) {
         String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
 
         while (metadata.getFiles().get(encryptedFileName) != null) {

+ 148 - 54
app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java

@@ -23,17 +23,30 @@
 
 package com.owncloud.android.operations;
 
+import android.content.Context;
 import android.text.TextUtils;
 
+import com.nextcloud.client.account.User;
+import com.nextcloud.client.network.ClientFactory;
+import com.nextcloud.client.network.ClientFactoryImpl;
+import com.nextcloud.common.NextcloudClient;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.resources.files.FileUtils;
 import com.owncloud.android.lib.resources.shares.CreateShareRemoteOperation;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.users.GetPublicKeyRemoteOperation;
 import com.owncloud.android.operations.common.SyncOperation;
+import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.EncryptionUtilsV2;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -44,15 +57,19 @@ import java.util.Set;
  */
 public class CreateShareWithShareeOperation extends SyncOperation {
 
-    private String path;
-    private String shareeName;
-    private ShareType shareType;
-    private int permissions;
-    private String noteMessage;
-    private String sharePassword;
-    private boolean hideFileDownload;
-    private long expirationDateInMillis;
+    private final String path;
+    private final String shareeName;
+    private final ShareType shareType;
+    private final int permissions;
+    private final String noteMessage;
+    private final String sharePassword;
+    private final boolean hideFileDownload;
+    private final long expirationDateInMillis;
     private String label;
+    private final Context context;
+    private final User user;
+
+    private ArbitraryDataProvider arbitraryDataProvider;
 
     private static final Set<ShareType> supportedShareTypes = new HashSet<>(Arrays.asList(ShareType.USER,
                                                                                           ShareType.GROUP,
@@ -68,35 +85,9 @@ public class CreateShareWithShareeOperation extends SyncOperation {
      * @param shareeName  User or group name of the target sharee.
      * @param shareType   Type of share determines type of sharee; {@link ShareType#USER} and {@link ShareType#GROUP}
      *                    are the only valid values for the moment.
-     * @param permissions Share permissions key as detailed in
-     *                    https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html#create-a-new-share
-     *                    .
-     */
-    public CreateShareWithShareeOperation(String path,
-                                          String shareeName,
-                                          ShareType shareType,
-                                          int permissions,
-                                          FileDataStorageManager storageManager) {
-        super(storageManager);
-
-        if (!supportedShareTypes.contains(shareType)) {
-            throw new IllegalArgumentException("Illegal share type " + shareType);
-        }
-        this.path = path;
-        this.shareeName = shareeName;
-        this.shareType = shareType;
-        this.permissions = permissions;
-    }
-
-    /**
-     * Constructor.
-     *
-     * @param path        Full path of the file/folder being shared.
-     * @param shareeName  User or group name of the target sharee.
-     * @param shareType   Type of share determines type of sharee; {@link ShareType#USER} and {@link ShareType#GROUP}
-     *                    are the only valid values for the moment.
-     * @param permissions Share permissions key as detailed in https://doc.owncloud.org/server/8.2/developer_manual/core/ocs-share-api.html
-     *                    .
+     * @param permissions Share permissions key as detailed in <a
+     *                    href="https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html">OCS
+     *                    Share API</a>.
      */
     public CreateShareWithShareeOperation(String path,
                                           String shareeName,
@@ -106,7 +97,10 @@ public class CreateShareWithShareeOperation extends SyncOperation {
                                           String sharePassword,
                                           long expirationDateInMillis,
                                           boolean hideFileDownload,
-                                          FileDataStorageManager storageManager) {
+                                          FileDataStorageManager storageManager,
+                                          Context context,
+                                          User user,
+                                          ArbitraryDataProvider arbitraryDataProvider) {
         super(storageManager);
 
         if (!supportedShareTypes.contains(shareType)) {
@@ -120,10 +114,52 @@ public class CreateShareWithShareeOperation extends SyncOperation {
         this.hideFileDownload = hideFileDownload;
         this.noteMessage = noteMessage;
         this.sharePassword = sharePassword;
+        this.context = context;
+        this.user = user;
+        this.arbitraryDataProvider = arbitraryDataProvider;
     }
 
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
+        OCFile folder = getStorageManager().getFileByDecryptedRemotePath(path);
+
+        if (folder == null) {
+            throw new IllegalArgumentException("Trying to share on a null folder: " + path);
+        }
+
+        boolean isEncrypted = folder.isEncrypted();
+        String token = null;
+        long newCounter = folder.getE2eCounter() + 1;
+
+        // E2E: lock folder
+        if (isEncrypted) {
+            try {
+                String publicKey = EncryptionUtils.getPublicKey(user, shareeName, arbitraryDataProvider);
+
+                if (publicKey.equals("")) {
+                    NextcloudClient nextcloudClient = new ClientFactoryImpl(context).createNextcloudClient(user);
+                    RemoteOperationResult<String> result = new GetPublicKeyRemoteOperation(shareeName).execute(nextcloudClient);
+                    if (result.isSuccess()) {
+                        // store it
+                        EncryptionUtils.savePublicKey(
+                            user,
+                            result.getResultData(),
+                            shareeName,
+                            arbitraryDataProvider
+                                                     );
+                    } else {
+                        RemoteOperationResult e = new RemoteOperationResult(new IllegalStateException());
+                        e.setMessage(context.getString(R.string.secure_share_not_set_up));
+
+                        return e;
+                    }
+                }
+
+                token = EncryptionUtils.lockFolder(folder, client, newCounter);
+            } catch (UploadException | ClientFactory.CreationException e) {
+                return new RemoteOperationResult(e);
+            }
+        }
 
         CreateShareRemoteOperation operation = new CreateShareRemoteOperation(
             path,
@@ -135,30 +171,88 @@ public class CreateShareWithShareeOperation extends SyncOperation {
             noteMessage
         );
         operation.setGetShareDetails(true);
-        RemoteOperationResult result = operation.execute(client);
+        RemoteOperationResult shareResult = operation.execute(client);
 
+        if (!shareResult.isSuccess() || shareResult.getData().size() == 0) {
+            // something went wrong
+            return shareResult;
+        }
+
+        // E2E: update metadata
+        if (isEncrypted) {
+            Object object = EncryptionUtils.downloadFolderMetadata(folder,
+                                                                   client,
+                                                                   context,
+                                                                   user
+                                                                  );
 
-        if (result.isSuccess() && result.getData().size() > 0) {
-            OCShare share = (OCShare) result.getData().get(0);
+            if (object instanceof DecryptedFolderMetadataFileV1) {
+                throw new RuntimeException("Trying to share on e2e v1!");
+            }
 
-            //once creating share link update other information
-            UpdateShareInfoOperation updateShareInfoOperation = new UpdateShareInfoOperation(share, getStorageManager());
-            updateShareInfoOperation.setExpirationDateInMillis(expirationDateInMillis);
-            updateShareInfoOperation.setHideFileDownload(hideFileDownload);
-            updateShareInfoOperation.setLabel(label);
+            DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object;
 
-            //update permissions for external share (will otherwise default to read-only)
-            updateShareInfoOperation.setPermissions(permissions);
+            boolean metadataExists;
+            if (metadata == null) {
+                String cert = EncryptionUtils.retrievePublicKeyForUser(user, context);
+                metadata = new EncryptionUtilsV2().createDecryptedFolderMetadataFile();
+                metadata.getUsers().add(new DecryptedUser(client.getUserId(), cert));
 
-            //execute and save the result in database
-            RemoteOperationResult updateShareInfoResult = updateShareInfoOperation.execute(client);
-            if (updateShareInfoResult.isSuccess() && updateShareInfoResult.getData().size() > 0) {
-                OCShare shareUpdated = (OCShare) updateShareInfoResult.getData().get(0);
-                updateData(shareUpdated);
+                metadataExists = false;
+            } else {
+                metadataExists = true;
             }
+
+            EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
+
+            // add sharee to metadata
+            String publicKey = EncryptionUtils.getPublicKey(user, shareeName, arbitraryDataProvider);
+            DecryptedFolderMetadataFile newMetadata = encryptionUtilsV2.addShareeToMetadata(metadata,
+                                                                                            shareeName,
+                                                                                            publicKey);
+
+            // upload metadata
+            metadata.getMetadata().setCounter(newCounter);
+            try {
+                encryptionUtilsV2.serializeAndUploadMetadata(folder,
+                                                             newMetadata,
+                                                             token,
+                                                             client,
+                                                             metadataExists,
+                                                             context,
+                                                             user,
+                                                             getStorageManager());
+            } catch (UploadException e) {
+                return new RemoteOperationResult<>(new RuntimeException("Uploading metadata failed"));
+            }
+
+            // E2E: unlock folder
+            RemoteOperationResult<Void> unlockResult = EncryptionUtils.unlockFolder(folder, client, token);
+            if (!unlockResult.isSuccess()) {
+                return new RemoteOperationResult<>(new RuntimeException("Unlock failed"));
+            }
+        }
+
+        OCShare share = (OCShare) shareResult.getData().get(0);
+
+        // once creating share link update other information
+        UpdateShareInfoOperation updateShareInfoOperation = new UpdateShareInfoOperation(share, getStorageManager());
+        updateShareInfoOperation.setExpirationDateInMillis(expirationDateInMillis);
+        updateShareInfoOperation.setHideFileDownload(hideFileDownload);
+        updateShareInfoOperation.setNote(noteMessage);
+        updateShareInfoOperation.setLabel(label);
+
+        //update permissions for external share (will otherwise default to read-only)
+        updateShareInfoOperation.setPermissions(permissions);
+
+        // execute and save the result in database
+        RemoteOperationResult updateShareInfoResult = updateShareInfoOperation.execute(client);
+        if (updateShareInfoResult.isSuccess() && updateShareInfoResult.getData().size() > 0) {
+            OCShare shareUpdated = (OCShare) updateShareInfoResult.getData().get(0);
+            updateData(shareUpdated);
         }
 
-        return result;
+        return shareResult;
     }
 
     private void updateData(OCShare share) {

+ 45 - 12
app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

@@ -28,9 +28,11 @@ import android.webkit.MimeTypeMap;
 
 import com.nextcloud.client.account.User;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
-import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
 import com.owncloud.android.lib.common.operations.OperationCancelledException;
@@ -50,6 +52,8 @@ import java.util.Iterator;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
+
 /**
  * Remote DownloadOperation performing the download of a file to an ownCloud server
  */
@@ -217,20 +221,49 @@ public class DownloadFileOperation extends RemoteOperation {
 
                 OCFile parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.getParentRemotePath());
 
-                DecryptedFolderMetadata metadata = EncryptionUtils.downloadFolderMetadata(parent,
-                                                                                          client,
-                                                                                          operationContext,
-                                                                                          user);
+                Object object = EncryptionUtils.downloadFolderMetadata(parent,
+                                                                       client,
+                                                                       operationContext,
+                                                                       user);
 
-                if (metadata == null) {
+                if (object == null) {
                     return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
                 }
-                byte[] key = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
-                        .get(file.getEncryptedFileName()).getEncrypted().getKey());
-                byte[] iv = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
-                        .get(file.getEncryptedFileName()).getInitializationVector());
-                byte[] authenticationTag = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
-                        .get(file.getEncryptedFileName()).getAuthenticationTag());
+
+                String keyString;
+                String nonceString;
+                String authenticationTagString;
+                if (object instanceof DecryptedFolderMetadataFile) {
+                    DecryptedFile decryptedFile = ((DecryptedFolderMetadataFile) object)
+                        .getMetadata()
+                        .getFiles()
+                        .get(file.getEncryptedFileName());
+
+                    if (decryptedFile == null) {
+                        return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
+                    }
+
+                    keyString = decryptedFile.getKey();
+                    nonceString = decryptedFile.getNonce();
+                    authenticationTagString = decryptedFile.getAuthenticationTag();
+                } else {
+                    com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile =
+                        ((DecryptedFolderMetadataFileV1) object)
+                            .getFiles()
+                            .get(file.getEncryptedFileName());
+
+                    if (decryptedFile == null) {
+                        return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
+                    }
+
+                    keyString = decryptedFile.getEncrypted().getKey();
+                    nonceString = decryptedFile.getInitializationVector();
+                    authenticationTagString = decryptedFile.getAuthenticationTag();
+                }
+
+                byte[] key = decodeStringToBase64Bytes(keyString);
+                byte[] iv = decodeStringToBase64Bytes(nonceString);
+                byte[] authenticationTag = decodeStringToBase64Bytes(authenticationTagString);
 
                 try {
                     byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile,

+ 118 - 25
app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -29,9 +29,11 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.common.NextcloudClient;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
-import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.DirectEditing;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientFactory;
@@ -47,6 +49,7 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile;
 import com.owncloud.android.lib.resources.shares.GetSharesForFileRemoteOperation;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.lib.resources.users.GetPredefinedStatusesRemoteOperation;
 import com.owncloud.android.lib.resources.users.PredefinedStatus;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
@@ -55,6 +58,7 @@ import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeType;
 import com.owncloud.android.utils.MimeTypeUtil;
+import com.owncloud.android.utils.theme.CapabilityUtils;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -236,6 +240,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
         if (result.isSuccess()) {
             if (mRemoteFolderChanged) {
+                // TODO catch IllegalStateException, show properly to user
                 result = fetchAndSyncRemoteFolder(client);
             } else {
                 mChildren = mStorageManager.getFolderContent(mLocalFolder, false);
@@ -403,7 +408,8 @@ public class RefreshFolderOperation extends RemoteOperation {
     private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
         String remotePath = mLocalFolder.getRemotePath();
         RemoteOperationResult result = new ReadFolderRemoteOperation(remotePath).execute(client);
-        Log_OC.d(TAG, "Synchronizing " + user.getAccountName() + remotePath);
+        Log_OC.d(TAG, "Refresh folder " + user.getAccountName() + remotePath);
+        Log_OC.d(TAG, "Refresh folder with remote id" + mLocalFolder.getRemoteId());
 
         if (result.isSuccess()) {
             synchronizeData(result.getData());
@@ -470,15 +476,38 @@ public class RefreshFolderOperation extends RemoteOperation {
         // update size
         mLocalFolder.setFileLength(remoteFolder.getFileLength());
 
-        DecryptedFolderMetadata metadata = getDecryptedFolderMetadata(encryptedAncestor,
-                                                                      mLocalFolder,
-                                                                      getClient(),
-                                                                      user,
-                                                                      mContext);
+        Object object = null;
+        if (mLocalFolder.isEncrypted()) {
+            object = getDecryptedFolderMetadata(encryptedAncestor,
+                                                mLocalFolder,
+                                                getClient(),
+                                                user,
+                                                mContext);
+        }
+
+        if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
+            if (encryptedAncestor && object == null) {
+                throw new IllegalStateException("metadata is null!");
+            }
+        }
 
         // get current data about local contents of the folder to synchronize
-        Map<String, OCFile> localFilesMap = prefillLocalFilesMap(metadata,
-                                                                 mStorageManager.getFolderContent(mLocalFolder, false));
+        Map<String, OCFile> localFilesMap;
+        E2EVersion e2EVersion;
+        if (object instanceof DecryptedFolderMetadataFileV1) {
+            e2EVersion = E2EVersion.V1_2;
+            localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object,
+                                                 mStorageManager.getFolderContent(mLocalFolder, false));
+        } else {
+            e2EVersion = E2EVersion.V2_0;
+            localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFile) object,
+                                                 mStorageManager.getFolderContent(mLocalFolder, false));
+
+            // update counter
+            if (object != null) {
+                mLocalFolder.setE2eCounter(((DecryptedFolderMetadataFile) object).getMetadata().getCounter());
+            }
+        }
 
         // loop to update every child
         OCFile remoteFile;
@@ -518,8 +547,17 @@ public class RefreshFolderOperation extends RemoteOperation {
             FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());
 
             // update file name for encrypted files
-            if (metadata != null) {
-                updateFileNameForEncryptedFile(mStorageManager, metadata, updatedFile);
+            if (e2EVersion == E2EVersion.V1_2) {
+                updateFileNameForEncryptedFileV1(mStorageManager,
+                                                 (DecryptedFolderMetadataFileV1) object,
+                                                 updatedFile);
+            } else {
+                updateFileNameForEncryptedFile(mStorageManager,
+                                               (DecryptedFolderMetadataFile) object,
+                                               updatedFile);
+                if (localFile != null) {
+                    updatedFile.setE2eCounter(localFile.getE2eCounter());
+                }
             }
 
             // we parse content, so either the folder itself or its direct parent (which we check) must be encrypted
@@ -531,8 +569,14 @@ public class RefreshFolderOperation extends RemoteOperation {
 
         // save updated contents in local database
         // update file name for encrypted files
-        if (metadata != null) {
-            updateFileNameForEncryptedFile(mStorageManager, metadata, mLocalFolder);
+        if (e2EVersion == E2EVersion.V1_2) {
+            updateFileNameForEncryptedFileV1(mStorageManager,
+                                             (DecryptedFolderMetadataFileV1) object,
+                                             mLocalFolder);
+        } else {
+            updateFileNameForEncryptedFile(mStorageManager,
+                                           (DecryptedFolderMetadataFile) object,
+                                           mLocalFolder);
         }
         mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values());
 
@@ -540,12 +584,12 @@ public class RefreshFolderOperation extends RemoteOperation {
     }
 
     @Nullable
-    public static DecryptedFolderMetadata getDecryptedFolderMetadata(boolean encryptedAncestor,
-                                                                     OCFile localFolder,
-                                                                     OwnCloudClient client,
-                                                                     User user,
-                                                                     Context context) {
-        DecryptedFolderMetadata metadata;
+    public static Object getDecryptedFolderMetadata(boolean encryptedAncestor,
+                                                    OCFile localFolder,
+                                                    OwnCloudClient client,
+                                                    User user,
+                                                    Context context) {
+        Object metadata;
         if (encryptedAncestor) {
             metadata = EncryptionUtils.downloadFolderMetadata(localFolder, client, context, user);
         } else {
@@ -554,13 +598,62 @@ public class RefreshFolderOperation extends RemoteOperation {
         return metadata;
     }
 
+    public static void updateFileNameForEncryptedFileV1(FileDataStorageManager storageManager,
+                                                        @NonNull DecryptedFolderMetadataFileV1 metadata,
+                                                        OCFile updatedFile) {
+        try {
+            String decryptedFileName;
+            String mimetype;
+
+            if (updatedFile.isFolder()) {
+                decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getFilename();
+                mimetype = MimeType.DIRECTORY;
+            } else {
+                com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile decryptedFile =
+                    metadata.getFiles().get(updatedFile.getFileName());
+                decryptedFileName = decryptedFile.getEncrypted().getFilename();
+                mimetype = decryptedFile.getEncrypted().getMimetype();
+            }
+
+
+            OCFile parentFile = storageManager.getFileById(updatedFile.getParentId());
+            String decryptedRemotePath = parentFile.getDecryptedRemotePath() + decryptedFileName;
+
+            if (updatedFile.isFolder()) {
+                decryptedRemotePath += "/";
+            }
+            updatedFile.setDecryptedRemotePath(decryptedRemotePath);
+
+            if (mimetype == null || mimetype.isEmpty()) {
+                if (updatedFile.isFolder()) {
+                    updatedFile.setMimeType(MimeType.DIRECTORY);
+                } else {
+                    updatedFile.setMimeType("application/octet-stream");
+                }
+            } else {
+                updatedFile.setMimeType(mimetype);
+            }
+        } catch (NullPointerException e) {
+            Log_OC.e(TAG, "DecryptedMetadata for file " + updatedFile.getFileId() + " not found!");
+        }
+    }
+
     public static void updateFileNameForEncryptedFile(FileDataStorageManager storageManager,
-                                                      @NonNull DecryptedFolderMetadata metadata,
+                                                      @NonNull DecryptedFolderMetadataFile metadata,
                                                       OCFile updatedFile) {
         try {
-            String decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted()
-                .getFilename();
-            String mimetype = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getMimetype();
+            String decryptedFileName;
+            String mimetype;
+
+            if (updatedFile.isFolder()) {
+                decryptedFileName = metadata.getMetadata().getFolders().get(updatedFile.getFileName());
+                mimetype = MimeType.DIRECTORY;
+            } else {
+                DecryptedFile decryptedFile = metadata.getMetadata().getFiles().get(updatedFile.getFileName());
+                decryptedFileName = decryptedFile.getFilename();
+                mimetype = decryptedFile.getMimetype();
+            }
+
 
             OCFile parentFile = storageManager.getFileById(updatedFile.getParentId());
             String decryptedRemotePath = parentFile.getDecryptedRemotePath() + decryptedFileName;
@@ -580,7 +673,7 @@ public class RefreshFolderOperation extends RemoteOperation {
                 updatedFile.setMimeType(mimetype);
             }
         } catch (NullPointerException e) {
-            Log_OC.e(TAG, "Metadata for file " + updatedFile.getFileId() + " not found!");
+            Log_OC.e(TAG, "DecryptedMetadata for file " + updatedFile.getFileId() + " not found!");
         }
     }
 
@@ -634,7 +727,7 @@ public class RefreshFolderOperation extends RemoteOperation {
     }
 
     @NonNull
-    public static Map<String, OCFile> prefillLocalFilesMap(DecryptedFolderMetadata metadata, List<OCFile> localFiles) {
+    public static Map<String, OCFile> prefillLocalFilesMap(Object metadata, List<OCFile> localFiles) {
         Map<String, OCFile> localFilesMap = Maps.newHashMapWithExpectedSize(localFiles.size());
 
         for (OCFile file : localFiles) {

+ 3 - 2
app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java

@@ -104,10 +104,11 @@ public class RemoveFileOperation extends SyncOperation {
             if (fileToRemove.isEncrypted()) {
                 OCFile parent = getStorageManager().getFileByPath(fileToRemove.getParentRemotePath());
                 operation = new RemoveRemoteEncryptedFileOperation(fileToRemove.getRemotePath(),
-                                                                   parent.getLocalId(),
                                                                    user,
                                                                    context,
-                                                                   fileToRemove.getEncryptedFileName());
+                                                                   fileToRemove.getEncryptedFileName(),
+                                                                   parent,
+                                                                   fileToRemove.isFolder());
             } else {
                 operation = new RemoveFileRemoteOperation(fileToRemove.getRemotePath());
             }

+ 46 - 88
app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java

@@ -23,68 +23,58 @@ package com.owncloud.android.operations;
 
 import android.content.Context;
 
-import com.google.gson.reflect.TypeToken;
 import com.nextcloud.client.account.User;
 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.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.e2ee.GetMetadataRemoteOperation;
-import com.owncloud.android.lib.resources.e2ee.LockFileRemoteOperation;
-import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation;
-import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation;
 import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.EncryptionUtilsV2;
 
 import org.apache.commons.httpclient.HttpStatus;
 import org.apache.commons.httpclient.NameValuePair;
 import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
 
-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;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
+import kotlin.Pair;
 
 /**
  * Remote operation performing the removal of a remote encrypted file or folder
  */
-public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
+public class RemoveRemoteEncryptedFileOperation extends RemoteOperation<Void> {
     private static final String TAG = RemoveRemoteEncryptedFileOperation.class.getSimpleName();
-
     private static final int REMOVE_READ_TIMEOUT = 30000;
     private static final int REMOVE_CONNECTION_TIMEOUT = 5000;
-
     private final String remotePath;
-    private final long parentId;
-    private User user;
-
-    private final ArbitraryDataProvider arbitraryDataProvider;
+    private final OCFile parentFolder;
+    private final User user;
     private final String fileName;
+    private final Context context;
+    private final boolean isFolder;
+    private final ArbitraryDataProvider arbitraryDataProvider;
 
     /**
      * Constructor
      *
-     * @param remotePath RemotePath of the remote file or folder to remove from the server
-     * @param parentId   local id of parent folder
+     * @param remotePath   RemotePath of the remote file or folder to remove from the server
+     * @param parentFolder parent folder
      */
     RemoveRemoteEncryptedFileOperation(String remotePath,
-                                       long parentId,
                                        User user,
                                        Context context,
-                                       String fileName) {
+                                       String fileName,
+                                       OCFile parentFolder,
+                                       boolean isFolder) {
         this.remotePath = remotePath;
-        this.parentId = parentId;
         this.user = user;
         this.fileName = fileName;
+        this.context = context;
+        this.parentFolder = parentFolder;
+        this.isFolder = isFolder;
 
         arbitraryDataProvider = new ArbitraryDataProviderImpl(context);
     }
@@ -93,46 +83,19 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
      * Performs the remove operation.
      */
     @Override
-    protected RemoteOperationResult run(OwnCloudClient client) {
-        RemoteOperationResult result;
+    protected RemoteOperationResult<Void> run(OwnCloudClient client) {
+        RemoteOperationResult<Void> result;
         DeleteMethod delete = null;
         String token = null;
-        DecryptedFolderMetadata metadata;
-
-        String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
-        String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
 
         try {
             // Lock folder
-            RemoteOperationResult lockFileOperationResult = new LockFileRemoteOperation(parentId).execute(client);
-
-            if (lockFileOperationResult.isSuccess()) {
-                token = (String) lockFileOperationResult.getData().get(0);
-            } else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
-                throw new RemoteOperationFailedException("Forbidden! Please try again later.)");
-            } else {
-                throw new RemoteOperationFailedException("Unknown error!");
-            }
+            token = EncryptionUtils.lockFolder(parentFolder, client);
 
-            // refresh metadata
-            RemoteOperationResult getMetadataOperationResult = new GetMetadataRemoteOperation(parentId).execute(client);
-
-            if (getMetadataOperationResult.isSuccess()) {
-                // decrypt metadata
-                String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
-
-                EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
-                    serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
-                    });
-
-                metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata,
-                                                                 privateKey,
-                                                                 arbitraryDataProvider,
-                                                                 user,
-                                                                 parentId);
-            } else {
-                throw new RemoteOperationFailedException("No Metadata found!");
-            }
+            EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
+            Pair<Boolean, DecryptedFolderMetadataFile> pair = encryptionUtilsV2.retrieveMetadata(parentFolder, client, user, context);
+            boolean metadataExists = pair.getFirst();
+            DecryptedFolderMetadataFile metadata = pair.getSecond();
 
             // delete file remote
             delete = new DeleteMethod(client.getFilesDavUri(remotePath));
@@ -140,35 +103,29 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
             int status = client.executeMethod(delete, REMOVE_READ_TIMEOUT, REMOVE_CONNECTION_TIMEOUT);
 
             delete.getResponseBodyAsString();   // exhaust the response, although not interesting
-            result = new RemoteOperationResult(delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, delete);
+            result = new RemoteOperationResult<>(delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, delete);
             Log_OC.i(TAG, "Remove " + remotePath + ": " + result.getLogMessage());
 
-            // remove file from metadata
-            metadata.getFiles().remove(fileName);
-
-            EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(
-                metadata,
-                publicKey,
-                arbitraryDataProvider,
-                user,
-                parentId);
-            String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
+            if (isFolder) {
+                encryptionUtilsV2.removeFolderFromMetadata(fileName, metadata);
+            } else {
+                encryptionUtilsV2.removeFileFromMetadata(fileName, metadata);
+            }
 
             // upload metadata
-            RemoteOperationResult uploadMetadataOperationResult =
-                new UpdateMetadataRemoteOperation(parentId,
-                                                  serializedFolderMetadata, token).execute(client);
-
-            if (!uploadMetadataOperationResult.isSuccess()) {
-                throw new RemoteOperationFailedException("Metadata not uploaded!");
-            }
+            encryptionUtilsV2.serializeAndUploadMetadata(parentFolder,
+                                                         metadata,
+                                                         token,
+                                                         client,
+                                                         metadataExists,
+                                                         context,
+                                                         user,
+                                                         new FileDataStorageManager(user, context.getContentResolver()));
 
             // return success
             return result;
-        } catch (NoSuchAlgorithmException | IOException | InvalidKeyException | InvalidAlgorithmParameterException |
-                 NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException |
-                 CertificateException e) {
-            result = new RemoteOperationResult(e);
+        } catch (Exception e) {
+            result = new RemoteOperationResult<>(e);
             Log_OC.e(TAG, "Remove " + remotePath + ": " + result.getLogMessage(), e);
 
         } finally {
@@ -178,11 +135,12 @@ public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
 
             // unlock file
             if (token != null) {
-                RemoteOperationResult unlockFileOperationResult = new UnlockFileRemoteOperation(parentId, token)
-                    .execute(client);
+                RemoteOperationResult<Void> unlockFileOperationResult = EncryptionUtils.unlockFolder(parentFolder,
+                                                                                                     client,
+                                                                                                     token);
 
                 if (!unlockFileOperationResult.isSuccess()) {
-                    Log_OC.e(TAG, "Failed to unlock " + parentId);
+                    Log_OC.e(TAG, "Failed to unlock " + parentFolder.getLocalId());
                 }
             }
         }

+ 45 - 14
app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java

@@ -26,9 +26,10 @@ import android.text.TextUtils;
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.files.downloader.FileDownloadHelper;
-import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.OperationCancelledException;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -37,6 +38,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
 import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation;
 import com.owncloud.android.lib.resources.files.model.RemoteFile;
+import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.operations.common.SyncOperation;
 import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.utils.FileStorageUtils;
@@ -49,6 +51,8 @@ import java.util.Map;
 import java.util.Vector;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
 
 /**
  *  Remote operation performing the synchronization of the list of files contained
@@ -215,6 +219,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
         ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(mRemotePath);
         RemoteOperationResult result = operation.execute(client);
         Log_OC.d(TAG, "Synchronizing " + user.getAccountName() + mRemotePath);
+        Log_OC.d(TAG, "Synchronizing remote id" + mLocalFolder.getRemoteId());
 
         if (result.isSuccess()) {
             synchronizeData(result.getData());
@@ -281,17 +286,29 @@ public class SynchronizeFolderOperation extends SyncOperation {
         // update richWorkspace
         mLocalFolder.setRichWorkspace(remoteFolder.getRichWorkspace());
 
-        DecryptedFolderMetadata metadata = RefreshFolderOperation.getDecryptedFolderMetadata(encryptedAncestor,
-                                                                                             mLocalFolder,
-                                                                                             getClient(),
-                                                                                             user,
-                                                                                             mContext);
-
+        Object object = RefreshFolderOperation.getDecryptedFolderMetadata(encryptedAncestor,
+                                                                                                 mLocalFolder,
+                                                                                                 getClient(),
+                                                                                                 user,
+                                                                                                 mContext);
+        if (mLocalFolder.isEncrypted() && object == null) {
+            throw new IllegalStateException("metadata is null!");
+        }
+        
         // get current data about local contents of the folder to synchronize
-        Map<String, OCFile> localFilesMap =
-            RefreshFolderOperation.prefillLocalFilesMap(metadata,
-                                                        storageManager.getFolderContent(mLocalFolder, false));
+        Map<String, OCFile> localFilesMap;
+        E2EVersion e2EVersion;
 
+        if (object instanceof DecryptedFolderMetadataFileV1) {
+            e2EVersion = E2EVersion.V1_2;
+            localFilesMap = RefreshFolderOperation.prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object,
+                                                                        storageManager.getFolderContent(mLocalFolder, false));
+        } else {
+            e2EVersion = E2EVersion.V2_0;
+            localFilesMap = RefreshFolderOperation.prefillLocalFilesMap((DecryptedFolderMetadataFile) object,
+                                                                        storageManager.getFolderContent(mLocalFolder, false));
+        }
+        
         // loop to synchronize every child
         List<OCFile> updatedFiles = new ArrayList<>(folderAndFiles.size() - 1);
         OCFile remoteFile;
@@ -323,8 +340,14 @@ public class SynchronizeFolderOperation extends SyncOperation {
             FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName());
 
             // update file name for encrypted files
-            if (metadata != null) {
-                RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadata, updatedFile);
+            if (e2EVersion == E2EVersion.V1_2) {
+                RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager,
+                                                 (DecryptedFolderMetadataFileV1) object,
+                                                 updatedFile);
+            } else {
+                RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager,
+                                               (DecryptedFolderMetadataFile) object,
+                                               updatedFile);
             }
 
             // we parse content, so either the folder itself or its direct parent (which we check) must be encrypted
@@ -337,8 +360,15 @@ public class SynchronizeFolderOperation extends SyncOperation {
             updatedFiles.add(updatedFile);
         }
 
-        if (metadata != null) {
-            RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadata, mLocalFolder);
+        // update file name for encrypted files
+        if (e2EVersion == E2EVersion.V1_2) {
+            RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager,
+                                                                    (DecryptedFolderMetadataFileV1) object,
+                                                                    mLocalFolder);
+        } else {
+            RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager,
+                                                                  (DecryptedFolderMetadataFile) object,
+                                                                  mLocalFolder);
         }
 
         // save updated contents in local database
@@ -391,6 +421,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
     }
 
 
+    @SuppressFBWarnings("JLM")
     private void prepareOpsFromLocalKnowledge() throws OperationCancelledException {
         List<OCFile> children = getStorageManager().getFolderContent(mLocalFolder, false);
         for (OCFile child : children) {

+ 71 - 2
app/src/main/java/com/owncloud/android/operations/UnshareOperation.java

@@ -21,8 +21,13 @@
 
 package com.owncloud.android.operations;
 
+import android.content.Context;
+
+import com.nextcloud.client.account.User;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@@ -32,6 +37,8 @@ import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.RemoveShareRemoteOperation;
 import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.operations.common.SyncOperation;
+import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.EncryptionUtilsV2;
 
 import java.util.List;
 
@@ -45,27 +52,89 @@ public class UnshareOperation extends SyncOperation {
 
     private final String remotePath;
     private final long shareId;
-
-    public UnshareOperation(String remotePath, long shareId, FileDataStorageManager storageManager) {
+    private final Context context;
+    private final User user;
+
+    public UnshareOperation(String remotePath,
+                            long shareId,
+                            FileDataStorageManager storageManager,
+                            User user,
+                            Context context) {
         super(storageManager);
 
         this.remotePath = remotePath;
         this.shareId = shareId;
+        this.user = user;
+        this.context = context;
     }
 
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result;
+        String token = null;
 
         // Get Share for a file
         OCShare share = getStorageManager().getShareById(shareId);
 
         if (share != null) {
             OCFile file = getStorageManager().getFileByEncryptedRemotePath(remotePath);
+
+            if (file.isEncrypted() && share.getShareType() != ShareType.PUBLIC_LINK) {
+                // E2E: lock folder
+                try {
+                    token = EncryptionUtils.lockFolder(file, client, file.getE2eCounter() + 1);
+                } catch (UploadException e) {
+                    return new RemoteOperationResult(e);
+                }
+
+                // download metadata
+                Object object = EncryptionUtils.downloadFolderMetadata(file,
+                                                                       client,
+                                                                       context,
+                                                                       user);
+
+                if (object == null) {
+                    return new RemoteOperationResult(new RuntimeException("No metadata!"));
+                }
+
+                if (object instanceof DecryptedFolderMetadataFileV1) {
+                    throw new RuntimeException("Trying to unshare on e2e v1!");
+                }
+
+                DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object;
+
+                // remove sharee from metadata
+                EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
+                DecryptedFolderMetadataFile newMetadata = encryptionUtilsV2.removeShareeFromMetadata(metadata,
+                                                                                                     share.getShareWith());
+
+                // upload metadata
+                try {
+                    encryptionUtilsV2.serializeAndUploadMetadata(file,
+                                                                 newMetadata,
+                                                                 token,
+                                                                 client,
+                                                                 true,
+                                                                 context,
+                                                                 user,
+                                                                 getStorageManager());
+                } catch (UploadException e) {
+                    return new RemoteOperationResult(new RuntimeException("Upload of metadata failed!"));
+                }
+            }
+
             RemoveShareRemoteOperation operation = new RemoveShareRemoteOperation(share.getRemoteId());
             result = operation.execute(client);
 
             if (result.isSuccess()) {
+                // E2E: unlock folder
+                if (file.isEncrypted() && share.getShareType() != ShareType.PUBLIC_LINK) {
+                    RemoteOperationResult<Void> unlockResult = EncryptionUtils.unlockFolder(file, client, token);
+                    if (!unlockResult.isSuccess()) {
+                        return new RemoteOperationResult<>(new RuntimeException("Unlock failed"));
+                    }
+                }
+
                 Log_OC.d(TAG, "Share id = " + share.getRemoteId() + " deleted");
 
                 if (ShareType.PUBLIC_LINK == share.getShareType()) {

+ 190 - 100
app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -25,7 +25,6 @@ import android.annotation.SuppressLint;
 import android.content.Context;
 import android.net.Uri;
 import android.text.TextUtils;
-import android.util.Pair;
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.device.BatteryStatus;
@@ -34,12 +33,17 @@ import com.nextcloud.client.network.Connectivity;
 import com.nextcloud.client.network.ConnectivityService;
 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.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.UploadsStorageManager;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.Data;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata;
+import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile;
+import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.NameCollisionPolicy;
@@ -56,13 +60,16 @@ import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
 import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
 import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
 import com.owncloud.android.lib.resources.files.model.RemoteFile;
+import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.operations.common.SyncOperation;
 import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.EncryptionUtilsV2;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.FileUtil;
 import com.owncloud.android.utils.MimeType;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.UriUtils;
+import com.owncloud.android.utils.theme.CapabilityUtils;
 
 import org.apache.commons.httpclient.HttpStatus;
 import org.apache.commons.httpclient.methods.RequestEntity;
@@ -80,9 +87,11 @@ import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.channels.OverlappingFileLockException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import androidx.annotation.CheckResult;
@@ -90,8 +99,7 @@ import androidx.annotation.Nullable;
 
 
 /**
- * Operation performing the update in the ownCloud server
- * of a file that was modified locally.
+ * Operation performing the update in the ownCloud server of a file that was modified locally.
  */
 public class UploadFileOperation extends SyncOperation {
 
@@ -230,10 +238,10 @@ public class UploadFileOperation extends SyncOperation {
         mUpload = upload;
         if (file == null) {
             mFile = obtainNewOCFileToUpload(
-                    upload.getRemotePath(),
-                    upload.getLocalPath(),
-                    upload.getMimeType()
-            );
+                upload.getRemotePath(),
+                upload.getLocalPath(),
+                upload.getMimeType()
+                                           );
         } else {
             mFile = file;
         }
@@ -261,7 +269,9 @@ public class UploadFileOperation extends SyncOperation {
         return mWhileChargingOnly;
     }
 
-    public boolean isIgnoringPowerSaveMode() { return mIgnoringPowerSaveMode; }
+    public boolean isIgnoringPowerSaveMode() {
+        return mIgnoringPowerSaveMode;
+    }
 
     public User getUser() {
         return user;
@@ -392,7 +402,7 @@ public class UploadFileOperation extends SyncOperation {
 
         String remoteParentPath = new File(getRemotePath()).getParent();
         remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ?
-                remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
+            remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
 
         OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
 
@@ -444,8 +454,6 @@ public class UploadFileOperation extends SyncOperation {
         String token = null;
 
         ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(getContext());
-
-        String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY);
         String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY);
 
         try {
@@ -456,32 +464,75 @@ public class UploadFileOperation extends SyncOperation {
                 return result;
             }
             /***** E2E *****/
+            // Only on V2+: whenever we change something, increase counter
+            long counter = -1;
+            if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
+                counter = parentFile.getE2eCounter() + 1;
+            }
 
             // we might have an old token from interrupted upload
             if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) {
                 token = mFolderUnlockToken;
             } else {
-                token = EncryptionUtils.lockFolder(parentFile, client);
+                token = EncryptionUtils.lockFolder(parentFile, client, counter);
                 // immediately store it
                 mUpload.setFolderUnlockToken(token);
                 uploadsStorageManager.updateUpload(mUpload);
             }
 
             // Update metadata
-            Pair<Boolean, DecryptedFolderMetadata> metadataPair = EncryptionUtils.retrieveMetadata(parentFile,
-                                                                                                   client,
-                                                                                                   privateKey,
-                                                                                                   publicKey,
-                                                                                                   arbitraryDataProvider,
-                                                                                                   user);
+            EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2();
+//            kotlin.Pair<Boolean, DecryptedFolderMetadataFile> metadataPair =
+//                encryptionUtilsV2.retrieveMetadata(parentFile,
+//                                                   client,
+//                                                   user,
+//                                                   mContext);
+
+            Object object = EncryptionUtils.downloadFolderMetadata(parentFile, client, mContext, user);
+
+            if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) {
+                if (object == null) {
+                    // TODO return error
+                    return new RemoteOperationResult(new IllegalStateException("Metadata does not exist"));
+                } else {
+                    metadataExists = true;
+                }
+            } else {
+                // v1 is allowed to be null, thus create it
+                DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1();
+                metadata.setMetadata(new DecryptedMetadata());
+                metadata.getMetadata().setVersion(1.2);
+                metadata.getMetadata().setMetadataKeys(new HashMap<>());
+                String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
+                String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
+                metadata.getMetadata().setMetadataKey(encryptedMetadataKey);
+
+                object = metadata;
+                metadataExists = false;
+            }
 
-            metadataExists = metadataPair.first;
-            DecryptedFolderMetadata metadata = metadataPair.second;
+            // todo fail if no metadata
 
+//            metadataExists = metadataPair.getFirst();
+//            DecryptedFolderMetadataFile metadata = metadataPair.getSecond();
+
+            // TODO E2E: check counter: must be less than our counter, check rest: signature, etc
             /**** E2E *****/
 
             // check name collision
-            RemoteOperationResult collisionResult = checkNameCollision(client, metadata, parentFile.isEncrypted());
+            List<String> fileNames = new ArrayList<>();
+            if (object instanceof DecryptedFolderMetadataFileV1 metadata) {
+                for (DecryptedFile file : metadata.getFiles().values()) {
+                    fileNames.add(file.getEncrypted().getFilename());
+                }
+            } else {
+                for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file :
+                    ((DecryptedFolderMetadataFile) object).getMetadata().getFiles().values()) {
+                    fileNames.add(file.getFilename());
+                }
+            }
+
+            RemoteOperationResult collisionResult = checkNameCollision(client, fileNames, parentFile.isEncrypted());
             if (collisionResult != null) {
                 result = collisionResult;
                 return collisionResult;
@@ -509,18 +560,24 @@ public class UploadFileOperation extends SyncOperation {
             // IV, always generate new one
             byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
 
-            EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv);
+            EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv);
 
             // new random file name, check if it exists in metadata
-            String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
+            String encryptedFileName = EncryptionUtils.generateUid();
 
-            while (metadata.getFiles().get(encryptedFileName) != null) {
-                encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
+            if (object instanceof DecryptedFolderMetadataFileV1 metadata) {
+                while (metadata.getFiles().get(encryptedFileName) != null) {
+                    encryptedFileName = EncryptionUtils.generateUid();
+                }
+            } else {
+                while (((DecryptedFolderMetadataFile) object).getMetadata().getFiles().get(encryptedFileName) != null) {
+                    encryptedFileName = EncryptionUtils.generateUid();
+                }
             }
 
             File encryptedTempFile = File.createTempFile("encFile", encryptedFileName);
             FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
-            fileOutputStream.write(encryptedFile.encryptedBytes);
+            fileOutputStream.write(encryptedFile.getEncryptedBytes());
             fileOutputStream.close();
 
             /***** E2E *****/
@@ -556,7 +613,6 @@ public class UploadFileOperation extends SyncOperation {
                 size = new File(mFile.getStoragePath()).length();
             }
 
-
             updateSize(size);
 
             /// perform the upload
@@ -605,48 +661,82 @@ public class UploadFileOperation extends SyncOperation {
                 mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + originalFile.getName());
                 mFile.setRemotePath(parentFile.getRemotePath() + encryptedFileName);
 
-                // update metadata
-                DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
-                DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
-                data.setFilename(mFile.getDecryptedFileName());
-                data.setMimetype(mFile.getMimeType());
-                data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
 
-                decryptedFile.setEncrypted(data);
-                decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv));
-                decryptedFile.setAuthenticationTag(encryptedFile.authenticationTag);
+                if (object instanceof DecryptedFolderMetadataFileV1 metadata) {
+                    // update metadata
+                    DecryptedFile decryptedFile = new DecryptedFile();
+                    Data data = new Data();
+                    data.setFilename(mFile.getDecryptedFileName());
+                    data.setMimetype(mFile.getMimeType());
+                    data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
 
-                metadata.getFiles().put(encryptedFileName, decryptedFile);
+                    decryptedFile.setEncrypted(data);
+                    decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv));
+                    decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
 
-                EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
-                                                                                                        publicKey,
-                                                                                                        arbitraryDataProvider,
-                                                                                                        user,
-                                                                                                        parentFile.getLocalId());
+                    metadata.getFiles().put(encryptedFileName, decryptedFile);
 
-                String serializedFolderMetadata;
+                    EncryptedFolderMetadataFileV1 encryptedFolderMetadata =
+                        EncryptionUtils.encryptFolderMetadata(metadata,
+                                                              publicKey,
+                                                              parentFile.getLocalId(),
+                                                              user,
+                                                              arbitraryDataProvider
+                                                             );
 
-                // check if we need metadataKeys
-                if (metadata.getMetadata().getMetadataKey() != null) {
-                    serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true);
-                } else {
-                    serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
-                }
+                    String serializedFolderMetadata;
 
-                // upload metadata
-                EncryptionUtils.uploadMetadata(parentFile,
-                                               serializedFolderMetadata,
-                                               token,
-                                               client,
-                                               metadataExists,
-                                               arbitraryDataProvider,
-                                               user);
+                    // check if we need metadataKeys
+                    if (metadata.getMetadata().getMetadataKey() != null) {
+                        serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true);
+                    } else {
+                        serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
+                    }
 
-                // unlock
-                result = EncryptionUtils.unlockFolder(parentFile, client, token);
+                    // upload metadata
+                    EncryptionUtils.uploadMetadata(parentFile,
+                                                   serializedFolderMetadata,
+                                                   token,
+                                                   client,
+                                                   metadataExists,
+                                                   E2EVersion.V1_2,
+                                                   "",
+                                                   arbitraryDataProvider,
+                                                   user);
+
+                    // unlock
+                    result = EncryptionUtils.unlockFolderV1(parentFile, client, token);
+
+                    if (result.isSuccess()) {
+                        token = null;
+                    }
+                } else {
+                    DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object;
+                    encryptionUtilsV2.addFileToMetadata(
+                        encryptedFileName,
+                        mFile,
+                        iv,
+                        encryptedFile.getAuthenticationTag(),
+                        key,
+                        metadata,
+                        getStorageManager());
+
+                    // upload metadata
+                    encryptionUtilsV2.serializeAndUploadMetadata(parentFile,
+                                                                 metadata,
+                                                                 token,
+                                                                 client,
+                                                                 metadataExists,
+                                                                 mContext,
+                                                                 user,
+                                                                 getStorageManager());
 
-                if (result.isSuccess()) {
-                    token = null;
+                    // unlock
+                    result = EncryptionUtils.unlockFolder(parentFile, client, token);
+
+                    if (result.isSuccess()) {
+                        token = null;
+                    }
                 }
             }
         } catch (FileNotFoundException e) {
@@ -717,24 +807,24 @@ public class UploadFileOperation extends SyncOperation {
         final BatteryStatus battery = powerManagementService.getBattery();
         if (mWhileChargingOnly && !battery.isCharging()) {
             Log_OC.d(TAG, "Upload delayed until the device is charging: " + getRemotePath());
-            remoteOperationResult =  new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING);
+            remoteOperationResult = new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING);
         }
 
         // check that device is not in power save mode
         if (!mIgnoringPowerSaveMode && powerManagementService.isPowerSavingEnabled()) {
             Log_OC.d(TAG, "Upload delayed because device is in power save mode: " + getRemotePath());
-            remoteOperationResult =  new RemoteOperationResult(ResultCode.DELAYED_IN_POWER_SAVE_MODE);
+            remoteOperationResult = new RemoteOperationResult(ResultCode.DELAYED_IN_POWER_SAVE_MODE);
         }
 
         // check if the file continues existing before schedule the operation
         if (!originalFile.exists()) {
             Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore");
-            remoteOperationResult =  new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
+            remoteOperationResult = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
         }
 
         // check that internet is not behind walled garden
         if (!connectivityService.getConnectivity().isConnected() || connectivityService.isInternetWalled()) {
-            remoteOperationResult =  new RemoteOperationResult(ResultCode.NO_NETWORK_CONNECTION);
+            remoteOperationResult = new RemoteOperationResult(ResultCode.NO_NETWORK_CONNECTION);
         }
 
         return remoteOperationResult;
@@ -903,7 +993,7 @@ public class UploadFileOperation extends SyncOperation {
 
     private void updateSize(long size) {
         OCUpload ocUpload = uploadsStorageManager.getUploadById(getOCUploadId());
-        if(ocUpload != null){
+        if (ocUpload != null) {
             ocUpload.setFileSize(size);
             uploadsStorageManager.updateUpload(ocUpload);
         }
@@ -928,7 +1018,7 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     private RemoteOperationResult copyFile(File originalFile, String expectedPath) throws OperationCancelledException,
-            IOException {
+        IOException {
         if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY && !mOriginalStoragePath.equals(expectedPath)) {
             String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) +
                 mFile.getRemotePath();
@@ -947,18 +1037,18 @@ public class UploadFileOperation extends SyncOperation {
 
     @CheckResult
     private RemoteOperationResult checkNameCollision(OwnCloudClient client,
-                                                     DecryptedFolderMetadata metadata,
+                                                     List<String> fileNames,
                                                      boolean encrypted)
         throws OperationCancelledException {
         Log_OC.d(TAG, "Checking name collision in server");
 
-        if (existsFile(client, mRemotePath, metadata, encrypted)) {
+        if (existsFile(client, mRemotePath, fileNames, encrypted)) {
             switch (mNameCollisionPolicy) {
                 case CANCEL:
                     Log_OC.d(TAG, "File exists; canceling");
                     throw new OperationCancelledException();
                 case RENAME:
-                    mRemotePath = getNewAvailableRemotePath(client, mRemotePath, metadata, encrypted);
+                    mRemotePath = getNewAvailableRemotePath(client, mRemotePath, fileNames, encrypted);
                     mWasRenamed = true;
                     createNewOCFile(mRemotePath);
                     Log_OC.d(TAG, "File renamed as " + mRemotePath);
@@ -1041,15 +1131,14 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     /**
-     * Checks the existence of the folder where the current file will be uploaded both
-     * in the remote server and in the local database.
+     * Checks the existence of the folder where the current file will be uploaded both in the remote server and in the
+     * local database.
      * <p/>
-     * If the upload is set to enforce the creation of the folder, the method tries to
-     * create it both remote and locally.
+     * If the upload is set to enforce the creation of the folder, the method tries to create it both remote and
+     * locally.
      *
      * @param pathToGrant Full remote path whose existence will be granted.
-     * @return An {@link OCFile} instance corresponding to the folder where the file
-     * will be uploaded.
+     * @return An {@link OCFile} instance corresponding to the folder where the file will be uploaded.
      */
     private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnCloudClient client) {
         RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, false);
@@ -1075,7 +1164,7 @@ public class UploadFileOperation extends SyncOperation {
     private OCFile createLocalFolder(String remotePath) {
         String parentPath = new File(remotePath).getParent();
         parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ?
-                parentPath : parentPath + OCFile.PATH_SEPARATOR;
+            parentPath : parentPath + OCFile.PATH_SEPARATOR;
         OCFile parent = getStorageManager().getFileByPath(parentPath);
         if (parent == null) {
             parent = createLocalFolder(parentPath);
@@ -1105,8 +1194,8 @@ public class UploadFileOperation extends SyncOperation {
         newFile.setMimeType(mFile.getMimeType());
         newFile.setModificationTimestamp(mFile.getModificationTimestamp());
         newFile.setModificationTimestampAtLastSyncForData(
-                mFile.getModificationTimestampAtLastSyncForData()
-        );
+            mFile.getModificationTimestampAtLastSyncForData()
+                                                         );
         newFile.setEtag(mFile.getEtag());
         newFile.setLastSyncDateForProperties(mFile.getLastSyncDateForProperties());
         newFile.setLastSyncDateForData(mFile.getLastSyncDateForData());
@@ -1117,15 +1206,16 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     /**
-     * Returns a new and available (does not exists on the server) remotePath.
-     * This adds an incremental suffix.
+     * Returns a new and available (does not exists on the server) remotePath. This adds an incremental suffix.
      *
      * @param client     OwnCloud client
      * @param remotePath remote path of the file
-     * @param metadata   metadata of encrypted folder
+     * @param fileNames  list of decrypted file names
      * @return new remote path
      */
-    private String getNewAvailableRemotePath(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
+    private String getNewAvailableRemotePath(OwnCloudClient client,
+                                             String remotePath,
+                                             List<String> fileNames,
                                              boolean encrypted) {
         int extPos = remotePath.lastIndexOf('.');
         String suffix;
@@ -1142,20 +1232,22 @@ public class UploadFileOperation extends SyncOperation {
         do {
             suffix = " (" + count + ")";
             newPath = extPos >= 0 ? remotePathWithoutExtension + suffix + "." + extension : remotePath + suffix;
-            exists = existsFile(client, newPath, metadata, encrypted);
+            exists = existsFile(client, newPath, fileNames, encrypted);
             count++;
         } while (exists);
 
         return newPath;
     }
 
-    private boolean existsFile(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
+    private boolean existsFile(OwnCloudClient client,
+                               String remotePath,
+                               List<String> fileNames,
                                boolean encrypted) {
         if (encrypted) {
             String fileName = new File(remotePath).getName();
 
-            for (DecryptedFolderMetadata.DecryptedFile file : metadata.getFiles().values()) {
-                if (file.getEncrypted().getFilename().equalsIgnoreCase(fileName)) {
+            for (String name : fileNames) {
+                if (name.equalsIgnoreCase(fileName)) {
                     return true;
                 }
             }
@@ -1169,9 +1261,8 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     /**
-     * Allows to cancel the actual upload operation. If actual upload operating
-     * is in progress it is cancelled, if upload preparation is being performed
-     * upload will not take place.
+     * Allows to cancel the actual upload operation. If actual upload operating is in progress it is cancelled, if
+     * upload preparation is being performed upload will not take place.
      */
     public void cancel(ResultCode cancellationReason) {
         if (mUploadOperation == null) {
@@ -1240,7 +1331,7 @@ public class UploadFileOperation extends SyncOperation {
                     int nRead;
                     byte[] buf = new byte[4096];
                     while (!mCancellationRequested.get() &&
-                            (nRead = in.read(buf)) > -1) {
+                        (nRead = in.read(buf)) > -1) {
                         out.write(buf, 0, nRead);
                     }
                     out.flush();
@@ -1259,7 +1350,7 @@ public class UploadFileOperation extends SyncOperation {
                     }
                 } catch (Exception e) {
                     Log_OC.d(TAG, "Weird exception while closing input stream for " +
-                            mOriginalStoragePath + " (ignoring)", e);
+                        mOriginalStoragePath + " (ignoring)", e);
                 }
                 try {
                     if (out != null) {
@@ -1267,7 +1358,7 @@ public class UploadFileOperation extends SyncOperation {
                     }
                 } catch (Exception e) {
                     Log_OC.d(TAG, "Weird exception while closing output stream for " +
-                            targetFile.getAbsolutePath() + " (ignoring)", e);
+                        targetFile.getAbsolutePath() + " (ignoring)", e);
                 }
             }
         }
@@ -1322,9 +1413,8 @@ public class UploadFileOperation extends SyncOperation {
     /**
      * Saves a OC File after a successful upload.
      * <p>
-     * A PROPFIND is necessary to keep the props in the local database
-     * synchronized with the server, specially the modification time and Etag
-     * (where available)
+     * A PROPFIND is necessary to keep the props in the local database synchronized with the server, specially the
+     * modification time and Etag (where available)
      */
     private void saveUploadedFile(OwnCloudClient client) {
         OCFile file = mFile;
@@ -1379,7 +1469,7 @@ public class UploadFileOperation extends SyncOperation {
 
         // generate new Thumbnail
         final ThumbnailsCacheManager.ThumbnailGenerationTask task =
-                new ThumbnailsCacheManager.ThumbnailGenerationTask(getStorageManager(), user);
+            new ThumbnailsCacheManager.ThumbnailGenerationTask(getStorageManager(), user);
         task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, file.getRemoteId()));
     }
 

+ 34 - 0
app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt

@@ -0,0 +1,34 @@
+/*
+ * Nextcloud Android client application
+ *
+ *  @author Álvaro Brey
+ *  Copyright (C) 2023 Álvaro Brey
+ *  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
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package com.owncloud.android.providers
+
+/**
+ * This is a data class that holds the configuration for the user and group searchable.
+ * As we cannot access searchable providers in runtime, injecting a singleton into them is the only way to change their
+ * config.
+ */
+data class UsersAndGroupsSearchConfig(var searchOnlyUsers: Boolean = false) {
+    fun reset() {
+        searchOnlyUsers = false
+    }
+}

+ 16 - 5
app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java

@@ -34,6 +34,7 @@ import android.os.Looper;
 import android.os.ParcelFileDescriptor;
 import android.provider.BaseColumns;
 import android.text.TextUtils;
+import android.util.Log;
 import android.widget.Toast;
 
 import com.nextcloud.client.account.User;
@@ -116,6 +117,8 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
 
     @Inject
     protected UserAccountManager accountManager;
+    @Inject
+    protected UsersAndGroupsSearchConfig searchConfig;
 
     private static final Map<String, ShareType> sShareTypes = new HashMap<>();
 
@@ -193,6 +196,10 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
     }
 
     private Cursor searchForUsersOrGroups(Uri uri) {
+
+        // TODO check searchConfig and filter results
+        Log.d(TAG, "searchForUsersOrGroups: searchConfig only users: " + searchConfig.getSearchOnlyUsers());
+
         String lastPathSegment = uri.getLastPathSegment();
 
         if (lastPathSegment == null) {
@@ -206,15 +213,14 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
         String userQuery = lastPathSegment.toLowerCase(Locale.ROOT);
 
         // request to the OC server about users and groups matching userQuery
-        GetShareesRemoteOperation searchRequest = new GetShareesRemoteOperation(userQuery, REQUESTED_PAGE,
+        GetShareesRemoteOperation searchRequest = new GetShareesRemoteOperation(userQuery,
+                                                                                REQUESTED_PAGE,
                                                                                 RESULTS_PER_PAGE);
-        RemoteOperationResult result = searchRequest.execute(user, getContext());
+        RemoteOperationResult<ArrayList<JSONObject>> result = searchRequest.execute(user, getContext());
         List<JSONObject> names = new ArrayList<>();
 
         if (result.isSuccess()) {
-            for (Object o : result.getData()) {
-                names.add((JSONObject) o);
-            }
+            names = result.getResultData();
         } else {
             showErrorMessage(result);
         }
@@ -272,6 +278,11 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
                         status = new Status(StatusType.OFFLINE, "", "", -1);
                     }
 
+                    if (searchConfig.getSearchOnlyUsers() && type != ShareType.USER) {
+                        // skip all types but users, as E2E secure share is only allowed to users on same server
+                        continue;
+                    }
+
                     switch (type) {
                         case GROUP:
                             displayName = userName;

+ 11 - 2
app/src/main/java/com/owncloud/android/services/OperationsService.java

@@ -44,6 +44,7 @@ import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.java.util.Optional;
 import com.nextcloud.utils.extensions.IntentExtensionsKt;
 import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudAccount;
@@ -140,6 +141,7 @@ public class OperationsService extends Service {
         mUndispatchedFinishedOperations = new ConcurrentHashMap<>();
 
     @Inject UserAccountManager accountManager;
+    @Inject ArbitraryDataProvider arbitraryDataProvider;
 
     private static class Target {
         public Uri mServerUrl;
@@ -610,7 +612,10 @@ public class OperationsService extends Service {
                                                                    sharePassword,
                                                                    expirationDateInMillis,
                                                                    hideFileDownload,
-                                                                   fileDataStorageManager);
+                                                                   fileDataStorageManager,
+                                                                   getApplicationContext(),
+                                                                   user,
+                                                                   arbitraryDataProvider);
 
                             if (operationIntent.hasExtra(EXTRA_SHARE_PUBLIC_LABEL)) {
                                 createShareWithShareeOperation.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL));
@@ -654,7 +659,11 @@ public class OperationsService extends Service {
                         shareId = operationIntent.getLongExtra(EXTRA_SHARE_ID, -1);
 
                         if (shareId > 0) {
-                            operation = new UnshareOperation(remotePath, shareId, fileDataStorageManager);
+                            operation = new UnshareOperation(remotePath,
+                                                             shareId,
+                                                             fileDataStorageManager,
+                                                             user,
+                                                             getApplicationContext());
                         }
                         break;
 

+ 11 - 1
app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java

@@ -81,6 +81,7 @@ import com.owncloud.android.operations.UpdateNoteForShareOperation;
 import com.owncloud.android.operations.UpdateShareInfoOperation;
 import com.owncloud.android.operations.UpdateSharePermissionsOperation;
 import com.owncloud.android.operations.UpdateShareViaLinkOperation;
+import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
 import com.owncloud.android.providers.UsersAndGroupsSearchProvider;
 import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
@@ -182,6 +183,12 @@ public abstract class FileActivity extends DrawerActivity
     @Inject
     EditorUtils editorUtils;
 
+    @Inject
+    UsersAndGroupsSearchConfig usersAndGroupsSearchConfig;
+
+    @Inject
+    ArbitraryDataProvider arbitraryDataProvider;
+
     @Override
     public void showFiles(boolean onDeviceOnly) {
         // must be specialized in subclasses
@@ -203,6 +210,7 @@ public abstract class FileActivity extends DrawerActivity
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        usersAndGroupsSearchConfig.reset();
         mHandler = new Handler();
         mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
         User user = null;
@@ -907,7 +915,9 @@ public abstract class FileActivity extends DrawerActivity
     protected void doShareWith(String shareeName, ShareType shareType) {
         FileDetailFragment fragment = getFileDetailFragment();
         if (fragment != null) {
-            fragment.initiateSharingProcess(shareeName, shareType);
+            fragment.initiateSharingProcess(shareeName,
+                                            shareType,
+                                            usersAndGroupsSearchConfig.getSearchOnlyUsers());
         }
     }
 

+ 2 - 2
app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java

@@ -474,7 +474,7 @@ public class SettingsActivity extends PreferenceActivity
     }
 
     private void setupE2EMnemonicPreference(PreferenceCategory preferenceCategoryMore) {
-        String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC);
+        String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim();
 
         Preference pMnemonic = findPreference("mnemonic");
         if (pMnemonic != null) {
@@ -991,7 +991,7 @@ public class SettingsActivity extends PreferenceActivity
                 RequestCredentialsActivity.KEY_CHECK_RESULT_TRUE) {
 
                 ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this);
-                String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC);
+                String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim();
 
                 AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.FallbackTheming_Dialog);
                 AlertDialog alertDialog = builder.setTitle(R.string.prefs_e2e_mnemonic)

+ 2 - 1
app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java

@@ -143,7 +143,8 @@ public class ShareActivity extends FileActivity {
         getSupportFragmentManager().beginTransaction().replace(R.id.share_fragment_container,
                                                                FileDetailsSharingProcessFragment.newInstance(getFile(),
                                                                                                              shareeName,
-                                                                                                             shareType),
+                                                                                                             shareType,
+                                                                                                             false),
                                                                FileDetailsSharingProcessFragment.TAG)
             .commit();
     }

+ 13 - 10
app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java

@@ -25,7 +25,6 @@ import com.nextcloud.ui.ImageDetailFragment;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment;
 import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
-import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 
 import androidx.annotation.NonNull;
@@ -39,15 +38,20 @@ import androidx.fragment.app.FragmentStatePagerAdapter;
 public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
     private final OCFile file;
     private final User user;
+    private final boolean showSharingTab;
 
     private FileDetailSharingFragment fileDetailSharingFragment;
     private FileDetailActivitiesFragment fileDetailActivitiesFragment;
     private ImageDetailFragment imageDetailFragment;
 
-    public FileDetailTabAdapter(FragmentManager fm, OCFile file, User user) {
+    public FileDetailTabAdapter(FragmentManager fm,
+                                OCFile file,
+                                User user,
+                                boolean showSharingTab) {
         super(fm);
         this.file = file;
         this.user = user;
+        this.showSharingTab = showSharingTab;
     }
 
     @NonNull
@@ -81,17 +85,16 @@ public class FileDetailTabAdapter extends FragmentStatePagerAdapter {
 
     @Override
     public int getCount() {
-        if (file.isEncrypted()) {
-            if (EncryptionUtils.supportsSecureFiledrop(file, user)) {
+        if (showSharingTab) {
+            if (MimeTypeUtil.isImage(file)) {
+                return 3;
+            }
+            return 2;
+        } else {
+            if (MimeTypeUtil.isImage(file)) {
                 return 2;
             }
-            // sharing not allowed for encrypted files, thus only show first tab (activities)
             return 1;
         }
-        // unencrypted files/folders
-        if (MimeTypeUtil.isImage(file)) {
-            return 3;
-        }
-        return 2;
     }
 }

+ 19 - 6
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -54,12 +54,13 @@ import com.owncloud.android.databinding.GridItemBinding;
 import com.owncloud.android.databinding.ListFooterBinding;
 import com.owncloud.android.databinding.ListHeaderBinding;
 import com.owncloud.android.databinding.ListItemBinding;
-import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.VirtualFolderType;
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.lib.common.OwnCloudClientFactory;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
@@ -285,6 +286,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         for (OCFile file : mFiles) {
             if (file.getRemoteId().equals(fileId)) {
                 file.setEncrypted(encrypted);
+                file.setE2eCounter(0L);
                 mStorageManager.saveFile(file);
 
                 break;
@@ -294,6 +296,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
         for (OCFile file : mFilesAll) {
             if (file.getRemoteId().equals(fileId)) {
                 file.setEncrypted(encrypted);
+                file.setE2eCounter(0L);
             }
         }
 
@@ -435,7 +438,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 return;
             }
 
-            ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, searchType);
+            ocFileListDelegate.bindGridViewHolder(gridViewHolder, file, currentDirectory, searchType);
             checkVisibilityOfMoreButtons(gridViewHolder);
             checkVisibilityOfFileFeaturesLayout(gridViewHolder);
 
@@ -890,19 +893,29 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 
                 OCFile parentFolder = mStorageManager.getFileById(ocFile.getParentId());
                 if (parentFolder != null && (ocFile.isEncrypted() || parentFolder.isEncrypted())) {
-                    DecryptedFolderMetadata metadata = RefreshFolderOperation.getDecryptedFolderMetadata(
+                    Object object = RefreshFolderOperation.getDecryptedFolderMetadata(
                         true,
                         parentFolder,
                         OwnCloudClientFactory.createOwnCloudClient(user.toPlatformAccount(), activity),
                         user,
                         activity);
 
-                    if (metadata == null) {
+                    if (object == null) {
                         throw new IllegalStateException("metadata is null!");
                     }
 
-                    // update ocFile
-                    RefreshFolderOperation.updateFileNameForEncryptedFile(mStorageManager, metadata, ocFile);
+                    if (object instanceof DecryptedFolderMetadataFileV1) {
+                        // update ocFile
+                        RefreshFolderOperation.updateFileNameForEncryptedFileV1(mStorageManager,
+                                                                                (DecryptedFolderMetadataFileV1) object,
+                                                                                ocFile);
+                    } else {
+                        // update ocFile
+                        RefreshFolderOperation.updateFileNameForEncryptedFile(mStorageManager,
+                                                                              (DecryptedFolderMetadataFile) object,
+                                                                              ocFile);
+                    }
+
                     ocFile = mStorageManager.saveFileWithParent(ocFile, activity);
                 }
 

+ 4 - 2
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt

@@ -205,6 +205,7 @@ class OCFileListDelegate(
     fun bindGridViewHolder(
         gridViewHolder: ListGridImageViewHolder,
         file: OCFile,
+        currentDirectory: OCFile?,
         searchType: SearchType?
     ) {
         // thumbnail
@@ -250,8 +251,9 @@ class OCFileListDelegate(
                 file.isEncrypted ||
                 file.isEncrypted &&
                 !EncryptionUtils.supportsSecureFiledrop(file, user) ||
-                searchType == SearchType.FAVORITE_SEARCH
-            )
+                searchType == SearchType.FAVORITE_SEARCH ||
+                file.isFolder && currentDirectory?.isEncrypted ?: false
+            ) // sharing an encrypted subfolder is not possible
         if (shouldHideShare) {
             gridViewHolder.shared.visibility = View.GONE
         } else {

+ 25 - 20
app/src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.kt

@@ -45,12 +45,12 @@ import com.owncloud.android.datamodel.ArbitraryDataProvider
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
 import com.owncloud.android.lib.common.accounts.AccountUtils
 import com.owncloud.android.lib.common.utils.Log_OC
-import com.owncloud.android.lib.resources.users.DeletePublicKeyOperation
-import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation
-import com.owncloud.android.lib.resources.users.GetPublicKeyOperation
-import com.owncloud.android.lib.resources.users.SendCSROperation
-import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation
-import com.owncloud.android.utils.CsrHelper
+import com.owncloud.android.lib.resources.e2ee.CsrHelper
+import com.owncloud.android.lib.resources.users.DeletePublicKeyRemoteOperation
+import com.owncloud.android.lib.resources.users.GetPrivateKeyRemoteOperation
+import com.owncloud.android.lib.resources.users.GetPublicKeyRemoteOperation
+import com.owncloud.android.lib.resources.users.SendCSRRemoteOperation
+import com.owncloud.android.lib.resources.users.StorePrivateKeyRemoteOperation
 import com.owncloud.android.utils.EncryptionUtils
 import com.owncloud.android.utils.theme.ViewThemeUtils
 import java.io.IOException
@@ -175,7 +175,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
 
         try {
             val privateKey = task?.get()
-            val mnemonicUnchanged = binding.encryptionPasswordInput.text.toString()
+            val mnemonicUnchanged = binding.encryptionPasswordInput.text.toString().trim()
             val mnemonic =
                 binding.encryptionPasswordInput.text.toString().replace("\\s".toRegex(), "")
                     .lowercase()
@@ -294,11 +294,11 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
             // if available
             //  - store public key
             //  - decrypt private key, store unencrypted private key in database
-            val context = mWeakContext.get()
-            val publicKeyOperation = GetPublicKeyOperation()
+            val context = mWeakContext.get() ?: return null
+            val publicKeyOperation = GetPublicKeyRemoteOperation()
             val user = user ?: return null
 
-            val publicKeyResult = publicKeyOperation.execute(user, context)
+            val publicKeyResult = publicKeyOperation.executeNextcloudClient(user, context)
 
             if (publicKeyResult.isSuccess) {
                 Log_OC.d(TAG, "public key successful downloaded for " + user.accountName)
@@ -317,7 +317,7 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                 return null
             }
 
-            val privateKeyResult = GetPrivateKeyOperation().execute(user, context)
+            val privateKeyResult = GetPrivateKeyRemoteOperation().executeNextcloudClient(user, context)
             if (privateKeyResult.isSuccess) {
                 Log_OC.d(TAG, "private key successful downloaded for " + user!!.accountName)
                 keyResult = KEY_EXISTING_USED
@@ -387,6 +387,11 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                 val context = mWeakContext.get()
                 val publicKeyString: String
 
+                if (context == null) {
+                    keyResult = KEY_FAILED
+                    return ""
+                }
+
                 // Create public/private key pair
                 val keyPair = EncryptionUtils.generateKeyPair()
 
@@ -395,12 +400,12 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                 val user = user ?: return ""
 
                 val userId = accountManager.getUserData(user.toPlatformAccount(), AccountUtils.Constants.KEY_USER_ID)
-                val urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, userId)
-                val operation = SendCSROperation(urlEncoded)
-                val result = operation.execute(user, context)
+                val urlEncoded = CsrHelper().generateCsrPemEncodedString(keyPair, userId)
+                val operation = SendCSRRemoteOperation(urlEncoded)
+                val result = operation.executeNextcloudClient(user, context)
 
                 if (result.isSuccess) {
-                    publicKeyString = result.data[0] as String
+                    publicKeyString = result.resultData
                     if (!EncryptionUtils.isMatchingKeys(keyPair, publicKeyString)) {
                         EncryptionUtils.reportE2eError(arbitraryDataProvider, user)
                         throw RuntimeException("Wrong CSR returned")
@@ -420,8 +425,8 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                 )
 
                 // upload encryptedPrivateKey
-                val storePrivateKeyOperation = StorePrivateKeyOperation(encryptedPrivateKey)
-                val storePrivateKeyResult = storePrivateKeyOperation.execute(user, context)
+                val storePrivateKeyOperation = StorePrivateKeyRemoteOperation(encryptedPrivateKey)
+                val storePrivateKeyResult = storePrivateKeyOperation.executeNextcloudClient(user, context)
                 if (storePrivateKeyResult.isSuccess) {
                     Log_OC.d(TAG, "private key success")
                     arbitraryDataProvider?.storeOrUpdateKeyValue(
@@ -441,10 +446,10 @@ class SetupEncryptionDialogFragment : DialogFragment(), Injectable {
                     )
                     keyResult = KEY_CREATED
 
-                    return storePrivateKeyResult.data[0] as String
+                    return storePrivateKeyResult.resultData
                 } else {
-                    val deletePublicKeyOperation = DeletePublicKeyOperation()
-                    deletePublicKeyOperation.execute(user, context)
+                    val deletePublicKeyOperation = DeletePublicKeyRemoteOperation()
+                    deletePublicKeyOperation.executeNextcloudClient(user, context)
                 }
             } catch (e: Exception) {
                 Log_OC.e(TAG, e.message)

+ 30 - 6
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -133,11 +133,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
      * @param user         Currently active user
      * @return New fragment with arguments set
      */
-    public static FileDetailFragment newInstance(OCFile fileToDetail, OCFile parentFile, User user) {
+    public static FileDetailFragment newInstance(OCFile fileToDetail, OCFile parentFolder, User user) {
         FileDetailFragment frag = new FileDetailFragment();
         Bundle args = new Bundle();
         args.putParcelable(ARG_FILE, fileToDetail);
-        args.putParcelable(ARG_PARENT_FOLDER, parentFile);
+        args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
         args.putParcelable(ARG_USER, user);
         frag.setArguments(args);
         return frag;
@@ -304,7 +304,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.drawer_item_activities).setIcon(R.drawable.ic_activity));
 
 
-        if (!getFile().isEncrypted() || EncryptionUtils.supportsSecureFiledrop(getFile(), user)) {
+        if (showSharingTab()) {
             binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.shared_via_users));
         }
 
@@ -314,7 +314,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
 
         viewThemeUtils.material.themeTabLayout(binding.tabLayout);
 
-        final FileDetailTabAdapter adapter = new FileDetailTabAdapter(getFragmentManager(), getFile(), user);
+        final FileDetailTabAdapter adapter = new FileDetailTabAdapter(getFragmentManager(),
+                                                                      getFile(),
+                                                                      user,
+                                                                      showSharingTab());
         binding.pager.setAdapter(adapter);
         binding.pager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout) {
             @Override
@@ -733,11 +736,14 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
      * @param shareeName
      * @param shareType
      */
-    public void initiateSharingProcess(String shareeName, ShareType shareType) {
+    public void initiateSharingProcess(String shareeName,
+                                       ShareType shareType,
+                                       boolean secureShare) {
         requireActivity().getSupportFragmentManager().beginTransaction().add(R.id.sharing_frame_container,
                                                                              FileDetailsSharingProcessFragment.newInstance(getFile(),
                                                                                                                            shareeName,
-                                                                                                                           shareType),
+                                                                                                                           shareType,
+                                                                                                                           secureShare),
                                                                              FileDetailsSharingProcessFragment.TAG)
             .commit();
 
@@ -801,6 +807,24 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
         }
     }
 
+    private boolean showSharingTab() {
+        if (getFile().isEncrypted()) {
+            if (parentFolder == null) {
+                parentFolder = storageManager.getFileById(getFile().getParentId());
+            }
+            if (EncryptionUtils.supportsSecureFiledrop(getFile(), user) && !parentFolder.isEncrypted()) {
+                return true;
+            } else {
+                // sharing not allowed for encrypted files, thus only show first tab (activities)
+                // sharing not allowed for encrypted subfolders
+                return false;
+            }
+        } else {
+            // unencrypted files/folders
+            return true;
+        }
+    }
+
     /**
      * Helper class responsible for updating the progress bar shown for file downloading.
      */

+ 43 - 14
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java

@@ -59,6 +59,7 @@ import com.owncloud.android.lib.resources.shares.ShareType;
 import com.owncloud.android.lib.resources.status.NextcloudVersion;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
+import com.owncloud.android.providers.UsersAndGroupsSearchConfig;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.ui.adapter.ShareeListAdapter;
@@ -108,6 +109,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
     @Inject UserAccountManager accountManager;
     @Inject ClientFactory clientFactory;
     @Inject ViewThemeUtils viewThemeUtils;
+    @Inject UsersAndGroupsSearchConfig searchConfig;
 
     public static FileDetailSharingFragment newInstance(OCFile file, User user) {
         FileDetailSharingFragment fragment = new FileDetailSharingFragment();
@@ -204,26 +206,52 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
         }
     }
 
+    @Override
+    public void onStart() {
+        super.onStart();
+        searchConfig.setSearchOnlyUsers(file.isEncrypted());
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        searchConfig.reset();
+    }
+
     private void setupView() {
         setShareWithYou();
 
-        if (file.isEncrypted()) {
-            binding.searchContainer.setVisibility(View.GONE);
-        } else {
-            FileDetailSharingFragmentHelper.setupSearchView(
-                (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE),
-                binding.searchView,
-                fileActivity.getComponentName());
-            viewThemeUtils.androidx.themeToolbarSearchView(binding.searchView);
+        OCFile parentFile = fileDataStorageManager.getFileById(file.getParentId());
 
-            if (file.canReshare()) {
-                binding.searchView.setQueryHint(getResources().getString(R.string.share_search));
+        FileDetailSharingFragmentHelper.setupSearchView(
+            (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE),
+            binding.searchView,
+            fileActivity.getComponentName());
+        viewThemeUtils.androidx.themeToolbarSearchView(binding.searchView);
+
+
+        if (file.canReshare()) {
+            if (file.isEncrypted() || (parentFile != null && parentFile.isEncrypted())) {
+                if (file.getE2eCounter() == -1) {
+                    // V1 cannot share
+                    binding.searchContainer.setVisibility(View.GONE);
+                } else {
+                    binding.searchView.setQueryHint(getResources().getString(R.string.secure_share_search));
+
+                    if (file.isSharedViaLink()) {
+                        binding.searchView.setQueryHint(getResources().getString(R.string.share_not_allowed_when_file_drop));
+                        binding.searchView.setInputType(InputType.TYPE_NULL);
+                        disableSearchView(binding.searchView);
+                    }
+                }
             } else {
-                binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed));
-                binding.searchView.setInputType(InputType.TYPE_NULL);
-                binding.pickContactEmailBtn.setVisibility(View.GONE);
-                disableSearchView(binding.searchView);
+                binding.searchView.setQueryHint(getResources().getString(R.string.share_search));
             }
+        } else {
+            binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed));
+            binding.searchView.setInputType(InputType.TYPE_NULL);
+            binding.pickContactEmailBtn.setVisibility(View.GONE);
+            disableSearchView(binding.searchView);
         }
     }
 
@@ -424,6 +452,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
      * before reading database.
      */
     public void refreshSharesFromDB() {
+        file = fileDataStorageManager.getFileById(file.getFileId());
         ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter();
 
         if (adapter == null) {

+ 38 - 5
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt

@@ -72,6 +72,7 @@ class FileDetailsSharingProcessFragment :
         private const val ARG_SCREEN_TYPE = "arg_screen_type"
         private const val ARG_RESHARE_SHOWN = "arg_reshare_shown"
         private const val ARG_EXP_DATE_SHOWN = "arg_exp_date_shown"
+        private const val ARG_SECURE_SHARE = "secure_share"
 
         // types of screens to be displayed
         const val SCREEN_TYPE_PERMISSION = 1 // permissions screen
@@ -81,11 +82,17 @@ class FileDetailsSharingProcessFragment :
          * fragment instance to be called while creating new share for internal and external share
          */
         @JvmStatic
-        fun newInstance(file: OCFile, shareeName: String, shareType: ShareType): FileDetailsSharingProcessFragment {
+        fun newInstance(
+            file: OCFile,
+            shareeName: String,
+            shareType: ShareType,
+            secureShare: Boolean
+        ): FileDetailsSharingProcessFragment {
             val args = Bundle()
             args.putParcelable(ARG_OCFILE, file)
             args.putSerializable(ARG_SHARE_TYPE, shareType)
             args.putString(ARG_SHAREE_NAME, shareeName)
+            args.putBoolean(ARG_SECURE_SHARE, secureShare)
             val fragment = FileDetailsSharingProcessFragment()
             fragment.arguments = args
             return fragment
@@ -127,6 +134,7 @@ class FileDetailsSharingProcessFragment :
     private var share: OCShare? = null
     private var isReShareShown: Boolean = true // show or hide reShare option
     private var isExpDateShown: Boolean = true // show or hide expiry date option
+    private var isSecureShare: Boolean = false
 
     private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null
 
@@ -156,6 +164,7 @@ class FileDetailsSharingProcessFragment :
             shareProcessStep = it.getInt(ARG_SCREEN_TYPE, SCREEN_TYPE_PERMISSION)
             isReShareShown = it.getBoolean(ARG_RESHARE_SHOWN, true)
             isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true)
+            isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false)
         }
 
         fileActivity = activity as FileActivity?
@@ -222,8 +231,22 @@ class FileDetailsSharingProcessFragment :
         binding.shareProcessEditShareLink.visibility = View.VISIBLE
         binding.shareProcessGroupTwo.visibility = View.GONE
 
-        if (share != null) setupModificationUI() else setupUpdateUI()
-        binding.shareProcessSetExpDateSwitch.visibility = if (isExpDateShown) View.VISIBLE else View.GONE
+        if (share != null) {
+            setupModificationUI()
+        } else {
+            setupUpdateUI()
+        }
+
+        if (isSecureShare) {
+            binding.shareProcessAdvancePermissionTitle.visibility = View.GONE
+        }
+
+        // show or hide expiry date
+        if (isExpDateShown && !isSecureShare) {
+            binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE
+        } else {
+            binding.shareProcessSetExpDateSwitch.visibility = View.GONE
+        }
         shareProcessStep = SCREEN_TYPE_PERMISSION
     }
 
@@ -310,7 +333,11 @@ class FileDetailsSharingProcessFragment :
         binding.shareProcessChangeNameSwitch.visibility = View.GONE
         binding.shareProcessChangeNameContainer.visibility = View.GONE
         binding.shareProcessHideDownloadCheckbox.visibility = View.GONE
-        binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
+        if (isSecureShare) {
+            binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
+        } else {
+            binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
+        }
         binding.shareProcessSetPasswordSwitch.visibility = View.GONE
 
         if (share != null) {
@@ -367,6 +394,11 @@ class FileDetailsSharingProcessFragment :
         binding.shareProcessPermissionUploadEditing.text =
             requireContext().resources.getString(R.string.link_share_allow_upload_and_editing)
         binding.shareProcessPermissionFileDrop.visibility = View.VISIBLE
+        if (isSecureShare) {
+            binding.shareProcessPermissionFileDrop.visibility = View.GONE
+            binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
+            binding.shareProcessSetExpDateSwitch.visibility = View.GONE
+        }
     }
 
     /**
@@ -569,7 +601,8 @@ class FileDetailsSharingProcessFragment :
                 binding.shareProcessEnterPassword.text.toString().trim(),
                 chosenExpDateInMills,
                 noteText,
-                binding.shareProcessChangeName.text.toString().trim()
+                binding.shareProcessChangeName.text.toString().trim(),
+                true
             )
         }
         removeCurrentFragment()

+ 47 - 42
app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -73,12 +73,11 @@ 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;
 import com.owncloud.android.datamodel.VirtualFolderType;
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile;
 import com.owncloud.android.lib.common.Creator;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
@@ -87,6 +86,7 @@ import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.e2ee.ToggleEncryptionRemoteOperation;
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
 import com.owncloud.android.lib.resources.files.ToggleFavoriteRemoteOperation;
+import com.owncloud.android.lib.resources.status.E2EVersion;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
@@ -116,6 +116,7 @@ import com.owncloud.android.ui.preview.PreviewMediaActivity;
 import com.owncloud.android.ui.preview.PreviewTextFileFragment;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.EncryptionUtilsV2;
 import com.owncloud.android.utils.FileSortOrder;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
@@ -1712,13 +1713,15 @@ public class OCFileListFragment extends ExtendedListFragment implements
             dialog.setTargetFragment(this, SETUP_ENCRYPTION_REQUEST_CODE);
             dialog.show(getParentFragmentManager(), SETUP_ENCRYPTION_DIALOG_TAG);
         } else {
+            // TODO E2E: if encryption fails, to not set it as encrypted!
             encryptFolder(file,
                           event.getLocalId(),
                           event.getRemoteId(),
                           event.getRemotePath(),
                           event.getShouldBeEncrypted(),
                           publicKey,
-                          privateKey);
+                          privateKey,
+                          storageManager);
         }
     }
 
@@ -1727,9 +1730,11 @@ public class OCFileListFragment extends ExtendedListFragment implements
                                String remoteId,
                                String remotePath,
                                boolean shouldBeEncrypted,
-                               String publicKey,
-                               String privateKey) {
+                               String publicKeyString,
+                               String privateKeyString,
+                               FileDataStorageManager storageManager) {
         try {
+            Log_OC.d(TAG, "encrypt folder " + folder.getRemoteId());
             User user = accountManager.getUser();
             OwnCloudClient client = clientFactory.create(user);
             RemoteOperationResult remoteOperationResult = new ToggleEncryptionRemoteOperation(localId,
@@ -1741,44 +1746,44 @@ public class OCFileListFragment extends ExtendedListFragment implements
                 // 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);
+                OCCapability ocCapability = mContainerActivity.getStorageManager().getCapability(user.getAccountName());
+
+                if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V2_0) {
+                    // Update metadata
+                    Pair<Boolean, DecryptedFolderMetadataFile> metadataPair = EncryptionUtils.retrieveMetadata(folder,
+                                                                                                               client,
+                                                                                                               privateKeyString,
+                                                                                                               publicKeyString,
+                                                                                                               storageManager,
+                                                                                                               user,
+                                                                                                               requireContext(),
+                                                                                                               arbitraryDataProvider);
+
+                    boolean metadataExists = metadataPair.first;
+                    DecryptedFolderMetadataFile metadata = metadataPair.second;
+
+                    new EncryptionUtilsV2().serializeAndUploadMetadata(folder,
+                                                                       metadata,
+                                                                       token,
+                                                                       client,
+                                                                       metadataExists,
+                                                                       requireContext(),
+                                                                       user,
+                                                                       storageManager);
+
+                    // unlock folder
+                    EncryptionUtils.unlockFolder(folder, client, token);
+
+                } else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_0 ||
+                    ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_1 ||
+                    ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V1_2
+                ) {
+                    // unlock folder
+                    EncryptionUtils.unlockFolderV1(folder, client, token);
+                } else if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.UNKNOWN) {
+                    throw new IllegalArgumentException("Unknown E2E version");
                 }
 
-                // upload metadata
-                EncryptionUtils.uploadMetadata(folder,
-                                               serializedFolderMetadata,
-                                               token,
-                                               client,
-                                               metadataExists,
-                                               arbitraryDataProvider,
-                                               user);
-
-                // unlock folder
-                EncryptionUtils.unlockFolder(folder, client, token);
-
                 mAdapter.setEncryptionAttributeForItemID(remoteId, shouldBeEncrypted);
             } else if (remoteOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
                 Snackbar.make(getRecyclerView(),
@@ -1790,7 +1795,7 @@ public class OCFileListFragment extends ExtendedListFragment implements
                               Snackbar.LENGTH_LONG).show();
             }
 
-        } catch (Exception e) {
+        } catch (Throwable e) {
             Log_OC.e(TAG, "Error creating encrypted folder", e);
         }
     }

+ 14 - 5
app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -568,13 +568,22 @@ public class FileOperationsHelper {
      * @param note                   note message for the receiver. Null or empty for no message
      * @param label                  new label
      */
-    public void shareFileWithSharee(OCFile file, String shareeName, ShareType shareType, int permissions,
-                                    boolean hideFileDownload, String password, long expirationTimeInMillis,
-                                    String note, String label) {
+    public void shareFileWithSharee(OCFile file,
+                                    String shareeName,
+                                    ShareType shareType,
+                                    int permissions,
+                                    boolean hideFileDownload,
+                                    String password,
+                                    long expirationTimeInMillis,
+                                    String note,
+                                    String label,
+                                    boolean showLoadingDialog) {
         if (file != null) {
             // TODO check capability?
-            fileActivity.showLoadingDialog(fileActivity.getApplicationContext().
-                                               getString(R.string.wait_a_moment));
+            if (showLoadingDialog) {
+                fileActivity.showLoadingDialog(fileActivity.getApplicationContext().
+                                                   getString(R.string.wait_a_moment));
+            }
 
             Intent service = new Intent(fileActivity, OperationsService.class);
             service.setAction(OperationsService.ACTION_CREATE_SHARE_WITH_SHAREE);

+ 0 - 82
app/src/main/java/com/owncloud/android/utils/CsrHelper.java

@@ -1,82 +0,0 @@
-package com.owncloud.android.utils;
-
-import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
-import org.bouncycastle.asn1.x500.X500Name;
-import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
-import org.bouncycastle.asn1.x509.BasicConstraints;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.asn1.x509.ExtensionsGenerator;
-import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
-import org.bouncycastle.crypto.util.PrivateKeyFactory;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
-import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
-import org.bouncycastle.operator.OperatorCreationException;
-import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
-import org.bouncycastle.pkcs.PKCS10CertificationRequest;
-import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
-import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
-
-import java.io.IOException;
-import java.security.KeyPair;
-
-import androidx.annotation.VisibleForTesting;
-
-/**
- * copied & modified from:
- * https://github.com/awslabs/aws-sdk-android-samples/blob/master/CreateIotCertWithCSR/src/com/amazonaws/demo/csrcert/CsrHelper.java
- * accessed at 31.08.17
- * Original parts are licensed under the Apache License, Version 2.0: http://aws.amazon.com/apache2.0
- * Own parts are licensed under GPLv3+.
- */
-
-public final class CsrHelper {
-
-    private CsrHelper() {
-        // utility class -> private constructor
-    }
-
-    /**
-     * Generate CSR with PEM encoding
-     *
-     * @param keyPair the KeyPair with private and public keys
-     * @param userId  userId of CSR owner
-     * @return PEM encoded CSR string
-     * @throws IOException               thrown if key cannot be created
-     * @throws OperatorCreationException thrown if contentSigner cannot be build
-     */
-    public static String generateCsrPemEncodedString(KeyPair keyPair, String userId)
-            throws IOException, OperatorCreationException {
-        PKCS10CertificationRequest csr = CsrHelper.generateCSR(keyPair, userId);
-        byte[] derCSR = csr.getEncoded();
-        return "-----BEGIN CERTIFICATE REQUEST-----\n" + android.util.Base64.encodeToString(derCSR,
-                android.util.Base64.NO_WRAP) + "\n-----END CERTIFICATE REQUEST-----";
-    }
-
-    /**
-     * Create the certificate signing request (CSR) from private and public keys
-     *
-     * @param keyPair the KeyPair with private and public keys
-     * @param userId  userId of CSR owner
-     * @return PKCS10CertificationRequest with the certificate signing request (CSR) data
-     * @throws IOException               thrown if key cannot be created
-     * @throws OperatorCreationException thrown if contentSigner cannot be build
-     */
-    @VisibleForTesting
-    public static PKCS10CertificationRequest generateCSR(KeyPair keyPair, String userId) throws IOException,
-        OperatorCreationException {
-        String principal = "CN=" + userId;
-        AsymmetricKeyParameter privateKey = PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded());
-        AlgorithmIdentifier signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1WITHRSA");
-        AlgorithmIdentifier digestAlgorithm = new DefaultDigestAlgorithmIdentifierFinder().find("SHA-1");
-        ContentSigner signer = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm).build(privateKey);
-
-        PKCS10CertificationRequestBuilder csrBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(principal),
-                                                                                                keyPair.getPublic());
-        ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
-        extensionsGenerator.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
-        csrBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
-
-        return csrBuilder.build(signer);
-    }
-}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 499 - 177
app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java


+ 1116 - 0
app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt

@@ -0,0 +1,1116 @@
+/*
+ *
+ * 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.utils
+
+import android.accounts.AccountManager
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import com.google.gson.reflect.TypeToken
+import com.nextcloud.client.account.User
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1
+import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata
+import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFiledrop
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedMetadata
+import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedUser
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.accounts.AccountUtils
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.e2ee.GetMetadataRemoteOperation
+import com.owncloud.android.lib.resources.e2ee.MetadataResponse
+import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation
+import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation
+import com.owncloud.android.operations.UploadException
+import org.apache.commons.httpclient.HttpStatus
+import org.bouncycastle.asn1.ASN1Sequence
+import org.bouncycastle.asn1.cms.ContentInfo
+import org.bouncycastle.cert.jcajce.JcaCertStore
+import org.bouncycastle.cms.CMSProcessableByteArray
+import org.bouncycastle.cms.CMSSignedData
+import org.bouncycastle.cms.CMSSignedDataGenerator
+import org.bouncycastle.cms.SignerInformation
+import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
+import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
+import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
+import java.io.BufferedReader
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.math.BigInteger
+import java.security.MessageDigest
+import java.security.PrivateKey
+import java.security.cert.X509Certificate
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
+
+@Suppress("TooManyFunctions", "LargeClass")
+class EncryptionUtilsV2 {
+    @VisibleForTesting
+    fun encryptMetadata(metadata: DecryptedMetadata, metadataKey: ByteArray): EncryptedMetadata {
+        val json = EncryptionUtils.serializeJSON(metadata, true)
+        val gzip = gZipCompress(json)
+
+        return EncryptionUtils.encryptStringSymmetric(
+            gzip,
+            metadataKey,
+            EncryptionUtils.ivDelimiter
+        )
+    }
+
+    @VisibleForTesting
+    fun decryptMetadata(metadata: EncryptedMetadata, metadataKey: ByteArray): DecryptedMetadata {
+        val decrypted = EncryptionUtils.decryptStringSymmetric(
+            metadata.ciphertext,
+            metadataKey,
+            metadata.authenticationTag,
+            metadata.nonce
+        )
+        val json = gZipDecompress(decrypted)
+
+        val decryptedMetadata = EncryptionUtils.deserializeJSON(json, object : TypeToken<DecryptedMetadata>() {})
+        decryptedMetadata.metadataKey = metadataKey
+
+        return decryptedMetadata
+    }
+
+    @Suppress("LongParameterList")
+    fun encryptFolderMetadataFile(
+        metadataFile: DecryptedFolderMetadataFile,
+        userId: String,
+        folder: OCFile,
+        storageManager: FileDataStorageManager,
+        client: OwnCloudClient,
+        privateKey: String,
+        user: User,
+        context: Context,
+        arbitraryDataProvider: ArbitraryDataProvider
+    ): EncryptedFolderMetadataFile {
+        val encryptedUsers: List<EncryptedUser>
+        val encryptedMetadata: EncryptedMetadata
+        if (metadataFile.users.isEmpty()) {
+            // we are in a subfolder, re-use users array
+            val key = retrieveTopMostMetadataKey(
+                folder,
+                storageManager,
+                client,
+                userId,
+                privateKey,
+                user,
+                context,
+                arbitraryDataProvider
+            )
+
+            // do not store metadata key
+            metadataFile.metadata.metadataKey = ByteArray(0)
+            metadataFile.metadata.keyChecksums.clear()
+
+            encryptedUsers = emptyList()
+            encryptedMetadata = encryptMetadata(metadataFile.metadata, key)
+        } else {
+            encryptedUsers = metadataFile.users.map {
+                encryptUser(
+                    it,
+                    metadataFile.metadata.metadataKey
+                )
+            }
+            encryptedMetadata = encryptMetadata(metadataFile.metadata, metadataFile.metadata.metadataKey)
+        }
+
+        return EncryptedFolderMetadataFile(
+            encryptedMetadata,
+            encryptedUsers,
+            mutableMapOf()
+        )
+
+        // if (metadataFile.users.isEmpty()) {
+        //     // we are in a subfolder, re-use users array
+        //     retrieveTopMostMetadata(
+        //         ocFile,
+        //         storageManager,
+        //         client
+        //     )
+        // } else {
+        //    val encryptedUsers = metadataFile.users.map {
+        //         encryptUser(it, metadataFile.metadata.metadataKey)
+        //     }
+        //
+        //     return EncryptedFolderMetadataFile(
+        //         encryptedMetadata,
+        //         encryptedUsers,
+        //         emptyMap()
+        //     )
+        // }
+    }
+
+    @Throws(IllegalStateException::class, UploadException::class, Throwable::class)
+    @Suppress("LongParameterList", "LongMethod", "ThrowsCount")
+    fun decryptFolderMetadataFile(
+        metadataFile: EncryptedFolderMetadataFile,
+        userId: String,
+        privateKey: String,
+        ocFile: OCFile,
+        storageManager: FileDataStorageManager,
+        client: OwnCloudClient,
+        oldCounter: Long,
+        signature: String,
+        user: User,
+        context: Context,
+        arbitraryDataProvider: ArbitraryDataProvider
+    ): DecryptedFolderMetadataFile {
+        val parent =
+            storageManager.getFileById(ocFile.parentId) ?: throw IllegalStateException("Cannot retrieve metadata")
+
+        var filesDropCountBefore = 0
+        var filesBefore = 0
+        val decryptedFolderMetadataFile = if (parent.isEncrypted) {
+            // we are in a subfolder, decrypt information is in top most encrypted folder
+            val topMostMetadata = retrieveTopMostMetadata(
+                ocFile,
+                storageManager,
+                client,
+                userId,
+                privateKey,
+                user,
+                context,
+                arbitraryDataProvider
+            )
+
+            val decryptedMetadata = decryptMetadata(metadataFile.metadata, topMostMetadata.metadata.metadataKey)
+            decryptedMetadata.metadataKey = topMostMetadata.metadata.metadataKey
+            decryptedMetadata.keyChecksums.addAll(topMostMetadata.metadata.keyChecksums)
+
+            DecryptedFolderMetadataFile(
+                decryptedMetadata,
+                mutableListOf(), // subfolder do not store user array
+                mutableMapOf()
+            )
+        } else {
+            // Top folder
+            val encryptedUser = metadataFile.users.find { it.userId == userId }
+                ?: throw IllegalStateException("Cannot find current user in metadata")
+
+            val decryptedMetadataKey = decryptMetadataKey(encryptedUser, privateKey)
+
+            val users = metadataFile.users.map { transformUser(it) }.toMutableList()
+
+            val decryptedMetadata = decryptMetadata(
+                metadataFile.metadata,
+                decryptedMetadataKey
+            )
+
+            // only top folder can have files drop
+            filesBefore = decryptedMetadata.files.size
+            if (metadataFile.filedrop != null) {
+                filesDropCountBefore = metadataFile.filedrop.size
+            }
+
+            val fileDrop = metadataFile.filedrop
+            if (fileDrop != null) {
+                for (entry in fileDrop) {
+                    val key = entry.key
+                    val encryptedFile = entry.value
+
+                    val decryptedFile = decryptFiledrop(
+                        encryptedFile,
+                        privateKey,
+                        arbitraryDataProvider,
+                        user
+                    )
+
+                    decryptedMetadata.files[key] = decryptedFile
+                }
+            }
+
+            DecryptedFolderMetadataFile(
+                decryptedMetadata,
+                users,
+                mutableMapOf()
+            )
+        }
+
+        verifyMetadata(metadataFile, decryptedFolderMetadataFile, oldCounter, signature)
+
+        val transferredFiledrop = filesDropCountBefore > 0 &&
+            decryptedFolderMetadataFile.metadata.files.size == filesBefore + filesDropCountBefore
+
+        if (transferredFiledrop) {
+            // lock folder
+            val token = EncryptionUtils.lockFolder(ocFile, client)
+
+            serializeAndUploadMetadata(
+                ocFile,
+                decryptedFolderMetadataFile,
+                token,
+                client,
+                true,
+                context,
+                user,
+                storageManager
+            )
+
+            // unlock folder
+            val unlockFolderResult: RemoteOperationResult<*> = EncryptionUtils.unlockFolder(ocFile, client, token)
+            if (!unlockFolderResult.isSuccess) {
+                Log_OC.e(TAG, unlockFolderResult.message)
+                throw IllegalStateException()
+            }
+        }
+
+        return decryptedFolderMetadataFile
+    }
+
+    @Throws(Throwable::class)
+    fun decryptFiledrop(
+        filedrop: EncryptedFiledrop,
+        privateKey: String,
+        arbitraryDataProvider: ArbitraryDataProvider,
+        user: User
+    ): DecryptedFile {
+        // decrypt key
+        val encryptedKey = EncryptionUtils.decryptStringAsymmetricAsBytes(
+            filedrop.users[0].encryptedFiledropKey,
+            privateKey
+        )
+
+        // decrypt encrypted blob with key
+        val decryptedData = EncryptionUtils.decryptStringSymmetricAsString(
+            filedrop.ciphertext,
+            encryptedKey,
+            EncryptionUtils.decodeStringToBase64Bytes(filedrop.nonce),
+            EncryptionUtils.decodeStringToBase64Bytes(filedrop.authenticationTag),
+            true,
+            arbitraryDataProvider,
+            user
+        )
+
+        return EncryptionUtils.deserializeJSON(
+            decryptedData,
+            object : TypeToken<DecryptedFile>() {}
+        )
+    }
+
+    @Throws(IllegalStateException::class)
+    @Suppress("ThrowsCount", "LongParameterList")
+    fun retrieveTopMostMetadata(
+        folder: OCFile,
+        storageManager: FileDataStorageManager,
+        client: OwnCloudClient,
+        userId: String,
+        privateKey: String,
+        user: User,
+        context: Context,
+        arbitraryDataProvider: ArbitraryDataProvider
+    ): DecryptedFolderMetadataFile {
+        var topMost = folder
+        var parent =
+            storageManager.getFileById(topMost.parentId) ?: throw IllegalStateException("Cannot retrieve metadata")
+
+        while (parent.isEncrypted) {
+            topMost = parent
+
+            parent =
+                storageManager.getFileById(topMost.parentId) ?: throw IllegalStateException("Cannot retrieve metadata")
+        }
+
+        // parent is now top most encrypted folder
+        val result = GetMetadataRemoteOperation(topMost.localId).execute(client)
+
+        if (result.isSuccess) {
+            val v2 = EncryptionUtils.deserializeJSON(
+                result.resultData.metadata,
+                object : TypeToken<EncryptedFolderMetadataFile>() {}
+            )
+
+            return decryptFolderMetadataFile(
+                v2,
+                userId,
+                privateKey,
+                topMost,
+                storageManager,
+                client,
+                topMost.e2eCounter,
+                result.resultData.signature,
+                user,
+                context,
+                arbitraryDataProvider
+            )
+        } else {
+            throw IllegalStateException("Cannot retrieve metadata")
+        }
+    }
+
+    @Throws(IllegalStateException::class)
+    @Suppress("ThrowsCount", "LongParameterList")
+    fun retrieveTopMostMetadataKey(
+        folder: OCFile,
+        storageManager: FileDataStorageManager,
+        client: OwnCloudClient,
+        userId: String,
+        privateKey: String,
+        user: User,
+        context: Context,
+        arbitraryDataProvider: ArbitraryDataProvider
+    ): ByteArray {
+        return retrieveTopMostMetadata(
+            folder,
+            storageManager,
+            client,
+            userId,
+            privateKey,
+            user,
+            context,
+            arbitraryDataProvider
+        )
+            .metadata.metadataKey
+    }
+
+    @VisibleForTesting
+    fun encryptUser(user: DecryptedUser, metadataKey: ByteArray): EncryptedUser {
+        val encryptedKey = EncryptionUtils.encryptStringAsymmetricV2(
+            metadataKey,
+            user.certificate
+        )
+
+        return EncryptedUser(
+            user.userId,
+            user.certificate,
+            encryptedKey
+        )
+    }
+
+    @VisibleForTesting
+    fun transformUser(user: EncryptedUser): DecryptedUser {
+        return DecryptedUser(
+            user.userId,
+            user.certificate
+        )
+    }
+
+    @VisibleForTesting
+    fun decryptMetadataKey(user: EncryptedUser, privateKey: String): ByteArray {
+        return EncryptionUtils.decryptStringAsymmetricV2(
+            user.encryptedMetadataKey,
+            privateKey
+        )
+    }
+
+    fun gZipCompress(string: String): ByteArray {
+        val outputStream = ByteArrayOutputStream()
+        GZIPOutputStream(outputStream).apply {
+            write(string.toByteArray())
+            flush()
+            close()
+        }
+
+        return outputStream.toByteArray()
+    }
+
+    fun gZipDecompress(compressed: String): String {
+        return gZipDecompress(compressed.byteInputStream())
+    }
+
+    fun gZipDecompress(compressed: ByteArray): String {
+        return gZipDecompress(compressed.inputStream())
+    }
+
+    @VisibleForTesting
+    fun gZipDecompress(inputStream: InputStream): String {
+        val stringBuilder = StringBuilder()
+        val inputStream = GZIPInputStream(inputStream)
+        // val inputStream = compressed.inputStream()
+        val bufferedReader = BufferedReader(InputStreamReader(inputStream))
+
+        // val sb = java.lang.StringBuilder()
+        // for (b in compressed) {
+        //     sb.append(String.format("%02X ", b))
+        // }
+        // val out = sb.toString()
+
+        var line = bufferedReader.readLine()
+        while (line != null) {
+            stringBuilder.appendLine(line)
+            line = bufferedReader.readLine()
+        }
+
+        return stringBuilder.toString()
+    }
+
+    fun addShareeToMetadata(
+        metadataFile: DecryptedFolderMetadataFile,
+        userId: String,
+        cert: String
+    ): DecryptedFolderMetadataFile {
+        metadataFile.users.add(DecryptedUser(userId, cert))
+        metadataFile.metadata.metadataKey = EncryptionUtils.generateKey()
+        metadataFile.metadata.keyChecksums.add(hashMetadataKey(metadataFile.metadata.metadataKey))
+
+        return metadataFile
+    }
+
+    @Throws(RuntimeException::class)
+    fun removeShareeFromMetadata(
+        metadataFile: DecryptedFolderMetadataFile,
+        userIdToRemove: String
+    ): DecryptedFolderMetadataFile {
+        val remove = metadataFile.users.remove(metadataFile.users.find { it.userId == userIdToRemove })
+
+        if (!remove) {
+            throw java.lang.RuntimeException("Removal of user $userIdToRemove failed!")
+        }
+
+        metadataFile.metadata.metadataKey = EncryptionUtils.generateKey()
+        metadataFile.metadata.keyChecksums.add(hashMetadataKey(metadataFile.metadata.metadataKey))
+
+        return metadataFile
+    }
+
+    @Suppress("LongParameterList")
+    fun addFileToMetadata(
+        encryptedFileName: String,
+        ocFile: OCFile,
+        initializationVector: ByteArray,
+        authenticationTag: String,
+        key: ByteArray,
+        metadataFile: DecryptedFolderMetadataFile,
+        fileDataStorageManager: FileDataStorageManager
+    ): DecryptedFolderMetadataFile {
+        val decryptedFile = DecryptedFile(
+            ocFile.decryptedFileName,
+            ocFile.mimeType,
+            EncryptionUtils.encodeBytesToBase64String(initializationVector),
+            authenticationTag,
+            EncryptionUtils.encodeBytesToBase64String(key)
+        )
+
+        metadataFile.metadata.files[encryptedFileName] = decryptedFile
+        metadataFile.metadata.counter++
+        ocFile.setE2eCounter(metadataFile.metadata.counter)
+        fileDataStorageManager.saveFile(ocFile)
+
+        return metadataFile
+    }
+
+    fun addFolderToMetadata(
+        encryptedFileName: String,
+        fileName: String,
+        metadataFile: DecryptedFolderMetadataFile,
+        ocFile: OCFile,
+        fileDataStorageManager: FileDataStorageManager
+    ): DecryptedFolderMetadataFile {
+        metadataFile.metadata.folders[encryptedFileName] = fileName
+        metadataFile.metadata.counter++
+        ocFile.setE2eCounter(metadataFile.metadata.counter)
+        fileDataStorageManager.saveFile(ocFile)
+
+        return metadataFile
+    }
+
+    fun removeFolderFromMetadata(
+        encryptedFileName: String,
+        metadataFile: DecryptedFolderMetadataFile
+    ): DecryptedFolderMetadataFile {
+        metadataFile.metadata.folders.remove(encryptedFileName)
+
+        return metadataFile
+    }
+
+    @Throws(IllegalStateException::class)
+    fun removeFileFromMetadata(
+        fileName: String,
+        metadata: DecryptedFolderMetadataFile
+    ) {
+        metadata.metadata.files.remove(fileName)
+            ?: throw IllegalStateException("File $fileName not found in metadata!")
+    }
+
+    @Throws(IllegalStateException::class)
+    fun renameFile(
+        key: String,
+        newName: String,
+        metadataFile: DecryptedFolderMetadataFile
+    ) {
+        if (!metadataFile.metadata.files.containsKey(key)) {
+            throw IllegalStateException("File with key $key not found in metadata!")
+        }
+
+        metadataFile.metadata.files[key]!!.filename = newName
+    }
+
+    @Throws(UploadException::class, IllegalStateException::class)
+    fun retrieveMetadata(
+        folder: OCFile,
+        client: OwnCloudClient,
+        user: User,
+        context: Context
+    ): Pair<Boolean, DecryptedFolderMetadataFile> {
+        val getMetadataOperationResult = GetMetadataRemoteOperation(folder.localId).execute(client)
+
+        return if (getMetadataOperationResult.isSuccess) {
+            // decrypt metadata
+            val metadataResponse = getMetadataOperationResult.resultData
+            val metadata = parseAnyMetadata(
+                metadataResponse,
+                user,
+                client,
+                context,
+                folder
+            )
+
+            Pair(true, metadata)
+        } else if (getMetadataOperationResult.httpCode == HttpStatus.SC_NOT_FOUND) {
+            // check parent folder
+            val parentFolder = FileDataStorageManager(user, context.contentResolver).getFileById(folder.parentId)
+                ?: throw IllegalStateException("Cannot retrieve metadata!")
+
+            val metadata = if (parentFolder.isEncrypted) {
+                // new metadata but without sharing part
+                createDecryptedFolderMetadataFile()
+            } else {
+                // new metadata
+                val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(context)
+                val publicKey: String = arbitraryDataProvider.getValue(user.accountName, EncryptionUtils.PUBLIC_KEY)
+
+                createDecryptedFolderMetadataFile().apply {
+                    users = mutableListOf(DecryptedUser(client.userId, publicKey))
+                }
+            }
+
+            Pair(false, metadata)
+        } else {
+            // TODO error
+            throw UploadException("something wrong")
+        }
+    }
+
+    @Throws(IllegalStateException::class)
+    @Suppress("TooGenericExceptionCaught")
+    fun parseAnyMetadata(
+        metadataResponse: MetadataResponse,
+        user: User,
+        client: OwnCloudClient,
+        context: Context,
+        folder: OCFile
+    ): DecryptedFolderMetadataFile {
+        val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(context)
+        val privateKey: String = arbitraryDataProvider.getValue(user.accountName, EncryptionUtils.PRIVATE_KEY)
+        val storageManager = FileDataStorageManager(user, context.contentResolver)
+
+        val v2 = EncryptionUtils.deserializeJSON(
+            metadataResponse.metadata,
+            object : TypeToken<EncryptedFolderMetadataFile>() {}
+        )
+
+        val decryptedFolderMetadata = if (v2.version == "2.0" || v2.version == "2") {
+            val userId = AccountManager.get(context).getUserData(
+                user.toPlatformAccount(),
+                AccountUtils.Constants.KEY_USER_ID
+            )
+            decryptFolderMetadataFile(
+                v2,
+                userId,
+                privateKey,
+                folder,
+                storageManager,
+                client,
+                folder.e2eCounter,
+                metadataResponse.signature,
+                user,
+                context,
+                arbitraryDataProvider
+            )
+        } else {
+            // try to deserialize v1
+            val v1 = EncryptionUtils.deserializeJSON(
+                metadataResponse.metadata,
+                object : TypeToken<EncryptedFolderMetadataFileV1?>() {}
+            )
+
+            // decrypt
+            try {
+                // decrypt metadata
+                val decryptedV1 = EncryptionUtils.decryptFolderMetaData(
+                    v1,
+                    privateKey,
+                    arbitraryDataProvider,
+                    user,
+                    folder.localId
+                )
+                val publicKey: String = arbitraryDataProvider.getValue(
+                    user.accountName,
+                    EncryptionUtils.PUBLIC_KEY
+                )
+
+                // migrate to v2
+                migrateV1ToV2andUpload(
+                    decryptedV1,
+                    client.userIdPlain,
+                    publicKey,
+                    folder,
+                    storageManager,
+                    client,
+                    user,
+                    context
+                )
+            } catch (e: Exception) {
+                // TODO do better
+                throw IllegalStateException("Cannot decrypt metadata")
+            }
+        }
+
+        // TODO verify metadata
+        // if (!verifyMetadata(decryptedFolderMetadata)) {
+        //     throw IllegalStateException("Metadata is corrupt!")
+        // }
+
+        return decryptedFolderMetadata
+
+        // handle filesDrops
+        // TODO re-add
+//        try {
+//            int filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size();
+//            DecryptedFolderMetadataFile decryptedFolderMetadata = new EncryptionUtilsV2().decryptFolderMetadataFile(
+//                encryptedFolderMetadata,
+//                privateKey);
+//
+//            boolean transferredFiledrop = filesDropCountBefore > 0 && decryptedFolderMetadata.getFiles().size() ==
+//                encryptedFolderMetadata.getFiles().size() + filesDropCountBefore;
+//
+//            if (transferredFiledrop) {
+//                // lock folder, only if not already locked
+//                String token;
+//                if (existingLockToken == null) {
+//                    token = EncryptionUtils.lockFolder(folder, client);
+//                } else {
+//                    token = existingLockToken;
+//                }
+//
+//                // upload metadata
+//                EncryptedFolderMetadataFile encryptedFolderMetadataNew =
+//                encryptFolderMetadata(decryptedFolderMetadata, privateKey);
+//
+//                String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataNew);
+//
+//                EncryptionUtils.uploadMetadata(folder,
+//                                               serializedFolderMetadata,
+//                                               token,
+//                                               client,
+//                                               true);
+//
+//                // unlock folder, only if not previously locked
+//                if (existingLockToken == null) {
+//                    RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token);
+//
+//                    if (!unlockFolderResult.isSuccess()) {
+//                        Log_OC.e(TAG, unlockFolderResult.getMessage());
+//
+//                        return null;
+//                    }
+//                }
+//            }
+//
+//            return decryptedFolderMetadata;
+//        } catch (Exception e) {
+//            Log_OC.e(TAG, e.getMessage());
+//            return null;
+//        }
+
+        // TODO to check
+//                try {
+//                    int filesDropCountBefore = 0;
+//                    if (encryptedFolderMetadata.getFiledrop() != null) {
+//                        filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size();
+//                    }
+//                    DecryptedFolderMetadataFile decryptedFolderMetadata = EncryptionUtils.decryptFolderMetaData(
+//                        encryptedFolderMetadata,
+//                        privateKey,
+//                        arbitraryDataProvider,
+//                        user,
+//                        folder.getLocalId());
+//
+//                    boolean transferredFiledrop = filesDropCountBefore > 0 &&
+//                    decryptedFolderMetadata.getFiles().size() ==
+//                        encryptedFolderMetadata.getFiles().size() + filesDropCountBefore;
+//
+//                    if (transferredFiledrop) {
+//                        // lock folder
+//                        String token = EncryptionUtils.lockFolder(folder, client);
+//
+//                        // upload metadata
+//                        EncryptedFolderMetadata encryptedFolderMetadataNew =
+//                        encryptFolderMetadata(decryptedFolderMetadata,
+//                                              publicKey,
+//                                              arbitraryDataProvider,
+//                                              user,
+//                                              folder.getLocalId());
+//
+    }
+
+    @Throws(UploadException::class)
+    @Suppress("LongParameterList")
+    fun migrateV1ToV2andUpload(
+        v1: DecryptedFolderMetadataFileV1,
+        userId: String,
+        cert: String,
+        folder: OCFile,
+        storageManager: FileDataStorageManager,
+        client: OwnCloudClient,
+        user: User,
+        context: Context
+    ): DecryptedFolderMetadataFile {
+        val newMetadata = migrateV1ToV2(
+            v1,
+            userId,
+            cert,
+            folder,
+            storageManager
+        )
+        // lock
+        val token = EncryptionUtils.lockFolder(folder, client)
+
+        // upload
+        serializeAndUploadMetadata(
+            folder,
+            newMetadata,
+            token,
+            client,
+            true,
+            context,
+            user,
+            storageManager
+        )
+
+        // unlock
+        EncryptionUtils.unlockFolder(folder, client, token)
+
+        return newMetadata
+    }
+
+    @Throws(IllegalStateException::class)
+    fun migrateV1ToV2(
+        v1: DecryptedFolderMetadataFileV1,
+        userId: String,
+        cert: String,
+        folder: OCFile,
+        storageManager: FileDataStorageManager
+    ): DecryptedFolderMetadataFile {
+        // key
+        val key = if (v1.metadata.metadataKeys != null && v1.metadata.metadataKeys.size > 1) {
+            v1.metadata.metadataKeys[0]
+        } else {
+            v1.metadata.metadataKey
+        }
+
+        // create new metadata
+        val metadataV2 = DecryptedMetadata(
+            mutableListOf(),
+            false,
+            0,
+            v1
+                .files
+                .filter { it.value.encrypted.mimetype == MimeType.WEBDAV_FOLDER }
+                .mapValues { it.value.encrypted.filename }
+                .toMutableMap(),
+            v1
+                .files
+                .filter { it.value.encrypted.mimetype != MimeType.WEBDAV_FOLDER }
+                .mapValues { migrateDecryptedFileV1ToV2(it.value) }
+                .toMutableMap(),
+            EncryptionUtils.decodeStringToBase64Bytes(key) ?: throw IllegalStateException("Metadata key not found!")
+        )
+
+        // upon migration there can only be one user, as there is no sharing yet in place
+        val users = if (storageManager.getFileById(folder.parentId)?.isEncrypted == false) {
+            mutableListOf(DecryptedUser(userId, cert))
+        } else {
+            mutableListOf()
+        }
+
+        // TODO
+        val filedrop = mutableMapOf<String, DecryptedFile>()
+
+        val newMetadata = DecryptedFolderMetadataFile(metadataV2, users, filedrop)
+        val metadataKey = EncryptionUtils.generateKey() ?: throw UploadException("Could not encrypt folder!")
+
+        newMetadata.metadata.metadataKey = metadataKey
+        newMetadata.metadata.keyChecksums.add(EncryptionUtilsV2().hashMetadataKey(metadataKey))
+
+        return newMetadata
+    }
+
+    @VisibleForTesting
+    fun migrateDecryptedFileV1ToV2(v1: com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile): DecryptedFile {
+        return DecryptedFile(
+            v1.encrypted.filename,
+            v1.encrypted.mimetype,
+            v1.initializationVector,
+            v1.authenticationTag ?: "",
+            v1.encrypted.key
+        )
+    }
+
+    @Throws(UploadException::class)
+    @Suppress("LongParameterList")
+    fun serializeAndUploadMetadata(
+        folder: OCFile,
+        metadata: DecryptedFolderMetadataFile,
+        token: String,
+        client: OwnCloudClient,
+        metadataExists: Boolean,
+        context: Context,
+        user: User,
+        storageManager: FileDataStorageManager
+    ) {
+        serializeAndUploadMetadata(
+            folder.remoteId,
+            metadata,
+            token,
+            client,
+            metadataExists,
+            context,
+            user,
+            folder,
+            storageManager
+        )
+    }
+
+    @Throws(UploadException::class)
+    @Suppress("LongParameterList")
+    fun serializeAndUploadMetadata(
+        remoteId: String,
+        metadata: DecryptedFolderMetadataFile,
+        token: String,
+        client: OwnCloudClient,
+        metadataExists: Boolean,
+        context: Context,
+        user: User,
+        folder: OCFile,
+        storageManager: FileDataStorageManager
+    ) {
+        val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(context)
+        val privateKeyString: String = arbitraryDataProvider.getValue(user.accountName, EncryptionUtils.PRIVATE_KEY)
+        val publicKeyString: String = arbitraryDataProvider.getValue(user.accountName, EncryptionUtils.PUBLIC_KEY)
+
+        val encryptedFolderMetadata = encryptFolderMetadataFile(
+            metadata,
+            client.userId,
+            folder,
+            storageManager,
+            client,
+            privateKeyString,
+            user,
+            context,
+            arbitraryDataProvider
+        )
+        val serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true)
+        val cert = EncryptionUtils.convertCertFromString(publicKeyString)
+        val privateKey = EncryptionUtils.PEMtoPrivateKey(privateKeyString)
+
+        val signature = getMessageSignature(cert, privateKey, encryptedFolderMetadata)
+        val uploadMetadataOperationResult = if (metadataExists) {
+            // update metadata
+            UpdateMetadataV2RemoteOperation(
+                remoteId,
+                serializedFolderMetadata,
+                token,
+                signature
+            )
+                .execute(client)
+        } else {
+            // store metadata
+            StoreMetadataV2RemoteOperation(
+                remoteId,
+                serializedFolderMetadata,
+                token,
+                signature
+            )
+                .execute(client)
+        }
+        if (!uploadMetadataOperationResult.isSuccess) {
+            if (metadataExists) {
+                throw UploadException("Updating metadata was not successful")
+            } else {
+                throw UploadException("Storing metadata was not successful")
+            }
+        }
+    }
+
+    @Throws(IllegalStateException::class)
+    @Suppress("ThrowsCount")
+    @VisibleForTesting
+    fun verifyMetadata(
+        encryptedFolderMetadataFile: EncryptedFolderMetadataFile,
+        decryptedFolderMetadataFile: DecryptedFolderMetadataFile,
+        oldCounter: Long,
+        ans: String // base 64 encoded BER
+    ) {
+        // check counter
+        if (decryptedFolderMetadataFile.metadata.counter < oldCounter) {
+            throw IllegalStateException("Counter is too old")
+        }
+
+        // check signature
+        val json = EncryptionUtils.serializeJSON(encryptedFolderMetadataFile, true)
+        val certs = decryptedFolderMetadataFile.users.map { EncryptionUtils.convertCertFromString(it.certificate) }
+
+        val base64 = EncryptionUtils.encodeStringToBase64String(json)
+
+        // if (!verifySignedMessage(ans, base64, certs)) {
+        //     throw IllegalStateException("Signature does not match")
+        // }
+
+        val hashedMetadataKey = hashMetadataKey(decryptedFolderMetadataFile.metadata.metadataKey)
+        if (!decryptedFolderMetadataFile.metadata.keyChecksums.contains(hashedMetadataKey)) {
+            throw IllegalStateException("Hash not found")
+            // TODO E2E: fake this to present problem to user
+        }
+
+        // TODO E2E: check certs
+    }
+
+    fun createDecryptedFolderMetadataFile(): DecryptedFolderMetadataFile {
+        val metadata = DecryptedMetadata().apply {
+            keyChecksums.add(hashMetadataKey(metadataKey))
+        }
+
+        return DecryptedFolderMetadataFile(metadata)
+    }
+
+    /**
+     * SHA-256 hash of metadata-key
+     */
+    @Suppress("MagicNumber")
+    fun hashMetadataKey(metadataKey: ByteArray): String {
+        val bytes = MessageDigest
+            .getInstance("SHA-256")
+            .digest(metadataKey)
+
+        return BigInteger(1, bytes).toString(16).padStart(32, '0')
+    }
+
+    fun signMessage(cert: X509Certificate, key: PrivateKey, data: ByteArray): CMSSignedData {
+        val content = CMSProcessableByteArray(data)
+        val certs = JcaCertStore(listOf(cert))
+
+        val sha1signer = JcaContentSignerBuilder("SHA256withRSA").build(key)
+        val signGen = CMSSignedDataGenerator().apply {
+            addSignerInfoGenerator(
+                JcaSignerInfoGeneratorBuilder(JcaDigestCalculatorProviderBuilder().build()).build(
+                    sha1signer,
+                    cert
+                )
+            )
+            addCertificates(certs)
+        }
+        return signGen.generate(
+            content,
+            false
+        )
+    }
+
+    /**
+     * Sign the data with key, embed the certificate associated within the CMSSignedData
+     * detached data not possible, as to restore asn.1
+     */
+    fun signMessage(cert: X509Certificate, key: PrivateKey, message: EncryptedFolderMetadataFile): CMSSignedData {
+        val json = EncryptionUtils.serializeJSON(message, true)
+        val base64 = EncryptionUtils.encodeStringToBase64String(json)
+        val data = base64.toByteArray()
+
+        return signMessage(cert, key, data)
+    }
+
+    fun signMessage(cert: X509Certificate, key: PrivateKey, string: String): CMSSignedData {
+        val base64 = EncryptionUtils.encodeStringToBase64String(string)
+        val data = base64.toByteArray()
+
+        return signMessage(cert, key, data)
+    }
+
+    fun extractSignedString(signedData: CMSSignedData): String {
+        val ans = signedData.getEncoded("BER")
+        return EncryptionUtils.encodeBytesToBase64String(ans)
+    }
+
+    fun getMessageSignature(cert: String, privateKey: String, metadataFile: EncryptedFolderMetadataFile): String {
+        return getMessageSignature(
+            EncryptionUtils.convertCertFromString(cert),
+            EncryptionUtils.PEMtoPrivateKey(privateKey),
+            metadataFile
+        )
+    }
+
+    fun getMessageSignature(cert: X509Certificate, key: PrivateKey, message: EncryptedFolderMetadataFile): String {
+        val signedMessage = signMessage(cert, key, message)
+        return extractSignedString(signedMessage)
+    }
+
+    fun getMessageSignature(cert: X509Certificate, key: PrivateKey, string: String): String {
+        val signedMessage = signMessage(cert, key, string)
+        return extractSignedString(signedMessage)
+    }
+
+    /**
+     * Verify the signature but does not use the certificate in the signed object
+     */
+    fun verifySignedMessage(data: CMSSignedData, certs: List<X509Certificate>): Boolean {
+        val signer: SignerInformation = data.signerInfos.signers.iterator().next() as SignerInformation
+
+        certs.forEach {
+            try {
+                if (signer.verify(JcaSimpleSignerInfoVerifierBuilder().build(it))) {
+                    return true
+                }
+            } catch (e: java.lang.Exception) {
+                Log_OC.e("Encryption", "error", e)
+            }
+        }
+
+        return false
+    }
+
+    /**
+     * Verify the signature but does not use the certificate in the signed object
+     */
+    fun verifySignedMessage(base64encodedAns: String, originalMessage: String, certs: List<X509Certificate>): Boolean {
+        val ans = EncryptionUtils.decodeStringToBase64Bytes(base64encodedAns)
+        val contentInfo = ContentInfo.getInstance(ASN1Sequence.fromByteArray(ans))
+        val content = CMSProcessableByteArray(originalMessage.toByteArray())
+        val sig = CMSSignedData(content, contentInfo)
+
+        return verifySignedMessage(sig, certs)
+    }
+
+    companion object {
+        private val TAG = EncryptionUtils::class.java.simpleName
+    }
+}

+ 3 - 0
app/src/main/res/values/strings.xml

@@ -510,6 +510,7 @@
     <string name="share_via_link_unset_password">Unset</string>
 
     <string name="share_search">Name, Federated Cloud ID or email address …</string>
+    <string name="secure_share_search">Secure share …</string>
 
     <string name="share_group_clarification">%1$s (group)</string>
     <string name="share_remote_clarification">%1$s (remote)</string>
@@ -1125,4 +1126,6 @@
     <string name="sub_folder_rule_year">Year</string>
     <string name="sub_folder_rule_month">Year/Month</string>
     <string name="sub_folder_rule_day">Year/Month/Day</string>
+    <string name="secure_share_not_set_up">Secure sharing is not set up for this user</string>
+    <string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string>
 </resources>

Vissa filer visades inte eftersom för många filer har ändrats