Skip to content

Commit a38431a

Browse files
author
ostrya
committed
store password securely
Use encrypted preferences for storing password starting with Android 6+. Lower versions sadly do not offer the necessary Keystore API, so for now the password stays unencrypted for these. Also extract each preference into its own class for better separation and rename preferences where necessary to make their content more clear.
1 parent 0a39a3d commit a38431a

40 files changed

+705
-364
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ afterEvaluate {
6969
dependencies {
7070
implementation 'androidx.appcompat:appcompat:1.1.0'
7171
implementation 'androidx.preference:preference:1.1.0'
72+
implementation 'androidx.security:security-crypto:1.0.0-alpha02'
7273
implementation('com.hypertrack:hyperlog:0.0.10') {
7374
exclude group: 'com.android.support'
7475
exclude group: 'com.android.volley'

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
1515
android:maxSdkVersion="18" />
1616

17+
<uses-sdk tools:overrideLibrary="androidx.security" />
18+
1719
<application
1820
android:allowBackup="false"
1921
android:icon="@mipmap/ic_launcher"

app/src/main/java/org/ostrya/presencepublisher/ForegroundService.java

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,27 @@
3131
import java.util.concurrent.Executors;
3232
import java.util.concurrent.atomic.AtomicBoolean;
3333

34-
import static org.ostrya.presencepublisher.ui.ConnectionFragment.*;
35-
import static org.ostrya.presencepublisher.ui.ScheduleFragment.*;
34+
import static org.ostrya.presencepublisher.ui.ScheduleFragment.SSID;
3635
import static org.ostrya.presencepublisher.ui.notification.NotificationFactory.getServiceNotification;
3736
import static org.ostrya.presencepublisher.ui.notification.NotificationFactory.updateServiceNotification;
37+
import static org.ostrya.presencepublisher.ui.preference.AutostartPreference.AUTOSTART;
38+
import static org.ostrya.presencepublisher.ui.preference.BatteryTopicPreference.BATTERY_TOPIC;
39+
import static org.ostrya.presencepublisher.ui.preference.ClientCertificatePreference.CLIENT_CERTIFICATE;
40+
import static org.ostrya.presencepublisher.ui.preference.HostPreference.HOST;
41+
import static org.ostrya.presencepublisher.ui.preference.LastSuccessTimestampPreference.LAST_SUCCESS;
42+
import static org.ostrya.presencepublisher.ui.preference.MessageSchedulePreference.MESSAGE_SCHEDULE;
43+
import static org.ostrya.presencepublisher.ui.preference.NextScheduleTimestampPreference.NEXT_SCHEDULE;
44+
import static org.ostrya.presencepublisher.ui.preference.OfflineContentPreference.OFFLINE_CONTENT;
45+
import static org.ostrya.presencepublisher.ui.preference.PasswordPreference.PASSWORD;
46+
import static org.ostrya.presencepublisher.ui.preference.PortPreference.PORT;
47+
import static org.ostrya.presencepublisher.ui.preference.PresenceTopicPreference.PRESENCE_TOPIC;
48+
import static org.ostrya.presencepublisher.ui.preference.SendBatteryMessagePreference.SEND_BATTERY_MESSAGE;
49+
import static org.ostrya.presencepublisher.ui.preference.SendOfflineMessagePreference.SEND_OFFLINE_MESSAGE;
50+
import static org.ostrya.presencepublisher.ui.preference.SendViaMobileNetworkPreference.SEND_VIA_MOBILE_NETWORK;
51+
import static org.ostrya.presencepublisher.ui.preference.SsidListPreference.SSID_LIST;
52+
import static org.ostrya.presencepublisher.ui.preference.UseTlsPreference.USE_TLS;
53+
import static org.ostrya.presencepublisher.ui.preference.UsernamePreference.USERNAME;
54+
import static org.ostrya.presencepublisher.ui.preference.WifiContentPreference.WIFI_CONTENT_PREFIX;
3855

3956
public class ForegroundService extends Service {
4057
public static final String ALARM_ACTION = "org.ostrya.presencepublisher.ALARM_ACTION";
@@ -51,9 +68,9 @@ public class ForegroundService extends Service {
5168
private MqttService mqttService;
5269
private ConnectivityManager connectivityManager;
5370
private AlarmManager alarmManager;
54-
private long lastPing;
71+
private long lastSuccess;
5572
private SharedPreferences sharedPreferences;
56-
private long nextPing;
73+
private long nextSchedule;
5774
private WifiMessageProvider wifiMessageProvider;
5875
private BatteryMessageProvider batteryMessageProvider;
5976
private final OnSharedPreferenceChangeListener sharedPreferenceListener = this::onSharedPreferenceChanged;
@@ -107,8 +124,8 @@ public void onCreate() {
107124
intent.setClass(getApplicationContext(), AlarmReceiver.class);
108125
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, 0);
109126
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
110-
lastPing = sharedPreferences.getLong(LAST_PING, 0L);
111-
nextPing = sharedPreferences.getLong(NEXT_PING, 0L);
127+
lastSuccess = sharedPreferences.getLong(LAST_SUCCESS, 0L);
128+
nextSchedule = sharedPreferences.getLong(NEXT_SCHEDULE, 0L);
112129
wifiMessageProvider = new WifiMessageProvider(this);
113130
batteryMessageProvider = new BatteryMessageProvider(this);
114131
registerPreferenceCallback();
@@ -156,24 +173,32 @@ private void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Stri
156173
switch (key) {
157174
case HOST:
158175
case PORT:
159-
case TLS:
160-
case CLIENT_CERT:
176+
case USE_TLS:
177+
case CLIENT_CERTIFICATE:
161178
case PRESENCE_TOPIC:
162-
case PING:
163-
case LOGIN:
179+
case MESSAGE_SCHEDULE:
180+
case USERNAME:
164181
case PASSWORD:
165182
case SSID_LIST:
166-
case OFFLINE_PING:
167-
case MOBILE_NETWORK_PING:
183+
case SEND_OFFLINE_MESSAGE:
184+
case SEND_VIA_MOBILE_NETWORK:
185+
case SEND_BATTERY_MESSAGE:
186+
case BATTERY_TOPIC:
187+
case OFFLINE_CONTENT:
168188
HyperLog.i(TAG, "Changed parameter " + key);
169189
start();
170190
break;
171191
case AUTOSTART:
172-
case LAST_PING:
173-
case NEXT_PING:
192+
case LAST_SUCCESS:
193+
case NEXT_SCHEDULE:
174194
break;
175195
default:
176-
HyperLog.v(TAG, "Ignoring unexpected value " + key);
196+
if (key.startsWith(WIFI_CONTENT_PREFIX)) {
197+
HyperLog.i(TAG, "Changed parameter " + key);
198+
start();
199+
} else {
200+
HyperLog.v(TAG, "Ignoring unexpected value " + key);
201+
}
177202
}
178203
}
179204

@@ -192,17 +217,17 @@ private void start() {
192217
} catch (RuntimeException e) {
193218
HyperLog.w(TAG, "Error while getting messages to send", e);
194219
}
195-
int ping = sharedPreferences.getInt(PING, 15);
196-
nextPing = System.currentTimeMillis() + ping * 60_000L;
197-
HyperLog.i(TAG, "Re-scheduling for " + new Date(nextPing));
198-
sharedPreferences.edit().putLong(NEXT_PING, nextPing).apply();
220+
int ping = sharedPreferences.getInt(MESSAGE_SCHEDULE, 15);
221+
nextSchedule = System.currentTimeMillis() + ping * 60_000L;
222+
HyperLog.i(TAG, "Re-scheduling for " + new Date(nextSchedule));
223+
sharedPreferences.edit().putLong(NEXT_SCHEDULE, nextSchedule).apply();
199224
updateNotification();
200225
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
201-
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextPing, pendingIntent);
226+
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextSchedule, pendingIntent);
202227
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
203-
alarmManager.setExact(AlarmManager.RTC_WAKEUP, nextPing, pendingIntent);
228+
alarmManager.setExact(AlarmManager.RTC_WAKEUP, nextSchedule, pendingIntent);
204229
} else {
205-
alarmManager.set(AlarmManager.RTC_WAKEUP, nextPing, pendingIntent);
230+
alarmManager.set(AlarmManager.RTC_WAKEUP, nextSchedule, pendingIntent);
206231
}
207232
} finally {
208233
currentlyRunning.set(false);
@@ -211,7 +236,7 @@ private void start() {
211236

212237
private void updateNotification() {
213238
NotificationManagerCompat.from(this)
214-
.notify(NOTIFICATION_ID, updateServiceNotification(getApplicationContext(), lastPing, nextPing, CHANNEL_ID));
239+
.notify(NOTIFICATION_ID, updateServiceNotification(getApplicationContext(), lastSuccess, nextSchedule, CHANNEL_ID));
215240
}
216241

217242
@Override
@@ -222,8 +247,8 @@ public IBinder onBind(final Intent intent) {
222247
private void doSend(List<Message> messages) {
223248
try {
224249
mqttService.sendMessages(messages);
225-
lastPing = System.currentTimeMillis();
226-
sharedPreferences.edit().putLong(LAST_PING, lastPing).apply();
250+
lastSuccess = System.currentTimeMillis();
251+
sharedPreferences.edit().putLong(LAST_SUCCESS, lastSuccess).apply();
227252
updateNotification();
228253
} catch (Exception e) {
229254
HyperLog.w(TAG, "Error while sending messages", e);

app/src/main/java/org/ostrya/presencepublisher/message/battery/BatteryMessageProvider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import java.util.Collections;
1313
import java.util.List;
1414

15-
import static org.ostrya.presencepublisher.ui.ScheduleFragment.BATTERY_MESSAGE;
16-
import static org.ostrya.presencepublisher.ui.ScheduleFragment.BATTERY_TOPIC;
15+
import static org.ostrya.presencepublisher.ui.preference.BatteryTopicPreference.BATTERY_TOPIC;
16+
import static org.ostrya.presencepublisher.ui.preference.SendBatteryMessagePreference.SEND_BATTERY_MESSAGE;
1717

1818
public class BatteryMessageProvider {
1919
private static final String TAG = "BatteryMessageProvider";
@@ -27,7 +27,7 @@ public BatteryMessageProvider(Context context) {
2727
}
2828

2929
public List<Message> getMessages() {
30-
if (!sharedPreferences.getBoolean(BATTERY_MESSAGE, false)) {
30+
if (!sharedPreferences.getBoolean(SEND_BATTERY_MESSAGE, false)) {
3131
HyperLog.d(TAG, "Battery messages disabled, not generating any messages");
3232
return Collections.emptyList();
3333
}

app/src/main/java/org/ostrya/presencepublisher/message/wifi/WifiMessageProvider.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@
1111
import androidx.preference.PreferenceManager;
1212
import com.hypertrack.hyperlog.HyperLog;
1313
import org.ostrya.presencepublisher.message.Message;
14+
import org.ostrya.presencepublisher.ui.preference.OfflineContentPreference;
15+
import org.ostrya.presencepublisher.ui.preference.WifiContentPreference;
1416

1517
import java.util.Collections;
1618
import java.util.List;
1719
import java.util.Set;
1820

1921
import static android.content.Context.CONNECTIVITY_SERVICE;
2022
import static android.content.Context.WIFI_SERVICE;
21-
import static org.ostrya.presencepublisher.ui.ConnectionFragment.PRESENCE_TOPIC;
22-
import static org.ostrya.presencepublisher.ui.ContentFragment.*;
23-
import static org.ostrya.presencepublisher.ui.ScheduleFragment.*;
23+
import static org.ostrya.presencepublisher.ui.preference.PresenceTopicPreference.PRESENCE_TOPIC;
24+
import static org.ostrya.presencepublisher.ui.preference.SendOfflineMessagePreference.SEND_OFFLINE_MESSAGE;
25+
import static org.ostrya.presencepublisher.ui.preference.SendViaMobileNetworkPreference.SEND_VIA_MOBILE_NETWORK;
26+
import static org.ostrya.presencepublisher.ui.preference.SsidListPreference.SSID_LIST;
2427

2528
public class WifiMessageProvider {
2629
private static final String TAG = "WifiMessageProvider";
@@ -48,14 +51,14 @@ public List<Message> getMessages() {
4851
String ssid = getSsidIfMatching();
4952
if (ssid != null) {
5053
HyperLog.i(TAG, "Scheduling message for SSID " + ssid);
51-
String onlineContent = sharedPreferences.getString(WIFI_PREFIX + ssid, DEFAULT_CONTENT_ONLINE);
54+
String onlineContent = sharedPreferences.getString(WifiContentPreference.WIFI_CONTENT_PREFIX + ssid, WifiContentPreference.DEFAULT_CONTENT_ONLINE);
5255
return Collections.singletonList(messageBuilder.withContent(onlineContent));
5356
}
5457
}
55-
if (sharedPreferences.getBoolean(OFFLINE_PING, false)
56-
&& (connectedToWiFi || sharedPreferences.getBoolean(MOBILE_NETWORK_PING, false))) {
58+
if (sharedPreferences.getBoolean(SEND_OFFLINE_MESSAGE, false)
59+
&& (connectedToWiFi || sharedPreferences.getBoolean(SEND_VIA_MOBILE_NETWORK, false))) {
5760
HyperLog.i(TAG, "Scheduling offline message");
58-
String offlineContent = sharedPreferences.getString(CONTENT_OFFLINE, DEFAULT_CONTENT_OFFLINE);
61+
String offlineContent = sharedPreferences.getString(OfflineContentPreference.OFFLINE_CONTENT, OfflineContentPreference.DEFAULT_CONTENT_OFFLINE);
5962
return Collections.singletonList(messageBuilder.withContent(offlineContent));
6063
}
6164
return Collections.emptyList();

app/src/main/java/org/ostrya/presencepublisher/mqtt/MqttService.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,38 @@
1010
import org.eclipse.paho.client.mqttv3.MqttException;
1111
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
1212
import org.ostrya.presencepublisher.message.Message;
13+
import org.ostrya.presencepublisher.security.SecurePreferencesHelper;
1314

1415
import java.nio.charset.Charset;
1516
import java.util.List;
1617

17-
import static org.ostrya.presencepublisher.ui.ConnectionFragment.*;
18+
import static org.ostrya.presencepublisher.ui.preference.ClientCertificatePreference.CLIENT_CERTIFICATE;
19+
import static org.ostrya.presencepublisher.ui.preference.HostPreference.HOST;
20+
import static org.ostrya.presencepublisher.ui.preference.PasswordPreference.PASSWORD;
21+
import static org.ostrya.presencepublisher.ui.preference.PortPreference.PORT;
22+
import static org.ostrya.presencepublisher.ui.preference.UseTlsPreference.USE_TLS;
23+
import static org.ostrya.presencepublisher.ui.preference.UsernamePreference.USERNAME;
1824

1925
public class MqttService {
2026
private static final String TAG = "MqttService";
2127

2228
private final AndroidSslSocketFactoryFactory factory;
2329
private final SharedPreferences sharedPreferences;
30+
private final SharedPreferences securePreferences;
2431

2532
public MqttService(Context context) {
2633
Context applicationContext = context.getApplicationContext();
2734
factory = new AndroidSslSocketFactoryFactory(applicationContext);
2835
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext);
36+
securePreferences = SecurePreferencesHelper.getSecurePreferences(applicationContext);
2937
}
3038

3139
public void sendMessages(List<Message> messages) throws MqttException {
3240
HyperLog.i(TAG, "Sending messages to server");
33-
boolean tls = sharedPreferences.getBoolean(TLS, false);
34-
String clientCertAlias = sharedPreferences.getString(CLIENT_CERT, null);
35-
String login = sharedPreferences.getString(LOGIN, "");
36-
String password = sharedPreferences.getString(PASSWORD, "");
41+
boolean tls = sharedPreferences.getBoolean(USE_TLS, false);
42+
String clientCertAlias = sharedPreferences.getString(CLIENT_CERTIFICATE, null);
43+
String login = sharedPreferences.getString(USERNAME, "");
44+
String password = securePreferences.getString(PASSWORD, "");
3745

3846
MqttClient mqttClient = new MqttClient(getMqttUrl(tls), Settings.Secure.ANDROID_ID, new MemoryPersistence());
3947
MqttConnectOptions options = new MqttConnectOptions();

app/src/main/java/org/ostrya/presencepublisher/receiver/SystemBroadcastReceiver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import com.hypertrack.hyperlog.HyperLog;
1010
import org.ostrya.presencepublisher.ForegroundService;
1111

12-
import static org.ostrya.presencepublisher.ui.ScheduleFragment.AUTOSTART;
12+
import static org.ostrya.presencepublisher.ui.preference.AutostartPreference.AUTOSTART;
1313

1414
public class SystemBroadcastReceiver extends BroadcastReceiver {
1515
private static final String TAG = "SystemBroadcastReceiver";
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.ostrya.presencepublisher.security;
2+
3+
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
import android.os.Build;
6+
import android.security.keystore.KeyGenParameterSpec;
7+
import androidx.annotation.Nullable;
8+
import androidx.preference.PreferenceDataStore;
9+
import androidx.security.crypto.EncryptedSharedPreferences;
10+
import androidx.security.crypto.MasterKeys;
11+
import com.hypertrack.hyperlog.HyperLog;
12+
13+
public class SecurePreferencesHelper {
14+
private static final String TAG = "SecurePreferencesHelper";
15+
16+
private static final String FILENAME = "encryptedPreferences";
17+
18+
public static PreferenceDataStore getSecurePreferenceDataStore(Context context) {
19+
return new PreferenceDataStore() {
20+
@Override
21+
public void putString(String key, @Nullable String value) {
22+
SharedPreferences preferences = getSecurePreferences(context);
23+
preferences.edit().putString(key, value).apply();
24+
}
25+
26+
@Nullable
27+
@Override
28+
public String getString(String key, @Nullable String defValue) {
29+
SharedPreferences preferences = getSecurePreferences(context);
30+
return preferences.getString(key, defValue);
31+
}
32+
};
33+
}
34+
35+
public static SharedPreferences getSecurePreferences(Context context) {
36+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
37+
try {
38+
KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
39+
String masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);
40+
return EncryptedSharedPreferences
41+
.create(
42+
FILENAME,
43+
masterKeyAlias,
44+
context,
45+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
46+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
47+
);
48+
} catch (Exception e) {
49+
HyperLog.w(TAG, "Unable to get secure preferences", e);
50+
throw new RuntimeException("Unable to get secure preferences");
51+
}
52+
} else {
53+
return context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE);
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)