Skip to content

Instantly share code, notes, and snippets.

@shafty023
Created May 7, 2019 14:06
Show Gist options
  • Save shafty023/142e742e5bd701731ca25b5291fa131f to your computer and use it in GitHub Desktop.
Save shafty023/142e742e5bd701731ca25b5291fa131f to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2016 AirWatch, LLC. All rights reserved.
* This product is protected by copyright and intellectual property laws in
* the United States and other countries as well as by international treaties.
* AirWatch products may be covered by one or more patents listed at
* http://www.vmware.com/go/patents.
*/
package com.boxer.sdk;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import com.airwatch.crypto.MasterKeyManager;
import com.airwatch.crypto.openssl.OpenSSLLoadException;
import com.airwatch.keymanagement.AWKeyUtils;
import com.airwatch.sdk.context.SDKContext;
import com.airwatch.sdk.context.SDKContextException;
import com.airwatch.task.IFutureCallback;
import com.boxer.BuildConfig;
import com.boxer.common.app.AWApplicationWrapper;
import com.boxer.common.crashreport.CrashLogger;
import com.boxer.common.logging.LogUtils;
import com.boxer.common.logging.Logging;
import com.boxer.injection.ObjectGraphController;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.inject.Singleton;
/**
* Class that manages the {@link com.airwatch.sdk.context.SDKContext}.
*
* <p>Provides APIs to initialize the {@link com.airwatch.sdk.context.SDKContext}.
* Also, provides APIs to change the pass key used to initialize the
* {@link com.airwatch.sdk.context.SDKContext}</p>
*/
@Singleton
public class SDKContextManager {
private static final String LOG_TAG = Logging.prependLogTag("SDKContextManager");
/** The constant set when sdk type is not set. */
private static final int INIT_TYPE_NOT_SET = 0;
/**
* The constant set when the sdk is initialized with the passcode returned by
* {@link AWApplicationWrapper#getPassword()}.
*/
private static final int INIT_TYPE_BOXER_DEFAULT_PASSCODE = 1;
/**
* The constant set when the sdk is initialized with the user passcode set in Boxer.
*/
private static final int INIT_TYPE_USER_PASSCODE_SET_IN_BOXER = 2;
/**
* The constant set when the sdk is initialized with the passcode set via SDK's unified pin
* mode.
*/
private static final int INIT_TYPE_WITH_SDK_PASSCODE = 3;
@IntDef({INIT_TYPE_BOXER_DEFAULT_PASSCODE, INIT_TYPE_USER_PASSCODE_SET_IN_BOXER,
INIT_TYPE_WITH_SDK_PASSCODE})
@Retention(RetentionPolicy.SOURCE)
private @interface InitType {}
private final Context context;
private final AWApplicationWrapper awApplicationWrapper;
protected boolean initFailedWithFatalError;
/**
* Indicates the current {@link SDKContext} initialization password.
*
* Must use {@link #setInitializationType(int)} to update and {@link #getInitializationType()} to
* read the current initialization type.
*/
@InitType private int initType = INIT_TYPE_NOT_SET;
/**
* Initialize the SDK context manager.
* @param context Context of the calling application/activity/service.
* @param awAppWrapper the accessor to {@link com.airwatch.app.AWApplication} APIs.
*/
SDKContextManager(@NonNull final Context context,
@NonNull final AWApplicationWrapper awAppWrapper) {
this.context = context;
awApplicationWrapper = awAppWrapper;
try {
if (awApplicationWrapper.hasEP1()) {
initializeInSDKPasscodeMode();
return;
}
initializeWithDefaultPassKey();
} catch (RuntimeException e) {
handleSDKInitializationException(e);
}
/**then try initializing using app-level passcode mode.
but this will be done using {@link #initializeWithUserPasscode(String)} **/
}
private void handleSDKInitializationException(@NonNull final RuntimeException e) {
if (e.getCause() instanceof OpenSSLLoadException) {
initFailedWithFatalError = true;
LogUtils.e(LOG_TAG, e, "Unable to initialize SDK");
} else {
// Rethrow exception
throw e;
}
}
/**
* Returns the initialization type that points to the owner of the passkey that initialized
* the {@link SDKContextManager}.
*
* @return any initialization type from InitType.
* @throws IllegalStateException if called when SDKContext is not initialized.
*/
@InitType private synchronized int getInitializationType() {
if (!isInitialized()) {
throw new IllegalStateException("Need to initialize SDK first!");
}
if (initType == INIT_TYPE_NOT_SET) {
throw new IllegalStateException("Init type is not set while the sdk is initialized!");
}
return initType;
}
/**
* Sets the way the SDK is initialized.
*
* <p>
* Note: Setting initialization type as SDK via {@link #initializationHandledBySDK()} will
* <b>NOT</b> guarantee the internal state to change to SDK. SDK Passcode might be disabled
* which will result in {@link #isInitializedWithDefaultPasscode()} to return {@code true}, if
* called consecutively.
* </p>
*
* @param initType initialization type from InitType.
* @throws IllegalStateException if called when SDKContext is not initialized.
*/
private synchronized void setInitializationType(@InitType int initType) {
if (!isInitialized()) {
throw new IllegalStateException("Need to initialize SDK first!");
}
int currentInitType = this.initType;
if (initType == INIT_TYPE_WITH_SDK_PASSCODE) {
if (isPasscodeValid(awApplicationWrapper.getPassword())) {
initType = INIT_TYPE_BOXER_DEFAULT_PASSCODE;
}
}
if (currentInitType != initType) {
String initAction = (currentInitType == INIT_TYPE_NOT_SET) ? "set" : "updated";
LogUtils.i(LOG_TAG, "Initialization type is %s to '%s'",
initAction, stringifyInitType(initType));
this.initType = initType;
}
}
/**
* Returns {@code true} if SDK is currently initialized with obfuscated key.
*/
public boolean isInitializedWithDefaultPasscode() {
return getInitializationType() == INIT_TYPE_BOXER_DEFAULT_PASSCODE;
}
/**
* Returns {@code true} if SDK is currently initialized with a user defined passcode.
*/
public boolean isInitializedWithUserPassKey() {
return getInitializationType() == INIT_TYPE_USER_PASSCODE_SET_IN_BOXER;
}
/**
* Returns {@code true} if SDK is currently initialized with a sdk defined passcode.
*/
public boolean isInitializedWithSDKPassKey() {
return getInitializationType() == INIT_TYPE_WITH_SDK_PASSCODE;
}
/**
* Updates the initialization type to SDK passcode.
*/
public void initializationHandledBySDK() {
setInitializationType(INIT_TYPE_WITH_SDK_PASSCODE);
}
/**
* Compares the current initialization key with the provided pass key.
*
* @param pass the pass to compare
* @return {@code true} if sdk is initialized with given pass.
*/
private boolean isPasscodeValid(@NonNull byte[] pass) {
return getSdkContext().getKeyManager().isPassCodeValid(pass);
}
/**
*
* Converts the initialization constants to meaningful states.
*
* <p><b>For debugging and logging purposes only.</b></p>
*
* @param initWith init type.
* @return String representation of init type.
*/
private String stringifyInitType(@InitType int initWith) {
String type = "";
switch (initWith) {
case INIT_TYPE_NOT_SET:
type = "NOT_SET";
break;
case INIT_TYPE_BOXER_DEFAULT_PASSCODE:
type = "DEFAULT";
break;
case INIT_TYPE_USER_PASSCODE_SET_IN_BOXER:
type = "USER_PASSCODE";
break;
case INIT_TYPE_WITH_SDK_PASSCODE:
type = "SDK_PASSCODE";
}
return type;
}
/**
* Initializes the SDK when boxer is using SDK's passcode settings.
*/
@VisibleForTesting synchronized void initializeInSDKPasscodeMode() {
awApplicationWrapper.init().on(new IFutureCallback<Boolean>() {
@Override
public void onFailure(Exception exception) {
LogUtils.e(LOG_TAG, exception, "Unable to initialize SDK");
awApplicationWrapper.notifyPinInputRequired();
}
@Override
public void onSuccess(Boolean result) {
if (result) {
setInitializationType(INIT_TYPE_WITH_SDK_PASSCODE);
} else {
awApplicationWrapper.notifyPinInputRequired();
}
}
});
}
/**
* Returns the initialized {@link SDKContext}.
*/
@NonNull public synchronized SDKContext getSdkContext() {
// Never cache this(say in a member variable), since, depending on the state of the SDK, the
// SDK's context implementation returned could be different.
return com.airwatch.sdk.context.SDKContextManager.getSDKContext();
}
/**
* Initialize the {@link SDKContext} with given user passcode.
*
* <p>The key used to initialize the {@link SDKContext} is derived from the user passcode if
* user pin encryption is turned on.</p>
*
* @param passcode User passcode.
* @return {@code true} if {@link SDKContext} is initialized correctly, else {@code false}.
*/
@WorkerThread
public synchronized boolean initializeWithUserPasscode(@NonNull final char[] passcode) {
if (BuildConfig.DEBUG && isUsingSDKPasscodeMode()) {
throw new IllegalStateException("Cannot initialize with user passcode when using " +
"shared passcode mode");
}
byte[] passKey = AWKeyUtils.generateAwUniqueUidV4(context, passcode);
if (!initializeWithUserPassKey(passKey)) {
// fallback API that uses default UTF decoder
passKey = AWKeyUtils.generateAwUniqueUidV4Legacy_DefaultEncoder(context, passcode)
.getBytes(StandardCharsets.UTF_8);
if (!initializeSDKContextWithPassKey(passKey)) {
//fallback API that uses pre-P UTF decoder
passKey = AWKeyUtils.generateAwUniqueUidV4Legacy_OreoEncoder(context, passcode)
.getBytes(StandardCharsets.UTF_8);
if (!initializeSDKContextWithPassKey(passKey)) {
// Unable to initialize sdk
return false;
}
}
//Update the passcode from fallback API
getSdkContext().updatePasscode(context, passKey,
AWKeyUtils.generateAwUniqueUidV4(context, passcode));
}
return initializeWithUserPassKey(passKey);
}
/**
* Initialize the {@link SDKContext} with the user-key.<p/>
*
* <p>While {@link #initializeWithUserPasscode(char[])} generates a key from user
* passcode, this method directly requests the key. Note: silently rotates the key if a new
* passKey is passed. </p>
*
* @param passKey the key to initialize the SDK Context with.
* @return {@code true} if {@link SDKContext} is initialized correctly, else {@code false}.
*/
@WorkerThread
/*package*/ synchronized boolean initializeWithUserPassKey(@NonNull byte[] passKey) {
boolean success = true;
if (isInitialized()) {
// SDK Context is already initialized, rotate instead.
if (!isPasscodeValid(passKey)) {
success = rotateToNewUserKey(passKey);
}
} else {
success = initializeSDKContextWithPassKey(passKey);
}
if (success) {
setInitializationType(INIT_TYPE_USER_PASSCODE_SET_IN_BOXER);
}
return success;
}
/**
* Returns {@code true} if initialization attempt with obfuscated key succeeds.
*/
@WorkerThread
@VisibleForTesting synchronized boolean initializeWithDefaultPassKey() {
if (!isInitialized()) {
byte[] defaultPassKey = awApplicationWrapper.getPassword();
boolean initSuccess = initializeSDKContextWithPassKey(defaultPassKey);
if (initSuccess) {
setInitializationType(INIT_TYPE_BOXER_DEFAULT_PASSCODE);
}
return initSuccess;
}
return true;
}
/**
* Initializes the SDK with a given passkey.
*
* <p>This is either the obfuscated pass key if user pin encryption is not enabled, or
* a pass key generated from the user passcode if user pin encryption is enabled.</p>
*
* @param passKey pass key used to initialize the {@link SDKContext}.
* @return {@code true} if {@link SDKContext} is initialized correctly, else {@code false}
*/
@WorkerThread
/*package*/ synchronized boolean initializeSDKContextWithPassKey(@NonNull final byte[] passKey) {
if (!isInitialized()) {
try {
getSdkContext().init(context, passKey);
} catch (SDKContextException e) {
LogUtils.e(LOG_TAG, "Failed to initialize SDKContext");
// LockedPasscodeManager#checkPasscode() will try to initialize sdk with the
// provided passcode from the user. We call AWKeyUtils.generateAwUniqueUidV4()
// which saves the entered passcode to alarmManager even if it is the wrong passcode.
// Therefore, clear the passcode from the alarmManager if sdk init failed when using
// pin based encryption.
if (!Arrays.equals(awApplicationWrapper.getPassword(), passKey)) {
clearPassKey();
}
return false;
}
}
return true;
}
@WorkerThread
public boolean rotateToAppsDefaultPasscode() {
int previousInitState = getInitializationType();
byte[] defaultAppPasscode = awApplicationWrapper.getPassword();
if (previousInitState == INIT_TYPE_BOXER_DEFAULT_PASSCODE) {
LogUtils.i(LOG_TAG, "Skipping key rotation to default app passcode because it is " +
"not necessary..");
return true;
}
boolean success = rotateToNewUserKey(defaultAppPasscode);
if (!success) {
throw new IllegalStateException("Rotation to app default passcode failed!");
}
// necessary clean up after rotation
if (previousInitState == INIT_TYPE_USER_PASSCODE_SET_IN_BOXER) {
clearPassKey();
} else if (previousInitState == INIT_TYPE_WITH_SDK_PASSCODE) {
awApplicationWrapper.clearToken();
}
setInitializationType(INIT_TYPE_BOXER_DEFAULT_PASSCODE);
return true;
}
/**
* @see com.airwatch.crypto.MasterKeyManager#rotate(byte[])
*/
@WorkerThread
private synchronized boolean rotateToNewUserKey(@NonNull final byte[] newKey) {
if (!isInitialized()) {
throw new IllegalStateException("SDK needs to be initialized before rotation!");
}
// Initialization type should be set by the caller!
return getSdkContext().getKeyManager().rotate(newKey);
}
/**
* Checks whether SDK Context is initialized correctly.
* <p>It also leaves detailed error breadcrumb if the the crash reporting is allowed.</p>
* <p>
* Returns {@code true} if SDK context was initialized correctly, else {@code false}.
*/
public synchronized boolean isInitialized() {
final boolean isInitialized = getSdkContext().getCurrentState() != SDKContext.State.IDLE
&& getSdkContext().getKeyManager() != null && getSdkContext().getKeyManager()
.hasDk();
if (!isInitialized) {
final CrashLogger crashLogger = ObjectGraphController.getObjectGraph().getCrashLogger();
crashLogger.leaveBreadcrumb("SDKContext is not initialized correctly with errors: " +
getSDKContextNotInitializedErrorString(getSdkContext()));
}
return isInitialized;
}
/**
* Returns the detailed errors due to which SDK Context is not initialized.
*/
@VisibleForTesting
synchronized String getSDKContextNotInitializedErrorString(
@NonNull final SDKContext sdkContext) {
final StringBuilder errors = new StringBuilder();
if (sdkContext.getCurrentState() == SDKContext.State.IDLE) {
errors.append("SDKContext state is IDLE.\n");
}
final MasterKeyManager masterKeyManager = sdkContext.getKeyManager();
if (masterKeyManager == null) {
errors.append("SDKContext's MasterKeyManager is null.\n");
} else if (!masterKeyManager.hasDk()) {
errors.append("SDKContext's MasterKeyManager doesn't have derived key.\n");
}
return errors.toString();
}
/**
* Returns {@code true} if SDK initialization failed with a fatal error.
*/
public boolean initFailedWithFatalError() {
return initFailedWithFatalError;
}
/**
* Returns {@code true} if the SDK has cached the pass key, else {@code false}.
*/
public boolean hasPassKey() {
return AWKeyUtils.hasAwUniqueUidV4(context);
}
/**
* Removes the cached pass key from the SDK.
*/
public void clearPassKey() {
AWKeyUtils.clearAwUniqueUidV4(context);
}
private boolean isUsingSDKPasscodeMode() {
return ObjectGraphController.getObjectGraph().getInsecurePreferences()
.getPasscodeMode() == PasscodeModeConstants.PASSCODE_MODE_SDK_DEFINED;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment