Skip to content

Commit

Permalink
Merge pull request #620 from braintree/encrypted_shared_prefs_defensi…
Browse files Browse the repository at this point in the history
…ve_guards

Add Defensive Guards to Prevent Crashes Related to Jetpack Security Crypto
  • Loading branch information
jcnoriega authored Nov 7, 2022
2 parents a32642e + e52d7bd commit 67049b6
Show file tree
Hide file tree
Showing 23 changed files with 932 additions and 342 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ private static BraintreeClientParams createDefaultParams(Context context, String
.browserSwitchClient(new BrowserSwitchClient())
.manifestValidator(new ManifestValidator())
.UUIDHelper(new UUIDHelper())
.configurationLoader(new ConfigurationLoader(httpClient));
.configurationLoader(new ConfigurationLoader(context, httpClient));
}

/**
Expand Down Expand Up @@ -178,9 +178,27 @@ public BraintreeClient(@NonNull Context context, @NonNull ClientTokenProvider cl
public void getConfiguration(@NonNull final ConfigurationCallback callback) {
getAuthorization(new AuthorizationCallback() {
@Override
public void onAuthorizationResult(@Nullable Authorization authorization, @Nullable Exception error) {
public void onAuthorizationResult(@Nullable final Authorization authorization, @Nullable Exception error) {
if (authorization != null) {
configurationLoader.loadConfiguration(applicationContext, authorization, callback);
configurationLoader.loadConfiguration(authorization, new ConfigurationLoaderCallback() {
@Override
public void onResult(@Nullable ConfigurationLoaderResult result, @Nullable Exception error) {
if (result != null) {
Configuration configuration = result.getConfiguration();
callback.onResult(configuration, null);

if (result.getLoadFromCacheError() != null) {
sendAnalyticsEvent("configuration.cache.load.failed", configuration, authorization);
}
if (result.getSaveToCacheError() != null) {
sendAnalyticsEvent("configuration.cache.save.failed", configuration, authorization);
}

} else {
callback.onResult(null, error);
}
}
});
} else {
callback.onResult(null, error);
}
Expand All @@ -200,16 +218,20 @@ public void onAuthorizationResult(@Nullable final Authorization authorization, @
getConfiguration(new ConfigurationCallback() {
@Override
public void onResult(@Nullable Configuration configuration, @Nullable Exception error) {
if (isAnalyticsEnabled(configuration)) {
analyticsClient.sendEvent(configuration, eventName, sessionId, getIntegrationType(), authorization);
}
sendAnalyticsEvent(eventName, configuration, authorization);
}
});
}
}
});
}

private void sendAnalyticsEvent(String eventName, Configuration configuration, Authorization authorization) {
if (isAnalyticsEnabled(configuration)) {
analyticsClient.sendEvent(configuration, eventName, sessionId, getIntegrationType(), authorization);
}
}

void sendGET(final String url, final HttpResponseCallback responseCallback) {
getAuthorization(new AuthorizationCallback() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,49 @@ class ConfigurationCache {
private static final long TIME_TO_LIVE = TimeUnit.MINUTES.toMillis(5);

private static volatile ConfigurationCache INSTANCE;
private final BraintreeSharedPreferences braintreeSharedPreferences;

static ConfigurationCache getInstance() {
private final BraintreeSharedPreferences sharedPreferences;

static ConfigurationCache getInstance(Context context) {
if (INSTANCE == null) {
synchronized (ConfigurationCache.class) {
// double check that instance was not created in another thread
if (INSTANCE == null) {
INSTANCE = new ConfigurationCache(BraintreeSharedPreferences.getInstance());
INSTANCE = new ConfigurationCache(BraintreeSharedPreferences.getInstance(context));
}
}
}
return INSTANCE;
}

@VisibleForTesting
ConfigurationCache(BraintreeSharedPreferences braintreeSharedPreferences) {
this.braintreeSharedPreferences = braintreeSharedPreferences;
ConfigurationCache(BraintreeSharedPreferences sharedPreferences) {
this.sharedPreferences = sharedPreferences;
}

String getConfiguration(Context context, String cacheKey) {
return getConfiguration(context, cacheKey, System.currentTimeMillis());
String getConfiguration(String cacheKey) throws BraintreeSharedPreferencesException {
return getConfiguration(cacheKey, System.currentTimeMillis());
}

@VisibleForTesting
String getConfiguration(Context context, String cacheKey, long currentTimeMillis) {
String getConfiguration(String cacheKey, long currentTimeMillis) throws BraintreeSharedPreferencesException {
String timestampKey = cacheKey + "_timestamp";
if (braintreeSharedPreferences.containsKey(context, timestampKey)) {
long timeInCache = (currentTimeMillis - braintreeSharedPreferences.getLong(context, timestampKey));
if (sharedPreferences.containsKey(timestampKey)) {
long timeInCache = (currentTimeMillis - sharedPreferences.getLong(timestampKey));
if (timeInCache < TIME_TO_LIVE) {
return braintreeSharedPreferences.getString(context, cacheKey, "");
return sharedPreferences.getString(cacheKey, "");
}
}

return null;
}

void saveConfiguration(Context context, Configuration configuration, String cacheKey) {
saveConfiguration(context, configuration, cacheKey, System.currentTimeMillis());
void saveConfiguration(Configuration configuration, String cacheKey) throws BraintreeSharedPreferencesException {
saveConfiguration(configuration, cacheKey, System.currentTimeMillis());
}

@VisibleForTesting
void saveConfiguration(Context context, Configuration configuration, String cacheKey, long currentTimeMillis) {
void saveConfiguration(Configuration configuration, String cacheKey, long currentTimeMillis) throws BraintreeSharedPreferencesException {
String timestampKey = String.format("%s_timestamp", cacheKey);
braintreeSharedPreferences.putStringAndLong(context, cacheKey, configuration.toJson(), timestampKey, currentTimeMillis);
sharedPreferences.putStringAndLong(cacheKey, configuration.toJson(), timestampKey, currentTimeMillis);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ class ConfigurationLoader {
private final BraintreeHttpClient httpClient;
private final ConfigurationCache configurationCache;

ConfigurationLoader(BraintreeHttpClient httpClient) {
this(httpClient, ConfigurationCache.getInstance());
ConfigurationLoader(Context context, BraintreeHttpClient httpClient) {
this(httpClient, ConfigurationCache.getInstance(context));
}

@VisibleForTesting
Expand All @@ -23,7 +23,7 @@ class ConfigurationLoader {
this.configurationCache = configurationCache;
}

void loadConfiguration(final Context context, final Authorization authorization, final ConfigurationCallback callback) {
void loadConfiguration(final Authorization authorization, final ConfigurationLoaderCallback callback) {
if (authorization instanceof InvalidAuthorization) {
String message = ((InvalidAuthorization) authorization).getErrorMessage();
callback.onResult(null, new BraintreeException(message));
Expand All @@ -36,20 +36,39 @@ void loadConfiguration(final Context context, final Authorization authorization,
.build()
.toString();

Configuration cachedConfig = getCachedConfiguration(context, authorization, configUrl);

Configuration cachedConfig = null;
BraintreeSharedPreferencesException loadFromCacheException = null;
try {
cachedConfig = getCachedConfiguration(authorization, configUrl);
} catch (BraintreeSharedPreferencesException e) {
loadFromCacheException = e;
}

if (cachedConfig != null) {
callback.onResult(cachedConfig, null);
ConfigurationLoaderResult resultFromCache = new ConfigurationLoaderResult(cachedConfig);
callback.onResult(resultFromCache, null);
} else {

final BraintreeSharedPreferencesException finalLoadFromCacheException = loadFromCacheException;
httpClient.get(configUrl, null, authorization, HttpClient.RETRY_MAX_3_TIMES, new HttpResponseCallback() {

@Override
public void onResult(String responseBody, Exception httpError) {
if (responseBody != null) {
try {
Configuration configuration = Configuration.fromJson(responseBody);
saveConfigurationToCache(context, configuration, authorization, configUrl);
callback.onResult(configuration, null);

BraintreeSharedPreferencesException saveToCacheException = null;
try {
saveConfigurationToCache(configuration, authorization, configUrl);
} catch (BraintreeSharedPreferencesException e) {
saveToCacheException = e;
}

ConfigurationLoaderResult resultFromNetwork =
new ConfigurationLoaderResult(configuration, finalLoadFromCacheException, saveToCacheException);
callback.onResult(resultFromNetwork, null);
} catch (JSONException jsonException) {
callback.onResult(null, jsonException);
}
Expand All @@ -65,14 +84,14 @@ public void onResult(String responseBody, Exception httpError) {
}
}

private void saveConfigurationToCache(Context context, Configuration configuration, Authorization authorization, String configUrl) {
private void saveConfigurationToCache(Configuration configuration, Authorization authorization, String configUrl) throws BraintreeSharedPreferencesException {
String cacheKey = createCacheKey(authorization, configUrl);
configurationCache.saveConfiguration(context, configuration, cacheKey);
configurationCache.saveConfiguration(configuration, cacheKey);
}

private Configuration getCachedConfiguration(Context context, Authorization authorization, String configUrl) {
private Configuration getCachedConfiguration(Authorization authorization, String configUrl) throws BraintreeSharedPreferencesException {
String cacheKey = createCacheKey(authorization, configUrl);
String cachedConfigResponse = configurationCache.getConfiguration(context, cacheKey);
String cachedConfigResponse = configurationCache.getConfiguration(cacheKey);
try {
return Configuration.fromJson(cachedConfigResponse);
} catch (JSONException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.braintreepayments.api;

import androidx.annotation.Nullable;

interface ConfigurationLoaderCallback {

void onResult(@Nullable ConfigurationLoaderResult result, @Nullable Exception error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.braintreepayments.api;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

class ConfigurationLoaderResult {

private final Configuration configuration;
private final Exception loadFromCacheError;
private final Exception saveToCacheError;

ConfigurationLoaderResult(@NonNull Configuration configuration) {
this(configuration, null, null);
}

ConfigurationLoaderResult(@NonNull Configuration configuration, @Nullable Exception loadFromCacheError, @Nullable Exception saveToCacheError) {
this.configuration = configuration;
this.loadFromCacheError = loadFromCacheError;
this.saveToCacheError = saveToCacheError;
}

public Configuration getConfiguration() {
return configuration;
}

public Exception getLoadFromCacheError() {
return loadFromCacheError;
}

public Exception getSaveToCacheError() {
return saveToCacheError;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,25 @@ class UUIDHelper {
* @return A persistent UUID for this application install.
*/
String getPersistentUUID(Context context) {
return getPersistentUUID(context, BraintreeSharedPreferences.getInstance());
return getPersistentUUID(BraintreeSharedPreferences.getInstance(context));
}

