Store passwords encrypted

pull/153/head
M66B 6 years ago
parent de57f2ede1
commit a3063c3da4

@ -702,10 +702,11 @@ Long version:
<a name="faq37"></a> <a name="faq37"></a>
**(37) How are passwords stored?** **(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. On Android 6 Marshmallow and later passwords are stored encrypted in an app private database.
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. Passwords are encrypted with the cipher AES/GCM/NoPadding
Storing a secret together with encrypted passwords would not add anything, so passwords are stored in plain text in a safe, inaccessible place. and a generated secret key stored by the [Android keystore system](https://developer.android.com/training/articles/keystore).
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,6 +3,7 @@ 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;
@ -49,7 +50,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 = 52, version = 53,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -560,6 +561,33 @@ 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,6 +95,20 @@ 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);
} }
@ -124,7 +138,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", password); json.put("password", getPassword());
json.put("realm", realm); json.put("realm", realm);
json.put("name", name); json.put("name", name);
@ -156,7 +170,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.password = json.getString("password"); account.setPassword(json.getString("password"));
if (json.has("realm")) if (json.has("realm"))
account.realm = json.getString("realm"); account.realm = json.getString("realm");
@ -194,7 +208,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.password.equals(other.password) && this.getPassword().equals(other.getPassword()) &&
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,6 +19,8 @@ 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;
@ -99,6 +101,20 @@ 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);
@ -116,7 +132,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", password); json.put("password", getPassword());
json.put("realm", realm); json.put("realm", realm);
json.put("use_ip", use_ip); json.put("use_ip", use_ip);
@ -154,7 +170,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.password = json.getString("password"); identity.setPassword(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"))
@ -199,7 +215,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.password.equals(other.password) && this.getPassword().equals(other.getPassword()) &&
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.password) || !user.equals(account.user) || !password.equals(account.getPassword()) ||
!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.password = password; account.setPassword(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.password); tilPassword.getEditText().setText(account == null ? null : account.getPassword());
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.password); tilPassword.getEditText().setText(account.getPassword());
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.password) || !user.equals(identity.user) || !password.equals(identity.getPassword()) ||
!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.password = password; identity.setPassword(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.password); tilPassword.getEditText().setText(identity == null ? null : identity.getPassword());
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).password); tilPassword.getEditText().setText(accounts.get(pos).getPassword());
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.password = password; account.setPassword(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.password = password; identity.setPassword(password);
identity.synchronize = true; identity.synchronize = true;
identity.primary = true; identity.primary = true;

@ -42,7 +42,10 @@ 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;
@ -77,6 +80,8 @@ 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;
@ -91,6 +96,10 @@ 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;
@ -100,6 +109,7 @@ 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;
@ -809,12 +819,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.password); istore.connect(account.host, account.port, account.user, account.getPassword());
} catch (AuthenticationFailedException ex) { } catch (AuthenticationFailedException ex) {
if (account.auth_type == AUTH_TYPE_GMAIL) { 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); 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 } else
throw ex; throw ex;
} }
@ -1043,4 +1053,56 @@ 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.password); itransport.connect(ident.host, ident.port, ident.user, ident.getPassword());
} 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.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); 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 } else
throw ex; throw ex;
} }

Loading…
Cancel
Save