From 2ec6e7d2ecfea51fe6f37d9bbde9b8eabdd6517f Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 15 Dec 2021 16:44:47 +0200 Subject: [PATCH 1/5] Added ability to forward detached atts(encrypted).| #1538 --- .../28.json | 1012 +++++++++++++++++ .../flowcrypt/email/database/MigrationTest.kt | 3 +- .../java/com/flowcrypt/email/Constants.kt | 2 +- .../flowcrypt/email/api/email/EmailUtil.kt | 5 +- .../email/api/email/model/AttachmentInfo.kt | 13 +- .../email/database/FlowCryptRoomDatabase.kt | 12 +- .../email/database/entity/AttachmentEntity.kt | 7 +- .../ForwardedAttachmentsDownloaderWorker.kt | 80 +- ...PrepareOutgoingMessagesJobIntentService.kt | 4 +- .../fragment/MessageDetailsFragment.kt | 16 +- .../adapter/AttachmentsRecyclerViewAdapter.kt | 2 +- 11 files changed, 1120 insertions(+), 36 deletions(-) create mode 100644 FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/28.json diff --git a/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/28.json b/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/28.json new file mode 100644 index 0000000000..996708af61 --- /dev/null +++ b/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/28.json @@ -0,0 +1,1012 @@ +{ + "formatVersion": 1, + "database": { + "version": 28, + "identityHash": "0fc7ed2b822179b5c5a60c9dab8549f3", + "entities": [ + { + "tableName": "accounts_aliases", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `account_type` TEXT NOT NULL, `send_as_email` TEXT NOT NULL, `display_name` TEXT DEFAULT NULL, `is_default` INTEGER DEFAULT 0, `verification_status` TEXT NOT NULL, FOREIGN KEY(`email`, `account_type`) REFERENCES `accounts`(`email`, `account_type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountType", + "columnName": "account_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sendAsEmail", + "columnName": "send_as_email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isDefault", + "columnName": "is_default", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "verificationStatus", + "columnName": "verification_status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "email_account_type_send_as_email_in_accounts_aliases", + "unique": true, + "columnNames": [ + "email", + "account_type", + "send_as_email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_account_type_send_as_email_in_accounts_aliases` ON `${TABLE_NAME}` (`email`, `account_type`, `send_as_email`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email", + "account_type" + ], + "referencedColumns": [ + "email", + "account_type" + ] + } + ] + }, + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `account_type` TEXT DEFAULT NULL, `display_name` TEXT DEFAULT NULL, `given_name` TEXT DEFAULT NULL, `family_name` TEXT DEFAULT NULL, `photo_url` TEXT DEFAULT NULL, `is_enabled` INTEGER DEFAULT 1, `is_active` INTEGER DEFAULT 0, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `imap_server` TEXT NOT NULL, `imap_port` INTEGER DEFAULT 143, `imap_use_ssl_tls` INTEGER DEFAULT 0, `imap_use_starttls` INTEGER DEFAULT 0, `imap_auth_mechanisms` TEXT, `smtp_server` TEXT NOT NULL, `smtp_port` INTEGER DEFAULT 25, `smtp_use_ssl_tls` INTEGER DEFAULT 0, `smtp_use_starttls` INTEGER DEFAULT 0, `smtp_auth_mechanisms` TEXT, `smtp_use_custom_sign` INTEGER DEFAULT 0, `smtp_username` TEXT DEFAULT NULL, `smtp_password` TEXT DEFAULT NULL, `contacts_loaded` INTEGER DEFAULT 0, `show_only_encrypted` INTEGER DEFAULT 0, `uuid` TEXT DEFAULT NULL, `client_configuration` TEXT DEFAULT NULL, `use_api` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountType", + "columnName": "account_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "displayName", + "columnName": "display_name", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "givenName", + "columnName": "given_name", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "photoUrl", + "columnName": "photo_url", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isEnabled", + "columnName": "is_enabled", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "1" + }, + { + "fieldPath": "isActive", + "columnName": "is_active", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imapServer", + "columnName": "imap_server", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "imapPort", + "columnName": "imap_port", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "143" + }, + { + "fieldPath": "imapUseSslTls", + "columnName": "imap_use_ssl_tls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "imapUseStarttls", + "columnName": "imap_use_starttls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "imapAuthMechanisms", + "columnName": "imap_auth_mechanisms", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "smtpServer", + "columnName": "smtp_server", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "smtpPort", + "columnName": "smtp_port", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "25" + }, + { + "fieldPath": "smtpUseSslTls", + "columnName": "smtp_use_ssl_tls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "smtpUseStarttls", + "columnName": "smtp_use_starttls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "smtpAuthMechanisms", + "columnName": "smtp_auth_mechanisms", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "smtpUseCustomSign", + "columnName": "smtp_use_custom_sign", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "smtpUsername", + "columnName": "smtp_username", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "smtpPassword", + "columnName": "smtp_password", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "contactsLoaded", + "columnName": "contacts_loaded", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "showOnlyEncrypted", + "columnName": "show_only_encrypted", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "clientConfiguration", + "columnName": "client_configuration", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "useAPI", + "columnName": "use_api", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "email_account_type_in_accounts", + "unique": true, + "columnNames": [ + "email", + "account_type" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_account_type_in_accounts` ON `${TABLE_NAME}` (`email`, `account_type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "action_queue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `action_type` TEXT NOT NULL, `action_json` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionType", + "columnName": "action_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionJson", + "columnName": "action_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `folder` TEXT NOT NULL, `uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `encodedSize` INTEGER DEFAULT 0, `type` TEXT NOT NULL, `attachment_id` TEXT, `file_uri` TEXT, `forwarded_folder` TEXT, `forwarded_uid` INTEGER DEFAULT -1, `decrypt_when_forward` INTEGER NOT NULL DEFAULT 0, `path` TEXT NOT NULL, FOREIGN KEY(`email`, `folder`, `uid`) REFERENCES `messages`(`email`, `folder`, `uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encodedSize", + "columnName": "encodedSize", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachmentId", + "columnName": "attachment_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileUri", + "columnName": "file_uri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forwardedFolder", + "columnName": "forwarded_folder", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "forwardedUid", + "columnName": "forwarded_uid", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "-1" + }, + { + "fieldPath": "decryptWhenForward", + "columnName": "decrypt_when_forward", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "email_uid_folder_path_in_attachment", + "unique": true, + "columnNames": [ + "email", + "uid", + "folder", + "path" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_uid_folder_path_in_attachment` ON `${TABLE_NAME}` (`email`, `uid`, `folder`, `path`)" + }, + { + "name": "email_folder_uid_in_attachment", + "unique": false, + "columnNames": [ + "email", + "folder", + "uid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `email_folder_uid_in_attachment` ON `${TABLE_NAME}` (`email`, `folder`, `uid`)" + } + ], + "foreignKeys": [ + { + "table": "messages", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email", + "folder", + "uid" + ], + "referencedColumns": [ + "email", + "folder", + "uid" + ] + } + ] + }, + { + "tableName": "recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `name` TEXT DEFAULT NULL, `last_use` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastUse", + "columnName": "last_use", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "name_in_recipients", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `name_in_recipients` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "last_use_in_recipients", + "unique": false, + "columnNames": [ + "last_use" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `last_use_in_recipients` ON `${TABLE_NAME}` (`last_use`)" + }, + { + "name": "email_in_recipients", + "unique": true, + "columnNames": [ + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_in_recipients` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "keys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `fingerprint` TEXT NOT NULL, `account` TEXT NOT NULL, `account_type` TEXT DEFAULT NULL, `source` TEXT NOT NULL, `public_key` BLOB NOT NULL, `private_key` BLOB NOT NULL, `passphrase` TEXT DEFAULT NULL, `passphrase_type` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account`, `account_type`) REFERENCES `accounts`(`email`, `account_type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountType", + "columnName": "account_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "storedPassphrase", + "columnName": "passphrase", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "passphraseType", + "columnName": "passphrase_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "fingerprint_account_account_type_in_keys", + "unique": true, + "columnNames": [ + "fingerprint", + "account", + "account_type" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `fingerprint_account_account_type_in_keys` ON `${TABLE_NAME}` (`fingerprint`, `account`, `account_type`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account", + "account_type" + ], + "referencedColumns": [ + "email", + "account_type" + ] + } + ] + }, + { + "tableName": "labels", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `account_type` TEXT DEFAULT NULL, `name` TEXT NOT NULL, `alias` TEXT DEFAULT NULL, `is_custom` INTEGER NOT NULL DEFAULT 0, `messages_total` INTEGER NOT NULL DEFAULT 0, `message_unread` INTEGER NOT NULL DEFAULT 0, `attributes` TEXT DEFAULT NULL, `next_page_token` TEXT DEFAULT NULL, `history_id` TEXT DEFAULT NULL, FOREIGN KEY(`email`, `account_type`) REFERENCES `accounts`(`email`, `account_type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountType", + "columnName": "account_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alias", + "columnName": "alias", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isCustom", + "columnName": "is_custom", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messagesTotal", + "columnName": "messages_total", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "messagesUnread", + "columnName": "message_unread", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "nextPageToken", + "columnName": "next_page_token", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "historyId", + "columnName": "history_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "email_account_type_name_in_labels", + "unique": true, + "columnNames": [ + "email", + "account_type", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_account_type_name_in_labels` ON `${TABLE_NAME}` (`email`, `account_type`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "accounts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "email", + "account_type" + ], + "referencedColumns": [ + "email", + "account_type" + ] + } + ] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `folder` TEXT NOT NULL, `uid` INTEGER NOT NULL, `received_date` INTEGER DEFAULT NULL, `sent_date` INTEGER DEFAULT NULL, `from_address` TEXT DEFAULT NULL, `to_address` TEXT DEFAULT NULL, `cc_address` TEXT DEFAULT NULL, `subject` TEXT DEFAULT NULL, `flags` TEXT DEFAULT NULL, `raw_message_without_attachments` TEXT DEFAULT NULL, `is_message_has_attachments` INTEGER DEFAULT 0, `is_encrypted` INTEGER DEFAULT -1, `is_new` INTEGER DEFAULT -1, `state` INTEGER DEFAULT -1, `attachments_directory` TEXT, `error_msg` TEXT DEFAULT NULL, `reply_to` TEXT DEFAULT NULL, `thread_id` TEXT DEFAULT NULL, `history_id` TEXT DEFAULT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedDate", + "columnName": "received_date", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "sentDate", + "columnName": "sent_date", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "fromAddress", + "columnName": "from_address", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "toAddress", + "columnName": "to_address", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "ccAddress", + "columnName": "cc_address", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "rawMessageWithoutAttachments", + "columnName": "raw_message_without_attachments", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "hasAttachments", + "columnName": "is_message_has_attachments", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "-1" + }, + { + "fieldPath": "isNew", + "columnName": "is_new", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "-1" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "-1" + }, + { + "fieldPath": "attachmentsDirectory", + "columnName": "attachments_directory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "errorMsg", + "columnName": "error_msg", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "replyTo", + "columnName": "reply_to", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "threadId", + "columnName": "thread_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "historyId", + "columnName": "history_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "email_in_messages", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `email_in_messages` ON `${TABLE_NAME}` (`email`)" + }, + { + "name": "email_uid_folder_in_messages", + "unique": true, + "columnNames": [ + "email", + "uid", + "folder" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_uid_folder_in_messages` ON `${TABLE_NAME}` (`email`, `uid`, `folder`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "public_keys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `recipient` TEXT NOT NULL, `fingerprint` TEXT NOT NULL, `public_key` BLOB NOT NULL, FOREIGN KEY(`recipient`) REFERENCES `recipients`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recipient", + "columnName": "recipient", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "recipient_fingerprint_in_public_keys", + "unique": true, + "columnNames": [ + "recipient", + "fingerprint" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `recipient_fingerprint_in_public_keys` ON `${TABLE_NAME}` (`recipient`, `fingerprint`)" + }, + { + "name": "recipient_in_public_keys", + "unique": false, + "columnNames": [ + "recipient" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `recipient_in_public_keys` ON `${TABLE_NAME}` (`recipient`)" + }, + { + "name": "fingerprint_in_public_keys", + "unique": false, + "columnNames": [ + "fingerprint" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `fingerprint_in_public_keys` ON `${TABLE_NAME}` (`fingerprint`)" + } + ], + "foreignKeys": [ + { + "table": "recipients", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "recipient" + ], + "referencedColumns": [ + "email" + ] + } + ] + } + ], + "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, '0fc7ed2b822179b5c5a60c9dab8549f3')" + ] + } +} \ No newline at end of file diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/database/MigrationTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/database/MigrationTest.kt index 660f5686b6..53dc1c07d9 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/database/MigrationTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/database/MigrationTest.kt @@ -37,7 +37,8 @@ class MigrationTest { FlowCryptRoomDatabase.MIGRATION_23_24, FlowCryptRoomDatabase.MIGRATION_24_25, FlowCryptRoomDatabase.MIGRATION_25_26, - FlowCryptRoomDatabase.MIGRATION_26_27 + FlowCryptRoomDatabase.MIGRATION_26_27, + FlowCryptRoomDatabase.MIGRATION_27_28 ) @get:Rule diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt index 8de46cc24c..690a29396f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt @@ -88,7 +88,7 @@ class Constants { const val PASSWORD_QUALITY_WEAK = "weak" const val PASSWORD_QUALITY_POOR = "poor" - const val PGP_FILE_EXT = ".pgp" + const val PGP_FILE_EXT = "pgp" val PASSWORD_WEAK_WORDS = arrayOf( "crypt", diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt index 186bca656b..f8820e34cf 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt @@ -677,14 +677,15 @@ class EmailUtil { ): Message { val session = Session.getInstance(Properties()) val senderEmail = outgoingMsgInfo.from - val senderPgpKeyDetailsList = - SecurityUtils.getSenderPgpKeyDetailsList(context, accountEntity, senderEmail) var pubKeys: List? = null var prvKeys: List? = null var ringProtector: SecretKeyRingProtector? = null if (outgoingMsgInfo.encryptionType === MessageEncryptionType.ENCRYPTED) { val recipients = outgoingMsgInfo.getAllRecipients().toMutableList() + val senderPgpKeyDetailsList = + SecurityUtils.getSenderPgpKeyDetailsList(context, accountEntity, senderEmail) + pubKeys = mutableListOf() pubKeys.addAll(SecurityUtils.getRecipientsUsablePubKeys(context, recipients)) pubKeys.addAll(senderPgpKeyDetailsList.map { it.publicKey }) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt index 937f976c61..a4427bfd0b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt @@ -10,6 +10,7 @@ import android.os.Parcel import android.os.Parcelable import com.flowcrypt.email.Constants import com.flowcrypt.email.security.SecurityUtils +import org.apache.commons.io.FilenameUtils /** * Simple POJO which defines information about email attachments. @@ -36,7 +37,8 @@ data class AttachmentInfo constructor( var isForwarded: Boolean = false, var isDecrypted: Boolean = false, var isEncryptionAllowed: Boolean = true, - var orderNumber: Int = 0 + var orderNumber: Int = 0, + var decryptWhenForward: Boolean = false, ) : Parcelable { val uniqueStringId: String @@ -69,7 +71,8 @@ data class AttachmentInfo constructor( source.readByte() != 0.toByte(), source.readByte() != 0.toByte(), source.readByte() != 0.toByte(), - source.readInt() + source.readInt(), + source.readByte() != 0.toByte(), ) override fun describeContents(): Int { @@ -95,6 +98,7 @@ data class AttachmentInfo constructor( writeByte(if (isDecrypted) 1.toByte() else 0.toByte()) writeByte(if (isEncryptionAllowed) 1.toByte() else 0.toByte()) writeInt(orderNumber) + writeByte(if (decryptWhenForward) 1.toByte() else 0.toByte()) } } @@ -156,6 +160,11 @@ data class AttachmentInfo constructor( fun isHidden() = name.isNullOrEmpty() && type.lowercase() == "application/pgp-encrypted; name=\"\"" + fun isEncrypted(): Boolean { + val fileExtension = FilenameUtils.getExtension(name) + return fileExtension.equals(Constants.PGP_FILE_EXT, true) + } + companion object { const val DEPTH_SEPARATOR = "/" const val INNER_ATTACHMENT_PREFIX = "inner_" diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt index 9bd842c3da..dafbe9968b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt @@ -90,7 +90,7 @@ abstract class FlowCryptRoomDatabase : RoomDatabase() { companion object { const val DB_NAME = "flowcrypt.db" - const val DB_VERSION = 27 + const val DB_VERSION = 28 private val MIGRATION_1_3 = object : FlowCryptMigration(1, 3) { override fun doMigration(database: SupportSQLiteDatabase) { @@ -545,6 +545,13 @@ abstract class FlowCryptRoomDatabase : RoomDatabase() { } } + @VisibleForTesting + val MIGRATION_27_28 = object : FlowCryptMigration(27, 28) { + override fun doMigration(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE attachment ADD COLUMN decrypt_when_forward INTEGER NOT NULL DEFAULT 0;") + } + } + // Singleton prevents multiple instances of database opening at the same time. @Volatile private var INSTANCE: FlowCryptRoomDatabase? = null @@ -585,7 +592,8 @@ abstract class FlowCryptRoomDatabase : RoomDatabase() { MIGRATION_23_24, MIGRATION_24_25, MIGRATION_25_26, - MIGRATION_26_27 + MIGRATION_26_27, + MIGRATION_27_28 ).build() INSTANCE = instance return instance diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt index 1db73de0e0..43df27f594 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt @@ -49,6 +49,7 @@ data class AttachmentEntity( @ColumnInfo(name = "file_uri") val fileUri: String?, @ColumnInfo(name = "forwarded_folder") val forwardedFolder: String?, @ColumnInfo(name = "forwarded_uid", defaultValue = "-1") val forwardedUid: Long?, + @ColumnInfo(name = "decrypt_when_forward", defaultValue = "0") val decryptWhenForward: Boolean, val path: String ) { @@ -66,7 +67,8 @@ data class AttachmentEntity( fwdUid = forwardedUid ?: -1, path = path, isForwarded = forwardedFolder?.isNotEmpty() == true && (forwardedUid != null && forwardedUid > 0), - isEncryptionAllowed = true + isEncryptionAllowed = true, + decryptWhenForward = decryptWhenForward ) } @@ -89,7 +91,8 @@ data class AttachmentEntity( fileUri = if (uri != null) uri.toString() else null, forwardedFolder = fwdFolder, forwardedUid = fwdUid, - path = path + path = path, + decryptWhenForward = decryptWhenForward ) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt index 4de474f16a..fb4d4864b3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt @@ -27,7 +27,9 @@ import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.jetpack.viewmodel.AccountViewModel +import com.flowcrypt.email.security.KeysStorageImpl import com.flowcrypt.email.security.SecurityUtils +import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify import com.flowcrypt.email.security.pgp.PgpEncryptAndOrSign import com.flowcrypt.email.ui.notifications.ErrorNotificationManager import com.flowcrypt.email.util.FileAndDirectoryUtils @@ -40,6 +42,8 @@ import com.sun.mail.imap.IMAPFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.apache.commons.io.FileUtils +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.key.protection.SecretKeyRingProtector import java.io.File import java.io.InputStream import java.util.* @@ -224,17 +228,21 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam -> InputStream? ): MessageState = withContext(Dispatchers.IO) { var msgState = MessageState.QUEUED - var pubKeys: List? = null + val keysStorage = KeysStorageImpl.getInstance(applicationContext) + val secretKeys = PGPSecretKeyRingCollection(keysStorage.getPGPSecretKeyRings()) + val ringProtector = keysStorage.getSecretKeyRingProtector() + + var publicKeys: List? = null if (msgEntity.isEncrypted == true) { val senderEmail = EmailUtil.getFirstAddressString(msgEntity.from) val recipients = msgEntity.allRecipients.toMutableList() - pubKeys = mutableListOf() - pubKeys.addAll(SecurityUtils.getRecipientsUsablePubKeys(applicationContext, recipients)) - pubKeys.addAll( + + publicKeys = mutableListOf() + publicKeys.addAll(SecurityUtils.getRecipientsUsablePubKeys(applicationContext, recipients)) + publicKeys.addAll( SecurityUtils.getSenderPgpKeyDetailsList(applicationContext, account, senderEmail) - .map { it.publicKey } - ) + .map { it.publicKey }) } for (attachmentEntity in atts) { @@ -257,7 +265,15 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam val inputStream = action.invoke(attachmentEntity) val tempFile = File(fwdAttsCacheDir, UUID.randomUUID().toString()) if (inputStream != null) { - downloadFile(msgEntity, pubKeys, tempFile, inputStream) + downloadFileAndProcess( + destFile = tempFile, + srcInputStream = inputStream, + shouldBeEncrypted = msgEntity.isEncrypted ?: false, + shouldBeDecryptedBeforeProcess = attachmentEntity.decryptWhenForward, + publicKeys = publicKeys, + secretKeys = secretKeys, + protector = ringProtector + ) if (msgAttsDir.exists()) { FileUtils.moveFile(tempFile, attFile) @@ -288,18 +304,50 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam return@withContext msgState } - private suspend fun downloadFile( - msgEntity: MessageEntity, pubKeys: List?, - destFile: File, srcInputStream: InputStream - ) = - withContext(Dispatchers.IO) { - if (msgEntity.isEncrypted == true) { - requireNotNull(pubKeys) - PgpEncryptAndOrSign.encryptAndOrSign(srcInputStream, destFile.outputStream(), pubKeys) + private suspend fun downloadFileAndProcess( + destFile: File, + srcInputStream: InputStream, + shouldBeEncrypted: Boolean, + shouldBeDecryptedBeforeProcess: Boolean, + publicKeys: List?, + secretKeys: PGPSecretKeyRingCollection? = null, + protector: SecretKeyRingProtector? = null + ) = withContext(Dispatchers.IO) { + var tempFileForDecryptionPurposes: File? = null + val finalSrcInputStream = if (shouldBeDecryptedBeforeProcess) { + tempFileForDecryptionPurposes = File.createTempFile("tmp", null, destFile.parentFile) + tempFileForDecryptionPurposes.outputStream().use { middleDestFileOutputStream -> + PgpDecryptAndOrVerify.decrypt( + srcInputStream = srcInputStream, + destOutputStream = middleDestFileOutputStream, + secretKeys = requireNotNull(secretKeys), + protector = requireNotNull(protector) + ) + } + tempFileForDecryptionPurposes.inputStream() + } else { + srcInputStream + } + + finalSrcInputStream.use { + if (shouldBeEncrypted) { + requireNotNull(publicKeys) + PgpEncryptAndOrSign.encryptAndOrSign( + finalSrcInputStream, + destFile.outputStream(), + publicKeys + ) } else { - FileUtils.copyInputStreamToFile(srcInputStream, destFile) + FileUtils.copyInputStreamToFile(finalSrcInputStream, destFile) + } + } + + tempFileForDecryptionPurposes?.let { + if (!it.delete()) { + throw IllegalStateException("Can't delete temp file") } } + } companion object { private val TAG = ForwardedAttachmentsDownloaderWorker::class.java.simpleName diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt index 1c4ae2c5b1..a56e9bb53c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/PrepareOutgoingMessagesJobIntentService.kt @@ -300,7 +300,7 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { if (att.isEncryptionAllowed && outgoingMsgInfo.encryptionType === MessageEncryptionType.ENCRYPTED ) { - val fileName = att.getSafeName() + Constants.PGP_FILE_EXT + val fileName = att.getSafeName() + "." + Constants.PGP_FILE_EXT var encryptedTempFile = File(attsCacheDir, fileName) if (encryptedTempFile.exists()) { @@ -357,7 +357,7 @@ class PrepareOutgoingMessagesJobIntentService : JobIntentService() { if (att.isEncryptionAllowed && outgoingMsgInfo.encryptionType === MessageEncryptionType.ENCRYPTED) { val encryptedAtt = att.copy(JavaEmailConstants.FOLDER_OUTBOX, uid) - encryptedAtt.name = encryptedAtt.name + Constants.PGP_FILE_EXT + encryptedAtt.name = encryptedAtt.name + "." + Constants.PGP_FILE_EXT cachedAtts.add(encryptedAtt) } else { cachedAtts.add(att.copy(JavaEmailConstants.FOLDER_OUTBOX, uid)) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index 801bb78d5f..9a6d01f60d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -104,6 +104,7 @@ import com.google.android.gms.auth.UserRecoverableAuthException import com.google.android.material.snackbar.Snackbar import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import org.apache.commons.io.FileUtils +import org.apache.commons.io.FilenameUtils import java.io.File import java.nio.charset.StandardCharsets import java.util.* @@ -388,13 +389,14 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi R.id.layoutFwdButton -> { if (msgEncryptType === MessageEncryptionType.ENCRYPTED) { - if (attachmentsRecyclerViewAdapter.currentList.isNotEmpty()) { - Toast.makeText( - context, - R.string.cannot_forward_encrypted_attachments, - Toast.LENGTH_LONG - ).show() - } + msgInfo?.atts = + attachmentsRecyclerViewAdapter.currentList.map { + it.copy( + isForwarded = true, + name = if (it.isEncrypted()) FilenameUtils.removeExtension(it.name) else it.name, + decryptWhenForward = it.isEncrypted() + ) + } } else { msgInfo?.atts = attachmentsRecyclerViewAdapter.currentList.map { it.copy(isForwarded = true) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttachmentsRecyclerViewAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttachmentsRecyclerViewAdapter.kt index 917c54b9bd..fd7526860e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttachmentsRecyclerViewAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttachmentsRecyclerViewAdapter.kt @@ -57,7 +57,7 @@ class AttachmentsRecyclerViewAdapter(private val listener: Listener) : itemView.setOnClickListener { view -> attachmentInfo.uri?.let { uri -> - if (uri.lastPathSegment?.endsWith(Constants.PGP_FILE_EXT) == true) { + if (uri.lastPathSegment?.endsWith("." + Constants.PGP_FILE_EXT) == true) { view.performClick() } else { val intentOpenFile = Intent(Intent.ACTION_VIEW, uri) From 36082298cb022d9ae631279e918aa7fdfbf97a7a Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 16 Dec 2021 10:34:15 +0200 Subject: [PATCH 2/5] Improved ForwardedAttachmentsDownloaderWorker.downloadFileAndProcess().| #1538 --- .../ForwardedAttachmentsDownloaderWorker.kt | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt index fb4d4864b3..8449f356d6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt @@ -314,37 +314,39 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam protector: SecretKeyRingProtector? = null ) = withContext(Dispatchers.IO) { var tempFileForDecryptionPurposes: File? = null - val finalSrcInputStream = if (shouldBeDecryptedBeforeProcess) { - tempFileForDecryptionPurposes = File.createTempFile("tmp", null, destFile.parentFile) - tempFileForDecryptionPurposes.outputStream().use { middleDestFileOutputStream -> - PgpDecryptAndOrVerify.decrypt( - srcInputStream = srcInputStream, - destOutputStream = middleDestFileOutputStream, - secretKeys = requireNotNull(secretKeys), - protector = requireNotNull(protector) - ) - } - tempFileForDecryptionPurposes.inputStream() - } else { - srcInputStream - } - - finalSrcInputStream.use { - if (shouldBeEncrypted) { - requireNotNull(publicKeys) - PgpEncryptAndOrSign.encryptAndOrSign( - finalSrcInputStream, - destFile.outputStream(), - publicKeys - ) + try { + val finalSrcInputStream = if (shouldBeDecryptedBeforeProcess) { + tempFileForDecryptionPurposes = File.createTempFile("tmp", null, destFile.parentFile) + tempFileForDecryptionPurposes.outputStream().use { middleDestFileOutputStream -> + PgpDecryptAndOrVerify.decrypt( + srcInputStream = srcInputStream, + destOutputStream = middleDestFileOutputStream, + secretKeys = requireNotNull(secretKeys), + protector = requireNotNull(protector) + ) + } + tempFileForDecryptionPurposes.inputStream() } else { - FileUtils.copyInputStreamToFile(finalSrcInputStream, destFile) + srcInputStream } - } - tempFileForDecryptionPurposes?.let { - if (!it.delete()) { - throw IllegalStateException("Can't delete temp file") + finalSrcInputStream.use { + if (shouldBeEncrypted) { + requireNotNull(publicKeys) + PgpEncryptAndOrSign.encryptAndOrSign( + finalSrcInputStream, + destFile.outputStream(), + publicKeys + ) + } else { + FileUtils.copyInputStreamToFile(finalSrcInputStream, destFile) + } + } + } finally { + tempFileForDecryptionPurposes?.let { + if (it.exists() && !it.delete()) { + throw IllegalStateException("Can't delete temp file") + } } } } From 328b147ea6c3060c2e317d17b9825aca8de8e69a Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 17 Dec 2021 12:18:28 +0200 Subject: [PATCH 3/5] ForwardedAttachmentsDownloaderWorker. Modified logic to just download an attachment as-is(without post-processing).| #1538 --- .../ForwardedAttachmentsDownloaderWorker.kt | 86 ++----------------- 1 file changed, 6 insertions(+), 80 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt index 8449f356d6..1c3a603f1a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/ForwardedAttachmentsDownloaderWorker.kt @@ -15,7 +15,6 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters import com.flowcrypt.email.Constants -import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.protocol.ImapProtocolUtil @@ -27,10 +26,6 @@ import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.jetpack.viewmodel.AccountViewModel -import com.flowcrypt.email.security.KeysStorageImpl -import com.flowcrypt.email.security.SecurityUtils -import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify -import com.flowcrypt.email.security.pgp.PgpEncryptAndOrSign import com.flowcrypt.email.ui.notifications.ErrorNotificationManager import com.flowcrypt.email.util.FileAndDirectoryUtils import com.flowcrypt.email.util.GeneralUtil @@ -42,9 +37,8 @@ import com.sun.mail.imap.IMAPFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.apache.commons.io.FileUtils -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection -import org.pgpainless.key.protection.SecretKeyRingProtector import java.io.File +import java.io.FileOutputStream import java.io.InputStream import java.util.* import javax.mail.Folder @@ -228,23 +222,6 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam -> InputStream? ): MessageState = withContext(Dispatchers.IO) { var msgState = MessageState.QUEUED - val keysStorage = KeysStorageImpl.getInstance(applicationContext) - val secretKeys = PGPSecretKeyRingCollection(keysStorage.getPGPSecretKeyRings()) - val ringProtector = keysStorage.getSecretKeyRingProtector() - - var publicKeys: List? = null - - if (msgEntity.isEncrypted == true) { - val senderEmail = EmailUtil.getFirstAddressString(msgEntity.from) - val recipients = msgEntity.allRecipients.toMutableList() - - publicKeys = mutableListOf() - publicKeys.addAll(SecurityUtils.getRecipientsUsablePubKeys(applicationContext, recipients)) - publicKeys.addAll( - SecurityUtils.getSenderPgpKeyDetailsList(applicationContext, account, senderEmail) - .map { it.publicKey }) - } - for (attachmentEntity in atts) { val attInfo = attachmentEntity.toAttInfo() @@ -265,15 +242,11 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam val inputStream = action.invoke(attachmentEntity) val tempFile = File(fwdAttsCacheDir, UUID.randomUUID().toString()) if (inputStream != null) { - downloadFileAndProcess( - destFile = tempFile, - srcInputStream = inputStream, - shouldBeEncrypted = msgEntity.isEncrypted ?: false, - shouldBeDecryptedBeforeProcess = attachmentEntity.decryptWhenForward, - publicKeys = publicKeys, - secretKeys = secretKeys, - protector = ringProtector - ) + inputStream.use { srcStream -> + FileOutputStream(tempFile).use { destStream -> + srcStream.copyTo(destStream) + } + } if (msgAttsDir.exists()) { FileUtils.moveFile(tempFile, attFile) @@ -304,53 +277,6 @@ class ForwardedAttachmentsDownloaderWorker(context: Context, params: WorkerParam return@withContext msgState } - private suspend fun downloadFileAndProcess( - destFile: File, - srcInputStream: InputStream, - shouldBeEncrypted: Boolean, - shouldBeDecryptedBeforeProcess: Boolean, - publicKeys: List?, - secretKeys: PGPSecretKeyRingCollection? = null, - protector: SecretKeyRingProtector? = null - ) = withContext(Dispatchers.IO) { - var tempFileForDecryptionPurposes: File? = null - try { - val finalSrcInputStream = if (shouldBeDecryptedBeforeProcess) { - tempFileForDecryptionPurposes = File.createTempFile("tmp", null, destFile.parentFile) - tempFileForDecryptionPurposes.outputStream().use { middleDestFileOutputStream -> - PgpDecryptAndOrVerify.decrypt( - srcInputStream = srcInputStream, - destOutputStream = middleDestFileOutputStream, - secretKeys = requireNotNull(secretKeys), - protector = requireNotNull(protector) - ) - } - tempFileForDecryptionPurposes.inputStream() - } else { - srcInputStream - } - - finalSrcInputStream.use { - if (shouldBeEncrypted) { - requireNotNull(publicKeys) - PgpEncryptAndOrSign.encryptAndOrSign( - finalSrcInputStream, - destFile.outputStream(), - publicKeys - ) - } else { - FileUtils.copyInputStreamToFile(finalSrcInputStream, destFile) - } - } - } finally { - tempFileForDecryptionPurposes?.let { - if (it.exists() && !it.delete()) { - throw IllegalStateException("Can't delete temp file") - } - } - } - } - companion object { private val TAG = ForwardedAttachmentsDownloaderWorker::class.java.simpleName val NAME = ForwardedAttachmentsDownloaderWorker::class.java.simpleName From b30e554cc566a068b504d2a8bfaf76575258073e Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 17 Dec 2021 12:29:38 +0200 Subject: [PATCH 4/5] Renamed AttachmentInfo.isEncrypted() to isPossiblyEncrypted(). Improved and refactored code.| #1538 --- .../email/api/email/model/AttachmentInfo.kt | 7 ++-- .../flowcrypt/email/security/SecurityUtils.kt | 8 ++--- .../security/pgp/PgpDecryptAndOrVerify.kt | 3 -- .../flowcrypt/email/security/pgp/PgpMsg.kt | 2 +- .../AttachmentDownloadManagerService.kt | 2 +- .../fragment/MessageDetailsFragment.kt | 6 ++-- .../security/pgp/PgpDecryptAndOrVerifyTest.kt | 32 +++++++++---------- 7 files changed, 27 insertions(+), 33 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt index a4427bfd0b..ceff0a5c2d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/AttachmentInfo.kt @@ -10,7 +10,7 @@ import android.os.Parcel import android.os.Parcelable import com.flowcrypt.email.Constants import com.flowcrypt.email.security.SecurityUtils -import org.apache.commons.io.FilenameUtils +import com.flowcrypt.email.security.pgp.PgpMsg /** * Simple POJO which defines information about email attachments. @@ -160,9 +160,8 @@ data class AttachmentInfo constructor( fun isHidden() = name.isNullOrEmpty() && type.lowercase() == "application/pgp-encrypted; name=\"\"" - fun isEncrypted(): Boolean { - val fileExtension = FilenameUtils.getExtension(name) - return fileExtension.equals(Constants.PGP_FILE_EXT, true) + fun isPossiblyEncrypted(): Boolean { + return PgpMsg.ENCRYPTED_FILE_REGEX.containsMatchIn(name ?: "") } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/SecurityUtils.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/SecurityUtils.kt index 58b2ba8463..5004c19d22 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/SecurityUtils.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/SecurityUtils.kt @@ -13,8 +13,8 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails import com.flowcrypt.email.security.model.PgpKeyDetails -import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify import com.flowcrypt.email.security.pgp.PgpKey +import com.flowcrypt.email.security.pgp.PgpMsg import com.flowcrypt.email.security.pgp.PgpPwd import com.flowcrypt.email.util.exception.DifferentPassPhrasesException import com.flowcrypt.email.util.exception.EmptyPassphraseException @@ -180,10 +180,8 @@ class SecurityUtils { * Check if the file extension fits the encrypted pattern. * If yes - it can mean the file is encrypted */ - fun isEncryptedData(fileName: String?): Boolean { - return PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find( - fileName ?: "" - ) != null + fun isPossiblyEncryptedData(fileName: String?): Boolean { + return PgpMsg.ENCRYPTED_FILE_REGEX.containsMatchIn(fileName ?: "") } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt index fe690b1efc..1e8cd46eea 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt @@ -34,9 +34,6 @@ import java.io.OutputStream * E-mail: DenBond7@gmail.com */ object PgpDecryptAndOrVerify { - val DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN = - "(?i)(\\.pgp$)|(\\.gpg$)|(\\.[a-zA-Z0-9]{3,4}\\.asc$)".toRegex() - fun decrypt( srcInputStream: InputStream, destOutputStream: OutputStream, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt index 8ebe6a6b3d..55d553f3cb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt @@ -98,7 +98,7 @@ object PgpMsg { "openpgp-encrypted-message.asc" ) - private val ENCRYPTED_FILE_REGEX = Regex( + val ENCRYPTED_FILE_REGEX = Regex( pattern = "(\\.pgp\$)|(\\.gpg\$)|(\\.[a-zA-Z0-9]{3,4}\\.asc\$)", option = RegexOption.IGNORE_CASE ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt index 4304d62f3c..e3ff70857b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt @@ -662,7 +662,7 @@ class AttachmentDownloadManagerService : Service() { throw NullPointerException("Error. The file is missing") } - if (!SecurityUtils.isEncryptedData(att.name)) { + if (!SecurityUtils.isPossiblyEncryptedData(att.name)) { return file } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index 9a6d01f60d..696bb680d7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -140,7 +140,7 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi lastClickedAtt = attachmentInfo lastClickedAtt?.orderNumber = GeneralUtil.genAttOrderId(requireContext()) - if (SecurityUtils.isEncryptedData(attachmentInfo.name)) { + if (SecurityUtils.isPossiblyEncryptedData(attachmentInfo.name)) { for (block in msgInfo?.msgBlocks ?: emptyList()) { if (block.type == MsgBlock.Type.DECRYPT_ERROR) { val decryptErrorMsgBlock = block as? DecryptErrorMsgBlock ?: continue @@ -393,8 +393,8 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi attachmentsRecyclerViewAdapter.currentList.map { it.copy( isForwarded = true, - name = if (it.isEncrypted()) FilenameUtils.removeExtension(it.name) else it.name, - decryptWhenForward = it.isEncrypted() + name = if (it.isPossiblyEncrypted()) FilenameUtils.removeExtension(it.name) else it.name, + decryptWhenForward = it.isPossiblyEncrypted() ) } } else { diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt index d205601dce..a56e795cb7 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerifyTest.kt @@ -160,22 +160,22 @@ class PgpDecryptAndOrVerifyTest { @Test fun testPatternToDetectEncryptedAtts() { //"(?i)(\\.pgp$)|(\\.gpg$)|(\\.[a-zA-Z0-9]{3,4}\\.asc$)" - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("file.pgp")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("file.PgP")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("file.gpg")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("file.gPg")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.fs12.asc")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.fs12.ASC")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.s12.asc")) - assertNotNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.ft2.ASC")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("filepgp")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("filePgP")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("filegpg")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("filegPg")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.fs12asc")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.fs12ASC")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.s12asc")) - assertNull(PgpDecryptAndOrVerify.DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN.find("d.ft2ASC")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("file.pgp")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("file.PgP")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("file.gpg")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("file.gPg")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.fs12.asc")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.fs12.ASC")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.s12.asc")) + assertNotNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.ft2.ASC")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("filepgp")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("filePgP")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("filegpg")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("filegPg")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.fs12asc")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.fs12ASC")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.s12asc")) + assertNull(PgpMsg.ENCRYPTED_FILE_REGEX.find("d.ft2ASC")) } private fun testDecryptFileSuccess(shouldSrcBeArmored: Boolean) { From 24098cc98953d2d4add1ed066278dd01d170be62 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 17 Dec 2021 15:02:43 +0200 Subject: [PATCH 5/5] Moved logic that relates to decrypting and or encrypting forwarded attachments from ForwardedAttachmentsDownloaderWorker to MessagesSenderWorker. Refactored code.| #1538 --- .../email/database/entity/AttachmentEntity.kt | 7 +- .../workmanager/MessagesSenderWorker.kt | 98 +++++++++++++++++-- .../security/pgp/PgpDecryptAndOrVerify.kt | 49 +++++++--- .../email/security/pgp/PgpEncryptAndOrSign.kt | 67 ++++++++----- 4 files changed, 170 insertions(+), 51 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt index 43df27f594..95b6216c4a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt @@ -10,6 +10,7 @@ import android.provider.BaseColumns import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey import com.flowcrypt.email.api.email.model.AttachmentInfo @@ -53,6 +54,10 @@ data class AttachmentEntity( val path: String ) { + @Ignore + val isForwarded: Boolean = + forwardedFolder?.isNotEmpty() == true && (forwardedUid != null && forwardedUid > 0) + fun toAttInfo(): AttachmentInfo { return AttachmentInfo( email = email, @@ -66,7 +71,7 @@ data class AttachmentEntity( fwdFolder = forwardedFolder, fwdUid = forwardedUid ?: -1, path = path, - isForwarded = forwardedFolder?.isNotEmpty() == true && (forwardedUid != null && forwardedUid > 0), + isForwarded = isForwarded, isEncryptionAllowed = true, decryptWhenForward = decryptWhenForward ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index 912a5229ba..d596ad7c3c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -20,6 +20,7 @@ import androidx.work.WorkerParameters import com.flowcrypt.email.BuildConfig import com.flowcrypt.email.Constants import com.flowcrypt.email.R +import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper @@ -33,6 +34,10 @@ import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.jetpack.viewmodel.AccountViewModel +import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.SecurityUtils +import com.flowcrypt.email.security.pgp.PgpDecryptAndOrVerify +import com.flowcrypt.email.security.pgp.PgpEncryptAndOrSign import com.flowcrypt.email.ui.notifications.NotificationChannelManager import com.flowcrypt.email.util.FileAndDirectoryUtils import com.flowcrypt.email.util.GeneralUtil @@ -51,8 +56,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import org.apache.commons.io.IOUtils +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.pgpainless.key.protection.SecretKeyRingProtector import java.io.BufferedInputStream import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -336,7 +344,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : val attachments = roomDatabase.attachmentDao() .getAttachmentsSuspend(email, JavaEmailConstants.FOLDER_OUTBOX, msgEntity.uid) - val mimeMsg = createMimeMsg(sess, msgEntity, attachments) + val mimeMsg = createMimeMsg(sess, account, msgEntity, attachments) roomDatabase.msgDao().resetMsgsWithSendingStateSuspend(account.email) roomDatabase.msgDao().updateSuspend(msgEntity.copy(state = MessageState.SENDING.value)) @@ -416,7 +424,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : atts: List, sess: Session?, store: Store? ): Boolean = withContext(Dispatchers.IO) { - val mimeMsg = createMimeMsg(sess, msgEntity, atts) + val mimeMsg = createMimeMsg(sess, account, msgEntity, atts) val roomDatabase = FlowCryptRoomDatabase.getDatabase(applicationContext) when (account.accountType) { @@ -509,12 +517,13 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : */ private suspend fun createMimeMsg( sess: Session?, - details: MessageEntity, + account: AccountEntity, + msgEntity: MessageEntity, atts: List ): MimeMessage = withContext(Dispatchers.IO) { val stream = - IOUtils.toInputStream(details.rawMessageWithoutAttachments, StandardCharsets.UTF_8) + IOUtils.toInputStream(msgEntity.rawMessageWithoutAttachments, StandardCharsets.UTF_8) val mimeMsg = MimeMessage(sess, stream) //https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#:~:text=User%2DAgent%20and%20X%2DMailer%20are%20common%20Email%20header%20fields,use%20of%20different%20email%20clients. @@ -522,9 +531,28 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : if (mimeMsg.content is MimeMultipart && atts.isNotEmpty()) { val mimeMultipart = mimeMsg.content as MimeMultipart + val keysStorage = KeysStorageImpl.getInstance(applicationContext) + val secretKeys = PGPSecretKeyRingCollection(keysStorage.getPGPSecretKeyRings()) + val ringProtector = keysStorage.getSecretKeyRingProtector() + + val publicKeys = mutableListOf() + val senderEmail = EmailUtil.getFirstAddressString(msgEntity.from) + val recipients = msgEntity.allRecipients.toMutableList() + publicKeys.addAll( + SecurityUtils.getRecipientsUsablePubKeys(applicationContext, recipients) + ) + publicKeys.addAll( + SecurityUtils.getSenderPgpKeyDetailsList(applicationContext, account, senderEmail) + .map { it.publicKey }) for (att in atts) { - val attBodyPart = genBodyPartWithAtt(att) + val attBodyPart = genBodyPartWithAtt( + att = att, + shouldBeEncrypted = msgEntity.isEncrypted ?: false, + publicKeys = publicKeys, + secretKeys = secretKeys, + ringProtector = ringProtector + ) mimeMultipart.addBodyPart(attBodyPart) } @@ -543,10 +571,29 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : * @return Generated [MimeBodyPart] with the attachment. * @throws MessagingException */ - private fun genBodyPartWithAtt(att: AttachmentEntity): BodyPart { + private fun genBodyPartWithAtt( + att: AttachmentEntity, + shouldBeEncrypted: Boolean, + publicKeys: List?, + secretKeys: PGPSecretKeyRingCollection, + ringProtector: SecretKeyRingProtector + ): BodyPart { val attBodyPart = MimeBodyPart() val attInfo = att.toAttInfo() - attBodyPart.dataHandler = DataHandler(AttachmentInfoDataSource(applicationContext, attInfo)) + attBodyPart.dataHandler = if (attInfo.isForwarded) { + DataHandler( + ForwardedAttachmentInfoDataSource( + applicationContext, + attInfo, + shouldBeEncrypted, + publicKeys, + secretKeys, + ringProtector + ) + ) + } else { + DataHandler(AttachmentInfoDataSource(applicationContext, attInfo)) + } attBodyPart.fileName = attInfo.getSafeName() attBodyPart.contentID = attInfo.id @@ -591,9 +638,9 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : /** * The [DataSource] realization for a file which received from [Uri] */ - private class AttachmentInfoDataSource( + private open class AttachmentInfoDataSource( private val context: Context, - private val att: AttachmentInfo + protected val att: AttachmentInfo ) : DataSource { override fun getInputStream(): InputStream? { @@ -624,6 +671,39 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } } + private class ForwardedAttachmentInfoDataSource( + context: Context, + att: AttachmentInfo, + private val shouldBeEncrypted: Boolean, + private val publicKeys: List? = null, + private val secretKeys: PGPSecretKeyRingCollection, + private val protector: SecretKeyRingProtector + ) : AttachmentInfoDataSource(context, att) { + override fun getInputStream(): InputStream? { + val inputStream = super.getInputStream() ?: return null + val srcInputStream = if (att.decryptWhenForward) PgpDecryptAndOrVerify.genDecryptionStream( + srcInputStream = inputStream, + secretKeys = secretKeys, + protector = protector + ) else inputStream + + return if (shouldBeEncrypted) { + //here we use [ByteArrayOutputStream] as a temp destination of encrypted data. + //it should be improved in the future for better performance + val tempByteArrayOutputStream = ByteArrayOutputStream() + PgpEncryptAndOrSign.encryptAndOrSign( + srcInputStream = srcInputStream, + destOutputStream = tempByteArrayOutputStream, + pubKeys = requireNotNull(publicKeys) + ) + + return ByteArrayInputStream(tempByteArrayOutputStream.toByteArray()) + } else { + srcInputStream + } + } + } + companion object { private val TAG = MessagesSenderWorker::class.java.simpleName private const val NOTIFICATION_ID = -10000 diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt index 1e8cd46eea..7983bf10b2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpDecryptAndOrVerify.kt @@ -14,6 +14,7 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection import org.bouncycastle.openpgp.PGPSecretKeyRingCollection import org.pgpainless.PGPainless import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.decryption_verification.DecryptionStream import org.pgpainless.decryption_verification.MissingKeyPassphraseStrategy import org.pgpainless.decryption_verification.OpenPgpMetadata import org.pgpainless.exception.MessageNotIntegrityProtectedException @@ -43,13 +44,11 @@ object PgpDecryptAndOrVerify { srcInputStream.use { srcStream -> destOutputStream.use { outStream -> try { - val decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(srcStream) - .withOptions( - ConsumerOptions() - .addDecryptionKeys(secretKeys, protector) - .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) - ) + val decryptionStream = genDecryptionStream( + srcInputStream = srcStream, + secretKeys = secretKeys, + protector = protector + ) decryptionStream.use { it.copyTo(outStream) } return decryptionStream.result } catch (e: Exception) { @@ -70,15 +69,13 @@ object PgpDecryptAndOrVerify { val destOutputStream = ByteArrayOutputStream() destOutputStream.use { outStream -> try { - val decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(srcStream) - .withOptions( - ConsumerOptions() - .addDecryptionKeys(secretKeys, protector) - .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) - .setIgnoreMDCErrors(ignoreMdcErrors) - .addVerificationCerts(publicKeys) - ) + val decryptionStream = genDecryptionStream( + srcInputStream = srcStream, + publicKeys = publicKeys, + secretKeys = secretKeys, + protector = protector, + ignoreMdcErrors = ignoreMdcErrors + ) decryptionStream.use { it.copyTo(outStream) } return DecryptionResult( @@ -94,6 +91,26 @@ object PgpDecryptAndOrVerify { } } + fun genDecryptionStream( + srcInputStream: InputStream, + publicKeys: PGPPublicKeyRingCollection? = null, + secretKeys: PGPSecretKeyRingCollection, + protector: SecretKeyRingProtector, + ignoreMdcErrors: Boolean = false + ): DecryptionStream { + return PGPainless.decryptAndOrVerify() + .onInputStream(srcInputStream) + .withOptions( + ConsumerOptions() + .addDecryptionKeys(secretKeys, protector) + .setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy.THROW_EXCEPTION) + .setIgnoreMDCErrors(ignoreMdcErrors) + .apply { + publicKeys?.let { addVerificationCerts(it) } + } + ) + } + private fun processDecryptionException(e: Exception): DecryptionException { return when (e) { is WrongPassphraseException -> { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncryptAndOrSign.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncryptAndOrSign.kt index 8141d69972..2fbe0342e1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncryptAndOrSign.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpEncryptAndOrSign.kt @@ -11,6 +11,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection import org.pgpainless.PGPainless import org.pgpainless.algorithm.DocumentSignatureType import org.pgpainless.encryption_signing.EncryptionOptions +import org.pgpainless.encryption_signing.EncryptionStream import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.key.protection.SecretKeyRingProtector @@ -91,34 +92,50 @@ object PgpEncryptAndOrSign { doArmor: Boolean = false ) { srcInputStream.use { srcStream -> - destOutputStream.use { outStream -> - val encOpt = EncryptionOptions().apply { - pgpPublicKeyRingCollection.forEach { - addRecipient(it) - } + genEncryptionStream( + destOutputStream, + pgpPublicKeyRingCollection, + pgpSecretKeyRingCollection, + secretKeyRingProtector, + doArmor + ).use { encryptionStream -> + srcStream.copyTo(encryptionStream) + } + } + } + + fun genEncryptionStream( + destOutputStream: OutputStream, + pgpPublicKeyRingCollection: PGPPublicKeyRingCollection, + pgpSecretKeyRingCollection: PGPSecretKeyRingCollection?, + secretKeyRingProtector: SecretKeyRingProtector?, + doArmor: Boolean + ): EncryptionStream { + destOutputStream.use { outStream -> + val encOpt = EncryptionOptions().apply { + pgpPublicKeyRingCollection.forEach { + addRecipient(it) } + } - val producerOptions: ProducerOptions = - if (pgpSecretKeyRingCollection?.keyRings?.hasNext() == true) { - ProducerOptions.signAndEncrypt(encOpt, SigningOptions().apply { - pgpSecretKeyRingCollection.forEach { - addInlineSignature( - secretKeyRingProtector, it, DocumentSignatureType.BINARY_DOCUMENT - ) - } - }) - } else { - ProducerOptions.encrypt(encOpt) - } + val producerOptions: ProducerOptions = + if (pgpSecretKeyRingCollection?.keyRings?.hasNext() == true) { + ProducerOptions.signAndEncrypt(encOpt, SigningOptions().apply { + pgpSecretKeyRingCollection.forEach { + addInlineSignature( + secretKeyRingProtector, it, DocumentSignatureType.BINARY_DOCUMENT + ) + } + }) + } else { + ProducerOptions.encrypt(encOpt) + } - PGPainless.encryptAndOrSign() - .onOutputStream(outStream) - .withOptions( - producerOptions.setAsciiArmor(doArmor) - ).use { encryptionStream -> - srcStream.copyTo(encryptionStream) - } - } + return PGPainless.encryptAndOrSign() + .onOutputStream(outStream) + .withOptions( + producerOptions.setAsciiArmor(doArmor) + ) } } }