Created
May 7, 2019 14:06
-
-
Save shafty023/142e742e5bd701731ca25b5291fa131f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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