Revert "Store passwords encrypted"

This reverts commit a3063c3da4.
pull/153/head
M66B 6 years ago
parent 285340244e
commit cb78dddb4e

@ -702,11 +702,10 @@ Long version:
<a name="faq37"></a> <a name="faq37"></a>
**(37) How are passwords stored?** **(37) How are passwords stored?**
On Android 6 Marshmallow and later passwords are stored encrypted in an app private database. Providers require passwords in plain text, so the background service that takes care of synchronizing messages needs to send passwords in plain text.
Passwords are encrypted with the cipher AES/GCM/NoPadding 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.
and a generated secret key stored by the [Android keystore system](https://developer.android.com/training/articles/keystore). 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 earlier Android versions passwords are stored in plain text.
<br /> <br />

File diff suppressed because it is too large Load Diff

@ -3,7 +3,6 @@ package eu.faircode.email;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor; import android.database.Cursor;
import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.TextUtils; import android.text.TextUtils;
@ -50,7 +49,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html // https://developer.android.com/topic/libraries/architecture/room.html
@Database( @Database(
version = 53, version = 52,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -561,33 +560,6 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `folder` ADD COLUMN `total` INTEGER"); 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(); .build();
} }

@ -95,20 +95,6 @@ public class EntityAccount implements Serializable {
return "imap" + (starttls ? "" : "s"); 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) { static String getNotificationChannelName(long account) {
return "notification" + (account == 0 ? "" : "." + account); return "notification" + (account == 0 ? "" : "." + account);
} }
@ -138,7 +124,7 @@ public class EntityAccount implements Serializable {
json.put("insecure", insecure); json.put("insecure", insecure);
json.put("port", port); json.put("port", port);
json.put("user", user); json.put("user", user);
json.put("password", getPassword()); json.put("password", password);
json.put("realm", realm); json.put("realm", realm);
json.put("name", name); json.put("name", name);
@ -170,7 +156,7 @@ public class EntityAccount implements Serializable {
account.insecure = (json.has("insecure") && json.getBoolean("insecure")); account.insecure = (json.has("insecure") && json.getBoolean("insecure"));
account.port = json.getInt("port"); account.port = json.getInt("port");
account.user = json.getString("user"); account.user = json.getString("user");
account.setPassword(json.getString("password")); account.password = json.getString("password");
if (json.has("realm")) if (json.has("realm"))
account.realm = json.getString("realm"); account.realm = json.getString("realm");
@ -208,7 +194,7 @@ public class EntityAccount implements Serializable {
this.insecure == other.insecure && this.insecure == other.insecure &&
this.port.equals(other.port) && this.port.equals(other.port) &&
this.user.equals(other.user) && this.user.equals(other.user) &&
this.getPassword().equals(other.getPassword()) && this.password.equals(other.password) &&
Objects.equals(this.realm, other.realm) && Objects.equals(this.realm, other.realm) &&
Objects.equals(this.name, other.name) && Objects.equals(this.name, other.name) &&
Objects.equals(this.color, other.color) && Objects.equals(this.color, other.color) &&

@ -19,8 +19,6 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B) Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/ */
import android.os.Build;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -101,20 +99,6 @@ public class EntityIdentity {
return (starttls ? "smtp" : "smtps"); 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 { public JSONObject toJSON() throws JSONException {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("id", id); json.put("id", id);
@ -132,7 +116,7 @@ public class EntityIdentity {
json.put("insecure", insecure); json.put("insecure", insecure);
json.put("port", port); json.put("port", port);
json.put("user", user); json.put("user", user);
json.put("password", getPassword()); json.put("password", password);
json.put("realm", realm); json.put("realm", realm);
json.put("use_ip", use_ip); json.put("use_ip", use_ip);
@ -170,7 +154,7 @@ public class EntityIdentity {
identity.insecure = (json.has("insecure") && json.getBoolean("insecure")); identity.insecure = (json.has("insecure") && json.getBoolean("insecure"));
identity.port = json.getInt("port"); identity.port = json.getInt("port");
identity.user = json.getString("user"); identity.user = json.getString("user");
identity.setPassword(json.getString("password")); identity.password = json.getString("password");
if (json.has("realm")) if (json.has("realm"))
identity.realm = json.getString("realm"); identity.realm = json.getString("realm");
if (json.has("use_ip")) if (json.has("use_ip"))
@ -215,7 +199,7 @@ public class EntityIdentity {
this.insecure.equals(other.insecure) && this.insecure.equals(other.insecure) &&
this.port.equals(other.port) && this.port.equals(other.port) &&
this.user.equals(other.user) && this.user.equals(other.user) &&
this.getPassword().equals(other.getPassword()) && this.password.equals(other.password) &&
Objects.equals(this.realm, other.realm) && Objects.equals(this.realm, other.realm) &&
this.use_ip == other.use_ip && this.use_ip == other.use_ip &&
this.synchronize.equals(other.synchronize) && this.synchronize.equals(other.synchronize) &&

@ -845,7 +845,7 @@ public class FragmentAccount extends FragmentBase {
boolean check = (synchronize && (account == null || boolean check = (synchronize && (account == null ||
auth_type != account.auth_type || auth_type != account.auth_type ||
!host.equals(account.host) || Integer.parseInt(port) != account.port || !host.equals(account.host) || Integer.parseInt(port) != account.port ||
!user.equals(account.user) || !password.equals(account.getPassword()) || !user.equals(account.user) || !password.equals(account.password) ||
!Objects.equals(realm, accountRealm))); !Objects.equals(realm, accountRealm)));
boolean reload = (check || account == null || boolean reload = (check || account == null ||
!Objects.equals(account.prefix, prefix) || !Objects.equals(account.prefix, prefix) ||
@ -914,7 +914,7 @@ public class FragmentAccount extends FragmentBase {
account.insecure = insecure; account.insecure = insecure;
account.port = Integer.parseInt(port); account.port = Integer.parseInt(port);
account.user = user; account.user = user;
account.setPassword(password); account.password = password;
account.realm = realm; account.realm = realm;
account.name = name; 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.setTag(account == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user);
etUser.setText(account == null ? null : account.user); etUser.setText(account == null ? null : account.user);
tilPassword.getEditText().setText(account == null ? null : account.getPassword()); tilPassword.getEditText().setText(account == null ? null : account.password);
etRealm.setText(account == null ? null : account.realm); etRealm.setText(account == null ? null : account.realm);
etName.setText(account == null ? null : account.name); etName.setText(account == null ? null : account.name);

@ -237,7 +237,7 @@ public class FragmentIdentity extends FragmentBase {
etEmail.setText(account.user); etEmail.setText(account.user);
etUser.setTag(auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user); etUser.setTag(auth_type == Helper.AUTH_TYPE_PASSWORD ? null : account.user);
etUser.setText(account.user); etUser.setText(account.user);
tilPassword.getEditText().setText(account.getPassword()); tilPassword.getEditText().setText(account.password);
etRealm.setText(account.realm); etRealm.setText(account.realm);
tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD); tilPassword.setEnabled(auth_type == Helper.AUTH_TYPE_PASSWORD);
etRealm.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 || boolean check = (synchronize && (identity == null ||
auth_type != identity.auth_type || auth_type != identity.auth_type ||
!host.equals(identity.host) || Integer.parseInt(port) != identity.port || !host.equals(identity.host) || Integer.parseInt(port) != identity.port ||
!user.equals(identity.user) || !password.equals(identity.getPassword()) || !user.equals(identity.user) || !password.equals(identity.password) ||
!Objects.equals(realm, identityRealm) || !Objects.equals(realm, identityRealm) ||
use_ip != identity.use_ip)); use_ip != identity.use_ip));
boolean reload = (identity == null || identity.synchronize != synchronize || check); boolean reload = (identity == null || identity.synchronize != synchronize || check);
@ -655,7 +655,7 @@ public class FragmentIdentity extends FragmentBase {
identity.insecure = insecure; identity.insecure = insecure;
identity.port = Integer.parseInt(port); identity.port = Integer.parseInt(port);
identity.user = user; identity.user = user;
identity.setPassword(password); identity.password = password;
identity.realm = realm; identity.realm = realm;
identity.use_ip = use_ip; identity.use_ip = use_ip;
identity.synchronize = synchronize; identity.synchronize = synchronize;
@ -759,7 +759,7 @@ public class FragmentIdentity extends FragmentBase {
etPort.setText(identity == null ? null : Long.toString(identity.port)); etPort.setText(identity == null ? null : Long.toString(identity.port));
etUser.setTag(identity == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : identity.user); etUser.setTag(identity == null || auth_type == Helper.AUTH_TYPE_PASSWORD ? null : identity.user);
etUser.setText(identity == null ? null : identity.user); etUser.setText(identity == null ? null : identity.user);
tilPassword.getEditText().setText(identity == null ? null : identity.getPassword()); tilPassword.getEditText().setText(identity == null ? null : identity.password);
etRealm.setText(identity == null ? null : identity.realm); etRealm.setText(identity == null ? null : identity.realm);
cbUseIp.setChecked(identity == null ? true : identity.use_ip); cbUseIp.setChecked(identity == null ? true : identity.use_ip);
cbSynchronize.setChecked(identity == null ? true : identity.synchronize); cbSynchronize.setChecked(identity == null ? true : identity.synchronize);
@ -864,7 +864,7 @@ public class FragmentIdentity extends FragmentBase {
spAccount.setSelection(pos); spAccount.setSelection(pos);
// OAuth token could be updated // OAuth token could be updated
if (pos > 0 && accounts.get(pos).auth_type != Helper.AUTH_TYPE_PASSWORD) if (pos > 0 && accounts.get(pos).auth_type != Helper.AUTH_TYPE_PASSWORD)
tilPassword.getEditText().setText(accounts.get(pos).getPassword()); tilPassword.getEditText().setText(accounts.get(pos).password);
break; break;
} }
} }

@ -335,7 +335,7 @@ public class FragmentQuickSetup extends FragmentBase {
account.insecure = false; account.insecure = false;
account.port = provider.imap_port; account.port = provider.imap_port;
account.user = user; account.user = user;
account.setPassword(password); account.password = password;
account.name = provider.name; account.name = provider.name;
account.color = null; account.color = null;
@ -389,7 +389,7 @@ public class FragmentQuickSetup extends FragmentBase {
identity.insecure = false; identity.insecure = false;
identity.port = provider.smtp_port; identity.port = provider.smtp_port;
identity.user = user; identity.user = user;
identity.setPassword(password); identity.password = password;
identity.synchronize = true; identity.synchronize = true;
identity.primary = true; identity.primary = true;

@ -42,10 +42,7 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.PowerManager; import android.os.PowerManager;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Base64;
import android.view.Menu; import android.view.Menu;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -80,8 +77,6 @@ import java.io.UnsupportedEncodingException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URL; import java.net.URL;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.DateFormat; import java.text.DateFormat;
@ -96,10 +91,6 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ThreadFactory; 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.Address;
import javax.mail.AuthenticationFailedException; import javax.mail.AuthenticationFailedException;
import javax.mail.FolderClosedException; import javax.mail.FolderClosedException;
@ -109,7 +100,6 @@ import javax.mail.internet.InternetAddress;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
@ -819,12 +809,12 @@ public class Helper {
static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException { static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException {
try { try {
istore.connect(account.host, account.port, account.user, account.getPassword()); istore.connect(account.host, account.port, account.user, account.password);
} catch (AuthenticationFailedException ex) { } catch (AuthenticationFailedException ex) {
if (account.auth_type == AUTH_TYPE_GMAIL) { if (account.auth_type == AUTH_TYPE_GMAIL) {
account.setPassword(refreshToken(context, "com.google", account.user, account.getPassword())); account.password = refreshToken(context, "com.google", account.user, account.password);
DB.getInstance(context).account().setAccountPassword(account.id, account.password); DB.getInstance(context).account().setAccountPassword(account.id, account.password);
istore.connect(account.host, account.port, account.user, account.getPassword()); istore.connect(account.host, account.port, account.user, account.password);
} else } else
throw ex; throw ex;
} }
@ -1053,56 +1043,4 @@ public class Helper {
return organization; 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;
}
}
} }

@ -294,13 +294,13 @@ public class ServiceSend extends LifecycleService {
// Connect transport // Connect transport
db.identity().setIdentityState(ident.id, "connecting"); db.identity().setIdentityState(ident.id, "connecting");
try { try {
itransport.connect(ident.host, ident.port, ident.user, ident.getPassword()); itransport.connect(ident.host, ident.port, ident.user, ident.password);
} catch (AuthenticationFailedException ex) { } catch (AuthenticationFailedException ex) {
if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) { if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) {
EntityAccount account = db.account().getAccount(ident.account); EntityAccount account = db.account().getAccount(ident.account);
ident.setPassword(Helper.refreshToken(this, "com.google", ident.user, account.getPassword())); ident.password = Helper.refreshToken(this, "com.google", ident.user, account.password);
DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password); DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password);
itransport.connect(ident.host, ident.port, ident.user, ident.getPassword()); itransport.connect(ident.host, ident.port, ident.user, ident.password);
} else } else
throw ex; throw ex;
} }

Loading…
Cancel
Save