@VisibleForTesting
String getPersistentUUID(Context context, BraintreeSharedPreferences braintreeSharedPreferences) {
String uuid = braintreeSharedPreferences.getString(context, BRAINTREE_UUID_KEY, null);
String getPersistentUUID(BraintreeSharedPreferences braintreeSharedPreferences) {
String uuid = null;
try {
uuid = braintreeSharedPreferences.getString(BRAINTREE_UUID_KEY, null);
} catch (BraintreeSharedPreferencesException ignored) {
// protect against shared prefs failure: default to creating a new UUID in this scenario
}

if (uuid == null) {
uuid = getFormattedUUID();
braintreeSharedPreferences.putString(context, BRAINTREE_UUID_KEY, uuid);
try {
braintreeSharedPreferences.putString(BRAINTREE_UUID_KEY, uuid);
} catch (BraintreeSharedPreferencesException ignored) {
// protect against shared prefs failure: no-op when we're unable to persist the UUID
}
}

return uuid;
Expand All @@ -36,18 +45,26 @@ String getFormattedUUID() {
}

String getInstallationGUID(Context context) {
return getInstallationGUID(context, BraintreeSharedPreferences.getInstance());
return getInstallationGUID(BraintreeSharedPreferences.getInstance(context));
}

@VisibleForTesting
String getInstallationGUID(Context context, BraintreeSharedPreferences braintreeSharedPreferences) {
String existingGUID = braintreeSharedPreferences.getString(context, INSTALL_GUID, null);
if (existingGUID != null) {
return existingGUID;
} else {
String newGuid = UUID.randomUUID().toString();
braintreeSharedPreferences.putString(context, INSTALL_GUID, newGuid);
return newGuid;
String getInstallationGUID(BraintreeSharedPreferences braintreeSharedPreferences) {
String installationGUID = null;
try {
installationGUID = braintreeSharedPreferences.getString(INSTALL_GUID, null);
} catch (BraintreeSharedPreferencesException ignored) {
// protect against shared prefs failure: default to creating a new GUID in this scenario
}

if (installationGUID == null) {
installationGUID = UUID.randomUUID().toString();
try {
braintreeSharedPreferences.putString(INSTALL_GUID, installationGUID);
} catch (BraintreeSharedPreferencesException ignored) {
// protect against shared prefs failure: no-op when we're unable to persist the GUID
}
}
return installationGUID;
}
}
Loading

0 comments on commit 67049b6

Please sign in to comment.