Skip to content

Commit cd27c3b

Browse files
authored
Merge pull request #6 from snabble/keystore_migration
Keystore to RSA migration
2 parents e5aab10 + 6fe716f commit cd27c3b

File tree

9 files changed

+93
-50
lines changed

9 files changed

+93
-50
lines changed

core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ dependencies {
6060
implementation 'org.iban4j:iban4j:3.2.1'
6161
implementation 'com.caverock:androidsvg-aar:1.4'
6262
implementation 'com.google.android.gms:play-services-wallet:18.1.3'
63+
implementation 'androidx.biometric:biometric:1.2.0-alpha03'
6364

6465
api "com.squareup.okhttp3:okhttp:${project.okhttpVersion}"
6566
implementation "com.squareup.okhttp3:logging-interceptor:${project.okhttpVersion}"

core/src/main/java/io/snabble/sdk/payment/PaymentCredentials.java

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
import javax.crypto.spec.OAEPParameterSpec;
2323
import javax.crypto.spec.PSource;
2424

25-
import io.snabble.sdk.Events;
2625
import io.snabble.sdk.PaymentMethod;
2726
import io.snabble.sdk.R;
2827
import io.snabble.sdk.Snabble;
28+
import io.snabble.sdk.utils.Dispatch;
2929
import io.snabble.sdk.utils.GsonHolder;
3030
import io.snabble.sdk.utils.Logger;
3131
import io.snabble.sdk.utils.Utils;
@@ -130,9 +130,11 @@ private static class TegutEmployeeCard {
130130
}
131131

132132
private String obfuscatedId;
133+
// comes from previously saved data on deserialization - was used in encrypt() from old code
133134
private boolean isKeyStoreEncrypted;
134-
private boolean canBypassKeyStore;
135-
private String encryptedData;
135+
@Deprecated
136+
private String encryptedData; // old key-store based encrypted data
137+
private String rsaEncryptedData; // rsa encrypted data
136138
private String signature;
137139
private long validTo;
138140
private Type type;
@@ -172,8 +174,7 @@ public static PaymentCredentials fromSEPA(String name, String iban) {
172174
String json = GsonHolder.get().toJson(data, SepaData.class);
173175

174176
X509Certificate certificate = certificates.get(0);
175-
pc.encryptedData = pc.rsaEncrypt(certificate, json.getBytes());
176-
pc.encrypt();
177+
pc.rsaEncryptedData = pc.rsaEncrypt(certificate, json.getBytes());
177178
pc.signature = pc.sha256Signature(certificate);
178179
pc.brand = Brand.UNKNOWN;
179180
pc.appId = Snabble.getInstance().getConfig().appId;
@@ -232,15 +233,14 @@ public static PaymentCredentials fromCreditCardData(String name,
232233
String json = GsonHolder.get().toJson(creditCardData, CreditCardData.class);
233234

234235
X509Certificate certificate = certificates.get(0);
235-
pc.encryptedData = pc.rsaEncrypt(certificate, json.getBytes());
236-
pc.encrypt();
236+
pc.rsaEncryptedData = pc.rsaEncrypt(certificate, json.getBytes());
237237
pc.signature = pc.sha256Signature(certificate);
238238
pc.brand = brand;
239239
pc.appId = Snabble.getInstance().getConfig().appId;
240240
pc.projectId = projectId;
241241
pc.validTo = parseValidTo("MM/yyyy", expirationMonth, expirationYear);
242242

243-
if (pc.encryptedData == null) {
243+
if (pc.rsaEncryptedData == null) {
244244
return null;
245245
}
246246

@@ -290,8 +290,7 @@ public static PaymentCredentials fromPaydirekt(PaydirektAuthorizationData author
290290
String json = GsonHolder.get().toJson(paydirektData, PaydirektData.class);
291291

292292
X509Certificate certificate = certificates.get(0);
293-
pc.encryptedData = pc.rsaEncrypt(certificate, json.getBytes());
294-
pc.encrypt();
293+
pc.rsaEncryptedData = pc.rsaEncrypt(certificate, json.getBytes());
295294
pc.signature = pc.sha256Signature(certificate);
296295
pc.appId = Snabble.getInstance().getConfig().appId;
297296

@@ -301,7 +300,7 @@ public static PaymentCredentials fromPaydirekt(PaydirektAuthorizationData author
301300
pc.additionalData.put("deviceFingerprint", authorizationData.fingerprint);
302301
pc.additionalData.put("deviceIPAddress", authorizationData.ipAddress);
303302

304-
if (pc.encryptedData == null) {
303+
if (pc.rsaEncryptedData == null) {
305304
return null;
306305
}
307306

@@ -342,15 +341,14 @@ public static PaymentCredentials fromDatatrans(String token, Brand brand, String
342341
String json = GsonHolder.get().toJson(datatransData, DatatransData.class);
343342

344343
X509Certificate certificate = certificates.get(0);
345-
pc.encryptedData = pc.rsaEncrypt(certificate, json.getBytes());
346-
pc.encrypt();
344+
pc.rsaEncryptedData = pc.rsaEncrypt(certificate, json.getBytes());
347345
pc.signature = pc.sha256Signature(certificate);
348346
pc.appId = Snabble.getInstance().getConfig().appId;
349347
pc.brand = brand;
350348
pc.obfuscatedId = obfuscatedId;
351349
pc.validTo = parseValidTo("MM/yy", expirationMonth, expirationYear);
352350

353-
if (pc.encryptedData == null) {
351+
if (pc.rsaEncryptedData == null) {
354352
return null;
355353
}
356354

@@ -387,9 +385,8 @@ public static PaymentCredentials fromTegutEmployeeCard(String obfuscatedId, Stri
387385
pc.brand = Brand.UNKNOWN;
388386
pc.appId = Snabble.getInstance().getConfig().appId;
389387
pc.projectId = projectId;
390-
pc.canBypassKeyStore = true;
391388

392-
if (pc.encryptedData == null) {
389+
if (pc.rsaEncryptedData == null) {
393390
return null;
394391
}
395392

@@ -458,26 +455,8 @@ private String rsaEncrypt(X509Certificate certificate, byte[] data) {
458455
}
459456
}
460457

461-
/** Synchronous encryption using the user credentials **/
462-
private void encrypt() {
463-
PaymentCredentialsStore store = Snabble.getInstance().getPaymentCredentialsStore();
464-
465-
if (store.keyStoreCipher == null) {
466-
return;
467-
}
468-
469-
byte[] keyStoreEncrypted = store.keyStoreCipher.encrypt(encryptedData.getBytes());
470-
471-
if (keyStoreEncrypted != null) {
472-
encryptedData = Base64.encodeToString(keyStoreEncrypted, Base64.NO_WRAP);
473-
isKeyStoreEncrypted = true;
474-
} else {
475-
Logger.e("Could not encrypt payment credentials: KeyStore unavailable");
476-
}
477-
}
478-
479458
/** Synchronous decryption using the user credentials **/
480-
private String decrypt() {
459+
private String decryptUsingKeyStore() {
481460
PaymentCredentialsStore store = Snabble.getInstance().getPaymentCredentialsStore();
482461

483462
if (store.keyStoreCipher == null) {
@@ -510,7 +489,7 @@ public boolean isAvailableInCurrentApp() {
510489
}
511490

512491
public boolean canBypassKeyStore() {
513-
return canBypassKeyStore;
492+
return rsaEncryptedData != null;
514493
}
515494

516495
private boolean validateCertificate(X509Certificate certificate) {
@@ -615,7 +594,35 @@ public String getObfuscatedId() {
615594
}
616595

617596
public String getEncryptedData() {
618-
return decrypt();
597+
if (rsaEncryptedData != null) {
598+
return rsaEncryptedData;
599+
}
600+
601+
Logger.logEvent("Accessing not migrated keystore credentials");
602+
return decryptUsingKeyStore();
603+
}
604+
605+
public boolean migrateFromKeyStore() {
606+
// if data is available and we have not migrated before, start migration
607+
if (rsaEncryptedData == null && encryptedData != null) {
608+
rsaEncryptedData = decryptUsingKeyStore();
609+
610+
if (rsaEncryptedData != null) {
611+
Logger.logEvent("Successfully migrated payment credentials");
612+
return true;
613+
} else {
614+
Logger.logEvent("Payment credential migration was unsuccessful");
615+
return false;
616+
}
617+
}
618+
619+
if (rsaEncryptedData == null) {
620+
Logger.errorEvent("Payment credentials are contain no data - removing");
621+
return false;
622+
}
623+
624+
// if already migrated do nothing
625+
return true;
619626
}
620627

621628
public Map<String, String> getAdditionalData() {

core/src/main/java/io/snabble/sdk/payment/PaymentCredentialsStore.java

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ private class Data {
3030
private List<PaymentCredentials> credentialsList;
3131
private String id;
3232
private boolean removedOldCreditCards;
33+
private boolean isMigratedFromKeyStore;
3334
private boolean isKeyguarded;
3435
}
3536

@@ -38,8 +39,8 @@ private class Data {
3839
private List<Callback> callbacks = new CopyOnWriteArrayList<>();
3940
private List<OnPaymentCredentialsAddedListener> onPaymentCredentialsAddedListeners = new CopyOnWriteArrayList<>();
4041
private String credentialsKey;
41-
private UserPreferences userPreferences;
4242

43+
// this still is needed for migration
4344
KeyStoreCipher keyStoreCipher;
4445

4546
public PaymentCredentialsStore() {
@@ -49,7 +50,6 @@ public PaymentCredentialsStore() {
4950
public void init(Context context, Environment environment) {
5051
sharedPreferences = context.getSharedPreferences("snabble_payment", Context.MODE_PRIVATE);
5152
credentialsKey = "credentials_" + (environment != null ? environment.name() : "_UNKNOWN");
52-
userPreferences = Snabble.getInstance().getUserPreferences();
5353

5454
load();
5555
initializeKeyStore();
@@ -137,7 +137,7 @@ public boolean hasRemovedOldCreditCards() {
137137
return data.removedOldCreditCards;
138138
}
139139

140-
public void add(PaymentCredentials credentials) {
140+
public synchronized void add(PaymentCredentials credentials) {
141141
if (credentials == null) {
142142
return;
143143
}
@@ -150,14 +150,19 @@ public void add(PaymentCredentials credentials) {
150150
notifyChanged();
151151
}
152152

153-
public void remove(PaymentCredentials credentials) {
153+
public synchronized void remove(PaymentCredentials credentials) {
154154
if (data.credentialsList.remove(credentials)) {
155155
save();
156156
notifyChanged();
157157
}
158158
}
159159

160-
public void removeInvalidCredentials() {
160+
public synchronized void removeInvalidCredentials() {
161+
// we do not lose payment information if we are not using key store
162+
if (data.isMigratedFromKeyStore) {
163+
return;
164+
}
165+
161166
List<PaymentCredentials> removals = new ArrayList<>();
162167
for (PaymentCredentials credentials : data.credentialsList) {
163168
if (!credentials.canBypassKeyStore()) {
@@ -183,21 +188,40 @@ public void removeInvalidCredentials() {
183188
}
184189
}
185190

186-
public void clear() {
191+
public synchronized void clear() {
187192
if (data.credentialsList.size() > 0) {
188193
data.credentialsList.clear();
189194
save();
190195
notifyChanged();
191196
}
192197
}
193198

194-
private void save() {
199+
public synchronized void maybeMigrateKeyStoreCredentials() {
200+
if (!data.isMigratedFromKeyStore) {
201+
List<PaymentCredentials> removals = new ArrayList<>();
202+
203+
for (PaymentCredentials paymentCredentials : data.credentialsList) {
204+
if (!paymentCredentials.migrateFromKeyStore()) {
205+
removals.add(paymentCredentials);
206+
}
207+
}
208+
209+
for (PaymentCredentials credentials : removals) {
210+
data.credentialsList.remove(credentials);
211+
}
212+
213+
data.isMigratedFromKeyStore = true;
214+
save();
215+
}
216+
}
217+
218+
private synchronized void save() {
195219
Gson gson = new Gson();
196220
String json = gson.toJson(data);
197221
sharedPreferences.edit().putString(credentialsKey, json).apply();
198222
}
199223

200-
private void load() {
224+
private synchronized void load() {
201225
Gson gson = new Gson();
202226
data = new Data();
203227
data.credentialsList = new ArrayList<>();
@@ -218,7 +242,7 @@ private void load() {
218242
}
219243
}
220244

221-
private void validate() {
245+
private synchronized void validate() {
222246
boolean changed = false;
223247

224248
List<PaymentCredentials> removals = new ArrayList<>();

ui/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
implementation 'ch.datatrans:android-sdk:1.4.2'
6666
implementation 'com.google.android.gms:play-services-wallet:18.1.3'
6767
implementation 'eu.rekisoft.android.util:LazyWorker:2.0.1'
68+
implementation 'androidx.biometric:biometric:1.2.0-alpha03'
6869

6970
def camerax_version = "1.0.1"
7071
implementation "androidx.camera:camera-core:${camerax_version}"

ui/src/main/AndroidManifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
<manifest package="io.snabble.sdk.ui"
33
xmlns:android="http://schemas.android.com/apk/res/android">
44

5-
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
6-
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
5+
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
6+
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
77

88
<application>
99
<provider

ui/src/main/java/io/snabble/sdk/ui/Keyguard.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.concurrent.Executors;
2020

21+
import io.snabble.sdk.Snabble;
2122
import io.snabble.sdk.utils.Dispatch;
2223

2324
public class Keyguard {
@@ -111,6 +112,9 @@ private static void success(Callback callback) {
111112
return;
112113
}
113114

115+
// migrate key store stored values to rsa stored values
116+
Snabble.getInstance().getPaymentCredentialsStore().maybeMigrateKeyStoreCredentials();
117+
114118
if (currentCallback != null) {
115119
currentCallback.success();
116120
currentCallback = null;

utils/src/main/java/io/snabble/sdk/utils/security/KeyStoreCipher.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77

88
import io.snabble.sdk.utils.Logger;
99

10+
@Deprecated
1011
public abstract class KeyStoreCipher {
1112
public abstract String id();
1213
public abstract void validate();
1314
public abstract void purge();
1415
public abstract byte[] encrypt(byte[] data);
1516
public abstract byte[] decrypt(byte[] encrypted);
1617

18+
@Deprecated
1719
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
1820
public static KeyStoreCipher create(Context context, String tag, boolean requireUserAuthentication) {
1921
try {

utils/src/main/java/io/snabble/sdk/utils/security/KeyStoreCipherJellyBeanMR2.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.snabble.sdk.utils.Logger;
3232
import io.snabble.sdk.utils.Utils;
3333

34+
@Deprecated
3435
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
3536
public class KeyStoreCipherJellyBeanMR2 extends KeyStoreCipher {
3637
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
@@ -47,6 +48,7 @@ public class KeyStoreCipherJellyBeanMR2 extends KeyStoreCipher {
4748
private boolean requireUserAuthentication;
4849
private Context context;
4950

51+
@Deprecated
5052
KeyStoreCipherJellyBeanMR2(Context context, String alias, boolean requireUserAuthentication) {
5153
this.alias = alias + "_JB_MR2";
5254
this.context = context;

utils/src/main/java/io/snabble/sdk/utils/security/KeyStoreCipherMarshmallow.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.snabble.sdk.utils.Logger;
2222
import io.snabble.sdk.utils.Utils;
2323

24+
@Deprecated
2425
@RequiresApi(api = Build.VERSION_CODES.M)
2526
public class KeyStoreCipherMarshmallow extends KeyStoreCipher {
2627
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
@@ -32,7 +33,8 @@ public class KeyStoreCipherMarshmallow extends KeyStoreCipher {
3233
private boolean requireUserAuthentication;
3334
private boolean wasNotAccessible = false;
3435
private boolean wasPermanentlyInvalidated = false;
35-
36+
37+
@Deprecated
3638
KeyStoreCipherMarshmallow(String alias, boolean requireUserAuthentication) {
3739
this.alias = alias + "_M";
3840
this.requireUserAuthentication = requireUserAuthentication;
@@ -103,7 +105,7 @@ private boolean isKeyAccessible() {
103105
} catch (Exception e) {
104106
wasNotAccessible = true;
105107
Logger.logEvent("KeyStore inaccessible: " + e.getClass().getName() + ": " + e.getMessage());
106-
return true;
108+
return false;
107109
}
108110
}
109111

0 commit comments

Comments
 (0)