From 4980e519209a106e245f3104d718123a8cbaa152 Mon Sep 17 00:00:00 2001 From: Den Date: Thu, 27 May 2021 11:08:04 +0300 Subject: [PATCH] Issue 372 keep pass phrase in memory (#1249) * Added PassPhrasesInRAMService. Refactored code.| #372 * Dropped using 'longId'. Migrated to use only 'fingerprint'. Updated the database version to 25. Refactored code.| #372 * Fixed bugs in tests.| #372 * Modified KeysStorage. Moved to use Passphrase instead of String where it is possible. Refactored code.| #372 * Moved NodeKeyDetails to 'security.model'.| #372 * Renamed KeyDetails.Type to KeyDetails.SourceType.| #372 * Renamed KeyDetails to KeyImportDetails.| #372 * Renamed NodeKeyDetails to PgpKeyDetails.| #372 * Refactored code.| #372 * Fixed refactoring errors.| #372 * Refactored code.| #372 * Refactored code.| #372 * Fixed PrivateKeysManager.| #372 * Refactored code.| #372 * Replaced KeyEntity with PgpKeyDetails where it is possible.| #372 * Modified the database schema to store different passphrase types.| #372 * Modified KeysDao to prevent saving passphrase if passphraseType == KeyEntity.PassphraseType.RAM.| #372 * Modified UI in CheckKeysActivity. Added some logic to CheckPrivateKeysViewModel. Refactored code.| #372 * Fixed conflicts after merge.| #372 * Modified PrivateKeysViewModel to be able to store passphrase in RAM. Refactored code in KeysStorageImpl.| #372 * Added auto manage of PassPhrasesInRAMService.| #372 * Improved KeysStorageImpl.updatePassPhrasesCache().| #372 * Modified PrivateKeyDetailsFragment to be able to update(set or erase) a pass phrase for passphraseType == KeyEntity.PassphraseType.RAM.| #372 * Updated a notification in PassPhrasesInRAMService.| #372 * Fixed a bug in CheckKeysActivity. Refactored code.| #372 * Fixed the database migration.| #372 * Fixed a bug in KeysStorageImpl.getPGPSecretKeyRingByFingerprint().| #372 * Added some logic to KeysStorageImpl.| #372 * Fixed a bug in AttachmentDownloadManagerService.| #372 * Fixed some tests.| #372 * Fixed lint warnings.| #372 * Marked some code as deprecated.| #372 * Renamed some strings.| #372 * Disabled "Keep pass phrase in memory" for release builds.| #372 * Made notifications for PassPhrasesInRAMService silent.| #372 * Removed unused code.| #372 * Fixed method names.| #372 --- .../24.json | 16 +- .../25.json | 1000 +++++++++++++++++ .../flowcrypt/email/database/MigrationTest.kt | 12 +- .../rules/AddPrivateKeyToDatabaseRule.kt | 14 +- .../activity/AddOtherAccountFragmentTest.kt | 20 +- .../ui/activity/BackupKeysActivityTest.kt | 10 +- .../CheckKeysActivityTestMultiBackups.kt | 58 +- .../CheckKeysActivityWithExistingKeysTest.kt | 6 +- ...heckKeysActivityWithoutExistingKeysTest.kt | 4 +- .../activity/ContactsSettingsActivityTest.kt | 2 +- .../ui/activity/CreateMessageActivityTest.kt | 8 +- ...mportPrivateKeyActivityFromSettingsTest.kt | 8 +- ...portPrivateKeyActivityNoPubOrgRulesTest.kt | 6 +- .../ui/activity/KeysSettingsActivityTest.kt | 30 +- .../ui/activity/MessageDetailsActivityTest.kt | 6 +- .../PreviewImportPgpContactActivityTest.kt | 2 +- .../activity/PublicKeyDetailsFragmentTest.kt | 15 +- .../ui/activity/SearchMessagesActivityTest.kt | 2 +- .../ui/activity/SelectContactsActivityTest.kt | 4 +- .../email/util/PrivateKeysManager.kt | 72 +- FlowCrypt/src/main/AndroidManifest.xml | 7 + .../flowcrypt/email/FlowCryptApplication.kt | 17 +- .../flowcrypt/email/api/email/EmailUtil.kt | 19 +- .../email/api/email/gmail/GmailApiHelper.kt | 12 +- .../api/retrofit/node/RequestsManager.kt | 15 +- .../request/node/DecryptFileRequest.kt | 11 +- .../request/node/ParseDecryptMsgRequest.kt | 23 +- .../response/model/LookUpPublicKeyInfo.kt | 53 - .../response/model/node/DecryptError.kt | 3 - .../retrofit/response/model/node/Longids.kt | 50 - .../response/model/node/NodeKeyDetails.kt | 194 ---- .../response/model/node/PublicKeyMsgBlock.kt | 5 +- .../email/database/FlowCryptRoomDatabase.kt | 120 +- .../converters/PassphraseTypeConverter.kt | 27 + .../flowcrypt/email/database/dao/KeysDao.kt | 82 +- .../email/database/entity/ContactEntity.kt | 9 +- .../email/database/entity/KeyEntity.kt | 71 +- .../org/bouncycastle/openpgp/PGPKeyRingExt.kt | 36 +- .../org/pgpainless/util/PassphraseExt.kt | 19 + .../viewmodel/AccountKeysInfoViewModel.kt | 12 +- .../jetpack/viewmodel/BackupsViewModel.kt | 14 +- .../viewmodel/CheckPrivateKeysViewModel.kt | 35 +- .../jetpack/viewmodel/ContactsViewModel.kt | 29 +- .../viewmodel/DecryptMessageViewModel.kt | 214 ---- .../viewmodel/LoadPrivateKeysViewModel.kt | 16 +- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 34 +- .../jetpack/viewmodel/ParseKeysViewModel.kt | 6 +- .../viewmodel/PgpKeyDetailsViewModel.kt | 97 ++ .../jetpack/viewmodel/PrivateKeysViewModel.kt | 417 ++++--- .../viewmodel/SubmitPubKeyViewModel.kt | 4 +- .../factory/PgpKeyDetailsViewModelFactory.kt | 28 + .../{KeyDetails.kt => KeyImportDetails.kt} | 38 +- .../flowcrypt/email/model/KeyImportModel.kt | 6 +- .../com/flowcrypt/email/model/KeysStorage.kt | 42 +- .../com/flowcrypt/email/model/PgpContact.kt | 12 +- .../flowcrypt/email/model/PublicKeyInfo.kt | 9 +- .../java/com/flowcrypt/email/node/TestData.kt | 37 +- .../email/security/KeysStorageImpl.kt | 275 +++-- .../flowcrypt/email/security/SecurityUtils.kt | 39 +- .../model/node => security/model}/Algo.kt | 2 +- .../model/node => security/model}/KeyId.kt | 17 +- .../email/security/model/PgpKeyDetails.kt | 226 ++++ .../flowcrypt/email/security/pgp/PgpKey.kt | 22 +- .../flowcrypt/email/security/pgp/PgpPwd.kt | 7 +- .../email/service/BaseLifecycleService.kt | 44 + .../service/CheckClipboardToFindKeyService.kt | 6 +- .../email/service/PassPhrasesInRAMService.kt | 107 ++ .../actions/BackupPrivateKeyToInboxAction.kt | 46 +- .../EncryptPrivateKeysIfNeededAction.kt | 19 +- .../AttachmentDownloadManagerService.kt | 14 +- .../email/ui/activity/BackupKeysActivity.kt | 2 +- .../ui/activity/ChangePassPhraseActivity.kt | 4 +- .../email/ui/activity/CheckKeysActivity.kt | 209 ++-- .../ui/activity/CreatePrivateKeyActivity.kt | 47 +- .../email/ui/activity/EditContactActivity.kt | 12 +- .../ui/activity/ImportPgpContactActivity.kt | 8 +- .../ui/activity/ImportPrivateKeyActivity.kt | 59 +- .../ui/activity/ImportPublicKeyActivity.kt | 8 +- .../email/ui/activity/NodeTestActivity.kt | 6 +- .../ui/activity/base/BaseImportKeyActivity.kt | 22 +- .../fragment/AddOtherAccountFragment.kt | 10 +- .../activity/fragment/MainSignInFragment.kt | 12 +- .../fragment/MessageDetailsFragment.kt | 5 +- .../PreviewImportPgpContactFragment.kt | 17 +- .../fragment/PrivateKeyDetailsFragment.kt | 285 +++-- .../fragment/PrivateKeysListFragment.kt | 18 +- .../fragment/PublicKeyDetailsFragment.kt | 22 +- .../fragment/base/BaseSingInFragment.kt | 10 +- .../fragment/base/CreateMessageFragment.kt | 23 +- .../dialog/ChoosePublicKeyDialogFragment.kt | 12 +- .../UpdatePublicKeyOfContactDialogFragment.kt | 49 +- .../preferences/SecuritySettingsFragment.kt | 19 +- .../settings/SearchBackupsInEmailActivity.kt | 4 +- .../email/ui/adapter/AttesterKeyAdapter.kt | 43 +- .../ui/adapter/ContactsRecyclerViewAdapter.kt | 2 +- .../ImportPgpContactsRecyclerViewAdapter.kt | 4 +- .../adapter/PrivateKeysRecyclerViewAdapter.kt | 38 +- .../email/ui/adapter/PubKeysArrayAdapter.kt | 8 +- .../selection/NodeKeyDetailsKeyProvider.kt | 6 +- .../selection/PrivateKeyItemDetailsLookup.kt | 6 +- .../SelectionNodeKeyDetailsDetails.kt | 6 +- .../NotificationChannelManager.kt | 25 +- .../exception/KeyAlreadyAddedException.kt | 4 +- .../SavePrivateKeyToDatabaseException.kt | 4 +- .../drawable/ic_baseline_password_24dp.xml | 15 + .../main/res/layout/activity_check_keys.xml | 27 +- .../layout/fragment_private_key_details.xml | 332 +++--- .../layout/fragment_public_key_details.xml | 4 +- FlowCrypt/src/main/res/layout/key_item.xml | 2 +- .../layout/pub_key_adapter_item_checkbox.xml | 4 +- .../pub_key_adapter_item_radio_button.xml | 4 +- FlowCrypt/src/main/res/values/ids.xml | 1 + FlowCrypt/src/main/res/values/strings.xml | 20 +- .../email/security/pgp/PgpKeyTest.kt | 49 +- 114 files changed, 3563 insertions(+), 1846 deletions(-) create mode 100644 FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/25.json delete mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/LookUpPublicKeyInfo.kt delete mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Longids.kt delete mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/NodeKeyDetails.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/database/converters/PassphraseTypeConverter.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/pgpainless/util/PassphraseExt.kt delete mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DecryptMessageViewModel.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PgpKeyDetailsViewModel.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/factory/PgpKeyDetailsViewModelFactory.kt rename FlowCrypt/src/main/java/com/flowcrypt/email/model/{KeyDetails.kt => KeyImportDetails.kt} (52%) rename FlowCrypt/src/main/java/com/flowcrypt/email/{api/retrofit/response/model/node => security/model}/Algo.kt (95%) rename FlowCrypt/src/main/java/com/flowcrypt/email/{api/retrofit/response/model/node => security/model}/KeyId.kt (58%) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/security/model/PgpKeyDetails.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/service/BaseLifecycleService.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/service/PassPhrasesInRAMService.kt create mode 100644 FlowCrypt/src/main/res/drawable/ic_baseline_password_24dp.xml diff --git a/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/24.json b/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/24.json index a5cde5e966..9fa8230bb5 100644 --- a/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/24.json +++ b/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/24.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 24, - "identityHash": "27b09b859233fc03abaab220d650bf92", + "identityHash": "66f9c3b99371f2874df7dd3184f8b604", "entities": [ { "tableName": "accounts_aliases", @@ -608,7 +608,7 @@ }, { "tableName": "keys", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `long_id` 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, FOREIGN KEY(`account`, `account_type`) REFERENCES `accounts`(`email`, `account_type`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "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, FOREIGN KEY(`account`, `account_type`) REFERENCES `accounts`(`email`, `account_type`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -617,8 +617,8 @@ "notNull": false }, { - "fieldPath": "longId", - "columnName": "long_id", + "fieldPath": "fingerprint", + "columnName": "fingerprint", "affinity": "TEXT", "notNull": true }, @@ -669,14 +669,14 @@ }, "indices": [ { - "name": "long_id_account_account_type_in_keys", + "name": "fingerprint_account_account_type_in_keys", "unique": true, "columnNames": [ - "long_id", + "fingerprint", "account", "account_type" ], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `long_id_account_account_type_in_keys` ON `${TABLE_NAME}` (`long_id`, `account`, `account_type`)" + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `fingerprint_account_account_type_in_keys` ON `${TABLE_NAME}` (`fingerprint`, `account`, `account_type`)" } ], "foreignKeys": [ @@ -987,7 +987,7 @@ "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, '27b09b859233fc03abaab220d650bf92')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '66f9c3b99371f2874df7dd3184f8b604')" ] } } \ No newline at end of file diff --git a/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/25.json b/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/25.json new file mode 100644 index 0000000000..0893da53a5 --- /dev/null +++ b/FlowCrypt/schemas/com.flowcrypt.email.database.FlowCryptRoomDatabase/25.json @@ -0,0 +1,1000 @@ +{ + "formatVersion": 1, + "database": { + "version": 25, + "identityHash": "424a5613be14bb041d6a44a5f930b216", + "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_enable` 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_is_use_ssl_tls` INTEGER DEFAULT 0, `imap_is_use_starttls` INTEGER DEFAULT 0, `imap_auth_mechanisms` TEXT, `smtp_server` TEXT NOT NULL, `smtp_port` INTEGER DEFAULT 25, `smtp_is_use_ssl_tls` INTEGER DEFAULT 0, `smtp_is_use_starttls` INTEGER DEFAULT 0, `smtp_auth_mechanisms` TEXT, `smtp_is_use_custom_sign` INTEGER DEFAULT 0, `smtp_username` TEXT DEFAULT NULL, `smtp_password` TEXT DEFAULT NULL, `ic_contacts_loaded` INTEGER DEFAULT 0, `is_show_only_encrypted` INTEGER DEFAULT 0, `uuid` TEXT DEFAULT NULL, `domain_rules` TEXT DEFAULT NULL, `is_restore_access_required` INTEGER DEFAULT 0, `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_enable", + "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": "imapIsUseSslTls", + "columnName": "imap_is_use_ssl_tls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "imapIsUseStarttls", + "columnName": "imap_is_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": "smtpIsUseSslTls", + "columnName": "smtp_is_use_ssl_tls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "smtpIsUseStarttls", + "columnName": "smtp_is_use_starttls", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "smtpAuthMechanisms", + "columnName": "smtp_auth_mechanisms", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "useCustomSignForSmtp", + "columnName": "smtp_is_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": "areContactsLoaded", + "columnName": "ic_contacts_loaded", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "isShowOnlyEncrypted", + "columnName": "is_show_only_encrypted", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "domainRules", + "columnName": "domain_rules", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isRestoreAccessRequired", + "columnName": "is_restore_access_required", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "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, `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": "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": "contacts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `email` TEXT NOT NULL, `name` TEXT DEFAULT NULL, `public_key` BLOB DEFAULT NULL, `has_pgp` INTEGER NOT NULL, `client` TEXT DEFAULT NULL, `attested` INTEGER DEFAULT NULL, `fingerprint` TEXT DEFAULT NULL, `long_id` TEXT DEFAULT NULL, `keywords` 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": "publicKey", + "columnName": "public_key", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "hasPgp", + "columnName": "has_pgp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "client", + "columnName": "client", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "attested", + "columnName": "attested", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "longId", + "columnName": "long_id", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "lastUse", + "columnName": "last_use", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "has_pgp_in_contacts", + "unique": false, + "columnNames": [ + "has_pgp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `has_pgp_in_contacts` ON `${TABLE_NAME}` (`has_pgp`)" + }, + { + "name": "name_in_contacts", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `name_in_contacts` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "long_id_in_contacts", + "unique": false, + "columnNames": [ + "long_id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `long_id_in_contacts` ON `${TABLE_NAME}` (`long_id`)" + }, + { + "name": "last_use_in_contacts", + "unique": false, + "columnNames": [ + "last_use" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `last_use_in_contacts` ON `${TABLE_NAME}` (`last_use`)" + }, + { + "name": "email_in_contacts", + "unique": true, + "columnNames": [ + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `email_in_contacts` 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": [] + } + ], + "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, '424a5613be14bb041d6a44a5f930b216')" + ] + } +} \ 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 ebad995011..cfc38ac569 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/database/MigrationTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/database/MigrationTest.kt @@ -30,11 +30,13 @@ import java.io.IOException class MigrationTest { // Array of all migrations which we are going to test private val arrayOfMigrations = arrayOf( - FlowCryptRoomDatabase.MIGRATION_19_20, - FlowCryptRoomDatabase.MIGRATION_20_21, - FlowCryptRoomDatabase.MIGRATION_21_22, - FlowCryptRoomDatabase.MIGRATION_22_23, - FlowCryptRoomDatabase.MIGRATION_23_24) + FlowCryptRoomDatabase.MIGRATION_19_20, + FlowCryptRoomDatabase.MIGRATION_20_21, + FlowCryptRoomDatabase.MIGRATION_21_22, + FlowCryptRoomDatabase.MIGRATION_22_23, + FlowCryptRoomDatabase.MIGRATION_23_24, + FlowCryptRoomDatabase.MIGRATION_24_25 + ) @get:Rule val migrationTestHelper: MigrationTestHelper = MigrationTestHelper( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/rules/AddPrivateKeyToDatabaseRule.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/rules/AddPrivateKeyToDatabaseRule.kt index 1f2542bd95..3c32ff7d7f 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/rules/AddPrivateKeyToDatabaseRule.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/rules/AddPrivateKeyToDatabaseRule.kt @@ -6,9 +6,9 @@ package com.flowcrypt.email.rules import com.flowcrypt.email.TestConstants -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.AccountDaoManager import com.flowcrypt.email.util.PrivateKeysManager @@ -24,19 +24,19 @@ import org.junit.runners.model.Statement class AddPrivateKeyToDatabaseRule(val accountEntity: AccountEntity, val keyPath: String, val passphrase: String, - val type: KeyDetails.Type) : BaseRule() { + val sourceType: KeyImportDetails.SourceType) : BaseRule() { - lateinit var nodeKeyDetails: NodeKeyDetails + lateinit var pgpKeyDetails: PgpKeyDetails private set constructor() : this(AccountDaoManager.getDefaultAccountDao(), "pgp/default@flowcrypt.test_fisrtKey_prv_strong.asc", - TestConstants.DEFAULT_STRONG_PASSWORD, KeyDetails.Type.EMAIL) + TestConstants.DEFAULT_STRONG_PASSWORD, KeyImportDetails.SourceType.EMAIL) override fun apply(base: Statement, description: Description): Statement { return object : Statement() { override fun evaluate() { - nodeKeyDetails = PgpKey.parseKeys(context.assets.open(keyPath)).toNodeKeyDetailsList().first() - PrivateKeysManager.saveKeyToDatabase(accountEntity, nodeKeyDetails, passphrase, type) + pgpKeyDetails = PgpKey.parseKeys(context.assets.open(keyPath)).toPgpKeyDetailsList().first() + PrivateKeysManager.saveKeyToDatabase(accountEntity, pgpKeyDetails, passphrase, sourceType) base.evaluate() } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/AddOtherAccountFragmentTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/AddOtherAccountFragmentTest.kt index b4c97a4d0e..a2a7c12dd2 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/AddOtherAccountFragmentTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/AddOtherAccountFragmentTest.kt @@ -35,10 +35,11 @@ import com.flowcrypt.email.api.email.model.AuthCredentials import com.flowcrypt.email.api.email.model.SecurityType import com.flowcrypt.email.base.BaseTest import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.junit.annotations.DependsOnMailServer import com.flowcrypt.email.junit.annotations.NotReadyForCI import com.flowcrypt.email.matchers.CustomMatchers.Companion.withSecurityTypeOption -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule @@ -337,18 +338,19 @@ class AddOtherAccountFragmentTest : BaseTest() { @Test @DependsOnMailServer fun testWhenNoAccountsAndHasBackup() { - val prvKey = PrivateKeysManager.getNodeKeyDetailsFromAssets( - "pgp/default@flowcrypt.test_fisrtKey_prv_default.asc") + val prvKey = PrivateKeysManager + .getNodeKeyDetailsFromAssets("pgp/default@flowcrypt.test_fisrtKey_prv_default.asc") + .copy(passphraseType = KeyEntity.PassphraseType.DATABASE) intending(hasComponent(ComponentName(getTargetContext(), CheckKeysActivity::class.java))) - .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().apply { - putExtra(CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS, ArrayList(listOf(prvKey))) - })) + .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().apply { + putExtra(CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS, ArrayList(listOf(prvKey))) + })) val creds = AuthCredentialsManager.getAuthCredentials() onView(withId(R.id.editTextEmail)) - .perform(clearText(), typeText(creds.email), closeSoftKeyboard()) + .perform(clearText(), typeText(creds.email), closeSoftKeyboard()) onView(withId(R.id.editTextPassword)) .perform(clearText(), typeText(creds.password), closeSoftKeyboard()) onView(withId(R.id.buttonTryToConnect)) @@ -366,7 +368,7 @@ class AddOtherAccountFragmentTest : BaseTest() { accountEntity = user, keyPath = "pgp/key_testing@flowcrypt.test_keyB_default.asc", passphrase = TestConstants.DEFAULT_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) testWhenNoAccountsAndHasBackup() @@ -380,7 +382,7 @@ class AddOtherAccountFragmentTest : BaseTest() { accountEntity = existedUser, keyPath = "pgp/default@flowcrypt.test_fisrtKey_prv_default.asc", passphrase = TestConstants.DEFAULT_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) onView(withId(R.id.editTextEmail)) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/BackupKeysActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/BackupKeysActivityTest.kt index 358e2323ca..1b4b96b3cb 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/BackupKeysActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/BackupKeysActivityTest.kt @@ -30,7 +30,7 @@ import com.flowcrypt.email.R import com.flowcrypt.email.TestConstants import com.flowcrypt.email.base.BaseTest import com.flowcrypt.email.junit.annotations.DependsOnMailServer -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.AddAccountToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.RetryRule @@ -255,7 +255,7 @@ class BackupKeysActivityTest : BaseTest() { accountEntity = addAccountToDatabaseRule.account, keyPath = "pgp/default@flowcrypt.test_fisrtKey_prv_default.asc", passphrase = TestConstants.DEFAULT_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) } @@ -264,7 +264,7 @@ class BackupKeysActivityTest : BaseTest() { accountEntity = addAccountToDatabaseRule.account, keyPath = "pgp/default@flowcrypt.test_fisrtKey_prv_strong.asc", passphrase = TestConstants.DEFAULT_STRONG_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) } @@ -273,7 +273,7 @@ class BackupKeysActivityTest : BaseTest() { accountEntity = addAccountToDatabaseRule.account, keyPath = TestConstants.DEFAULT_SECOND_KEY_PRV_STRONG, passphrase = TestConstants.DEFAULT_STRONG_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) } @@ -282,7 +282,7 @@ class BackupKeysActivityTest : BaseTest() { accountEntity = addAccountToDatabaseRule.account, keyPath = "pgp/default@flowcrypt.test_secondKey_prv_strong_second.asc", passphrase = TestConstants.DEFAULT_SECOND_STRONG_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityTestMultiBackups.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityTestMultiBackups.kt index 85ee1d6f18..6ba332e1f2 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityTestMultiBackups.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityTestMultiBackups.kt @@ -22,7 +22,7 @@ import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.TestConstants import com.flowcrypt.email.base.BaseTest -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule @@ -320,9 +320,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleSingleBinaryKeyFromFile() { val keysPaths = arrayOf("pgp/keys/single_prv_key_binary.key") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(1, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(1, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -331,9 +331,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyBinaryKeysFromFile() { val keysPaths = arrayOf("pgp/keys/10_prv_keys_binary.key") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(10, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(10, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -342,9 +342,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyBinaryKeysPrvAndPubFromFile() { val keysPaths = arrayOf("pgp/keys/10_prv_and_pub_keys_binary.key") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(5, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(5, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -353,9 +353,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleSingleArmoredKeyFromFile() { val keysPaths = arrayOf("pgp/keys/single_prv_key_armored.asc") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(1, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(1, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -365,9 +365,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyArmoredKeysFromFileOwnHeader() { val keysPaths = arrayOf("pgp/keys/10_prv_keys_armored_own_header.asc") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(10, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(10, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -377,9 +377,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyArmoredKeysFromFileSingleHeader() { val keysPaths = arrayOf("pgp/keys/10_prv_keys_armored_single_header.asc") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(10, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(10, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -390,9 +390,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyArmoredKeysFromFileOwnWithSingleHeader() { val keysPaths = arrayOf("pgp/keys/10_prv_keys_armored_own_with_single_header.asc") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(10, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(10, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -402,9 +402,9 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyArmoredPrvPubKeysFromFileOwnHeader() { val keysPaths = arrayOf("pgp/keys/10_prv_and_pub_keys_armored_own_header.asc") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(6, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(6, keysPaths, KeyImportDetails.SourceType.FILE) } /** @@ -415,13 +415,13 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { @Test fun testSubTitleManyArmoredPrvPubKeysFromFileOwnWithSingleHeaderdd() { val keysPaths = arrayOf("pgp/keys/10_prv_and_pub_keys_armored_own_with_single_header.asc") - launchActivity(keysPaths, KeyDetails.Type.FILE) + launchActivity(keysPaths, KeyImportDetails.SourceType.FILE) - checkKeysTitleAtStart(4, keysPaths, KeyDetails.Type.FILE) + checkKeysTitleAtStart(4, keysPaths, KeyImportDetails.SourceType.FILE) } - private fun launchActivity(keysPaths: Array, type: KeyDetails.Type = KeyDetails.Type.EMAIL) { - activeActivityRule.launch(getStartCheckKeysActivityIntent(keysPaths, type)) + private fun launchActivity(keysPaths: Array, sourceType: KeyImportDetails.SourceType = KeyImportDetails.SourceType.EMAIL) { + activeActivityRule.launch(getStartCheckKeysActivityIntent(keysPaths, sourceType)) registerAllIdlingResources() } @@ -452,10 +452,10 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { } private fun checkKeysTitleAtStart(expectedKeyCount: Int, keysPaths: Array? = null, - type: KeyDetails.Type = KeyDetails.Type.EMAIL) { + sourceType: KeyImportDetails.SourceType = KeyImportDetails.SourceType.EMAIL) { val text: String - when (type) { - KeyDetails.Type.FILE -> { + when (sourceType) { + KeyImportDetails.SourceType.FILE -> { assert(keysPaths?.size == 1) val fileName = FilenameUtils.getName(keysPaths?.first()) text = getQuantityString(R.plurals.file_contains_some_amount_of_keys, @@ -472,12 +472,12 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { .check(matches(withText(text))) } - private fun getStartCheckKeysActivityIntent(keysPaths: Array, type: KeyDetails.Type = KeyDetails.Type.EMAIL): Intent { + private fun getStartCheckKeysActivityIntent(keysPaths: Array, sourceType: KeyImportDetails.SourceType = KeyImportDetails.SourceType.EMAIL): Intent { val keyDetailsList = PrivateKeysManager.getKeysFromAssets(keysPaths, true) val bottomTitle: String - when (type) { - KeyDetails.Type.FILE -> { + when (sourceType) { + KeyImportDetails.SourceType.FILE -> { assert(keysPaths.size == 1) val fileName = FilenameUtils.getName(keysPaths.first()) bottomTitle = getQuantityString(R.plurals.file_contains_some_amount_of_keys, @@ -492,11 +492,11 @@ class CheckKeysActivityTestMultiBackups : BaseTest() { return CheckKeysActivity.newIntent( context = getTargetContext(), privateKeys = keyDetailsList, - type = type, + sourceType = sourceType, subTitle = bottomTitle, positiveBtnTitle = getTargetContext().getString(R.string.continue_), negativeBtnTitle = getTargetContext().getString(R.string.choose_another_key), - isExtraImportOpt = type != KeyDetails.Type.EMAIL + isExtraImportOpt = sourceType != KeyImportDetails.SourceType.EMAIL ) } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithExistingKeysTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithExistingKeysTest.kt index 7524b13c15..c33f584abb 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithExistingKeysTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithExistingKeysTest.kt @@ -19,7 +19,7 @@ import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.TestConstants import com.flowcrypt.email.base.BaseTest -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.AddAccountToDatabaseRule import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule @@ -47,7 +47,7 @@ class CheckKeysActivityWithExistingKeysTest : BaseTest() { override val activityScenarioRule = activityScenarioRule( CheckKeysActivity.newIntent(getTargetContext(), privateKeys = privateKeys, - type = KeyDetails.Type.EMAIL, + sourceType = KeyImportDetails.SourceType.EMAIL, subTitle = getQuantityString(R.plurals.found_backup_of_your_account_key, privateKeys.size, privateKeys.size), positiveBtnTitle = getTargetContext().getString(R.string.continue_), @@ -64,7 +64,7 @@ class CheckKeysActivityWithExistingKeysTest : BaseTest() { accountEntity = addAccountToDatabaseRule.account, keyPath = "pgp/not_attester_user@flowcrypt.test_prv_default.asc", passphrase = TestConstants.DEFAULT_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL )) .around(RetryRule.DEFAULT) .around(activityScenarioRule) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithoutExistingKeysTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithoutExistingKeysTest.kt index 3792c84329..a4c3022c85 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithoutExistingKeysTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CheckKeysActivityWithoutExistingKeysTest.kt @@ -18,7 +18,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.base.BaseTest -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule @@ -45,7 +45,7 @@ class CheckKeysActivityWithoutExistingKeysTest : BaseTest() { override val activityScenarioRule = activityScenarioRule( CheckKeysActivity.newIntent(getTargetContext(), privateKeys, - KeyDetails.Type.EMAIL, + KeyImportDetails.SourceType.EMAIL, getQuantityString(R.plurals.found_backup_of_your_account_key, privateKeys.size, privateKeys.size), getTargetContext().getString(R.string.continue_), diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ContactsSettingsActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ContactsSettingsActivityTest.kt index 9a10ee6ebc..44485395bc 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ContactsSettingsActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ContactsSettingsActivityTest.kt @@ -87,7 +87,7 @@ class ContactsSettingsActivityTest : BaseTest() { private fun addContactsToDatabase() { for (email in EMAILS) { - val pgpContact = PgpContact(email, null, "", true, null, null, null, 0) + val pgpContact = PgpContact(email, null, "", true, null, null, 0) FlowCryptRoomDatabase.getDatabase(getTargetContext()).contactsDao().insert(pgpContact.toContactEntity()) } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt index f8c54f2fb3..257e86353a 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/CreateMessageActivityTest.kt @@ -45,7 +45,7 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.expiration import com.flowcrypt.email.matchers.CustomMatchers.Companion.withAppBarLayoutBackgroundColor import com.flowcrypt.email.matchers.CustomMatchers.Companion.withChipsBackgroundColor -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.MessageEncryptionType import com.flowcrypt.email.model.MessageType import com.flowcrypt.email.model.PgpContact @@ -439,7 +439,7 @@ class CreateMessageActivityTest : BaseTest() { .check(matches(isDisplayed())) .perform(click()) - val att = EmailUtil.genAttInfoFromPubKey(addPrivateKeyToDatabaseRule.nodeKeyDetails) + val att = EmailUtil.genAttInfoFromPubKey(addPrivateKeyToDatabaseRule.pgpKeyDetails) onView(withText(att?.name)) .check(matches(isDisplayed())) @@ -450,7 +450,7 @@ class CreateMessageActivityTest : BaseTest() { val secondKeyDetails = PrivateKeysManager.getNodeKeyDetailsFromAssets(TestConstants.DEFAULT_SECOND_KEY_PRV_STRONG) PrivateKeysManager.saveKeyToDatabase(addAccountToDatabaseRule.account, secondKeyDetails, - TestConstants.DEFAULT_STRONG_PASSWORD, KeyDetails.Type.EMAIL) + TestConstants.DEFAULT_STRONG_PASSWORD, KeyImportDetails.SourceType.EMAIL) val att = EmailUtil.genAttInfoFromPubKey(secondKeyDetails) activeActivityRule.launch(intent) @@ -480,7 +480,7 @@ class CreateMessageActivityTest : BaseTest() { val keyDetails = PrivateKeysManager.getNodeKeyDetailsFromAssets( "pgp/key_testing@flowcrypt.test_keyB_default.asc") PrivateKeysManager.saveKeyToDatabase(addAccountToDatabaseRule.account, keyDetails, - TestConstants.DEFAULT_PASSWORD, KeyDetails.Type.EMAIL) + TestConstants.DEFAULT_PASSWORD, KeyImportDetails.SourceType.EMAIL) activeActivityRule.launch(intent) registerAllIdlingResources() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityFromSettingsTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityFromSettingsTest.kt index ea5458f2d9..194b07ce82 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityFromSettingsTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityFromSettingsTest.kt @@ -25,14 +25,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.TestConstants -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.base.BaseTest +import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.junit.annotations.DependsOnMailServer import com.flowcrypt.email.junit.annotations.NotReadyForCI import com.flowcrypt.email.rules.AddAccountToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.util.PrivateKeysManager import com.flowcrypt.email.util.TestGeneralUtil import org.hamcrest.Matchers.`is` @@ -145,7 +146,7 @@ class ImportPrivateKeyActivityFromSettingsTest : BaseTest() { private fun useIntentionFromRunCheckKeysActivity() { val intent = Intent() - val list: ArrayList = ArrayList() + val list: ArrayList = ArrayList() list.add(keyDetails) intent.putExtra(CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS, list) @@ -164,7 +165,8 @@ class ImportPrivateKeyActivityFromSettingsTest : BaseTest() { @BeforeClass @JvmStatic fun createResources() { - keyDetails.passphrase = TestConstants.DEFAULT_STRONG_PASSWORD + keyDetails.tempPassphrase = TestConstants.DEFAULT_STRONG_PASSWORD.toCharArray() + keyDetails.passphraseType = KeyEntity.PassphraseType.DATABASE privateKey = keyDetails.privateKey!! fileWithPrivateKey = TestGeneralUtil.createFileAndFillWithContent( TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER + "_sec.asc", privateKey) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityNoPubOrgRulesTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityNoPubOrgRulesTest.kt index 584d9f7f36..5e6e088b14 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityNoPubOrgRulesTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivityNoPubOrgRulesTest.kt @@ -24,12 +24,12 @@ import com.flowcrypt.email.TestConstants import com.flowcrypt.email.api.retrofit.ApiHelper import com.flowcrypt.email.api.retrofit.request.model.InitialLegacySubmitModel import com.flowcrypt.email.api.retrofit.response.attester.InitialLegacySubmitResponse -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.base.BaseTest import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.FlowCryptMockWebServerRule import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.util.AccountDaoManager import com.flowcrypt.email.util.PrivateKeysManager import com.flowcrypt.email.util.TestGeneralUtil @@ -88,7 +88,7 @@ class ImportPrivateKeyActivityNoPubOrgRulesTest : BaseTest() { private fun useIntentionFromRunCheckKeysActivity() { val intent = Intent() - val list: ArrayList = ArrayList() + val list: ArrayList = ArrayList() list.add(keyDetails) intent.putExtra(CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS, list) @@ -106,7 +106,7 @@ class ImportPrivateKeyActivityNoPubOrgRulesTest : BaseTest() { @BeforeClass @JvmStatic fun createResources() { - keyDetails.passphrase = TestConstants.DEFAULT_PASSWORD + keyDetails.tempPassphrase = TestConstants.DEFAULT_PASSWORD.toCharArray() privateKey = keyDetails.privateKey!! } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/KeysSettingsActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/KeysSettingsActivityTest.kt index e57f9b7a37..f1eaf9dede 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/KeysSettingsActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/KeysSettingsActivityTest.kt @@ -39,7 +39,7 @@ import com.flowcrypt.email.base.BaseTest import com.flowcrypt.email.junit.annotations.NotReadyForCI import com.flowcrypt.email.matchers.CustomMatchers.Companion.withEmptyRecyclerView import com.flowcrypt.email.matchers.CustomMatchers.Companion.withRecyclerViewItemCount -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.AddAccountToDatabaseRule import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule @@ -60,7 +60,6 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith import java.io.File import java.util.* -import java.util.concurrent.TimeUnit /** * @author Denis Bondarenko @@ -102,9 +101,9 @@ class KeysSettingsActivityTest : BaseTest() { val details = PrivateKeysManager.getNodeKeyDetailsFromAssets("pgp/default@flowcrypt.test_secondKey_prv_default.asc") PrivateKeysManager.saveKeyToDatabase( accountEntity = addAccountToDatabaseRule.account, - nodeKeyDetails = details, + pgpKeyDetails = details, passphrase = TestConstants.DEFAULT_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) onView(withId(R.id.floatActionButtonAddKey)) @@ -131,7 +130,7 @@ class KeysSettingsActivityTest : BaseTest() { @Test fun testKeyDetailsShowPubKey() { selectFirstKey() - val keyDetails = addPrivateKeyToDatabaseRule.nodeKeyDetails + val keyDetails = addPrivateKeyToDatabaseRule.pgpKeyDetails onView(withId(R.id.btnShowPubKey)) .check(matches(isDisplayed())) .perform(click()) @@ -141,12 +140,12 @@ class KeysSettingsActivityTest : BaseTest() { @Test fun testKeyDetailsCopyToClipBoard() { selectFirstKey() - val details = addPrivateKeyToDatabaseRule.nodeKeyDetails + val details = addPrivateKeyToDatabaseRule.pgpKeyDetails onView(withId(R.id.btnCopyToClipboard)) .check(matches(isDisplayed())) .perform(click()) isToastDisplayed(decorView, getResString(R.string.copied)) - UiThreadStatement.runOnUiThread { checkClipboardText(details.publicKey ?: "") } + UiThreadStatement.runOnUiThread { checkClipboardText(details.publicKey) } } @Test @@ -162,19 +161,15 @@ class KeysSettingsActivityTest : BaseTest() { @Test fun testKeyDetailsCheckDetails() { selectFirstKey() - val details = addPrivateKeyToDatabaseRule.nodeKeyDetails + val details = addPrivateKeyToDatabaseRule.pgpKeyDetails onView(withId(R.id.textViewFingerprint)) .check(matches(withText(getHtmlString(getResString(R.string.template_fingerprint, GeneralUtil.doSectionsInText(" ", details.fingerprint, 4)!!))))) - onView(withId(R.id.textViewLongId)).check( - matches(withText(getResString(R.string.template_longid, details.longId ?: "")))) - onView(withId(R.id.textViewDate)) .check(matches(withText(getHtmlString(getResString(R.string.template_date, - DateFormat.getMediumDateFormat(getTargetContext()).format( - Date(TimeUnit.MILLISECONDS.convert(details.created, TimeUnit.SECONDS)))))))) + DateFormat.getMediumDateFormat(getTargetContext()).format(Date(details.created))))))) onView(withId(R.id.tVPassPhraseVerification)) .check(matches(withText(getResString(R.string.stored_pass_phrase_matched)))) @@ -195,9 +190,9 @@ class KeysSettingsActivityTest : BaseTest() { "pgp/default@flowcrypt.test_secondKey_prv_default.asc") PrivateKeysManager.saveKeyToDatabase( accountEntity = addAccountToDatabaseRule.account, - nodeKeyDetails = details, + pgpKeyDetails = details, passphrase = "wrong passphrase", - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) onView(withId(R.id.recyclerViewKeys)) @@ -211,10 +206,11 @@ class KeysSettingsActivityTest : BaseTest() { @Test fun testKeyDetailsSavePubKeyToFileWhenFileIsNotExist() { selectFirstKey() - val details = addPrivateKeyToDatabaseRule.nodeKeyDetails + val details = addPrivateKeyToDatabaseRule.pgpKeyDetails val file = - File(getTargetContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "0x" + details.longId + ".asc") + File(getTargetContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), + "0x" + details.fingerprint + ".asc") if (file.exists()) { file.delete() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt index dea8fa4f7a..828f09ddfd 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/MessageDetailsActivityTest.kt @@ -51,7 +51,7 @@ import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.junit.annotations.NotReadyForCI import com.flowcrypt.email.matchers.CustomMatchers import com.flowcrypt.email.matchers.CustomMatchers.Companion.withDrawable -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.rules.AddAccountToDatabaseRule import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule import com.flowcrypt.email.rules.ClearAppSettingsRule @@ -202,7 +202,7 @@ class MessageDetailsActivityTest : BaseTest() { accountEntity = addAccountToDatabaseRule.account, keyPath = TestConstants.DEFAULT_SECOND_KEY_PRV_STRONG, passphrase = TestConstants.DEFAULT_STRONG_PASSWORD, - type = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL ) val incomingMsgInfoFixed = @@ -280,7 +280,7 @@ class MessageDetailsActivityTest : BaseTest() { PrivateKeysManager.saveKeyFromAssetsToDatabase(addAccountToDatabaseRule .account, TestConstants.DEFAULT_SECOND_KEY_PRV_STRONG, - TestConstants.DEFAULT_STRONG_PASSWORD, KeyDetails.Type.EMAIL) + TestConstants.DEFAULT_STRONG_PASSWORD, KeyImportDetails.SourceType.EMAIL) val msg = getQuantityString(R.plurals diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PreviewImportPgpContactActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PreviewImportPgpContactActivityTest.kt index 0e1920cdad..d4442917f6 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PreviewImportPgpContactActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PreviewImportPgpContactActivityTest.kt @@ -71,7 +71,7 @@ class PreviewImportPgpContactActivityTest : BaseTest() { @Test fun testIsDisplayedSingleItem() { val pgpContact = PgpContact("default@flowcrypt.test", null, - singlePublicKeyForUnsavedContact, true, null, null, null, 0) + singlePublicKeyForUnsavedContact, true, null, null, 0) FlowCryptRoomDatabase.getDatabase(getTargetContext()).contactsDao().insert(pgpContact.toContactEntity()) activeActivityRule.launch(PreviewImportPgpContactActivity.newIntent(getTargetContext(), singlePublicKeyForUnsavedContact)) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PublicKeyDetailsFragmentTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PublicKeyDetailsFragmentTest.kt index e4b0cde7ca..96eca34e5c 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PublicKeyDetailsFragmentTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/PublicKeyDetailsFragmentTest.kt @@ -56,7 +56,6 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith import java.io.File import java.util.* -import java.util.concurrent.TimeUnit /** * @author Denis Bondarenko @@ -77,7 +76,7 @@ class PublicKeyDetailsFragmentTest : BaseTest() { .outerRule(ClearAppSettingsRule()) .around(AddAccountToDatabaseRule()) .around(AddContactsToDatabaseRule(listOf(PgpContact(EMAIL_DENBOND7, USER_DENBOND7, - keyDetails.publicKey, true, null, null, null, 0)))) + keyDetails.publicKey, true, null, null, 0)))) .around(RetryRule.DEFAULT) .around(activityScenarioRule) .around(ScreenshotTestRule()) @@ -93,22 +92,22 @@ class PublicKeyDetailsFragmentTest : BaseTest() { fun testPubKeyDetails() { chooseContact() - keyDetails.users?.forEachIndexed { index, s -> + keyDetails.users.forEachIndexed { index, s -> onView(withText(getResString(R.string.template_user, index + 1, s))) .check(matches(isDisplayed())) } - keyDetails.ids?.forEachIndexed { index, s -> - onView(withText(getResString(R.string.template_long_id, index + 1, s.longId!!))) + keyDetails.ids.forEachIndexed { index, s -> + onView(withText(getResString(R.string.template_fingerprint_2, index + 1, s.fingerprint))) .check(matches(isDisplayed())) } onView(withId(R.id.textViewAlgorithm)) - .check(matches(withText(getResString(R.string.template_algorithm, keyDetails.algo!!.algorithm!!)))) + .check(matches(withText(getResString(R.string.template_algorithm, keyDetails.algo.algorithm!!)))) onView(withId(R.id.textViewCreated)) .check(matches(withText(getResString(R.string.template_created, DateFormat.getMediumDateFormat(getTargetContext()).format( - Date(TimeUnit.MILLISECONDS.convert(keyDetails.created, TimeUnit.SECONDS))))))) + Date(keyDetails.created)))))) } @Test @@ -129,7 +128,7 @@ class PublicKeyDetailsFragmentTest : BaseTest() { chooseContact() val sanitizedEmail = EMAIL_DENBOND7.replace("[^a-z0-9]".toRegex(), "") - val fileName = "0x" + keyDetails.longId + "-" + sanitizedEmail + "-publickey" + ".asc" + val fileName = "0x" + keyDetails.fingerprint + "-" + sanitizedEmail + "-publickey" + ".asc" val file = File(getTargetContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SearchMessagesActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SearchMessagesActivityTest.kt index e2855ace66..76405ba901 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SearchMessagesActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SearchMessagesActivityTest.kt @@ -115,7 +115,7 @@ class SearchMessagesActivityTest : BaseEmailListActivityTest() { //todo-denbond7 Need to improve this code Thread.sleep(2000) onView(withId(R.id.rVMsgs)) - .check(matches(withRecyclerViewItemCount(4))).check(matches(isDisplayed())) + .check(matches(withRecyclerViewItemCount(3))).check(matches(isDisplayed())) } @Test diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SelectContactsActivityTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SelectContactsActivityTest.kt index f78b43b3e5..b620f89424 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SelectContactsActivityTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/activity/SelectContactsActivityTest.kt @@ -169,9 +169,9 @@ class SelectContactsActivityTest : BaseTest() { for (i in EMAILS.indices) { val email = EMAILS[i] val pgpContact = if (i % 2 == 0) { - PgpContact(email, getUserName(email), "publicKey", true, null, null, null, 0) + PgpContact(email, getUserName(email), "publicKey", true, null, null, 0) } else { - PgpContact(email, null, "publicKey", true, null, null, null, 0) + PgpContact(email, null, "publicKey", true, null, null, 0) } CONTACTS.add(pgpContact) } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/PrivateKeysManager.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/PrivateKeysManager.kt index a3d3fcd6fb..97f0d2848c 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/PrivateKeysManager.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/PrivateKeysManager.kt @@ -6,11 +6,12 @@ package com.flowcrypt.email.util import androidx.test.platform.app.InstrumentationRegistry -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.KeyStoreCryptoManager +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import org.pgpainless.key.collection.PGPKeyRingCollection import java.util.* @@ -25,46 +26,71 @@ import java.util.* */ class PrivateKeysManager { companion object { - fun saveKeyFromAssetsToDatabase(accountEntity: AccountEntity, keyPath: String, - passphrase: String, type: KeyDetails.Type) { + fun saveKeyFromAssetsToDatabase( + accountEntity: AccountEntity, keyPath: String, + passphrase: String, sourceType: KeyImportDetails.SourceType + ) { val nodeKeyDetails = getNodeKeyDetailsFromAssets(keyPath) - saveKeyToDatabase(accountEntity, nodeKeyDetails, passphrase, type) + saveKeyToDatabase(accountEntity, nodeKeyDetails, passphrase, sourceType) } - fun saveKeyToDatabase(accountEntity: AccountEntity, nodeKeyDetails: NodeKeyDetails, - passphrase: String, type: KeyDetails.Type) { + fun saveKeyToDatabase( + accountEntity: AccountEntity, pgpKeyDetails: PgpKeyDetails, + passphrase: String, sourceType: KeyImportDetails.SourceType + ) { val context = InstrumentationRegistry.getInstrumentation().targetContext val roomDatabase = FlowCryptRoomDatabase.getDatabase(context) - val keyEntity = nodeKeyDetails.toKeyEntity(accountEntity).copy( - source = type.toString(), - privateKey = KeyStoreCryptoManager.encrypt(nodeKeyDetails.privateKey).toByteArray(), - passphrase = KeyStoreCryptoManager.encrypt(passphrase)) - roomDatabase.keysDao().insertWithReplace(keyEntity) + val keyEntity = pgpKeyDetails + .copy(passphraseType = KeyEntity.PassphraseType.DATABASE) + .toKeyEntity(accountEntity) + .copy( + source = sourceType.toString(), + privateKey = KeyStoreCryptoManager.encrypt(pgpKeyDetails.privateKey).toByteArray(), + storedPassphrase = KeyStoreCryptoManager.encrypt(passphrase) + ) + val existingKey = roomDatabase.keysDao().getKeyByAccountAndFingerprint( + accountEntity.email, + pgpKeyDetails.fingerprint + ) + existingKey?.let { roomDatabase.keysDao().delete(it) } + roomDatabase.keysDao().insert(keyEntity) // Added timeout for a better sync between threads. Thread.sleep(3000) } - fun getNodeKeyDetailsFromAssets(assetsPath: String, onlyPrivate: Boolean = false): NodeKeyDetails { + fun getNodeKeyDetailsFromAssets( + assetsPath: String, + onlyPrivate: Boolean = false + ): PgpKeyDetails { return getNodeKeyDetailsListFromAssets(assetsPath, onlyPrivate).first() } - fun getNodeKeyDetailsListFromAssets(assetsPath: String, onlyPrivate: Boolean = false): List { + fun getNodeKeyDetailsListFromAssets( + assetsPath: String, + onlyPrivate: Boolean = false + ): List { val parsedCollections = - PgpKey.parseKeys(TestGeneralUtil.readFileFromAssetsAsStream(assetsPath)) + PgpKey.parseKeys(TestGeneralUtil.readFileFromAssetsAsStream(assetsPath)) if (onlyPrivate) { val onlyPrivateKeysCollection = PgpKey.ParseKeyResult( - PGPKeyRingCollection(parsedCollections.pgpKeyRingCollection - .pgpSecretKeyRingCollection.keyRings.asSequence().toList(), false)) + PGPKeyRingCollection( + parsedCollections.pgpKeyRingCollection + .pgpSecretKeyRingCollection.keyRings.asSequence().toList(), false + ) + ) - return onlyPrivateKeysCollection.toNodeKeyDetailsList() + return onlyPrivateKeysCollection.toPgpKeyDetailsList() } else { - return parsedCollections.toNodeKeyDetailsList() + return parsedCollections.toPgpKeyDetailsList() } } - fun getKeysFromAssets(keysPaths: Array, onlyPrivate: Boolean = false): ArrayList { - val privateKeys = ArrayList() + fun getKeysFromAssets( + keysPaths: Array, + onlyPrivate: Boolean = false + ): ArrayList { + val privateKeys = ArrayList() keysPaths.forEach { path -> privateKeys.addAll(getNodeKeyDetailsListFromAssets(path, onlyPrivate)) } @@ -75,8 +101,8 @@ class PrivateKeysManager { val nodeKeyDetails = getNodeKeyDetailsFromAssets(keyPath) val context = InstrumentationRegistry.getInstrumentation().targetContext val roomDatabase = FlowCryptRoomDatabase.getDatabase(context) - nodeKeyDetails.longId?.let { - roomDatabase.keysDao().deleteByAccountAndLongId(accountEntity.email, it) + nodeKeyDetails.fingerprint.let { + roomDatabase.keysDao().deleteByAccountAndFingerprint(accountEntity.email, it) } // Added timeout for a better sync between threads. diff --git a/FlowCrypt/src/main/AndroidManifest.xml b/FlowCrypt/src/main/AndroidManifest.xml index 39c1087bdf..fcfa60f0df 100644 --- a/FlowCrypt/src/main/AndroidManifest.xml +++ b/FlowCrypt/src/main/AndroidManifest.xml @@ -23,6 +23,9 @@ + + + + = withContext(Dispatchers.IO) { + suspend fun getPrivateKeyBackups(context: Context, account: AccountEntity): List = withContext(Dispatchers.IO) { try { - val list = mutableListOf() + val list = mutableListOf() val searchQuery = EmailUtil.getGmailBackupSearchQuery(account.email) val gmailApiService = generateGmailApiService(context, account) @@ -670,7 +670,7 @@ class GmailApiHelper { } try { - list.addAll(PgpKey.parseKeys(backup).toNodeKeyDetailsList()) + list.addAll(PgpKey.parseKeys(backup).toPgpKeyDetailsList()) } catch (e: NodeException) { e.printStackTrace() ExceptionUtil.handleError(e) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt index f2e6bc587e..855530875c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/node/RequestsManager.kt @@ -9,10 +9,8 @@ import android.content.Context import android.net.Uri import android.os.AsyncTask import androidx.lifecycle.LiveData -import com.flowcrypt.email.api.retrofit.request.node.DecryptFileRequest import com.flowcrypt.email.api.retrofit.request.node.NodeRequest import com.flowcrypt.email.api.retrofit.request.node.NodeRequestWrapper -import com.flowcrypt.email.api.retrofit.request.node.ParseDecryptMsgRequest import com.flowcrypt.email.api.retrofit.request.node.VersionRequest import com.flowcrypt.email.api.retrofit.response.node.BaseNodeResponse import com.flowcrypt.email.api.retrofit.response.node.NodeResponseWrapper @@ -22,8 +20,7 @@ import com.flowcrypt.email.jetpack.livedata.SingleLiveEvent /** * @author DenBond7 */ -//todo-denbond7 It's old code. Need to remove this and replace with a better approach. We use it -// only for debugging +//todo-denbond7 It's old code. We should drop this file @Deprecated("We should drop this file") object RequestsManager { private var data: SingleLiveEvent> = SingleLiveEvent() @@ -37,24 +34,20 @@ object RequestsManager { } fun encryptMsg(requestCode: Int, msg: String) { - //load(requestCode, EncryptMsgRequest(msg, listOf(*TestData.mixedPubKeys))) + } fun decryptMsg(requestCode: Int, data: ByteArray = ByteArray(0), uri: Uri? = null, prvKeys: Array, isEmail: Boolean = false) { - load(requestCode, ParseDecryptMsgRequest(data = data, uri = uri, keyEntities = listOf(*prvKeys), isEmail = isEmail)) + } fun encryptFile(requestCode: Int, data: ByteArray) { - //load(requestCode, EncryptFileRequest(data, "file.txt", listOf(*TestData.mixedPubKeys))) + } fun encryptFile(requestCode: Int, context: Context, fileUri: Uri) { - //load(requestCode, EncryptFileRequest(context, fileUri, "file.txt", listOf(*TestData.mixedPubKeys))) - } - fun decryptFile(requestCode: Int, encryptedData: ByteArray, prvKeys: Array) { - load(requestCode, DecryptFileRequest(encryptedData, listOf(*prvKeys))) } private fun load(requestCode: Int, nodeRequest: NodeRequest) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/DecryptFileRequest.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/DecryptFileRequest.kt index bcca42e485..9e9ca73336 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/DecryptFileRequest.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/DecryptFileRequest.kt @@ -7,7 +7,7 @@ package com.flowcrypt.email.api.retrofit.request.node import com.flowcrypt.email.api.retrofit.node.NodeService import com.flowcrypt.email.api.retrofit.request.model.node.PrivateKeyInfo -import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import com.google.gson.annotations.Expose import retrofit2.Response @@ -19,11 +19,16 @@ import retrofit2.Response * Time: 4:32 PM * E-mail: DenBond7@gmail.com */ -class DecryptFileRequest(override val data: ByteArray, keyEntities: List) : BaseNodeRequest() { +class DecryptFileRequest(override val data: ByteArray, keyEntities: List) : + BaseNodeRequest() { @Expose private val keys: List = keyEntities.map { - PrivateKeyInfo(it.privateKeyAsString, it.longId, it.passphrase) + PrivateKeyInfo( + privateKey = it.privateKey ?: throw IllegalArgumentException("Empty private key"), + longid = it.fingerprint, + passphrase = String(it.tempPassphrase ?: CharArray(0)) + ) } override val endpoint: String = "decryptFile" diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ParseDecryptMsgRequest.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ParseDecryptMsgRequest.kt index 0dd42fb632..8122d0fc51 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ParseDecryptMsgRequest.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/request/node/ParseDecryptMsgRequest.kt @@ -9,7 +9,7 @@ import android.content.Context import android.net.Uri import com.flowcrypt.email.api.retrofit.node.NodeService import com.flowcrypt.email.api.retrofit.request.model.node.PrivateKeyInfo -import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import com.google.gson.annotations.Expose import retrofit2.Response @@ -22,16 +22,21 @@ import retrofit2.Response * E-mail: DenBond7@gmail.com */ class ParseDecryptMsgRequest @JvmOverloads constructor( - context: Context? = null, - override val data: ByteArray = ByteArray(0), - override val uri: Uri? = null, - override val hasEncryptedDataInUri: Boolean = false, - keyEntities: List, - @Expose val isEmail: Boolean = false) : BaseNodeRequest(context, uri) { + context: Context? = null, + override val data: ByteArray = ByteArray(0), + override val uri: Uri? = null, + override val hasEncryptedDataInUri: Boolean = false, + pgpKeyDetailsList: List, + @Expose val isEmail: Boolean = false +) : BaseNodeRequest(context, uri) { @Expose - private val keys: List = keyEntities.map { - PrivateKeyInfo(it.privateKeyAsString, it.longId, it.passphrase) + private val keys: List = pgpKeyDetailsList.map { + PrivateKeyInfo( + privateKey = it.privateKey ?: throw IllegalArgumentException("Empty private key"), + longid = it.fingerprint, + passphrase = String(it.tempPassphrase ?: CharArray(0)) + ) } override val endpoint: String = "parseDecryptMsg" diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/LookUpPublicKeyInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/LookUpPublicKeyInfo.kt deleted file mode 100644 index 66a4984d0a..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/LookUpPublicKeyInfo.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.api.retrofit.response.model - -import android.os.Parcel -import android.os.Parcelable -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName -import java.util.* - -/** - * This POJO class describes information about a public key from the - * API ` https://flowcrypt.com/attester/lookup/` - * - * @author Denis Bondarenko - * Date: 05.05.2018 - * Time: 14:04 - * E-mail: DenBond7@gmail.com - */ -data class LookUpPublicKeyInfo constructor(@SerializedName("longid") @Expose val longId: String?, - @SerializedName("pubkey") @Expose val pubKey: String?, - @Expose val query: String?, - @Expose val attests: ArrayList?) : Parcelable { - constructor(source: Parcel) : this( - source.readString(), - source.readString(), - source.readString(), - source.createStringArrayList() - ) - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) = - with(dest) { - writeString(longId) - writeString(pubKey) - writeString(query) - writeStringList(attests) - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): LookUpPublicKeyInfo = LookUpPublicKeyInfo(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/DecryptError.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/DecryptError.kt index 3b7d40bc68..41a46433e9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/DecryptError.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/DecryptError.kt @@ -19,12 +19,10 @@ import com.google.gson.annotations.SerializedName */ data class DecryptError constructor(@Expose val isSuccess: Boolean, @SerializedName("error") @Expose val details: DecryptErrorDetails?, - @SerializedName("longids") @Expose val longids: Longids?, @Expose val isEncrypted: Boolean) : Parcelable { constructor(source: Parcel) : this( 1 == source.readInt(), source.readParcelable(DecryptErrorDetails::class.java.classLoader), - source.readParcelable(Longids::class.java.classLoader), 1 == source.readInt() ) @@ -36,7 +34,6 @@ data class DecryptError constructor(@Expose val isSuccess: Boolean, with(dest) { writeInt((if (isSuccess) 1 else 0)) writeParcelable(details, flags) - writeParcelable(longids, flags) writeInt((if (isEncrypted) 1 else 0)) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Longids.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Longids.kt deleted file mode 100644 index 41101ab82c..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Longids.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.api.retrofit.response.model.node - -import android.os.Parcel -import android.os.Parcelable - -import com.google.gson.annotations.Expose - -/** - * @author Denis Bondarenko - * Date: 1/18/19 - * Time: 10:05 AM - * E-mail: DenBond7@gmail.com - */ -data class Longids constructor(@Expose val message: List?, - @Expose val matching: List?, - @Expose val chosen: List?, - @Expose val needPassphrase: List?) : Parcelable { - - constructor(source: Parcel) : this( - source.createStringArrayList(), - source.createStringArrayList(), - source.createStringArrayList(), - source.createStringArrayList() - ) - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) = - with(dest) { - writeStringList(message) - writeStringList(matching) - writeStringList(chosen) - writeStringList(needPassphrase) - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): Longids = Longids(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/NodeKeyDetails.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/NodeKeyDetails.kt deleted file mode 100644 index dba438f9ba..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/NodeKeyDetails.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: - * DenBond7 - * Ivan Pizhenko - */ - -package com.flowcrypt.email.api.retrofit.response.model.node - -import android.os.Parcel -import android.os.Parcelable -import android.text.TextUtils -import android.util.Patterns -import com.flowcrypt.email.database.entity.AccountEntity -import com.flowcrypt.email.database.entity.KeyEntity -import com.flowcrypt.email.model.PgpContact -import com.flowcrypt.email.security.model.PrivateKeySourceType -import com.flowcrypt.email.util.exception.FlowCryptException -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName -import java.util.* -import javax.mail.internet.AddressException -import javax.mail.internet.InternetAddress - -/** - * @author Denis Bondarenko - * Date: 2/11/19 - * Time: 1:23 PM - * E-mail: DenBond7@gmail.com - */ -data class NodeKeyDetails constructor(@Expose val isFullyDecrypted: Boolean?, - @Expose val isFullyEncrypted: Boolean?, - @Expose @SerializedName("private") val privateKey: String?, - @Expose @SerializedName("public") val publicKey: String?, - @Expose val users: List?, - @Expose val ids: List?, - @Expose val created: Long, - @Expose val lastModified: Long, - @Expose val expiration: Long, - @Expose val algo: Algo?, - var passphrase: String?, - var errorMsg: String?) : Parcelable { - - val primaryPgpContact: PgpContact - get() = determinePrimaryPgpContact() - val pgpContacts: ArrayList - get() = determinePgpContacts() - val longId: String? - get() = ids?.first()?.longId - val fingerprint: String? - get() = ids?.first()?.fingerprint - val isPrivate: Boolean - get() = !TextUtils.isEmpty(privateKey) - - val isExpired: Boolean - get() = expiration > 0 && (System.currentTimeMillis() / 1000 > expiration) - - val mimeAddresses: List - get() = parseMimeAddresses() - - val isPartiallyEncrypted: Boolean - get() { - return isFullyDecrypted == false && isFullyEncrypted == false - } - - constructor(source: Parcel) : this( - source.readValue(Boolean::class.java.classLoader) as Boolean?, - source.readValue(Boolean::class.java.classLoader) as Boolean?, - source.readString(), - source.readString(), - source.createStringArrayList(), - source.createTypedArrayList(KeyId.CREATOR), - source.readLong(), - source.readLong(), - source.readLong(), - source.readParcelable(Algo::class.java.classLoader), - source.readString(), - source.readString() - ) - - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { - writeValue(isFullyDecrypted) - writeValue(isFullyEncrypted) - writeString(privateKey) - writeString(publicKey) - writeStringList(users) - writeTypedList(ids) - writeLong(created) - writeLong(lastModified) - writeLong(expiration) - writeParcelable(algo, 0) - writeString(passphrase) - writeString(errorMsg) - } - - private fun determinePrimaryPgpContact(): PgpContact { - val address = users?.first() - - address?.let { - val (fingerprint1, longId1) = ids!!.first() - var email: String? = null - var name: String? = null - try { - val internetAddresses = InternetAddress.parse(it) - email = internetAddresses.first().address - name = internetAddresses.first().personal - } catch (e: AddressException) { - e.printStackTrace() - val pattern = Patterns.EMAIL_ADDRESS - val matcher = pattern.matcher(users!!.first()) - if (matcher.find()) { - email = matcher.group() - name = email - } - } - - if (email == null) { - throw object : FlowCryptException("No user ids with mail address") {} - } - - return PgpContact( - email = email.toLowerCase(Locale.US), - name = name, - pubkey = publicKey, - hasPgp = !TextUtils.isEmpty(publicKey), - client = null, - fingerprint = fingerprint1, - longid = longId1 - ) - } - - return PgpContact("", "") - } - - private fun determinePgpContacts(): ArrayList { - val pgpContacts = ArrayList() - - users?.let { - for (user in it) { - try { - val internetAddresses = InternetAddress.parse(user) - - for (internetAddress in internetAddresses) { - val email = internetAddress.address.toLowerCase(Locale.US) - val name = internetAddress.personal - - pgpContacts.add(PgpContact(email, name)) - } - } catch (e: AddressException) { - e.printStackTrace() - } - } - } - - return pgpContacts - } - - private fun parseMimeAddresses(): List { - val results = mutableListOf() - - for (user in users ?: emptyList()) { - try { - results.addAll(listOf(*InternetAddress.parse(user))) - } catch (e: AddressException) { - //do nothing - } - } - - return results - } - - fun toKeyEntity(accountEntity: AccountEntity): KeyEntity { - return KeyEntity( - longId = longId ?: throw NullPointerException("nodeKeyDetails.longId == null"), - account = accountEntity.email.toLowerCase(Locale.US), - accountType = accountEntity.accountType, - source = PrivateKeySourceType.BACKUP.toString(), - publicKey = publicKey?.toByteArray() - ?: throw NullPointerException("nodeKeyDetails.publicKey == null"), - privateKey = privateKey?.toByteArray() - ?: throw NullPointerException("nodeKeyDetails.privateKey == null"), - passphrase = passphrase) - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): NodeKeyDetails = NodeKeyDetails(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/PublicKeyMsgBlock.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/PublicKeyMsgBlock.kt index 5c7771f1d1..0fdf3f2d57 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/PublicKeyMsgBlock.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/PublicKeyMsgBlock.kt @@ -8,6 +8,7 @@ package com.flowcrypt.email.api.retrofit.response.model.node import android.os.Parcel import android.os.Parcelable import com.flowcrypt.email.model.PgpContact +import com.flowcrypt.email.security.model.PgpKeyDetails import com.google.gson.annotations.Expose @@ -21,7 +22,7 @@ import com.google.gson.annotations.Expose */ data class PublicKeyMsgBlock constructor(@Expose override val content: String?, @Expose override val complete: Boolean, - @Expose val keyDetails: NodeKeyDetails?) : MsgBlock { + @Expose val keyDetails: PgpKeyDetails?) : MsgBlock { @Expose override val type: MsgBlock.Type = MsgBlock.Type.PUBLIC_KEY @@ -30,7 +31,7 @@ data class PublicKeyMsgBlock constructor(@Expose override val content: String?, constructor(source: Parcel) : this( source.readString(), 1 == source.readInt(), - source.readParcelable(NodeKeyDetails::class.java.classLoader) + source.readParcelable(PgpKeyDetails::class.java.classLoader) ) override fun describeContents(): Int { 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 1d2417e6f7..3a56084fcc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt @@ -13,8 +13,10 @@ import androidx.annotation.WorkerThread import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import androidx.sqlite.db.SupportSQLiteDatabase import com.flowcrypt.email.api.email.JavaEmailConstants +import com.flowcrypt.email.database.converters.PassphraseTypeConverter import com.flowcrypt.email.database.dao.AccountAliasesDao import com.flowcrypt.email.database.dao.AccountDao import com.flowcrypt.email.database.dao.ActionQueueDao @@ -31,6 +33,8 @@ import com.flowcrypt.email.database.entity.ContactEntity import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.security.pgp.PgpKey +import org.pgpainless.key.OpenPgpV4Fingerprint /** @@ -42,18 +46,20 @@ import com.flowcrypt.email.database.entity.MessageEntity * Time: 12:20 * E-mail: DenBond7@gmail.com */ -@Database(entities = -[ - AccountAliasesEntity::class, - AccountEntity::class, - ActionQueueEntity::class, - AttachmentEntity::class, - ContactEntity::class, - KeyEntity::class, - LabelEntity::class, - MessageEntity::class -], - version = FlowCryptRoomDatabase.DB_VERSION) +@Database( + entities = [ + AccountAliasesEntity::class, + AccountEntity::class, + ActionQueueEntity::class, + AttachmentEntity::class, + ContactEntity::class, + KeyEntity::class, + LabelEntity::class, + MessageEntity::class + ], + version = FlowCryptRoomDatabase.DB_VERSION +) +@TypeConverters(PassphraseTypeConverter::class) abstract class FlowCryptRoomDatabase : RoomDatabase() { abstract fun msgDao(): MessageDao @@ -78,7 +84,7 @@ abstract class FlowCryptRoomDatabase : RoomDatabase() { companion object { const val DB_NAME = "flowcrypt.db" - const val DB_VERSION = 24 + const val DB_VERSION = 25 private val MIGRATION_1_3 = object : FlowCryptMigration(1, 3) { override fun doMigration(database: SupportSQLiteDatabase) { @@ -343,6 +349,39 @@ abstract class FlowCryptRoomDatabase : RoomDatabase() { } } + @VisibleForTesting + val MIGRATION_24_25 = object : FlowCryptMigration(24, 25) { + override fun doMigration(database: SupportSQLiteDatabase) { + //create temp table with existed content + database.execSQL("CREATE TEMP TABLE IF NOT EXISTS keys_temp AS SELECT * FROM keys;") + //drop old table + database.execSQL("DROP TABLE IF EXISTS keys;") + //create a new table 'keys' with 'fingerprint' instead of 'long_id' + database.execSQL("CREATE TABLE IF NOT EXISTS `keys` (`_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 )") + //create indices for new table + database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `fingerprint_account_account_type_in_keys` ON `keys` (`fingerprint`, `account`, `account_type`)") + //fill new keys table with existed data. Later we will update fingerprints + database.execSQL("INSERT INTO keys(_id, fingerprint, account, account_type, source, public_key, private_key, passphrase) SELECT * FROM keys_temp;") + //drop temp table + database.execSQL("DROP TABLE IF EXISTS keys_temp;") + + val cursor = database.query("SELECT * FROM keys;") + if (cursor.count > 0) { + while (cursor.moveToNext()) { + val longId = cursor.getString(cursor.getColumnIndexOrThrow("fingerprint")) + val pubKeyAsByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow("public_key")) + val pubKey = PgpKey.parseKeys(pubKeyAsByteArray) + .pgpKeyRingCollection.pgpPublicKeyRingCollection.first() + val fingerprint = OpenPgpV4Fingerprint(pubKey).toString() + database.execSQL( + "UPDATE keys SET fingerprint = ?, passphrase_type = 0 WHERE fingerprint = ?;", + arrayOf(fingerprint, longId) + ) + } + } + } + } + // Singleton prevents multiple instances of database opening at the same time. @Volatile private var INSTANCE: FlowCryptRoomDatabase? = null @@ -355,33 +394,34 @@ abstract class FlowCryptRoomDatabase : RoomDatabase() { synchronized(this) { val instance = Room.databaseBuilder( - context.applicationContext, - FlowCryptRoomDatabase::class.java, - DB_NAME) - .addMigrations( - MIGRATION_1_3, - MIGRATION_3_4, - MIGRATION_4_5, - MIGRATION_5_6, - MIGRATION_6_7, - MIGRATION_7_8, - MIGRATION_8_9, - MIGRATION_9_10, - MIGRATION_10_11, - MIGRATION_11_12, - MIGRATION_12_13, - MIGRATION_13_14, - MIGRATION_14_15, - MIGRATION_15_16, - MIGRATION_16_17, - MIGRATION_17_18, - MIGRATION_18_19, - MIGRATION_19_20, - MIGRATION_20_21, - MIGRATION_21_22, - MIGRATION_22_23, - MIGRATION_23_24) - .build() + context.applicationContext, + FlowCryptRoomDatabase::class.java, + DB_NAME + ).addMigrations( + MIGRATION_1_3, + MIGRATION_3_4, + MIGRATION_4_5, + MIGRATION_5_6, + MIGRATION_6_7, + MIGRATION_7_8, + MIGRATION_8_9, + MIGRATION_9_10, + MIGRATION_10_11, + MIGRATION_11_12, + MIGRATION_12_13, + MIGRATION_13_14, + MIGRATION_14_15, + MIGRATION_15_16, + MIGRATION_16_17, + MIGRATION_17_18, + MIGRATION_18_19, + MIGRATION_19_20, + MIGRATION_20_21, + MIGRATION_21_22, + MIGRATION_22_23, + MIGRATION_23_24, + MIGRATION_24_25 + ).build() INSTANCE = instance return instance } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/converters/PassphraseTypeConverter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/converters/PassphraseTypeConverter.kt new file mode 100644 index 0000000000..2f1dabbae9 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/converters/PassphraseTypeConverter.kt @@ -0,0 +1,27 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.database.converters + +import androidx.room.TypeConverter +import com.flowcrypt.email.database.entity.KeyEntity + +/** + * @author Denis Bondarenko + * Date: 5/17/21 + * Time: 6:13 PM + * E-mail: DenBond7@gmail.com + */ +class PassphraseTypeConverter { + @TypeConverter + fun fromKeyType(passphraseType: KeyEntity.PassphraseType): Int { + return passphraseType.id + } + + @TypeConverter + fun toKeyType(id: Int): KeyEntity.PassphraseType { + return KeyEntity.PassphraseType.findValueById(id) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/KeysDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/KeysDao.kt index b9a1d67615..f02d856319 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/KeysDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/KeysDao.kt @@ -7,8 +7,13 @@ package com.flowcrypt.email.database.dao import androidx.lifecycle.LiveData import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert import androidx.room.Query +import androidx.room.Update import com.flowcrypt.email.database.entity.KeyEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext /** * @author DenBond7 @@ -17,7 +22,7 @@ import com.flowcrypt.email.database.entity.KeyEntity * E-mail: DenBond7@gmail.com */ @Dao -abstract class KeysDao : BaseDao { +abstract class KeysDao { @Query("SELECT * FROM keys WHERE account = :account") abstract fun getAllKeysByAccount(account: String): List @@ -27,12 +32,75 @@ abstract class KeysDao : BaseDao { @Query("SELECT * FROM keys WHERE account = :account") abstract fun getAllKeysByAccountLD(account: String): LiveData> - @Query("DELETE FROM keys WHERE account = :account AND long_id = :longId") - abstract fun deleteByAccountAndLongId(account: String, longId: String): Int + @Query("DELETE FROM keys WHERE account = :account AND fingerprint = :fingerprint") + abstract fun deleteByAccountAndFingerprint(account: String, fingerprint: String): Int - @Query("DELETE FROM keys WHERE account = :account AND long_id = :longId") - abstract suspend fun deleteByAccountAndLongIdSuspend(account: String, longId: String): Int + @Query("DELETE FROM keys WHERE account = :account AND fingerprint = :fingerprint") + abstract suspend fun deleteByAccountAndFingerprintSuspend( + account: String, fingerprint: String): Int - @Query("SELECT * FROM keys WHERE account = :account AND long_id = :longId") - abstract suspend fun getKeyByAccountAndLongIdSuspend(account: String, longId: String): KeyEntity? + @Query("SELECT * FROM keys WHERE account = :account AND fingerprint = :fingerprint") + abstract suspend fun getKeyByAccountAndFingerprintSuspend( + account: String, fingerprint: String): KeyEntity? + + @Query("SELECT * FROM keys WHERE account = :account AND fingerprint = :fingerprint") + abstract fun getKeyByAccountAndFingerprint(account: String, fingerprint: String): KeyEntity? + + @Insert + abstract fun insertInternal(entity: KeyEntity): Long + + @Insert + abstract suspend fun insertInternalSuspend(entity: KeyEntity): Long + + @Insert + abstract suspend fun insertInternalSuspend(entities: Iterable) + + @Update + abstract fun updateInternal(entities: Iterable): Int + + @Update + abstract suspend fun updateInternalSuspend(entity: KeyEntity): Int + + @Update + abstract suspend fun updateInternalSuspend(entities: Iterable): Int + + @Delete + abstract suspend fun deleteSuspend(entities: Iterable): Int + + @Delete + abstract fun delete(entity: KeyEntity): Int + + @Insert + open fun insert(keyEntity: KeyEntity): Long { + return insertInternal(processKeyEntity(keyEntity)) + } + + @Update + open fun update(entities: Iterable): Int { + return updateInternal(entities.map { processKeyEntity(it) }) + } + + @Insert + open suspend fun insertSuspend(keyEntity: KeyEntity): Long = + withContext(Dispatchers.IO) { insertInternalSuspend(processKeyEntity(keyEntity)) } + + @Insert + open suspend fun insertSuspend(entities: Iterable) = + withContext(Dispatchers.IO) { insertInternalSuspend(entities.map { processKeyEntity(it) }) } + + @Update + open suspend fun updateSuspend(keyEntity: KeyEntity): Int = + withContext(Dispatchers.IO) { updateInternalSuspend(processKeyEntity(keyEntity)) } + + @Update + open suspend fun updateSuspend(entities: Iterable): Int = + withContext(Dispatchers.IO) { updateInternalSuspend(entities.map { processKeyEntity(it) }) } + + private fun processKeyEntity(keyEntity: KeyEntity) = + keyEntity.copy( + storedPassphrase = if (keyEntity.passphraseType == KeyEntity.PassphraseType.RAM) { + null + } else { + keyEntity.storedPassphrase + }) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ContactEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ContactEntity.kt index 7a55c181e2..e38fb1187d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ContactEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ContactEntity.kt @@ -13,8 +13,8 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.model.PgpContact +import com.flowcrypt.email.security.model.PgpKeyDetails /** * @author Denis Bondarenko @@ -40,13 +40,13 @@ data class ContactEntity( @ColumnInfo(defaultValue = "NULL") val client: String? = null, @ColumnInfo(defaultValue = "NULL") val attested: Boolean? = null, @ColumnInfo(defaultValue = "NULL") val fingerprint: String? = null, - @ColumnInfo(name = "long_id", defaultValue = "NULL") val longId: String? = null, + @Deprecated("Unused") @ColumnInfo(name = "long_id", defaultValue = "NULL") val longId: String? = null, @Deprecated("Unused") @ColumnInfo(defaultValue = "NULL") val keywords: String? = null, @ColumnInfo(name = "last_use", defaultValue = "0") val lastUse: Long = 0 ) : Parcelable { @Ignore - var nodeKeyDetails: NodeKeyDetails? = null + var pgpKeyDetails: PgpKeyDetails? = null constructor(parcel: Parcel) : this( parcel.readValue(Long::class.java.classLoader) as? Long, @@ -124,9 +124,8 @@ data class ContactEntity( hasPgp = hasPgp, client = client, fingerprint = fingerprint, - longid = longId, lastUse = lastUse, - nodeKeyDetails = nodeKeyDetails) + pgpKeyDetails = pgpKeyDetails) } companion object CREATOR : Parcelable.Creator { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/KeyEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/KeyEntity.kt index 891b28a92e..2700342e56 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/KeyEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/KeyEntity.kt @@ -5,6 +5,8 @@ package com.flowcrypt.email.database.entity +import android.os.Parcel +import android.os.Parcelable import android.provider.BaseColumns import androidx.room.ColumnInfo import androidx.room.Entity @@ -12,6 +14,7 @@ import androidx.room.ForeignKey import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey +import org.pgpainless.util.Passphrase /** * @author Denis Bondarenko @@ -20,37 +23,54 @@ import androidx.room.PrimaryKey * E-mail: DenBond7@gmail.com */ @Entity(tableName = "keys", - indices = [Index(name = "long_id_account_account_type_in_keys", value = ["long_id", "account", "account_type"], unique = true)], + indices = [ + Index( + name = "fingerprint_account_account_type_in_keys", + value = ["fingerprint", "account", "account_type"], + unique = true) + ], foreignKeys = [ - ForeignKey(entity = AccountEntity::class, parentColumns = ["email", "account_type"], - childColumns = ["account", "account_type"], onDelete = ForeignKey.CASCADE) + ForeignKey( + entity = AccountEntity::class, + parentColumns = ["email", "account_type"], + childColumns = ["account", "account_type"], + onDelete = ForeignKey.CASCADE) ] ) data class KeyEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long? = null, - @ColumnInfo(name = "long_id") val longId: String, + @ColumnInfo(name = "fingerprint") val fingerprint: String, val account: String, @ColumnInfo(name = "account_type", defaultValue = "NULL") val accountType: String? = null, val source: String, @ColumnInfo(name = "public_key") val publicKey: ByteArray, @ColumnInfo(name = "private_key") val privateKey: ByteArray, - @ColumnInfo(defaultValue = "NULL") val passphrase: String?) { + @ColumnInfo(name = "passphrase", defaultValue = "NULL") val storedPassphrase: String?, + @ColumnInfo(name = "passphrase_type", defaultValue = "0") val passphraseType: PassphraseType) { @Ignore val privateKeyAsString = String(privateKey) @Ignore - val publicKeyAsString = String(privateKey) + val publicKeyAsString = String(publicKey) + + @Ignore + val passphrase: Passphrase = if (storedPassphrase == null) { + Passphrase.emptyPassphrase() + } else { + Passphrase.fromPassword(storedPassphrase) + } override fun toString(): String { return "KeyEntity(id=$id," + - " longId='$longId'," + + " fingerprint='$fingerprint'," + " account='$account'," + " account_type='$accountType'," + " source='$source'," + " publicKey=${publicKey.contentToString()}," + " privateKey=${privateKey.contentToString()}," + - " passphrase=(hidden))" + " storedPassphrase=(hidden))" + + " passphraseType='$passphraseType'," } override fun equals(other: Any?): Boolean { @@ -60,12 +80,14 @@ data class KeyEntity( other as KeyEntity if (id != other.id) return false - if (longId != other.longId) return false + if (fingerprint != other.fingerprint) return false if (account != other.account) return false if (accountType != other.accountType) return false if (source != other.source) return false if (!publicKey.contentEquals(other.publicKey)) return false if (!privateKey.contentEquals(other.privateKey)) return false + if (storedPassphrase != other.storedPassphrase) return false + if (passphraseType != other.passphraseType) return false if (passphrase != other.passphrase) return false return true @@ -73,13 +95,38 @@ data class KeyEntity( override fun hashCode(): Int { var result = id?.hashCode() ?: 0 - result = 31 * result + longId.hashCode() + result = 31 * result + fingerprint.hashCode() result = 31 * result + account.hashCode() - result = 31 * result + accountType.hashCode() + result = 31 * result + (accountType?.hashCode() ?: 0) result = 31 * result + source.hashCode() result = 31 * result + publicKey.contentHashCode() result = 31 * result + privateKey.contentHashCode() - result = 31 * result + (passphrase?.hashCode() ?: 0) + result = 31 * result + (storedPassphrase?.hashCode() ?: 0) + result = 31 * result + passphraseType.hashCode() + result = 31 * result + passphrase.hashCode() return result } + + enum class PassphraseType(val id: Int) : Parcelable { + DATABASE(0), + RAM(1); + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeInt(ordinal) + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(source: Parcel): PassphraseType = values()[source.readInt()] + override fun newArray(size: Int): Array = arrayOfNulls(size) + + fun findValueById(id: Int): PassphraseType { + return values().firstOrNull { it.id == id } + ?: throw IllegalArgumentException("Unsupported key type") + } + } + } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/bouncycastle/openpgp/PGPKeyRingExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/bouncycastle/openpgp/PGPKeyRingExt.kt index eb1ccb5fa8..03e30b47fc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/bouncycastle/openpgp/PGPKeyRingExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/bouncycastle/openpgp/PGPKeyRingExt.kt @@ -5,11 +5,9 @@ package com.flowcrypt.email.extensions.org.bouncycastle.openpgp -import com.flowcrypt.email.api.retrofit.response.model.node.Algo -import com.flowcrypt.email.api.retrofit.response.model.node.KeyId -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails -import com.flowcrypt.email.extensions.org.pgpainless.key.longId -import com.flowcrypt.email.extensions.org.pgpainless.key.shortId +import com.flowcrypt.email.security.model.Algo +import com.flowcrypt.email.security.model.KeyId +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpArmor import org.bouncycastle.bcpg.ArmoredOutputStream import org.bouncycastle.openpgp.PGPKeyRing @@ -23,7 +21,6 @@ import java.io.ByteArrayOutputStream import java.io.IOException import java.nio.charset.StandardCharsets import java.time.Instant -import java.util.concurrent.TimeUnit /** * @author Denis Bondarenko @@ -31,7 +28,7 @@ import java.util.concurrent.TimeUnit * Time: 10:08 AM * E-mail: DenBond7@gmail.com */ -fun PGPKeyRing.toNodeKeyDetails(): NodeKeyDetails { +fun PGPKeyRing.toPgpKeyDetails(): PgpKeyDetails { val keyRingInfo = KeyRingInfo(this) val algo = Algo( @@ -45,16 +42,18 @@ fun PGPKeyRing.toNodeKeyDetails(): NodeKeyDetails { } ) - val ids = publicKeys.iterator().asSequence().toList() + val keyIdList = publicKeys.iterator().asSequence().toList() .map { val fingerprint = OpenPgpV4Fingerprint(it) KeyId( - fingerprint = fingerprint.toString(), - longId = fingerprint.longId, - shortId = fingerprint.shortId, + fingerprint = fingerprint.toString() ) } + if (keyIdList.isEmpty()) { + throw IllegalArgumentException("There are no fingerprints") + } + val privateKey = if (keyRingInfo.isSecretKey) armor() else null val publicKey = if (keyRingInfo.isSecretKey) { (this as PGPSecretKeyRing).toPublicKeyRing().armor() @@ -62,20 +61,17 @@ fun PGPKeyRing.toNodeKeyDetails(): NodeKeyDetails { armor() } - return NodeKeyDetails( + return PgpKeyDetails( isFullyDecrypted = keyRingInfo.isFullyDecrypted, isFullyEncrypted = keyRingInfo.isFullyEncrypted, privateKey = privateKey, publicKey = publicKey, users = keyRingInfo.userIds, - ids = ids, - created = TimeUnit.SECONDS.convert(keyRingInfo.creationDate.time, TimeUnit.MILLISECONDS), - lastModified = TimeUnit.SECONDS.convert(keyRingInfo.lastModified.time, TimeUnit.MILLISECONDS), - expiration = TimeUnit.SECONDS.convert(keyRingInfo.primaryKeyExpirationDate?.time - ?: 0, TimeUnit.MILLISECONDS), - algo = algo, - passphrase = null, - errorMsg = null) + ids = keyIdList, + created = keyRingInfo.creationDate.time, + lastModified = keyRingInfo.lastModified.time, + expiration = keyRingInfo.primaryKeyExpirationDate?.time, + algo = algo) } @Throws(IOException::class) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/pgpainless/util/PassphraseExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/pgpainless/util/PassphraseExt.kt new file mode 100644 index 0000000000..eff44449a3 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/org/pgpainless/util/PassphraseExt.kt @@ -0,0 +1,19 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.extensions.org.pgpainless.util + +import org.pgpainless.util.Passphrase + +/** + * @author Denis Bondarenko + * Date: 5/7/21 + * Time: 6:36 PM + * E-mail: DenBond7@gmail.com + */ +val Passphrase.asString: String? + get() { + return chars?.let { String(it) } + } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountKeysInfoViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountKeysInfoViewModel.kt index 3074b37e84..32cf38690f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountKeysInfoViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountKeysInfoViewModel.kt @@ -17,8 +17,8 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.retrofit.ApiRepository import com.flowcrypt.email.api.retrofit.FlowcryptApiRepository import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.exception.ExceptionUtil import kotlinx.coroutines.Dispatchers @@ -40,7 +40,7 @@ import java.util.* class AccountKeysInfoViewModel(application: Application) : AccountViewModel(application) { private val apiRepository: ApiRepository = FlowcryptApiRepository() - val accountKeysInfoLiveData = MediatorLiveData>>() + val accountKeysInfoLiveData = MediatorLiveData>>() private val initLiveData = Transformations .switchMap(activeAccountLiveData) { accountEntity -> liveData { @@ -48,7 +48,7 @@ class AccountKeysInfoViewModel(application: Application) : AccountViewModel(appl emit(getResult(accountEntity)) } } - private val refreshingLiveData = MutableLiveData>>() + private val refreshingLiveData = MutableLiveData>>() init { accountKeysInfoLiveData.addSource(initLiveData) { accountKeysInfoLiveData.value = it } @@ -91,9 +91,9 @@ class AccountKeysInfoViewModel(application: Application) : AccountViewModel(appl } } - private suspend fun getResult(accountEntity: AccountEntity?): Result> { + private suspend fun getResult(accountEntity: AccountEntity?): Result> { return if (accountEntity != null) { - val results = mutableListOf() + val results = mutableListOf() val emails = ArrayList() emails.add(accountEntity.email) @@ -104,7 +104,7 @@ class AccountKeysInfoViewModel(application: Application) : AccountViewModel(appl for (email in emails) { val pubResponseResult = apiRepository.getPub(context = getApplication(), identData = email) pubResponseResult.data?.pubkey?.let { key -> - results.addAll(PgpKey.parseKeys(key).toNodeKeyDetailsList()) + results.addAll(PgpKey.parseKeys(key).toPgpKeyDetailsList()) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/BackupsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/BackupsViewModel.kt index e8034a63cf..e2d5ea4702 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/BackupsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/BackupsViewModel.kt @@ -17,8 +17,8 @@ import com.flowcrypt.email.api.email.SearchBackupsUtil import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.protocol.SmtpProtocolUtil import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.exception.ManualHandledException import com.sun.mail.imap.IMAPFolder @@ -37,7 +37,7 @@ import javax.mail.Store * E-mail: DenBond7@gmail.com */ class BackupsViewModel(application: Application) : AccountViewModel(application) { - val onlineBackupsLiveData: LiveData?>> = Transformations.switchMap(activeAccountLiveData) { accountEntity -> + val onlineBackupsLiveData: LiveData?>> = Transformations.switchMap(activeAccountLiveData) { accountEntity -> liveData { accountEntity?.let { emit(Result.loading()) @@ -56,7 +56,7 @@ class BackupsViewModel(application: Application) : AccountViewModel(application) } else { val connection = IMAPStoreManager.activeConnections[accountEntity.id] if (connection == null) { - emit(Result.exception?>(NullPointerException("There is no active connection for ${accountEntity.email}"))) + emit(Result.exception?>(NullPointerException("There is no active connection for ${accountEntity.email}"))) } else { when (accountEntity.accountType) { AccountEntity.ACCOUNT_TYPE_GOOGLE -> { @@ -71,7 +71,7 @@ class BackupsViewModel(application: Application) : AccountViewModel(application) } } catch (e: Exception) { e.printStackTrace() - emit(Result.exception?>(e)) + emit(Result.exception?>(e)) } } } @@ -129,8 +129,8 @@ class BackupsViewModel(application: Application) : AccountViewModel(application) } } - private suspend fun getPrivateKeyBackupsUsingJavaMailAPI(account: AccountEntity, store: Store): MutableList = withContext(Dispatchers.IO) { - val keyDetailsList = mutableListOf() + private suspend fun getPrivateKeyBackupsUsingJavaMailAPI(account: AccountEntity, store: Store): MutableList = withContext(Dispatchers.IO) { + val keyDetailsList = mutableListOf() val folders = store.defaultFolder.list("*") for (folder in folders) { @@ -145,7 +145,7 @@ class BackupsViewModel(application: Application) : AccountViewModel(application) continue } - keyDetailsList.addAll(PgpKey.parseKeys(backup).toNodeKeyDetailsList()) + keyDetailsList.addAll(PgpKey.parseKeys(backup).toPgpKeyDetailsList()) } } catch (e: Exception) { e.printStackTrace() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CheckPrivateKeysViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CheckPrivateKeysViewModel.kt index a5a84acb41..d1fc315915 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CheckPrivateKeysViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CheckPrivateKeysViewModel.kt @@ -11,7 +11,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.extensions.org.pgpainless.util.asString +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.exception.WrongPassPhraseException @@ -29,36 +31,40 @@ import org.pgpainless.util.Passphrase class CheckPrivateKeysViewModel(application: Application) : BaseAndroidViewModel(application) { val checkPrvKeysLiveData: MutableLiveData>> = MutableLiveData() - fun checkKeys(keys: List, passphrase: String) { + fun checkKeys(keys: List, passphrase: Passphrase, + passphraseType: KeyEntity.PassphraseType) { viewModelScope.launch { checkPrvKeysLiveData.value = Result.loading() - if (passphrase.isEmpty()) { + if (passphrase.isEmpty) { checkPrvKeysLiveData.value = Result.error(emptyList()) return@launch } - checkPrvKeysLiveData.value = Result.success(checkKeysInternal(keys, passphrase)) + checkPrvKeysLiveData.value = + Result.success(checkKeysInternal(keys, passphrase, passphraseType)) } } - private suspend fun checkKeysInternal(keys: List, - passphrase: String): List = + private suspend fun checkKeysInternal(keys: List, + passphrase: Passphrase, + passphraseType: KeyEntity.PassphraseType): + List = withContext(Dispatchers.IO) { val context: Context = getApplication() val resultList = mutableListOf() for (keyDetails in keys) { - val copy = keyDetails.copy() + val copy = keyDetails.copy(passphraseType = passphraseType) var e: Exception? = null if (copy.isPrivate) { val prvKey = copy.privateKey if (prvKey.isNullOrEmpty()) { e = IllegalArgumentException("Empty source") } else { - if (copy.isFullyDecrypted == true) { - copy.passphrase = passphrase + if (copy.isFullyDecrypted) { + copy.tempPassphrase = passphrase.chars } else { try { - PgpKey.decryptKey(prvKey, Passphrase.fromPassword(passphrase)) - copy.passphrase = passphrase + PgpKey.decryptKey(prvKey, passphrase) + copy.tempPassphrase = passphrase.chars } catch (ex: Exception) { //to prevent leak sensitive info we skip printing stack trace for release builds if (GeneralUtil.isDebugBuild()) { @@ -73,11 +79,14 @@ class CheckPrivateKeysViewModel(application: Application) : BaseAndroidViewModel e = IllegalArgumentException(context.getString(R.string.not_private_key)) } - resultList.add(CheckResult(copy, passphrase, e)) + resultList.add(CheckResult( + pgpKeyDetails = copy, + passphrase = passphrase.asString ?: throw IllegalArgumentException(), + e = e)) } return@withContext resultList } - data class CheckResult(val nodeKeyDetails: NodeKeyDetails, + data class CheckResult(val pgpKeyDetails: PgpKeyDetails, val passphrase: String, val e: Exception? = null) } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ContactsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ContactsViewModel.kt index 2cd301c30a..a48e2ca661 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ContactsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ContactsViewModel.kt @@ -17,9 +17,9 @@ import com.flowcrypt.email.api.retrofit.node.NodeRepository import com.flowcrypt.email.api.retrofit.response.attester.PubResponse import com.flowcrypt.email.api.retrofit.response.base.ApiError import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.ContactEntity import com.flowcrypt.email.model.PgpContact +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.exception.ApiException @@ -91,7 +91,6 @@ class ContactsViewModel(application: Application) : AccountViewModel(application originalContactEntity.copy( publicKey = contactEntity.publicKey, fingerprint = contactEntity.fingerprint, - longId = contactEntity.longId, hasPgp = true)) } } @@ -114,7 +113,7 @@ class ContactsViewModel(application: Application) : AccountViewModel(application * 1. save an empty record eg `new PgpContact(email, null);` - this means we don't know if they have PGP yet * 1. look up the email on `flowcrypt.com/attester/pub/EMAIL>` * 1. if pubkey comes back, create something like `new PgpContact(js, email, null, pubkey, - * client);`. The PgpContact constructor will define has_pgp, longid, fingerprint, etc + * client);`. The PgpContact constructor will define has_pgp, fingerprint, etc * for you. Then save that object into database. * 1. if no pubkey found, create `new PgpContact(js, email, null, null, null, null);` - this * means we know they don't currently have PGP @@ -140,8 +139,8 @@ class ContactsViewModel(application: Application) : AccountViewModel(application cachedContactEntity = roomDatabase.contactsDao().getContactByEmailSuspend(emailLowerCase) } else { cachedContactEntity.publicKey?.let { - val result = PgpKey.parseKeys(it).toNodeKeyDetailsList() - cachedContactEntity?.nodeKeyDetails = result.firstOrNull() + val result = PgpKey.parseKeys(it).toPgpKeyDetailsList() + cachedContactEntity?.pgpKeyDetails = result.firstOrNull() } } @@ -151,11 +150,11 @@ class ContactsViewModel(application: Application) : AccountViewModel(application cachedContactEntity = updateCachedInfoWithAttesterInfo(cachedContactEntity, it, emailLowerCase) } } else { - cachedContactEntity?.nodeKeyDetails?.fingerprint?.let { fingerprint -> + cachedContactEntity?.pgpKeyDetails?.fingerprint?.let { fingerprint -> getPgpContactInfoFromServer(fingerprint = fingerprint)?.let { - val cacheLastModified = cachedContactEntity?.nodeKeyDetails?.lastModified ?: 0 - val attesterLastModified = it.nodeKeyDetails?.lastModified ?: 0 - val attesterFingerprint = it.nodeKeyDetails?.fingerprint + val cacheLastModified = cachedContactEntity?.pgpKeyDetails?.lastModified ?: 0 + val attesterLastModified = it.pgpKeyDetails?.lastModified ?: 0 + val attesterFingerprint = it.pgpKeyDetails?.fingerprint if (attesterLastModified > cacheLastModified && fingerprint.equals(attesterFingerprint, true)) { cachedContactEntity = updateCachedInfoWithAttesterInfo(cachedContactEntity, it, emailLowerCase) @@ -200,8 +199,8 @@ class ContactsViewModel(application: Application) : AccountViewModel(application val lastVersion = roomDatabase.contactsDao().getContactByEmailSuspend(emailLowerCase) lastVersion?.publicKey?.let { - val result = PgpKey.parseKeys(it).toNodeKeyDetailsList() - lastVersion.nodeKeyDetails = result.firstOrNull() + val result = PgpKey.parseKeys(it).toPgpKeyDetailsList() + lastVersion.pgpKeyDetails = result.firstOrNull() } return lastVersion @@ -232,10 +231,10 @@ class ContactsViewModel(application: Application) : AccountViewModel(application } } - fun updateContactPgpInfo(contactEntity: ContactEntity?, nodeKeyDetails: NodeKeyDetails) { + fun updateContactPgpInfo(contactEntity: ContactEntity?, pgpKeyDetails: PgpKeyDetails) { viewModelScope.launch { contactEntity?.let { - val contactEntityFromPrimaryPgpContact = nodeKeyDetails.primaryPgpContact.toContactEntity() + val contactEntityFromPrimaryPgpContact = pgpKeyDetails.primaryPgpContact.toContactEntity() roomDatabase.contactsDao().updateSuspend(contactEntityFromPrimaryPgpContact.copy( id = contactEntity.id, email = contactEntity.email.toLowerCase(Locale.US), @@ -301,10 +300,10 @@ class ContactsViewModel(application: Application) : AccountViewModel(application val client = ContactEntity.CLIENT_PGP if (pubKeyString?.isNotEmpty() == true) { - PgpKey.parseKeys(pubKeyString).toNodeKeyDetailsList().firstOrNull()?.let { + PgpKey.parseKeys(pubKeyString).toPgpKeyDetailsList().firstOrNull()?.let { val pgpContact = it.primaryPgpContact pgpContact.client = client - pgpContact.nodeKeyDetails = it + pgpContact.pgpKeyDetails = it return@withContext pgpContact } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DecryptMessageViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DecryptMessageViewModel.kt deleted file mode 100644 index 30eaf0f619..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DecryptMessageViewModel.kt +++ /dev/null @@ -1,214 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.jetpack.viewmodel - -import android.app.Application -import android.content.Context -import android.net.Uri -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.flowcrypt.email.api.email.EmailUtil -import com.flowcrypt.email.api.email.JavaEmailConstants -import com.flowcrypt.email.api.retrofit.node.NodeRepository -import com.flowcrypt.email.api.retrofit.node.PgpApiRepository -import com.flowcrypt.email.api.retrofit.request.node.ParseDecryptMsgRequest -import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.PublicKeyMsgBlock -import com.flowcrypt.email.api.retrofit.response.node.ParseDecryptedMsgResult -import com.flowcrypt.email.database.entity.KeyEntity -import com.flowcrypt.email.security.KeyStoreCryptoManager -import com.flowcrypt.email.security.KeysStorageImpl -import com.flowcrypt.email.util.CacheManager -import com.flowcrypt.email.util.cache.DiskLruCache -import com.flowcrypt.email.util.exception.ExceptionUtil -import com.sun.mail.util.ASCIIUtility -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.apache.commons.io.IOUtils -import org.bouncycastle.bcpg.ArmoredInputStream -import java.io.ByteArrayInputStream -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.util.* -import javax.mail.BodyPart -import javax.mail.MessagingException -import javax.mail.Multipart -import javax.mail.Part -import javax.mail.internet.MimeMessage - -/** - * This [ViewModel] implementation can be used to parse and decrypt (if needed) an incoming message. - * - * @author Denis Bondarenko - * Date: 3/21/19 - * Time: 11:47 AM - * E-mail: DenBond7@gmail.com - */ -class DecryptMessageViewModel(application: Application) : BaseNodeApiViewModel(application) { - val headersLiveData: MutableLiveData = MutableLiveData() - val decryptLiveData: MutableLiveData> = MutableLiveData() - - private val keysStorage: KeysStorageImpl = KeysStorageImpl.getInstance(application) - private val apiRepository: PgpApiRepository = NodeRepository() - - fun decryptMessage(rawMimeBytes: ByteArray) { - viewModelScope.launch { - decryptLiveData.value = Result.loading() - ByteArrayInputStream(rawMimeBytes).use { - headersLiveData.postValue(getHeaders(it)) - } - val list = keysStorage.getLatestAllPgpPrivateKeys() - val result = apiRepository.parseDecryptMsg( - request = ParseDecryptMsgRequest(data = rawMimeBytes, keyEntities = list, isEmail = true)) - - modifyMsgBlocksIfNeeded(result) - decryptLiveData.value = result - } - } - - fun decryptMessage(context: Context, msgSnapshot: DiskLruCache.Snapshot) { - viewModelScope.launch { - decryptLiveData.value = Result.loading() - val uri = msgSnapshot.getUri(0) - if (uri != null) { - withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri)?.use { uriInputStream -> - headersLiveData.postValue(getHeaders(uriInputStream, true)) - } - } - - val list = keysStorage.getLatestAllPgpPrivateKeys() - val largerThan1Mb = msgSnapshot.getLength(0) > 1024 * 1000 - val result = if (largerThan1Mb) { - parseMimeAndDecrypt(context, uri, list) - } else { - apiRepository.parseDecryptMsg( - request = ParseDecryptMsgRequest(context = context, uri = uri, keyEntities = list, - isEmail = true, hasEncryptedDataInUri = true)) - } - - modifyMsgBlocksIfNeeded(result) - decryptLiveData.value = result - } else { - val byteArray = msgSnapshot.getByteArray(0) - decryptMessage(byteArray) - } - } - } - - private suspend fun modifyMsgBlocksIfNeeded(result: Result) { - result.data?.let { parseDecryptMsgResult -> - for (block in parseDecryptMsgResult.msgBlocks ?: mutableListOf()) { - if (block is PublicKeyMsgBlock) { - val keyDetails = block.keyDetails ?: continue - val pgpContact = keyDetails.primaryPgpContact - val contactEntity = roomDatabase.contactsDao().getContactByEmailSuspend(pgpContact.email) - block.existingPgpContact = contactEntity?.toPgpContact() - } - } - } - } - - private suspend fun parseMimeAndDecrypt(context: Context, uri: Uri, list: List): - Result { - val uriOfEncryptedPart = getUriOfEncryptedPart(context, uri) - return if (uriOfEncryptedPart != null) { - apiRepository.parseDecryptMsg( - request = ParseDecryptMsgRequest(context = context, uri = uriOfEncryptedPart, keyEntities = list, isEmail = false)) - } else { - apiRepository.parseDecryptMsg( - request = ParseDecryptMsgRequest(context = context, uri = uri, keyEntities = list, isEmail = true, hasEncryptedDataInUri = true)) - } - } - - private suspend fun getMimeMessageFromInputStream(context: Context, uri: Uri) = - withContext(Dispatchers.IO) { - val inputStream = context.contentResolver.openInputStream(uri) - if (inputStream != null) { - MimeMessage(null, KeyStoreCryptoManager.getCipherInputStream(inputStream)) - } else throw NullPointerException("Stream is empty") - } - - private suspend fun getUriOfEncryptedPart(context: Context, uri: Uri): Uri? { - val mimeMessage: MimeMessage = getMimeMessageFromInputStream(context, uri) - return findEncryptedPart(mimeMessage) - } - - private suspend fun findEncryptedPart(part: Part): Uri? = withContext(Dispatchers.Default) { - try { - if (part.isMimeType(JavaEmailConstants.MIME_TYPE_MULTIPART)) { - val multiPart = part.content as Multipart - val partsNumber = multiPart.count - for (partCount in 0 until partsNumber) { - val bodyPart = multiPart.getBodyPart(partCount) - if (bodyPart.isMimeType(JavaEmailConstants.MIME_TYPE_MULTIPART)) { - val encryptedPart = findEncryptedPart(bodyPart) - if (encryptedPart != null) { - return@withContext encryptedPart - } - } else if (bodyPart?.disposition?.toLowerCase(Locale.getDefault()) in listOf(Part.ATTACHMENT, Part.INLINE)) { - val fileName = bodyPart.fileName?.toLowerCase(Locale.getDefault()) ?: "" - if (fileName in listOf("message", "msg.asc", "message.asc", "encrypted.asc", "encrypted.eml.pgp", "Message.pgp", "")) { - val file = prepareTempFile(bodyPart) - return@withContext Uri.fromFile(file) - } - - val contentType = bodyPart.contentType?.toLowerCase(Locale.getDefault()) ?: "" - if (contentType in listOf("application/octet-stream", "application/pgp-encrypted")) { - val file = prepareTempFile(bodyPart) - return@withContext Uri.fromFile(file) - } - } - } - return@withContext null - } else { - return@withContext null - } - } catch (e: MessagingException) { - e.printStackTrace() - return@withContext null - } catch (e: IOException) { - e.printStackTrace() - return@withContext null - } - } - - private suspend fun prepareTempFile(bodyPart: BodyPart): File = withContext(Dispatchers.IO) { - val tempDir = CacheManager.getCurrentMsgTempDir() - val file = File(tempDir, FILE_NAME_ENCRYPTED_MESSAGE) - IOUtils.copy(ArmoredInputStream(bodyPart.inputStream), FileOutputStream(file)) - return@withContext file - } - - /** - * We fetch the first 50Kb from the given input stream and extract headers. - */ - private suspend fun getHeaders(inputStream: InputStream?, - isDataEncrypted: Boolean = false): String = withContext(Dispatchers.IO) { - inputStream ?: return@withContext "" - val d = ByteArray(50000) - try { - if (isDataEncrypted) { - IOUtils.read(KeyStoreCryptoManager.getCipherInputStream(inputStream), d) - } else { - IOUtils.read(inputStream, d) - } - } catch (e: Exception) { - e.printStackTrace() - ExceptionUtil.handleError(e) - } - EmailUtil.getHeadersFromRawMIME(ASCIIUtility.toString(d)) - } - - companion object { - private const val FILE_NAME_ENCRYPTED_MESSAGE = "temp_encrypted_msg.asc" - } - -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoadPrivateKeysViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoadPrivateKeysViewModel.kt index 8af93c1c09..a13771d135 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoadPrivateKeysViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LoadPrivateKeysViewModel.kt @@ -18,8 +18,8 @@ import com.flowcrypt.email.api.email.SearchBackupsUtil import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.protocol.OpenStoreHelper import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.NodeException @@ -44,7 +44,7 @@ import kotlin.collections.ArrayList * E-mail: DenBond7@gmail.com */ class LoadPrivateKeysViewModel(application: Application) : BaseAndroidViewModel(application) { - val privateKeysLiveData = MutableLiveData?>>() + val privateKeysLiveData = MutableLiveData?>>() fun fetchAvailableKeys(accountEntity: AccountEntity?) { viewModelScope.launch { @@ -59,7 +59,7 @@ class LoadPrivateKeysViewModel(application: Application) : BaseAndroidViewModel( } } - private suspend fun fetchKeys(accountEntity: AccountEntity): Result> = + private suspend fun fetchKeys(accountEntity: AccountEntity): Result> = withContext(Dispatchers.IO) { try { when (accountEntity.accountType) { @@ -79,17 +79,17 @@ class LoadPrivateKeysViewModel(application: Application) : BaseAndroidViewModel( } /** - * Get a list of [NodeKeyDetails] using the standard JavaMail API + * Get a list of [PgpKeyDetails] using the standard JavaMail API * * @param session A [Session] object. - * @return A list of [NodeKeyDetails] + * @return A list of [PgpKeyDetails] * @throws MessagingException * @throws IOException * @throws GoogleAuthException */ - private suspend fun getPrivateKeyBackupsUsingJavaMailAPI(accountEntity: AccountEntity): Collection = + private suspend fun getPrivateKeyBackupsUsingJavaMailAPI(accountEntity: AccountEntity): Collection = withContext(Dispatchers.IO) { - val details = ArrayList() + val details = ArrayList() OpenStoreHelper.openStore(getApplication(), accountEntity, OpenStoreHelper.getAccountSess(getApplication(), accountEntity)).use { store -> try { val context: Context = getApplication() @@ -113,7 +113,7 @@ class LoadPrivateKeysViewModel(application: Application) : BaseAndroidViewModel( } try { - details.addAll(PgpKey.parseKeys(backup).toNodeKeyDetailsList()) + details.addAll(PgpKey.parseKeys(backup).toPgpKeyDetailsList()) } catch (e: NodeException) { e.printStackTrace() ExceptionUtil.handleError(e) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 20898a0dad..226bbdc7fb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.Transformations import androidx.lifecycle.liveData +import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import com.flowcrypt.email.R import com.flowcrypt.email.api.email.EmailUtil @@ -38,12 +39,12 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity -import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.uid import com.flowcrypt.email.jetpack.workmanager.sync.UpdateMsgsSeenStateWorker import com.flowcrypt.email.security.KeyStoreCryptoManager import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.SearchMessagesActivity import com.flowcrypt.email.util.CacheManager import com.flowcrypt.email.util.cache.DiskLruCache @@ -101,16 +102,17 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa uid = messageEntity.uid)) } - private val afterKeysUpdatedMsgLiveData: LiveData = Transformations.switchMap(keysStorage.nodeKeyDetailsLiveData) { - liveData { - if (it.isNotEmpty()) { - emit(roomDatabase.msgDao().getMsgSuspend( - account = messageEntity.email, - folder = messageEntity.folder, - uid = messageEntity.uid)) + private val afterKeysUpdatedMsgLiveData: LiveData = + keysStorage.secretKeyRingsLiveData.switchMap { + liveData { + if (it.isNotEmpty()) { + emit(roomDatabase.msgDao().getMsgSuspend( + account = messageEntity.email, + folder = messageEntity.folder, + uid = messageEntity.uid)) + } + } } - } - } private val mediatorMsgLiveData: MediatorLiveData = MediatorLiveData() @@ -311,7 +313,7 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa private suspend fun processingMsgSnapshot(msgSnapshot: DiskLruCache.Snapshot): Result = withContext(Dispatchers.IO) { val uri = msgSnapshot.getUri(0) if (uri != null) { - val list = keysStorage.getLatestAllPgpPrivateKeys() + val list = keysStorage.getPgpKeyDetailsList() val largerThan1Mb = msgSnapshot.getLength(0) > 1024 * 1000 val result = if (largerThan1Mb) { parseMimeAndDecrypt(context = getApplication(), uri = uri, list = list) @@ -320,7 +322,7 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa request = ParseDecryptMsgRequest( context = getApplication(), uri = uri, - keyEntities = list, + pgpKeyDetailsList = list, isEmail = true, hasEncryptedDataInUri = true )) @@ -340,7 +342,7 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa val result = apiRepository.parseDecryptMsg( request = ParseDecryptMsgRequest( data = rawMimeBytes, - keyEntities = keysStorage.getLatestAllPgpPrivateKeys(), + pgpKeyDetailsList = keysStorage.getPgpKeyDetailsList(), isEmail = true )) modifyMsgBlocksIfNeeded(result) @@ -361,15 +363,15 @@ class MsgDetailsViewModel(val localFolder: LocalFolder, val messageEntity: Messa } } - private suspend fun parseMimeAndDecrypt(context: Context, uri: Uri, list: List): + private suspend fun parseMimeAndDecrypt(context: Context, uri: Uri, list: List): Result { val uriOfEncryptedPart = getUriOfEncryptedPart(context, uri) return if (uriOfEncryptedPart != null) { apiRepository.parseDecryptMsg( - request = ParseDecryptMsgRequest(context = context, uri = uriOfEncryptedPart, keyEntities = list, isEmail = false)) + request = ParseDecryptMsgRequest(context = context, uri = uriOfEncryptedPart, pgpKeyDetailsList = list, isEmail = false)) } else { apiRepository.parseDecryptMsg( - request = ParseDecryptMsgRequest(context = context, uri = uri, keyEntities = list, isEmail = true, hasEncryptedDataInUri = true)) + request = ParseDecryptMsgRequest(context = context, uri = uri, pgpKeyDetailsList = list, isEmail = true, hasEncryptedDataInUri = true)) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ParseKeysViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ParseKeysViewModel.kt index e6296b2cd3..77cc1b2548 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ParseKeysViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ParseKeysViewModel.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey /** @@ -25,11 +25,11 @@ import com.flowcrypt.email.security.pgp.PgpKey */ class ParseKeysViewModel(application: Application) : AccountViewModel(application) { private val keysSourceLiveData = MutableLiveData() - val parseKeysLiveData: LiveData>> = + val parseKeysLiveData: LiveData>> = Transformations.switchMap(keysSourceLiveData) { source -> liveData { emit(Result.loading()) - emit(Result.success(PgpKey.parseKeys(source).toNodeKeyDetailsList())) + emit(Result.success(PgpKey.parseKeys(source).toPgpKeyDetailsList())) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PgpKeyDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PgpKeyDetailsViewModel.kt new file mode 100644 index 0000000000..5bfab4d0ea --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PgpKeyDetailsViewModel.kt @@ -0,0 +1,97 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.jetpack.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.switchMap +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails +import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.model.PgpKeyDetails +import org.pgpainless.util.Passphrase +import java.time.Instant + +/** + * @author Denis Bondarenko + * Date: 5/24/21 + * Time: 5:33 PM + * E-mail: DenBond7@gmail.com + */ +class PgpKeyDetailsViewModel(val fingerprint: String?, application: Application) : + BaseNodeApiViewModel(application) { + private val keysStorage: KeysStorageImpl = KeysStorageImpl.getInstance(getApplication()) + + private val pgpKeyDetailsLiveDataDirect: LiveData> = + keysStorage.secretKeyRingsLiveData.switchMap { list -> + liveData { + emit(Result.loading()) + emit(Result.success(list.firstOrNull { + it.toPgpKeyDetails().fingerprint.equals( + fingerprint, + true + ) + }?.toPgpKeyDetails())) + } + } + + private val pgpKeyDetailsLiveDataAfterPassphraseUpdates: LiveData> = + keysStorage.passphrasesUpdatesLiveData.switchMap { list -> + liveData { + emit(Result.loading()) + val pgpKeyDetailsResult = pgpKeyDetailsLiveDataDirect.value ?: Result.success( + keysStorage.getPGPSecretKeyRingByFingerprint(fingerprint ?: "")?.toPgpKeyDetails() + ) + emit(pgpKeyDetailsResult) + } + } + + val pgpKeyDetailsLiveData = MediatorLiveData>() + + init { + pgpKeyDetailsLiveData.addSource(pgpKeyDetailsLiveDataDirect) { + pgpKeyDetailsLiveData.value = it + } + pgpKeyDetailsLiveData.addSource(pgpKeyDetailsLiveDataAfterPassphraseUpdates) { + pgpKeyDetailsLiveData.value = it + } + } + + fun getPgpKeyDetails(): PgpKeyDetails? = pgpKeyDetailsLiveData.value?.data + + fun getPassphrase(): Passphrase? { + return fingerprint?.let { keysStorage.getPassphraseByFingerprint(it) } + } + + fun getPassphraseType(): KeyEntity.PassphraseType? { + return fingerprint?.let { keysStorage.getPassphraseTypeByFingerprint(it) } + } + + fun forgetPassphrase() { + fingerprint?.let { + keysStorage.putPassPhraseToCache( + fingerprint = it, + passphrase = Passphrase.emptyPassphrase(), + validUntil = Instant.now(), + passphraseType = KeyEntity.PassphraseType.RAM + ) + } + } + + fun updatePassphrase(passphrase: Passphrase) { + fingerprint?.let { + keysStorage.putPassPhraseToCache( + fingerprint = it, + passphrase = passphrase, + validUntil = KeysStorageImpl.calculateLifeTimeForPassphrase(), + passphraseType = KeyEntity.PassphraseType.RAM + ) + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt index c305c850f2..abd865ec4e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/PrivateKeysViewModel.kt @@ -10,12 +10,11 @@ package com.flowcrypt.email.jetpack.viewmodel import android.app.Application import android.content.Context import android.net.Uri -import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData +import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import com.flowcrypt.email.R import com.flowcrypt.email.api.email.EmailUtil @@ -27,16 +26,18 @@ import com.flowcrypt.email.api.retrofit.FlowcryptApiRepository import com.flowcrypt.email.api.retrofit.request.model.InitialLegacySubmitModel import com.flowcrypt.email.api.retrofit.request.model.TestWelcomeModel import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.ActionQueueEntity -import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toNodeKeyDetails -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails +import com.flowcrypt.email.extensions.org.pgpainless.util.asString +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.KeyImportModel import com.flowcrypt.email.model.PgpContact import com.flowcrypt.email.security.KeyStoreCryptoManager import com.flowcrypt.email.security.KeysStorageImpl import com.flowcrypt.email.security.SecurityUtils +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.service.actionqueue.actions.BackupPrivateKeyToInboxAction import com.flowcrypt.email.service.actionqueue.actions.RegisterUserPublicKeyAction @@ -73,42 +74,46 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl val saveBackupAsFileLiveData = MutableLiveData>() val savePrivateKeysLiveData = MutableLiveData>() val parseKeysLiveData = MutableLiveData>() - val createPrivateKeyLiveData = MutableLiveData>() - val nodeKeyDetailsLiveData = keysStorage.nodeKeyDetailsLiveData + val createPrivateKeyLiveData = MutableLiveData>() val deleteKeysLiveData = MutableLiveData>() - val parseKeysResultLiveData: LiveData>> = - Transformations.switchMap(keysStorage.nodeKeyDetailsLiveData) { list -> - liveData { - emit(Result.loading()) - emit(Result.success(list)) - } + val parseKeysResultLiveData: LiveData>> = + keysStorage.secretKeyRingsLiveData.switchMap { list -> + liveData { + emit(Result.loading()) + emit(Result.success(list.map { it.toPgpKeyDetails() })) } + } - fun changePassphrase(newPassphrase: String) { + fun changePassphrase(newPassphrase: Passphrase) { viewModelScope.launch { try { changePassphraseLiveData.value = Result.loading() val account = roomDatabase.accountDao().getActiveAccountSuspend() requireNotNull(account) - val list = keysStorage.getAllPgpPrivateKeys() + val list = keysStorage.getRawKeys() - if (CollectionUtils.isEmpty(list)) { + if (list.isEmpty()) { throw NoPrivateKeysAvailableException(getApplication(), account.email) } roomDatabase.keysDao().updateSuspend(list.map { keyEntity -> - with(getModifiedNodeKeyDetails(keyEntity.passphrase, newPassphrase, keyEntity.privateKeyAsString)) { - if (isFullyDecrypted == true) { + with( + getModifiedNodeKeyDetails( + keyEntity.passphrase, + newPassphrase, + keyEntity.privateKeyAsString + ) + ) { + if (isFullyDecrypted) { throw IllegalArgumentException("Error. The key is decrypted!") } keyEntity.copy( - privateKey = KeyStoreCryptoManager.encryptSuspend(privateKey).toByteArray(), - publicKey = publicKey?.toByteArray() - ?: throw NullPointerException("NodeKeyDetails.publicKey == null"), - passphrase = KeyStoreCryptoManager.encryptSuspend(newPassphrase) + privateKey = KeyStoreCryptoManager.encryptSuspend(privateKey).toByteArray(), + publicKey = publicKey.toByteArray(), + storedPassphrase = KeyStoreCryptoManager.encryptSuspend(newPassphrase.asString) ) } }) @@ -126,7 +131,8 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl saveBackupToInboxLiveData.value = Result.loading() withContext(Dispatchers.IO) { try { - val account = getAccountEntityWithDecryptedInfo(roomDatabase.accountDao().getActiveAccountSuspend()) + val account = + getAccountEntityWithDecryptedInfo(roomDatabase.accountDao().getActiveAccountSuspend()) requireNotNull(account) val sess = OpenStoreHelper.getAccountSess(getApplication(), account) @@ -152,7 +158,8 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl requireNotNull(account) val backup = SecurityUtils.genPrivateKeysBackup(getApplication(), account) - val result = GeneralUtil.writeFileFromStringToUri(getApplication(), destinationUri, backup) > 0 + val result = + GeneralUtil.writeFileFromStringToUri(getApplication(), destinationUri, backup) > 0 saveBackupAsFileLiveData.postValue(Result.success(result)) } catch (e: Exception) { @@ -164,33 +171,60 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl } } - fun encryptAndSaveKeysToDatabase(accountEntity: AccountEntity?, keys: List, - type: KeyDetails.Type, addAccountIfNotExist: Boolean = false) { + fun encryptAndSaveKeysToDatabase( + accountEntity: AccountEntity?, keys: List, + sourceType: KeyImportDetails.SourceType, addAccountIfNotExist: Boolean = false + ) { requireNotNull(accountEntity) viewModelScope.launch { savePrivateKeysLiveData.value = Result.loading() try { for (keyDetails in keys) { - val longId = keyDetails.longId - requireNotNull(longId) - if (roomDatabase.keysDao().getKeyByAccountAndLongIdSuspend(accountEntity.email.toLowerCase(Locale.US), longId) == null) { + val fingerprint = keyDetails.fingerprint + if (roomDatabase.keysDao().getKeyByAccountAndFingerprintSuspend( + accountEntity.email.toLowerCase(Locale.US), + fingerprint + ) == null + ) { if (addAccountIfNotExist) { - val existedAccount = roomDatabase.accountDao().getAccountSuspend(accountEntity.email.toLowerCase(Locale.US)) + val existedAccount = roomDatabase.accountDao() + .getAccountSuspend(accountEntity.email.toLowerCase(Locale.US)) if (existedAccount == null) { roomDatabase.accountDao().addAccountSuspend(accountEntity) } } - val passphrase = if (keyDetails.isFullyDecrypted == true) "" else keyDetails.passphrase - ?: "" - val keyEntity = keyDetails.toKeyEntity(accountEntity) - .copy(source = type.toPrivateKeySourceTypeString(), - privateKey = KeyStoreCryptoManager.encryptSuspend(keyDetails.privateKey).toByteArray(), - passphrase = KeyStoreCryptoManager.encryptSuspend(passphrase)) + val encryptedPassphrase = + if (keyDetails.passphraseType == KeyEntity.PassphraseType.DATABASE) { + val passphrase = if (keyDetails.isFullyDecrypted) { + "" + } else { + keyDetails.tempPassphrase?.let { String(it) } ?: "" + } + + KeyStoreCryptoManager.encryptSuspend(passphrase) + } else null + + val encryptedPrvKey = + KeyStoreCryptoManager.encryptSuspend(keyDetails.privateKey).toByteArray() + + val keyEntity = keyDetails.toKeyEntity(accountEntity).copy( + source = sourceType.toPrivateKeySourceTypeString(), + privateKey = encryptedPrvKey, + storedPassphrase = encryptedPassphrase + ) val isAdded = roomDatabase.keysDao().insertSuspend(keyEntity) > 0 if (isAdded) { + if (keyDetails.passphraseType == KeyEntity.PassphraseType.RAM) { + keysStorage.putPassPhraseToCache( + fingerprint = fingerprint, + passphrase = Passphrase(keyDetails.tempPassphrase), + validUntil = KeysStorageImpl.calculateLifeTimeForPassphrase(), + passphraseType = KeyEntity.PassphraseType.RAM + ) + } //update contacts table val contactsDao = roomDatabase.contactsDao() for (pgpContact in keyDetails.pgpContacts) { @@ -216,8 +250,10 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl /** * Parse keys from the given resource (string or file). */ - fun parseKeys(keyImportModel: KeyImportModel?, isCheckSizeEnabled: Boolean, - filterOnlyPrivate: Boolean = false) { + fun parseKeys( + keyImportModel: KeyImportModel?, isCheckSizeEnabled: Boolean, + filterOnlyPrivate: Boolean = false + ) { viewModelScope.launch { val context: Context = getApplication() try { @@ -230,8 +266,8 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl var parseKeyResult: PgpKey.ParseKeyResult val sourceNotAvailableMsg = context.getString(R.string.source_is_empty_or_not_available) - when (keyImportModel.type) { - KeyDetails.Type.FILE -> { + when (keyImportModel.sourceType) { + KeyImportDetails.SourceType.FILE -> { if (isCheckSizeEnabled && isKeyTooBig(keyImportModel.fileUri)) { throw IllegalArgumentException(context.getString(R.string.file_is_too_big)) } @@ -241,22 +277,25 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl } val source = context.contentResolver.openInputStream(keyImportModel.fileUri) - ?: throw java.lang.IllegalStateException(sourceNotAvailableMsg) + ?: throw java.lang.IllegalStateException(sourceNotAvailableMsg) parseKeyResult = PgpKey.parseKeys(source, false) } - KeyDetails.Type.CLIPBOARD, KeyDetails.Type.EMAIL, KeyDetails.Type.MANUAL_ENTERING -> { + KeyImportDetails.SourceType.CLIPBOARD, KeyImportDetails.SourceType.EMAIL, KeyImportDetails.SourceType.MANUAL_ENTERING -> { val source = keyImportModel.keyString - ?: throw IllegalStateException(sourceNotAvailableMsg) + ?: throw IllegalStateException(sourceNotAvailableMsg) parseKeyResult = PgpKey.parseKeys(source, false) } - else -> throw IllegalStateException("Unsupported : ${keyImportModel.type}") + else -> throw IllegalStateException("Unsupported : ${keyImportModel.sourceType}") } if (filterOnlyPrivate) { parseKeyResult = PgpKey.ParseKeyResult( - PGPKeyRingCollection(parseKeyResult.pgpKeyRingCollection - .pgpSecretKeyRingCollection.keyRings.asSequence().toList(), true)) + PGPKeyRingCollection( + parseKeyResult.pgpKeyRingCollection + .pgpSecretKeyRingCollection.keyRings.asSequence().toList(), true + ) + ) } parseKeysLiveData.value = Result.success(parseKeyResult) @@ -268,27 +307,34 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl } } - fun createPrivateKey(accountEntity: AccountEntity, passphrase: String) { + fun createPrivateKey( + accountEntity: AccountEntity, passphrase: String, + passphraseType: KeyEntity.PassphraseType + ) { viewModelScope.launch { createPrivateKeyLiveData.value = Result.loading() - var nodeKeyDetails: NodeKeyDetails? = null + var pgpKeyDetails: PgpKeyDetails? = null try { - nodeKeyDetails = PGPainless.generateKeyRing().simpleEcKeyRing( - UserId.nameAndEmail(accountEntity.displayName - ?: accountEntity.email, accountEntity.email), passphrase).toNodeKeyDetails() - - val existedAccount = roomDatabase.accountDao().getAccountSuspend(accountEntity.email.toLowerCase(Locale.US)) + pgpKeyDetails = PGPainless.generateKeyRing().simpleEcKeyRing( + UserId.nameAndEmail( + accountEntity.displayName + ?: accountEntity.email, accountEntity.email + ), passphrase + ).toPgpKeyDetails().copy(passphraseType = passphraseType) + + val existedAccount = + roomDatabase.accountDao().getAccountSuspend(accountEntity.email.toLowerCase(Locale.US)) if (existedAccount == null) { roomDatabase.accountDao().addAccountSuspend(accountEntity) } - savePrivateKeyToDatabase(accountEntity, nodeKeyDetails, passphrase) - doAdditionalOperationsAfterKeyCreation(accountEntity, nodeKeyDetails) - createPrivateKeyLiveData.value = Result.success(nodeKeyDetails) + savePrivateKeyToDatabase(accountEntity, pgpKeyDetails, passphrase) + doAdditionalOperationsAfterKeyCreation(accountEntity, pgpKeyDetails) + createPrivateKeyLiveData.value = Result.success(pgpKeyDetails) } catch (e: Exception) { e.printStackTrace() - nodeKeyDetails?.longId?.let { - roomDatabase.keysDao().deleteByAccountAndLongIdSuspend(accountEntity.email, it) + pgpKeyDetails?.fingerprint?.let { + roomDatabase.keysDao().deleteByAccountAndFingerprintSuspend(accountEntity.email, it) } createPrivateKeyLiveData.value = Result.exception(e) ExceptionUtil.handleError(e) @@ -296,14 +342,19 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl } } - fun deleteKeys(accountEntity: AccountEntity, keys: List) { + fun deleteKeys(accountEntity: AccountEntity, keys: List) { viewModelScope.launch { deleteKeysLiveData.value = Result.loading() try { val context: Context = getApplication() - val allKeyEntitiesOfAccount = roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email) - val longIdListOfDeleteCandidates = keys.mapNotNull { it.longId?.toLowerCase(Locale.US) } - val deleteCandidates = allKeyEntitiesOfAccount.filter { longIdListOfDeleteCandidates.contains(it.longId.toLowerCase(Locale.US)) } + val allKeyEntitiesOfAccount = + roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email) + val fingerprintListOfDeleteCandidates = keys.map { + it.fingerprint.toLowerCase(Locale.US) + } + val deleteCandidates = allKeyEntitiesOfAccount.filter { + fingerprintListOfDeleteCandidates.contains(it.fingerprint.toLowerCase(Locale.US)) + } if (keys.size == allKeyEntitiesOfAccount.size) { throw IllegalArgumentException(context.getString(R.string.please_leave_at_least_one_key)) @@ -320,20 +371,28 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl } } - private suspend fun savePrivateKeyToDatabase(accountEntity: AccountEntity, nodeKeyDetails: NodeKeyDetails, passphrase: String) { - val keyEntity = nodeKeyDetails.toKeyEntity(accountEntity).copy( - source = KeyDetails.Type.NEW.toPrivateKeySourceTypeString(), - privateKey = KeyStoreCryptoManager.encryptSuspend(nodeKeyDetails.privateKey).toByteArray(), - passphrase = KeyStoreCryptoManager.encryptSuspend(passphrase)) + private suspend fun savePrivateKeyToDatabase( + accountEntity: AccountEntity, + pgpKeyDetails: PgpKeyDetails, + passphrase: String + ) { + val keyEntity = pgpKeyDetails.toKeyEntity(accountEntity).copy( + source = KeyImportDetails.SourceType.NEW.toPrivateKeySourceTypeString(), + privateKey = KeyStoreCryptoManager.encryptSuspend(pgpKeyDetails.privateKey).toByteArray(), + storedPassphrase = KeyStoreCryptoManager.encryptSuspend(passphrase) + ) if (roomDatabase.keysDao().insertSuspend(keyEntity) == -1L) { throw NullPointerException("Cannot save a generated private key") } } - private suspend fun doAdditionalOperationsAfterKeyCreation(accountEntity: AccountEntity, nodeKeyDetails: NodeKeyDetails) { + private suspend fun doAdditionalOperationsAfterKeyCreation( + accountEntity: AccountEntity, + pgpKeyDetails: PgpKeyDetails + ) { if (accountEntity.isRuleExist(AccountEntity.DomainRule.ENFORCE_ATTESTER_SUBMIT)) { - val model = InitialLegacySubmitModel(accountEntity.email, nodeKeyDetails.publicKey!!) + val model = InitialLegacySubmitModel(accountEntity.email, pgpKeyDetails.publicKey) val initialLegacySubmitResult = apiRepository.postInitialLegacySubmit(getApplication(), model) when (initialLegacySubmitResult.status) { @@ -351,75 +410,93 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl } if (!accountEntity.isRuleExist(AccountEntity.DomainRule.NO_PRV_BACKUP)) { - if (!saveCreatedPrivateKeyAsBackupToInbox(accountEntity, nodeKeyDetails)) { - val backupAction = ActionQueueEntity.fromAction(BackupPrivateKeyToInboxAction(0, - accountEntity.email, 0, nodeKeyDetails.longId!!)) + if (!saveCreatedPrivateKeyAsBackupToInbox(accountEntity, pgpKeyDetails)) { + val backupAction = ActionQueueEntity.fromAction( + BackupPrivateKeyToInboxAction( + 0, + accountEntity.email, 0, pgpKeyDetails.fingerprint + ) + ) backupAction?.let { action -> roomDatabase.actionQueueDao().insertSuspend(action) } } } } else { if (!accountEntity.isRuleExist(AccountEntity.DomainRule.NO_PRV_BACKUP)) { - if (!saveCreatedPrivateKeyAsBackupToInbox(accountEntity, nodeKeyDetails)) { - val backupAction = ActionQueueEntity.fromAction(BackupPrivateKeyToInboxAction(0, - accountEntity.email, 0, nodeKeyDetails.longId!!)) + if (!saveCreatedPrivateKeyAsBackupToInbox(accountEntity, pgpKeyDetails)) { + val backupAction = ActionQueueEntity.fromAction( + BackupPrivateKeyToInboxAction( + 0, + accountEntity.email, 0, pgpKeyDetails.fingerprint + ) + ) backupAction?.let { action -> roomDatabase.actionQueueDao().insertSuspend(action) } } } - if (!registerUserPublicKey(accountEntity, nodeKeyDetails)) { - val registerAction = ActionQueueEntity.fromAction(RegisterUserPublicKeyAction(0, - accountEntity.email, 0, nodeKeyDetails.publicKey!!)) + if (!registerUserPublicKey(accountEntity, pgpKeyDetails)) { + val registerAction = ActionQueueEntity.fromAction( + RegisterUserPublicKeyAction( + 0, + accountEntity.email, 0, pgpKeyDetails.publicKey + ) + ) registerAction?.let { action -> roomDatabase.actionQueueDao().insertSuspend(action) } } } - if (!requestingTestMsgWithNewPublicKey(accountEntity, nodeKeyDetails)) { - val welcomeEmailAction = ActionQueueEntity.fromAction(SendWelcomeTestEmailAction(0, - accountEntity.email, 0, nodeKeyDetails.publicKey!!)) + if (!requestingTestMsgWithNewPublicKey(accountEntity, pgpKeyDetails)) { + val welcomeEmailAction = ActionQueueEntity.fromAction( + SendWelcomeTestEmailAction( + 0, + accountEntity.email, 0, pgpKeyDetails.publicKey + ) + ) welcomeEmailAction?.let { action -> roomDatabase.actionQueueDao().insertSuspend(action) } } } - private suspend fun getModifiedNodeKeyDetails(oldPassphrase: String?, - newPassphrase: String, - originalPrivateKey: String?): NodeKeyDetails = - withContext(Dispatchers.IO) { - val keyDetailsList = PgpKey.parseKeys(originalPrivateKey!!.toByteArray()) - .toNodeKeyDetailsList() - if (CollectionUtils.isEmpty(keyDetailsList) || keyDetailsList.size != 1) { - throw IllegalStateException("Parse keys error") - } - - val nodeKeyDetails = keyDetailsList[0] - val longId = nodeKeyDetails.longId + private suspend fun getModifiedNodeKeyDetails( + oldPassphrase: Passphrase, + newPassphrase: Passphrase, + originalPrivateKey: String? + ): PgpKeyDetails = + withContext(Dispatchers.IO) { + val keyDetailsList = PgpKey.parseKeys(originalPrivateKey!!.toByteArray()) + .toPgpKeyDetailsList() + if (CollectionUtils.isEmpty(keyDetailsList) || keyDetailsList.size != 1) { + throw IllegalStateException("Parse keys error") + } - if (TextUtils.isEmpty(oldPassphrase)) { - throw IllegalStateException("Passphrase for key with longid $longId not found") - } + val nodeKeyDetails = keyDetailsList[0] + val fingerprint = nodeKeyDetails.fingerprint - val encryptedKey: String - try { - encryptedKey = PgpKey.changeKeyPassphrase( - nodeKeyDetails.privateKey!!, - Passphrase.fromPassword(oldPassphrase!!), - Passphrase.fromPassword(newPassphrase) - ) - } catch (e: Exception) { - throw IllegalStateException( - "Can't change passphrase for the key with longid " + longId!!, - e - ) - } + if (oldPassphrase.isEmpty) { + throw IllegalStateException("Passphrase for key with fingerprint $fingerprint not found") + } - val modifiedKeyDetailsList = PgpKey.parseKeys(encryptedKey.toByteArray()) - .toNodeKeyDetailsList() - if (CollectionUtils.isEmpty(modifiedKeyDetailsList) || modifiedKeyDetailsList.size != 1) { - throw IllegalStateException("Parse keys error") - } + val encryptedKey: String + try { + encryptedKey = PgpKey.changeKeyPassphrase( + nodeKeyDetails.privateKey!!, + oldPassphrase, + newPassphrase + ) + } catch (e: Exception) { + throw IllegalStateException( + "Can't change passphrase for the key with fingerprint " + fingerprint, + e + ) + } - modifiedKeyDetailsList[0] + val modifiedKeyDetailsList = PgpKey.parseKeys(encryptedKey.toByteArray()) + .toPgpKeyDetailsList() + if (CollectionUtils.isEmpty(modifiedKeyDetailsList) || modifiedKeyDetailsList.size != 1) { + throw IllegalStateException("Parse keys error") } + modifiedKeyDetailsList[0] + } + /** * Check that the key size not bigger then [.MAX_SIZE_IN_BYTES]. * @@ -435,45 +512,51 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl * * @return true if message was send. */ - private suspend fun saveCreatedPrivateKeyAsBackupToInbox(accountEntity: AccountEntity, - keyDetails: NodeKeyDetails): Boolean = - withContext(Dispatchers.IO) { - try { - val context: Context = getApplication() - val session = OpenStoreHelper.getAccountSess(context, accountEntity) - val transport = SmtpProtocolUtil.prepareSmtpTransport(context, session, accountEntity) - val msg = EmailUtil.genMsgWithPrivateKeys(context, accountEntity, session, - EmailUtil.genBodyPartWithPrivateKey(accountEntity, keyDetails.privateKey!!)) - transport.sendMessage(msg, msg.allRecipients) - return@withContext true - } catch (e: Exception) { - e.printStackTrace() - return@withContext false - } + private suspend fun saveCreatedPrivateKeyAsBackupToInbox( + accountEntity: AccountEntity, + keyDetails: PgpKeyDetails + ): Boolean = + withContext(Dispatchers.IO) { + try { + val context: Context = getApplication() + val session = OpenStoreHelper.getAccountSess(context, accountEntity) + val transport = SmtpProtocolUtil.prepareSmtpTransport(context, session, accountEntity) + val msg = EmailUtil.genMsgWithPrivateKeys( + context, accountEntity, session, + EmailUtil.genBodyPartWithPrivateKey(accountEntity, keyDetails.privateKey!!) + ) + transport.sendMessage(msg, msg.allRecipients) + return@withContext true + } catch (e: Exception) { + e.printStackTrace() + return@withContext false } + } - private suspend fun genContacts(accountEntity: AccountEntity): List = withContext(Dispatchers.IO) { - val pgpContactMain = PgpContact(accountEntity.email, accountEntity.displayName) - val contacts = ArrayList() - - when (accountEntity.accountType) { - AccountEntity.ACCOUNT_TYPE_GOOGLE -> { - contacts.add(pgpContactMain) - val gmail = GmailApiHelper.generateGmailApiService(getApplication(), accountEntity) - val aliases = gmail.users().settings().sendAs().list(GmailApiHelper.DEFAULT_USER_ID).execute() - for (alias in aliases.sendAs) { - if (alias.verificationStatus != null) { - contacts.add(PgpContact(alias.sendAsEmail, alias.displayName)) + private suspend fun genContacts(accountEntity: AccountEntity): List = + withContext(Dispatchers.IO) { + val pgpContactMain = PgpContact(accountEntity.email, accountEntity.displayName) + val contacts = ArrayList() + + when (accountEntity.accountType) { + AccountEntity.ACCOUNT_TYPE_GOOGLE -> { + contacts.add(pgpContactMain) + val gmail = GmailApiHelper.generateGmailApiService(getApplication(), accountEntity) + val aliases = + gmail.users().settings().sendAs().list(GmailApiHelper.DEFAULT_USER_ID).execute() + for (alias in aliases.sendAs) { + if (alias.verificationStatus != null) { + contacts.add(PgpContact(alias.sendAsEmail, alias.displayName)) + } } } + + else -> contacts.add(pgpContactMain) } - else -> contacts.add(pgpContactMain) + return@withContext contacts } - return@withContext contacts - } - /** * Registering a key with attester API. * Note: this will only be successful if it's the first time submitting a key for this email address, or if the @@ -484,9 +567,12 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl * @param keyDetails Details of the created key. * @return true if no errors. */ - private suspend fun registerUserPublicKey(accountEntity: AccountEntity, keyDetails: NodeKeyDetails): Boolean = withContext(Dispatchers.IO) { + private suspend fun registerUserPublicKey( + accountEntity: AccountEntity, + keyDetails: PgpKeyDetails + ): Boolean = withContext(Dispatchers.IO) { return@withContext try { - val model = InitialLegacySubmitModel(accountEntity.email, keyDetails.publicKey!!) + val model = InitialLegacySubmitModel(accountEntity.email, keyDetails.publicKey) val initialLegacySubmitResult = apiRepository.postInitialLegacySubmit(getApplication(), model) when (initialLegacySubmitResult.status) { Result.Status.SUCCESS -> { @@ -510,26 +596,29 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl * @param keyDetails Details of the created key. * @return true if no errors. */ - private suspend fun requestingTestMsgWithNewPublicKey(accountEntity: AccountEntity, keyDetails: NodeKeyDetails): Boolean = - withContext(Dispatchers.IO) { - return@withContext try { - val model = TestWelcomeModel(accountEntity.email, keyDetails.publicKey!!) - val testWelcomeResult = apiRepository.postTestWelcome(getApplication(), model) - when (testWelcomeResult.status) { - Result.Status.SUCCESS -> { - val testWelcomeResponse = testWelcomeResult.data - testWelcomeResponse != null && testWelcomeResponse.isSent - } + private suspend fun requestingTestMsgWithNewPublicKey( + accountEntity: AccountEntity, + keyDetails: PgpKeyDetails + ): Boolean = + withContext(Dispatchers.IO) { + return@withContext try { + val model = TestWelcomeModel(accountEntity.email, keyDetails.publicKey) + val testWelcomeResult = apiRepository.postTestWelcome(getApplication(), model) + when (testWelcomeResult.status) { + Result.Status.SUCCESS -> { + val testWelcomeResponse = testWelcomeResult.data + testWelcomeResponse != null && testWelcomeResponse.isSent + } - else -> { - false - } + else -> { + false } - } catch (e: Exception) { - e.printStackTrace() - false } + } catch (e: Exception) { + e.printStackTrace() + false } + } companion object { /** diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SubmitPubKeyViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SubmitPubKeyViewModel.kt index 07840588a6..67282b6012 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SubmitPubKeyViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SubmitPubKeyViewModel.kt @@ -15,10 +15,10 @@ import com.flowcrypt.email.api.retrofit.request.model.InitialLegacySubmitModel import com.flowcrypt.email.api.retrofit.response.attester.InitialLegacySubmitResponse import com.flowcrypt.email.api.retrofit.response.base.ApiResponse import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.ActionQueueEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.service.actionqueue.actions.RegisterUserPublicKeyAction import kotlinx.coroutines.launch @@ -32,7 +32,7 @@ class SubmitPubKeyViewModel(application: Application) : BaseAndroidViewModel(app private val repository: ApiRepository = FlowcryptApiRepository() val submitPubKeyLiveData: MutableLiveData?> = MutableLiveData() - fun submitPubKey(account: AccountEntity, keys: List) { + fun submitPubKey(account: AccountEntity, keys: List) { submitPubKeyLiveData.value = Result.loading() val context: Context = getApplication() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/factory/PgpKeyDetailsViewModelFactory.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/factory/PgpKeyDetailsViewModelFactory.kt new file mode 100644 index 0000000000..25cefe87da --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/factory/PgpKeyDetailsViewModelFactory.kt @@ -0,0 +1,28 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.jetpack.viewmodel.factory + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.flowcrypt.email.jetpack.viewmodel.PgpKeyDetailsViewModel + +/** + * @author Denis Bondarenko + * Date: 5/24/21 + * Time: 5:35 PM + * E-mail: DenBond7@gmail.com + */ +class PgpKeyDetailsViewModelFactory( + val fingerprint: String?, + val application: Application +) : + ViewModelProvider.AndroidViewModelFactory(application) { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return PgpKeyDetailsViewModel(fingerprint, application) as T + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyDetails.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportDetails.kt similarity index 52% rename from FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyDetails.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportDetails.kt index c352ec036b..b70758972b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyDetails.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportDetails.kt @@ -13,9 +13,9 @@ import com.flowcrypt.email.security.model.PrivateKeySourceType * This class describes a details about the key. The key can be one of three * different types: * - * * [KeyDetails.Type.EMAIL] - * * [KeyDetails.Type.FILE] - * * [KeyDetails.Type.CLIPBOARD] + * * [KeyImportDetails.SourceType.EMAIL] + * * [KeyImportDetails.SourceType.FILE] + * * [KeyImportDetails.SourceType.CLIPBOARD] * * * @author Denis Bondarenko @@ -23,26 +23,26 @@ import com.flowcrypt.email.security.model.PrivateKeySourceType * Time: 12:56 * E-mail: DenBond7@gmail.com */ -data class KeyDetails constructor(val keyName: String? = null, - val value: String, - val bornType: Type, - val isPrivateKey: Boolean = false, - val pgpContact: PgpContact? = null) : Parcelable { +data class KeyImportDetails constructor(val keyName: String? = null, + val value: String, + val sourceType: SourceType, + val isPrivateKey: Boolean = false, + val pgpContact: PgpContact? = null) : Parcelable { - constructor(value: String, bornType: Type) : this(null, value, bornType, true) - constructor(value: String, bornType: Type, isPrivateKey: Boolean) : this(null, value, bornType, isPrivateKey, null) + constructor(value: String, sourceType: SourceType) : this(null, value, sourceType, true) + constructor(value: String, sourceType: SourceType, isPrivateKey: Boolean) : this(null, value, sourceType, isPrivateKey, null) /** * The key available types. */ - enum class Type : Parcelable { + enum class SourceType : Parcelable { EMAIL, FILE, CLIPBOARD, NEW, MANUAL_ENTERING; companion object { @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): Type = values()[source.readInt()] - override fun newArray(size: Int): Array = arrayOfNulls(size) + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): SourceType = values()[source.readInt()] + override fun newArray(size: Int): Array = arrayOfNulls(size) } } @@ -66,7 +66,7 @@ data class KeyDetails constructor(val keyName: String? = null, constructor(source: Parcel) : this( source.readString(), source.readString()!!, - source.readParcelable(Type::class.java.classLoader)!!, + source.readParcelable(SourceType::class.java.classLoader)!!, source.readInt() == 1, source.readParcelable(PgpContact::class.java.classLoader)!! ) @@ -79,16 +79,16 @@ data class KeyDetails constructor(val keyName: String? = null, with(dest) { writeString(keyName) writeString(value) - writeParcelable(bornType, flags) + writeParcelable(sourceType, flags) writeInt((if (isPrivateKey) 1 else 0)) writeParcelable(pgpContact, flags) } companion object { @JvmField - val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): KeyDetails = KeyDetails(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): KeyImportDetails = KeyImportDetails(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportModel.kt index ddd0bc0b65..14369945ff 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeyImportModel.kt @@ -20,12 +20,12 @@ import android.os.Parcelable data class KeyImportModel constructor(val fileUri: Uri? = null, val keyString: String? = null, val isPrivateKey: Boolean = false, - val type: KeyDetails.Type) : Parcelable { + val sourceType: KeyImportDetails.SourceType) : Parcelable { constructor(source: Parcel) : this( source.readParcelable(Uri::class.java.classLoader), source.readString(), source.readInt() == 1, - source.readParcelable(KeyDetails.Type::class.java.classLoader)!! + source.readParcelable(KeyImportDetails.SourceType::class.java.classLoader)!! ) override fun describeContents(): Int { @@ -37,7 +37,7 @@ data class KeyImportModel constructor(val fileUri: Uri? = null, writeParcelable(fileUri, flags) writeString(keyString) writeInt((if (isPrivateKey) 1 else 0)) - writeParcelable(type, flags) + writeParcelable(sourceType, flags) } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt index 0b69299f37..69b766438a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/KeysStorage.kt @@ -6,37 +6,41 @@ package com.flowcrypt.email.model import androidx.annotation.Keep -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.security.model.PgpKeyDetails +import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.util.Passphrase +import java.time.Instant @Keep interface KeysStorage { + fun getRawKeys(): List - fun getAllPgpPrivateKeys(): List + fun getPGPSecretKeyRings(): List - fun getPgpPrivateKey(longId: String?): KeyEntity? + fun getPgpKeyDetailsList(): List - /** - * if 2 keys requested and only one found, will return list of 1: [KeyEntity] - */ - fun getFilteredPgpPrivateKeys(longIds: Array): List + fun getPGPSecretKeyRingByFingerprint(fingerprint: String): PGPSecretKeyRing? - /** - * Get [List] of [KeyEntity] where each key has [PgpContact] with the given email. - * - * Note: this method returns a list of not-expired [KeyEntity] only - */ - fun getPgpPrivateKeysByEmail(email: String?): List + fun getPGPSecretKeyRingsByFingerprints(fingerprints: Collection): List - /** - * Get [List] of [NodeKeyDetails] where each key has [PgpContact] with the given email. - * - * Note: this method returns a list of not-expired [NodeKeyDetails] only - */ - fun getNodeKeyDetailsListByEmail(email: String?): List + fun getPGPSecretKeyRingsByUserId(user: String): List + + fun getPassphraseByFingerprint(fingerprint: String): Passphrase? + + fun getPassphraseTypeByFingerprint(fingerprint: String): KeyEntity.PassphraseType? fun getSecretKeyRingProtector(): SecretKeyRingProtector + + fun updatePassPhrasesCache() + + fun putPassPhraseToCache( + fingerprint: String, + passphrase: Passphrase, + validUntil: Instant, + passphraseType: KeyEntity.PassphraseType + ) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/PgpContact.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/PgpContact.kt index 896ecdb439..c7b6e815d3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/PgpContact.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/PgpContact.kt @@ -7,8 +7,8 @@ package com.flowcrypt.email.model import android.os.Parcel import android.os.Parcelable -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.ContactEntity +import com.flowcrypt.email.security.model.PgpKeyDetails import java.util.* data class PgpContact constructor(var email: String, @@ -17,9 +17,8 @@ data class PgpContact constructor(var email: String, var hasPgp: Boolean = false, var client: String? = null, var fingerprint: String? = null, - var longid: String? = null, var lastUse: Long = 0, - var nodeKeyDetails: NodeKeyDetails? = null) : Parcelable { + var pgpKeyDetails: PgpKeyDetails? = null) : Parcelable { constructor(source: Parcel) : this( source.readString()!!, @@ -28,9 +27,8 @@ data class PgpContact constructor(var email: String, source.readInt() == 1, source.readString(), source.readString(), - source.readString(), source.readLong(), - source.readParcelable(NodeKeyDetails::class.java.classLoader) + source.readParcelable(PgpKeyDetails::class.java.classLoader) ) constructor(email: String, name: String?) : this(email) { @@ -49,9 +47,8 @@ data class PgpContact constructor(var email: String, writeInt((if (hasPgp) 1 else 0)) writeString(client) writeString(fingerprint) - writeString(longid) writeLong(lastUse) - writeParcelable(nodeKeyDetails, flags) + writeParcelable(pgpKeyDetails, flags) } fun toContactEntity(): ContactEntity { @@ -62,7 +59,6 @@ data class PgpContact constructor(var email: String, hasPgp = hasPgp, client = client, fingerprint = fingerprint, - longId = longid, lastUse = lastUse, attested = false ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/PublicKeyInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/PublicKeyInfo.kt index 26613569c9..1f0f07ae83 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/PublicKeyInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/model/PublicKeyInfo.kt @@ -20,18 +20,16 @@ import java.util.* */ data class PublicKeyInfo constructor(val fingerprint: String, val keyOwner: String, - val longId: String, var pgpContact: PgpContact? = null, val publicKey: String) : Parcelable { val isUpdateEnabled: Boolean - get() = pgpContact != null && (pgpContact!!.longid == null || pgpContact!!.longid != longId) + get() = pgpContact != null && (pgpContact!!.fingerprint == null || pgpContact!!.fingerprint != fingerprint) fun hasPgpContact(): Boolean { return pgpContact != null } constructor(source: Parcel) : this( - source.readString()!!, source.readString()!!, source.readString()!!, source.readParcelable(PgpContact::class.java.classLoader), @@ -43,7 +41,6 @@ data class PublicKeyInfo constructor(val fingerprint: String, override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { writeString(fingerprint) writeString(keyOwner) - writeString(longId) writeParcelable(pgpContact, flags) writeString(publicKey) } @@ -53,8 +50,7 @@ data class PublicKeyInfo constructor(val fingerprint: String, email = keyOwner.toLowerCase(Locale.getDefault()), publicKey = publicKey.toByteArray(), hasPgp = true, - fingerprint = fingerprint, - longId = longId + fingerprint = fingerprint ) } @@ -66,7 +62,6 @@ data class PublicKeyInfo constructor(val fingerprint: String, hasPgp = true, client = null, fingerprint = fingerprint, - longid = longId, lastUse = 0 ) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/node/TestData.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/node/TestData.kt index d42fd2e917..fdbeaddc91 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/node/TestData.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/node/TestData.kt @@ -8,6 +8,10 @@ package com.flowcrypt.email.node import com.flowcrypt.email.database.entity.KeyEntity import java.util.* +@Deprecated( + "old code. Some of these tests are wrongly having longids " + + "in them, and we plan to remove the class soon." +) class TestData internal constructor() { companion object { private const val ECC_PUB_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + @@ -322,38 +326,47 @@ class TestData internal constructor() { @JvmStatic fun eccPrvKeyInfo(): Array { - return arrayOf(KeyEntity( - longId = "063635B3E33EB14C", + return arrayOf( + KeyEntity( + fingerprint = "063635B3E33EB14C", account = "usr@usr.com", source = "TEST", privateKey = ECC_PRV_KEY.toByteArray(), publicKey = ECC_PUB_KEY.toByteArray(), - passphrase = "some long pp" - )) + storedPassphrase = "some long pp", + passphraseType = KeyEntity.PassphraseType.DATABASE + ) + ) } @JvmStatic fun rsa2048PrvKeyInfo(): Array { - return arrayOf(KeyEntity( - longId = "3A30F4CC0A9A8F10", + return arrayOf( + KeyEntity( + fingerprint = "3A30F4CC0A9A8F10", account = "t@est.com", source = "TEST", privateKey = RSA_2048_PRV_KEY.toByteArray(), publicKey = RSA_2048_PUB_KEY.toByteArray(), - passphrase = "some long pp" - )) + storedPassphrase = "some long pp", + passphraseType = KeyEntity.PassphraseType.DATABASE + ) + ) } @JvmStatic fun rsa4096PrvKeyInfo(): Array { - return arrayOf(KeyEntity( - longId = "7C307E6F2092962D", + return arrayOf( + KeyEntity( + fingerprint = "7C307E6F2092962D", account = "usr@usr.com", source = "TEST", privateKey = RSA_4096_PRV_KEY.toByteArray(), publicKey = RSA_4096_PUB_KEY.toByteArray(), - passphrase = "some long pp" - )) + storedPassphrase = "some long pp", + passphraseType = KeyEntity.PassphraseType.DATABASE + ) + ) } @JvmStatic diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt index c855019cf0..a87bed4ab3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt @@ -6,29 +6,28 @@ package com.flowcrypt.email.security import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData import androidx.lifecycle.switchMap -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.FlowCryptRoomDatabase -import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.KeyEntity -import com.flowcrypt.email.extensions.org.pgpainless.key.longId +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails import com.flowcrypt.email.model.KeysStorage -import com.flowcrypt.email.node.Node +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpDecrypt import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.exception.DecryptionException -import org.bouncycastle.bcpg.ArmoredInputStream import org.bouncycastle.openpgp.PGPException -import org.pgpainless.PGPainless +import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.key.protection.KeyRingProtectionSettings import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.util.Passphrase -import java.io.ByteArrayInputStream +import java.time.Instant +import java.util.* +import java.util.concurrent.TimeUnit +import javax.mail.internet.InternetAddress /** * This class implements [KeysStorage]. Here we collect information about imported private keys @@ -38,123 +37,130 @@ import java.io.ByteArrayInputStream * Date: 05.05.2017 * Time: 13:06 * E-mail: DenBond7@gmail.com + * + * @version 2.0 Updated to use [Passphrase] and [PGPSecretKeyRing] */ class KeysStorageImpl private constructor(context: Context) : KeysStorage { private val roomDatabase = FlowCryptRoomDatabase.getDatabase(context) - private val nodeLiveData = Node.getInstance(context.applicationContext).liveData - private var keys = mutableListOf() - private var nodeKeyDetailsList = mutableListOf() + private val passPhraseMap = TreeMap(String.CASE_INSENSITIVE_ORDER) - private val pureActiveAccountLiveData: LiveData = - Transformations.switchMap(nodeLiveData) { - roomDatabase.accountDao().getActiveAccountLD() - } + private val pureActiveAccountLiveData = roomDatabase.accountDao().getActiveAccountLD() - private val encryptedKeysLiveData: LiveData> = - Transformations.switchMap(pureActiveAccountLiveData) { - roomDatabase.keysDao().getAllKeysByAccountLD(it?.email ?: "") - } + private val encryptedKeysLiveData = pureActiveAccountLiveData.switchMap { + roomDatabase.keysDao().getAllKeysByAccountLD(it?.email ?: "") + } private val keysLiveData = encryptedKeysLiveData.switchMap { list -> liveData { emit(list.map { it.copy( privateKey = KeyStoreCryptoManager.decryptSuspend(it.privateKeyAsString).toByteArray(), - passphrase = KeyStoreCryptoManager.decryptSuspend(it.passphrase) + storedPassphrase = KeyStoreCryptoManager.decryptSuspend(it.storedPassphrase) ) }) } } - val nodeKeyDetailsLiveData: LiveData> = - Transformations.switchMap(keysLiveData) { - liveData { - emit( - PgpKey.parseKeys( - it.joinToString(separator = "\n") { keyEntity -> keyEntity.privateKeyAsString }) - .toNodeKeyDetailsList() - ) - } + val secretKeyRingsLiveData = keysLiveData.switchMap { + liveData { + val combinedSource = + it.joinToString(separator = "\n") { keyEntity -> keyEntity.privateKeyAsString } + val parseKeyResult = PgpKey.parseKeys(combinedSource) + val keys = parseKeyResult.pgpKeyRingCollection.pgpSecretKeyRingCollection.keyRings + .asSequence().toList() + emit(keys) } + } + + val passphrasesUpdatesLiveData = MutableLiveData() init { keysLiveData.observeForever { - keys.clear() - keys.addAll(it) + preparePassphrasesMap(it) } + } - nodeKeyDetailsLiveData.observeForever { - nodeKeyDetailsList.clear() - nodeKeyDetailsList.addAll(it) - } + override fun getRawKeys(): List { + return keysLiveData.value ?: emptyList() } - override fun getPgpPrivateKey(longId: String?): KeyEntity? { - return keys.firstOrNull { it.longId.equals(longId, true) } + override fun getPGPSecretKeyRings(): List { + return secretKeyRingsLiveData.value ?: emptyList() } - override fun getFilteredPgpPrivateKeys(longIds: Array): List { - return keys.filter { longIds.contains(it.longId) } + override fun getPgpKeyDetailsList(): List { + val list = mutableListOf() + for (secretKey in getPGPSecretKeyRings()) { + val pgpKeyDetails = secretKey.toPgpKeyDetails() + val passphrase = getPassphraseByFingerprint(pgpKeyDetails.fingerprint) + list.add(pgpKeyDetails.copy(tempPassphrase = passphrase?.chars)) + } + return list } - override fun getPgpPrivateKeysByEmail(email: String?): List { - val keys = mutableListOf() + override fun getPGPSecretKeyRingByFingerprint(fingerprint: String): PGPSecretKeyRing? { + return getPGPSecretKeyRings().firstOrNull { + val openPgpV4Fingerprint = OpenPgpV4Fingerprint(it.secretKey) + openPgpV4Fingerprint.toString().equals(fingerprint, true) + } + } - nodeKeyDetailsList.forEach { nodeKeyDetails -> - for (contact in nodeKeyDetails.pgpContacts) { - if (email?.equals(contact.email, true) == true && !nodeKeyDetails.isExpired) { - getPgpPrivateKey(nodeKeyDetails.longId)?.let { keyEntity -> keys.add(keyEntity) } - } + override fun getPGPSecretKeyRingsByFingerprints(fingerprints: Collection): + List { + val list = mutableListOf() + val set = fingerprints.map { it.toUpperCase(Locale.US) }.toSet() + for (secretKey in getPGPSecretKeyRings()) { + val openPgpV4Fingerprint = OpenPgpV4Fingerprint(secretKey) + if (openPgpV4Fingerprint.toString() in set) { + list.add(secretKey) } } - - return keys + return list } - override fun getNodeKeyDetailsListByEmail(email: String?): List { - val list = mutableListOf() - - nodeKeyDetailsList.forEach { nodeKeyDetails -> - for (contact in nodeKeyDetails.pgpContacts) { - if (email?.equals(contact.email, true) == true && !nodeKeyDetails.isExpired) { - list.add(nodeKeyDetails) + override fun getPGPSecretKeyRingsByUserId(user: String): List { + val list = mutableListOf() + for (secretKey in getPGPSecretKeyRings()) { + for (userId in secretKey.publicKey.userIDs) { + try { + val internetAddresses = InternetAddress.parse(userId) + for (internetAddress in internetAddresses) { + if (user.equals(internetAddress.address, true)) { + list.add(secretKey) + continue + } + } + } catch (e: Exception) { + e.printStackTrace() } } } - return list } - override fun getSecretKeyRingProtector(): SecretKeyRingProtector { - val prvKeys = keys.map { it.privateKeyAsString } - val inputStream = ByteArrayInputStream(prvKeys.joinToString(separator = "\n").toByteArray()) - val pgpSecretKeyRingCollection = inputStream.use { - ArmoredInputStream(it).use { armoredInputStream -> - PGPainless.readKeyRing().secretKeyRingCollection(armoredInputStream) - } - } + override fun getPassphraseByFingerprint(fingerprint: String): Passphrase? { + return passPhraseMap[fingerprint]?.passphrase + } + override fun getPassphraseTypeByFingerprint(fingerprint: String): KeyEntity.PassphraseType? { + return passPhraseMap[fingerprint]?.passphraseType + } + + override fun getSecretKeyRingProtector(): SecretKeyRingProtector { val keyRingProtectionSettings = KeyRingProtectionSettings.secureDefaultSettings() + val availablePGPSecretKeyRings = getPGPSecretKeyRings() return PasswordBasedSecretKeyRingProtector(keyRingProtectionSettings) { keyId -> - for (pgpSecretKeyRing in pgpSecretKeyRingCollection) { + for (pgpSecretKeyRing in availablePGPSecretKeyRings) { val keyIDs = pgpSecretKeyRing.secretKeys.iterator().asSequence().map { it.keyID } if (keyIDs.contains(keyId)) { for (secretKey in pgpSecretKeyRing.secretKeys) { val openPgpV4Fingerprint = OpenPgpV4Fingerprint(secretKey) - val key = getPgpPrivateKey(openPgpV4Fingerprint.longId) - if (key != null) { - val passphrase: Passphrase - if (key.passphrase == null) { - throw DecryptionException( - decryptionErrorType = PgpDecrypt.DecryptionErrorType.NEED_PASSPHRASE, - e = PGPException("flowcrypt: need passphrase") - ) - } else { - passphrase = Passphrase.fromPassword(key.passphrase) - } - - return@PasswordBasedSecretKeyRingProtector passphrase - } + val passphrase = getPassphraseByFingerprint(openPgpV4Fingerprint.toString()) + ?: throw DecryptionException( + decryptionErrorType = PgpDecrypt.DecryptionErrorType.NEED_PASSPHRASE, + e = PGPException("flowcrypt: need passphrase") + ) + return@PasswordBasedSecretKeyRingProtector passphrase } } } @@ -163,52 +169,99 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage { } } - override fun getAllPgpPrivateKeys(): List { - return keys + override fun updatePassPhrasesCache() { + for (key in getRawKeys()) { + if (key.passphraseType == KeyEntity.PassphraseType.RAM) { + val entry = passPhraseMap[key.fingerprint] ?: continue + if (entry.passphrase.isEmpty) continue + val now = Instant.now() + if (entry.validUntil == now || entry.validUntil.isBefore(now)) { + passPhraseMap[key.fingerprint] = entry.copy( + passphrase = Passphrase.emptyPassphrase() + ) + passphrasesUpdatesLiveData.postValue(System.currentTimeMillis()) + } + } + } } - /** - * Return the latest all private keys for an active account. We can use this method to fetch - * keys as they are stored in the database. It can't be used in UI thread. - */ - suspend fun getLatestAllPgpPrivateKeys(): List { - val account = pureActiveAccountLiveData.value - ?: roomDatabase.accountDao().getActiveAccountSuspend() - account?.let { accountEntity -> - val cachedKeysLongIds = keys.map { it.longId }.toSet() - val latestEncryptedKeys = - roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email) - val latestKeysLongIds = - roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email).map { it.longId } - .toSet() + override fun putPassPhraseToCache( + fingerprint: String, + passphrase: Passphrase, + validUntil: Instant, + passphraseType: KeyEntity.PassphraseType + ) { + passPhraseMap[fingerprint] = PassPhraseInRAM( + passphrase = passphrase, + validUntil = validUntil, + passphraseType = passphraseType + ) - if (cachedKeysLongIds == latestKeysLongIds) { - return keys - } + passphrasesUpdatesLiveData.postValue(System.currentTimeMillis()) + } - return latestEncryptedKeys.map { - it.copy( - privateKey = KeyStoreCryptoManager.decryptSuspend(it.privateKeyAsString).toByteArray(), - passphrase = KeyStoreCryptoManager.decryptSuspend(it.passphrase) - ) - } + private fun preparePassphrasesMap(keyEntityList: List) { + val existedIdList = passPhraseMap.keys + val refreshedIdList = keyEntityList.map { it.fingerprint } + val removeCandidates = existedIdList - refreshedIdList + val addCandidates = refreshedIdList - existedIdList + val updateCandidates = refreshedIdList - addCandidates + + for (id in removeCandidates) { + passPhraseMap.remove(id) } - return emptyList() - } + for (keyEntity in keyEntityList) { + val id = keyEntity.fingerprint + if (id in updateCandidates) { + if (keyEntity.passphraseType == KeyEntity.PassphraseType.DATABASE) { + passPhraseMap[id] = PassPhraseInRAM( + passphrase = keyEntity.passphrase, + validUntil = Instant.MAX, + passphraseType = keyEntity.passphraseType + ) + } + } - private fun getDecryptedKeyEntity(keyEntity: KeyEntity): KeyEntity { - val privateKey = KeyStoreCryptoManager.decrypt(keyEntity.privateKeyAsString) - val passphrase = KeyStoreCryptoManager.decrypt(keyEntity.passphrase) + if (id in addCandidates) { + when (keyEntity.passphraseType) { + KeyEntity.PassphraseType.RAM -> { + passPhraseMap[id] = PassPhraseInRAM( + passphrase = Passphrase.emptyPassphrase(), + validUntil = Instant.now(), + passphraseType = keyEntity.passphraseType + ) + } - return keyEntity.copy(privateKey = privateKey.toByteArray(), passphrase = passphrase) + KeyEntity.PassphraseType.DATABASE -> { + passPhraseMap[id] = PassPhraseInRAM( + passphrase = keyEntity.passphrase, + validUntil = Instant.MAX, + passphraseType = keyEntity.passphraseType + ) + } + } + } + } } interface OnKeysUpdatedListener { fun onKeysUpdated() } + private data class PassPhraseInRAM( + val passphrase: Passphrase, + val validUntil: Instant, + val passphraseType: KeyEntity.PassphraseType + ) + companion object { + private val MAX_LIFE_TIME_OF_KEYS_IN_RAM = TimeUnit.HOURS.toMillis(4) + + fun calculateLifeTimeForPassphrase(): Instant { + return Instant.ofEpochMilli(System.currentTimeMillis() + MAX_LIFE_TIME_OF_KEYS_IN_RAM) + } + @Volatile private var INSTANCE: KeysStorageImpl? = null 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 cefc210d73..4e34cf1758 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/SecurityUtils.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/SecurityUtils.kt @@ -9,18 +9,19 @@ package com.flowcrypt.email.security import android.content.Context import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails 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.PgpKey import com.flowcrypt.email.security.pgp.PgpPwd import com.flowcrypt.email.util.exception.DifferentPassPhrasesException import com.flowcrypt.email.util.exception.NoKeyAvailableException import com.flowcrypt.email.util.exception.NoPrivateKeysAvailableException import com.flowcrypt.email.util.exception.PrivateKeyStrengthException -import com.google.android.gms.common.util.CollectionUtils import org.apache.commons.codec.android.binary.Hex import org.apache.commons.codec.android.digest.DigestUtils +import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.util.Passphrase import java.util.* @@ -55,19 +56,19 @@ class SecurityUtils { */ fun genPrivateKeysBackup(context: Context, account: AccountEntity): String { val builder = StringBuilder() - val keys = KeysStorageImpl.getInstance(context.applicationContext).getAllPgpPrivateKeys() + val keysStorage = KeysStorageImpl.getInstance(context.applicationContext) + val keys = keysStorage.getPGPSecretKeyRings() - if (CollectionUtils.isEmpty(keys)) { + if (keys.isEmpty()) { throw NoPrivateKeysAvailableException(context, account.email) } - var firstPassPhrase: String? = null + var firstPassPhrase: Passphrase? = null for (i in keys.indices) { val key = keys[i] - - val passPhrase = key.passphrase - val private = key.privateKeyAsString + val fingerprint = OpenPgpV4Fingerprint(key) + val passPhrase = keysStorage.getPassphraseByFingerprint(fingerprint.toString()) if (i == 0) { firstPassPhrase = passPhrase @@ -75,17 +76,15 @@ class SecurityUtils { throw DifferentPassPhrasesException(context.getString(R.string.keys_have_different_pass_phrase)) } - if (passPhrase.isNullOrEmpty()) { + if (passPhrase == null || passPhrase.isEmpty) { throw PrivateKeyStrengthException(context.getString(R.string.empty_pass_phrase)) } PgpPwd.checkForWeakPassphrase(passPhrase) - val nodeKeyDetailsList = PgpKey.parseKeys(private).toNodeKeyDetailsList() - val keyDetails = nodeKeyDetailsList.first() - - val encryptedKey = if (keyDetails.isFullyDecrypted == true) { - PgpKey.encryptKey(private, Passphrase.fromPassword(passPhrase)) + val keyDetails = key.toPgpKeyDetails() + val encryptedKey = if (keyDetails.isFullyDecrypted) { + PgpKey.encryptKey(keyDetails.privateKey ?: throw IllegalStateException(), passPhrase) } else { keyDetails.privateKey } @@ -108,7 +107,7 @@ class SecurityUtils { fun getRecipientsPubKeys(context: Context, emails: MutableList): MutableList { val publicKeys = mutableListOf() val contacts = FlowCryptRoomDatabase.getDatabase(context).contactsDao() - .getContactsByEmails(emails) + .getContactsByEmails(emails) for (contact in contacts) { if (contact.publicKey?.isNotEmpty() == true) { @@ -129,9 +128,13 @@ class SecurityUtils { * @throws NoKeyAvailableException */ @JvmStatic - fun getSenderKeyDetails(context: Context, account: AccountEntity, senderEmail: String): NodeKeyDetails { + fun getSenderKeyDetails( + context: Context, + account: AccountEntity, + senderEmail: String + ): PgpKeyDetails { val keysStorage = KeysStorageImpl.getInstance(context.applicationContext) - val keys = keysStorage.getNodeKeyDetailsListByEmail(senderEmail) + val keys = keysStorage.getPGPSecretKeyRingsByUserId(senderEmail) if (keys.isEmpty()) { if (account.email.equals(senderEmail, ignoreCase = true)) { @@ -141,7 +144,7 @@ class SecurityUtils { } } - return keys.first() + return keys.first().toPgpKeyDetails() } /** diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Algo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/Algo.kt similarity index 95% rename from FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Algo.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/security/model/Algo.kt index d33f22e57a..58a3e579fd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/Algo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/Algo.kt @@ -3,7 +3,7 @@ * Contributors: DenBond7 */ -package com.flowcrypt.email.api.retrofit.response.model.node +package com.flowcrypt.email.security.model import android.os.Parcel import android.os.Parcelable diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/KeyId.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/KeyId.kt similarity index 58% rename from FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/KeyId.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/security/model/KeyId.kt index 3b7fad2567..00ffe9f956 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/node/KeyId.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/KeyId.kt @@ -3,13 +3,11 @@ * Contributors: DenBond7 */ -package com.flowcrypt.email.api.retrofit.response.model.node +package com.flowcrypt.email.security.model import android.os.Parcel import android.os.Parcelable - import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName /** * @author Denis Bondarenko @@ -17,14 +15,9 @@ import com.google.gson.annotations.SerializedName * Time: 1:38 PM * E-mail: DenBond7@gmail.com */ -data class KeyId constructor(@Expose val fingerprint: String?, - @SerializedName("longid") @Expose val longId: String?, - @SerializedName("shortid") @Expose val shortId: String?) : Parcelable { - constructor(source: Parcel) : this( - source.readString(), - source.readString(), - source.readString() - ) +data class KeyId constructor(@Expose val fingerprint: String) : Parcelable { + constructor(source: Parcel) : this(source.readString() + ?: throw IllegalArgumentException("fingerprint can't be null")) override fun describeContents(): Int { return 0 @@ -33,8 +26,6 @@ data class KeyId constructor(@Expose val fingerprint: String?, override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { writeString(fingerprint) - writeString(longId) - writeString(shortId) } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/PgpKeyDetails.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/PgpKeyDetails.kt new file mode 100644 index 0000000000..f2a69f3a5d --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/model/PgpKeyDetails.kt @@ -0,0 +1,226 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.security.model + +import android.os.Parcel +import android.os.Parcelable +import android.util.Patterns +import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.database.entity.KeyEntity +import com.flowcrypt.email.model.PgpContact +import com.flowcrypt.email.util.exception.FlowCryptException +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import java.util.* +import javax.mail.internet.AddressException +import javax.mail.internet.InternetAddress + +/** + * This class collects base info of [org.bouncycastle.openpgp.PGPKeyRing] + * that can be used via [Parcelable] mechanism. + * + * @author Denis Bondarenko + * Date: 2/11/19 + * Time: 1:23 PM + * E-mail: DenBond7@gmail.com + */ +data class PgpKeyDetails constructor(@Expose val isFullyDecrypted: Boolean, + @Expose val isFullyEncrypted: Boolean, + @Expose @SerializedName("private") val privateKey: String?, + @Expose @SerializedName("public") val publicKey: String, + @Expose val users: List, + @Expose val ids: List, + @Expose val created: Long, + @Expose val lastModified: Long, + @Expose val expiration: Long? = null, + @Expose val algo: Algo, + var tempPassphrase: CharArray? = null, + var passphraseType: KeyEntity.PassphraseType? = null) : Parcelable { + + val primaryPgpContact: PgpContact + get() = determinePrimaryPgpContact() + val pgpContacts: ArrayList + get() = determinePgpContacts() + val fingerprint: String + get() = ids.first().fingerprint + val isPrivate: Boolean + get() = privateKey != null + + val isExpired: Boolean + get() = expiration != null && (System.currentTimeMillis() > expiration) + + val mimeAddresses: List + get() = parseMimeAddresses() + + val isPartiallyEncrypted: Boolean + get() { + return !isFullyDecrypted && !isFullyEncrypted + } + + constructor(source: Parcel) : this( + source.readValue(Boolean::class.java.classLoader) as Boolean, + source.readValue(Boolean::class.java.classLoader) as Boolean, + source.readString(), + source.readString() ?: throw IllegalArgumentException("pubkey can't be null"), + source.createStringArrayList() ?: throw NullPointerException(), + source.createTypedArrayList(KeyId.CREATOR) ?: throw NullPointerException(), + source.readLong(), + source.readLong(), + source.readValue(Long::class.java.classLoader) as Long?, + source.readParcelable(Algo::class.java.classLoader) ?: throw NullPointerException(), + source.createCharArray(), + source.readParcelable( + KeyEntity.PassphraseType::class.java.classLoader) + ) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { + writeValue(isFullyDecrypted) + writeValue(isFullyEncrypted) + writeString(privateKey) + writeString(publicKey) + writeStringList(users) + writeTypedList(ids) + writeLong(created) + writeLong(lastModified) + writeValue(expiration) + writeParcelable(algo, flags) + writeCharArray(tempPassphrase) + writeParcelable(passphraseType, flags) + } + + private fun determinePrimaryPgpContact(): PgpContact { + val address = users.first() + val fingerprintFromKeyId = ids.first().fingerprint + var email: String? = null + var name: String? = null + try { + val internetAddresses = InternetAddress.parse(address) + email = internetAddresses.first().address + name = internetAddresses.first().personal + } catch (e: AddressException) { + e.printStackTrace() + val pattern = Patterns.EMAIL_ADDRESS + val matcher = pattern.matcher(users.first()) + if (matcher.find()) { + email = matcher.group() + name = email + } + } + + if (email == null) { + throw object : FlowCryptException("No user ids with mail address") {} + } + + return PgpContact( + email = email.toLowerCase(Locale.US), + name = name, + pubkey = publicKey, + hasPgp = true, + client = null, + fingerprint = fingerprintFromKeyId + ) + } + + private fun determinePgpContacts(): ArrayList { + val pgpContacts = ArrayList() + for (user in users) { + try { + val internetAddresses = InternetAddress.parse(user) + + for (internetAddress in internetAddresses) { + val email = internetAddress.address.toLowerCase(Locale.US) + val name = internetAddress.personal + + pgpContacts.add(PgpContact(email, name)) + } + } catch (e: AddressException) { + e.printStackTrace() + } + } + + return pgpContacts + } + + private fun parseMimeAddresses(): List { + val results = mutableListOf() + + for (user in users) { + try { + results.addAll(listOf(*InternetAddress.parse(user))) + } catch (e: AddressException) { + //do nothing + } + } + + return results + } + + fun toKeyEntity(accountEntity: AccountEntity): KeyEntity { + return KeyEntity( + fingerprint = fingerprint, + account = accountEntity.email.toLowerCase(Locale.US), + accountType = accountEntity.accountType, + source = PrivateKeySourceType.BACKUP.toString(), + publicKey = publicKey.toByteArray(), + privateKey = privateKey?.toByteArray() + ?: throw NullPointerException("nodeKeyDetails.privateKey == null"), + storedPassphrase = tempPassphrase?.let { String(it) }, + passphraseType = passphraseType + ?: throw IllegalArgumentException("passphraseType is not defined") + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PgpKeyDetails + + if (isFullyDecrypted != other.isFullyDecrypted) return false + if (isFullyEncrypted != other.isFullyEncrypted) return false + if (privateKey != other.privateKey) return false + if (publicKey != other.publicKey) return false + if (users != other.users) return false + if (ids != other.ids) return false + if (created != other.created) return false + if (lastModified != other.lastModified) return false + if (expiration != other.expiration) return false + if (algo != other.algo) return false + if (tempPassphrase != null) { + if (other.tempPassphrase == null) return false + if (!tempPassphrase.contentEquals(other.tempPassphrase)) return false + } else if (other.tempPassphrase != null) return false + if (passphraseType != other.passphraseType) return false + + return true + } + + override fun hashCode(): Int { + var result = isFullyDecrypted.hashCode() + result = 31 * result + isFullyEncrypted.hashCode() + result = 31 * result + (privateKey?.hashCode() ?: 0) + result = 31 * result + publicKey.hashCode() + result = 31 * result + users.hashCode() + result = 31 * result + ids.hashCode() + result = 31 * result + created.hashCode() + result = 31 * result + lastModified.hashCode() + result = 31 * result + (expiration?.hashCode() ?: 0) + result = 31 * result + algo.hashCode() + result = 31 * result + (tempPassphrase?.contentHashCode() ?: 0) + result = 31 * result + (passphraseType?.hashCode() ?: 0) + return result + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): PgpKeyDetails = PgpKeyDetails(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpKey.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpKey.kt index 6a58fbaacc..d66b769a14 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpKey.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpKey.kt @@ -6,7 +6,7 @@ package com.flowcrypt.email.security.pgp import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.armor -import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toNodeKeyDetails +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails import org.bouncycastle.openpgp.PGPKeyRing import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.PGPainless @@ -39,7 +39,8 @@ object PgpKey { * * @param armored Should be a single private key. */ - fun changeKeyPassphrase(armored: String, oldPassphrase: Passphrase, newPassphrase: Passphrase): String { + fun changeKeyPassphrase(armored: String, + oldPassphrase: Passphrase, newPassphrase: Passphrase): String { return changeKeyPassphrase(extractSecretKeyRing(armored), oldPassphrase, newPassphrase).armor() } @@ -59,18 +60,19 @@ object PgpKey { * @return parsing result object */ fun parseKeys(source: InputStream, throwExceptionIfUnknownSource: Boolean = true): ParseKeyResult { - return ParseKeyResult(PGPainless.readKeyRing().keyRingCollection(source, throwExceptionIfUnknownSource)) + return ParseKeyResult( + PGPainless.readKeyRing().keyRingCollection(source, throwExceptionIfUnknownSource)) } fun decryptKey(key: PGPSecretKeyRing, passphrase: Passphrase): PGPSecretKeyRing { return PGPainless.modifyKeyRing(key) - .changePassphraseFromOldPassphrase(passphrase) - .withSecureDefaultSettings() - .toNoPassphrase() - .done() + .changePassphraseFromOldPassphrase(passphrase) + .withSecureDefaultSettings() + .toNoPassphrase() + .done() } - fun encryptKey(key: PGPSecretKeyRing, passphrase: Passphrase): PGPSecretKeyRing { + private fun encryptKey(key: PGPSecretKeyRing, passphrase: Passphrase): PGPSecretKeyRing { return PGPainless.modifyKeyRing(key) .changePassphraseFromOldPassphrase(null) .withSecureDefaultSettings() @@ -78,7 +80,7 @@ object PgpKey { .done() } - fun changeKeyPassphrase( + private fun changeKeyPassphrase( key: PGPSecretKeyRing, oldPassphrase: Passphrase, newPassphrase: Passphrase @@ -102,6 +104,6 @@ object PgpKey { pgpKeyRingCollection.pgpSecretKeyRingCollection.keyRings.asSequence().toList() + pgpKeyRingCollection.pgpPublicKeyRingCollection.keyRings.asSequence().toList() - fun toNodeKeyDetailsList() = getAllKeys().map { it.toNodeKeyDetails() } + fun toPgpKeyDetailsList() = getAllKeys().map { it.toPgpKeyDetails() } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt index bd8f972f1e..cdcc9bf380 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt @@ -6,8 +6,10 @@ package com.flowcrypt.email.security.pgp import com.flowcrypt.email.Constants +import com.flowcrypt.email.extensions.org.pgpainless.util.asString import com.flowcrypt.email.util.exception.PrivateKeyStrengthException import com.nulabinc.zxcvbn.Zxcvbn +import org.pgpainless.util.Passphrase import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -39,8 +41,9 @@ object PgpPwd { * @throws [PrivateKeyStrengthException] if the given passphrase is weak * @throws [IllegalArgumentException] if missing passphrase strength evaluation */ - fun checkForWeakPassphrase(passphrase: String) { - val measure = Zxcvbn().measure(passphrase, listOf(*Constants.PASSWORD_WEAK_WORDS)).guesses + fun checkForWeakPassphrase(passphrase: Passphrase) { + val measure = Zxcvbn().measure(passphrase.asString, + listOf(*Constants.PASSWORD_WEAK_WORDS)).guesses val passwordStrength = estimateStrength(measure) when (passwordStrength.word.word) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/BaseLifecycleService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/BaseLifecycleService.kt new file mode 100644 index 0000000000..f128b25ace --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/BaseLifecycleService.kt @@ -0,0 +1,44 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.service + +import android.content.Intent +import androidx.lifecycle.LifecycleService +import com.flowcrypt.email.util.LogsUtil + +/** + * @author Denis Bondarenko + * Date: 5/6/21 + * Time: 2:51 PM + * E-mail: DenBond7@gmail.com + */ +abstract class BaseLifecycleService : LifecycleService() { + override fun onCreate() { + super.onCreate() + LogsUtil.d(javaClass.simpleName, "onCreate") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + LogsUtil.d(javaClass.simpleName, + "onStartCommand |intent =$intent |flags = $flags |startId = $startId") + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + super.onDestroy() + LogsUtil.d(javaClass.simpleName, "onDestroy") + } + + override fun onRebind(intent: Intent) { + super.onRebind(intent) + LogsUtil.d(javaClass.simpleName, "onRebind:$intent") + } + + override fun onUnbind(intent: Intent): Boolean { + LogsUtil.d(javaClass.simpleName, "onUnbind:$intent") + return super.onUnbind(intent) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/CheckClipboardToFindKeyService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/CheckClipboardToFindKeyService.kt index 932190bfc4..e67334474d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/CheckClipboardToFindKeyService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/CheckClipboardToFindKeyService.kt @@ -20,7 +20,7 @@ import android.os.Message import android.os.Messenger import android.os.RemoteException import android.text.TextUtils -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.KeyImportModel import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.LogsUtil @@ -125,7 +125,7 @@ class CheckClipboardToFindKeyService : Service(), ClipboardManager.OnPrimaryClip val key = message.obj as String checkClipboardToFindKeyService?.keyImportModel = KeyImportModel(null, key, - weakRef.get()!!.isPrivateKeyMode, KeyDetails.Type.CLIPBOARD) + weakRef.get()!!.isPrivateKeyMode, KeyImportDetails.SourceType.CLIPBOARD) LogsUtil.d(TAG, "Found a valid private key in clipboard") } } @@ -147,7 +147,7 @@ class CheckClipboardToFindKeyService : Service(), ClipboardManager.OnPrimaryClip MESSAGE_WHAT -> { val clipboardText = msg.obj as String try { - val nodeKeyDetails = PgpKey.parseKeys(clipboardText).toNodeKeyDetailsList() + val nodeKeyDetails = PgpKey.parseKeys(clipboardText).toPgpKeyDetailsList() if (!CollectionUtils.isEmpty(nodeKeyDetails)) { sendReply(msg) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/PassPhrasesInRAMService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/PassPhrasesInRAMService.kt new file mode 100644 index 0000000000..a540a2624f --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/PassPhrasesInRAMService.kt @@ -0,0 +1,107 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.service + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.lifecycle.lifecycleScope +import com.flowcrypt.email.R +import com.flowcrypt.email.model.KeysStorage +import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.ui.activity.EmailManagerActivity +import com.flowcrypt.email.ui.notifications.NotificationChannelManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit + +/** + * @author Denis Bondarenko + * Date: 5/6/21 + * Time: 9:56 AM + * E-mail: DenBond7@gmail.com + */ +class PassPhrasesInRAMService : BaseLifecycleService() { + private lateinit var keysStorage: KeysStorage + private lateinit var repeatableActionFlow: Flow + + override fun onCreate() { + super.onCreate() + keysStorage = KeysStorageImpl.getInstance(applicationContext) + runAsForeground() + runChecking() + } + + private fun runAsForeground() { + val pendingIntent: PendingIntent = + Intent(this, EmailManagerActivity::class.java).let { notificationIntent -> + PendingIntent.getActivity(this, 0, notificationIntent, 0) + } + + val notification: Notification = Notification.Builder( + this, + NotificationChannelManager.CHANNEL_ID_SILENT + ) + .setContentTitle(getString(R.string.active_passphrase_session)) + .setSmallIcon(R.drawable.ic_baseline_password_24dp) + .setContentIntent(pendingIntent) + .build() + + startForeground(R.id.notification_id_passphrase_service, notification) + } + + private fun runChecking() { + setupFlowForPeriodicCheck() + + lifecycleScope.launch { + repeatableActionFlow.collect { + keysStorage.updatePassPhrasesCache() + } + } + } + + private fun setupFlowForPeriodicCheck() { + repeatableActionFlow = flow { + while (lifecycleScope.isActive) { + emit(System.currentTimeMillis()) + delay(DELAY_TIMEOUT) + } + }.flowOn(Dispatchers.Default) + } + + companion object { + /** + * We will run checking every minute. + */ + private val DELAY_TIMEOUT = TimeUnit.MINUTES.toMillis(1) + + /** + * Start [PassPhrasesInRAMService]. + * + * @param context Interface to global information about an application environment. + */ + fun start(context: Context) { + val startEmailServiceIntent = Intent(context, PassPhrasesInRAMService::class.java) + context.startForegroundService(startEmailServiceIntent) + } + + /** + * Stop [PassPhrasesInRAMService]. + * + * @param context Interface to global information about an application environment. + */ + fun stop(context: Context) { + context.stopService(Intent(context, PassPhrasesInRAMService::class.java)) + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/BackupPrivateKeyToInboxAction.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/BackupPrivateKeyToInboxAction.kt index dc6391eb2e..e9ef839ca7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/BackupPrivateKeyToInboxAction.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/BackupPrivateKeyToInboxAction.kt @@ -14,11 +14,11 @@ import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.protocol.OpenStoreHelper import com.flowcrypt.email.api.email.protocol.SmtpProtocolUtil import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails import com.flowcrypt.email.jetpack.viewmodel.AccountViewModel import com.flowcrypt.email.security.KeysStorageImpl import com.flowcrypt.email.security.pgp.PgpKey import com.google.gson.annotations.SerializedName -import org.pgpainless.util.Passphrase /** * This action describes a task which backups a private key to INBOX. @@ -31,7 +31,7 @@ import org.pgpainless.util.Passphrase data class BackupPrivateKeyToInboxAction @JvmOverloads constructor(override var id: Long = 0, override var email: String, override val version: Int = 0, - private val privateKeyLongId: String) : Action { + private val privateKeyFingerprint: String) : Action { @SerializedName(Action.TAG_NAME_ACTION_TYPE) override val type: Action.Type = Action.Type.BACKUP_PRIVATE_KEY_TO_INBOX @@ -40,30 +40,28 @@ data class BackupPrivateKeyToInboxAction @JvmOverloads constructor(override var val encryptedAccount = roomDatabase.accountDao().getAccount(email) ?: return val account = AccountViewModel.getAccountEntityWithDecryptedInfo(encryptedAccount) ?: return val keysStorage = KeysStorageImpl.getInstance(context) - val keyEntity = keysStorage.getPgpPrivateKey(privateKeyLongId) ?: return - if (keyEntity.privateKey.isNotEmpty()) { - val session = OpenStoreHelper.getAccountSess(context, account) - val transport = SmtpProtocolUtil.prepareSmtpTransport(context, session, account) + val pgpKeyDetails = keysStorage + .getPGPSecretKeyRingByFingerprint(privateKeyFingerprint)?.toPgpKeyDetails() ?: return - val key = PgpKey.parseKeys(keyEntity.privateKey).toNodeKeyDetailsList().first() - val encryptedKey: String - if (key.isFullyEncrypted == true) { - encryptedKey = key.privateKey ?: throw IllegalArgumentException("empty key") - } else { - try { - encryptedKey = PgpKey.encryptKey( - keyEntity.privateKeyAsString, - Passphrase.fromPassword(keyEntity.passphrase!!) - ) - } catch (e: Exception) { - throw IllegalStateException("An error occurred during encrypting some key", e) - } + val encryptedKey: String + if (pgpKeyDetails.isFullyEncrypted) { + encryptedKey = pgpKeyDetails.privateKey ?: throw IllegalArgumentException("empty key") + } else { + try { + val passphrase = keysStorage.getPassphraseByFingerprint(pgpKeyDetails.fingerprint) ?: return + encryptedKey = PgpKey.encryptKey( + armored = pgpKeyDetails.privateKey ?: throw IllegalArgumentException("empty key"), + passphrase = passphrase) + } catch (e: Exception) { + throw IllegalStateException("An error occurred during encrypting some key", e) } - - val mimeBodyPart = EmailUtil.genBodyPartWithPrivateKey(encryptedAccount, encryptedKey) - val message = EmailUtil.genMsgWithPrivateKeys(context, encryptedAccount, session, mimeBodyPart) - transport.sendMessage(message, message.allRecipients) } + + val session = OpenStoreHelper.getAccountSess(context, account) + val transport = SmtpProtocolUtil.prepareSmtpTransport(context, session, account) + val mimeBodyPart = EmailUtil.genBodyPartWithPrivateKey(encryptedAccount, encryptedKey) + val message = EmailUtil.genMsgWithPrivateKeys(context, encryptedAccount, session, mimeBodyPart) + transport.sendMessage(message, message.allRecipients) } constructor(source: Parcel) : this( @@ -82,7 +80,7 @@ data class BackupPrivateKeyToInboxAction @JvmOverloads constructor(override var writeLong(id) writeString(email) writeInt(version) - writeString(privateKeyLongId) + writeString(privateKeyFingerprint) } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/EncryptPrivateKeysIfNeededAction.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/EncryptPrivateKeysIfNeededAction.kt index adf1e5e9ec..330fac3577 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/EncryptPrivateKeysIfNeededAction.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/actionqueue/actions/EncryptPrivateKeysIfNeededAction.kt @@ -24,7 +24,6 @@ import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.PrivateKeyStrengthException import com.google.android.gms.common.util.CollectionUtils import com.google.gson.annotations.SerializedName -import org.pgpainless.util.Passphrase /** * This [Action] checks all available private keys are they encrypted. If not we will try to encrypt a key and @@ -42,7 +41,7 @@ data class EncryptPrivateKeysIfNeededAction @JvmOverloads constructor(override v override val type: Action.Type = Action.Type.ENCRYPT_PRIVATE_KEYS override fun run(context: Context) { - val keyEntities = KeysStorageImpl.getInstance(context).getAllPgpPrivateKeys().map { it.copy() } + val keyEntities = KeysStorageImpl.getInstance(context).getRawKeys().map { it.copy() } val modifiedKeyEntities = mutableListOf() val roomDatabase = FlowCryptRoomDatabase.getDatabase(context) @@ -51,10 +50,10 @@ data class EncryptPrivateKeysIfNeededAction @JvmOverloads constructor(override v } for (keyEntity in keyEntities) { - val passphrase = keyEntity.passphrase ?: continue + val passphrase = keyEntity.passphrase val keyDetailsList = PgpKey.parseKeys(keyEntity.privateKeyAsString.toByteArray(), false) - .toNodeKeyDetailsList() + .toPgpKeyDetailsList() if (keyDetailsList.isEmpty() || keyDetailsList.size != 1) { ExceptionUtil.handleError( IllegalArgumentException("An error occurred during the key parsing| 1: " @@ -64,19 +63,16 @@ data class EncryptPrivateKeysIfNeededAction @JvmOverloads constructor(override v val keyDetails = keyDetailsList.first() - if (keyDetails.isFullyEncrypted == true) { + if (keyDetails.isFullyEncrypted) { continue } try { PgpPwd.checkForWeakPassphrase(passphrase) - val encryptedKey = PgpKey.encryptKey( - keyDetails.privateKey!!, - Passphrase.fromPassword(passphrase) - ) + val encryptedKey = PgpKey.encryptKey(keyDetails.privateKey!!, passphrase) val encryptedKeyDetailsList = PgpKey.parseKeys(encryptedKey.toByteArray(), false) - .toNodeKeyDetailsList() + .toPgpKeyDetailsList() if (encryptedKeyDetailsList.isEmpty() || encryptedKeyDetailsList.size != 1) { ExceptionUtil.handleError(IllegalArgumentException("An error occurred during the key parsing| 2")) continue @@ -85,8 +81,7 @@ data class EncryptPrivateKeysIfNeededAction @JvmOverloads constructor(override v val keyDetailsWithPgpEncryptedInfo = encryptedKeyDetailsList.first() val modifiedKeyEntity = keyEntity.copy( privateKey = KeyStoreCryptoManager.encrypt(keyDetailsWithPgpEncryptedInfo.privateKey).toByteArray(), - publicKey = keyDetailsWithPgpEncryptedInfo.publicKey?.toByteArray() - ?: keyEntity.publicKey) + publicKey = keyDetailsWithPgpEncryptedInfo.publicKey.toByteArray()) modifiedKeyEntities.add(modifiedKeyEntity) } catch (e: PrivateKeyStrengthException) { val account = roomDatabase.accountDao().getActiveAccount() ?: return 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 135ecbd1c6..9ccdf36f39 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 @@ -38,7 +38,6 @@ 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.pgp.PgpDecrypt -import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.util.FileAndDirectoryUtils import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.LogsUtil @@ -49,6 +48,7 @@ import com.sun.mail.imap.IMAPFolder import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils import org.apache.commons.io.IOUtils +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection import java.io.File import java.io.FileInputStream import java.io.InputStream @@ -659,23 +659,19 @@ class AttachmentDownloadManagerService : Service() { FileInputStream(file).use { inputStream -> val decryptedFile = File.createTempFile("tmp", null, context.externalCacheDir) - att.name = FilenameUtils.getBaseName(att.name) - - val combinedSource = KeysStorageImpl.getInstance(context) - .getAllPgpPrivateKeys() - .joinToString(separator = "\n") { keyEntity -> keyEntity.privateKeyAsString } - val parseKeyResult = PgpKey.parseKeys(combinedSource) - val keys = parseKeyResult.pgpKeyRingCollection.pgpSecretKeyRingCollection + val pgpSecretKeyRings = KeysStorageImpl.getInstance(context).getPGPSecretKeyRings() + val pgpSecretKeyRingCollection = PGPSecretKeyRingCollection(pgpSecretKeyRings) val protector = KeysStorageImpl.getInstance(context).getSecretKeyRingProtector() try { val result = PgpDecrypt.decrypt( srcInputStream = inputStream, destOutputStream = decryptedFile.outputStream(), - pgpSecretKeyRingCollection = keys, + pgpSecretKeyRingCollection = pgpSecretKeyRingCollection, protector = protector ) + att.name = FilenameUtils.getBaseName(att.name) result.fileInfo?.fileName?.let { fileName -> if (att.name == null) { att.name = fileName diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/BackupKeysActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/BackupKeysActivity.kt index 5df8fcb90f..3bef7eb3d5 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/BackupKeysActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/BackupKeysActivity.kt @@ -72,7 +72,7 @@ class BackupKeysActivity : BaseSettingsBackStackSyncActivity(), View.OnClickList override fun onClick(v: View) { when (v.id) { R.id.buttonBackupAction -> { - if (KeysStorageImpl.getInstance(application).getAllPgpPrivateKeys().isNullOrEmpty()) { + if (KeysStorageImpl.getInstance(application).getRawKeys().isNullOrEmpty()) { showInfoSnackbar(rootView, getString(R.string.there_are_no_private_keys, activeAccount?.email), Snackbar.LENGTH_LONG) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ChangePassPhraseActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ChangePassPhraseActivity.kt index ddb8c8ea45..24ed69e1bf 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ChangePassPhraseActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ChangePassPhraseActivity.kt @@ -22,6 +22,7 @@ import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel import com.flowcrypt.email.ui.activity.base.BasePassPhraseManagerActivity import com.flowcrypt.email.ui.notifications.SystemNotificationManager import com.flowcrypt.email.util.UIUtil +import org.pgpainless.util.Passphrase /** * This activity describes a logic of changing the pass phrase of all imported private keys of an active account. @@ -36,7 +37,8 @@ class ChangePassPhraseActivity : BasePassPhraseManagerActivity() { private val privateKeysViewModel: PrivateKeysViewModel by viewModels() override fun onConfirmPassPhraseSuccess() { - privateKeysViewModel.changePassphrase(editTextKeyPassword.text.toString()) + privateKeysViewModel.changePassphrase( + Passphrase.fromPassword(editTextKeyPassword.text.toString())) } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CheckKeysActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CheckKeysActivity.kt index 29d0e3c7e3..34d9ffd426 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CheckKeysActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CheckKeysActivity.kt @@ -13,23 +13,26 @@ import android.os.Parcelable import android.view.View import android.widget.Button import android.widget.EditText +import android.widget.RadioGroup import android.widget.TextView import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.showInfoDialogFragment import com.flowcrypt.email.jetpack.viewmodel.CheckPrivateKeysViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.fragment.dialog.InfoDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.WebViewInfoDialogFragment import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.UIUtil import org.apache.commons.io.IOUtils +import org.pgpainless.util.Passphrase import java.io.IOException import java.nio.charset.StandardCharsets @@ -42,22 +45,24 @@ import java.nio.charset.StandardCharsets * Time: 9:59 * E-mail: DenBond7@gmail.com */ -class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFragment.OnInfoDialogButtonClickListener { - private var originalKeys: MutableList = mutableListOf() - private val unlockedKeys: ArrayList = ArrayList() - private val remainingKeys: ArrayList = ArrayList() - private var keyDetailsAndLongIdsMap: MutableMap? = null +class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, + InfoDialogFragment.OnInfoDialogButtonClickListener { + private var originalKeys: MutableList = mutableListOf() + private val unlockedKeys: ArrayList = ArrayList() + private val remainingKeys: ArrayList = ArrayList() + private var keyDetailsAndFingerprintsMap: MutableMap = mutableMapOf() private lateinit var checkPrivateKeysViewModel: CheckPrivateKeysViewModel private var editTextKeyPassword: EditText? = null private var textViewSubTitle: TextView? = null private var progressBar: View? = null + private var rGPassphraseType: RadioGroup? = null private var subTitle: String? = null private var positiveBtnTitle: String? = null private var negativeBtnTitle: String? = null private var uniqueKeysCount: Int = 0 - private var type: KeyDetails.Type? = null + private var sourceType: KeyImportDetails.SourceType? = null override val isDisplayHomeAsUpEnabled: Boolean get() = false @@ -78,15 +83,15 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr getExtras() if (originalKeys.isNotEmpty()) { - this.keyDetailsAndLongIdsMap = prepareMapFromKeyDetailsList(originalKeys) - this.uniqueKeysCount = getUniqueKeysLongIdsCount(keyDetailsAndLongIdsMap) + this.keyDetailsAndFingerprintsMap = prepareMapFromKeyDetailsList(originalKeys) + this.uniqueKeysCount = getCountOfUniqueKeys(keyDetailsAndFingerprintsMap) if (!intent.getBooleanExtra(KEY_EXTRA_IS_EXTRA_IMPORT_OPTION, false)) { if (intent.getBooleanExtra(KEY_EXTRA_SKIP_IMPORTED_KEYS, false)) { removeAlreadyImportedKeys() } - this.uniqueKeysCount = getUniqueKeysLongIdsCount(keyDetailsAndLongIdsMap) - this.originalKeys = ArrayList(keyDetailsAndLongIdsMap?.keys ?: emptyList()) + this.uniqueKeysCount = getCountOfUniqueKeys(keyDetailsAndFingerprintsMap) + this.originalKeys = ArrayList(keyDetailsAndFingerprintsMap.keys) when (uniqueKeysCount) { 0 -> { @@ -96,19 +101,23 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr 1 -> { this.subTitle = resources.getQuantityString( - R.plurals.found_backup_of_your_account_key, uniqueKeysCount, uniqueKeysCount) + R.plurals.found_backup_of_your_account_key, uniqueKeysCount, uniqueKeysCount + ) } else -> { - if (originalKeys.size != keyDetailsAndLongIdsMap?.size) { + if (originalKeys.size != keyDetailsAndFingerprintsMap.size) { val map = prepareMapFromKeyDetailsList(originalKeys) - val remainingKeyCount = getUniqueKeysLongIdsCount(map) + val remainingKeyCount = getCountOfUniqueKeys(map) - this.subTitle = resources.getQuantityString(R.plurals.not_recovered_all_keys, remainingKeyCount, - uniqueKeysCount - remainingKeyCount, uniqueKeysCount, remainingKeyCount) + this.subTitle = resources.getQuantityString( + R.plurals.not_recovered_all_keys, remainingKeyCount, + uniqueKeysCount - remainingKeyCount, uniqueKeysCount, remainingKeyCount + ) } else { this.subTitle = resources.getQuantityString( - R.plurals.found_backup_of_your_account_key, uniqueKeysCount, uniqueKeysCount) + R.plurals.found_backup_of_your_account_key, uniqueKeysCount, uniqueKeysCount + ) } } } @@ -132,12 +141,18 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr when (v.id) { R.id.buttonPositiveAction -> { UIUtil.hideSoftInput(this, editTextKeyPassword) - val passphrase = editTextKeyPassword?.text?.toString() - if (passphrase.isNullOrEmpty()) { + val typedText = editTextKeyPassword?.text?.toString() + if (typedText.isNullOrEmpty()) { showInfoSnackbar(editTextKeyPassword, getString(R.string.passphrase_must_be_non_empty)) } else { snackBar?.dismiss() - checkPrivateKeysViewModel.checkKeys(remainingKeys, passphrase) + getPassphraseType()?.let { passphraseType -> + checkPrivateKeysViewModel.checkKeys( + keys = remainingKeys, + passphrase = Passphrase.fromPassword(typedText), + passphraseType = passphraseType + ) + } } } @@ -151,15 +166,24 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr } R.id.imageButtonHint -> { - val infoDialogFragment = InfoDialogFragment.newInstance(dialogMsg = - getString(R.string.hint_when_found_keys_in_email)) + val infoDialogFragment = InfoDialogFragment.newInstance( + dialogMsg = getString(R.string.hint_when_found_keys_in_email) + ) infoDialogFragment.show(supportFragmentManager, InfoDialogFragment::class.java.simpleName) } R.id.imageButtonPasswordHint -> try { - val webViewInfoDialogFragment = WebViewInfoDialogFragment.newInstance("", - IOUtils.toString(assets.open("html/forgotten_pass_phrase_hint.htm"), StandardCharsets.UTF_8)) - webViewInfoDialogFragment.show(supportFragmentManager, WebViewInfoDialogFragment::class.java.simpleName) + val webViewInfoDialogFragment = WebViewInfoDialogFragment.newInstance( + dialogTitle = "", + dialogMsg = IOUtils.toString( + assets.open("html/forgotten_pass_phrase_hint.htm"), + StandardCharsets.UTF_8 + ) + ) + webViewInfoDialogFragment.show( + supportFragmentManager, + WebViewInfoDialogFragment::class.java.simpleName + ) } catch (e: IOException) { e.printStackTrace() } @@ -172,9 +196,9 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr } private fun getExtras() { - val keys: List? = intent?.getParcelableArrayListExtra(KEY_EXTRA_PRIVATE_KEYS) + val keys: List? = intent?.getParcelableArrayListExtra(KEY_EXTRA_PRIVATE_KEYS) keys?.let { originalKeys.addAll(it) } - this.type = intent?.getParcelableExtra(KEY_EXTRA_TYPE) + this.sourceType = intent?.getParcelableExtra(KEY_EXTRA_TYPE) this.subTitle = intent?.getStringExtra(KEY_EXTRA_SUB_TITLE) this.positiveBtnTitle = intent?.getStringExtra(KEY_EXTRA_POSITIVE_BUTTON_TITLE) this.negativeBtnTitle = intent?.getStringExtra(KEY_EXTRA_NEGATIVE_BUTTON_TITLE) @@ -185,7 +209,7 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr initButton(R.id.buttonNegativeAction, text = negativeBtnTitle) val imageButtonHint = findViewById(R.id.imageButtonHint) - if (originalKeys.isNotEmpty() && type === KeyDetails.Type.EMAIL) { + if (originalKeys.isNotEmpty() && sourceType === KeyImportDetails.SourceType.EMAIL) { imageButtonHint?.visibility = View.VISIBLE imageButtonHint?.setOnClickListener(this) } else { @@ -204,6 +228,12 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr val textViewTitle = findViewById(R.id.textViewTitle) textViewTitle.setText(R.string.import_private_key) } + + rGPassphraseType = findViewById(R.id.rGPassphraseType) + + if (GeneralUtil.isDebugBuild()) { + findViewById(R.id.rBStoreInRAM).isEnabled = true + } } private fun initButton(buttonViewId: Int, visibility: Int = View.VISIBLE, text: String?) { @@ -229,27 +259,31 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr Result.Status.SUCCESS -> { val resultKeys = it.data ?: emptyList() val sessionUnlockedKeys = resultKeys - .filter { checkResult -> - checkResult.nodeKeyDetails.passphrase?.isNotEmpty() == true - }.map { checkResult -> checkResult.nodeKeyDetails } + .filter { checkResult -> + checkResult.pgpKeyDetails.tempPassphrase?.isNotEmpty() == true + }.map { checkResult -> checkResult.pgpKeyDetails } if (sessionUnlockedKeys.isNotEmpty()) { unlockedKeys.addAll(sessionUnlockedKeys) for (key in sessionUnlockedKeys) { remainingKeys.removeAll(remainingKeys.filter { details -> - (details.longId == key.longId) + (details.fingerprint == key.fingerprint) }) } if (remainingKeys.isNotEmpty()) { - initButton(R.id.buttonSkipRemainingBackups, text = getString(R.string.skip_remaining_backups)) + initButton( + R.id.buttonSkipRemainingBackups, + text = getString(R.string.skip_remaining_backups) + ) editTextKeyPassword?.text = null val mapOfRemainingBackups = prepareMapFromKeyDetailsList(remainingKeys) - val remainingKeyCount = getUniqueKeysLongIdsCount(mapOfRemainingBackups) + val remainingKeyCount = getCountOfUniqueKeys(mapOfRemainingBackups) textViewSubTitle?.text = resources.getQuantityString( - R.plurals.not_recovered_all_keys, remainingKeyCount, - uniqueKeysCount - remainingKeyCount, uniqueKeysCount, remainingKeyCount) + R.plurals.not_recovered_all_keys, remainingKeyCount, + uniqueKeysCount - remainingKeyCount, uniqueKeysCount, remainingKeyCount + ) } else { returnUnlockedKeys(Activity.RESULT_OK) } @@ -294,15 +328,15 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr * Remove the already imported keys from the list of found backups. */ private fun removeAlreadyImportedKeys() { - val longIds = getUniqueKeysLongIds(keyDetailsAndLongIdsMap!!) + val fingerprints = getUniqueFingerprints(keyDetailsAndFingerprintsMap) val keysStorage = KeysStorageImpl.getInstance(this) - for (longId in longIds) { - if (keysStorage.getPgpPrivateKey(longId) != null) { - val iterator = keyDetailsAndLongIdsMap!!.entries.iterator() + for (fingerprint in fingerprints) { + if (keysStorage.getPassphraseByFingerprint(fingerprint) != null) { + val iterator = keyDetailsAndFingerprintsMap.entries.iterator() while (iterator.hasNext()) { val entry = iterator.next() - if (longId == entry.value) { + if (fingerprint == entry.value) { iterator.remove() } } @@ -311,43 +345,56 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr } /** - * Get a count of unique longIds. + * Get a count of unique fingerprints. * - * @param mapOfKeyDetailsAndLongIds An input map of [NodeKeyDetails]. - * @return A count of unique longIds. + * @param map An input map of [PgpKeyDetails]. + * @return A count of unique fingerprints. */ - private fun getUniqueKeysLongIdsCount(mapOfKeyDetailsAndLongIds: Map?): Int { - return HashSet(mapOfKeyDetailsAndLongIds?.values ?: emptyList()).size + private fun getCountOfUniqueKeys(map: Map): Int { + return getUniqueFingerprints(map).size } /** - * Get a set of unique longIds. + * Get a set of unique fingerprints. * - * @param mapOfKeyDetailsAndLongIds An input map of [NodeKeyDetails]. - * @return A list of unique longIds. + * @param map An input map of [PgpKeyDetails]. + * @return A list of unique fingerprints. */ - private fun getUniqueKeysLongIds(mapOfKeyDetailsAndLongIds: Map): Set { - return HashSet(mapOfKeyDetailsAndLongIds.values) + private fun getUniqueFingerprints(map: Map): Set { + return HashSet(map.values) } /** - * Generate a map of incoming list of [NodeKeyDetails] objects where values will be a [NodeKeyDetails] - * longId. + * Generate a map of incoming list of [PgpKeyDetails] objects where values + * will be a [PgpKeyDetails] fingerprints. * - * @param keys An incoming list of [NodeKeyDetails] objects. + * @param keys An incoming list of [PgpKeyDetails] objects. * @return A generated map. */ - private fun prepareMapFromKeyDetailsList(keys: List?): MutableMap { - val map = HashMap() + private fun prepareMapFromKeyDetailsList(keys: List?): + MutableMap { + val map = HashMap() keys?.let { for (keyDetails in it) { - map[keyDetails] = keyDetails.longId ?: "" + map[keyDetails] = keyDetails.fingerprint } } return map } + private fun getPassphraseType(): KeyEntity.PassphraseType? { + return when (rGPassphraseType?.checkedRadioButtonId) { + R.id.rBStoreLocally -> { + KeyEntity.PassphraseType.DATABASE + } + R.id.rBStoreInRAM -> { + KeyEntity.PassphraseType.RAM + } + else -> null + } + } + companion object { const val RESULT_NEGATIVE = 10 @@ -355,29 +402,45 @@ class CheckKeysActivity : BaseNodeActivity(), View.OnClickListener, InfoDialogFr const val RESULT_NO_NEW_KEYS = 12 val KEY_EXTRA_PRIVATE_KEYS = GeneralUtil.generateUniqueExtraKey( - "KEY_EXTRA_PRIVATE_KEYS", CheckKeysActivity::class.java) + "KEY_EXTRA_PRIVATE_KEYS", CheckKeysActivity::class.java + ) val KEY_EXTRA_TYPE = GeneralUtil.generateUniqueExtraKey( - "KEY_EXTRA_TYPE", CheckKeysActivity::class.java) + "KEY_EXTRA_TYPE", CheckKeysActivity::class.java + ) val KEY_EXTRA_SUB_TITLE = GeneralUtil.generateUniqueExtraKey( - "KEY_EXTRA_SUB_TITLE", CheckKeysActivity::class.java) + "KEY_EXTRA_SUB_TITLE", CheckKeysActivity::class.java + ) val KEY_EXTRA_POSITIVE_BUTTON_TITLE = GeneralUtil.generateUniqueExtraKey( - "KEY_EXTRA_POSITIVE_BUTTON_TITLE", CheckKeysActivity::class.java) + "KEY_EXTRA_POSITIVE_BUTTON_TITLE", CheckKeysActivity::class.java + ) val KEY_EXTRA_NEGATIVE_BUTTON_TITLE = - GeneralUtil.generateUniqueExtraKey("KEY_EXTRA_NEGATIVE_BUTTON_TITLE", CheckKeysActivity::class.java) + GeneralUtil.generateUniqueExtraKey( + "KEY_EXTRA_NEGATIVE_BUTTON_TITLE", + CheckKeysActivity::class.java + ) val KEY_EXTRA_IS_EXTRA_IMPORT_OPTION = - GeneralUtil.generateUniqueExtraKey("KEY_EXTRA_IS_EXTRA_IMPORT_OPTION", CheckKeysActivity::class.java) + GeneralUtil.generateUniqueExtraKey( + "KEY_EXTRA_IS_EXTRA_IMPORT_OPTION", + CheckKeysActivity::class.java + ) val KEY_EXTRA_UNLOCKED_PRIVATE_KEYS = GeneralUtil.generateUniqueExtraKey( - "KEY_EXTRA_UNLOCKED_PRIVATE_KEYS", CheckKeysActivity::class.java) - val KEY_EXTRA_SKIP_IMPORTED_KEYS = GeneralUtil.generateUniqueExtraKey("KEY_EXTRA_SKIP_IMPORTED_KEYS", CheckKeysActivity::class.java) - - fun newIntent(context: Context, privateKeys: ArrayList, - type: KeyDetails.Type? = null, subTitle: String? = null, positiveBtnTitle: - String? = null, negativeBtnTitle: String? = null, - isExtraImportOpt: Boolean = false, - skipImportedKeys: Boolean = false): Intent { + "KEY_EXTRA_UNLOCKED_PRIVATE_KEYS", CheckKeysActivity::class.java + ) + val KEY_EXTRA_SKIP_IMPORTED_KEYS = GeneralUtil.generateUniqueExtraKey( + "KEY_EXTRA_SKIP_IMPORTED_KEYS", + CheckKeysActivity::class.java + ) + + fun newIntent( + context: Context, privateKeys: ArrayList, + sourceType: KeyImportDetails.SourceType? = null, subTitle: String? = null, positiveBtnTitle: + String? = null, negativeBtnTitle: String? = null, + isExtraImportOpt: Boolean = false, + skipImportedKeys: Boolean = false + ): Intent { val intent = Intent(context, CheckKeysActivity::class.java) intent.putExtra(KEY_EXTRA_PRIVATE_KEYS, privateKeys) - intent.putExtra(KEY_EXTRA_TYPE, type as Parcelable) + intent.putExtra(KEY_EXTRA_TYPE, sourceType as Parcelable) intent.putExtra(KEY_EXTRA_SUB_TITLE, subTitle) intent.putExtra(KEY_EXTRA_POSITIVE_BUTTON_TITLE, positiveBtnTitle) intent.putExtra(KEY_EXTRA_NEGATIVE_BUTTON_TITLE, negativeBtnTitle) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreatePrivateKeyActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreatePrivateKeyActivity.kt index d437ea54de..e895435ada 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreatePrivateKeyActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreatePrivateKeyActivity.kt @@ -15,11 +15,12 @@ import android.widget.Toast import androidx.activity.viewModels import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.base.BasePassPhraseManagerActivity import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.UIUtil @@ -34,7 +35,7 @@ import com.google.android.material.snackbar.Snackbar */ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { private val privateKeysViewModel: PrivateKeysViewModel by viewModels() - private var createdPrivateKeyLongId: String? = null + private var createdPrivateKeyFingerprint: String? = null private var tempAccount: AccountEntity? = null override val contentViewResourceId: Int @@ -44,7 +45,13 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { get() = findViewById(R.id.layoutContent) override fun onConfirmPassPhraseSuccess() { - tempAccount?.let { privateKeysViewModel.createPrivateKey(it, editTextKeyPassword.text.toString()) } + tempAccount?.let { + privateKeysViewModel.createPrivateKey( + accountEntity = it, + passphrase = editTextKeyPassword.text.toString(), + passphraseType = KeyEntity.PassphraseType.DATABASE + ) + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -57,7 +64,8 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { } if (savedInstanceState != null) { - this.createdPrivateKeyLongId = savedInstanceState.getString(KEY_CREATED_PRIVATE_KEY_LONG_ID) + this.createdPrivateKeyFingerprint = + savedInstanceState.getString(KEY_CREATED_PRIVATE_KEY_FINGERPRINT) } setupPrivateKeyViewModel() @@ -65,20 +73,21 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { override fun onBackPressed() { if (isBackEnabled) { - if (TextUtils.isEmpty(createdPrivateKeyLongId)) { + if (TextUtils.isEmpty(createdPrivateKeyFingerprint)) { super.onBackPressed() } else { setResult(Activity.RESULT_OK) finish() } } else { - Toast.makeText(this, R.string.please_wait_while_key_will_be_created, Toast.LENGTH_SHORT).show() + Toast.makeText(this, R.string.please_wait_while_key_will_be_created, Toast.LENGTH_SHORT) + .show() } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putString(KEY_CREATED_PRIVATE_KEY_LONG_ID, createdPrivateKeyLongId) + outState.putString(KEY_CREATED_PRIVATE_KEY_FINGERPRINT, createdPrivateKeyFingerprint) } override fun onClick(v: View) { @@ -102,7 +111,7 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { textViewSuccessSubTitle.setText(R.string.you_can_send_and_receive_encrypted_emails) btnSuccess.setText(R.string.continue_) - if (!TextUtils.isEmpty(this.createdPrivateKeyLongId)) { + if (!TextUtils.isEmpty(this.createdPrivateKeyFingerprint)) { layoutProgress.visibility = View.GONE layoutFirstPasswordCheck.visibility = View.GONE layoutSecondPasswordCheck.visibility = View.GONE @@ -123,8 +132,8 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { Result.Status.SUCCESS -> { isBackEnabled = true - val nodeKeyDetails: NodeKeyDetails? = it.data - createdPrivateKeyLongId = nodeKeyDetails?.longId + val pgpKeyDetails: PgpKeyDetails? = it.data + createdPrivateKeyFingerprint = pgpKeyDetails?.fingerprint layoutSecondPasswordCheck.visibility = View.GONE layoutSuccess.visibility = View.VISIBLE UIUtil.exchangeViewVisibility(false, layoutProgress, layoutContentView) @@ -137,8 +146,10 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { it.exception?.let { exception -> if (exception is ApiException) { - showSnackbar(rootView, exception.apiError?.msg ?: it.javaClass.simpleName, - getString(R.string.retry), Snackbar.LENGTH_LONG) { + showSnackbar( + rootView, exception.apiError?.msg ?: it.javaClass.simpleName, + getString(R.string.retry), Snackbar.LENGTH_LONG + ) { onConfirmPassPhraseSuccess() } } else { @@ -154,9 +165,15 @@ class CreatePrivateKeyActivity : BasePassPhraseManagerActivity() { } companion object { - val KEY_EXTRA_ACCOUNT = GeneralUtil.generateUniqueExtraKey("KEY_EXTRA_ACCOUNT", BasePassPhraseManagerActivity::class.java) - val KEY_CREATED_PRIVATE_KEY_LONG_ID = - GeneralUtil.generateUniqueExtraKey("KEY_CREATED_PRIVATE_KEY_LONG_ID", CreatePrivateKeyActivity::class.java) + val KEY_EXTRA_ACCOUNT = GeneralUtil.generateUniqueExtraKey( + "KEY_EXTRA_ACCOUNT", + BasePassPhraseManagerActivity::class.java + ) + val KEY_CREATED_PRIVATE_KEY_FINGERPRINT = + GeneralUtil.generateUniqueExtraKey( + "KEY_CREATED_PRIVATE_KEY_FINGERPRINT", + CreatePrivateKeyActivity::class.java + ) fun newIntent(context: Context, account: AccountEntity?): Intent { val intent = Intent(context, CreatePrivateKeyActivity::class.java) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/EditContactActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/EditContactActivity.kt index f56f082300..fe1d96551f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/EditContactActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/EditContactActivity.kt @@ -14,14 +14,14 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.core.widget.addTextChangedListener import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.ContactEntity import com.flowcrypt.email.extensions.showDialogFragment import com.flowcrypt.email.extensions.showInfoDialogFragment import com.flowcrypt.email.jetpack.viewmodel.ContactsViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.KeyImportModel +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.base.BaseImportKeyActivity import com.flowcrypt.email.ui.activity.fragment.dialog.UpdatePublicKeyOfContactDialogFragment import com.flowcrypt.email.util.GeneralUtil @@ -56,7 +56,7 @@ class EditContactActivity : BaseImportKeyActivity(), UpdatePublicKeyOfContactDia isCheckingClipboardEnabled = false } - override fun onKeyFound(type: KeyDetails.Type, keyDetailsList: List) { + override fun onKeyFound(sourceType: KeyImportDetails.SourceType, keyDetailsList: List) { if (keyDetailsList.size > 1) { showInfoDialogFragment(dialogMsg = getString(R.string.more_than_one_public_key_found)) return @@ -76,13 +76,13 @@ class EditContactActivity : BaseImportKeyActivity(), UpdatePublicKeyOfContactDia buttonCheck?.setOnClickListener { dismissSnackBar() - keyImportModel = KeyImportModel(null, editTextNewPubKey?.text.toString(), isPrivateKeyMode, KeyDetails.Type.MANUAL_ENTERING) + keyImportModel = KeyImportModel(null, editTextNewPubKey?.text.toString(), isPrivateKeyMode, KeyImportDetails.SourceType.MANUAL_ENTERING) keyImportModel?.let { privateKeysViewModel.parseKeys(it, false) } } } - override fun onKeySelected(nodeKeyDetails: NodeKeyDetails) { - contactsViewModel.updateContactPgpInfo(contactEntity, nodeKeyDetails) + override fun onKeySelected(pgpKeyDetails: PgpKeyDetails) { + contactsViewModel.updateContactPgpInfo(contactEntity, pgpKeyDetails) finish() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPgpContactActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPgpContactActivity.kt index 86b304c09f..69a58a6acd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPgpContactActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPgpContactActivity.kt @@ -21,12 +21,12 @@ import androidx.lifecycle.Observer import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.attester.PubResponse import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.jetpack.viewmodel.ContactsViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.base.BaseImportKeyActivity import com.flowcrypt.email.ui.activity.settings.FeedbackActivity import com.flowcrypt.email.util.GeneralUtil @@ -100,8 +100,8 @@ class ImportPgpContactActivity : BaseImportKeyActivity() { } } - override fun onKeyFound(type: KeyDetails.Type, keyDetailsList: List) { - if (type == KeyDetails.Type.CLIPBOARD) { + override fun onKeyFound(sourceType: KeyImportDetails.SourceType, keyDetailsList: List) { + if (sourceType == KeyImportDetails.SourceType.CLIPBOARD) { if (keyDetailsList.isNotEmpty()) { UIUtil.exchangeViewVisibility(true, layoutProgress, layoutContentView) startActivityForResult(PreviewImportPgpContactActivity.newIntent(this, keyImportModel!! diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivity.kt index ef1d4353aa..0f72928d10 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPrivateKeyActivity.kt @@ -16,7 +16,6 @@ import android.widget.Toast import androidx.activity.viewModels import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely @@ -24,9 +23,10 @@ import com.flowcrypt.email.extensions.showDialogFragment import com.flowcrypt.email.extensions.toast import com.flowcrypt.email.jetpack.viewmodel.BackupsViewModel import com.flowcrypt.email.jetpack.viewmodel.SubmitPubKeyViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.KeyImportModel import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.base.BaseImportKeyActivity import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment import com.flowcrypt.email.util.GeneralUtil @@ -45,9 +45,9 @@ import com.google.android.material.snackbar.Snackbar */ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.OnTwoWayDialogListener { private val backupsViewModel: BackupsViewModel by viewModels() - private var privateKeysFromEmailBackups = mutableListOf() - private val unlockedKeys: MutableList = ArrayList() - private var keyDetailsType: KeyDetails.Type = KeyDetails.Type.EMAIL + private var privateKeysFromEmailBackups = mutableListOf() + private val unlockedKeys: MutableList = ArrayList() + private var sourceType: KeyImportDetails.SourceType = KeyImportDetails.SourceType.EMAIL private var layoutSyncStatus: View? = null private var buttonImportBackup: Button? = null @@ -85,11 +85,11 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O R.id.buttonImportBackup -> { unlockedKeys.clear() if (!CollectionUtils.isEmpty(privateKeysFromEmailBackups)) { - keyDetailsType = KeyDetails.Type.EMAIL + sourceType = KeyImportDetails.SourceType.EMAIL startActivityForResult(CheckKeysActivity.newIntent( context = this, privateKeys = ArrayList(privateKeysFromEmailBackups), - type = KeyDetails.Type.EMAIL, + sourceType = KeyImportDetails.SourceType.EMAIL, positiveBtnTitle = getString(R.string.continue_), negativeBtnTitle = getString(R .string.choose_another_key), @@ -114,7 +114,7 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O when (resultCode) { Activity.RESULT_OK -> { - val keys: List? = data?.getParcelableArrayListExtra( + val keys: List? = data?.getParcelableArrayListExtra( CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS) keys?.let { @@ -138,7 +138,7 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O } } - override fun onKeyFound(type: KeyDetails.Type, keyDetailsList: List) { + override fun onKeyFound(sourceType: KeyImportDetails.SourceType, keyDetailsList: List) { var areFreshKeysExisted = false var arePrivateKeysExisted = false @@ -147,8 +147,9 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O arePrivateKeysExisted = true } - val longId = key.longId ?: continue - if (KeysStorageImpl.getInstance(application).getPgpPrivateKey(longId) == null) { + val fingerprint = key.fingerprint + if (KeysStorageImpl.getInstance(application) + .getPGPSecretKeyRingByFingerprint(fingerprint) == null) { areFreshKeysExisted = true } } @@ -171,9 +172,9 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O return } - when (type) { - KeyDetails.Type.FILE -> { - keyDetailsType = KeyDetails.Type.FILE + when (sourceType) { + KeyImportDetails.SourceType.FILE -> { + this.sourceType = KeyImportDetails.SourceType.FILE val fileName = GeneralUtil.getFileNameFromUri(this, keyImportModel!!.fileUri) val bottomTitle = resources.getQuantityString(R.plurals.file_contains_some_amount_of_keys, keyDetailsList.size, fileName, keyDetailsList.size) @@ -181,7 +182,7 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O val intent = CheckKeysActivity.newIntent( context = this, privateKeys = ArrayList(keyDetailsList), - type = keyDetailsType, + sourceType = sourceType, subTitle = bottomTitle, positiveBtnTitle = posBtnTitle, negativeBtnTitle = getString @@ -192,14 +193,14 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O startActivityForResult(intent, REQUEST_CODE_CHECK_PRIVATE_KEYS) } - KeyDetails.Type.CLIPBOARD -> { - keyDetailsType = KeyDetails.Type.CLIPBOARD + KeyImportDetails.SourceType.CLIPBOARD -> { + this.sourceType = KeyImportDetails.SourceType.CLIPBOARD val title = resources.getQuantityString(R.plurals.loaded_private_keys_from_clipboard, keyDetailsList.size, keyDetailsList.size) val clipboardIntent = CheckKeysActivity.newIntent( context = this, privateKeys = ArrayList(keyDetailsList), - type = keyDetailsType, + sourceType = sourceType, subTitle = title, positiveBtnTitle = getString(R.string.continue_), negativeBtnTitle = getString(R @@ -244,17 +245,17 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O val connector = KeysStorageImpl.getInstance(this) val iterator = privateKeysFromEmailBackups.iterator() - val uniqueKeysLongIds = HashSet() + val uniqueKeysFingerprints = HashSet() while (iterator.hasNext()) { - val privateKey = iterator.next() - uniqueKeysLongIds.add(privateKey.longId!!) - if (connector.getPgpPrivateKey(privateKey.longId!!) != null) { + val pgpKeyDetails = iterator.next() + uniqueKeysFingerprints.add(pgpKeyDetails.fingerprint) + if (connector.getPGPSecretKeyRingByFingerprint(pgpKeyDetails.fingerprint) != null) { iterator.remove() - uniqueKeysLongIds.remove(privateKey.longId!!) + uniqueKeysFingerprints.remove(pgpKeyDetails.fingerprint) } } - return uniqueKeysLongIds + return uniqueKeysFingerprints } private fun setupSubmitPubKeyViewModel() { @@ -321,7 +322,7 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O if (e is SavePrivateKeyToDatabaseException) { showSnackbar(rootView, e.message ?: e.javaClass.simpleName, getString(R.string.retry), Snackbar.LENGTH_INDEFINITE) { - privateKeysViewModel.encryptAndSaveKeysToDatabase(tempAccount, e.keys, KeyDetails.Type.EMAIL) + privateKeysViewModel.encryptAndSaveKeysToDatabase(tempAccount, e.keys, KeyImportDetails.SourceType.EMAIL) } } else { showInfoSnackbar(rootView, e?.message ?: e?.javaClass?.simpleName @@ -349,13 +350,13 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O if (keys.isNotEmpty()) { privateKeysFromEmailBackups.addAll(keys) - val uniqueKeysLongIds = filterKeys() + val uniqueKeysFingerprints = filterKeys() if (privateKeysFromEmailBackups.isEmpty()) { hideImportButton() } else { - buttonImportBackup?.text = resources.getQuantityString(R.plurals.import_keys, uniqueKeysLongIds.size) - textViewTitle.text = resources.getQuantityString(R.plurals.you_have_backups_that_was_not_imported, uniqueKeysLongIds.size) + buttonImportBackup?.text = resources.getQuantityString(R.plurals.import_keys, uniqueKeysFingerprints.size) + textViewTitle.text = resources.getQuantityString(R.plurals.you_have_backups_that_was_not_imported, uniqueKeysFingerprints.size) } } else { hideImportButton() @@ -383,7 +384,7 @@ class ImportPrivateKeyActivity : BaseImportKeyActivity(), TwoWayDialogFragment.O private fun handleSuccessSubmit() { textViewProgressText.setText(R.string.saving_prv_keys) - privateKeysViewModel.encryptAndSaveKeysToDatabase(tempAccount, unlockedKeys, keyDetailsType, intent.getBooleanExtra(KEY_EXTRA_ADD_ACCOUNT_IF_NOT_EXIST, false)) + privateKeysViewModel.encryptAndSaveKeysToDatabase(tempAccount, unlockedKeys, sourceType, intent.getBooleanExtra(KEY_EXTRA_ADD_ACCOUNT_IF_NOT_EXIST, false)) } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPublicKeyActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPublicKeyActivity.kt index b63b5ccc29..4381898bfb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPublicKeyActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/ImportPublicKeyActivity.kt @@ -11,11 +11,11 @@ import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.jetpack.viewmodel.ContactsViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.PgpContact +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.base.BaseImportKeyActivity import com.flowcrypt.email.util.GeneralUtil import com.google.android.material.snackbar.Snackbar @@ -48,7 +48,7 @@ class ImportPublicKeyActivity : BaseImportKeyActivity() { } } - override fun onKeyFound(type: KeyDetails.Type, keyDetailsList: List) { + override fun onKeyFound(sourceType: KeyImportDetails.SourceType, keyDetailsList: List) { if (keyDetailsList.isNotEmpty()) { if (keyDetailsList.size == 1) { val key = keyDetailsList.first() @@ -70,7 +70,7 @@ class ImportPublicKeyActivity : BaseImportKeyActivity() { } } - private fun updateInformationAboutPgpContact(keyDetails: NodeKeyDetails) { + private fun updateInformationAboutPgpContact(keyDetails: PgpKeyDetails) { val pgpContactFromKey = keyDetails.primaryPgpContact pgpContact?.pubkey = pgpContactFromKey.pubkey pgpContact?.let { contactsViewModel.updateContactPgpInfo(it, pgpContactFromKey) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt index 7e12a09b9b..58a4f71f96 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/NodeTestActivity.kt @@ -64,7 +64,7 @@ class NodeTestActivity : AppCompatActivity(), View.OnClickListener, Observer { val rsa2048DecryptedFileResult = responseWrapper.result as DecryptedFileResult? printDecryptFileResult("decrypt-file-rsa2048", TEST_MSG.toByteArray(), rsa2048DecryptedFileResult!!, responseWrapper.executionTime) - requestsManager!!.decryptFile(R.id.req_id_decrypt_file_rsa_4096, encryptBytes!!, TestData.rsa4096PrvKeyInfo()) + //requestsManager!!.decryptFile(R.id.req_id_decrypt_file_rsa_4096, encryptBytes!!, TestData.rsa4096PrvKeyInfo()) } R.id.req_id_decrypt_file_rsa_4096 -> { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/base/BaseImportKeyActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/base/BaseImportKeyActivity.kt index 7e765daf31..ce6bda2b35 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/base/BaseImportKeyActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/base/BaseImportKeyActivity.kt @@ -22,14 +22,14 @@ import android.widget.Toast import androidx.activity.viewModels import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.showInfoDialogFragment import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.model.KeyImportModel +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.service.CheckClipboardToFindKeyService import com.flowcrypt.email.ui.activity.fragment.dialog.InfoDialogFragment import com.flowcrypt.email.util.GeneralUtil @@ -85,7 +85,7 @@ abstract class BaseImportKeyActivity : BaseBackStackSyncActivity(), View.OnClick override val rootView: View get() = findViewById(R.id.layoutContent) - abstract fun onKeyFound(type: KeyDetails.Type, keyDetailsList: List) + abstract fun onKeyFound(sourceType: KeyImportDetails.SourceType, keyDetailsList: List) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -187,7 +187,7 @@ abstract class BaseImportKeyActivity : BaseBackStackSyncActivity(), View.OnClick val privateKeyFromClipboard = item.text if (!TextUtils.isEmpty(privateKeyFromClipboard)) { keyImportModel = KeyImportModel(null, privateKeyFromClipboard.toString(), - isPrivateKeyMode, KeyDetails.Type.CLIPBOARD) + isPrivateKeyMode, KeyImportDetails.SourceType.CLIPBOARD) keyImportModel?.let { privateKeysViewModel.parseKeys(it, false, isPrivateKeyMode) } } else { showClipboardIsEmptyInfoDialog() @@ -206,7 +206,7 @@ abstract class BaseImportKeyActivity : BaseBackStackSyncActivity(), View.OnClick * @param uri A [Uri] of the selected file. */ protected open fun handleSelectedFile(uri: Uri) { - keyImportModel = KeyImportModel(uri, null, isPrivateKeyMode, KeyDetails.Type.FILE) + keyImportModel = KeyImportModel(uri, null, isPrivateKeyMode, KeyImportDetails.SourceType.FILE) privateKeysViewModel.parseKeys(keyImportModel, true, isPrivateKeyMode) } @@ -243,12 +243,12 @@ abstract class BaseImportKeyActivity : BaseBackStackSyncActivity(), View.OnClick val parseKeyResult = it.data if (parseKeyResult == null || parseKeyResult.pgpKeyRingCollection.size() == 0) { - val msg = when (keyImportModel?.type) { - KeyDetails.Type.FILE -> + val msg = when (keyImportModel?.sourceType) { + KeyImportDetails.SourceType.FILE -> getString(R.string.file_has_wrong_pgp_structure, if (isPrivateKeyMode) getString(R.string.private_) else getString(R.string.public_)) - KeyDetails.Type.CLIPBOARD -> + KeyImportDetails.SourceType.CLIPBOARD -> getString(R.string.clipboard_has_wrong_structure, if (isPrivateKeyMode) getString(R.string.private_) else getString(R.string.public_)) @@ -258,7 +258,7 @@ abstract class BaseImportKeyActivity : BaseBackStackSyncActivity(), View.OnClick } showInfoDialogFragment(dialogMsg = msg) } else { - keyImportModel?.type?.let { type -> onKeyFound(type, parseKeyResult.toNodeKeyDetailsList()) } + keyImportModel?.sourceType?.let { type -> onKeyFound(type, parseKeyResult.toPgpKeyDetailsList()) } } countingIdlingResource.decrementSafely() @@ -279,8 +279,8 @@ abstract class BaseImportKeyActivity : BaseBackStackSyncActivity(), View.OnClick if (WRONG_STRUCTURE_ERROR == nodeException?.nodeError?.msg) { val mode = if (isPrivateKeyMode) getString(R.string.private_) else getString(R.string.public_) - msg = when (keyImportModel?.type) { - KeyDetails.Type.FILE -> + msg = when (keyImportModel?.sourceType) { + KeyImportDetails.SourceType.FILE -> getString(R.string.file_has_wrong_pgp_structure, mode) else -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/AddOtherAccountFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/AddOtherAccountFragment.kt index 9776a674a6..c86b2914e9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/AddOtherAccountFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/AddOtherAccountFragment.kt @@ -34,13 +34,13 @@ import com.flowcrypt.email.api.email.model.AuthCredentials import com.flowcrypt.email.api.email.model.SecurityType import com.flowcrypt.email.api.oauth.OAuth2Helper import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.addInputFilter import com.flowcrypt.email.extensions.hideKeyboard import com.flowcrypt.email.extensions.showInfoDialog import com.flowcrypt.email.extensions.showTwoWayDialog -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.CheckKeysActivity import com.flowcrypt.email.ui.activity.CreateOrImportKeyActivity import com.flowcrypt.email.ui.activity.SignInActivity @@ -414,7 +414,7 @@ class AddOtherAccountFragment : BaseSingInFragment(), AdapterView.OnItemSelected Result.Status.SUCCESS -> { dismissCurrentSnackBar() - val keyDetailsList = result.data as ArrayList? + val keyDetailsList = result.data as ArrayList? if (keyDetailsList?.isEmpty() == true) { authCreds?.let { authCredentials -> val account = AccountEntity(authCredentials) @@ -432,7 +432,7 @@ class AddOtherAccountFragment : BaseSingInFragment(), AdapterView.OnItemSelected val intent = CheckKeysActivity.newIntent( context = requireContext(), privateKeys = keyDetailsList ?: ArrayList(), - type = KeyDetails.Type.EMAIL, + sourceType = KeyImportDetails.SourceType.EMAIL, subTitle = subTitle, positiveBtnTitle = getString(R.string.continue_), negativeBtnTitle = getString(R.string.use_another_account) @@ -719,7 +719,7 @@ class AddOtherAccountFragment : BaseSingInFragment(), AdapterView.OnItemSelected private fun handleResultFromCheckKeysActivity(resultCode: Int, data: Intent?) { when (resultCode) { Activity.RESULT_OK, CheckKeysActivity.RESULT_SKIP_REMAINING_KEYS -> { - val keys: List? = data?.getParcelableArrayListExtra( + val keys: List? = data?.getParcelableArrayListExtra( CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS) if (keys.isNullOrEmpty()) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index bca13c2bce..6101151a48 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -19,12 +19,12 @@ import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.retrofit.response.api.DomainRulesResponse import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.showInfoDialog import com.flowcrypt.email.jetpack.viewmodel.EnterpriseDomainRulesViewModel -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.SecurityUtils +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.service.CheckClipboardToFindKeyService import com.flowcrypt.email.service.actionqueue.actions.LoadGmailAliasesAction import com.flowcrypt.email.ui.activity.CheckKeysActivity @@ -290,7 +290,7 @@ class MainSignInFragment : BaseSingInFragment() { if (result != null) { when (result.status) { Result.Status.SUCCESS -> { - onFetchKeysCompleted(result.data as ArrayList?) + onFetchKeysCompleted(result.data as ArrayList?) } Result.Status.ERROR, Result.Status.EXCEPTION -> { @@ -313,7 +313,7 @@ class MainSignInFragment : BaseSingInFragment() { } } - private fun onFetchKeysCompleted(keyDetailsList: ArrayList?) { + private fun onFetchKeysCompleted(keyDetailsList: ArrayList?) { if (keyDetailsList.isNullOrEmpty()) { getTempAccount()?.let { requireContext().startService(Intent(requireContext(), CheckClipboardToFindKeyService::class.java)) @@ -325,7 +325,7 @@ class MainSignInFragment : BaseSingInFragment() { val positiveBtnTitle = getString(R.string.continue_) val negativeBtnTitle = getString(R.string.use_another_account) val intent = CheckKeysActivity.newIntent(context = requireContext(), privateKeys = keyDetailsList, - type = KeyDetails.Type.EMAIL, subTitle = subTitle, positiveBtnTitle = positiveBtnTitle, + sourceType = KeyImportDetails.SourceType.EMAIL, subTitle = subTitle, positiveBtnTitle = positiveBtnTitle, negativeBtnTitle = negativeBtnTitle) startActivityForResult(intent, REQUEST_CODE_CHECK_PRIVATE_KEYS_FROM_GMAIL) } @@ -365,7 +365,7 @@ class MainSignInFragment : BaseSingInFragment() { private fun handleResultFromCheckKeysActivity(resultCode: Int, data: Intent?) { when (resultCode) { Activity.RESULT_OK, CheckKeysActivity.RESULT_SKIP_REMAINING_KEYS -> { - val keys: List? = data?.getParcelableArrayListExtra( + val keys: List? = data?.getParcelableArrayListExtra( CheckKeysActivity.KEY_EXTRA_UNLOCKED_PRIVATE_KEYS) if (keys.isNullOrEmpty()) { 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 e2aceb65b3..cb498d78b8 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 @@ -890,8 +890,9 @@ class MessageDetailsFragment : BaseFragment(), ProgressBehaviour, View.OnClickLi if (button != null) { if (existingPgpContact == null) { initSaveContactButton(block, button) - } else if (TextUtils.isEmpty(existingPgpContact.longid) - || keyDetails?.longId?.equals(existingPgpContact.longid!!, ignoreCase = true) == true) { + } else if (TextUtils.isEmpty(existingPgpContact.fingerprint) + || keyDetails?.fingerprint?.equals( + existingPgpContact.fingerprint!!, ignoreCase = true) == true) { initUpdateContactButton(block, button) } else { initReplaceContactButton(block, button) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PreviewImportPgpContactFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PreviewImportPgpContactFragment.kt index 9bf12e9162..c88dec4551 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PreviewImportPgpContactFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PreviewImportPgpContactFragment.kt @@ -24,13 +24,13 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.Constants import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.ContactEntity import com.flowcrypt.email.jetpack.viewmodel.ContactsViewModel import com.flowcrypt.email.model.PgpContact import com.flowcrypt.email.model.PublicKeyInfo import com.flowcrypt.email.model.results.LoaderResult +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.ui.adapter.ImportPgpContactsRecyclerViewAdapter @@ -224,7 +224,7 @@ class PreviewImportPgpContactFragment : BaseFragment(), View.OnClickListener, private fun parseKeys(inputStream: InputStream): LoaderResult { try { - val details = PgpKey.parseKeys(inputStream).toNodeKeyDetailsList() + val details = PgpKey.parseKeys(inputStream).toPgpKeyDetailsList() return if (!CollectionUtils.isEmpty(details)) { LoaderResult(parsePublicKeysInfo(details), null) @@ -246,7 +246,7 @@ class PreviewImportPgpContactFragment : BaseFragment(), View.OnClickListener, } } - private fun parsePublicKeysInfo(details: List): List { + private fun parsePublicKeysInfo(details: List): List { val publicKeyInfoList = ArrayList() val emails = HashSet() @@ -275,10 +275,9 @@ class PreviewImportPgpContactFragment : BaseFragment(), View.OnClickListener, return publicKeyInfoList } - private fun getPublicKeyInfo(nodeKeyDetails: NodeKeyDetails, emails: MutableSet): PublicKeyInfo? { - val fingerprint = nodeKeyDetails.fingerprint - val longId = nodeKeyDetails.longId - var keyOwner: String? = nodeKeyDetails.primaryPgpContact.email + private fun getPublicKeyInfo(pgpKeyDetails: PgpKeyDetails, emails: MutableSet): PublicKeyInfo? { + val fingerprint = pgpKeyDetails.fingerprint + var keyOwner: String? = pgpKeyDetails.primaryPgpContact.email if (keyOwner != null) { keyOwner = keyOwner.toLowerCase(Locale.getDefault()) @@ -292,7 +291,7 @@ class PreviewImportPgpContactFragment : BaseFragment(), View.OnClickListener, if (weakRef.get() != null) { val contact = FlowCryptRoomDatabase.getDatabase(weakRef.get()?.requireContext()!!) .contactsDao().getContactByEmail(keyOwner)?.toPgpContact() - return PublicKeyInfo(fingerprint!!, keyOwner, longId!!, contact, nodeKeyDetails.publicKey!!) + return PublicKeyInfo(fingerprint, keyOwner, contact, pgpKeyDetails.publicKey) } } return null @@ -312,7 +311,7 @@ class PreviewImportPgpContactFragment : BaseFragment(), View.OnClickListener, for (publicKeyInfo in publicKeyInfoList) { val pgpContact = PgpContact(publicKeyInfo.keyOwner, null, publicKeyInfo.publicKey, - true, null, publicKeyInfo.fingerprint, publicKeyInfo.longId, 0) + true, null, publicKeyInfo.fingerprint, 0) if (publicKeyInfo.hasPgpContact()) { if (publicKeyInfo.isUpdateEnabled) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeyDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeyDetailsFragment.kt index ad0d50d8b8..dde72c8dbc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeyDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeyDetailsFragment.kt @@ -18,6 +18,8 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import android.widget.Button +import android.widget.EditText import android.widget.TextView import android.widget.Toast import androidx.fragment.app.Fragment @@ -25,25 +27,30 @@ import androidx.fragment.app.viewModels import com.flowcrypt.email.Constants import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.database.entity.KeyEntity import com.flowcrypt.email.extensions.decrementSafely +import com.flowcrypt.email.extensions.gone import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.showInfoDialog import com.flowcrypt.email.extensions.showTwoWayDialog import com.flowcrypt.email.extensions.toast +import com.flowcrypt.email.extensions.visible import com.flowcrypt.email.jetpack.viewmodel.CheckPrivateKeysViewModel +import com.flowcrypt.email.jetpack.viewmodel.PgpKeyDetailsViewModel import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel -import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.jetpack.viewmodel.factory.PgpKeyDetailsViewModelFactory +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment +import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour import com.flowcrypt.email.ui.activity.fragment.dialog.InfoDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.UIUtil import com.flowcrypt.email.util.exception.ExceptionUtil import com.google.android.material.snackbar.Snackbar +import org.pgpainless.util.Passphrase import java.io.FileNotFoundException import java.util.* -import java.util.concurrent.TimeUnit /** * This [Fragment] helps to show details about the given key. @@ -53,33 +60,35 @@ import java.util.concurrent.TimeUnit * Time: 12:43 * E-mail: DenBond7@gmail.com */ -class PrivateKeyDetailsFragment : BaseFragment() { +class PrivateKeyDetailsFragment : BaseFragment(), ProgressBehaviour { + override val progressView: View? + get() = view?.findViewById(R.id.progress) + override val contentView: View? + get() = view?.findViewById(R.id.content) + override val statusView: View? + get() = view?.findViewById(R.id.status) + + private var tVFingerprint: TextView? = null + private var tVDate: TextView? = null + private var tVUsers: TextView? = null private var tVPassPhraseVerification: TextView? = null + private var eTKeyPassword: EditText? = null + private var btnForgetPassphrase: Button? = null + private var gCheckPassphrase: View? = null private val privateKeysViewModel: PrivateKeysViewModel by viewModels() private val checkPrivateKeysViewModel: CheckPrivateKeysViewModel by viewModels() - private var nodeKeyDetails: NodeKeyDetails? = null + private val pgpKeyDetailsViewModel: PgpKeyDetailsViewModel by viewModels { + PgpKeyDetailsViewModelFactory( + arguments?.getString(KEY_PGP_KEY_FINGERPRINT), + requireActivity().application + ) + } override val contentResourceId: Int = R.layout.fragment_private_key_details override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) - - val args = arguments - if (args != null) { - nodeKeyDetails = args.getParcelable(KEY_NODE_KEY_DETAILS) - } - - if (nodeKeyDetails == null) { - parentFragmentManager.popBackStack() - } else { - nodeKeyDetails?.let { - val context = context ?: return@let - val passPhrase = KeysStorageImpl.getInstance(context) - .getPgpPrivateKey(it.longId)?.passphrase ?: "" - checkPrivateKeysViewModel.checkKeys(listOf(it), passPhrase) - } - } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -91,11 +100,13 @@ class PrivateKeyDetailsFragment : BaseFragment() { return when (item.itemId) { R.id.menuActionDeleteKey -> { showTwoWayDialog( - dialogTitle = "", - dialogMsg = requireContext().resources.getQuantityString(R.plurals.delete_key_question, 1, 1), - positiveButtonTitle = getString(android.R.string.ok), - negativeButtonTitle = getString(android.R.string.cancel), - requestCode = REQUEST_CODE_DELETE_KEY_DIALOG + dialogTitle = "", + dialogMsg = requireContext().resources.getQuantityString( + R.plurals.delete_key_question, 1, 1 + ), + positiveButtonTitle = getString(android.R.string.ok), + negativeButtonTitle = getString(android.R.string.cancel), + requestCode = REQUEST_CODE_DELETE_KEY_DIALOG ) true } @@ -108,6 +119,8 @@ class PrivateKeyDetailsFragment : BaseFragment() { super.onViewCreated(view, savedInstanceState) supportActionBar?.setTitle(R.string.key_details) initViews(view) + updateViews() + setupPgpKeyDetailsViewModel() setupPrivateKeysViewModel() setupCheckPrivateKeysViewModel() } @@ -123,8 +136,13 @@ class PrivateKeyDetailsFragment : BaseFragment() { REQUEST_CODE_DELETE_KEY_DIALOG -> { when (resultCode) { TwoWayDialogFragment.RESULT_OK -> { - nodeKeyDetails?.let { - account?.let { accountEntity -> privateKeysViewModel.deleteKeys(accountEntity, listOf(it)) } + pgpKeyDetailsViewModel.getPgpKeyDetails()?.let { + account?.let { accountEntity -> + privateKeysViewModel.deleteKeys( + accountEntity, + listOf(it) + ) + } } } } @@ -136,7 +154,11 @@ class PrivateKeyDetailsFragment : BaseFragment() { private fun saveKey(data: Intent) { try { - GeneralUtil.writeFileFromStringToUri(requireContext(), data.data!!, nodeKeyDetails!!.publicKey!!) + GeneralUtil.writeFileFromStringToUri( + context = requireContext(), + uri = data.data!!, + data = pgpKeyDetailsViewModel.getPgpKeyDetails()!!.publicKey + ) Toast.makeText(context, getString(R.string.saved), Toast.LENGTH_SHORT).show() } catch (e: Exception) { e.printStackTrace() @@ -148,7 +170,12 @@ class PrivateKeyDetailsFragment : BaseFragment() { showInfoSnackbar(requireView(), error, Snackbar.LENGTH_LONG) try { - data.data?.let { DocumentsContract.deleteDocument(requireContext().contentResolver, it) } + data.data?.let { + DocumentsContract.deleteDocument( + requireContext().contentResolver, + it + ) + } } catch (fileNotFound: FileNotFoundException) { fileNotFound.printStackTrace() } @@ -166,41 +193,96 @@ class PrivateKeyDetailsFragment : BaseFragment() { } private fun initViews(view: View) { - val pgpContacts = nodeKeyDetails?.pgpContacts ?: emptyList() - val emails = ArrayList() - - for ((email) in pgpContacts) { - emails.add(email) - } - - val textViewFingerprint = view.findViewById(R.id.textViewFingerprint) - UIUtil.setHtmlTextToTextView(getString(R.string.template_fingerprint, - GeneralUtil.doSectionsInText(" ", nodeKeyDetails?.fingerprint, 4)), textViewFingerprint) - - val textViewLongId = view.findViewById(R.id.textViewLongId) - textViewLongId?.text = getString(R.string.template_longid, nodeKeyDetails?.longId) + tVFingerprint = view.findViewById(R.id.textViewFingerprint) + tVDate = view.findViewById(R.id.textViewDate) + tVUsers = view.findViewById(R.id.textViewUsers) + tVPassPhraseVerification = view.findViewById(R.id.tVPassPhraseVerification) + eTKeyPassword = view.findViewById(R.id.eTKeyPassword) + gCheckPassphrase = view.findViewById(R.id.gCheckPassphrase) - val textViewDate = view.findViewById(R.id.textViewDate) - textViewDate?.text = getString(R.string.template_date, DateFormat.getMediumDateFormat(context).format( - Date(TimeUnit.MILLISECONDS.convert(nodeKeyDetails?.created ?: 0, TimeUnit.SECONDS)))) + initButtons(view) + } - val textViewUsers = view.findViewById(R.id.textViewUsers) - textViewUsers.text = getString(R.string.template_users, TextUtils.join(", ", emails)) + private fun updateViews() { + pgpKeyDetailsViewModel.getPgpKeyDetails()?.let { value -> + UIUtil.setHtmlTextToTextView( + getString( + R.string.template_fingerprint, + GeneralUtil.doSectionsInText(" ", value.fingerprint, 4) + ), tVFingerprint + ) + + tVDate?.text = getString( + R.string.template_date, + DateFormat.getMediumDateFormat(context).format(Date(value.created)) + ) + + tVUsers?.text = getString( + R.string.template_users, + TextUtils.join(", ", value.pgpContacts.map { it.email }) + ) + + val passPhrase = pgpKeyDetailsViewModel.getPassphrase() + val passPhraseType = pgpKeyDetailsViewModel.getPassphraseType() + if (passPhrase == null || passPhrase.isEmpty) { + handlePassphraseNotProvided() + return + } - tVPassPhraseVerification = view.findViewById(R.id.tVPassPhraseVerification) + if (passPhraseType == KeyEntity.PassphraseType.RAM) { + btnForgetPassphrase?.visible() + } + } + } - initButtons(view) + private fun handlePassphraseNotProvided() { + tVPassPhraseVerification?.setTextColor(UIUtil.getColor(requireContext(), R.color.red)) + tVPassPhraseVerification?.text = getString(R.string.pass_phrase_not_provided) + btnForgetPassphrase?.gone() + gCheckPassphrase?.visible() } private fun initButtons(view: View) { + btnForgetPassphrase = view.findViewById(R.id.btnForgetPassphrase) + btnForgetPassphrase?.setOnClickListener { + pgpKeyDetailsViewModel.forgetPassphrase() + toast(getString(R.string.passphrase_purged_from_memory)) + eTKeyPassword?.text = null + } + + view.findViewById(R.id.btnUpdatePassphrase)?.setOnClickListener { + UIUtil.hideSoftInput(requireContext(), eTKeyPassword) + val typedText = eTKeyPassword?.text?.toString() + if (typedText.isNullOrEmpty()) { + showInfoSnackbar(eTKeyPassword, getString(R.string.passphrase_must_be_non_empty)) + } else { + snackBar?.dismiss() + eTKeyPassword?.let { + val passPhrase = Passphrase.fromPassword(typedText) + val pgpKeyDetails = pgpKeyDetailsViewModel.getPgpKeyDetails() ?: return@let + val passPhraseType = pgpKeyDetailsViewModel.getPassphraseType() ?: return@let + checkPrivateKeysViewModel.checkKeys(listOf(pgpKeyDetails), passPhrase, passPhraseType) + } + } + } + view.findViewById(R.id.btnShowPubKey)?.setOnClickListener { - val dialogFragment = InfoDialogFragment.newInstance("", nodeKeyDetails!!.publicKey!!) + val dialogFragment = InfoDialogFragment.newInstance( + dialogTitle = "", + dialogMsg = pgpKeyDetailsViewModel.getPgpKeyDetails()!!.publicKey + ) dialogFragment.show(parentFragmentManager, InfoDialogFragment::class.java.simpleName) } view.findViewById(R.id.btnCopyToClipboard)?.setOnClickListener { - val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setPrimaryClip(ClipData.newPlainText("pubKey", nodeKeyDetails?.publicKey)) + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip( + ClipData.newPlainText( + "pubKey", + pgpKeyDetailsViewModel.getPgpKeyDetails()?.publicKey + ) + ) Toast.makeText(context, getString(R.string.copied), Toast.LENGTH_SHORT).show() } view.findViewById(R.id.btnSaveToFile)?.setOnClickListener { @@ -215,10 +297,48 @@ class PrivateKeyDetailsFragment : BaseFragment() { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = Constants.MIME_TYPE_PGP_KEY - intent.putExtra(Intent.EXTRA_TITLE, "0x" + nodeKeyDetails!!.longId + ".asc") + intent.putExtra( + Intent.EXTRA_TITLE, + "0x" + pgpKeyDetailsViewModel.getPgpKeyDetails()!!.fingerprint + ".asc" + ) startActivityForResult(intent, REQUEST_CODE_GET_URI_FOR_SAVING_KEY) } + private fun setupPgpKeyDetailsViewModel() { + pgpKeyDetailsViewModel.pgpKeyDetailsLiveData.observe(viewLifecycleOwner, { + when (it.status) { + Result.Status.LOADING -> { + baseActivity.countingIdlingResource.incrementSafely() + showProgress() + } + + Result.Status.SUCCESS -> { + if (it.data == null) { + toast(getString(R.string.no_details_about_given_key)) + parentFragmentManager.popBackStack() + } else { + updateViews() + matchPassphrase(it.data) + } + showContent() + baseActivity.countingIdlingResource.decrementSafely() + } + + Result.Status.ERROR, Result.Status.EXCEPTION -> { + showContent() + baseActivity.countingIdlingResource.decrementSafely() + } + } + }) + } + + private fun matchPassphrase(pgpKeyDetails: PgpKeyDetails) { + val passPhrase = pgpKeyDetailsViewModel.getPassphrase() ?: return + if (passPhrase.isEmpty) return + val passPhraseType = pgpKeyDetailsViewModel.getPassphraseType() ?: return + checkPrivateKeysViewModel.checkKeys(listOf(pgpKeyDetails), passPhrase, passPhraseType) + } + private fun setupPrivateKeysViewModel() { privateKeysViewModel.deleteKeysLiveData.observe(viewLifecycleOwner, { when (it.status) { @@ -234,8 +354,10 @@ class PrivateKeyDetailsFragment : BaseFragment() { Result.Status.ERROR, Result.Status.EXCEPTION -> { showInfoDialog( - dialogMsg = it.exception?.message ?: it.exception?.javaClass?.simpleName - ?: "Couldn't delete a key with id = {${nodeKeyDetails?.longId ?: ""}}") + dialogMsg = it.exception?.message ?: it.exception?.javaClass?.simpleName + ?: "Couldn't delete a key with fingerprint =" + + " {${pgpKeyDetailsViewModel.getPgpKeyDetails()?.fingerprint ?: ""}}" + ) baseActivity.countingIdlingResource.decrementSafely() privateKeysViewModel.deleteKeysLiveData.value = Result.none() } @@ -252,23 +374,40 @@ class PrivateKeyDetailsFragment : BaseFragment() { Result.Status.SUCCESS -> { val checkResult = it.data?.firstOrNull() - val verificationMsg: String? + var verificationMsg: String? if (checkResult != null) { - if (checkResult.nodeKeyDetails.isPrivate) { + if (checkResult.pgpKeyDetails.isPrivate) { if (checkResult.e == null) { + tVPassPhraseVerification?.setTextColor( + UIUtil.getColor(requireContext(), R.color.colorPrimaryLight) + ) verificationMsg = getString(R.string.stored_pass_phrase_matched) + if (pgpKeyDetailsViewModel.getPassphraseType() == KeyEntity.PassphraseType.RAM) { + val existedPassphrase = pgpKeyDetailsViewModel.getPassphrase() + if (existedPassphrase == null || existedPassphrase.isEmpty) { + pgpKeyDetailsViewModel.updatePassphrase( + Passphrase.fromPassword(checkResult.passphrase) + ) + } + btnForgetPassphrase?.visible() + gCheckPassphrase?.gone() + } } else { - verificationMsg = getString(R.string.stored_pass_phrase_mismatch) - context?.let { - tVPassPhraseVerification?.setTextColor(UIUtil.getColor(it, R.color.red)) + if (pgpKeyDetailsViewModel.getPassphraseType() == KeyEntity.PassphraseType.RAM) { + eTKeyPassword?.requestFocus() + toast(R.string.password_is_incorrect) + verificationMsg = getString(R.string.pass_phrase_not_provided) + } else { + verificationMsg = getString(R.string.stored_pass_phrase_mismatch) + tVPassPhraseVerification?.setTextColor( + UIUtil.getColor(requireContext(), R.color.red) + ) } } } else verificationMsg = getString(R.string.not_private_key) } else { verificationMsg = getString(R.string.could_not_check_pass_phrase) - context?.let { - tVPassPhraseVerification?.setTextColor(UIUtil.getColor(it, R.color.red)) - } + tVPassPhraseVerification?.setTextColor(UIUtil.getColor(requireContext(), R.color.red)) } tVPassPhraseVerification?.text = verificationMsg @@ -276,9 +415,11 @@ class PrivateKeyDetailsFragment : BaseFragment() { } Result.Status.ERROR, Result.Status.EXCEPTION -> { - showInfoDialog(dialogMsg = it.exception?.message + showInfoDialog( + dialogMsg = it.exception?.message ?: it.exception?.javaClass?.simpleName - ?: getString(R.string.could_not_check_pass_phrase)) + ?: getString(R.string.could_not_check_pass_phrase) + ) baseActivity.countingIdlingResource.decrementSafely() } } @@ -286,16 +427,18 @@ class PrivateKeyDetailsFragment : BaseFragment() { } companion object { - private val KEY_NODE_KEY_DETAILS = - GeneralUtil.generateUniqueExtraKey("KEY_NODE_KEY_DETAILS", - PrivateKeyDetailsFragment::class.java) + private val KEY_PGP_KEY_FINGERPRINT = + GeneralUtil.generateUniqueExtraKey( + "KEY_PGP_KEY_FINGERPRINT", + PrivateKeyDetailsFragment::class.java + ) private const val REQUEST_CODE_GET_URI_FOR_SAVING_KEY = 1 private const val REQUEST_CODE_DELETE_KEY_DIALOG = 100 - fun newInstance(details: NodeKeyDetails): PrivateKeyDetailsFragment { + fun newInstance(fingerprint: String): PrivateKeyDetailsFragment { val keyDetailsFragment = PrivateKeyDetailsFragment() val args = Bundle() - args.putParcelable(KEY_NODE_KEY_DETAILS, details) + args.putString(KEY_PGP_KEY_FINGERPRINT, fingerprint) keyDetailsFragment.arguments = args return keyDetailsFragment } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeysListFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeysListFragment.kt index b41a693645..f3b00345cb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeysListFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PrivateKeysListFragment.kt @@ -25,11 +25,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.showTwoWayDialog import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.ImportPrivateKeyActivity import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment @@ -57,9 +57,9 @@ class PrivateKeysListFragment : BaseFragment(), View.OnClickListener, PrivateKey override val contentResourceId: Int = R.layout.fragment_private_keys - private var tracker: SelectionTracker? = null + private var tracker: SelectionTracker? = null private var actionMode: ActionMode? = null - private val selectionObserver = object : SelectionTracker.SelectionObserver() { + private val selectionObserver = object : SelectionTracker.SelectionObserver() { override fun onSelectionChanged() { super.onSelectionChanged() when { @@ -145,15 +145,15 @@ class PrivateKeysListFragment : BaseFragment(), View.OnClickListener, PrivateKey } } - override fun onKeySelected(position: Int, nodeKeyDetails: NodeKeyDetails?) { + override fun onKeySelected(position: Int, pgpKeyDetails: PgpKeyDetails?) { if (tracker?.hasSelection() == true) { return } - nodeKeyDetails?.let { + pgpKeyDetails?.let { parentFragmentManager - .beginTransaction() - .replace(R.id.layoutContent, PrivateKeyDetailsFragment.newInstance(it)) + .beginTransaction() + .replace(R.id.layoutContent, PrivateKeyDetailsFragment.newInstance(it.fingerprint)) .addToBackStack(null) .commit() } @@ -218,9 +218,9 @@ class PrivateKeysListFragment : BaseFragment(), View.OnClickListener, PrivateKey tracker = SelectionTracker.Builder( javaClass.simpleName, recyclerView, - NodeKeyDetailsKeyProvider(recyclerViewAdapter.nodeKeyDetailsList), + NodeKeyDetailsKeyProvider(recyclerViewAdapter.pgpKeyDetailsList), PrivateKeyItemDetailsLookup(recyclerView), - StorageStrategy.createParcelableStorage(NodeKeyDetails::class.java) + StorageStrategy.createParcelableStorage(PgpKeyDetails::class.java) ).build() recyclerViewAdapter.tracker = tracker diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PublicKeyDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PublicKeyDetailsFragment.kt index aba3d7e6ca..a24e6f559b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PublicKeyDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/PublicKeyDetailsFragment.kt @@ -25,11 +25,11 @@ import androidx.lifecycle.lifecycleScope import com.flowcrypt.email.Constants import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.ContactEntity import com.flowcrypt.email.jetpack.viewmodel.ContactsViewModel import com.flowcrypt.email.jetpack.viewmodel.ParseKeysViewModel +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.EditContactActivity import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.util.GeneralUtil @@ -39,7 +39,6 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import java.io.FileNotFoundException import java.util.* -import java.util.concurrent.TimeUnit /** * This fragment shows the given public key details @@ -54,11 +53,11 @@ class PublicKeyDetailsFragment : BaseFragment() { private val parseKeysViewModel: ParseKeysViewModel by viewModels() private var contactEntity: ContactEntity? = null - private var details: NodeKeyDetails? = null + private var details: PgpKeyDetails? = null private var progressBar: View? = null private var content: View? = null private var layoutUsers: ViewGroup? = null - private var layoutLongIds: ViewGroup? = null + private var layoutFingerprints: ViewGroup? = null private var textViewAlgorithm: TextView? = null private var textViewCreated: TextView? = null @@ -210,7 +209,7 @@ class PublicKeyDetailsFragment : BaseFragment() { progressBar = view.findViewById(R.id.progressBar) content = view.findViewById(R.id.layoutContent) layoutUsers = view.findViewById(R.id.layoutUsers) - layoutLongIds = view.findViewById(R.id.layoutLongIds) + layoutFingerprints = view.findViewById(R.id.layoutFingerprints) textViewAlgorithm = view.findViewById(R.id.textViewAlgorithm) textViewCreated = view.findViewById(R.id.textViewCreated) } @@ -223,17 +222,16 @@ class PublicKeyDetailsFragment : BaseFragment() { layoutUsers?.addView(textView) } - layoutLongIds?.removeAllViews() + layoutFingerprints?.removeAllViews() details?.ids?.forEachIndexed { index, s -> - val textViewLongId = TextView(context) - textViewLongId.text = getString(R.string.template_long_id, index + 1, s.longId) - layoutLongIds?.addView(textViewLongId) + val textViewFingerprint = TextView(context) + textViewFingerprint.text = getString(R.string.template_fingerprint_2, index + 1, s.fingerprint) + layoutFingerprints?.addView(textViewFingerprint) } textViewAlgorithm?.text = getString(R.string.template_algorithm, details?.algo?.algorithm) textViewCreated?.text = getString(R.string.template_created, - DateFormat.getMediumDateFormat(context).format( - Date(TimeUnit.MILLISECONDS.convert(details?.created ?: 0, TimeUnit.SECONDS)))) + DateFormat.getMediumDateFormat(context).format(Date(details?.created ?: 0))) } private fun chooseDest() { @@ -242,7 +240,7 @@ class PublicKeyDetailsFragment : BaseFragment() { intent.type = Constants.MIME_TYPE_PGP_KEY val sanitizedEmail = contactEntity?.email?.replace("[^a-z0-9]".toRegex(), "") - val fileName = "0x" + details?.longId + "-" + sanitizedEmail + "-publickey" + ".asc" + val fileName = "0x" + details?.fingerprint + "-" + sanitizedEmail + "-publickey" + ".asc" intent.putExtra(Intent.EXTRA_TITLE, fileName) startActivityForResult(intent, REQUEST_CODE_GET_URI_FOR_SAVING_KEY) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/BaseSingInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/BaseSingInFragment.kt index ab82d24674..ab0e9acc94 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/BaseSingInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/BaseSingInFragment.kt @@ -13,13 +13,13 @@ import androidx.fragment.app.viewModels import androidx.work.WorkManager import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.observeOnce import com.flowcrypt.email.extensions.showInfoDialog import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel import com.flowcrypt.email.jetpack.workmanager.sync.BaseSyncWorker -import com.flowcrypt.email.model.KeyDetails +import com.flowcrypt.email.model.KeyImportDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.service.IdleService import com.flowcrypt.email.service.actionqueue.actions.LoadGmailAliasesAction import com.flowcrypt.email.ui.activity.EmailManagerActivity @@ -37,7 +37,7 @@ abstract class BaseSingInFragment : BaseOAuthFragment(), ProgressBehaviour { protected val privateKeysViewModel: PrivateKeysViewModel by viewModels() protected val existedAccounts = mutableListOf() - protected val importCandidates = mutableListOf() + protected val importCandidates = mutableListOf() abstract fun getTempAccount(): AccountEntity? @@ -73,7 +73,7 @@ abstract class BaseSingInFragment : BaseOAuthFragment(), ProgressBehaviour { duration = Snackbar.LENGTH_INDEFINITE, onClickListener = { getTempAccount()?.let { accountEntity -> - privateKeysViewModel.encryptAndSaveKeysToDatabase(accountEntity, e.keys, KeyDetails.Type.EMAIL) + privateKeysViewModel.encryptAndSaveKeysToDatabase(accountEntity, e.keys, KeyImportDetails.SourceType.EMAIL) } } ) @@ -101,7 +101,7 @@ abstract class BaseSingInFragment : BaseOAuthFragment(), ProgressBehaviour { context?.let { context -> WorkManager.getInstance(context).cancelAllWorkByTag(BaseSyncWorker.TAG_SYNC) } getTempAccount()?.let { accountEntity -> - privateKeysViewModel.encryptAndSaveKeysToDatabase(accountEntity, importCandidates, KeyDetails.Type.EMAIL) + privateKeysViewModel.encryptAndSaveKeysToDatabase(accountEntity, importCandidates, KeyImportDetails.SourceType.EMAIL) } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/CreateMessageFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/CreateMessageFragment.kt index 6006fae121..8c3f631d2e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/CreateMessageFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/CreateMessageFragment.kt @@ -50,12 +50,12 @@ import com.flowcrypt.email.api.email.model.IncomingMessageInfo import com.flowcrypt.email.api.email.model.OutgoingMessageInfo import com.flowcrypt.email.api.email.model.ServiceInfo import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.ContactEntity import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely +import com.flowcrypt.email.extensions.org.bouncycastle.openpgp.toPgpKeyDetails import com.flowcrypt.email.extensions.showInfoDialog import com.flowcrypt.email.extensions.showKeyboard import com.flowcrypt.email.jetpack.viewmodel.AccountAliasesViewModel @@ -64,6 +64,7 @@ import com.flowcrypt.email.model.MessageEncryptionType import com.flowcrypt.email.model.MessageType import com.flowcrypt.email.model.PgpContact import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.CreateMessageActivity import com.flowcrypt.email.ui.activity.ImportPublicKeyActivity import com.flowcrypt.email.ui.activity.SelectContactsActivity @@ -895,7 +896,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad for (sublist in pgpContactsList) { sublist?.let { for (pgpContact in it) { - if (pgpContact.nodeKeyDetails?.isExpired == true) { + if (pgpContact.pgpKeyDetails?.isExpired == true) { showInfoDialog(dialogMsg = getString(R.string.warning_one_of_pub_keys_is_expired)) return true } @@ -923,7 +924,7 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad for (pgpContactChipSpan in pgpContactChipSpans) { if (pgpContact.email.equals(pgpContactChipSpan.text.toString(), ignoreCase = true)) { pgpContactChipSpan.hasPgp = pgpContact.hasPgp - pgpContactChipSpan.isExpired = pgpContact.nodeKeyDetails?.isExpired + pgpContactChipSpan.isExpired = pgpContact.pgpKeyDetails?.isExpired break } } @@ -1442,9 +1443,10 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad fromAddrs?.clear() fromAddrs?.addAll(aliases) - KeysStorageImpl.getInstance(requireContext().applicationContext).nodeKeyDetailsLiveData.value?.let { - updateFromAddressAdapter(it) - } + updateFromAddressAdapter( + KeysStorageImpl.getInstance(requireContext()).getPGPSecretKeyRings().map { key -> + key.toPgpKeyDetails() + }) if (msgInfo != null) { prepareAliasForReplyIfNeeded(aliases) @@ -1477,12 +1479,13 @@ class CreateMessageFragment : BaseSyncFragment(), View.OnFocusChangeListener, Ad } private fun setupPrivateKeysViewModel() { - KeysStorageImpl.getInstance(requireContext().applicationContext).nodeKeyDetailsLiveData.observe(viewLifecycleOwner, { - updateFromAddressAdapter(it) - }) + KeysStorageImpl.getInstance(requireContext()).secretKeyRingsLiveData + .observe(viewLifecycleOwner, { keys -> + updateFromAddressAdapter(keys.map { key -> key.toPgpKeyDetails() }) + }) } - private fun updateFromAddressAdapter(list: List) { + private fun updateFromAddressAdapter(list: List) { val setOfUsers = list.map { nodeKeyDetails -> nodeKeyDetails.pgpContacts } .flatten() .map { contact -> contact.email } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePublicKeyDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePublicKeyDialogFragment.kt index beac06bc72..b2d08b3265 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePublicKeyDialogFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/ChoosePublicKeyDialogFragment.kt @@ -22,11 +22,11 @@ import com.flowcrypt.email.R import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.model.AttachmentInfo import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel import com.flowcrypt.email.model.PgpContact +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.adapter.PubKeysArrayAdapter import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.UIUtil @@ -205,15 +205,15 @@ class ChoosePublicKeyDialogFragment : BaseDialogFragment(), View.OnClickListener } /** - * Get a list with the matched [NodeKeyDetails]. If the sender email matched email the email from + * Get a list with the matched [PgpKeyDetails]. If the sender email matched email the email from * [PgpContact] which got from the private key than we return a list with relevant public keys. * - * @return A matched [NodeKeyDetails] or null. + * @return A matched [PgpKeyDetails] or null. */ - private fun getMatchedKeys(nodeKeyDetailsList: List): List { - val keyDetails = ArrayList() + private fun getMatchedKeys(pgpKeyDetailsList: List): List { + val keyDetails = ArrayList() - for (nodeKeyDetails in nodeKeyDetailsList) { + for (nodeKeyDetails in pgpKeyDetailsList) { val (email) = nodeKeyDetails.primaryPgpContact if (email.equals(this.email, ignoreCase = true)) { keyDetails.add(nodeKeyDetails) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/UpdatePublicKeyOfContactDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/UpdatePublicKeyOfContactDialogFragment.kt index e9ede7110b..091c1b2797 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/UpdatePublicKeyOfContactDialogFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/UpdatePublicKeyOfContactDialogFragment.kt @@ -21,10 +21,9 @@ import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.util.GeneralUtil import java.util.* -import java.util.concurrent.TimeUnit /** * @author Denis Bondarenko @@ -33,7 +32,7 @@ import java.util.concurrent.TimeUnit * E-mail: DenBond7@gmail.com */ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { - private var nodeKeyDetails: NodeKeyDetails? = null + private var pgpKeyDetails: PgpKeyDetails? = null private var expectedEmail: String? = null private var onKeySelectedListener: OnKeySelectedListener? = null @@ -47,7 +46,7 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - nodeKeyDetails = arguments?.getParcelable(KEY_NODE_KEY_DETAILS) + pgpKeyDetails = arguments?.getParcelable(KEY_NODE_KEY_DETAILS) expectedEmail = arguments?.getString(KEY_EXPECTED_EMAIL) } @@ -67,8 +66,8 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { var isExpectedEmailFound = false - if (nodeKeyDetails?.mimeAddresses.isNullOrEmpty()) { - nodeKeyDetails?.users?.forEach { user -> + if (pgpKeyDetails?.mimeAddresses.isNullOrEmpty()) { + pgpKeyDetails?.users?.forEach { user -> val userLayout = layoutInflater.inflate(R.layout.item_user_with_email, lUsers, false) val tVUserName = userLayout.findViewById(R.id.tVUserName) tVUserName.text = user @@ -76,7 +75,7 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { expectedEmail?.let { email -> isExpectedEmailFound = user.contains(email, ignoreCase = true) } } } else { - nodeKeyDetails?.mimeAddresses?.forEach { address -> + pgpKeyDetails?.mimeAddresses?.forEach { address -> val userLayout = layoutInflater.inflate(R.layout.item_user_with_email, lUsers, false) val tVUserName = userLayout.findViewById(R.id.tVUserName) tVUserName.text = address.personal @@ -92,34 +91,32 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { tVWarning?.text = getString(R.string.warning_no_expected_email, expectedEmail ?: "") } - nodeKeyDetails?.ids?.forEach { uid -> + pgpKeyDetails?.ids?.forEach { uid -> val tVFingerprint = TextView(context) tVFingerprint.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.default_text_size_very_small)) tVFingerprint.typeface = Typeface.DEFAULT_BOLD tVFingerprint.setTextColor(ContextCompat.getColor(requireContext(), R.color.silver)) tVFingerprint.setTextIsSelectable(true) - tVFingerprint.text = "* ${GeneralUtil.doSectionsInText(" ", uid.fingerprint ?: "", 4)}" + tVFingerprint.text = "* ${GeneralUtil.doSectionsInText(" ", uid.fingerprint, 4)}" lFingerprints?.addView(tVFingerprint) } - tVAlgorithm?.text = getString(R.string.template_algorithm, nodeKeyDetails?.algo?.algorithm) - tVAlgorithmBitsOrCurve?.text = if (nodeKeyDetails?.algo?.bits == 0) { - getString(R.string.template_curve, nodeKeyDetails?.algo?.curve) + tVAlgorithm?.text = getString(R.string.template_algorithm, pgpKeyDetails?.algo?.algorithm) + tVAlgorithmBitsOrCurve?.text = if (pgpKeyDetails?.algo?.bits == 0) { + getString(R.string.template_curve, pgpKeyDetails?.algo?.curve) } else { - getString(R.string.template_algorithm_bits, nodeKeyDetails?.algo?.bits.toString()) + getString(R.string.template_algorithm_bits, pgpKeyDetails?.algo?.bits.toString()) } - tVCreated?.text = getString(R.string.template_created, DateFormat.getMediumDateFormat(context).format( - Date(TimeUnit.MILLISECONDS.convert(nodeKeyDetails?.created ?: 0, TimeUnit.SECONDS)))) - tVModified?.text = getString(R.string.template_modified, DateFormat.getMediumDateFormat(context) - .format(Date(TimeUnit.MILLISECONDS.convert(nodeKeyDetails?.lastModified - ?: 0, TimeUnit.SECONDS)))) + tVCreated?.text = getString(R.string.template_created, + DateFormat.getMediumDateFormat(context).format(Date(pgpKeyDetails?.created ?: 0))) + tVModified?.text = getString(R.string.template_modified, + DateFormat.getMediumDateFormat(context).format(Date(pgpKeyDetails?.lastModified ?: 0))) - if (nodeKeyDetails?.isExpired == true) { + if (pgpKeyDetails?.isExpired == true) { tVWarning.visibility = View.VISIBLE - val warningText = getString(R.string.warning_key_expired, DateFormat.getMediumDateFormat(context) - .format(Date(TimeUnit.MILLISECONDS.convert(nodeKeyDetails?.expiration - ?: 0, TimeUnit.SECONDS)))) + val warningText = getString(R.string.warning_key_expired, + DateFormat.getMediumDateFormat(context).format(Date(pgpKeyDetails?.expiration ?: 0))) if (tVWarning.text.isNullOrEmpty()) { tVWarning?.text = warningText } else tVWarning.append("\n\n" + warningText) @@ -130,7 +127,7 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { builder.setView(view) builder.setPositiveButton(getString(R.string.use_this_key)) { _, _ -> - nodeKeyDetails?.let { keyDetails -> onKeySelectedListener?.onKeySelected(keyDetails) } + pgpKeyDetails?.let { keyDetails -> onKeySelectedListener?.onKeySelected(keyDetails) } } builder.setNegativeButton(R.string.cancel) { _, _ -> }//do nothing @@ -139,7 +136,7 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { } interface OnKeySelectedListener { - fun onKeySelected(nodeKeyDetails: NodeKeyDetails) + fun onKeySelected(pgpKeyDetails: PgpKeyDetails) } companion object { @@ -148,11 +145,11 @@ class UpdatePublicKeyOfContactDialogFragment : BaseDialogFragment() { private val KEY_EXPECTED_EMAIL = GeneralUtil.generateUniqueExtraKey("KEY_EXPECTED_EMAIL", UpdatePublicKeyOfContactDialogFragment::class.java) - fun newInstance(expectedEmail: String?, nodeKeyDetails: NodeKeyDetails): DialogFragment { + fun newInstance(expectedEmail: String?, pgpKeyDetails: PgpKeyDetails): DialogFragment { return UpdatePublicKeyOfContactDialogFragment().apply { arguments = Bundle().apply { putString(KEY_EXPECTED_EMAIL, expectedEmail) - putParcelable(KEY_NODE_KEY_DETAILS, nodeKeyDetails) + putParcelable(KEY_NODE_KEY_DETAILS, pgpKeyDetails) } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/preferences/SecuritySettingsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/preferences/SecuritySettingsFragment.kt index 8a1010ec5e..42ce21d19c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/preferences/SecuritySettingsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/preferences/SecuritySettingsFragment.kt @@ -6,13 +6,10 @@ package com.flowcrypt.email.ui.activity.fragment.preferences import android.os.Bundle -import android.view.View -import androidx.fragment.app.viewModels import androidx.preference.Preference import com.flowcrypt.email.Constants import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails -import com.flowcrypt.email.jetpack.viewmodel.PrivateKeysViewModel +import com.flowcrypt.email.security.KeysStorageImpl import com.flowcrypt.email.ui.activity.ChangePassPhraseActivity import com.flowcrypt.email.ui.activity.fragment.base.BasePreferenceFragment import com.flowcrypt.email.util.UIUtil @@ -26,18 +23,6 @@ import com.flowcrypt.email.util.UIUtil * E-mail: DenBond7@gmail.com */ class SecuritySettingsFragment : BasePreferenceFragment(), Preference.OnPreferenceClickListener { - private val privateKeysViewModel: PrivateKeysViewModel by viewModels() - private var nodeKeyDetailsList: MutableList = mutableListOf() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - privateKeysViewModel.nodeKeyDetailsLiveData.observe(viewLifecycleOwner, { - nodeKeyDetailsList.clear() - nodeKeyDetailsList.addAll(it) - }) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences_security_settings, rootKey) findPreference(Constants.PREF_KEY_SECURITY_CHANGE_PASS_PHRASE)?.onPreferenceClickListener = this @@ -46,7 +31,7 @@ class SecuritySettingsFragment : BasePreferenceFragment(), Preference.OnPreferen override fun onPreferenceClick(preference: Preference): Boolean { return when (preference.key) { Constants.PREF_KEY_SECURITY_CHANGE_PASS_PHRASE -> { - if (nodeKeyDetailsList.isEmpty()) { + if (KeysStorageImpl.getInstance(requireContext()).getRawKeys().isEmpty()) { UIUtil.showInfoSnackbar(requireView(), getString(R.string.account_has_no_associated_keys, getString(R.string.support_email))) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/settings/SearchBackupsInEmailActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/settings/SearchBackupsInEmailActivity.kt index d475b8e2b6..602bedd959 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/settings/SearchBackupsInEmailActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/settings/SearchBackupsInEmailActivity.kt @@ -14,11 +14,11 @@ import android.widget.Toast import androidx.activity.viewModels import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.toast import com.flowcrypt.email.jetpack.viewmodel.BackupsViewModel +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.activity.BackupKeysActivity import com.flowcrypt.email.ui.activity.base.BaseSettingsBackStackSyncActivity import com.flowcrypt.email.util.UIUtil @@ -45,7 +45,7 @@ class SearchBackupsInEmailActivity : BaseSettingsBackStackSyncActivity(), View.O private lateinit var layoutBackupNotFound: View private lateinit var textViewBackupFound: TextView - private var privateKeys = mutableListOf() + private var privateKeys = mutableListOf() override val contentViewResourceId: Int get() = R.layout.activity_backup_settings diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttesterKeyAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttesterKeyAdapter.kt index 04d35db125..e7275fa315 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttesterKeyAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AttesterKeyAdapter.kt @@ -13,8 +13,8 @@ import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails import com.flowcrypt.email.security.KeysStorageImpl +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.util.UIUtil @@ -27,41 +27,41 @@ import com.flowcrypt.email.util.UIUtil * E-mail: DenBond7@gmail.com */ class AttesterKeyAdapter( - private val responses: MutableList = mutableListOf()) : RecyclerView.Adapter() { + private val pgpKeyDetailsList: MutableList = mutableListOf()) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.attester_key_item, parent, false)) } override fun getItemCount(): Int { - return responses.size + return pgpKeyDetailsList.size } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { val context = viewHolder.itemView.context - val nodeKeyDetails = responses[position] + val nodeKeyDetails = pgpKeyDetailsList[position] updateView(nodeKeyDetails, context, viewHolder) } - fun setData(newList: List) { - val productDiffUtilCallback = ResponseDiffUtilCallback(responses, newList) + fun setData(newList: List) { + val productDiffUtilCallback = ResponseDiffUtilCallback(pgpKeyDetailsList, newList) val productDiffResult = DiffUtil.calculateDiff(productDiffUtilCallback) - responses.clear() - responses.addAll(newList) + pgpKeyDetailsList.clear() + pgpKeyDetailsList.addAll(newList) productDiffResult.dispatchUpdatesTo(this) } - private fun updateView(nodeKeyDetails: NodeKeyDetails, context: Context, viewHolder: ViewHolder) { - viewHolder.textViewKeyOwner.text = nodeKeyDetails.primaryPgpContact.email + private fun updateView(pgpKeyDetails: PgpKeyDetails, context: Context, viewHolder: ViewHolder) { + viewHolder.textViewKeyOwner.text = pgpKeyDetails.primaryPgpContact.email when { - nodeKeyDetails.publicKey.isNullOrEmpty() -> { + pgpKeyDetails.publicKey.isNullOrEmpty() -> { viewHolder.textViewKeyAttesterStatus.setText(R.string.no_public_key_recorded) viewHolder.textViewKeyAttesterStatus.setTextColor(UIUtil.getColor(context, R.color.orange)) } - isPublicKeyMatched(viewHolder.itemView.context, nodeKeyDetails) -> { + isPublicKeyMatched(viewHolder.itemView.context, pgpKeyDetails) -> { viewHolder.textViewKeyAttesterStatus.setText(R.string.submitted_can_receive_encrypted_email) viewHolder.textViewKeyAttesterStatus.setTextColor(UIUtil.getColor(context, R.color.colorPrimary)) } @@ -74,14 +74,17 @@ class AttesterKeyAdapter( } /** - * Check is public key found, and the longid does not match any longids of saved keys. + * Check is public key found, and the fingerprint does not match any fingerprints of saved keys. * * @param context Interface to global information about an application environment. - * @param nodeKeyDetails The [NodeKeyDetails] object which contains info about a public key. - * @return true if public key found, and the longid does not match any longids of saved keys, otherwise false. + * @param pgpKeyDetails The [PgpKeyDetails] object which contains info about a public key. + * @return true if public key found, and the fingerprint does not match any + * fingerprints of saved keys, otherwise false. */ - private fun isPublicKeyMatched(context: Context, nodeKeyDetails: NodeKeyDetails): Boolean { - return KeysStorageImpl.getInstance(context).getPgpPrivateKey(nodeKeyDetails.longId) != null + private fun isPublicKeyMatched(context: Context, pgpKeyDetails: PgpKeyDetails): Boolean { + return pgpKeyDetails.fingerprint?.let { + KeysStorageImpl.getInstance(context).getPGPSecretKeyRingByFingerprint(it) != null + } ?: false } /** @@ -92,12 +95,12 @@ class AttesterKeyAdapter( var textViewKeyAttesterStatus: TextView = itemView.findViewById(R.id.textViewKeyAttesterStatus) } - inner class ResponseDiffUtilCallback(private val oldList: List, - private val newList: List) : DiffUtil.Callback() { + inner class ResponseDiffUtilCallback(private val oldList: List, + private val newList: List) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldProduct = oldList[oldItemPosition] val newProduct = newList[newItemPosition] - return oldProduct.longId == newProduct.longId + return oldProduct.fingerprint == newProduct.fingerprint } override fun getOldListSize(): Int = oldList.size diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ContactsRecyclerViewAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ContactsRecyclerViewAdapter.kt index 9cf8a12f7b..aa56e45eba 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ContactsRecyclerViewAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ContactsRecyclerViewAdapter.kt @@ -106,7 +106,7 @@ class ContactsRecyclerViewAdapter constructor(private val isDeleteEnabled: Boole override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldList[oldItemPosition] val newItem = newList[newItemPosition] - return oldItem.longId == newItem.longId + return oldItem.fingerprint == newItem.fingerprint } override fun getOldListSize(): Int = oldList.size diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ImportPgpContactsRecyclerViewAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ImportPgpContactsRecyclerViewAdapter.kt index 978526beb1..a1c10a3cde 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ImportPgpContactsRecyclerViewAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ImportPgpContactsRecyclerViewAdapter.kt @@ -80,7 +80,7 @@ class ImportPgpContactsRecyclerViewAdapter private fun saveContact(position: Int, v: View, context: Context, publicKeyInfo: PublicKeyInfo) { val pgpContact = PgpContact(publicKeyInfo.keyOwner, null, publicKeyInfo.publicKey, true, - null, publicKeyInfo.fingerprint, publicKeyInfo.longId, 0) + null, publicKeyInfo.fingerprint, 0) contactActionsListener?.onSaveContactClick(publicKeyInfo) Toast.makeText(context, R.string.contact_successfully_saved, Toast.LENGTH_SHORT).show() @@ -91,7 +91,7 @@ class ImportPgpContactsRecyclerViewAdapter private fun updateContact(position: Int, v: View, context: Context, publicKeyInfo: PublicKeyInfo) { val pgpContact = PgpContact(publicKeyInfo.keyOwner, null, publicKeyInfo.publicKey, true, - null, publicKeyInfo.fingerprint, publicKeyInfo.longId, 0) + null, publicKeyInfo.fingerprint, 0) contactActionsListener?.onUpdateContactClick(publicKeyInfo) Toast.makeText(context, R.string.contact_successfully_updated, Toast.LENGTH_SHORT).show() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysRecyclerViewAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysRecyclerViewAdapter.kt index 969fda4dee..b6ae33be0b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysRecyclerViewAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PrivateKeysRecyclerViewAdapter.kt @@ -16,11 +16,10 @@ import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.adapter.selection.SelectionNodeKeyDetailsDetails import com.flowcrypt.email.util.GeneralUtil import java.util.* -import java.util.concurrent.TimeUnit /** * This adapter will be used to show a list of private keys. @@ -32,10 +31,10 @@ import java.util.concurrent.TimeUnit */ class PrivateKeysRecyclerViewAdapter(context: Context, private val listener: OnKeySelectedListener?, - val nodeKeyDetailsList: MutableList = mutableListOf()) + val pgpKeyDetailsList: MutableList = mutableListOf()) : RecyclerView.Adapter() { private val dateFormat: java.text.DateFormat = DateFormat.getMediumDateFormat(context) - var tracker: SelectionTracker? = null + var tracker: SelectionTracker? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.key_item, parent, false) @@ -43,18 +42,17 @@ class PrivateKeysRecyclerViewAdapter(context: Context, } override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val nodeKeyDetails = nodeKeyDetailsList[position] + val nodeKeyDetails = pgpKeyDetailsList[position] tracker?.isSelected(nodeKeyDetails)?.let { viewHolder.setActivated(it) } val email = nodeKeyDetails.primaryPgpContact.email viewHolder.textViewKeyOwner.text = email - viewHolder.textViewLongId.text = GeneralUtil.doSectionsInText( + viewHolder.textViewFingerprint.text = GeneralUtil.doSectionsInText( originalString = nodeKeyDetails.fingerprint, groupSize = 4) val timestamp = nodeKeyDetails.created if (timestamp != -1L) { - viewHolder.textViewCreationDate.text = dateFormat.format( - Date(TimeUnit.MILLISECONDS.convert(timestamp, TimeUnit.SECONDS))) + viewHolder.textViewCreationDate.text = dateFormat.format(Date(timestamp)) } else { viewHolder.textViewCreationDate.text = null } @@ -65,25 +63,25 @@ class PrivateKeysRecyclerViewAdapter(context: Context, } override fun getItemCount(): Int { - return nodeKeyDetailsList.size + return pgpKeyDetailsList.size } - fun swap(newList: List) { - val diffUtilCallback = DiffUtilCallback(this.nodeKeyDetailsList, newList) + fun swap(newList: List) { + val diffUtilCallback = DiffUtilCallback(this.pgpKeyDetailsList, newList) val productDiffResult = DiffUtil.calculateDiff(diffUtilCallback) - nodeKeyDetailsList.clear() - nodeKeyDetailsList.addAll(newList) + pgpKeyDetailsList.clear() + pgpKeyDetailsList.addAll(newList) productDiffResult.dispatchUpdatesTo(this) } interface OnKeySelectedListener { - fun onKeySelected(position: Int, nodeKeyDetails: NodeKeyDetails?) + fun onKeySelected(position: Int, pgpKeyDetails: PgpKeyDetails?) } inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - fun getNodeKeyDetails(): ItemDetailsLookup.ItemDetails? { - return nodeKeyDetailsList.getOrNull(adapterPosition)?.let { + fun getNodeKeyDetails(): ItemDetailsLookup.ItemDetails? { + return pgpKeyDetailsList.getOrNull(adapterPosition)?.let { SelectionNodeKeyDetailsDetails(adapterPosition, it) } } @@ -93,16 +91,16 @@ class PrivateKeysRecyclerViewAdapter(context: Context, } val textViewKeyOwner: TextView = view.findViewById(R.id.textViewKeyOwner) - val textViewLongId: TextView = view.findViewById(R.id.textViewLongId) + val textViewFingerprint: TextView = view.findViewById(R.id.textViewFingerprint) val textViewCreationDate: TextView = view.findViewById(R.id.textViewCreationDate) } - inner class DiffUtilCallback(private val oldList: List, - private val newList: List) : DiffUtil.Callback() { + inner class DiffUtilCallback(private val oldList: List, + private val newList: List) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val old = oldList[oldItemPosition] val new = newList[newItemPosition] - return old.longId == new.longId + return old.fingerprint == new.fingerprint } override fun getOldListSize(): Int = oldList.size diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PubKeysArrayAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PubKeysArrayAdapter.kt index 5dc2f5e7f2..026f41ecb4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PubKeysArrayAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/PubKeysArrayAdapter.kt @@ -40,7 +40,7 @@ class PubKeysArrayAdapter(context: Context, atts: List, choiceMo viewHolder = ViewHolder() view = inflater.inflate(layoutId, parent, false) viewHolder.textViewEmail = view.findViewById(R.id.textViewEmail) - viewHolder.textViewLongId = view.findViewById(R.id.textViewLongId) + viewHolder.textViewFingerprint = view.findViewById(R.id.textViewFingerprint) view.tag = viewHolder } else { viewHolder = view.tag as ViewHolder @@ -53,11 +53,11 @@ class PubKeysArrayAdapter(context: Context, atts: List, choiceMo private fun updateView(att: AttachmentInfo?, viewHolder: ViewHolder) { viewHolder.textViewEmail?.text = att?.email - viewHolder.textViewLongId?.text = att?.name + viewHolder.textViewFingerprint?.text = att?.name } private class ViewHolder { - internal var textViewEmail: TextView? = null - internal var textViewLongId: TextView? = null + var textViewEmail: TextView? = null + var textViewFingerprint: TextView? = null } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/NodeKeyDetailsKeyProvider.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/NodeKeyDetailsKeyProvider.kt index 87b81b2768..bef6e63060 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/NodeKeyDetailsKeyProvider.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/NodeKeyDetailsKeyProvider.kt @@ -6,7 +6,7 @@ package com.flowcrypt.email.ui.adapter.selection import androidx.recyclerview.selection.ItemKeyProvider -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails /** * @author Denis Bondarenko @@ -14,7 +14,7 @@ import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails * Time: 4:56 PM * E-mail: DenBond7@gmail.com */ -class NodeKeyDetailsKeyProvider(private val items: List) : ItemKeyProvider(SCOPE_CACHED) { +class NodeKeyDetailsKeyProvider(private val items: List) : ItemKeyProvider(SCOPE_CACHED) { override fun getKey(position: Int) = items.getOrNull(position) - override fun getPosition(key: NodeKeyDetails) = items.indexOf(key) + override fun getPosition(key: PgpKeyDetails) = items.indexOf(key) } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/PrivateKeyItemDetailsLookup.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/PrivateKeyItemDetailsLookup.kt index d909bd37d1..f533749364 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/PrivateKeyItemDetailsLookup.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/PrivateKeyItemDetailsLookup.kt @@ -8,7 +8,7 @@ package com.flowcrypt.email.ui.adapter.selection import android.view.MotionEvent import androidx.recyclerview.selection.ItemDetailsLookup import androidx.recyclerview.widget.RecyclerView -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails import com.flowcrypt.email.ui.adapter.PrivateKeysRecyclerViewAdapter /** @@ -17,8 +17,8 @@ import com.flowcrypt.email.ui.adapter.PrivateKeysRecyclerViewAdapter * Time: 4:45 PM * E-mail: DenBond7@gmail.com */ -class PrivateKeyItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { - override fun getItemDetails(e: MotionEvent): ItemDetails? { +class PrivateKeyItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { + override fun getItemDetails(e: MotionEvent): ItemDetails? { return recyclerView.findChildViewUnder(e.x, e.y)?.let { (recyclerView.getChildViewHolder(it) as? PrivateKeysRecyclerViewAdapter.ViewHolder)?.getNodeKeyDetails() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/SelectionNodeKeyDetailsDetails.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/SelectionNodeKeyDetailsDetails.kt index d20265ecad..7a117746f2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/SelectionNodeKeyDetailsDetails.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/selection/SelectionNodeKeyDetailsDetails.kt @@ -6,7 +6,7 @@ package com.flowcrypt.email.ui.adapter.selection import androidx.recyclerview.selection.ItemDetailsLookup -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails /** * @author Denis Bondarenko @@ -15,7 +15,7 @@ import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails * E-mail: DenBond7@gmail.com */ class SelectionNodeKeyDetailsDetails(private val position: Int, - private val nodeKeyDetails: NodeKeyDetails?) : ItemDetailsLookup.ItemDetails() { - override fun getSelectionKey() = nodeKeyDetails + private val pgpKeyDetails: PgpKeyDetails?) : ItemDetailsLookup.ItemDetails() { + override fun getSelectionKey() = pgpKeyDetails override fun getPosition() = position } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/notifications/NotificationChannelManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/notifications/NotificationChannelManager.kt index 0d72ab1080..86328e6c60 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/notifications/NotificationChannelManager.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/notifications/NotificationChannelManager.kt @@ -24,6 +24,7 @@ object NotificationChannelManager { const val CHANNEL_ID_SYSTEM = "System" const val CHANNEL_ID_ERRORS = "Errors" const val CHANNEL_ID_SYNC = "Sync" + const val CHANNEL_ID_SILENT = "Silent" /** * Register [NotificationChannel](s) of the app in the system. @@ -38,6 +39,7 @@ object NotificationChannelManager { notificationManager.createNotificationChannel(genMsgsNotificationChannel(context)) notificationManager.createNotificationChannel(genErrorNotificationChannel(context)) notificationManager.createNotificationChannel(genSyncNotificationChannel(context)) + notificationManager.createNotificationChannel(genSilentNotificationChannel(context)) } /** @@ -86,7 +88,7 @@ object NotificationChannelManager { */ private fun genGeneralNotificationChannel(context: Context): NotificationChannel { val name = context.getString(R.string.system) - val description = context.getString(R.string.system_notifications_notification_chanel) + val description = context.getString(R.string.system_notifications_notification_channel) val importance = NotificationManager.IMPORTANCE_DEFAULT val notificationChannel = NotificationChannel(CHANNEL_ID_SYSTEM, name, importance) @@ -105,7 +107,7 @@ object NotificationChannelManager { */ private fun genErrorNotificationChannel(context: Context): NotificationChannel { val name = context.getString(R.string.errors_notifications) - val description = context.getString(R.string.errors_notifications_notification_chanel) + val description = context.getString(R.string.errors_notifications_notification_channel) val importance = NotificationManager.IMPORTANCE_HIGH val notificationChannel = NotificationChannel(CHANNEL_ID_ERRORS, name, importance) @@ -124,7 +126,7 @@ object NotificationChannelManager { */ private fun genSyncNotificationChannel(context: Context): NotificationChannel { val name = context.getString(R.string.sync) - val description = context.getString(R.string.sync_notifications_notification_chanel) + val description = context.getString(R.string.sync_notifications_notification_channel) val importance = NotificationManager.IMPORTANCE_LOW val notificationChannel = NotificationChannel(CHANNEL_ID_SYNC, name, importance) @@ -134,4 +136,21 @@ object NotificationChannelManager { return notificationChannel } + + /** + * Generate silent notification channel. + * + * @param context Interface to global information about an application environment. + * @return Generated [NotificationChannel] + */ + private fun genSilentNotificationChannel(context: Context): NotificationChannel { + val name = context.getString(R.string.silent) + val description = context.getString(R.string.silent_notifications_notification_channel) + val importance = NotificationManager.IMPORTANCE_LOW + + val notificationChannel = NotificationChannel(CHANNEL_ID_SILENT, name, importance) + notificationChannel.description = description + + return notificationChannel + } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/KeyAlreadyAddedException.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/KeyAlreadyAddedException.kt index 4dd1dcc975..8ba9c7405d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/KeyAlreadyAddedException.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/KeyAlreadyAddedException.kt @@ -5,7 +5,7 @@ package com.flowcrypt.email.util.exception -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails /** * This exception means that the key already added. @@ -15,4 +15,4 @@ import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails * Time: 14:09 * E-mail: DenBond7@gmail.com */ -class KeyAlreadyAddedException(val keyDetails: NodeKeyDetails, errorMsg: String) : Exception(errorMsg) +class KeyAlreadyAddedException(val keyDetails: PgpKeyDetails, errorMsg: String) : Exception(errorMsg) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/SavePrivateKeyToDatabaseException.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/SavePrivateKeyToDatabaseException.kt index 135436d3a7..af3b8c6cee 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/SavePrivateKeyToDatabaseException.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/exception/SavePrivateKeyToDatabaseException.kt @@ -5,7 +5,7 @@ package com.flowcrypt.email.util.exception -import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails +import com.flowcrypt.email.security.model.PgpKeyDetails /** * @author Denis Bondarenko @@ -13,4 +13,4 @@ import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails * Time: 4:03 PM * E-mail: DenBond7@gmail.com */ -class SavePrivateKeyToDatabaseException(val keys: List, e: Exception) : Exception(e) \ No newline at end of file +class SavePrivateKeyToDatabaseException(val keys: List, e: Exception) : Exception(e) \ No newline at end of file diff --git a/FlowCrypt/src/main/res/drawable/ic_baseline_password_24dp.xml b/FlowCrypt/src/main/res/drawable/ic_baseline_password_24dp.xml new file mode 100644 index 0000000000..e87b20aec8 --- /dev/null +++ b/FlowCrypt/src/main/res/drawable/ic_baseline_password_24dp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/FlowCrypt/src/main/res/layout/activity_check_keys.xml b/FlowCrypt/src/main/res/layout/activity_check_keys.xml index 26b7e1903c..3b7207c088 100644 --- a/FlowCrypt/src/main/res/layout/activity_check_keys.xml +++ b/FlowCrypt/src/main/res/layout/activity_check_keys.xml @@ -119,6 +119,31 @@ android:inputType="textPassword" /> + + + + + + +