From a3063c3da44808c5872075614ef5875fae36a15a Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 11 Mar 2019 14:01:27 +0000 Subject: [PATCH] Store passwords encrypted --- FAQ.md | 9 +- app/schemas/eu.faircode.email.DB/53.json | 1540 +++++++++++++++++ app/src/main/java/eu/faircode/email/DB.java | 30 +- .../java/eu/faircode/email/EntityAccount.java | 20 +- .../eu/faircode/email/EntityIdentity.java | 22 +- .../eu/faircode/email/FragmentAccount.java | 6 +- .../eu/faircode/email/FragmentIdentity.java | 10 +- .../eu/faircode/email/FragmentQuickSetup.java | 4 +- .../main/java/eu/faircode/email/Helper.java | 68 +- .../java/eu/faircode/email/ServiceSend.java | 6 +- 10 files changed, 1688 insertions(+), 27 deletions(-) create mode 100644 app/schemas/eu.faircode.email.DB/53.json diff --git a/FAQ.md b/FAQ.md index 7bb4d16374..7352babe6e 100644 --- a/FAQ.md +++ b/FAQ.md @@ -702,10 +702,11 @@ Long version: **(37) How are passwords stored?** -Providers require passwords in plain text, so the background service that takes care of synchronizing messages needs to send passwords in plain text. -Since encrypting passwords would require a secret and the background service needs to know this secret, this could only be done by storing that secret. -Storing a secret together with encrypted passwords would not add anything, so passwords are stored in plain text in a safe, inaccessible place. -Recent Android versions encrypt all user data anyway. +On Android 6 Marshmallow and later passwords are stored encrypted in an app private database. +Passwords are encrypted with the cipher AES/GCM/NoPadding +and a generated secret key stored by the [Android keystore system](https://developer.android.com/training/articles/keystore). + +On earlier Android versions passwords are stored in plain text.
diff --git a/app/schemas/eu.faircode.email.DB/53.json b/app/schemas/eu.faircode.email.DB/53.json new file mode 100644 index 0000000000..00e8318005 --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/53.json @@ -0,0 +1,1540 @@ +{ + "formatVersion": 1, + "database": { + "version": 53, + "identityHash": "cfa3a48b82bba1d9a9f736677e28d1dc", + "entities": [ + { + "tableName": "identity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `account` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `signature` TEXT, `auth_type` INTEGER NOT NULL, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `realm` TEXT, `use_ip` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `replyto` TEXT, `bcc` TEXT, `plain_only` INTEGER NOT NULL, `encrypt` INTEGER NOT NULL, `delivery_receipt` INTEGER NOT NULL, `read_receipt` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `sent_folder` INTEGER, `tbd` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "use_ip", + "columnName": "use_ip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyto", + "columnName": "replyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delivery_receipt", + "columnName": "delivery_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read_receipt", + "columnName": "read_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent_folder", + "columnName": "sent_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_identity_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX `index_identity_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_identity_account_email", + "unique": false, + "columnNames": [ + "account", + "email" + ], + "createSql": "CREATE INDEX `index_identity_account_email` ON `${TABLE_NAME}` (`account`, `email`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `auth_type` INTEGER NOT NULL, `pop` INTEGER NOT NULL, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `realm` TEXT, `name` TEXT, `signature` TEXT, `color` INTEGER, `synchronize` INTEGER NOT NULL, `ondemand` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `browse` INTEGER NOT NULL, `swipe_left` INTEGER, `swipe_right` INTEGER, `poll_interval` INTEGER NOT NULL, `prefix` TEXT, `created` INTEGER, `tbd` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pop", + "columnName": "pop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ondemand", + "columnName": "ondemand", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browse", + "columnName": "browse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "swipe_left", + "columnName": "swipe_left", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_right", + "columnName": "swipe_right", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `level` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `poll` INTEGER NOT NULL, `download` INTEGER NOT NULL, `sync_days` INTEGER NOT NULL, `keep_days` INTEGER NOT NULL, `display` TEXT, `hide` INTEGER NOT NULL, `unified` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `total` INTEGER, `keywords` TEXT, `initialize` INTEGER NOT NULL, `tbc` INTEGER, `tbd` INTEGER, `state` TEXT, `sync_state` TEXT, `error` TEXT, `last_sync` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "download", + "columnName": "download", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sync_days", + "columnName": "sync_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_days", + "columnName": "keep_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unified", + "columnName": "unified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "initialize", + "columnName": "initialize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tbc", + "columnName": "tbc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync_state", + "columnName": "sync_state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_sync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_folder_account_name", + "unique": true, + "columnNames": [ + "account", + "name" + ], + "createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)" + }, + { + "name": "index_folder_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_folder_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_folder_type", + "unique": false, + "columnNames": [ + "type" + ], + "createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_folder_unified", + "unique": false, + "columnNames": [ + "unified" + ], + "createSql": "CREATE INDEX `index_folder_unified` ON `${TABLE_NAME}` (`unified`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `folder` INTEGER NOT NULL, `identity` INTEGER, `extra` TEXT, `replying` INTEGER, `forwarding` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `deliveredto` TEXT, `inreplyto` TEXT, `thread` TEXT, `avatar` TEXT, `sender` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `headers` TEXT, `raw` INTEGER, `subject` TEXT, `size` INTEGER, `content` INTEGER NOT NULL, `preview` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `answered` INTEGER NOT NULL, `flagged` INTEGER NOT NULL, `flags` TEXT, `keywords` TEXT, `ui_seen` INTEGER NOT NULL, `ui_answered` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `ui_ignored` INTEGER NOT NULL, `ui_browsed` INTEGER NOT NULL, `ui_snoozed` INTEGER, `warning` TEXT, `error` TEXT, `last_attempt` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`forwarding`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identity", + "columnName": "identity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extra", + "columnName": "extra", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replying", + "columnName": "replying", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forwarding", + "columnName": "forwarding", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "msgid", + "columnName": "msgid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "references", + "columnName": "references", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deliveredto", + "columnName": "deliveredto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inreplyto", + "columnName": "inreplyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cc", + "columnName": "cc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "headers", + "columnName": "headers", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "preview", + "columnName": "preview", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sent", + "columnName": "sent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stored", + "columnName": "stored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "answered", + "columnName": "answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flagged", + "columnName": "flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ui_seen", + "columnName": "ui_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_answered", + "columnName": "ui_answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_flagged", + "columnName": "ui_flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_hide", + "columnName": "ui_hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_found", + "columnName": "ui_found", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_ignored", + "columnName": "ui_ignored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_browsed", + "columnName": "ui_browsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_snoozed", + "columnName": "ui_snoozed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_attempt", + "columnName": "last_attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_message_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_message_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_message_identity", + "unique": false, + "columnNames": [ + "identity" + ], + "createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)" + }, + { + "name": "index_message_folder_uid", + "unique": true, + "columnNames": [ + "folder", + "uid" + ], + "createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)" + }, + { + "name": "index_message_msgid_folder", + "unique": true, + "columnNames": [ + "msgid", + "folder" + ], + "createSql": "CREATE UNIQUE INDEX `index_message_msgid_folder` ON `${TABLE_NAME}` (`msgid`, `folder`)" + }, + { + "name": "index_message_thread", + "unique": false, + "columnNames": [ + "thread" + ], + "createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)" + }, + { + "name": "index_message_sender", + "unique": false, + "columnNames": [ + "sender" + ], + "createSql": "CREATE INDEX `index_message_sender` ON `${TABLE_NAME}` (`sender`)" + }, + { + "name": "index_message_received", + "unique": false, + "columnNames": [ + "received" + ], + "createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)" + }, + { + "name": "index_message_subject", + "unique": false, + "columnNames": [ + "subject" + ], + "createSql": "CREATE INDEX `index_message_subject` ON `${TABLE_NAME}` (`subject`)" + }, + { + "name": "index_message_ui_seen", + "unique": false, + "columnNames": [ + "ui_seen" + ], + "createSql": "CREATE INDEX `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)" + }, + { + "name": "index_message_ui_flagged", + "unique": false, + "columnNames": [ + "ui_flagged" + ], + "createSql": "CREATE INDEX `index_message_ui_flagged` ON `${TABLE_NAME}` (`ui_flagged`)" + }, + { + "name": "index_message_ui_hide", + "unique": false, + "columnNames": [ + "ui_hide" + ], + "createSql": "CREATE INDEX `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)" + }, + { + "name": "index_message_ui_found", + "unique": false, + "columnNames": [ + "ui_found" + ], + "createSql": "CREATE INDEX `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)" + }, + { + "name": "index_message_ui_ignored", + "unique": false, + "columnNames": [ + "ui_ignored" + ], + "createSql": "CREATE INDEX `index_message_ui_ignored` ON `${TABLE_NAME}` (`ui_ignored`)" + }, + { + "name": "index_message_ui_browsed", + "unique": false, + "columnNames": [ + "ui_browsed" + ], + "createSql": "CREATE INDEX `index_message_ui_browsed` ON `${TABLE_NAME}` (`ui_browsed`)" + }, + { + "name": "index_message_ui_snoozed", + "unique": false, + "columnNames": [ + "ui_snoozed" + ], + "createSql": "CREATE INDEX `index_message_ui_snoozed` ON `${TABLE_NAME}` (`ui_snoozed`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "identity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "identity" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "replying" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "forwarding" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "attachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `disposition` TEXT, `cid` TEXT, `encryption` INTEGER, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sequence", + "columnName": "sequence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disposition", + "columnName": "disposition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cid", + "columnName": "cid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryption", + "columnName": "encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_attachment_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_attachment_message_sequence", + "unique": true, + "columnNames": [ + "message", + "sequence" + ], + "createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)" + }, + { + "name": "index_attachment_message_cid", + "unique": true, + "columnNames": [ + "message", + "cid" + ], + "createSql": "CREATE UNIQUE INDEX `index_attachment_message_cid` ON `${TABLE_NAME}` (`message`, `cid`)" + } + ], + "foreignKeys": [ + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "operation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "args", + "columnName": "args", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_operation_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX `index_operation_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_operation_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_operation_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX `index_operation_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` INTEGER NOT NULL, `email` TEXT NOT NULL, `name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_contact_email_type", + "unique": true, + "columnNames": [ + "email", + "type" + ], + "createSql": "CREATE UNIQUE INDEX `index_contact_email_type` ON `${TABLE_NAME}` (`email`, `type`)" + }, + { + "name": "index_contact_name_type", + "unique": false, + "columnNames": [ + "name", + "type" + ], + "createSql": "CREATE INDEX `index_contact_name_type` ON `${TABLE_NAME}` (`name`, `type`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `stop` INTEGER NOT NULL, `condition` TEXT NOT NULL, `action` TEXT NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stop", + "columnName": "stop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_rule_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX `index_rule_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_rule_order", + "unique": false, + "columnNames": [ + "order" + ], + "createSql": "CREATE INDEX `index_rule_order` ON `${TABLE_NAME}` (`order`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_log_time", + "unique": false, + "columnNames": [ + "time" + ], + "createSql": "CREATE INDEX `index_log_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + } + ], + "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, \"cfa3a48b82bba1d9a9f736677e28d1dc\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/DB.java b/app/src/main/java/eu/faircode/email/DB.java index a607e8d79e..6c6bc242c2 100644 --- a/app/src/main/java/eu/faircode/email/DB.java +++ b/app/src/main/java/eu/faircode/email/DB.java @@ -3,6 +3,7 @@ package eu.faircode.email; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; +import android.os.Build; import android.preference.PreferenceManager; import android.text.TextUtils; @@ -49,7 +50,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory; // https://developer.android.com/topic/libraries/architecture/room.html @Database( - version = 52, + version = 53, entities = { EntityIdentity.class, EntityAccount.class, @@ -560,6 +561,33 @@ public abstract class DB extends RoomDatabase { db.execSQL("ALTER TABLE `folder` ADD COLUMN `total` INTEGER"); } }) + .addMigrations(new Migration(52, 53) { + @Override + public void migrate(SupportSQLiteDatabase db) { + Log.i("DB migration from version " + startVersion + " to " + endVersion); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Cursor cursor = db.query("SELECT id, password FROM account"); + while (cursor.moveToNext()) { + long id = cursor.getLong(0); + String plain = cursor.getString(1); + db.execSQL("UPDATE account SET password = ? WHERE id = ?", + new Object[]{id, Helper.encryptPassword(plain)}); + } + cursor.close(); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Cursor cursor = db.query("SELECT id, password FROM identity"); + while (cursor.moveToNext()) { + long id = cursor.getLong(0); + String plain = cursor.getString(1); + db.execSQL("UPDATE identity SET password = ? WHERE id = ?", + new Object[]{id, Helper.encryptPassword(plain)}); + } + cursor.close(); + } + } + }) .build(); } diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index 90600fb921..471a54f5e8 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -95,6 +95,20 @@ public class EntityAccount implements Serializable { return "imap" + (starttls ? "" : "s"); } + String getPassword() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return this.password; + else + return Helper.decryptPassword(this.password); + } + + void setPassword(String plain) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + this.password = plain; + else + this.password = Helper.encryptPassword(plain); + } + static String getNotificationChannelName(long account) { return "notification" + (account == 0 ? "" : "." + account); } @@ -124,7 +138,7 @@ public class EntityAccount implements Serializable { json.put("insecure", insecure); json.put("port", port); json.put("user", user); - json.put("password", password); + json.put("password", getPassword()); json.put("realm", realm); json.put("name", name); @@ -156,7 +170,7 @@ public class EntityAccount implements Serializable { account.insecure = (json.has("insecure") && json.getBoolean("insecure")); account.port = json.getInt("port"); account.user = json.getString("user"); - account.password = json.getString("password"); + account.setPassword(json.getString("password")); if (json.has("realm")) account.realm = json.getString("realm"); @@ -194,7 +208,7 @@ public class EntityAccount implements Serializable { this.insecure == other.insecure && this.port.equals(other.port) && this.user.equals(other.user) && - this.password.equals(other.password) && + this.getPassword().equals(other.getPassword()) && Objects.equals(this.realm, other.realm) && Objects.equals(this.name, other.name) && Objects.equals(this.color, other.color) && diff --git a/app/src/main/java/eu/faircode/email/EntityIdentity.java b/app/src/main/java/eu/faircode/email/EntityIdentity.java index 9d41d4eb41..416c983bae 100644 --- a/app/src/main/java/eu/faircode/email/EntityIdentity.java +++ b/app/src/main/java/eu/faircode/email/EntityIdentity.java @@ -19,6 +19,8 @@ package eu.faircode.email; Copyright 2018-2019 by Marcel Bokhorst (M66B) */ +import android.os.Build; + import org.json.JSONException; import org.json.JSONObject; @@ -99,6 +101,20 @@ public class EntityIdentity { return (starttls ? "smtp" : "smtps"); } + String getPassword() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return this.password; + else + return Helper.decryptPassword(this.password); + } + + void setPassword(String plain) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + this.password = plain; + else + this.password = Helper.encryptPassword(plain); + } + public JSONObject toJSON() throws JSONException { JSONObject json = new JSONObject(); json.put("id", id); @@ -116,7 +132,7 @@ public class EntityIdentity { json.put("insecure", insecure); json.put("port", port); json.put("user", user); - json.put("password", password); + json.put("password", getPassword()); json.put("realm", realm); json.put("use_ip", use_ip); @@ -154,7 +170,7 @@ public class EntityIdentity { identity.insecure = (json.has("insecure") && json.getBoolean("insecure")); identity.port = json.getInt("port"); identity.user = json.getString("user"); - identity.password = json.getString("password"); + identity.setPassword(json.getString("password")); if (json.has("realm")) identity.realm = json.getString("realm"); if (json.has("use_ip")) @@ -199,7 +215,7 @@ public class EntityIdentity { this.insecure.equals(other.insecure) && this.port.equals(other.port) && this.user.equals(other.user) && - this.password.equals(other.password) && + this.getPassword().equals(other.getPassword()) && Objects.equals(this.realm, other.realm) && this.use_ip == other.use_ip && this.synchronize.equals(other.synchronize) && diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index 44b9f1127b..cb623e0161 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -845,7 +845,7 @@ public class FragmentAccount extends FragmentBase { boolean check = (synchronize && (account == null || auth_type != account.auth_type || !host.equals(account.host) || Integer.parseInt(port) != account.port || - !user.equals(account.user) || !password.equals(account.password) || + !user.equals(account.user) || !password.equals(account.getPassword()) || !Objects.equals(realm, accountRealm))); boolean reload = (check || account == null || !Objects.equals(account.prefix, prefix) || @@ -914,7 +914,7 @@ public class FragmentAccount extends FragmentBase { account.insecure = insecure; account.port = Integer.parseInt(port); account.user = user; - account.password = password; + account.setPassword(password); account.realm = realm; account.name = name; @@ -1135,7 +1135,7 @@ public class FragmentAccount extends FragmentBase { etUser.setTag(account == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user); etUser.setText(account == null ? null : account.user); - tilPassword.getEditText().setText(account == null ? null : account.password); + tilPassword.getEditText().setText(account == null ? null : account.getPassword()); etRealm.setText(account == null ? null : account.realm); etName.setText(account == null ? null : account.name); diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index d5878436a2..0b032f964a 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -237,7 +237,7 @@ public class FragmentIdentity extends FragmentBase { etEmail.setText(account.user); etUser.setTag(auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user); etUser.setText(account.user); - tilPassword.getEditText().setText(account.password); + tilPassword.getEditText().setText(account.getPassword()); etRealm.setText(account.realm); tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD); etRealm.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD); @@ -589,7 +589,7 @@ public class FragmentIdentity extends FragmentBase { boolean check = (synchronize && (identity == null || auth_type != identity.auth_type || !host.equals(identity.host) || Integer.parseInt(port) != identity.port || - !user.equals(identity.user) || !password.equals(identity.password) || + !user.equals(identity.user) || !password.equals(identity.getPassword()) || !Objects.equals(realm, identityRealm) || use_ip != identity.use_ip)); boolean reload = (identity == null || identity.synchronize != synchronize || check); @@ -655,7 +655,7 @@ public class FragmentIdentity extends FragmentBase { identity.insecure = insecure; identity.port = Integer.parseInt(port); identity.user = user; - identity.password = password; + identity.setPassword(password); identity.realm = realm; identity.use_ip = use_ip; identity.synchronize = synchronize; @@ -759,7 +759,7 @@ public class FragmentIdentity extends FragmentBase { etPort.setText(identity == null ? null : Long.toString(identity.port)); etUser.setTag(identity == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : identity.user); etUser.setText(identity == null ? null : identity.user); - tilPassword.getEditText().setText(identity == null ? null : identity.password); + tilPassword.getEditText().setText(identity == null ? null : identity.getPassword()); etRealm.setText(identity == null ? null : identity.realm); cbUseIp.setChecked(identity == null ? true : identity.use_ip); cbSynchronize.setChecked(identity == null ? true : identity.synchronize); @@ -864,7 +864,7 @@ public class FragmentIdentity extends FragmentBase { spAccount.setSelection(pos); // OAuth token could be updated if (pos > 0 && accounts.get(pos).auth_type != Helper.AUTH_TYPE_PASSWORD) - tilPassword.getEditText().setText(accounts.get(pos).password); + tilPassword.getEditText().setText(accounts.get(pos).getPassword()); break; } } diff --git a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java index 5cff1e03b2..b55355c4ef 100644 --- a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java @@ -335,7 +335,7 @@ public class FragmentQuickSetup extends FragmentBase { account.insecure = false; account.port = provider.imap_port; account.user = user; - account.password = password; + account.setPassword(password); account.name = provider.name; account.color = null; @@ -389,7 +389,7 @@ public class FragmentQuickSetup extends FragmentBase { identity.insecure = false; identity.port = provider.smtp_port; identity.user = user; - identity.password = password; + identity.setPassword(password); identity.synchronize = true; identity.primary = true; diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index c07e4573f8..de650a1736 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -42,7 +42,10 @@ import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.preference.PreferenceManager; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; import android.text.TextUtils; +import android.util.Base64; import android.view.Menu; import android.view.View; import android.view.ViewGroup; @@ -77,6 +80,8 @@ import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.URL; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; @@ -91,6 +96,10 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.ThreadFactory; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; import javax.mail.Address; import javax.mail.AuthenticationFailedException; import javax.mail.FolderClosedException; @@ -100,6 +109,7 @@ import javax.mail.internet.InternetAddress; import javax.net.ssl.HttpsURLConnection; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.content.ContextCompat; import androidx.lifecycle.Lifecycle; @@ -809,12 +819,12 @@ public class Helper { static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException { try { - istore.connect(account.host, account.port, account.user, account.password); + istore.connect(account.host, account.port, account.user, account.getPassword()); } catch (AuthenticationFailedException ex) { if (account.auth_type == AUTH_TYPE_GMAIL) { - account.password = refreshToken(context, "com.google", account.user, account.password); + account.setPassword(refreshToken(context, "com.google", account.user, account.getPassword())); DB.getInstance(context).account().setAccountPassword(account.id, account.password); - istore.connect(account.host, account.port, account.user, account.password); + istore.connect(account.host, account.port, account.user, account.getPassword()); } else throw ex; } @@ -1043,4 +1053,56 @@ public class Helper { return organization; } } + + @RequiresApi(api = Build.VERSION_CODES.M) + private static SecretKey getSecretKey() throws Throwable { + final String alias = BuildConfig.APPLICATION_ID + ".key"; + + KeyStore store = KeyStore.getInstance("AndroidKeyStore"); + store.load(null); + + KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) store.getEntry(alias, null); + if (entry != null) + return entry.getSecretKey(); + + KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build(); + generator.init(spec); + return generator.generateKey(); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + static String decryptPassword(String secret) { + try { + int slash = secret.indexOf('/'); + byte[] iv = Base64.decode(secret.substring(0, slash), Base64.URL_SAFE); + byte[] encrypted = Base64.decode(secret.substring(slash + 1), Base64.URL_SAFE); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec spec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec); + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + } catch (Throwable ex) { + Log.e(ex); + return secret; + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + static String encryptPassword(String plain) { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()); + byte[] iv = cipher.getIV(); + byte[] encrypted = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8)); + return Base64.encodeToString(iv, Base64.URL_SAFE) + "/" + Base64.encodeToString(encrypted, Base64.URL_SAFE); + } catch (Throwable ex) { + Log.e(ex); + return plain; + } + } } diff --git a/app/src/main/java/eu/faircode/email/ServiceSend.java b/app/src/main/java/eu/faircode/email/ServiceSend.java index f59536e927..dcbb13a500 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSend.java +++ b/app/src/main/java/eu/faircode/email/ServiceSend.java @@ -294,13 +294,13 @@ public class ServiceSend extends LifecycleService { // Connect transport db.identity().setIdentityState(ident.id, "connecting"); try { - itransport.connect(ident.host, ident.port, ident.user, ident.password); + itransport.connect(ident.host, ident.port, ident.user, ident.getPassword()); } catch (AuthenticationFailedException ex) { if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) { EntityAccount account = db.account().getAccount(ident.account); - ident.password = Helper.refreshToken(this, "com.google", ident.user, account.password); + ident.setPassword(Helper.refreshToken(this, "com.google", ident.user, account.getPassword())); DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password); - itransport.connect(ident.host, ident.port, ident.user, ident.password); + itransport.connect(ident.host, ident.port, ident.user, ident.getPassword()); } else throw ex; }