Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix][Android] Avoid blocking threads to prevent deadlocks #576

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 72 additions & 43 deletions android/src/main/java/com/oblador/keychain/KeychainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandler;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandlerProvider;
import com.oblador.keychain.decryptionHandler.DecryptionResultListener;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.EmptyParameterException;
import com.oblador.keychain.exceptions.KeyStoreAccessException;
Expand Down Expand Up @@ -290,7 +291,7 @@ protected void getGenericPassword(@NonNull final String alias,
final String rules = getSecurityRulesOrDefault(options);
final PromptInfo promptInfo = getPromptInfo(options);

CipherStorage cipher = null;
final CipherStorage cipher;

// Only check for upgradable ciphers for FacebookConseal as that
// is the only cipher that can be upgraded
Expand All @@ -303,15 +304,34 @@ protected void getGenericPassword(@NonNull final String alias,
cipher = getCipherStorageByName(storageName);
}

final DecryptionResult decryptionResult = decryptCredentials(alias, cipher, resultSet, rules, promptInfo);

final WritableMap credentials = Arguments.createMap();
credentials.putString(Maps.SERVICE, alias);
credentials.putString(Maps.USERNAME, decryptionResult.username);
credentials.putString(Maps.PASSWORD, decryptionResult.password);
credentials.putString(Maps.STORAGE, cipher.getCipherStorageName());

promise.resolve(credentials);
final DecryptionResultListener listener =
new DecryptionResultListener() {
@Override
public void onDecrypt(@NonNull DecryptionResult decryptionResult) {
final WritableMap credentials = Arguments.createMap();
credentials.putString(Maps.SERVICE, alias);
credentials.putString(Maps.USERNAME, decryptionResult.username);
credentials.putString(Maps.PASSWORD, decryptionResult.password);
credentials.putString(Maps.STORAGE, cipher.getCipherStorageName());

promise.resolve(credentials);
}

@Override
public void onError(@NonNull Throwable error) {
Log.e(KEYCHAIN_MODULE, error.getMessage());

if (error instanceof KeyStoreAccessException) {
promise.reject(Errors.E_KEYSTORE_ACCESS_ERROR, error);
} else if (error instanceof CryptoFailedException) {
promise.reject(Errors.E_CRYPTO_FAILED, error);
} else {
promise.reject(Errors.E_UNKNOWN_ERROR, error);
}
}
};

decryptCredentials(alias, cipher, resultSet, rules, promptInfo, listener);
} catch (KeyStoreAccessException e) {
Log.e(KEYCHAIN_MODULE, e.getMessage());

Expand Down Expand Up @@ -632,18 +652,19 @@ private static PromptInfo getPromptInfo(@Nullable final ReadableMap options) {
* Extract credentials from current storage. In case if current storage is not matching
* results set then executed migration.
*/
@NonNull
private DecryptionResult decryptCredentials(@NonNull final String alias,
@NonNull final CipherStorage current,
@NonNull final ResultSet resultSet,
@Rules @NonNull final String rules,
@NonNull final PromptInfo promptInfo)
private void decryptCredentials(@NonNull final String alias,
@NonNull final CipherStorage current,
@NonNull final ResultSet resultSet,
@Rules @NonNull final String rules,
@NonNull final PromptInfo promptInfo,
@NonNull final DecryptionResultListener listener)
throws CryptoFailedException, KeyStoreAccessException {
final String storageName = resultSet.cipherStorageName;

// The encrypted data is encrypted using the current CipherStorage, so we just decrypt and return
if (storageName.equals(current.getCipherStorageName())) {
return decryptToResult(alias, current, resultSet, promptInfo);
decryptToResult(alias, current, resultSet, promptInfo, listener);
return;
}

// The encrypted data is encrypted using an older CipherStorage, so we need to decrypt the data first,
Expand All @@ -653,38 +674,46 @@ private DecryptionResult decryptCredentials(@NonNull final String alias,
throw new KeyStoreAccessException("Wrong cipher storage name '" + storageName + "' or cipher not available");
}

// decrypt using the older cipher storage
final DecryptionResult decryptionResult = decryptToResult(alias, oldStorage, resultSet, promptInfo);

if (Rules.AUTOMATIC_UPGRADE.equals(rules)) {
try {
// encrypt using the current cipher storage
migrateCipherStorage(alias, current, oldStorage, decryptionResult);
} catch (CryptoFailedException e) {
Log.w(KEYCHAIN_MODULE, "Migrating to a less safe storage is not allowed. Keeping the old one");
}
}
final DecryptionResultListener wrappedListener =
new DecryptionResultListener() {
@Override
public void onDecrypt(@NonNull DecryptionResult decryptionResult) {
if (Rules.AUTOMATIC_UPGRADE.equals(rules)) {
try {
// encrypt using the current cipher storage
migrateCipherStorage(alias, current, oldStorage, decryptionResult);
} catch (CryptoFailedException e) {
Log.w(
KEYCHAIN_MODULE,
"Migrating to a less safe storage is not allowed. Keeping the old one");
} catch (Throwable t) {
listener.onError(t);
return;
}
}

listener.onDecrypt(decryptionResult);
}

return decryptionResult;
@Override
public void onError(@NonNull Throwable error) {
listener.onError(error);
}
};

// decrypt using the older cipher storage
decryptToResult(alias, oldStorage, resultSet, promptInfo, wrappedListener);
}

/** Try to decrypt with provided storage. */
@NonNull
private DecryptionResult decryptToResult(@NonNull final String alias,
@NonNull final CipherStorage storage,
@NonNull final ResultSet resultSet,
@NonNull final PromptInfo promptInfo)
private void decryptToResult(@NonNull final String alias,
@NonNull final CipherStorage storage,
@NonNull final ResultSet resultSet,
@NonNull final PromptInfo promptInfo,
@NonNull final DecryptionResultListener listener)
throws CryptoFailedException {
final DecryptionResultHandler handler = getInteractiveHandler(storage, promptInfo);
storage.decrypt(handler, alias, resultSet.username, resultSet.password, SecurityLevel.ANY);

CryptoFailedException.reThrowOnError(handler.getError());

if (null == handler.getResult()) {
throw new CryptoFailedException("No decryption results and no error. Something deeply wrong!");
}

return handler.getResult();
storage.decrypt(handler, alias, resultSet.username, resultSet.password, SecurityLevel.ANY, listener);
}

/** Get instance of handler that resolves access to the keystore on system request. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.oblador.keychain.cipherStorage;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.oblador.keychain.SecurityLevel;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandler;
import com.oblador.keychain.decryptionHandler.DecryptionResultListener;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.KeyStoreAccessException;

Expand Down Expand Up @@ -91,20 +91,13 @@ EncryptionResult encrypt(@NonNull final String alias,
* In case of key stored in weaker security level than required will be raised exception.
* That can happens during migration from one version of library to another.
*/
@NonNull
DecryptionResult decrypt(@NonNull final String alias,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level)
throws CryptoFailedException;

/** Decrypt the credentials but redirect results of operation to handler. */
void decrypt(@NonNull final DecryptionResultHandler handler,
@NonNull final String alias,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level)
throws CryptoFailedException;
@NonNull final SecurityLevel level,
@NonNull final DecryptionResultListener listener)
throws CryptoFailedException;

/** Remove key (by alias) from storage. */
void removeKey(@NonNull final String alias) throws KeyStoreAccessException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.oblador.keychain.KeychainModule.KnownCiphers;
import com.oblador.keychain.SecurityLevel;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandler;
import com.oblador.keychain.decryptionHandler.DecryptionResultListener;
import com.oblador.keychain.exceptions.CryptoFailedException;

import java.security.GeneralSecurityException;
Expand Down Expand Up @@ -94,12 +95,13 @@ public EncryptionResult encrypt(@NonNull final String alias,
}
}

