Skip to content

Commit

Permalink
Fix #127: Implement secure password on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
hvge committed Oct 25, 2022
1 parent b7777c4 commit 60e6183
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,12 @@ class Constants {
* Time interval in milliseconds to keep pre-authorized biometric key in memory.
*/
static final int BIOMETRY_KEY_KEEP_ALIVE_TIME = 10_000;
/**
* Time interval in milliseconds to keep password object valid in memory.
*/
static final int PASSWORD_KEY_KEEP_ALIVE_TIME = 5 * 60 * 1_000;
/**
* Upper limit for Unicode Code Point.
*/
static final int CODEPOINT_MAX = 0x10FFFF;
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.getlime.security.powerauth.core.Password;
import io.getlime.security.powerauth.exception.PowerAuthErrorException;

/**
Expand Down Expand Up @@ -670,7 +671,7 @@ WritableMap debugDump() {
// Note: This is not very accurate, but we're using this only for the debugging purposes
final long bootTime = System.currentTimeMillis() - currentTime();
map.putString("id", key);
map.putString("class", object.getClass().getSimpleName());
map.putString("class", object.managedInstance().getClass().getSimpleName());
map.putArray("policies", debugPolicies);
map.putBoolean("isValid", isStillValid());
if (tag != null) {
Expand Down Expand Up @@ -712,6 +713,8 @@ void debugCommand(String command, ReadableMap options, Promise promise) {
objectClass = byte[].class;
} else if ("number".equals(objectType)) {
objectClass = Integer.class;
} else if ("password".equals(objectType)) {
objectClass = Password.class;
}
if ("create".equals(command)) {
// The "create" command creates a new instance of managed object
Expand Down Expand Up @@ -744,6 +747,8 @@ void debugCommand(String command, ReadableMap options, Promise promise) {
instance = ManagedAny.wrap("SECURE-DATA".getBytes(StandardCharsets.UTF_8));
} else if ("number".equals(objectType)) {
instance = ManagedAny.wrap(42);
} else if ("password".equals(objectType)) {
instance = ManagedAny.wrap(new Password(), Password::destroy);
}
if (instance != null) {
promise.resolve(registerObject(instance, objectTag, policies));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
import androidx.fragment.app.FragmentActivity;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;

Expand Down Expand Up @@ -61,14 +63,15 @@

@SuppressWarnings("unused")
@ReactModule(name = "PowerAuth")
public class PowerAuthRNModule extends ReactContextBaseJavaModule {
public class PowerAuthModule extends ReactContextBaseJavaModule {

private final ReactApplicationContext context;
private ObjectRegister objectRegister;
private final ObjectRegister objectRegister;

public PowerAuthRNModule(ReactApplicationContext context) {
public PowerAuthModule(ReactApplicationContext context, @NonNull ObjectRegister objectRegister) {
super(context);
this.context = context;
this.objectRegister = objectRegister;
}

// React integration
Expand All @@ -79,28 +82,6 @@ public String getName() {
return "PowerAuth";
}

@Override
public void initialize() {
super.initialize();
objectRegister = context.getNativeModule(ObjectRegister.class);
if (objectRegister == null) {
throw new IllegalStateException("PowerAuthObjectRegister not found");
}
}

@Override
public void invalidate() {
super.invalidate();
objectRegister = null;
}

ObjectRegister getObjectRegister() {
if (objectRegister == null) {
throw new IllegalStateException("PowerAuthObjectRegister is not set");
}
return objectRegister;
}

@ReactMethod
public void isConfigured(@Nonnull String instanceId, final Promise promise) {
try {
Expand All @@ -125,7 +106,7 @@ public void configure(final String instanceId, final ReadableMap configuration,
final PowerAuthSDK instance = new PowerAuthSDK.Builder(paConfig)
.clientConfiguration(paClientConfig)
.keychainConfiguration(paKeychainConfig)
.build(PowerAuthRNModule.this.context);
.build(PowerAuthModule.this.context);
return ManagedAny.wrap(instance);

});
Expand Down Expand Up @@ -552,71 +533,67 @@ public void run(@NonNull PowerAuthSDK sdk) {
}

@ReactMethod
public void unsafeChangePassword(String instanceId, final String oldPassword, final String newPassword, final Promise promise) {
this.usePowerAuth(instanceId, promise, new PowerAuthBlock() {
@Override
public void run(@NonNull PowerAuthSDK sdk) {
promise.resolve(sdk.changePasswordUnsafe(oldPassword, newPassword));
}
public void unsafeChangePassword(String instanceId, final Dynamic oldPassword, final Dynamic newPassword, final Promise promise) {
this.usePowerAuth(instanceId, promise, sdk -> {
final Password coreOldPassword = resolvePassword(oldPassword);
final Password coreNewPassword = resolvePassword(newPassword);
promise.resolve(sdk.changePasswordUnsafe(coreOldPassword, coreNewPassword));
});
}

@ReactMethod
public void changePassword(String instanceId, final String oldPassword, final String newPassword, final Promise promise) {
public void changePassword(String instanceId, final Dynamic oldPassword, final Dynamic newPassword, final Promise promise) {
final Context context = this.context;
this.usePowerAuth(instanceId, promise, new PowerAuthBlock() {
@Override
public void run(@NonNull PowerAuthSDK sdk) {
sdk.changePassword(context, oldPassword, newPassword, new IChangePasswordListener() {
@Override
public void onPasswordChangeSucceed() {
promise.resolve(null);
}
this.usePowerAuth(instanceId, promise, sdk -> {
final Password coreOldPassword = resolvePassword(oldPassword);
final Password coreNewPassword = resolvePassword(newPassword);
sdk.changePassword(context, coreOldPassword, coreNewPassword, new IChangePasswordListener() {
@Override
public void onPasswordChangeSucceed() {
promise.resolve(null);
}

@Override
public void onPasswordChangeFailed(@NonNull Throwable t) {
Errors.rejectPromise(promise, t);
}
});
}
@Override
public void onPasswordChangeFailed(@NonNull Throwable t) {
Errors.rejectPromise(promise, t);
}
});
});
}

@ReactMethod
public void addBiometryFactor(String instanceId, final String password, final String title, final String description, final Promise promise) {
public void addBiometryFactor(String instanceId, final Dynamic password, final String title, final String description, final Promise promise) {
final Context context = this.context;
this.usePowerAuthOnMainThread(instanceId, promise, new PowerAuthBlock() {
@Override
public void run(@NonNull PowerAuthSDK sdk) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
final FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
if (fragmentActivity == null) {
throw new IllegalStateException("Current fragment activity is not available");
}
sdk.addBiometryFactor(
context,
fragmentActivity,
title,
description,
password,
new IAddBiometryFactorListener() {
@Override
public void onAddBiometryFactorSucceed() {
promise.resolve(null);
}

@Override
public void onAddBiometryFactorFailed(@NonNull PowerAuthErrorException error) {
Errors.rejectPromise(promise, error);
}
});
} catch (Exception e) {
Errors.rejectPromise(promise, e);
this.usePowerAuthOnMainThread(instanceId, promise, sdk -> {
final Password corePassword = resolvePassword(password);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
final FragmentActivity fragmentActivity = (FragmentActivity) getCurrentActivity();
if (fragmentActivity == null) {
throw new IllegalStateException("Current fragment activity is not available");
}
} else {
promise.reject(Errors.EC_REACT_NATIVE_ERROR, "Biometry not supported on this android version.");
sdk.addBiometryFactor(
context,
fragmentActivity,
title,
description,
corePassword,
new IAddBiometryFactorListener() {
@Override
public void onAddBiometryFactorSucceed() {
promise.resolve(null);
}

@Override
public void onAddBiometryFactorFailed(@NonNull PowerAuthErrorException error) {
Errors.rejectPromise(promise, error);
}
});
} catch (Exception e) {
Errors.rejectPromise(promise, e);
}
} else {
promise.reject(Errors.EC_REACT_NATIVE_ERROR, "Biometry not supported on this android version.");
}
});
}
Expand Down Expand Up @@ -741,23 +718,21 @@ public void onDataSignedFailed(@NonNull Throwable t) {
}

@ReactMethod
public void validatePassword(String instanceId, final String password, final Promise promise) {
public void validatePassword(String instanceId, final Dynamic password, final Promise promise) {
final Context context = this.context;
this.usePowerAuth(instanceId, promise, new PowerAuthBlock() {
@Override
public void run(@NonNull PowerAuthSDK sdk) {
sdk.validatePassword(context, password, new IValidatePasswordListener() {
@Override
public void onPasswordValid() {
promise.resolve(null);
}
this.usePowerAuth(instanceId, promise, sdk -> {
final Password corePassword = resolvePassword(password);
sdk.validatePassword(context, corePassword, new IValidatePasswordListener() {
@Override
public void onPasswordValid() {
promise.resolve(null);
}

@Override
public void onPasswordValidationFailed(@NonNull Throwable t) {
Errors.rejectPromise(promise, t);
}
});
}
@Override
public void onPasswordValidationFailed(@NonNull Throwable t) {
Errors.rejectPromise(promise, t);
}
});
});
}

Expand Down Expand Up @@ -852,7 +827,7 @@ public void onBiometricDialogSuccess(@NonNull BiometricKeyData biometricKeyData)
final List<ReleasePolicy> releasePolicies = makeReusable
? Collections.singletonList(ReleasePolicy.keepAlive(Constants.BIOMETRY_KEY_KEEP_ALIVE_TIME))
: Arrays.asList(ReleasePolicy.afterUse(1), ReleasePolicy.expire(Constants.BIOMETRY_KEY_KEEP_ALIVE_TIME));
final String managedId = getObjectRegister().registerObject(managedBytes, instanceId, releasePolicies);
final String managedId = objectRegister.registerObject(managedBytes, instanceId, releasePolicies);
promise.resolve(managedId);
}

Expand Down Expand Up @@ -1116,14 +1091,19 @@ private PowerAuthAuthentication constructAuthentication(ReadableMap map, boolean
final String biometryKeyId = map.getString("biometryKeyId");
final byte[] biometryKey;
if (biometryKeyId != null) {
biometryKey = getObjectRegister().useObject(biometryKeyId, byte[].class);
biometryKey = objectRegister.useObject(biometryKeyId, byte[].class);
if (biometryKey == null) {
throw new WrapperException(Errors.EC_INVALID_NATIVE_OBJECT, "Biometric key in PowerAuthAuthentication object is no longer valid.");
}
} else {
biometryKey = null;
}
final String userPassword = map.getString("userPassword");
final Password userPassword;
if (map.hasKey("userPassword")) {
userPassword = resolvePassword(map.getDynamic("userPassword"));
} else {
userPassword = null;
}
if (forCommit) {
// Authentication for activation commit
if (userPassword == null) {
Expand Down Expand Up @@ -1152,9 +1132,35 @@ private PowerAuthAuthentication constructAuthentication(ReadableMap map, boolean
}
}

// Error constants reported back to RN


/**
* Function translate dynamic object type into core Password object.
* @param anyPassword Dynamic object representing a password.
* @return Resolved core password.
* @throws WrapperException In case that Password cannot be created.
*/
@NonNull
private Password resolvePassword(@Nullable Dynamic anyPassword) throws WrapperException {
if (anyPassword != null) {
if (anyPassword.getType() == ReadableType.String) {
// Direct string was provided
return new Password(anyPassword.asString());
}
if (anyPassword.getType() == ReadableType.Map) {
// Object is provided
final ReadableMap map = anyPassword.asMap();
final String passwordObjectId = map.getString("passwordObjectId");
if (passwordObjectId == null) {
throw new WrapperException(Errors.EC_INVALID_NATIVE_OBJECT, "PowerAuthPassword is not initialized");
}
Password password = objectRegister.useObject(passwordObjectId, Password.class);
if (password == null) {
throw new WrapperException(Errors.EC_INVALID_NATIVE_OBJECT, "PowerAuthPassword object is no longer valid");
}
return password;
}
}
throw new WrapperException(Errors.EC_WRONG_PARAMETER, "PowerAuthPassword or string is required");
}

// PowerAuthBlock instance

Expand Down Expand Up @@ -1209,26 +1215,23 @@ public void run() {

@Nullable
private PowerAuthSDK getPowerAuthInstance(String instanceId) throws PowerAuthErrorException {
final ObjectRegister register = getObjectRegister();
if (!register.isValidObjectId(instanceId)) {
if (!objectRegister.isValidObjectId(instanceId)) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "Instance identifier is missing or empty or forbidden string");
}
return register.findObject(instanceId, PowerAuthSDK.class);
return objectRegister.findObject(instanceId, PowerAuthSDK.class);
}

private void unregisterPowerAuthInstance(String instanceId) throws PowerAuthErrorException {
final ObjectRegister register = getObjectRegister();
if (!register.isValidObjectId(instanceId)) {
if (!objectRegister.isValidObjectId(instanceId)) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "Instance identifier is missing or empty or forbidden string");
}
register.removeAllObjectsWithTag(instanceId);
objectRegister.removeAllObjectsWithTag(instanceId);
}

private boolean registerPowerAuthInstance(String instanceId, ObjectRegister.ObjectFactory factory) throws Throwable {
final ObjectRegister register = getObjectRegister();
if (!register.isValidObjectId(instanceId)) {
if (!objectRegister.isValidObjectId(instanceId)) {
throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "Instance identifier is missing or empty or forbidden string");
}
return register.registerObjectWithId(instanceId, instanceId, Collections.singletonList(ReleasePolicy.manual()), factory);
return objectRegister.registerObjectWithId(instanceId, instanceId, Collections.singletonList(ReleasePolicy.manual()), factory);
}
}

0 comments on commit 60e6183

Please sign in to comment.