Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<string name="ota_restore_url" translatable="false">https://staging.commcarehq.org/ota_restore</string>
<!-- string name="PostURL">https://pact.dimagi.com/receiver/submit/pact</string -->
<!-- string name="ota_restore_url">https://pact.dimagi.com/provider/caselist</string -->
<string name="ConnectFetchDbKeyURL">/users/fetch_db_key</string>
<string name="ConnectTokenURL">/o/token/</string>
<string name="ConnectHeartbeatURL">/users/heartbeat</string>
<!-- region: All strings for multiple apps and app-agnostic properties -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import org.commcare.modern.database.Table;
import org.commcare.modern.models.MetaField;

import kotlin.Metadata;

/**
* DB model for storing the encrypted/encoded Connect DB passphrase
*
Expand All @@ -19,16 +17,16 @@ public class ConnectKeyRecord extends Persisted {
@Persisting(1)
String encryptedPassphrase;

@Deprecated
@Persisting(2)
@MetaField(IS_LOCAL)
boolean isLocal;

public ConnectKeyRecord() {
}

public ConnectKeyRecord(String encryptedPassphrase, boolean isLocal) {
public ConnectKeyRecord(String encryptedPassphrase) {
this.encryptedPassphrase = encryptedPassphrase;
this.isLocal = isLocal;
}

public String getEncryptedPassphrase() {
Expand All @@ -37,11 +35,8 @@ public String getEncryptedPassphrase() {
public void setEncryptedPassphrase(String passphrase) {
encryptedPassphrase = passphrase;
}
public boolean getIsLocal() {
return isLocal;
}

public static ConnectKeyRecord fromV6(ConnectKeyRecordV6 oldVersion) {
return new ConnectKeyRecord(oldVersion.getEncryptedPassphrase(), true);
return new ConnectKeyRecord(oldVersion.getEncryptedPassphrase());
}
}
62 changes: 0 additions & 62 deletions app/src/org/commcare/connect/PersonalIdManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,10 @@
import org.commcare.android.security.AndroidKeyStore;
import org.commcare.connect.database.ConnectAppDatabaseUtil;
import org.commcare.connect.database.ConnectDatabaseHelper;
import org.commcare.connect.database.ConnectDatabaseUtils;
import org.commcare.connect.database.ConnectJobUtils;
import org.commcare.connect.database.ConnectUserDatabaseUtil;
import org.commcare.connect.network.ApiPersonalId;
import org.commcare.connect.network.ConnectNetworkHelper;
import org.commcare.connect.network.ConnectSsoHelper;
import org.commcare.connect.network.IApiCallback;
import org.commcare.connect.network.TokenDeniedException;
import org.commcare.connect.network.TokenUnavailableException;
import org.commcare.connect.workers.ConnectHeartbeatWorker;
Expand All @@ -46,15 +43,9 @@
import org.commcare.utils.EncryptionKeyProvider;
import org.commcare.utils.GlobalErrors;
import org.commcare.views.dialogs.StandardAlertDialog;
import org.javarosa.core.io.StreamsUtil;
import org.javarosa.core.services.Logger;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

/**
Expand Down Expand Up @@ -118,11 +109,6 @@ public void init(Context parent) {
personalIdSatus = registering ? PersonalIdStatus.Registering : PersonalIdStatus.LoggedIn;

CrashUtil.registerUserData();

String remotePassphrase = ConnectDatabaseUtils.getConnectDbEncodedPassphrase(parent, false);
if (remotePassphrase == null) {
getRemoteDbPassphrase(parent, user);
}
} else if (ConnectDatabaseHelper.isDbBroken()) {
//Corrupt DB, inform user to recover
ConnectDatabaseHelper.crashDb(GlobalErrors.PERSONALID_DB_STARTUP_ERROR);
Expand Down Expand Up @@ -450,54 +436,6 @@ public AuthInfo.TokenAuth getTokenCredentialsForApp(String appId, String userId)
return null;
}

private void getRemoteDbPassphrase(Context context, ConnectUserRecord user) {
ApiPersonalId.fetchDbPassphrase(context, user, new IApiCallback() {
@Override
public void processSuccess(int responseCode, InputStream responseData) {
try (InputStream in = responseData) {
String responseAsString = new String(
StreamsUtil.inputStreamToByteArray(in));
if (responseAsString.length() > 0) {
JSONObject json = new JSONObject(responseAsString);
String key = ConnectConstants.CONNECT_KEY_DB_KEY;
if (json.has(key)) {
ConnectDatabaseHelper.handleReceivedDbPassphrase(context, json.getString(key));
}
}
} catch (JSONException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Logger.exception("Parsing return from DB key request", e);
}
}

@Override
public void processFailure(int responseCode, @Nullable InputStream errorResponse,String endPoint) {
Logger.log("ERROR", String.format(Locale.getDefault(), "Failed: %d", responseCode));
}

@Override
public void processNetworkFailure() {
Logger.log("ERROR", "Failed (network)");
}

@Override
public void processTokenUnavailableError() {
Logger.log("ERROR", "Failed (token unavailable)");
}

@Override
public void processTokenRequestDeniedError() {
ConnectNetworkHelper.handleTokenDeniedException();
}

@Override
public void processOldApiError() {
ConnectNetworkHelper.showOutdatedApiError(context);
}
});
}

public PersonalIdStatus getStatus() {
return personalIdSatus;
}
Expand Down
44 changes: 5 additions & 39 deletions app/src/org/commcare/connect/database/ConnectDatabaseHelper.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
package org.commcare.connect.database;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;


import org.commcare.android.database.connect.models.ConnectLinkedAppRecord;
import org.commcare.android.database.connect.models.ConnectUserRecord;
import org.commcare.android.database.global.models.ConnectKeyRecord;
import org.commcare.android.database.global.models.GlobalErrorRecord;
import org.commcare.connect.network.SsoToken;
import org.commcare.dalvik.R;
import org.commcare.google.services.analytics.FirebaseAnalyticsUtil;
import org.commcare.models.database.AndroidDbHelper;
import org.commcare.models.database.IDatabase;
import org.commcare.models.database.EncryptedDatabaseAdapter;
Expand All @@ -22,10 +15,8 @@
import org.commcare.modern.database.Table;
import org.commcare.utils.GlobalErrorUtil;
import org.commcare.utils.GlobalErrors;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.storage.Persistable;

import android.util.Base64;
import java.util.Date;


Expand All @@ -39,21 +30,8 @@ public class ConnectDatabaseHelper {
public static IDatabase connectDatabase;
static boolean dbBroken = false;

public static void handleReceivedDbPassphrase(Context context, String remotePassphrase) {
ConnectDatabaseUtils.storeConnectDbPassphrase(context, remotePassphrase, false);
try {
//Rekey the DB if the remote passphrase is different than local
String localPassphrase = ConnectDatabaseUtils.getConnectDbEncodedPassphrase(context, true);
if (connectDatabase != null && connectDatabase.isOpen() && !remotePassphrase.equals(localPassphrase)) {
DatabaseConnectOpenHelper.rekeyDB(connectDatabase, remotePassphrase);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also clear out the rekeyDB from DatabaseConnectOpenHelper or is it still used elsewhere ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, missed that one! becb0fc

FirebaseAnalyticsUtil.reportRekeyedDatabase();
}

//Store the received passphrase as what's in use locally
ConnectDatabaseUtils.storeConnectDbPassphrase(context, remotePassphrase, true);
} catch (Exception e) {
crashDb(GlobalErrors.PERSONALID_DB_UPGRADE_ERROR, e);
}
public static void handleReceivedDbPassphrase(Context context, String passphrase) {
ConnectDatabaseUtils.storeConnectDbPassphrase(context, passphrase);
}

public static boolean dbExists() {
Expand All @@ -71,25 +49,13 @@ public IDatabase getHandle() {
synchronized (connectDbHandleLock) {
if (connectDatabase == null || !connectDatabase.isOpen()) {
try {
byte[] passphrase = ConnectDatabaseUtils.getConnectDbPassphrase(context, true);
byte[] passphrase = ConnectDatabaseUtils.getConnectDbPassphrase(context);
if(passphrase == null) {
throw new IllegalStateException("Attempting to access Connect DB without a passphrase");
}

String remotePassphrase = ConnectDatabaseUtils.getConnectDbEncodedPassphrase(context, false);
String localPassphrase = ConnectDatabaseUtils.getConnectDbEncodedPassphrase(context, true);
DatabaseConnectOpenHelper dbConnectOpenHelper;
if (remotePassphrase != null && remotePassphrase.equals(localPassphrase)) {
//Using the UserSandboxUtils helper method to align with other code
dbConnectOpenHelper = new DatabaseConnectOpenHelper(this.c,
UserSandboxUtils.getSqlCipherEncodedKey(passphrase));
} else {
//LEGACY: Used to open the DB using the byte[], not String overload
Logger.exception("Legacy DB Usage", new Exception("Accessing Connect DB via legacy code"));
dbConnectOpenHelper = new DatabaseConnectOpenHelper(this.c,
Base64.encodeToString(passphrase, Base64.NO_WRAP));
}
connectDatabase = new EncryptedDatabaseAdapter(dbConnectOpenHelper);
connectDatabase = new EncryptedDatabaseAdapter(new DatabaseConnectOpenHelper(
this.c, UserSandboxUtils.getSqlCipherEncodedKey(passphrase)));
} catch (Exception e) {
//Flag the DB as broken if we hit an error opening it (usually means corrupted or bad encryption)
dbBroken = true;
Expand Down
43 changes: 14 additions & 29 deletions app/src/org/commcare/connect/database/ConnectDatabaseUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@
import org.commcare.util.Base64;
import org.commcare.util.Base64DecoderException;
import org.commcare.util.EncryptionUtils;
import org.commcare.utils.CrashUtil;
import org.commcare.utils.EncryptionKeyAndTransform;
import org.commcare.utils.EncryptionKeyProvider;
import org.javarosa.core.services.Logger;
import org.jetbrains.annotations.NotNull;

import java.util.Vector;

public class ConnectDatabaseUtils {
// the value of the key should not be renamed due to backward compatibility
private static final String SECRET_NAME = "secret";
public static void storeConnectDbPassphrase(@NotNull Context context, byte[] passphrase, boolean isLocal) {
public static void storeConnectDbPassphrase(@NotNull Context context, byte[] passphrase) {
try {
if (passphrase == null || passphrase.length == 0) {
throw new IllegalArgumentException("Passphrase must not be null or empty");
Expand All @@ -29,9 +25,9 @@ public static void storeConnectDbPassphrase(@NotNull Context context, byte[] pas
String encoded = EncryptionUtils.encrypt(passphrase, keyAndTransform.getKey(),
keyAndTransform.getTransformation(), true);

ConnectKeyRecord record = getKeyRecord(isLocal);
ConnectKeyRecord record = getKeyRecord();
if (record == null) {
record = new ConnectKeyRecord(encoded, isLocal);
record = new ConnectKeyRecord(encoded);
} else {
record.setEncryptedPassphrase(encoded);
}
Expand All @@ -42,39 +38,28 @@ record = new ConnectKeyRecord(encoded, isLocal);
}
}

public static ConnectKeyRecord getKeyRecord(boolean local) {
Vector<ConnectKeyRecord> records = CommCareApplication.instance()
.getGlobalStorage(ConnectKeyRecord.class)
.getRecordsForValue(ConnectKeyRecord.IS_LOCAL, local);
public static ConnectKeyRecord getKeyRecord() {
Iterable<ConnectKeyRecord> records = CommCareApplication.instance()
.getGlobalStorage(ConnectKeyRecord.class);

return records.size() > 0 ? records.firstElement() : null;
if (records.iterator().hasNext()) {
return records.iterator().next();
}
Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OrangeAndGreen I was thinking that app should ask for local specifically to check that all users are migrated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worried the same but tested and the false case is working fine through an app upgrade

return null;
}

public static void storeConnectDbPassphrase(Context context, String base64EncodedPassphrase, boolean isLocal) {
public static void storeConnectDbPassphrase(Context context, String base64EncodedPassphrase) {
try {
byte[] bytes = Base64.decode(base64EncodedPassphrase);
storeConnectDbPassphrase(context, bytes, isLocal);
storeConnectDbPassphrase(context, bytes);
} catch (Base64DecoderException e) {
throw new RuntimeException(e);
}
}

public static String getConnectDbEncodedPassphrase(Context context, boolean isLocal) {
try {
byte[] passBytes = getConnectDbPassphrase(context, isLocal);
if (passBytes != null) {
return Base64.encode(passBytes);
}
} catch (Exception e) {
Logger.exception("Getting DB passphrase", e);
}

return null;
}

public static byte[] getConnectDbPassphrase(Context context, boolean isLocal) {
public static byte[] getConnectDbPassphrase(Context context) {
try {
ConnectKeyRecord record = ConnectDatabaseUtils.getKeyRecord(isLocal);
ConnectKeyRecord record = ConnectDatabaseUtils.getKeyRecord();
if (record == null) {
return null;
}
Expand Down
9 changes: 0 additions & 9 deletions app/src/org/commcare/connect/network/ApiPersonalId.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,6 @@ public static AuthInfo.TokenAuth retrieveHqTokenSync(Context context, String hqU
throw new TokenUnavailableException();
}


public static void fetchDbPassphrase(Context context, ConnectUserRecord user, IApiCallback callback) {
String url = PersonalIdApiClient.BASE_URL + context.getString(R.string.ConnectFetchDbKeyURL);
ConnectNetworkHelper.get(context,
url,
API_VERSION_PERSONAL_ID, new AuthInfo.ProvidedAuth(user.getUserId(), user.getPassword(), false),
ArrayListMultimap.create(), true, callback);
}

public static void confirmBackupCode(Context context,
String backupCode, String token, IApiCallback callback) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public class CCAnalyticsEvent {
static final String CCC_PAYMENT_CONFIRMATION_DISPLAY = "ccc_payment_confirmation_display";
static final String CCC_PAYMENT_CONFIRMATION_INTERACT = "ccc_payment_confirmation_interact";
static final String CCC_NOTIFICATION_TYPE = "ccc_notification_type";
static final String CCC_REKEYED_DB = "ccc_rekeyed_db";
static final String CCC_BIOMETRIC_INVALIDATED = "ccc_biometric_invalidated";
static final String PERSONAL_ID_CONFIGURATION_FAILURE = "personal_id_configuration_failure";
static final String NAV_DRAWER_OPEN = "nav_drawer_open";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,10 +560,6 @@ public static void reportNotificationType(String notificationType) {
CCAnalyticsParam.NOTIFICATION_TYPE, notificationType);
}

public static void reportRekeyedDatabase() {
reportEvent(CCAnalyticsEvent.CCC_REKEYED_DB);
}

public static void reportBiometricInvalidated() {
reportEvent(CCAnalyticsEvent.CCC_BIOMETRIC_INVALIDATED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,6 @@ public static void deleteDb() {
getDbFile().delete();
}

public static void rekeyDB(IDatabase db, String newPassphrase) throws Base64DecoderException {
if(db != null) {
byte[] newBytes = Base64.decode(newPassphrase);
String newKeyEncoded = UserSandboxUtils.getSqlCipherEncodedKey(newBytes);

db.execSQL("PRAGMA rekey = '" + newKeyEncoded + "';");
db.close();
}
}

@Override
public void onCreate(SQLiteDatabase db) {
IDatabase database = new EncryptedDatabaseAdapter(db);
Expand Down
Loading