diff --git a/app/assets/locales/android_translatable_strings.txt b/app/assets/locales/android_translatable_strings.txt index 3af7a7ff77..b5af54a02c 100644 --- a/app/assets/locales/android_translatable_strings.txt +++ b/app/assets/locales/android_translatable_strings.txt @@ -196,11 +196,12 @@ post.generic.error=An error occurred while preparing the HTTP request post.dialog.title=Claiming... post.dialog.body=Claiming chosen data from server post.io.error=Error reading server response: ${0} -post.unknown.response=Received unknown resonse code (${0}) from server +post.unknown.response=Received unknown response code (${0}) from server post.client.error=Client-side error (code ${0}) received from network request. post.server.error=Server-side error (code ${0}) received from network request. post.gone.error=Case is not present on server. post.conflict.error=You have already claimed this case. +post.cache.encryption.key.error=Encryption error while processing server response: ${0} version.id.long=CommCare Android, version "${0}"(${1}). App v${5}. CommCare Version ${2}. Build ${3}, built on: ${4} version.id.short=CCDroid:"${0}"(${1}). v${5} CC${2}b[${3}] on ${4} @@ -263,6 +264,7 @@ app.handled.error.explanation=CommCare will now restart. You may need to correct app.key.request.message=Another android application has requested the ability to communicate securely with CommCare. This application will be able to pass information to CommCare and trigger actions (submission, login, etc). Do you want to grant access? app.key.request.grant=Grant Access app.key.request.deny=Deny Access +app.key.request.encryption.key.error=Error during encryption Key generation key.manage.title=Logging in key.manage.start=Logging in diff --git a/app/src/org/commcare/activities/InstallFromListActivity.java b/app/src/org/commcare/activities/InstallFromListActivity.java index f776b5259f..91de17ae95 100644 --- a/app/src/org/commcare/activities/InstallFromListActivity.java +++ b/app/src/org/commcare/activities/InstallFromListActivity.java @@ -32,6 +32,7 @@ import org.commcare.preferences.GlobalPrivilegesManager; import org.commcare.tasks.ModernHttpTask; import org.commcare.tasks.templates.CommCareTaskConnector; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.ConnectivityStatus; import org.commcare.views.UserfacingErrorHandling; @@ -355,6 +356,13 @@ public void handleIOException(IOException exception) { repeatRequestOrShowResults(true, false); } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + Logger.log(LogTypes.TYPE_ERROR_ENCRYPTION_KEY, + "An ENcryptionKeyException was encountered when pocessing available apps request: " + exception.getMessage()); + repeatRequestOrShowResults(true, false); + } + private void handleRequestError(int responseCode, boolean couldBeUserError) { Logger.log(LogTypes.TYPE_ERROR_SERVER_COMMS, "Request to " + urlCurrentlyRequestingFrom + " in get available apps request " + diff --git a/app/src/org/commcare/activities/KeyAccessRequestActivity.java b/app/src/org/commcare/activities/KeyAccessRequestActivity.java index 4a993dee1e..05dcb15288 100644 --- a/app/src/org/commcare/activities/KeyAccessRequestActivity.java +++ b/app/src/org/commcare/activities/KeyAccessRequestActivity.java @@ -4,12 +4,16 @@ import android.os.Bundle; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import org.commcare.CommCareApplication; import org.commcare.android.database.global.models.AndroidSharedKeyRecord; import org.commcare.dalvik.R; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.views.ManagedUi; import org.commcare.views.UiElement; +import org.javarosa.core.services.Logger; +import org.javarosa.core.services.locale.Localization; import androidx.appcompat.app.AppCompatActivity; @@ -34,7 +38,14 @@ protected void onCreate(Bundle savedInstanceState) { grantButton.setOnClickListener(v -> { Intent response = new Intent(getIntent()); - AndroidSharedKeyRecord record = AndroidSharedKeyRecord.generateNewSharingKey(); + AndroidSharedKeyRecord record = null; + try { + record = AndroidSharedKeyRecord.generateNewSharingKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + Toast.makeText(this, Localization.get("app.key.request.encryption.key.error"), Toast.LENGTH_LONG).show(); + Logger.exception("Exception while generating encryption key ", e); + return; + } CommCareApplication.instance().getGlobalStorage(AndroidSharedKeyRecord.class).write(record); record.writeResponseToIntent(response); setResult(AppCompatActivity.RESULT_OK, response); diff --git a/app/src/org/commcare/activities/PostRequestActivity.java b/app/src/org/commcare/activities/PostRequestActivity.java index 82d32563a3..a001f22965 100644 --- a/app/src/org/commcare/activities/PostRequestActivity.java +++ b/app/src/org/commcare/activities/PostRequestActivity.java @@ -19,6 +19,7 @@ import org.commcare.tasks.DataPullTask; import org.commcare.tasks.ModernHttpTask; import org.commcare.tasks.templates.CommCareTaskConnector; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.views.ManagedUi; import org.commcare.views.UiElement; import org.commcare.views.dialogs.CustomProgressDialog; @@ -214,6 +215,11 @@ public void handleIOException(IOException exception) { } } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + enterErrorState(Localization.get("post.cache.encryption.key.error", exception.getMessage())); + } + @Override public void onBackPressed() { if (inErrorState) { diff --git a/app/src/org/commcare/activities/QueryRequestActivity.java b/app/src/org/commcare/activities/QueryRequestActivity.java index c82d51958f..c21d660681 100644 --- a/app/src/org/commcare/activities/QueryRequestActivity.java +++ b/app/src/org/commcare/activities/QueryRequestActivity.java @@ -23,6 +23,7 @@ import org.commcare.session.RemoteQuerySessionManager; import org.commcare.tasks.ModernHttpTask; import org.commcare.tasks.templates.CommCareTaskConnector; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.utils.SessionRegistrationHelper; import org.commcare.utils.SessionUnavailableException; import org.commcare.views.UserfacingErrorHandling; @@ -196,6 +197,11 @@ public void handleIOException(IOException exception) { } } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + enterErrorState(Localization.get("post.cache.encryption.key.error", exception.getMessage())); + } + @Override public CustomProgressDialog generateProgressDialog(int taskId) { String title, message; diff --git a/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java b/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java index 4e62fbc811..1487090e20 100755 --- a/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java +++ b/app/src/org/commcare/android/database/global/models/AndroidSharedKeyRecord.java @@ -9,6 +9,7 @@ import org.commcare.models.framework.Persisting; import org.commcare.modern.database.Table; import org.commcare.modern.models.MetaField; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.services.Logger; import org.javarosa.core.util.PropertyUtils; @@ -48,7 +49,8 @@ public AndroidSharedKeyRecord(String keyId, byte[] privateKey, byte[] publicKey) this.publicKey = publicKey; } - public static AndroidSharedKeyRecord generateNewSharingKey() { + public static AndroidSharedKeyRecord generateNewSharingKey() + throws EncryptionKeyHelper.EncryptionKeyException { KeyPair pair = CryptUtil.generateRandomKeyPair(512); byte[] encodedPrivate = pair.getPrivate().getEncoded(); byte[] encodedPublic = pair.getPublic().getEncoded(); diff --git a/app/src/org/commcare/models/database/user/DemoUserBuilder.java b/app/src/org/commcare/models/database/user/DemoUserBuilder.java index 415de2f964..f90cc69317 100644 --- a/app/src/org/commcare/models/database/user/DemoUserBuilder.java +++ b/app/src/org/commcare/models/database/user/DemoUserBuilder.java @@ -11,6 +11,7 @@ import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.core.encryption.CryptUtil; import org.commcare.models.encryption.ByteEncrypter; +import org.commcare.util.EncryptionKeyHelper; import org.javarosa.core.model.User; import org.javarosa.core.util.PropertyUtils; @@ -65,8 +66,11 @@ private void createAndWriteKeyRecordAndUser() { int userCount = keyRecordDB.getIDsForValue(UserKeyRecord.META_USERNAME, username).size(); if (userCount == 0) { - SecretKey secretKey = CryptUtil.generateRandomSecretKey(); - if (secretKey == null) { + + SecretKey secretKey; + try { + secretKey = CryptUtil.generateRandomSecretKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { throw new RuntimeException("Error setting up user's encrypted storage"); } randomKey = secretKey.getEncoded(); diff --git a/app/src/org/commcare/network/GetAndParseActor.java b/app/src/org/commcare/network/GetAndParseActor.java index 7f23ab1958..f82948e87d 100644 --- a/app/src/org/commcare/network/GetAndParseActor.java +++ b/app/src/org/commcare/network/GetAndParseActor.java @@ -12,6 +12,7 @@ import org.commcare.core.network.AuthInfo; import org.commcare.core.network.AuthenticationInterceptor; import org.commcare.core.network.ModernHttpRequester; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.services.Logger; @@ -108,6 +109,13 @@ public void handleIOException(IOException exception) { } } + @Override + public void handleEncryptionKeyException(EncryptionKeyHelper.EncryptionKeyException exception) { + Logger.log(LogTypes.TYPE_ERROR_ENCRYPTION_KEY, + String.format("Encountered EncryptionKeyException while getting response stream for %s response: %s", + requestName, exception.getMessage())); + } + private void processErrorResponse(int responseCode) { Logger.log(LogTypes.TYPE_ERROR_SERVER_COMMS, String.format("Received error response from %s request: %s", requestName, responseCode)); diff --git a/app/src/org/commcare/network/HttpCalloutTask.java b/app/src/org/commcare/network/HttpCalloutTask.java index 4592aaeddb..cb4d21e543 100755 --- a/app/src/org/commcare/network/HttpCalloutTask.java +++ b/app/src/org/commcare/network/HttpCalloutTask.java @@ -10,6 +10,7 @@ import org.commcare.data.xml.DataModelPullParser; import org.commcare.data.xml.TransactionParserFactory; import org.commcare.tasks.templates.CommCareTask; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.io.StreamsUtil; @@ -43,7 +44,7 @@ public enum HttpCalloutOutcomes { NetworkFailureBadPassword, IncorrectPin, AuthOverHttp, - CaptivePortal + ResponseCacheError, CaptivePortal } private final Context c; @@ -99,6 +100,9 @@ protected HttpCalloutOutcomes doTaskBackground(Object... params) { //This is probably related to local files, actually e.printStackTrace(); outcome = HttpCalloutOutcomes.NetworkFailure; + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + e.printStackTrace(); + outcome = HttpCalloutOutcomes.ResponseCacheError; } //If we needed the callout to succeed and it didn't, return our failure. @@ -131,7 +135,8 @@ protected HttpCalloutOutcomes doSetupTaskBeforeRequest() { protected abstract Response doHttpRequest() throws IOException; - protected HttpCalloutOutcomes doResponseSuccess(Response response) throws IOException { + protected HttpCalloutOutcomes doResponseSuccess(Response response) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { beginResponseHandling(response); InputStream input = cacheResponseOpenHandle(response); @@ -162,7 +167,8 @@ protected HttpCalloutOutcomes doResponseSuccess(Response response) protected abstract TransactionParserFactory getTransactionParserFactory(); - protected InputStream cacheResponseOpenHandle(Response response) throws IOException { + protected InputStream cacheResponseOpenHandle(Response response) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { long dataSizeGuess = ModernHttpRequester.getContentLength(response); BitCache cache = BitCacheFactory.getCache(new AndroidCacheDirSetup(c), dataSizeGuess); diff --git a/app/src/org/commcare/network/RemoteDataPullResponse.java b/app/src/org/commcare/network/RemoteDataPullResponse.java index 6981596917..c6f0336581 100644 --- a/app/src/org/commcare/network/RemoteDataPullResponse.java +++ b/app/src/org/commcare/network/RemoteDataPullResponse.java @@ -7,6 +7,7 @@ import org.commcare.tasks.DataPullTask; import org.commcare.core.network.bitcache.BitCache; import org.commcare.core.network.bitcache.BitCacheFactory; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.io.StreamsUtil; @@ -49,7 +50,8 @@ protected RemoteDataPullResponse(DataPullTask task, * * @throws IOException If there is an issue reading or writing the response. */ - public BitCache writeResponseToCache(Context c) throws IOException { + public BitCache writeResponseToCache(Context c) + throws IOException, EncryptionKeyHelper.EncryptionKeyException { BitCache cache = null; try (InputStream input = getInputStream()) { final long dataSizeGuess = ModernHttpRequester.getContentLength(response); diff --git a/app/src/org/commcare/tasks/AsyncRestoreHelper.java b/app/src/org/commcare/tasks/AsyncRestoreHelper.java index 555c33ac2f..19299907a7 100644 --- a/app/src/org/commcare/tasks/AsyncRestoreHelper.java +++ b/app/src/org/commcare/tasks/AsyncRestoreHelper.java @@ -1,6 +1,7 @@ package org.commcare.tasks; import org.commcare.network.RemoteDataPullResponse; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.javarosa.core.io.StreamsUtil; import org.javarosa.core.services.Logger; @@ -74,7 +75,7 @@ private boolean parseProgressFromRetryResult(RemoteDataPullResponse response) { } eventType = parser.next(); } while (eventType != KXmlParser.END_DOCUMENT); - } catch (IOException | XmlPullParserException e) { + } catch (IOException | XmlPullParserException | EncryptionKeyHelper.EncryptionKeyException e) { Logger.log(LogTypes.TYPE_USER, "Error while parsing progress values of retry result"); } finally { diff --git a/app/src/org/commcare/tasks/DataPullTask.java b/app/src/org/commcare/tasks/DataPullTask.java index 180cb5a04c..57a8257f67 100644 --- a/app/src/org/commcare/tasks/DataPullTask.java +++ b/app/src/org/commcare/tasks/DataPullTask.java @@ -34,6 +34,7 @@ import org.commcare.services.CommCareSessionService; import org.commcare.sync.ExternalDataUpdateHelper; import org.commcare.tasks.templates.CommCareTask; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.util.LogTypes; import org.commcare.utils.FormSaveUtil; import org.commcare.utils.SessionUnavailableException; @@ -223,8 +224,10 @@ private byte[] getEncryptionKey() { private void initUKRForLogin() { if (blockRemoteKeyManagement || shouldGenerateFirstKey()) { - SecretKey newKey = CryptUtil.generateRandomSecretKey(); - if (newKey == null) { + SecretKey newKey = null; + try { + newKey = CryptUtil.generateRandomSecretKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { return; } String sandboxId = PropertyUtils.genUUID().replace("-", ""); @@ -294,6 +297,9 @@ private ResultAndError getRequestResultOrRetry(AndroidTransactio } catch (UnknownSyncError e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to Unknown Error|" + e.getMessage()); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + e.printStackTrace(); + Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to Cache encryption Error|" + e.getMessage()); } wipeLoginIfItOccurred(); @@ -305,8 +311,9 @@ private ResultAndError getRequestResultOrRetry(AndroidTransactio * @return the proper result, or null if we have not yet been able to determine the result to * return */ - private ResultAndError makeRequestAndHandleResponse(AndroidTransactionParserFactory factory) - throws IOException, UnknownSyncError { + private ResultAndError makeRequestAndHandleResponse( + AndroidTransactionParserFactory factory) + throws IOException, UnknownSyncError, EncryptionKeyHelper.EncryptionKeyException { RemoteDataPullResponse pullResponse = dataPullRequester.makeDataPullRequest(this, requestor, server, !loginNeeded, skipFixtures); @@ -352,7 +359,7 @@ private ResultAndError handleAuthFailed() { */ private ResultAndError handleSuccessResponseCode( RemoteDataPullResponse pullResponse, AndroidTransactionParserFactory factory) - throws IOException, UnknownSyncError { + throws IOException, UnknownSyncError, EncryptionKeyHelper.EncryptionKeyException { asyncRestoreHelper.completeServerProgressBarIfShowing(); handleLoginNeededOnSuccess(); @@ -541,6 +548,10 @@ private Pair recover(CommcareRequestEndpoints requestor, Androi //Ok, well, we're bailing here, but we didn't make any changes Logger.log(LogTypes.TYPE_USER, "Sync Recovery Failed due to IOException|" + e.getMessage()); return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, ""); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + e.printStackTrace(); + Logger.log(LogTypes.TYPE_USER, "Sync Recovery Failed due to Cache encryption error|" + e.getMessage()); + return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, ""); } this.publishProgress(PROGRESS_RECOVERY_STARTED); diff --git a/app/src/org/commcare/tasks/ModernHttpTask.java b/app/src/org/commcare/tasks/ModernHttpTask.java index 20d87623da..ce8cf425ab 100644 --- a/app/src/org/commcare/tasks/ModernHttpTask.java +++ b/app/src/org/commcare/tasks/ModernHttpTask.java @@ -12,9 +12,11 @@ import org.commcare.core.network.HTTPMethod; import org.commcare.core.network.ModernHttpRequester; import org.commcare.tasks.templates.CommCareTask; +import org.commcare.util.EncryptionKeyHelper; import java.io.IOException; import java.io.InputStream; +import java.security.Key; import java.util.HashMap; import javax.annotation.Nullable; @@ -36,7 +38,7 @@ public class ModernHttpTask private final ModernHttpRequester requester; private InputStream responseDataStream; - private IOException mException; + private Exception mException; private Response mResponse; // Use for GET request @@ -72,7 +74,7 @@ protected Void doTaskBackground(Void... params) { if (mResponse.isSuccessful()) { responseDataStream = requester.getResponseStream(mResponse); } - } catch (IOException e) { + } catch (IOException | EncryptionKeyHelper.EncryptionKeyException e) { mException = e; } return null; @@ -83,7 +85,11 @@ protected void deliverResult(HttpResponseProcessor httpResponseProcessor, Void result) { if (mException != null) { - httpResponseProcessor.handleIOException(mException); + if (mException instanceof IOException ioExcep) { + httpResponseProcessor.handleIOException(ioExcep); + } else if (mException instanceof EncryptionKeyHelper.EncryptionKeyException encryptKeyExcep) { + httpResponseProcessor.handleEncryptionKeyException(encryptKeyExcep); + } } else { // route to appropriate callback based on http response code ModernHttpRequester.processResponse( diff --git a/app/src/org/commcare/utils/TemplatePrinterUtils.java b/app/src/org/commcare/utils/TemplatePrinterUtils.java index cf29bee5bf..359bdb0c3a 100755 --- a/app/src/org/commcare/utils/TemplatePrinterUtils.java +++ b/app/src/org/commcare/utils/TemplatePrinterUtils.java @@ -6,6 +6,7 @@ import org.commcare.android.javarosa.IntentCallout; import org.commcare.core.encryption.CryptUtil; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.views.dialogs.StandardAlertDialog; import java.io.BufferedReader; @@ -38,7 +39,18 @@ public abstract class TemplatePrinterUtils { private static final String FORMAT_REGEX_WITH_DELIMITER = "((?<=%2$s)|(?=%1$s))"; - private static final SecretKey KEY = CryptUtil.generateRandomSecretKey(); + private static final SecretKey KEY; + + static { + SecretKey secretKey = null; + try { + secretKey = CryptUtil.generateRandomSecretKey(); + } catch (EncryptionKeyHelper.EncryptionKeyException e) { + secretKey = null; + } finally{ + KEY = secretKey; + } + } /** * Concatenate all Strings in a String array to one String. diff --git a/app/unit-tests/src/org/commcare/CommCareTestApplication.java b/app/unit-tests/src/org/commcare/CommCareTestApplication.java index 76cc4d8f48..70240c0446 100644 --- a/app/unit-tests/src/org/commcare/CommCareTestApplication.java +++ b/app/unit-tests/src/org/commcare/CommCareTestApplication.java @@ -26,6 +26,7 @@ import org.commcare.network.DataPullRequester; import org.commcare.network.LocalReferencePullResponseFactory; import org.commcare.services.CommCareSessionService; +import org.commcare.util.EncryptionKeyHelper; import org.commcare.utils.AndroidCacheDirSetup; import org.javarosa.core.model.User; import org.javarosa.core.reference.ReferenceManager; @@ -211,7 +212,11 @@ public void startUserSession(byte[] symetricKey, UserKeyRecord record, boolean r } if (user != null) { user.setCachedPwd(cachedUserPassword); - user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateRandomSecretKey().getEncoded(), cachedUserPassword)); + try { + user.setWrappedKey(ByteEncrypter.wrapByteArrayWithString(CryptUtil.generateRandomSecretKey().getEncoded(), cachedUserPassword)); + } catch (EncryptionKeyHelper.EncryptionKeyException e){ + throw new RuntimeException(e); + } } ccService.startSession(user, record); CommCareApplication.instance().setTestingService(ccService);