diff --git a/.idea/inspectionProfiles/ktlint.xml b/.idea/inspectionProfiles/ktlint.xml index 7d04a74be8..63c66a65e3 100644 --- a/.idea/inspectionProfiles/ktlint.xml +++ b/.idea/inspectionProfiles/ktlint.xml @@ -1,6 +1,9 @@ diff --git a/app/build.gradle b/app/build.gradle index 4177a3d9a1..c8e6be5214 100644 --- a/app/build.gradle +++ b/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' diff --git a/app/lint.xml b/app/lint.xml index 9c2bf970e7..36c6b2ca5b 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -48,6 +48,7 @@ + diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/77.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/77.json new file mode 100644 index 0000000000..8031ac6b64 --- /dev/null +++ b/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')" + ] + } +} diff --git a/app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java b/app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java new file mode 100644 index 0000000000..b66b610cef --- /dev/null +++ b/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 . + */ + +package com.nextcloud.client; + +public enum EndToEndAction { + CREATE_FOLDER, + GO_INTO_FOLDER, + GO_UP, + UPLOAD_FILE, + DOWNLOAD_FILE, + DELETE_FILE, +} diff --git a/app/src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java b/app/src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java deleted file mode 100644 index d78b3d2b3f..0000000000 --- a/app/src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java +++ /dev/null @@ -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 . - */ - -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 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 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 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 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 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 privateKeyRemoteOperationResult = new GetPrivateKeyOperation().execute(client); - RemoteOperationResult 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 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 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()); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 48c235647e..192210c058 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/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 optionalUser = UserAccountManagerImpl.fromContext(targetContext).getUser(account.name); return optionalUser.orElseThrow(IllegalAccessError::new); diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index 64ee4638de..af3833ee52 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/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; } diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt index 76c174e66a..49b3273a38 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt +++ b/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)) } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt index 404f31485b..9836e4a629 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt +++ b/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 { diff --git a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java index b35ec8ab0d..77a72864ec 100644 --- a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java +++ b/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() { - }); + 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() { - }); + 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() { + 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() { + }); + + // 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 filesdrop = new HashMap<>(); + Map 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() { - }); + 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 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 files = new HashMap<>(); + HashMap 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)); diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt new file mode 100644 index 0000000000..8857caae5f --- /dev/null +++ b/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 . + */ + +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) + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt new file mode 100644 index 0000000000..f36a94511c --- /dev/null +++ b/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 . + */ +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)) + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt new file mode 100644 index 0000000000..d8a742995d --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -0,0 +1,911 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2023 Tobias Kaminsky + * Copyright (C) 2023 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.utils + +import com.google.gson.reflect.TypeToken +import com.nextcloud.client.account.MockUser +import com.nextcloud.common.User +import com.owncloud.android.AbstractIT +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.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.EncryptedFiledropUser +import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile +import com.owncloud.android.util.EncryptionTestIT +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class EncryptionUtilsV2IT : AbstractIT() { + private val encryptionTestUtils = EncryptionTestUtils() + private val encryptionUtilsV2 = EncryptionUtilsV2() + + private val enc1UserId = "enc1" + private val enc1PrivateKey = """ + MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo + IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV + GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7 + Y0BJX9i/nW/L0L/VaE8CZTAqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCi + CC3qV99b0igRJGmmLQaGiAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umye + yy33OQgdUKaTl5zcS3VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoL + H2eiIJCi+61ZkSGfAgMBAAECggEBALFStCHrhBf+GL9a+qer4/8QZ/X6i91PmaBX/7 + SYk2jjjWVSXRNmex+V6+Y/jBRT2mvAgm8J+7LPwFdatE+lz0aZrMRD2gCWYF6Itpda + 90OlLkmQPVWWtGTgX2ta2tF5r2iSGzk0IdoL8zw98Q2UzpOcw30KnWtFMxuxWk0mHq + pgp00g80cDWg3+RPbWOhdLp5bflQ36fKDfmjq05cGlIk6unnVyC5HXpvh4d4k2EWlX + rjGsndVBPCjGkZePlLRgDHxT06r+5XdJ+1CBDZgCsmjGz3M8uOHyCfVW0WhB7ynzDT + agVgz0iqpuhAi9sPt6iWWwpAnRw8cQgqEKw9bvKKECgYEA/WPi2PJtL6u/xlysh/H7 + A717CId6fPHCMDace39ZNtzUzc0nT5BemlcF0wZ74NeJSur3Q395YzB+eBMLs5p8mA + 95wgGvJhM65/J+HX+k9kt6Z556zLMvtG+j1yo4D0VEwm3xahB4SUUP+1kD7dNvo4+8 + xeSCyjzNllvYZZC0DrECgYEA7w8pEqhHHn0a+twkPCZJS+gQTB9Rm+FBNGJqB3XpWs + TeLUxYRbVGk0iDve+eeeZ41drxcdyWP+WcL34hnrjgI1Fo4mK88saajpwUIYMy6+qM + LY+jC2NRSBox56eH7nsVYvQQK9eKqv9wbB+PF9SwOIvuETN7fd8mAY02UnoaaU8CgY + BoHRKocXPLkpZJuuppMVQiRUi4SHJbxDo19Tp2w+y0TihiJ1lvp7I3WGpcOt3LlMQk + tEbExSvrRZGxZKH6Og/XqwQsYuTEkEIz679F/5yYVosE6GkskrOXQAfh8Mb3/04xVV + tMaVgDQw0+CWVD4wyL+BNofGwBDNqsXTCdCsfxAQKBgQCDv2EtbRw0y1HRKv21QIxo + ju5cZW4+cDfVPN+eWPdQFOs1H7wOPsc0aGRiiupV2BSEF3O1ApKziEE5U1QH+29bR4 + R8L1pemeGX8qCNj5bCubKjcWOz5PpouDcEqimZ3q98p3E6GEHN15UHoaTkx0yO/V8o + j6zhQ9fYRxDHB5ACtQKBgQCOO7TJUO1IaLTjcrwS4oCfJyRnAdz49L1AbVJkIBK0fh + JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o + uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA== + """.trimIndent() + + private val enc1Cert = """ + -----BEGIN CERTIFICATE----- + MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu + bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0 + dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw + HhcNMTcwOTI2MTAwNDMwWhcNMzcwOTIxMTAwNDMwWjBuMRowGAYDVQQDDBF3d3cu + bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0 + dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw + ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDsn0JKS/THu328z1IgN0Vz + YU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzVGzKFvGfZ03fwFrN7Q8P8R2e8 + SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7Y0BJX9i/nW/L0L/VaE8CZT + AqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCiCC3qV99b0igRJGmmLQaG + iAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umyeyy33OQgdUKaTl5zcS3 + VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoLH2eiIJCi+61ZkSGf + AgMBAAGjUDBOMB0GA1UdDgQWBBTFrXz2tk1HivD9rQ75qeoyHrAgIjAfBgNVHSME + GDAWgBTFrXz2tk1HivD9rQ75qeoyHrAgIjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 + DQEBBQUAA4IBAQARQTX21QKO77gAzBszFJ6xVnjfa23YZF26Z4X1KaM8uV8TGzuN + JA95XmReeP2iO3r8EWXS9djVCD64m2xx6FOsrUI8HZaw1JErU8mmOaLAe8q9RsOm + 9Eq37e4vFp2YUEInYUqs87ByUcA4/8g3lEYeIUnRsRsWsA45S3wD7wy07t+KAn7j + yMmfxdma6hFfG9iN/egN6QXUAyIPXvUvlUuZ7/BhWBj/3sHMrF9quy9Q2DOI8F3t + 1wdQrkq4BtStKhciY5AIXz9SqsctFHTv4Lwgtkapoel4izJnO0ZqYTXVe7THwri9 + H/gua6uJDWH9jk2/CiZDWfsyFuNUuXvDSp05 + -----END CERTIFICATE----- + """.trimIndent() + + private val enc2Cert = """ + -----BEGIN CERTIFICATE----- + MIIC7DCCAdSgAwIBAgIBADANBgkqhkiG9w0BAQUFADAPMQ0wCwYDVQQDDARlbmMz + MB4XDTIwMDcwODA3MzE1OFoXDTQwMDcwMzA3MzE1OFowDzENMAsGA1UEAwwEZW5j + MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI/83eC/EF3xOocwjO+Z + ZkPc1TFxt3aUgjEvrpZu45LOqesG67kkkVDYgjeg3Biz9XRUQXqtXaAyxRZH8GiH + PFyKUiP1bUlCptd8X+hk9vxeN25YS5OS2RrxU9tDQ/dVOHr20427UvVCighotQnR + /6+md1FQMV92PFxji7OP5TWOE1y389X6eb7kSPLs8Tu+2PpqaNVQ9C/89Y8KNYWs + x9Zo+kbQhjfFFUikEpkuzMgT9QLaeq6xuXIPP+y1tzNmF6NTL0a2GoYULuxYWnCe + joFyXj77LuLmK+KXfPdhvlxa5Kl9XHSxKPHBVVQpwPqNMT+b2T1VLE2l7M9NfImy + iLcCAwEAAaNTMFEwHQYDVR0OBBYEFBKubDeR2lXwuyTrdyv6O7euPS4PMB8GA1Ud + IwQYMBaAFBKubDeR2lXwuyTrdyv6O7euPS4PMA8GA1UdEwEB/wQFMAMBAf8wDQYJ + KoZIhvcNAQEFBQADggEBAChCOIH8CkEpm1eqjsuuNPa93aduLjtnZXat5eIKsKCl + rL9nFslpg/DO5SeU5ynPY9F2QjX5CN/3RxDXum9vFfpXhTJphOv8N0uHU4ucmQxE + DN388Vt5VtN3V2pzNUL3JSiG6qeYG047/r/zhGFVpcgb2465G5mEwFT0qnkEseCC + VVZ63GN8hZgUobyRXxMIhkfWlbO1dgABB4VNyudq0CW8urmewkkbUBwCslvtUvPM + WuzpQjq2A80bvbrAqO5VUfvMcqRiUWkDgfa6cHXyV0o4N11mMIoxsMgh+PFYr6lR + BHkuQHqKEwP8kkWugIFj3TMcy9dYtXfMXWvzFaDoE4s= + -----END CERTIFICATE----- + """.trimIndent() + + private val enc2PrivateKey = """ + MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCP/N3gvxBd8TqH + MIzvmWZD3NUxcbd2lIIxL66WbuOSzqnrBuu5JJFQ2II3oNwYs/V0VEF6rV2gMsUW + R/BohzxcilIj9W1JQqbXfF/oZPb8XjduWEuTktka8VPbQ0P3VTh69tONu1L1QooI + aLUJ0f+vpndRUDFfdjxcY4uzj+U1jhNct/PV+nm+5Ejy7PE7vtj6amjVUPQv/PWP + CjWFrMfWaPpG0IY3xRVIpBKZLszIE/UC2nqusblyDz/stbczZhejUy9GthqGFC7s + WFpwno6Bcl4++y7i5ivil3z3Yb5cWuSpfVx0sSjxwVVUKcD6jTE/m9k9VSxNpezP + TXyJsoi3AgMBAAECggEACWwKFtlZ2FPfORZ3unwGwZ0TRFOFJljMdiyBF6307Vfh + rZP729clPS2Vw88eZ+1qu+yBhmYO0NtRo0Yc2LI0xHd2rYyzVI5sfYBRhFMLCHOf + 2/QiKet7knRFQP1TVr14Xy+Eo2slIBB1GNzFL/nSaeuSNjtxp6YEiCUpcJwTayAi + Squ5QWMxhlciLKvwUkraFRBqkugvMz3jXzuk/i+DcYlOgoj+tytweNn/azOMH9MH + mWI+3owYspjzE1rVpbrcWImvlnbInd0z9KaQPpBf7Njj7wtyBMaYww4K4GCMhboD + SQCYgpnznWkPIN3jyXtmNVSsZ1nvD+Laod+0p7giOQKBgQDA6KEKctYpbt051yTe + 2UP8hpq+MUSS7FIXiHlUc8s0PSujouypUyzfrPeL6yquI0GtKHkMVCWwfT+otMZR + VnklofrmPTPovvsUQFM4Di411NZwzfxEbBFyVXAUWcLd9NxJ1hZW7w+hLk/N5Bej + DOa2CncZmifyMNIlvIn7T1vDyQKBgQC/FE8HaDBoN98m/3rEjx7/rVtX8dCei5By + Fzg/yQ2u4ELbf/Qk/n4k75sy0690EwnFdJxVn2gdNgS1YDv8YP/N5Wfq8xnX9V9B + irWY/W24cN2qDNXm5i8o5wklyt+fDVqMcEHFfONUpLC+RYmOdc1rrFxPaQOYYYpp + dWsnuG0ofwKBgBm6rUf8ew35qG3/gP5sEgJLXbZCUfgapvRWkoAuFYs5IWno4BHR + cym+IyI5Um75atgSjtqTGpfIjMYOnmjY1L2tNg6hWRwQ5OIVlkPiuE0bvyI6hwwF + MeqC9LjyI+iAsSTz9fTQW9BOofw/ENwBa4AaMzpp8iv+UPkRhYHMWtvpAoGAX6As + RMqxnxaHCR9GM2Rk4RPC6OpNu2qhKVfRgKp/vIrjKrKIXpM2UgnPo8oovnBgrX7E + Vl1mX2gPRy4YFx/8JPCv5vcucdOMjmJ6q0v5QxrI9DdkPR/pbhDhlRZIf3LRZAMy + B0GPC2c4RKDMTI1L9pzVvbASaoo2GLz4mXJEvsUCgYEAibwFNXz1H52sZtL6/1zQ + 1rHCTS8qkryBhxl5eYa6MV5YkbLJZZstF0w2nLxkPba8NttS/nJqjX/iJobD5uLb + UzeD8jMeAWPNt4DZCtA4ossNYcXIMKqBVFKOANMvAAvLMpVdlNYSucNnTSQcLwI6 + 2J9mW5WvAAaG+j28Q/GKSuE= + """.trimIndent() + + @Test + fun testEncryptDecryptMetadata() { + val metadataKey = EncryptionUtils.generateKey() + + val metadata = DecryptedMetadata( + mutableListOf("hash1", "hash of key 2"), + false, + 1, + mutableMapOf( + Pair(EncryptionUtils.generateUid(), "Folder 1"), + Pair(EncryptionUtils.generateUid(), "Folder 2"), + Pair(EncryptionUtils.generateUid(), "Folder 3") + ), + mutableMapOf( + Pair( + EncryptionUtils.generateUid(), + DecryptedFile( + "file 1.png", + "image/png", + "initializationVector", + "authenticationTag", + "key 1" + ) + ), + Pair( + EncryptionUtils.generateUid(), + DecryptedFile( + "file 2.png", + "image/png", + "initializationVector 2", + "authenticationTag 2", + "key 2" + ) + ) + ), + metadataKey + ) + val encrypted = encryptionUtilsV2.encryptMetadata(metadata, metadataKey) + val decrypted = encryptionUtilsV2.decryptMetadata(encrypted, metadataKey) + + assertEquals(metadata, decrypted) + } + + @Throws(Throwable::class) + @Test + fun encryptDecryptSymmetric() { + val string = "123" + val metadataKey = EncryptionUtils.generateKeyString() + + val e = EncryptionUtils.encryptStringSymmetricAsString( + string, + metadataKey.toByteArray() + ) + + val d = EncryptionUtils.decryptStringSymmetric(e, metadataKey.toByteArray()) + assertEquals(string, d) + + val encryptedMetadata = EncryptionUtils.encryptStringSymmetric( + string, + metadataKey.toByteArray(), + EncryptionUtils.ivDelimiter + ) + + val d2 = EncryptionUtils.decryptStringSymmetric( + encryptedMetadata.ciphertext, + metadataKey.toByteArray() + ) + assertEquals(string, d2) + + val decrypted = EncryptionUtils.decryptStringSymmetric( + encryptedMetadata.ciphertext, + metadataKey.toByteArray(), + encryptedMetadata.authenticationTag, + encryptedMetadata.nonce + ) + + assertEquals(string, EncryptionUtils.decodeBase64BytesToString(decrypted)) + } + + @Test + fun testEncryptDecryptUser() { + val metadataKeyBase64 = EncryptionUtils.generateKeyString() + val metadataKey = EncryptionUtils.decodeStringToBase64Bytes(metadataKeyBase64) + + val user = DecryptedUser("t1", encryptionTestUtils.t1PublicKey) + + val encryptedUser = encryptionUtilsV2.encryptUser(user, metadataKey) + assertNotEquals(encryptedUser.encryptedMetadataKey, metadataKeyBase64) + + val decryptedMetadataKey = encryptionUtilsV2.decryptMetadataKey(encryptedUser, encryptionTestUtils.t1PrivateKey) + val decryptedMetadataKeyBase64 = EncryptionUtils.encodeBytesToBase64String(decryptedMetadataKey) + + assertEquals(metadataKeyBase64, decryptedMetadataKeyBase64) + } + + @Throws(com.owncloud.android.operations.UploadException::class, Throwable::class) + @Test + fun testEncryptDecryptMetadataFile() { + val enc1 = MockUser("enc1", "Nextcloud") + + val root = OCFile("/") + storageManager.saveFile(root) + + val folder = OCFile("/enc/").apply { + parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException() + } + + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1.accountName, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encrypted) + + val decrypted = encryptionUtilsV2.decryptFolderMetadataFile( + encrypted, + enc1.accountName, + enc1PrivateKey, + folder, + storageManager, + client, + 0, + signature, + user, + targetContext, + arbitraryDataProvider + ) + + assertEquals(metadataFile, decrypted) + } + + @Test + fun addFile() { + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + assertEquals(2, metadataFile.metadata.files.size) + assertEquals(1, metadataFile.metadata.counter) + + val updatedMetadata = encryptionUtilsV2.addFileToMetadata( + EncryptionUtils.generateUid(), + OCFile("/test.jpg").apply { + mimeType = MimeType.JPEG + }, + EncryptionUtils.generateIV(), + EncryptionUtils.generateUid(), // random string, not real tag + EncryptionUtils.generateKey(), + metadataFile, + storageManager + ) + + assertEquals(3, updatedMetadata.metadata.files.size) + assertEquals(2, updatedMetadata.metadata.counter) + } + + @Test + fun removeFile() { + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + assertEquals(2, metadataFile.metadata.files.size) + + val filename = metadataFile.metadata.files.keys.first() + + encryptionUtilsV2.removeFileFromMetadata(filename, metadataFile) + + assertEquals(1, metadataFile.metadata.files.size) + } + + @Test + fun renameFile() { + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + assertEquals(2, metadataFile.metadata.files.size) + + val key = metadataFile.metadata.files.keys.first() + val decryptedFile = metadataFile.metadata.files[key] + val filename = decryptedFile?.filename + val newFilename = "New File 1" + + encryptionUtilsV2.renameFile(key, newFilename, metadataFile) + + assertEquals(newFilename, metadataFile.metadata.files[key]?.filename) + assertNotEquals(filename, newFilename) + assertNotEquals(filename, metadataFile.metadata.files[key]?.filename) + } + + @Test + fun addFolder() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + assertEquals(2, metadataFile.metadata.files.size) + assertEquals(3, metadataFile.metadata.folders.size) + + val updatedMetadata = encryptionUtilsV2.addFolderToMetadata( + EncryptionUtils.generateUid(), + "new subfolder", + metadataFile, + folder, + storageManager + ) + + assertEquals(2, updatedMetadata.metadata.files.size) + assertEquals(4, updatedMetadata.metadata.folders.size) + } + + @Test + fun removeFolder() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + assertEquals(2, metadataFile.metadata.files.size) + assertEquals(3, metadataFile.metadata.folders.size) + + val encryptedFileName = EncryptionUtils.generateUid() + var updatedMetadata = encryptionUtilsV2.addFolderToMetadata( + encryptedFileName, + "new subfolder", + metadataFile, + folder, + storageManager + ) + + assertEquals(2, updatedMetadata.metadata.files.size) + assertEquals(4, updatedMetadata.metadata.folders.size) + + updatedMetadata = encryptionUtilsV2.removeFolderFromMetadata( + encryptedFileName, + updatedMetadata + ) + + assertEquals(2, updatedMetadata.metadata.files.size) + assertEquals(3, updatedMetadata.metadata.folders.size) + } + + @Test + fun verifyMetadata() { + val folder = OCFile("/e/") + val enc1 = MockUser("enc1", "Nextcloud") + val metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + val encrypted = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + enc1UserId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encrypted) + + encryptionUtilsV2.verifyMetadata(encrypted, metadataFile, 0, signature) + + assertTrue(true) // if we reach this, test is successful + } + + private fun generateDecryptedFileV1(): com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile { + return com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply { + encrypted = Data().apply { + key = EncryptionUtils.generateKeyString() + filename = "Random filename.jpg" + mimetype = MimeType.JPEG + version = 1.0 + } + initializationVector = EncryptionUtils.generateKeyString() + authenticationTag = EncryptionUtils.generateKeyString() + } + } + + @Test + fun testMigrateDecryptedV1ToV2() { + val v1 = generateDecryptedFileV1() + val v2 = encryptionUtilsV2.migrateDecryptedFileV1ToV2(v1) + + assertEquals(v1.encrypted.filename, v2.filename) + assertEquals(v1.encrypted.mimetype, v2.mimetype) + assertEquals(v1.authenticationTag, v2.authenticationTag) + assertEquals(v1.initializationVector, v2.nonce) + assertEquals(v1.encrypted.key, v2.key) + } + + @Test + fun testMigrateMetadataV1ToV2() { + OCFile("/").apply { + storageManager.saveFile(this) + } + + val folder = OCFile("/enc/").apply { + parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException() + } + + val v1 = DecryptedFolderMetadataFileV1().apply { + metadata = com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata().apply { + metadataKeys = mapOf(Pair(0, EncryptionUtils.generateKeyString())) + } + files = mapOf( + Pair(EncryptionUtils.generateUid(), generateDecryptedFileV1()), + Pair(EncryptionUtils.generateUid(), generateDecryptedFileV1()), + Pair( + EncryptionUtils.generateUid(), + com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply { + encrypted = Data().apply { + key = EncryptionUtils.generateKeyString() + filename = "subFolder" + mimetype = MimeType.WEBDAV_FOLDER + } + initializationVector = EncryptionUtils.generateKeyString() + authenticationTag = null + } + ) + ) + } + val v2 = encryptionUtilsV2.migrateV1ToV2( + v1, + enc1UserId, + enc1Cert, + folder, + storageManager + ) + + assertEquals(2, v2.metadata.files.size) + assertEquals(1, v2.metadata.folders.size) + assertEquals(1, v2.users.size) // only one user upon migration + } + + @Throws(com.owncloud.android.operations.UploadException::class, Throwable::class) + @Test + fun addSharee() { + val enc1 = MockUser("enc1", "Nextcloud") + val enc2 = MockUser("enc2", "Nextcloud") + + val root = OCFile("/") + storageManager.saveFile(root) + + val folder = OCFile("/enc/").apply { + parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException() + } + + var metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + + metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert) + + val encryptedMetadataFile = encryptionUtilsV2.encryptFolderMetadataFile( + metadataFile, + client.userId, + folder, + storageManager, + client, + enc1PrivateKey, + user, + targetContext, + arbitraryDataProvider + ) + + val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encryptedMetadataFile) + + val decryptedByEnc1 = encryptionUtilsV2.decryptFolderMetadataFile( + encryptedMetadataFile, + enc1.accountName, + enc1PrivateKey, + folder, + storageManager, + client, + metadataFile.metadata.counter, + signature, + user, + targetContext, + arbitraryDataProvider + ) + assertEquals(metadataFile.metadata, decryptedByEnc1.metadata) + + val decryptedByEnc2 = encryptionUtilsV2.decryptFolderMetadataFile( + encryptedMetadataFile, + enc2.accountName, + enc2PrivateKey, + folder, + storageManager, + client, + metadataFile.metadata.counter, + signature, + user, + targetContext, + arbitraryDataProvider + ) + assertEquals(metadataFile.metadata, decryptedByEnc2.metadata) + } + + @Test + fun removeSharee() { + val enc1 = MockUser("enc1", "Nextcloud") + val enc2 = MockUser("enc2", "Nextcloud") + var metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) + metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert) + + assertEquals(2, metadataFile.users.size) + + metadataFile = encryptionUtilsV2.removeShareeFromMetadata(metadataFile, enc2.accountName) + + assertEquals(1, metadataFile.users.size) + } + + private fun generateDecryptedFolderMetadataFile(user: User, cert: String): DecryptedFolderMetadataFile { + val metadata = DecryptedMetadata( + mutableListOf("hash1", "hash of key 2"), + false, + 1, + mutableMapOf( + Pair(EncryptionUtils.generateUid(), "Folder 1"), + Pair(EncryptionUtils.generateUid(), "Folder 2"), + Pair(EncryptionUtils.generateUid(), "Folder 3") + ), + mutableMapOf( + Pair( + EncryptionUtils.generateUid(), + DecryptedFile( + "file 1.png", + "image/png", + "initializationVector", + "authenticationTag", + "key 1" + ) + ), + Pair( + EncryptionUtils.generateUid(), + DecryptedFile( + "file 2.png", + "image/png", + "initializationVector 2", + "authenticationTag 2", + "key 2" + ) + ) + ), + EncryptionUtils.generateKey() + ) + + val users = mutableListOf( + DecryptedUser(user.accountName, cert) + ) + + metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadata.metadataKey)) + + return DecryptedFolderMetadataFile(metadata, users, mutableMapOf()) + } + + @Test + fun testGZip() { + val string = """ + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + This is a test. + It contains linewraps and special characters: + $$|²›³¥!’‘‘ + + """.trimIndent() + + val gzipped = encryptionUtilsV2.gZipCompress(string) + + val result = encryptionUtilsV2.gZipDecompress(gzipped) + + assertEquals(string, result) + } + + @Test + fun gunzip() { + val string = "H4sICNVkD2QAAwArycgsVgCiRIWS1OISPQDD9wZODwAAAA==" + val decoded = EncryptionUtils.decodeStringToBase64Bytes(string) + val gunzip = encryptionUtilsV2.gZipDecompress(decoded) + + assertEquals("this is a test.\n", gunzip) + } + +// @Test +// fun validate() { +// // ALEX +// val metadata1 = """{ +// "metadata": { +// "authenticationTag": "zMozev5R09UopLrq7Je1lw==", +// "ciphertext": "j0OBtUrEt4IveGiexjmGK7eKEaWrY70ZkteA5KxHDaZT/t2wwGy9j2FPQGpqXnW6OO3iAYPNgwFikI1smnfNvqdxzVDvhavl/IXa9Kg2niWyqK3D9zpz0YD6mDvl0XsOgTNVyGXNVREdWgzGEERCQoyHI1xowt/swe3KCXw+lf+XPF/t1PfHv0DiDVk70AeWGpPPPu6yggAIxB4Az6PEZhaQWweTC0an48l2FHj2MtB2PiMHtW2v7RMuE8Al3PtE4gOA8CMFrB+Npy6rKcFCXOgTZm5bp7q+J1qkhBDbiBYtvdsYujJ52Xa5SifTpEhGeWWLFnLLgPAQ8o6bXcWOyCoYfLfp4Jpft/Y7H8qzHbPewNSyD6maEv+xljjfU7hxibbszz5A4JjMdQy2BDGoTmJx7Mas+g6l6ZuHLVbdmgQOvD3waJBy6rOg0euux0Cn4bB4bIFEF2KvbhdGbY1Uiq9DYa7kEmSEnlcAYaHyroTkDg4ew7ER0vIBBMzKM3r+UdPVKKS66uyXtZc=", +// "nonce": "W+lxQJeGq7XAJiGfcDohkg==" +// }, +// "users": [{ +// "certificate": "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5leHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0\nNTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRFMRswGQYDVQQIDBJCYWRl\nbi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4\ndGNsb3VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E1AVnt98rvp3DmEfBHNzNB+DS9IBDkS\nSXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc9/\nS10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT\nnjz2Za/R68uXpxfz7J9uEXYiqsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp\nUthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1abQRxcu2CYnVo\n6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB\ntv70fTGkXWGAueDp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAyVtq9XAvW7nxSW/8\nhp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi\n6dg/7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB\nMz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid\nvigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8B8mh\nUtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P\nnDO4ew==\n-----END CERTIFICATE-----\n", +// "encryptedMetadataKey": "HVT49bYmwXbGs/dJ2avgU9unrKnPf03MYUI5ZysSR1Bz5pqz64gzH2GBAuUJ+Q4VmHtEfcMaWW7VXgzfCQv5xLBrk+RSgcLOKnlIya8jaDlfttWxbe8jJK+/0+QVPOc6ycA/t5HNCPg09hzj+gnb2L89UHxL5accZD0iEzb5cQbGrc/N6GthjgGrgFKtFf0HhDVplUr+DL9aTyKuKLBPjrjuZbv8M6ZfXO93mOMwSZH3c3rwDUHb/KEaTR/Og4pWQmrqr1VxGLqeV/+GKWhzMYThrOZAUz+5gsbckU2M5V9i+ph0yBI5BjOZVhNuDwW8yP8WtyRJwQc+UBRei/RGBQ==", +// "userId": "john" +// }], +// "version": "2" +// } +// +// """ +// +// val signature1 = +// "ewogICAgIm1ldGFkYXRhIjogewogICAgICAgICJhdXRoZW50aWNhdGlvblRhZyI6ICJ6TW96ZXY1UjA5VW9wTHJxN0plMWx3PT0iLAogICAgICAgICJjaXBoZXJ0ZXh0IjogImowT0J0VXJFdDRJdmVHaWV4am1HSzdlS0VhV3JZNzBaa3RlQTVLeEhEYVpUL3Qyd3dHeTlqMkZQUUdwcVhuVzZPTzNpQVlQTmd3RmlrSTFzbW5mTnZxZHh6VkR2aGF2bC9JWGE5S2cybmlXeXFLM0Q5enB6MFlENm1EdmwwWHNPZ1ROVnlHWE5WUkVkV2d6R0VFUkNRb3lISTF4b3d0L3N3ZTNLQ1h3K2xmK1hQRi90MVBmSHYwRGlEVms3MEFlV0dwUFBQdTZ5Z2dBSXhCNEF6NlBFWmhhUVd3ZVRDMGFuNDhsMkZIajJNdEIyUGlNSHRXMnY3Uk11RThBbDNQdEU0Z09BOENNRnJCK05weTZyS2NGQ1hPZ1RabTVicDdxK0oxcWtoQkRiaUJZdHZkc1l1ako1MlhhNVNpZlRwRWhHZVdXTEZuTExnUEFROG82YlhjV095Q29ZZkxmcDRKcGZ0L1k3SDhxekhiUGV3TlN5RDZtYUV2K3hsampmVTdoeGliYnN6ejVBNEpqTWRReTJCREdvVG1KeDdNYXMrZzZsNlp1SExWYmRtZ1FPdkQzd2FKQnk2ck9nMGV1dXgwQ240YkI0YklGRUYyS3ZiaGRHYlkxVWlxOURZYTdrRW1TRW5sY0FZYUh5cm9Ua0RnNGV3N0VSMHZJQkJNektNM3IrVWRQVktLUzY2dXlYdFpjPSIsCiAgICAgICAgIm5vbmNlIjogIlcrbHhRSmVHcTdYQUppR2ZjRG9oa2c9PSIKICAgIH0sCiAgICAidXNlcnMiOiB7CiAgICAgICAgImNlcnRpZmljYXRlIjogIi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLVxuTUlJRGtEQ0NBbmlnQXdJQkFnSUJBREFOQmdrcWhraUc5dzBCQVFVRkFEQmhNUXN3Q1FZRFZRUUdFd0pFUlRFYlxuTUJrR0ExVUVDQXdTUW1Ga1pXNHRWM1ZsY25SMFpXMWlaWEpuTVJJd0VBWURWUVFIREFsVGRIVjBkR2RoY25ReFxuRWpBUUJnTlZCQW9NQ1U1bGVIUmpiRzkxWkRFTk1Bc0dBMVVFQXd3RWFtOW9iakFlRncweU16QTNNVFF3TnpNMFxuTlRaYUZ3MDBNekEzTURrd056TTBOVFphTUdFeEN6QUpCZ05WQkFZVEFrUkZNUnN3R1FZRFZRUUlEQkpDWVdSbFxuYmkxWGRXVnlkSFJsYldKbGNtY3hFakFRQmdOVkJBY01DVk4wZFhSMFoyRnlkREVTTUJBR0ExVUVDZ3dKVG1WNFxuZEdOc2IzVmtNUTB3Q3dZRFZRUUREQVJxYjJodU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQlxuQ2dLQ0FRRUE3ajNFcjVZYWhKVDBMQW5TUkxocHFiUm8rRTFBVm50OThydnAzRG1FZkJITnpOQitEUzlJQkRrU1xuU1hNL1l0ZkFjaTZUY3c4dWpWQmpyWlgvV0VtcmY4eW5RSHhZbVNhSlNuUDh1QVQzMDYvTWNlWnBkcHJ1RWM5L1xuUzEwYTd2cDU0WmJsZDROWWRtZlM3MW9WRlZLZ003Yy9WdGh4K3JndTQ4ZnV4emJXQXZWWUxGY3g0N2h6MERKVFxubmp6MlphL1I2OHVYcHhmejdKOXVFWFlpcXNBcy9Gb2JEc0xabHVUM1J5eXdWUndLQmVkMUVaeFVlTElKaXl4cFxuVXRoaEdmSWI4YjNWZjlqWm9VVmkzbTVnbWM0c3BKUUh2WUFrZlpZSHpkOXJhczhqQnUxYWJRUnhjdTJDWW5Wb1xuNlkwbVR4aEtoUVMvbjVnanYzRXhpUUYzd3AvWFl3SURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVVtVGVJTFZ1QlxudHY3MGZUR2tYV0dBdWVEcDVrQXdId1lEVlIwakJCZ3dGb0FVbVRlSUxWdUJ0djcwZlRHa1hXR0F1ZURwNWtBd1xuRHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBeVZ0cTlYQXZXN254U1cvOFxuaHAzMHo2eGJ6R2l1dmlYaHkvSm85MVZFYThJUnNXQ0NuM09tREZpVmR1VEVvd3g3NnRmOGNsSlAwZ2s3UG96aVxuNmRnLzdGaW4rRnFRR1hmQ2s4YkxBaDlnWEtBaWtRMkdLOHlSTjNzbFJGd1lDMm1tMjNIckxkS1haSFVxSmNwQlxuTXoyenNTck9HUGoxWXNZT2wvVThGVTZLQTdZajdVM3E3a0RNWVRBZ3pVUFpBSCtkMURJU0dXcFpzTWEwUllpZFxudmlnQ0NMQnlpY2NtUy9DbzRTYjFlc0Y1OEgrWXRWNStuRkJSd3g4ODFVMmcyVGdES0YxbFBNSy95M2Q4QjhtaFxuVXRXK2xGeFJwdnlOVURwc01qT0VyT3J0TkZFWWJnb1VKTHRxd0JNbXlHUitubW1oNnhuYTMzMVFXY1JBbXcwUFxubkRPNGV3PT1cbi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS1cbiIsCiAgICAgICAgImVuY3J5cHRlZE1ldGFkYXRhS2V5IjogIkhWVDQ5Ylltd1hiR3MvZEoyYXZnVTl1bnJLblBmMDNNWVVJNVp5c1NSMUJ6NXBxejY0Z3pIMkdCQXVVSitRNFZtSHRFZmNNYVdXN1ZYZ3pmQ1F2NXhMQnJrK1JTZ2NMT0tubEl5YThqYURsZnR0V3hiZThqSksrLzArUVZQT2M2eWNBL3Q1SE5DUGcwOWh6aitnbmIyTDg5VUh4TDVhY2NaRDBpRXpiNWNRYkdyYy9ONkd0aGpnR3JnRkt0RmYwSGhEVnBsVXIrREw5YVR5S3VLTEJQanJqdVpidjhNNlpmWE85M21PTXdTWkgzYzNyd0RVSGIvS0VhVFIvT2c0cFdRbXJxcjFWeEdMcWVWLytHS1doek1ZVGhyT1pBVXorNWdzYmNrVTJNNVY5aStwaDB5Qkk1QmpPWlZoTnVEd1c4eVA4V3R5Ukp3UWMrVUJSZWkvUkdCUT09IiwKICAgICAgICAidXNlcklkIjogImpvaG4iCiAgICB9LAogICAgInZlcnNpb24iOiAiMiIKfQo=" +// +// // TOBI +// val metadata = +// """{"metadata":{"authenticationTag":"qDcJnAAGtGDlHWiQMBfXgw\u003d\u003d","ciphertext":"3zUhwIgJWMB7DvrbsDaMvh8MbJdoTxL0OMPCCdYSfBt7gB+V/hwqelL1IOaLto3avhHGSebnrotF06iEP/jZwWg9hApIPTHc8B4XTOY0/kezqYyVqTyquTUZpDpqgVAheQskZZ8I4Ir0seajUkt4KtVRfzO6v8CePRrEg6uKwdYsqDcJnAAGtGDlHWiQMBfXgw\u003d\u003d|4hbOyn1ykQL+9D6SnPY3cQ\u003d\u003d","nonce":"4hbOyn1ykQL+9D6SnPY3cQ\u003d\u003d"},"users":[{"certificate":"-----BEGIN CERTIFICATE-----\nMIIC6DCCAdCgAwIBAgIBADANBgkqhkiG9w0BAQUFADANMQswCQYDVQQDDAJ0MTAe\nFw0yMzA3MjUwNzU3MTJaFw00MzA3MjAwNzU3MTJaMA0xCzAJBgNVBAMMAnQxMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtafHmDBcBqIu4HmMxMDW3j0S\ny+S0YaKwHnBRt85KSwcEov0B5FOLuLknoBGx4Dn3u93ilThXXxacPMHeXL7WPuAs\n21/G7vsqwvrRRnCduf+FUO/AZeDCNErzpsQ8LmTa4PUloLPUcImpSjrHwhMs9Ekv\nEbLRjbeSmSp9XvM+1fV/3jkT5jkOSnCFx5TGwGN5uHqwUir4UWXasvg253NK2XmW\nipKCDCR9TmH1baP3pNdoiChdmErT1c6E4DbBXpTw8XgP5ZbYH+qg1UQ/hC8nRJ3D\nyCcHL+dg/GYraBMhDn4w2Vvq77xNNoNWQ9cT5Ay6cJbQLBQoJQirygQFrobYRQID\nAQABo1MwUTAdBgNVHQ4EFgQUE9zCeA9/QMAtVgLxD23X6ZcodhMwHwYDVR0jBBgw\nFoAUE9zCeA9/QMAtVgLxD23X6ZcodhMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG\n9w0BAQUFAAOCAQEAZdy/YjJlvnz3FQwxp6oVtMJccpdxveEPfLzgaverhtd/vP8O\nAvDzOLgQJHmrDS91SG503eU4cYGyuNKwd77OyTnqMg+GUEmJhGfPpSVrEIdh65jv\nq61T4oqBdehevVmBq54rGiwL0DGv1DlXQlwiJZP4qni2KnOEFcnvL3gVtRnQjXQ+\nkHvlMshkK6w021EMV5NfjG2zg67wC65rLaej5f6Ssp2S7g2VtmE4aXq1bjAuEbqk\n4TiyZHLDdsJuqzyGyyOpMV7i9ucXDoaZt9cGS9hT2vRxTrSH63vKR8Xeig9+stLw\nt9ONcUqCKP7hd8rajtxM4JIIRExwD8OkgARWGg\u003d\u003d\n-----END CERTIFICATE-----\n","encryptedMetadataKey":"s4kDkkLpk1mSmXedP7huiCNC4DYmDAmA2VYGem5M8jIGPC6miVQoo4WXZrEBhdsLw7Msf5iT3A3fTaHhwsI8Jf4McsFyM9/FXT1mCEaGOEpNjbKOlJY1uPUFNOhLqUfFiBos6oBT53hWwoXWjytYvLBbXuXY5YLOysjgBh6URrgFUZAJAmcOJ6OFKgfIIthoqkQc7CQUY97VsRzAXzeYTANBc2yW1pSN51HqftvMzvewFRsJQLcu7a9NjpTdG9LiLhn5eLXOLymXEE/aaPHKXeprlXLzrdWU1xwZRJqV+to2FEiH6CQNsO4+9h5m0VjXekiNeAFrsXB5cJgUipGuzQ\u003d\u003d","userId":"t1"}],"version":"2.0"}""" +// +// val base = EncryptionUtils.encodeStringToBase64String(metadata) +// +// val signature = +// "MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwCwYJKoZIhvcNAQcBoIAwggLoMIIB0KADAgECAgEAMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAMMAnQxMB4XDTIzMDcyNTA3NTcxMloXDTQzMDcyMDA3NTcxMlowDTELMAkGA1UEAwwCdDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1p8eYMFwGoi7geYzEwNbePRLL5LRhorAecFG3zkpLBwSi/QHkU4u4uSegEbHgOfe73eKVOFdfFpw8wd5cvtY+4CzbX8bu+yrC+tFGcJ25/4VQ78Bl4MI0SvOmxDwuZNrg9SWgs9RwialKOsfCEyz0SS8RstGNt5KZKn1e8z7V9X/eORPmOQ5KcIXHlMbAY3m4erBSKvhRZdqy+Dbnc0rZeZaKkoIMJH1OYfVto/ek12iIKF2YStPVzoTgNsFelPDxeA/lltgf6qDVRD+ELydEncPIJwcv52D8ZitoEyEOfjDZW+rvvE02g1ZD1xPkDLpwltAsFCglCKvKBAWuhthFAgMBAAGjUzBRMB0GA1UdDgQWBBQT3MJ4D39AwC1WAvEPbdfplyh2EzAfBgNVHSMEGDAWgBQT3MJ4D39AwC1WAvEPbdfplyh2EzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBl3L9iMmW+fPcVDDGnqhW0wlxyl3G94Q98vOBq96uG13+8/w4C8PM4uBAkeasNL3VIbnTd5ThxgbK40rB3vs7JOeoyD4ZQSYmEZ8+lJWsQh2HrmO+rrVPiioF16F69WYGrnisaLAvQMa/UOVdCXCIlk/iqeLYqc4QVye8veBW1GdCNdD6Qe+UyyGQrrDTbUQxXk1+MbbODrvALrmstp6Pl/pKynZLuDZW2YThperVuMC4RuqThOLJkcsN2wm6rPIbLI6kxXuL25xcOhpm31wZL2FPa9HFOtIfre8pHxd6KD36y0vC3041xSoIo/uF3ytqO3EzgkghETHAPw6SABFYaAAAxggHUMIIB0AIBATASMA0xCzAJBgNVBAMMAnQxAgEAMAsGCWCGSAFlAwQCAaCBljAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMzA3MjgwNzMwMTJaMCsGCSqGSIb3DQEJNDEeMBwwCwYJYIZIAWUDBAIBoQ0GCSqGSIb3DQEBCwUAMC8GCSqGSIb3DQEJBDEiBCAx7RTJg7hbY5Mkzjw3f6qhX7k/J0FdVz2cL3ow0AmyYjANBgkqhkiG9w0BAQsFAASCAQAbUmb9e7eoIcPNzDSmnzbrueBzgT8YszNGEI+1YCq8XdWN4kDztvP1ZNV21VCO6BvcbfUAnXXgcX5BPeLZNsgXPj3c8TbD59GQl3oT/tIchgMsA20RdAtIwvItlZKh+X6sp0OHkRPYSk/mEYKCKPqrKdJicRWex8ItCwpDR91KSOiKJrN/+DKOGG0sVI9gjzbtrHsN8HmVKxOoNV+wwipcLsWsEmuV+wvPCQ9HJidLX9Q17Bgfc+qJg19aB6iKLWPhjgnfpKGbK5VJuQTdDWPUJ2O4G3W/iwxJ0hAJ7tks4zIATmgGzhgTWYx5LVXbKcuL04xhIOjqwedHeCSBZSSaAAAAAAAA" +// +// val metadataFile = EncryptionUtils.deserializeJSON( +// metadata, +// object : TypeToken() {} +// ) +// assertNotNull(metadataFile) +// +// val certJohnString = metadataFile.users[0].certificate +// val certJohn = EncryptionUtils.convertCertFromString(certJohnString) +// +// val t1String = """-----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 t1cert = EncryptionUtils.convertCertFromString(t1String) +// val t1PrivateKeyKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) +// +// // val signed = encryptionUtilsV2.getMessageSignature( +// // t1cert, +// // t1PrivateKeyKey, +// // metadataFile +// // ) +// +// assertTrue(encryptionUtilsV2.verifySignedMessage(signature1, metadata1, listOf(certJohn, t1cert))) +// } + + @Throws(Throwable::class) + @Test + fun testSigning() { + val metadata = + """{"metadata": {"authenticationTag": "zMozev5R09UopLrq7Je1lw==","ciphertext": "j0OBtUrEt4IveGiexjm + |GK7eKEaWrY70ZkteA5KxHDaZT/t2wwGy9j2FPQGpqXnW6OO3iAYPNgwFikI1smnfNvqdxzVDvhavl/IXa9Kg2niWyqK3D9 + |zpz0YD6mDvl0XsOgTNVyGXNVREdWgzGEERCQoyHI1xowt/swe3KCXw+lf+XPF/t1PfHv0DiDVk70AeWGpPPPu6yggAIxB4 + |Az6PEZhaQWweTC0an48l2FHj2MtB2PiMHtW2v7RMuE8Al3PtE4gOA8CMFrB+Npy6rKcFCXOgTZm5bp7q+J1qkhBDbiBYtv + |dsYujJ52Xa5SifTpEhGeWWLFnLLgPAQ8o6bXcWOyCoYfLfp4Jpft/Y7H8qzHbPewNSyD6maEv+xljjfU7hxibbszz5A4Jj + |MdQy2BDGoTmJx7Mas+g6l6ZuHLVbdmgQOvD3waJBy6rOg0euux0Cn4bB4bIFEF2KvbhdGbY1Uiq9DYa7kEmSEnlcAYaHyr + |oTkDg4ew7ER0vIBBMzKM3r+UdPVKKS66uyXtZc=","nonce": "W+lxQJeGq7XAJiGfcDohkg=="},"users": [{"cert + |ificate": "-----BEGIN CERTIFICATE-----\nMIIDkDCCAnigAwIBAgIBADANBgkqhkiG9w0BAQUFADBhMQswCQYDVQ + |QGEwJERTEb\nMBkGA1UECAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRIwEAYDVQQHDAlTdHV0dGdhcnQx\nEjAQBgNVBAoMCU5l + |eHRjbG91ZDENMAsGA1UEAwwEam9objAeFw0yMzA3MTQwNzM0\nNTZaFw00MzA3MDkwNzM0NTZaMGExCzAJBgNVBAYTAkRF + |MRswGQYDVQQIDBJCYWRl\nbi1XdWVydHRlbWJlcmcxEjAQBgNVBAcMCVN0dXR0Z2FydDESMBAGA1UECgwJTmV4\ndGNsb3 + |VkMQ0wCwYDVQQDDARqb2huMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEA7j3Er5YahJT0LAnSRLhpqbRo+E + |1AVnt98rvp3DmEfBHNzNB+DS9IBDkS\nSXM/YtfAci6Tcw8ujVBjrZX/WEmrf8ynQHxYmSaJSnP8uAT306/MceZpdpruEc + |9/\nS10a7vp54Zbld4NYdmfS71oVFVKgM7c/Vthx+rgu48fuxzbWAvVYLFcx47hz0DJT\nnjz2Za/R68uXpxfz7J9uEXYi + |qsAs/FobDsLZluT3RyywVRwKBed1EZxUeLIJiyxp\nUthhGfIb8b3Vf9jZoUVi3m5gmc4spJQHvYAkfZYHzd9ras8jBu1a + |bQRxcu2CYnVo\n6Y0mTxhKhQS/n5gjv3ExiQF3wp/XYwIDAQABo1MwUTAdBgNVHQ4EFgQUmTeILVuB\ntv70fTGkXWGAue + |Dp5kAwHwYDVR0jBBgwFoAUmTeILVuBtv70fTGkXWGAueDp5kAw\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAA + |OCAQEAyVtq9XAvW7nxSW/8\nhp30z6xbzGiuviXhy/Jo91VEa8IRsWCCn3OmDFiVduTEowx76tf8clJP0gk7Pozi\n6dg/ + |7Fin+FqQGXfCk8bLAh9gXKAikQ2GK8yRN3slRFwYC2mm23HrLdKXZHUqJcpB\nMz2zsSrOGPj1YsYOl/U8FU6KA7Yj7U3q + |7kDMYTAgzUPZAH+d1DISGWpZsMa0RYid\nvigCCLByiccmS/Co4Sb1esF58H+YtV5+nFBRwx881U2g2TgDKF1lPMK/y3d8 + |B8mh\nUtW+lFxRpvyNUDpsMjOErOrtNFEYbgoUJLtqwBMmyGR+nmmh6xna331QWcRAmw0P\nnDO4ew==\n-----END CER + |TIFICATE-----\n","encryptedMetadataKey": "HVT49bYmwXbGs/dJ2avgU9unrKnPf03MYUI5ZysSR1Bz5pqz64gz + |H2GBAuUJ+Q4VmHtEfcMaWW7VXgzfCQv5xLBrk+RSgcLOKnlIya8jaDlfttWxbe8jJK+/0+QVPOc6ycA/t5HNCPg09hzj+g + |nb2L89UHxL5accZD0iEzb5cQbGrc/N6GthjgGrgFKtFf0HhDVplUr+DL9aTyKuKLBPjrjuZbv8M6ZfXO93mOMwSZH3c3rw + |DUHb/KEaTR/Og4pWQmrqr1VxGLqeV/+GKWhzMYThrOZAUz+5gsbckU2M5V9i+ph0yBI5BjOZVhNuDwW8yP8WtyRJwQc+UB + |Rei/RGBQ==","userId": "john"}],"version": "2"} + """.trimMargin() + + val base64Metadata = EncryptionUtils.encodeStringToBase64String(metadata) + + val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) + val certificateT1 = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) + val certificateEnc2 = EncryptionUtils.convertCertFromString(enc2Cert) + + val signed = encryptionUtilsV2.signMessage( + certificateT1, + privateKey, + metadata + ) + + val base64Ans = encryptionUtilsV2.extractSignedString(signed) + + // verify + val certs = listOf( + certificateEnc2, + certificateT1 + ) + assertTrue(encryptionUtilsV2.verifySignedMessage(signed, certs)) + assertTrue(encryptionUtilsV2.verifySignedMessage(base64Ans, base64Metadata, certs)) + } + + @Throws(Throwable::class) + @Test + fun sign() { + val sut = "randomstring123" + val json = "randomstring123" + val jsonBase64 = EncryptionUtils.encodeStringToBase64String(json) + + val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) + val certificate = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) + + val signed = encryptionUtilsV2.signMessage( + certificate, + privateKey, + sut + ) + + val base64Ans = encryptionUtilsV2.extractSignedString(signed) + + // verify + val certs = listOf( + EncryptionUtils.convertCertFromString(enc2Cert), + certificate + ) + assertTrue(encryptionUtilsV2.verifySignedMessage(signed, certs)) + assertTrue(encryptionUtilsV2.verifySignedMessage(base64Ans, jsonBase64, certs)) + } + + /** + * DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt -> decrypt -> JSON -> + * EncryptedFolderMetadata -> DecryptedFolderMetadata + */ + @Test + @Throws(Exception::class, Throwable::class) + fun encryptionMetadataV2() { + val decryptedFolderMetadata1: DecryptedFolderMetadataFile = + EncryptionTestUtils().generateFolderMetadataV2(client.userId, EncryptionTestIT.publicKey) + val root = OCFile("/") + storageManager.saveFile(root) + + val folder = OCFile("/enc") + folder.parentId = storageManager.getFileByDecryptedRemotePath("/")?.fileId ?: throw IllegalStateException() + + storageManager.saveFile(folder) + + decryptedFolderMetadata1.filedrop.clear() + + // encrypt + val encryptedFolderMetadata1 = encryptionUtilsV2.encryptFolderMetadataFile( + decryptedFolderMetadata1, + client.userId, + folder, + storageManager, + client, + EncryptionTestIT.publicKey, + user, + targetContext, + arbitraryDataProvider + ) + + val signature = encryptionUtilsV2.getMessageSignature(enc1Cert, enc1PrivateKey, encryptedFolderMetadata1) + + // serialize + val encryptedJson = EncryptionUtils.serializeJSON(encryptedFolderMetadata1) + + // de-serialize + val encryptedFolderMetadata2 = EncryptionUtils.deserializeJSON( + encryptedJson, + object : TypeToken() {} + ) + + // decrypt + val decryptedFolderMetadata2 = encryptionUtilsV2.decryptFolderMetadataFile( + encryptedFolderMetadata2!!, + getUserId(user), + EncryptionTestIT.privateKey, + folder, + fileDataStorageManager, + client, + decryptedFolderMetadata1.metadata.counter, + signature, + user, + targetContext, + arbitraryDataProvider + ) + + // compare + assertTrue( + EncryptionTestIT.compareJsonStrings( + EncryptionUtils.serializeJSON(decryptedFolderMetadata1), + EncryptionUtils.serializeJSON(decryptedFolderMetadata2) + ) + ) + } + + @Throws(Throwable::class) + @Test + fun decryptFiledropV2() { + val sut = EncryptedFiledrop( + """QE5nJmA8QC3rBJxbpsZu6MvkomwHMKTYf/3dEz9Zq3ITHLK/wNAIqWTbDehBJ7SlTfXakkKR9o0sOkUDI7PD8qJyv5hW7LzifszYGe + |xE0V1daFcCFApKrIEBABHVOq+ZHJd8IzNSz3hdA9bWd2eiaEGyQzgdTPELE6Ie84IwFANJHcaRB5B43aaDdbUXNJ4/oMboOReKTJ + |/vT6ZGhve4DRPEsez0quyDZDNlin5hD6UaUzw= + """.trimMargin(), + "HC87OgVzbR2CXdWp7rKI5A==", + "7PSq7INkM2WKfmEPpRpTPA==", + listOf( + EncryptedFiledropUser( + "android3", + """cNzk8cNyoTJ49Cj/x2WPlsMAnUWlZsfnKJ3VIRiczASeUYUFhaJpD8HDWE0uhkXSD7i9nzpe6pR7zllE7UE/QniDd+BQiF + |80E5fSO1KVfFkLZRT+2pX5oPnl4CVtMnxb4xG7J1nAUqMhfS8PtQIr0+S7NKDdrUc41aNOB/4kH0D9LSo/bSC38L7ewv + |mISM6ZFi1bfI1505kZV0HqcW12nZwHwe3s6rYkoSPBOPX1oPkvMYTVLkYuU+7DNL4HW7D9dc9X4bsSGLdj4joRi9FURi + |mMv6MOrWOnYlX2zmMKAF3nEjLlhngKG7pUi/qMIlft2AhRM4cJuuIQ29vvTGFFDQ== + """.trimMargin() + ) + ) + ) + + val privateKey = + """MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPNCnYcPgGQwCzL8sxLTiE0atn5tiW1nfPQc1aY/+aXvkpF4h2vT + |S/hQg2SCNFUlw8aYKksk5IH5FFcPv9QFG/TQDQOnZhi7moVPnVwLkx+cDfWQQs1EOhI/ZPdSo7MdaRLttbZZs/GJfnr1ziYZTxLO + |UUxT541cnSpqGTKmUXhGMoX+jQTmcn1NyBD537NdetOxSdMfvBIobjRQ70/9c1HFGQSrJa+DmPiis6iFkd1LH6WWRbreC6DsRSqK + |ne3sD1ujx39k+VxtBe035c2L9PbTMMW3kBdZxlRkV1tUQhDAys0K+CyvNIFsOjqvQKTnXNfWO+kVnpOkpbTK4imuPbAgMBAAECgf + |9T537U/6TuwJLSj4bfYev8qYaakfVIpyMkL33e4YBQnUzhlCPBVYgpHkDPwznk2XhjQQiVcRAycmUHBmy4aPkcOjuBmd87aTj03k + |niDk+doFDNU8myuwWTw/1fHdElRnLyZxEKrb391HD4SVVQMuxnw8UoC4iNcPnYneY/GTiTtB3dVcRKdabX3Oak2TFiJyJBtTz4RN + |sRYVXM3jyCbxj8uV+XNr+3OuQe5u7cV5gkXOXHqcNczOrxGzSXVGULuw8FiHIlhId7tot3dGdyVvWD9YIwwGA/9/3g8JixqpQHKZ + |6YJAeqltydisGa3CIIEzBAh52GJC7yzMKSC2ZAtW0CgYEA6B/O+EgtZthiXOwivqZmKKGgWGLSOGjVsExSa1iiTTz3EFwcdD54mU + |TKc6hw787NFlfN1m7B7EDQxIldRDI3One1q2dj87taco/qFqKsHuAuC3gmZIp2F4l2P8NpdHHFMzUzsfs+grY/wLHZiJdfOTdulA + |s9go5mDloMC96n0/UCgYEA5IQo7c4ZxwhlssIn89XaOlKGoIct07wsBMu47HZYFqgG2/NUN8zRfSdSvot+6zinAb6Z3iGZ2FBL+C + |MmoEMGwuXSQjWxeD//UU6V5AZqlgis5s9WakKWmkTkVV3bPSwW0DuNcqbMk7BxAXcQ6QGIiBtzeaPuL/3gzA9e9vm8xo8CgYEAqL + |I9S6nA/UZzLg8bLS1nf03/Z1ziZMajzk2ZdJRk1/dfow8eSskAAnvBGo8nDNFhsUQ8vwOdgeKVFtCx7JcGFkLbz+cC+CaIFExNFw + |hASOwp6oH2fQk3y+FGBA8ze8IXTCD1IftzMbHb4WIfsyo3tTB497S3jkOJHhMJQDMgC2UCgYEAzjUgRe98vWkrdFLWAKfSxFxiFg + |vF49JjGnTHy8HDHbbEccizD6NoyvooJb/1aMd3lRBtAtDpZhSXaTQ3D9lMCaWfxZV0LyH5AGLcyaasmfT8KU+iGEM8abuPHCWUyC + |+36nJC4tn3s7I9V2gdP1Xd4Yx7+KFgN7huGVYpiM61dasCgYAQs5mPHRBeU+BHtPRyaLHhYq+jjYeocwyOpfw5wkiH3jsyUWTK9+ + |GlAoV75SYvQVIQS0VH1C1/ajz9yV02frAaUXbGtZJbyeAcyy3DjCc7iF0swJ4slP3gGVJipVF4aQ0d9wMoJ7SBaaTR0ohXeUWmTT + |X+VGf+cZQ2IefKVnz9mg== + """.trimMargin() + + val decryptedFile = EncryptionUtilsV2().decryptFiledrop(sut, privateKey, arbitraryDataProvider, user) + assertEquals("test.txt", decryptedFile.filename) + } +} diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 45b3da6464..5f3e16a036 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/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 ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 20f9cb9571..5a9a208b94 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/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) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt index 4b6d60e050..5e9df449cd 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt +++ b/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? ) diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.java b/app/src/main/java/com/nextcloud/client/di/AppModule.java index d7d32b25d0..847f7b21b8 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.java +++ b/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(); + } + } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt index d4e0fbc537..346f337502 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt +++ b/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" } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java index 338b7cda17..c43923254a 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java +++ b/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); diff --git a/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java b/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java similarity index 96% rename from app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java rename to app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java index 4f4aafe19e..55927b1b06 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java +++ b/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 files; private Map filedrop; - public DecryptedFolderMetadata() { + public DecryptedFolderMetadataOld() { this.metadata = new Metadata(); this.files = new HashMap<>(); } - public DecryptedFolderMetadata(Metadata metadata, Map files) { + public DecryptedFolderMetadataOld(Metadata metadata, Map files) { this.metadata = metadata; this.files = files; } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 3d30d8aff2..3901ccbbbf 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/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, diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index 622e5945e7..4082456782 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -121,6 +121,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa private String lockToken; @Nullable private ImageDimension imageDimension; + private long e2eCounter = -1; @Nullable private GeoLocation geolocation; private List tags = new ArrayList<>(); @@ -1056,4 +1057,15 @@ public class OCFile implements Parcelable, Comparable, 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; + } + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java new file mode 100644 index 0000000000..4c279e3ce1 --- /dev/null +++ b/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 . + */ + +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; + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java new file mode 100644 index 0000000000..ff36946289 --- /dev/null +++ b/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 . + */ + +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; + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java new file mode 100644 index 0000000000..8117e3dc19 --- /dev/null +++ b/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 . + */ + +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 files; + private Map filedrop; + + public DecryptedFolderMetadataFileV1() { + this.metadata = new DecryptedMetadata(); + this.files = new HashMap<>(); + } + + public DecryptedFolderMetadataFileV1(DecryptedMetadata metadata, Map files) { + this.metadata = metadata; + this.files = files; + } + + public DecryptedMetadata getMetadata() { + return this.metadata; + } + + public Map getFiles() { + return this.files; + } + + public void setMetadata(DecryptedMetadata metadata) { + this.metadata = metadata; + } + + public void setFiles(Map files) { + this.files = files; + } + + @VisibleForTesting + public void setFiledrop(Map filedrop) { + this.filedrop = filedrop; + } + + public Map getFiledrop() { + return filedrop; + } + +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java new file mode 100644 index 0000000000..c2e2866c3a --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v1.decrypted; + +import java.util.Map; + +public class DecryptedMetadata { + transient + private Map metadataKeys; // outdated with v1.1 + private String metadataKey; + private String checksum; + private double version; + + @Override + public String toString() { + return String.valueOf(version); + } + + public Map 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 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; + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java new file mode 100644 index 0000000000..edccd60d25 --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v1.decrypted; + +import java.util.Map; + +public class Encrypted { + private Map metadataKeys; + + public Map getMetadataKeys() { + return this.metadataKeys; + } + + public void setMetadataKeys(Map metadataKeys) { + this.metadataKeys = metadataKeys; + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java new file mode 100644 index 0000000000..f63e2b89af --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v1.decrypted; + +import java.util.Map; + +public class Sharing { + private Map recipient; + private String signature; + + public Map getRecipient() { + return this.recipient; + } + + public String getSignature() { + return this.signature; + } + + public void setRecipient(Map recipient) { + this.recipient = recipient; + } + + public void setSignature(String signature) { + this.signature = signature; + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt new file mode 100644 index 0000000000..ee3c23d8b3 --- /dev/null +++ b/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 . + */ +package com.owncloud.android.datamodel.e2e.v1.encrypted + +class EncryptedFile(var encryptedBytes: ByteArray, var authenticationTag: String) diff --git a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java similarity index 73% rename from app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java rename to app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java index 51c23e1d7f..0f27532370 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java +++ b/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 . + * along with this program. If not, see . */ -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 files; private Map filedrop; - public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata, - Map files, - Map filesdrop) { + public EncryptedFolderMetadataFileV1(DecryptedMetadata metadata, + Map files, + Map 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; } diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt new file mode 100644 index 0000000000..8908b014f2 --- /dev/null +++ b/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 . + */ +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 +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt new file mode 100644 index 0000000000..cb0586bf4c --- /dev/null +++ b/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 . + */ +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 = mutableListOf(), + @Transient + val filedrop: MutableMap = HashMap(), + val version: String = "2.0" +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt new file mode 100644 index 0000000000..dc3a225246 --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v2.decrypted + +import com.owncloud.android.utils.EncryptionUtils + +data class DecryptedMetadata( + val keyChecksums: MutableList = mutableListOf(), + val deleted: Boolean = false, + var counter: Long = 0, + val folders: MutableMap = mutableMapOf(), + val files: MutableMap = 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 + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt new file mode 100644 index 0000000000..93a28a1572 --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v2.decrypted + +data class DecryptedUser( + val userId: String, + val certificate: String +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt new file mode 100644 index 0000000000..03208491a4 --- /dev/null +++ b/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 . + */ +package com.owncloud.android.datamodel.e2e.v2.encrypted + +data class EncryptedFiledrop( + val ciphertext: String, + val nonce: String, + val authenticationTag: String, + val users: List +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt new file mode 100644 index 0000000000..6bc6486159 --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v2.encrypted + +data class EncryptedFiledropUser( + val userId: String, + val encryptedFiledropKey: String +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt new file mode 100644 index 0000000000..3b1907f469 --- /dev/null +++ b/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 . + */ +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, + val filedrop: MutableMap?, + val version: String = "2.0" +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt new file mode 100644 index 0000000000..31af5e71a5 --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v2.encrypted + +data class EncryptedMetadata( + val ciphertext: String, + val nonce: String, + val authenticationTag: String +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt new file mode 100644 index 0000000000..c2cb7ce212 --- /dev/null +++ b/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 . + */ + +package com.owncloud.android.datamodel.e2e.v2.encrypted + +data class EncryptedUser( + val userId: String, + val certificate: String, + val encryptedMetadataKey: String +) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt new file mode 100644 index 0000000000..be691ab432 --- /dev/null +++ b/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 . + */ +package com.owncloud.android.datamodel.e2e.v2.encrypted + +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile + +class FiledropData { + private val files: Map = mutableMapOf() +} diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index eafc497902..1a4ccc5845 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/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 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"; diff --git a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java index e209d21bc3..64bcb78522 100644 --- a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -186,7 +186,7 @@ public class FileMenuFilter { private void filterShareFile(List 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 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 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 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()) { diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index cb1dab978e..4b9fc9908d 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/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 metadataPair = EncryptionUtils.retrieveMetadata(parent, - client, - privateKey, - publicKey, - arbitraryDataProvider, - user); + Pair 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 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 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 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); - - if (file != null && filename.equalsIgnoreCase(file.getEncrypted().getFilename())) { + 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; } } + + 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) { diff --git a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java index a84fa0983f..71a348d64b 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java +++ b/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 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 OCS + * Share API. */ 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 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; + } - if (result.isSuccess() && result.getData().size() > 0) { - OCShare share = (OCShare) result.getData().get(0); + // E2E: update metadata + if (isEncrypted) { + Object object = EncryptionUtils.downloadFolderMetadata(folder, + client, + context, + user + ); - //once creating share link update other information - UpdateShareInfoOperation updateShareInfoOperation = new UpdateShareInfoOperation(share, getStorageManager()); - updateShareInfoOperation.setExpirationDateInMillis(expirationDateInMillis); - updateShareInfoOperation.setHideFileDownload(hideFileDownload); - updateShareInfoOperation.setLabel(label); + if (object instanceof DecryptedFolderMetadataFileV1) { + throw new RuntimeException("Trying to share on e2e v1!"); + } - //update permissions for external share (will otherwise default to read-only) - updateShareInfoOperation.setPermissions(permissions); + DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object; - //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); + boolean metadataExists; + if (metadata == null) { + String cert = EncryptionUtils.retrievePublicKeyForUser(user, context); + metadata = new EncryptionUtilsV2().createDecryptedFolderMetadataFile(); + metadata.getUsers().add(new DecryptedUser(client.getUserId(), cert)); + + 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 unlockResult = EncryptionUtils.unlockFolder(folder, client, token); + if (!unlockResult.isSuccess()) { + return new RemoteOperationResult<>(new RuntimeException("Unlock failed")); } } - return result; + 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 shareResult; } private void updateData(OCShare share) { diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java index 517aeaca19..dc56c43ebe 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java +++ b/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, diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index b0a9749c1f..be25643a9b 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/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 localFilesMap = prefillLocalFilesMap(metadata, - mStorageManager.getFolderContent(mLocalFolder, false)); + Map 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,23 @@ public class RefreshFolderOperation extends RemoteOperation { return metadata; } - public static void updateFileNameForEncryptedFile(FileDataStorageManager storageManager, - @NonNull DecryptedFolderMetadata metadata, - OCFile updatedFile) { + public static void updateFileNameForEncryptedFileV1(FileDataStorageManager storageManager, + @NonNull DecryptedFolderMetadataFileV1 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.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; @@ -580,7 +634,46 @@ 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!"); + } + } + + public static void updateFileNameForEncryptedFile(FileDataStorageManager storageManager, + @NonNull DecryptedFolderMetadataFile metadata, + OCFile updatedFile) { + try { + 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; + + 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!"); } } @@ -634,7 +727,7 @@ public class RefreshFolderOperation extends RemoteOperation { } @NonNull - public static Map prefillLocalFilesMap(DecryptedFolderMetadata metadata, List localFiles) { + public static Map prefillLocalFilesMap(Object metadata, List localFiles) { Map localFilesMap = Maps.newHashMapWithExpectedSize(localFiles.size()); for (OCFile file : localFiles) { diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java b/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java index e052aea400..aae3ab6417 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java +++ b/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()); } diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java index 4095a1ccb2..e01059aa2f 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java +++ b/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 { 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 run(OwnCloudClient client) { + RemoteOperationResult 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); + token = EncryptionUtils.lockFolder(parentFolder, 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!"); - } - - // 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() { - }); - - metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, - privateKey, - arbitraryDataProvider, - user, - parentId); - } else { - throw new RemoteOperationFailedException("No Metadata found!"); - } + EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); + Pair 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 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()); } } } diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index ead21c30c9..bc1dc318e8 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/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 localFilesMap = - RefreshFolderOperation.prefillLocalFilesMap(metadata, - storageManager.getFolderContent(mLocalFolder, false)); + Map 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 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 children = getStorageManager().getFolderContent(mLocalFolder, false); for (OCFile child : children) { diff --git a/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java b/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java index a2a40bbf42..fadc83225d 100644 --- a/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java +++ b/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; + private final Context context; + private final User user; - public UnshareOperation(String remotePath, long shareId, FileDataStorageManager storageManager) { + 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 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()) { diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index d2a8b982ca..69eb593631 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/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 metadataPair = EncryptionUtils.retrieveMetadata(parentFile, - client, - privateKey, - publicKey, - arbitraryDataProvider, - user); + EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); +// kotlin.Pair metadataPair = +// encryptionUtilsV2.retrieveMetadata(parentFile, +// client, +// user, +// mContext); - metadataExists = metadataPair.first; - DecryptedFolderMetadata metadata = metadataPair.second; + 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; + } + + // 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 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); + String serializedFolderMetadata; + + // check if we need metadataKeys + if (metadata.getMetadata().getMetadataKey() != null) { + serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true); + } else { + serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); + } + + // upload metadata + EncryptionUtils.uploadMetadata(parentFile, + serializedFolderMetadata, + token, + client, + metadataExists, + E2EVersion.V1_2, + "", + arbitraryDataProvider, + user); + + // unlock + result = EncryptionUtils.unlockFolderV1(parentFile, client, token); + + if (result.isSuccess()) { + token = null; + } } else { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); - } + DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object; + encryptionUtilsV2.addFileToMetadata( + encryptedFileName, + mFile, + iv, + encryptedFile.getAuthenticationTag(), + key, + metadata, + getStorageManager()); - // upload metadata - EncryptionUtils.uploadMetadata(parentFile, - serializedFolderMetadata, - token, - client, - metadataExists, - arbitraryDataProvider, - user); + // upload metadata + encryptionUtilsV2.serializeAndUploadMetadata(parentFile, + metadata, + token, + client, + metadataExists, + mContext, + user, + getStorageManager()); - // unlock - result = EncryptionUtils.unlockFolder(parentFile, client, token); + // unlock + result = EncryptionUtils.unlockFolder(parentFile, client, token); - if (result.isSuccess()) { - token = null; + 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 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. *

- * 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 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 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. *

- * 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())); } diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt new file mode 100644 index 0000000000..e33b582df1 --- /dev/null +++ b/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 . + * + */ + +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 + } +} diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index 18bf0c0a09..91de26cd59 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/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 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> result = searchRequest.execute(user, getContext()); List 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; diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index fc18ebb174..3692dcab7a 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/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; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index 151d177df8..c8ef734ba8 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/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()); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java index da23f3168a..3017ff43b8 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java +++ b/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) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java index afb294126f..b5da833277 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java +++ b/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(); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java index b3b9f7bc36..6a747ef497 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/FileDetailTabAdapter.java +++ b/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; } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 0251d16d42..bd8c9bee40 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/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 metadataPair = EncryptionUtils.retrieveMetadata(folder, - client, - privateKey, - publicKey, - arbitraryDataProvider, - user); + OCCapability ocCapability = mContainerActivity.getStorageManager().getCapability(user.getAccountName()); - boolean metadataExists = metadataPair.first; - DecryptedFolderMetadata metadata = metadataPair.second; + if (ocCapability.getEndToEndEncryptionApiVersion() == E2EVersion.V2_0) { + // Update metadata + Pair metadataPair = EncryptionUtils.retrieveMetadata(folder, + client, + privateKeyString, + publicKeyString, + storageManager, + user, + requireContext(), + arbitraryDataProvider); - EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata, - publicKey, - arbitraryDataProvider, - user, - folder.getLocalId()); + boolean metadataExists = metadataPair.first; + DecryptedFolderMetadataFile metadata = metadataPair.second; - String serializedFolderMetadata; + new EncryptionUtilsV2().serializeAndUploadMetadata(folder, + metadata, + token, + client, + metadataExists, + requireContext(), + user, + storageManager); - // check if we need metadataKeys - if (metadata.getMetadata().getMetadataKey() != null) { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true); - } else { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); + // 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); } } diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index a6c89b8c73..5eee1f1353 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/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); diff --git a/app/src/main/java/com/owncloud/android/utils/CsrHelper.java b/app/src/main/java/com/owncloud/android/utils/CsrHelper.java deleted file mode 100644 index 438a3d0875..0000000000 --- a/app/src/main/java/com/owncloud/android/utils/CsrHelper.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java index 988b4db277..405668f84d 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtils.java @@ -34,22 +34,38 @@ import com.nextcloud.client.account.User; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.DecryptedFolderMetadata; import com.owncloud.android.datamodel.EncryptedFiledrop; -import com.owncloud.android.datamodel.EncryptedFolderMetadata; +import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile; +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFolderMetadataFileV1; +import com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedMetadata; +import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile; +import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1; +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile; +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser; +import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedFolderMetadataFile; +import com.owncloud.android.datamodel.e2e.v2.encrypted.EncryptedMetadata; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.e2ee.GetMetadataRemoteOperation; import com.owncloud.android.lib.resources.e2ee.LockFileRemoteOperation; +import com.owncloud.android.lib.resources.e2ee.MetadataResponse; import com.owncloud.android.lib.resources.e2ee.StoreMetadataRemoteOperation; +import com.owncloud.android.lib.resources.e2ee.StoreMetadataV2RemoteOperation; import com.owncloud.android.lib.resources.e2ee.UnlockFileRemoteOperation; +import com.owncloud.android.lib.resources.e2ee.UnlockFileV1RemoteOperation; import com.owncloud.android.lib.resources.e2ee.UpdateMetadataRemoteOperation; +import com.owncloud.android.lib.resources.e2ee.UpdateMetadataV2RemoteOperation; +import com.owncloud.android.lib.resources.files.model.ServerFileInterface; +import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.NextcloudVersion; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.lib.resources.status.Problem; import com.owncloud.android.lib.resources.status.SendClientDiagnosticRemoteOperation; import com.owncloud.android.operations.UploadException; +import com.owncloud.android.utils.theme.CapabilityUtils; import org.apache.commons.httpclient.HttpStatus; @@ -87,6 +103,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -123,8 +140,8 @@ public final class EncryptionUtils { private static final int keyStrength = 256; private static final String AES_CIPHER = "AES/GCM/NoPadding"; private static final String AES = "AES"; - private static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; - private static final String RSA = "RSA"; + public static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; + public static final String RSA = "RSA"; @VisibleForTesting public static final String MIGRATED_FOLDER_IDS = "MIGRATED_FOLDER_IDS"; @@ -150,9 +167,16 @@ public final class EncryptionUtils { public static String serializeJSON(Object data, boolean excludeTransient) { if (excludeTransient) { - return new Gson().toJson(data); + return new GsonBuilder() + .disableHtmlEscaping() + .create() + .toJson(data); } else { - return new GsonBuilder().excludeFieldsWithModifiers(0).create().toJson(data); + return new GsonBuilder() + .disableHtmlEscaping() + .excludeFieldsWithModifiers(0) + .create() + .toJson(data); } } @@ -165,27 +189,28 @@ public final class EncryptionUtils { */ /** - * Encrypt folder metaData + * Encrypt folder metaData V1 * * @param decryptedFolderMetadata folder metaData to encrypt - * @return EncryptedFolderMetadata encrypted folder metadata + * @return EncryptedFolderMetadataFile encrypted folder metadata */ - public static EncryptedFolderMetadata encryptFolderMetadata(DecryptedFolderMetadata decryptedFolderMetadata, - String publicKey, - ArbitraryDataProvider arbitraryDataProvider, - User user, - long parentId - ) + public static EncryptedFolderMetadataFileV1 encryptFolderMetadata( + DecryptedFolderMetadataFileV1 decryptedFolderMetadata, + String publicKey, + long parentId, + User user, + ArbitraryDataProvider arbitraryDataProvider + ) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException, - IllegalBlockSizeException, InvalidKeySpecException, CertificateException { + IllegalBlockSizeException, CertificateException { - HashMap files = new HashMap<>(); + HashMap files = new HashMap<>(); HashMap filesdrop = new HashMap<>(); - EncryptedFolderMetadata encryptedFolderMetadata = new EncryptedFolderMetadata(decryptedFolderMetadata - .getMetadata(), - files, - filesdrop); + EncryptedFolderMetadataFileV1 encryptedFolderMetadata = new EncryptedFolderMetadataFileV1(decryptedFolderMetadata + .getMetadata(), + files, + filesdrop); // set new metadata key byte[] metadataKeyBytes = EncryptionUtils.generateKey(); @@ -198,24 +223,24 @@ public final class EncryptionUtils { addIdToMigratedIds(parentId, user, arbitraryDataProvider); // Encrypt each file in "files" - for (Map.Entry entry : decryptedFolderMetadata + for (Map.Entry entry : decryptedFolderMetadata .getFiles().entrySet()) { String key = entry.getKey(); - DecryptedFolderMetadata.DecryptedFile decryptedFile = entry.getValue(); + DecryptedFile decryptedFile = entry.getValue(); - EncryptedFolderMetadata.EncryptedFile encryptedFile = new EncryptedFolderMetadata.EncryptedFile(); + EncryptedFolderMetadataFileV1.EncryptedFile encryptedFile = new EncryptedFolderMetadataFileV1.EncryptedFile(); encryptedFile.setInitializationVector(decryptedFile.getInitializationVector()); encryptedFile.setAuthenticationTag(decryptedFile.getAuthenticationTag()); // encrypt String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted()); - encryptedFile.setEncrypted(EncryptionUtils.encryptStringSymmetric(dataJson, metadataKeyBytes)); + encryptedFile.setEncrypted(EncryptionUtils.encryptStringSymmetricAsString(dataJson, metadataKeyBytes)); files.put(key, encryptedFile); } // set checksum - String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC); + String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim(); String checksum = EncryptionUtils.generateChecksum(decryptedFolderMetadata, mnemonic); encryptedFolderMetadata.getMetadata().setChecksum(checksum); @@ -226,14 +251,16 @@ public final class EncryptionUtils { * normally done on server only internal test */ @VisibleForTesting - public static void encryptFileDropFiles(DecryptedFolderMetadata decryptedFolderMetadata, - EncryptedFolderMetadata encryptedFolderMetadata, - String cert) throws NoSuchPaddingException, IllegalBlockSizeException, CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { + public static void encryptFileDropFiles(DecryptedFolderMetadataFileV1 decryptedFolderMetadata, + EncryptedFolderMetadataFileV1 encryptedFolderMetadata, + String cert) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, BadPaddingException, IllegalBlockSizeException, CertificateException, + InvalidAlgorithmParameterException { final Map filesdrop = encryptedFolderMetadata.getFiledrop(); - for (Map.Entry entry : decryptedFolderMetadata + for (Map.Entry entry : decryptedFolderMetadata .getFiledrop().entrySet()) { String key = entry.getKey(); - DecryptedFolderMetadata.DecryptedFile decryptedFile = entry.getValue(); + DecryptedFile decryptedFile = entry.getValue(); byte[] byt = generateKey(); String metadataKey0 = encodeBytesToBase64String(byt); @@ -241,7 +268,7 @@ public final class EncryptionUtils { String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted()); - String encJson = encryptStringSymmetric(dataJson, byt); + String encJson = encryptStringSymmetricAsString(dataJson, byt); int delimiterPosition = encJson.lastIndexOf(ivDelimiter); String encryptedInitializationVector = encJson.substring(delimiterPosition + ivDelimiter.length()); @@ -269,19 +296,19 @@ public final class EncryptionUtils { } /* - * decrypt folder metaData with private key + * decrypt folder metaData V1 with private key */ - public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetadata encryptedFolderMetadata, - String privateKey, - ArbitraryDataProvider arbitraryDataProvider, - User user, - long remoteId) + public static DecryptedFolderMetadataFileV1 decryptFolderMetaData(EncryptedFolderMetadataFileV1 encryptedFolderMetadata, + String privateKey, + ArbitraryDataProvider arbitraryDataProvider, + User user, + long remoteId) throws NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException { - HashMap files = new HashMap<>(); - DecryptedFolderMetadata decryptedFolderMetadata = new DecryptedFolderMetadata( + HashMap files = new HashMap<>(); + DecryptedFolderMetadataFileV1 decryptedFolderMetadata = new DecryptedFolderMetadataFileV1( encryptedFolderMetadata.getMetadata(), files); byte[] decryptedMetadataKey = null; @@ -294,12 +321,12 @@ public final class EncryptionUtils { } if (encryptedFolderMetadata.getFiles() != null) { - for (Map.Entry entry : encryptedFolderMetadata + for (Map.Entry entry : encryptedFolderMetadata .getFiles().entrySet()) { String key = entry.getKey(); - EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue(); + EncryptedFolderMetadataFileV1.EncryptedFile encryptedFile = entry.getValue(); - DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile(); + DecryptedFile decryptedFile = new DecryptedFile(); decryptedFile.setInitializationVector(encryptedFile.getInitializationVector()); decryptedFile.setMetadataKey(encryptedFile.getMetadataKey()); decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag()); @@ -314,7 +341,7 @@ public final class EncryptionUtils { // decrypt String dataJson = EncryptionUtils.decryptStringSymmetric(encryptedFile.getEncrypted(), decryptedMetadataKey); decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(dataJson, - new TypeToken() { + new TypeToken<>() { })); files.put(key, decryptedFile); @@ -322,7 +349,7 @@ public final class EncryptionUtils { } // verify checksum - String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC); + String mnemonic = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.MNEMONIC).trim(); String checksum = EncryptionUtils.generateChecksum(decryptedFolderMetadata, mnemonic); String decryptedFolderChecksum = decryptedFolderMetadata.getMetadata().getChecksum(); @@ -349,22 +376,22 @@ public final class EncryptionUtils { privateKey); // decrypt encrypted blob with key - String decryptedData = decryptStringSymmetric( + String decryptedData = decryptStringSymmetricAsString( encryptedFile.getEncrypted(), decodeStringToBase64Bytes(encryptedKey), decodeStringToBase64Bytes(encryptedFile.getEncryptedInitializationVector()), decodeStringToBase64Bytes(encryptedFile.getEncryptedTag()), arbitraryDataProvider, user - ); + ); - DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile(); + DecryptedFile decryptedFile = new DecryptedFile(); decryptedFile.setInitializationVector(encryptedFile.getInitializationVector()); decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag()); decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(decryptedData, - new TypeToken() { + new TypeToken<>() { })); files.put(key, decryptedFile); @@ -378,89 +405,119 @@ public final class EncryptionUtils { } /** - * Download metadata for folder and decrypt it + * Download metadata (v1 or v2) for folder and decrypt it * - * @return decrypted metadata or null + * @return decrypted v2 metadata or null */ + @SuppressFBWarnings("URV") public static @Nullable - DecryptedFolderMetadata downloadFolderMetadata(OCFile folder, - OwnCloudClient client, - Context context, - User user) { - RemoteOperationResult getMetadataOperationResult = new GetMetadataRemoteOperation(folder.getLocalId()) + Object + downloadFolderMetadata(OCFile folder, + OwnCloudClient client, + Context context, + User user + ) { + RemoteOperationResult getMetadataOperationResult = new GetMetadataRemoteOperation(folder.getLocalId()) .execute(client); if (!getMetadataOperationResult.isSuccess()) { return null; } + OCCapability capability = CapabilityUtils.getCapability(context); + // decrypt metadata - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); - String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0); - String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY); - String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); + EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); + String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata(); - EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON( - serializedEncryptedMetadata, new TypeToken() { - }); + E2EVersion version = determinateVersion(serializedEncryptedMetadata); - try { - int filesDropCountBefore = 0; - if (encryptedFolderMetadata.getFiledrop() != null) { - filesDropCountBefore = encryptedFolderMetadata.getFiledrop().size(); - } - DecryptedFolderMetadata decryptedFolderMetadata = EncryptionUtils.decryptFolderMetaData( - encryptedFolderMetadata, - privateKey, - arbitraryDataProvider, - user, - folder.getLocalId()); + switch (version) { + case UNKNOWN: + Log_OC.e(TAG, "Unknown e2e state"); + return null; - boolean transferredFiledrop = filesDropCountBefore > 0 && decryptedFolderMetadata.getFiles().size() == - encryptedFolderMetadata.getFiles().size() + filesDropCountBefore; + case V1_0, V1_1, V1_2: + ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); + String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY); + String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); + EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.deserializeJSON( + serializedEncryptedMetadata, new TypeToken<>() { + }); - if (transferredFiledrop) { - // lock folder - String token = EncryptionUtils.lockFolder(folder, client); - - // upload metadata - EncryptedFolderMetadata encryptedFolderMetadataNew = encryptFolderMetadata(decryptedFolderMetadata, - publicKey, - arbitraryDataProvider, - user, - folder.getLocalId()); - - String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadataNew); - - EncryptionUtils.uploadMetadata(folder, - serializedFolderMetadata, - token, - client, - true, - arbitraryDataProvider, - user); - - // unlock folder - RemoteOperationResult unlockFolderResult = EncryptionUtils.unlockFolder(folder, client, token); - - if (!unlockFolderResult.isSuccess()) { - Log_OC.e(TAG, unlockFolderResult.getMessage()); + try { + DecryptedFolderMetadataFileV1 v1 = decryptFolderMetaData(encryptedFolderMetadata, + privateKey, + arbitraryDataProvider, + user, + folder.getLocalId()); + if (capability.getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { + new EncryptionUtilsV2().migrateV1ToV2andUpload( + v1, + client.getUserId(), + publicKey, + folder, + new FileDataStorageManager(user, context.getContentResolver()), + client, + user, + context + ); + } else { + return v1; + } + } catch (Exception e) { + // TODO do not crash, but show meaningful error + Log_OC.e(TAG, "Could not decrypt metadata for " + folder.getDecryptedFileName(), e); return null; } - } - return decryptedFolderMetadata; - } catch (Exception e) { - Log_OC.e(TAG, e.getMessage()); - return null; + case V2_0: + return encryptionUtilsV2.parseAnyMetadata(getMetadataOperationResult.getResultData(), + user, + client, + context, + folder); } + return null; + } + + public static E2EVersion determinateVersion(String metadata) { + try { + EncryptedFolderMetadataFileV1 v1 = EncryptionUtils.deserializeJSON( + metadata, + new TypeToken<>() { + }); + + double version = v1.getMetadata().getVersion(); + + if (version == 1.0) { + return E2EVersion.V1_0; + } else if (version == 1.1) { + return E2EVersion.V1_1; + } else if (version == 1.2) { + return E2EVersion.V1_2; + } else { + throw new IllegalStateException("Unknown version"); + } + } catch (Exception e) { + EncryptedFolderMetadataFile v2 = EncryptionUtils.deserializeJSON( + metadata, + new TypeToken<>() { + }); + + if ("2.0".equals(v2.getVersion()) || "2".equals(v2.getVersion())) { + return E2EVersion.V2_0; + } + } + + return E2EVersion.UNKNOWN; } /* BASE 64 */ - + @SuppressFBWarnings({"DM", "MDM"}) public static byte[] encodeStringToBase64Bytes(String string) { try { return Base64.encode(string.getBytes(), Base64.NO_WRAP); @@ -469,6 +526,7 @@ public final class EncryptionUtils { } } + @SuppressFBWarnings({"DM", "MDM"}) public static String decodeBase64BytesToString(byte[] bytes) { try { return new String(Base64.decode(bytes, Base64.NO_WRAP)); @@ -477,10 +535,21 @@ public final class EncryptionUtils { } } + @SuppressFBWarnings({"DM", "MDM"}) public static String encodeBytesToBase64String(byte[] bytes) { return Base64.encodeToString(bytes, Base64.NO_WRAP); } + @SuppressFBWarnings({"DM", "MDM"}) + public static String encodeStringToBase64String(String string) { + return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP); + } + + @SuppressFBWarnings({"DM", "MDM"}) + public static String decodeBase64StringToString(String string) { + return new String(Base64.decode(string, Base64.NO_WRAP)); + } + public static byte[] decodeStringToBase64Bytes(String string) { return Base64.decode(string, Base64.NO_WRAP); } @@ -530,7 +599,8 @@ public final class EncryptionUtils { byte[] cryptedBytes = cipher.doFinal(fileBytes); String authenticationTag = encodeBytesToBase64String(Arrays.copyOfRange(cryptedBytes, - cryptedBytes.length - (128 / 8), cryptedBytes.length)); + cryptedBytes.length - (128 / 8), + cryptedBytes.length)); return new EncryptedFile(cryptedBytes, authenticationTag); } @@ -564,7 +634,8 @@ public final class EncryptionUtils { // check authentication tag byte[] extractedAuthenticationTag = Arrays.copyOfRange(fileBytes, - fileBytes.length - (128 / 8), fileBytes.length); + fileBytes.length - (128 / 8), + fileBytes.length); if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { reportE2eError(arbitraryDataProvider, user); @@ -574,16 +645,6 @@ public final class EncryptionUtils { return cipher.doFinal(fileBytes); } - public static class EncryptedFile { - public byte[] encryptedBytes; - public String authenticationTag; - - public EncryptedFile(byte[] encryptedBytes, String authenticationTag) { - this.encryptedBytes = encryptedBytes; - this.authenticationTag = authenticationTag; - } - } - /** * Encrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding Asymmetric encryption, with private * and public key @@ -618,6 +679,31 @@ public final class EncryptionUtils { return encodeBytesToBase64String(cryptedBytes); } + public static String encryptStringAsymmetricV2(byte[] bytes, String cert) + throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, + BadPaddingException, IllegalBlockSizeException, + CertificateException { + + Cipher cipher = Cipher.getInstance(RSA_CIPHER); + + String trimmedCert = cert.replace("-----BEGIN CERTIFICATE-----\n", "") + .replace("-----END CERTIFICATE-----\n", ""); + byte[] encodedCert = trimmedCert.getBytes(StandardCharsets.UTF_8); + byte[] decodedCert = org.apache.commons.codec.binary.Base64.decodeBase64(encodedCert); + + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + InputStream in = new ByteArrayInputStream(decodedCert); + X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(in); + PublicKey realPublicKey = certificate.getPublicKey(); + + cipher.init(Cipher.ENCRYPT_MODE, realPublicKey); + + byte[] cryptedBytes = cipher.doFinal(bytes); + + return encodeBytesToBase64String(cryptedBytes); + } + public static String encryptStringAsymmetric(String string, PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { Cipher cipher = Cipher.getInstance(RSA_CIPHER); @@ -659,6 +745,59 @@ public final class EncryptionUtils { return decodeBase64BytesToString(encodedBytes); } + public static byte[] decryptStringAsymmetricAsBytes(String string, String privateKeyString) + throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, + BadPaddingException, IllegalBlockSizeException, + InvalidKeySpecException { + + Cipher cipher = Cipher.getInstance(RSA_CIPHER); + + byte[] privateKeyBytes = decodeStringToBase64Bytes(privateKeyString); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory kf = KeyFactory.getInstance(RSA); + PrivateKey privateKey = kf.generatePrivate(keySpec); + + cipher.init(Cipher.DECRYPT_MODE, privateKey); + + byte[] bytes = decodeStringToBase64Bytes(string); + + return cipher.doFinal(bytes); + } + + public static byte[] decryptStringAsymmetricV2(String string, String privateKeyString) + throws NoSuchAlgorithmException, + NoSuchPaddingException, InvalidKeyException, + BadPaddingException, IllegalBlockSizeException, + InvalidKeySpecException { + + Cipher cipher = Cipher.getInstance(RSA_CIPHER); + + byte[] privateKeyBytes = decodeStringToBase64Bytes(privateKeyString); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory kf = KeyFactory.getInstance(RSA); + PrivateKey privateKey = kf.generatePrivate(keySpec); + + cipher.init(Cipher.DECRYPT_MODE, privateKey); + + byte[] bytes; + try { + bytes = decodeStringToBase64Bytes(string); + } catch (Exception e) { + bytes = encodeStringToBase64Bytes(string); + } + + return cipher.doFinal(bytes); + } + + /** + * Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding Asymmetric encryption, with private + * and public key + * + * @param string string to decrypt + * @param privateKey private key + * @return decrypted string + */ public static String decryptStringAsymmetric(String string, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { Cipher cipher = Cipher.getInstance(RSA_CIPHER); cipher.init(Cipher.DECRYPT_MODE, privateKey); @@ -670,37 +809,170 @@ public final class EncryptionUtils { } /** - * Encrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding Asymmetric encryption, with private - * and public key + * Decrypt string with AES/GCM/NoPadding * - * @param string String to encrypt - * @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()} - * @return encrypted string + * @param string string to decrypt + * @param encryptionKeyBytes key from metadata + * @return decrypted string */ - public static String encryptStringSymmetric(String string, byte[] encryptionKeyBytes) + public static String encryptStringSymmetricAsString(String string, byte[] encryptionKeyBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - return encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiter); + EncryptedMetadata metadata = encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiter); + + return metadata.getCiphertext(); } @VisibleForTesting - public static String encryptStringSymmetricOld(String string, byte[] encryptionKeyBytes) + public static String encryptStringSymmetricAsStringOld(String string, byte[] encryptionKeyBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { - return encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiterOld); + EncryptedMetadata metadata = encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiterOld); + + return metadata.getCiphertext(); } - private static String encryptStringSymmetric(String string, - byte[] encryptionKeyBytes, - String delimiter) +// /** +// * Encrypt string with AES/GCM/NoPadding +// * +// * @param string string to encrypt +// * @param encryptionKeyBytes key from metadata +// * @return decrypted string +// */ +// private static String encryptStringSymmetric(String string, +// byte[] encryptionKeyBytes, +// String delimiter) +// throws NoSuchAlgorithmException, +// InvalidAlgorithmParameterException, +// NoSuchPaddingException, +// InvalidKeyException, +// BadPaddingException, +// IllegalBlockSizeException { +// +// Cipher cipher = Cipher.getInstance(AES_CIPHER); +// byte[] iv = randomBytes(ivLength); +// +// Key key = new SecretKeySpec(encryptionKeyBytes, AES); +// GCMParameterSpec spec = new GCMParameterSpec(128, iv); +// cipher.init(Cipher.ENCRYPT_MODE, key, spec); +// +// byte[] bytes = encodeStringToBase64Bytes(string); +// byte[] cryptedBytes = cipher.doFinal(bytes); +// +// String encodedCryptedBytes = encodeBytesToBase64String(cryptedBytes); +// String encodedIV = encodeBytesToBase64String(iv); +// +// return encodedCryptedBytes + delimiter + encodedIV; +// } +public static String decryptStringSymmetricAsString(String string, + byte[] encryptionKeyBytes, + byte[] iv, + byte[] authenticationTag, + ArbitraryDataProvider arbitraryDataProvider, + User user + ) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + return decryptStringSymmetricAsString( + decodeStringToBase64Bytes(string), + encryptionKeyBytes, + iv, + authenticationTag, + false, + arbitraryDataProvider, + user); +} + + public static String decryptStringSymmetricAsString(String string, + byte[] encryptionKeyBytes, + byte[] iv, + byte[] authenticationTag, + boolean fileDropV2, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + return decryptStringSymmetricAsString( + decodeStringToBase64Bytes(string), + encryptionKeyBytes, + iv, + authenticationTag, + fileDropV2, + arbitraryDataProvider, + user); + } + + public static String decryptStringSymmetricAsString(byte[] bytes, + byte[] encryptionKeyBytes, + byte[] iv, + byte[] authenticationTag, + boolean fileDropV2, + ArbitraryDataProvider arbitraryDataProvider, + User user) + throws NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidAlgorithmParameterException, + InvalidKeyException, + IllegalBlockSizeException, + BadPaddingException { + Cipher cipher = Cipher.getInstance(AES_CIPHER); + Key key = new SecretKeySpec(encryptionKeyBytes, AES); + GCMParameterSpec spec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + + + // check authentication tag + byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes, + bytes.length - (128 / 8), + bytes.length); + + if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { + reportE2eError(arbitraryDataProvider, user); + throw new SecurityException("Tag not correct"); + } + + byte[] encodedBytes = cipher.doFinal(bytes); + + if (fileDropV2) { + return new EncryptionUtilsV2().gZipDecompress(encodedBytes); + } else { + return decodeBase64BytesToString(encodedBytes); + } + } + + public static EncryptedMetadata encryptStringSymmetric( + String string, + byte[] encryptionKeyBytes) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + return encryptStringSymmetric(string, encryptionKeyBytes, ivDelimiter); + } + + + public static EncryptedMetadata encryptStringSymmetric( + String string, + byte[] encryptionKeyBytes, + String delimiter) throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + byte[] bytes = encodeStringToBase64Bytes(string); + + return encryptStringSymmetric(bytes, encryptionKeyBytes, delimiter); + } + + /** + * Encrypt string with AES/GCM/NoPadding + * + * @param bytes byte array + * @param encryptionKeyBytes key from metadata + * @return decrypted string + */ + public static EncryptedMetadata encryptStringSymmetric( + byte[] bytes, + byte[] encryptionKeyBytes, + String delimiter) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, @@ -715,47 +987,15 @@ public final class EncryptionUtils { GCMParameterSpec spec = new GCMParameterSpec(128, iv); cipher.init(Cipher.ENCRYPT_MODE, key, spec); - byte[] bytes = encodeStringToBase64Bytes(string); byte[] cryptedBytes = cipher.doFinal(bytes); String encodedCryptedBytes = encodeBytesToBase64String(cryptedBytes); String encodedIV = encodeBytesToBase64String(iv); + String authenticationTag = encodeBytesToBase64String(Arrays.copyOfRange(cryptedBytes, + cryptedBytes.length - (128 / 8), + cryptedBytes.length)); - return encodedCryptedBytes + delimiter + encodedIV; - } - - public static String decryptStringSymmetric(String string, - byte[] encryptionKeyBytes, - byte[] iv, - byte[] authenticationTag, - ArbitraryDataProvider arbitraryDataProvider, - User user) - throws NoSuchPaddingException, - NoSuchAlgorithmException, - InvalidAlgorithmParameterException, - InvalidKeyException, - IllegalBlockSizeException, - BadPaddingException { - Cipher cipher = Cipher.getInstance(AES_CIPHER); - Key key = new SecretKeySpec(encryptionKeyBytes, AES); - GCMParameterSpec spec = new GCMParameterSpec(128, iv); - cipher.init(Cipher.DECRYPT_MODE, key, spec); - - byte[] bytes = decodeStringToBase64Bytes(string); - - // check authentication tag - byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes, - bytes.length - (128 / 8), - bytes.length); - - if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) { - reportE2eError(arbitraryDataProvider, user); - throw new SecurityException("Tag not correct"); - } - - byte[] encodedBytes = cipher.doFinal(bytes); - - return decodeBase64BytesToString(encodedBytes); + return new EncryptedMetadata(encodedCryptedBytes + delimiter + encodedIV, encodedIV, authenticationTag); } /** @@ -799,6 +1039,57 @@ public final class EncryptionUtils { return decodeBase64BytesToString(encodedBytes); } + /** + * Decrypt string with AES/GCM/NoPadding + * + * @param string string to decrypt + * @param encryptionKeyBytes key from metadata + * @param authenticationTag auth tag to check + * @return decrypted string + */ + public static byte[] decryptStringSymmetric(String string, + byte[] encryptionKeyBytes, + String authenticationTag, + String ivString) + throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException, + BadPaddingException, IllegalBlockSizeException { + + Cipher cipher = Cipher.getInstance(AES_CIPHER); + + int delimiterPosition = string.lastIndexOf(ivDelimiter); + + String cipherString; + if (delimiterPosition == -1) { + cipherString = string; + } else { + cipherString = string.substring(0, delimiterPosition); + } + + byte[] iv = new IvParameterSpec(decodeStringToBase64Bytes(ivString)).getIV(); + + Key key = new SecretKeySpec(encryptionKeyBytes, AES); + + GCMParameterSpec spec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.DECRYPT_MODE, key, spec); + + byte[] bytes = decodeStringToBase64Bytes(cipherString); + + // check authentication tag + if (authenticationTag != null) { + byte[] authenticationTagBytes = decodeStringToBase64Bytes(authenticationTag); + byte[] extractedAuthenticationTag = Arrays.copyOfRange(bytes, + bytes.length - (128 / 8), + bytes.length); + + if (!Arrays.equals(extractedAuthenticationTag, authenticationTagBytes)) { + throw new SecurityException("Tag not correct"); + } + } + + return cipher.doFinal(bytes); + } + /** * Encrypt private key with symmetric AES encryption, GCM mode mode and no padding * @@ -905,6 +1196,13 @@ public final class EncryptionUtils { return "-----BEGIN PRIVATE KEY-----\n" + privateKeyString.replaceAll("(.{65})", "$1\n") + "\n-----END PRIVATE KEY-----"; } + + public static PrivateKey PEMtoPrivateKey(String pem) throws NoSuchAlgorithmException, InvalidKeySpecException { + byte[] privateKeyBytes = EncryptionUtils.decodeStringToBase64Bytes(pem); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory kf = KeyFactory.getInstance(EncryptionUtils.RSA); + return kf.generatePrivate(keySpec); + } /* Helper @@ -934,12 +1232,22 @@ public final class EncryptionUtils { return outputLines; } + /** + * Generates private/public key pair, used for asymmetric encryption + * + * @return KeyPair + */ public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA); keyGen.initialize(2048, new SecureRandom()); return keyGen.generateKeyPair(); } + /** + * Generates key for symmetric encryption + * + * @return byte[] byteArray of key + */ public static byte[] generateKey() { KeyGenerator keyGenerator; try { @@ -954,6 +1262,15 @@ public final class EncryptionUtils { return null; } + /** + * Generates key for symmetric encryption + * + * @return String String base64 encoded key + */ + public static String generateKeyString() { + return EncryptionUtils.encodeBytesToBase64String(generateKey()); + } + public static byte[] randomBytes(int size) { SecureRandom random = new SecureRandom(); final byte[] iv = new byte[size]; @@ -1013,14 +1330,19 @@ public final class EncryptionUtils { return hashWithSalt.equals(newHash); } - public static String lockFolder(OCFile parentFile, OwnCloudClient client) throws UploadException { + public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient client) throws UploadException { + return lockFolder(parentFile, client, -1); + } + + public static String lockFolder(ServerFileInterface parentFile, OwnCloudClient client, long counter) throws UploadException { // Lock folder - LockFileRemoteOperation lockFileOperation = new LockFileRemoteOperation(parentFile.getLocalId()); - RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client); + LockFileRemoteOperation lockFileOperation = new LockFileRemoteOperation(parentFile.getLocalId(), + counter); + RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client); if (lockFileOperationResult.isSuccess() && - !TextUtils.isEmpty((String) lockFileOperationResult.getData().get(0))) { - return (String) lockFileOperationResult.getData().get(0); + !TextUtils.isEmpty(lockFileOperationResult.getResultData())) { + return lockFileOperationResult.getResultData(); } else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) { throw new UploadException("Forbidden! Please try again later.)"); } else { @@ -1032,29 +1354,29 @@ public final class EncryptionUtils { * @param parentFile file metadata should be retrieved for * @return Pair: boolean: true: metadata already exists, false: metadata new created */ - public static Pair retrieveMetadata(OCFile parentFile, - OwnCloudClient client, - String privateKey, - String publicKey, - ArbitraryDataProvider arbitraryDataProvider, - User user) + public static Pair retrieveMetadataV1(OCFile parentFile, + OwnCloudClient client, + String privateKey, + String publicKey, + ArbitraryDataProvider arbitraryDataProvider, + User user) throws UploadException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException, InvalidKeySpecException, CertificateException { long localId = parentFile.getLocalId(); GetMetadataRemoteOperation getMetadataOperation = new GetMetadataRemoteOperation(localId); - RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client); + RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client); - DecryptedFolderMetadata metadata; + DecryptedFolderMetadataFileV1 metadata; if (getMetadataOperationResult.isSuccess()) { // decrypt metadata - String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0); + String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata(); - EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON( - serializedEncryptedMetadata, new TypeToken() { + EncryptedFolderMetadataFileV1 encryptedFolderMetadata = EncryptionUtils.deserializeJSON( + serializedEncryptedMetadata, new TypeToken<>() { }); return new Pair<>(Boolean.TRUE, decryptFolderMetaData(encryptedFolderMetadata, @@ -1064,14 +1386,84 @@ public final class EncryptionUtils { localId)); } else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) { + // TODO extract // new metadata - metadata = new DecryptedFolderMetadata(); - metadata.setMetadata(new DecryptedFolderMetadata.Metadata()); + metadata = new DecryptedFolderMetadataFileV1(); + metadata.setMetadata(new DecryptedMetadata()); metadata.getMetadata().setMetadataKeys(new HashMap<>()); String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey()); String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey); metadata.getMetadata().setMetadataKey(encryptedMetadataKey); + return new Pair<>(Boolean.FALSE, metadata); + } else { + // TODO E2E: error + throw new UploadException("something wrong"); + } + } + + /** + * @param parentFile file metadata should be retrieved for + * @return Pair: boolean: true: metadata already exists, false: metadata new created + */ + public static Pair retrieveMetadata(OCFile parentFile, + OwnCloudClient client, + String privateKey, + String publicKey, + FileDataStorageManager storageManager, + User user, + Context context, + ArbitraryDataProvider arbitraryDataProvider) + throws UploadException, Throwable, + InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException, + IllegalBlockSizeException, InvalidKeyException, InvalidKeySpecException, CertificateException { + long localId = parentFile.getLocalId(); + + GetMetadataRemoteOperation getMetadataOperation = new GetMetadataRemoteOperation(localId); + RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client); + + + DecryptedFolderMetadataFile metadata; + + if (getMetadataOperationResult.isSuccess()) { + // decrypt metadata + String serializedEncryptedMetadata = getMetadataOperationResult.getResultData().getMetadata(); + + + EncryptedFolderMetadataFile encryptedFolderMetadata = EncryptionUtils.deserializeJSON( + serializedEncryptedMetadata, new TypeToken<>() { + }); + + return new Pair<>(Boolean.TRUE, + new EncryptionUtilsV2().decryptFolderMetadataFile(encryptedFolderMetadata, + client.getUserId(), + privateKey, + parentFile, + storageManager, + client, + parentFile.getE2eCounter(), + getMetadataOperationResult.getResultData().getSignature(), + user, + context, + arbitraryDataProvider) + ); + + } else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) { + // new metadata + metadata = new DecryptedFolderMetadataFile(new com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedMetadata(), + new ArrayList<>(), + new HashMap<>(), + E2EVersion.V2_0.getValue()); + metadata.getUsers().add(new DecryptedUser(client.getUserId(), publicKey)); + byte[] metadataKey = EncryptionUtils.generateKey(); + + if (metadataKey == null) { + throw new UploadException("Could not encrypt folder!"); + } + + metadata.getMetadata().setMetadataKey(metadataKey); + metadata.getMetadata().getKeyChecksums().add(new EncryptionUtilsV2().hashMetadataKey(metadataKey)); + return new Pair<>(Boolean.FALSE, metadata); } else { reportE2eError(arbitraryDataProvider, user); @@ -1079,24 +1471,49 @@ public final class EncryptionUtils { } } - public static void uploadMetadata(OCFile parentFile, + public static void uploadMetadata(ServerFileInterface parentFile, String serializedFolderMetadata, String token, OwnCloudClient client, boolean metadataExists, + E2EVersion version, + String signature, ArbitraryDataProvider arbitraryDataProvider, User user) throws UploadException { - RemoteOperationResult uploadMetadataOperationResult; + RemoteOperationResult uploadMetadataOperationResult; if (metadataExists) { // update metadata - UpdateMetadataRemoteOperation storeMetadataOperation = new UpdateMetadataRemoteOperation( - parentFile.getLocalId(), serializedFolderMetadata, token); - uploadMetadataOperationResult = storeMetadataOperation.execute(client); + if (version == E2EVersion.V2_0) { + uploadMetadataOperationResult = new UpdateMetadataV2RemoteOperation( + parentFile.getRemoteId(), + serializedFolderMetadata, + token, + signature) + .execute(client); + } else { + uploadMetadataOperationResult = new UpdateMetadataRemoteOperation( + parentFile.getLocalId(), + serializedFolderMetadata, + token) + .execute(client); + } } else { // store metadata - StoreMetadataRemoteOperation storeMetadataOperation = new StoreMetadataRemoteOperation( - parentFile.getLocalId(), serializedFolderMetadata); - uploadMetadataOperationResult = storeMetadataOperation.execute(client); + if (version == E2EVersion.V2_0) { + uploadMetadataOperationResult = new StoreMetadataV2RemoteOperation( + parentFile.getRemoteId(), + serializedFolderMetadata, + token, + signature + ) + .execute(client); + } else { + uploadMetadataOperationResult = new StoreMetadataRemoteOperation( + parentFile.getLocalId(), + serializedFolderMetadata + ) + .execute(client); + } } if (!uploadMetadataOperationResult.isSuccess()) { @@ -1105,15 +1522,23 @@ public final class EncryptionUtils { } } - public static RemoteOperationResult unlockFolder(OCFile parentFolder, OwnCloudClient client, String token) { + public static RemoteOperationResult unlockFolder(ServerFileInterface parentFolder, OwnCloudClient client, String token) { if (token != null) { return new UnlockFileRemoteOperation(parentFolder.getLocalId(), token).execute(client); } else { - return new RemoteOperationResult(new Exception("No token available")); + return new RemoteOperationResult<>(new Exception("No token available")); } } - public static RSAPublicKey convertPublicKeyFromString(String string) throws CertificateException { + public static RemoteOperationResult unlockFolderV1(ServerFileInterface parentFolder, OwnCloudClient client, String token) { + if (token != null) { + return new UnlockFileV1RemoteOperation(parentFolder.getLocalId(), token).execute(client); + } else { + return new RemoteOperationResult<>(new Exception("No token available")); + } + } + + public static X509Certificate convertCertFromString(String string) throws CertificateException { String trimmedCert = string.replace("-----BEGIN CERTIFICATE-----\n", "") .replace("-----END CERTIFICATE-----\n", ""); byte[] encodedCert = trimmedCert.getBytes(StandardCharsets.UTF_8); @@ -1121,8 +1546,11 @@ public final class EncryptionUtils { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); InputStream in = new ByteArrayInputStream(decodedCert); - X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(in); - return (RSAPublicKey) certificate.getPublicKey(); + return (X509Certificate) certFactory.generateCertificate(in); + } + + public static RSAPublicKey convertPublicKeyFromString(String string) throws CertificateException { + return (RSAPublicKey) convertCertFromString(string).getPublicKey(); } public static void removeE2E(ArbitraryDataProvider arbitraryDataProvider, User user) { @@ -1149,19 +1577,19 @@ public final class EncryptionUtils { user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.nextcloud_26); } - public static String generateChecksum(DecryptedFolderMetadata metadata, + public static String generateChecksum(DecryptedFolderMetadataFileV1 metadataFile, String mnemonic) throws NoSuchAlgorithmException { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(mnemonic.replaceAll(" ", "")); - ArrayList keys = new ArrayList<>(metadata.getFiles().keySet()); + ArrayList keys = new ArrayList<>(metadataFile.getFiles().keySet()); Collections.sort(keys); for (String key : keys) { stringBuilder.append(key); } - stringBuilder.append(metadata.getMetadata().getMetadataKey()); + stringBuilder.append(metadataFile.getMetadata().getMetadataKey()); // sha256 hash-sum return sha256(stringBuilder.toString()); @@ -1260,4 +1688,39 @@ public final class EncryptionUtils { return null; } } + + public static String generateUid() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } + + public static String retrievePublicKeyForUser(User user, Context context) { + return new ArbitraryDataProviderImpl(context).getValue(user, PUBLIC_KEY); + } + + public static byte[] generateIV() { + return EncryptionUtils.randomBytes(EncryptionUtils.ivLength); + } + + public static String byteToHex(byte[] bytes) { + StringBuilder sbKey = new StringBuilder(); + for (byte b : bytes) { + sbKey.append(String.format("%02X ", b)); + } + return sbKey.toString(); + } + + public static void savePublicKey(User currentUser, + String key, + String user, + ArbitraryDataProvider arbitraryDataProvider) { + arbitraryDataProvider.storeOrUpdateKeyValue(currentUser, + ArbitraryDataProvider.PUBLIC_KEY + user, + key); + } + + public static String getPublicKey(User currentUser, + String user, + ArbitraryDataProvider arbitraryDataProvider) { + return arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PUBLIC_KEY + user); + } } diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt new file mode 100644 index 0000000000..550096e6b1 --- /dev/null +++ b/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 . + */ + +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.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 + 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() {} + ) + } + + @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() {} + ) + + 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 { + 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() {} + ) + + 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() {} + ) + + // 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() + + 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): 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): 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 + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f515a825c..3aaa421c18 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -510,6 +510,7 @@ Unset Name, Federated Cloud ID or email address … + Secure share … %1$s (group) %1$s (remote) @@ -1125,4 +1126,6 @@ Year Year/Month Year/Month/Day + Secure sharing is not set up for this user + Resharing is not allowed during secure file drop