@NonNull
@Override
public DecryptionResult decrypt(@NonNull final String alias,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level)
public void decrypt(@NonNull final DecryptionResultHandler handler,
@NonNull final String alias,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level,
@NonNull final DecryptionResultListener listener)
throws CryptoFailedException {

throwIfInsufficientLevel(level);
Expand All @@ -112,29 +114,14 @@ public DecryptionResult decrypt(@NonNull final String alias,
final byte[] decryptedUsername = crypto.decrypt(username, usernameEntity);
final byte[] decryptedPassword = crypto.decrypt(password, passwordEntity);

return new DecryptionResult(
new String(decryptedUsername, UTF8),
new String(decryptedPassword, UTF8),
SecurityLevel.ANY);
} catch (Throwable fail) {
throw new CryptoFailedException("Decryption failed for alias: " + alias, fail);
}
}

/** redirect call to default {@link #decrypt(String, byte[], byte[], SecurityLevel)} method. */
@Override
public void decrypt(@NonNull DecryptionResultHandler handler,
@NonNull String service,
@NonNull byte[] username,
@NonNull byte[] password,
@NonNull final SecurityLevel level) {

try {
final DecryptionResult results = decrypt(service, username, password, level);

handler.onDecrypt(results, null);
final DecryptionResult result =
new DecryptionResult(
new String(decryptedUsername, UTF8),
new String(decryptedPassword, UTF8),
SecurityLevel.ANY);
listener.onDecrypt(result);
} catch (Throwable fail) {
handler.onDecrypt(null, fail);
listener.onError(new CryptoFailedException("Decryption failed for alias: " + alias, fail));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.oblador.keychain.KeychainModule.KnownCiphers;
import com.oblador.keychain.SecurityLevel;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandler;
import com.oblador.keychain.decryptionHandler.DecryptionResultListener;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.KeyStoreAccessException;

Expand Down Expand Up @@ -127,11 +128,12 @@ public EncryptionResult encrypt(@NonNull final String alias,
}

@Override
@NonNull
public DecryptionResult decrypt(@NonNull final String alias,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level)
public void decrypt(@NonNull final DecryptionResultHandler handler,
Copy link
Collaborator

Choose a reason for hiding this comment

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

is handler not used anymore?

@NonNull final String alias,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level,
@NonNull final DecryptionResultListener listener)
throws CryptoFailedException {

throwIfInsufficientLevel(level);
Expand All @@ -142,31 +144,16 @@ public DecryptionResult decrypt(@NonNull final String alias,
try {
final Key key = extractGeneratedKey(safeAlias, level, retries);

return new DecryptionResult(
decryptBytes(key, username),
decryptBytes(key, password),
getSecurityLevel(key));
final DecryptionResult result =
new DecryptionResult(
decryptBytes(key, username), decryptBytes(key, password), getSecurityLevel(key));
listener.onDecrypt(result);
} catch (GeneralSecurityException e) {
throw new CryptoFailedException("Could not decrypt data with alias: " + alias, e);
} catch (Throwable fail) {
throw new CryptoFailedException("Unknown error with alias: " + alias +
", error: " + fail.getMessage(), fail);
}
}

/** Redirect call to {@link #decrypt(String, byte[], byte[], SecurityLevel)} method. */
@Override
public void decrypt(@NonNull final DecryptionResultHandler handler,
@NonNull final String service,
@NonNull final byte[] username,
@NonNull final byte[] password,
@NonNull final SecurityLevel level) {
try {
final DecryptionResult results = decrypt(service, username, password, level);

handler.onDecrypt(results, null);
listener.onError(new CryptoFailedException("Could not decrypt data with alias: " + alias, e));
} catch (Throwable fail) {
handler.onDecrypt(null, fail);
listener.onError(
new CryptoFailedException(
"Unknown error with alias: " + alias + ", error: " + fail.getMessage(), fail));
}
}
//endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import com.oblador.keychain.KeychainModule;
import com.oblador.keychain.SecurityLevel;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandler;
import com.oblador.keychain.decryptionHandler.DecryptionResultHandlerNonInteractive;
import com.oblador.keychain.decryptionHandler.DecryptionResultListener;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.KeyStoreAccessException;

Expand Down Expand Up @@ -84,33 +84,13 @@ public EncryptionResult encrypt(@NonNull final String alias,
}
}

@NonNull
@Override
public DecryptionResult decrypt(@NonNull String alias,
@NonNull byte[] username,
@NonNull byte[] password,
@NonNull final SecurityLevel level)
throws CryptoFailedException {

final DecryptionResultHandlerNonInteractive handler = new DecryptionResultHandlerNonInteractive();
decrypt(handler, alias, username, password, level);

CryptoFailedException.reThrowOnError(handler.getError());

if (null == handler.getResult()) {
throw new CryptoFailedException("No decryption results and no error. Something deeply wrong!");
}

return handler.getResult();
}

@Override
@SuppressLint("NewApi")
public void decrypt(@NonNull DecryptionResultHandler handler,
public void decrypt(@NonNull final DecryptionResultHandler handler,
@NonNull String alias,
@NonNull byte[] username,
@NonNull byte[] password,
@NonNull final SecurityLevel level)
@NonNull final SecurityLevel level,
@NonNull final DecryptionResultListener listener)
throws CryptoFailedException {

throwIfInsufficientLevel(level);
Expand All @@ -130,18 +110,18 @@ public void decrypt(@NonNull DecryptionResultHandler handler,
decryptBytes(key, password)
);

handler.onDecrypt(results, null);
listener.onDecrypt(results);
} catch (final UserNotAuthenticatedException ex) {
Log.d(LOG_TAG, "Unlock of keystore is needed. Error: " + ex.getMessage(), ex);

// expected that KEY instance is extracted and we caught exception on decryptBytes operation
@SuppressWarnings("ConstantConditions") final DecryptionContext context =
new DecryptionContext(safeAlias, key, password, username);

handler.askAccessPermissions(context);
handler.askAccessPermissions(context, listener);
} catch (final Throwable fail) {
// any other exception treated as a failure
handler.onDecrypt(null, fail);
listener.onError(fail);
}
}

Expand Down