package com.greenaddress.greenbits.ui;
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.security.keystore.KeyProperties;
import android.text.TextUtils;
import android.util.Base64;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import com.dd.CircularProgressButton;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.greenaddress.greenapi.GAException;
import com.greenaddress.greenapi.LoginData;
import com.greenaddress.greenapi.LoginFailed;
import com.greenaddress.greenbits.GaService;
import com.greenaddress.greenbits.ui.preferences.NetworkSettingsActivity;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Observable;
import java.util.Observer;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
public class PinActivity extends LoginActivity implements Observer {
private Menu mMenu;
private static final String KEYSTORE_KEY = "NativeAndroidAuth";
private static final int ACTIVITY_REQUEST_CODE = 1;
private CircularProgressButton mPinLoginButton;
private EditText mPinText;
private TextView mPinError;
private void login() {
if (mPinLoginButton.getProgress() != 0)
return;
if (mPinText.length() < 4) {
shortToast(R.string.pinErrorLength);
return;
}
if (!mService.isConnected()) {
toast(R.string.err_send_not_connected_will_resume);
return;
}
mPinLoginButton.setProgress(50);
mPinText.setEnabled(false);
hideKeyboardFrom(mPinText);
setUpLogin(UI.getText(mPinText), new Runnable() {
public void run() {
UI.clear(mPinText);
mPinLoginButton.setProgress(0);
UI.enable(mPinText);
UI.show(mPinError);
final int counter = mService.cfg("pin").getInt("counter", 1);
mPinError.setText(getString(R.string.attemptsLeft, 3 - counter));
}
});
}
private void setUpLogin(final String pin, final Runnable onFailureFn) {
final AsyncFunction<Void, LoginData> connectToLogin = new AsyncFunction<Void, LoginData>() {
@Override
public ListenableFuture<LoginData> apply(final Void input) throws Exception {
return mService.pinLogin(pin);
}
};
final ListenableFuture<LoginData> loginFuture;
loginFuture = Futures.transform(mService.onConnected, connectToLogin, mService.getExecutor());
Futures.addCallback(loginFuture, new FutureCallback<LoginData>() {
@Override
public void onSuccess(final LoginData result) {
mService.cfgEdit("pin").putInt("counter", 0).apply();
if (getCallingActivity() == null) {
onLoginSuccess();
return;
}
setResult(RESULT_OK);
finishOnUiThread();
}
@Override
public void onFailure(final Throwable t) {
final String message;
final SharedPreferences prefs = mService.cfg("pin");
final int counter = prefs.getInt("counter", 0) + 1;
final Throwable error;
if (t instanceof GAException ||
Throwables.getRootCause(t) instanceof LoginFailed) {
final SharedPreferences.Editor editor = prefs.edit();
if (counter < 3) {
editor.putInt("counter", counter);
message = getString(R.string.attemptsLeftLong, 3 - counter);
} else {
message = getString(R.string.attemptsFinished);
editor.clear();
}
editor.apply();
error = null;
}
else {
error = t;
message = null;
}
PinActivity.this.runOnUiThread(new Runnable() {
public void run() {
if (error != null)
PinActivity.this.toast(error);
else
PinActivity.this.toast(message);
if (counter >= 3) {
startActivity(new Intent(PinActivity.this, FirstScreenActivity.class));
finish();
return;
}
if (onFailureFn != null)
onFailureFn.run();
}
});
}
}, mService.getExecutor());
}
@Override
protected void onCreateWithService(final Bundle savedInstanceState) {
final SharedPreferences prefs = mService.cfg("pin");
final String ident = prefs.getString("ident", null);
if (ident == null) {
startActivity(new Intent(this, FirstScreenActivity.class));
finish();
return;
}
setContentView(R.layout.activity_pin);
mPinLoginButton = UI.find(this, R.id.pinLoginButton);
mPinLoginButton.setIndeterminateProgressMode(true);
mPinText = UI.find(this, R.id.pinText);
mPinError = UI.find(this, R.id.pinErrorText);
final String nativePIN = prefs.getString("native", null);
if (TextUtils.isEmpty(nativePIN)) {
mPinText.setOnEditorActionListener(
UI.getListenerRunOnEnter(new Runnable() {
public void run() {
login();
}
}));
mPinLoginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
login();
}
});
} else {
mPinText.setEnabled(false);
mPinLoginButton.setProgress(50);
tryDecrypt();
}
}
@TargetApi(Build.VERSION_CODES.M)
private Cipher getAESCipher() throws NoSuchAlgorithmException, NoSuchPaddingException {
final String name = KeyProperties.KEY_ALGORITHM_AES + '/' +
KeyProperties.BLOCK_MODE_CBC + '/' +
KeyProperties.ENCRYPTION_PADDING_PKCS7;
return Cipher.getInstance(name);
}
@TargetApi(Build.VERSION_CODES.M)
private void tryDecrypt() {
if (mService.onConnected == null) {
toast(R.string.unable_to_connect_to_service);
finishOnUiThread();
return;
}
final SharedPreferences prefs = mService.cfg("pin");
final String nativePIN = prefs.getString("native", null);
final String nativeIV = prefs.getString("nativeiv", null);
try {
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
final SecretKey secretKey = (SecretKey) keyStore.getKey(KEYSTORE_KEY, null);
final Cipher cipher = getAESCipher();
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(Base64.decode(nativeIV, Base64.NO_WRAP)));
final byte[] decrypted = cipher.doFinal(Base64.decode(nativePIN, Base64.NO_WRAP));
final String pin = Base64.encodeToString(decrypted, Base64.NO_WRAP).substring(0, 15);
Futures.addCallback(mService.onConnected, new FutureCallback<Void>() {
@Override
public void onSuccess(final Void result) {
if (mService.isConnected()) {
setUpLogin(pin, null);
return;
}
// try again
tryDecrypt();
}
@Override
public void onFailure(final Throwable t) {
finishOnUiThread();
}
});
} catch (final KeyStoreException | InvalidKeyException e) {
showAuthenticationScreen();
} catch (final InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException |
CertificateException | UnrecoverableKeyException | IOException |
NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (requestCode == ACTIVITY_REQUEST_CODE) {
// Challenge completed, proceed with using cipher
if (resultCode == RESULT_OK) {
tryDecrypt();
} else {
// The user canceled or didn’t complete the lock screen
// operation. Go back to the initial login screen to allow
// them to enter mnemonics.
mService.setUserCancelledPINEntry(true);
startActivity(new Intent(this, FirstScreenActivity.class));
finish();
}
}
}
@TargetApi(Build.VERSION_CODES.M)
private void showAuthenticationScreen() {
final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
final Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
if (intent != null) {
startActivityForResult(intent, ACTIVITY_REQUEST_CODE);
}
}
@Override
public void onResumeWithService() {
mService.addConnectionObserver(this);
super.onResumeWithService();
}
@Override
public void onPauseWithService() {
mService.deleteConnectionObserver(this);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.common_menu, menu);
getMenuInflater().inflate(R.menu.preauth_menu, menu);
mMenu = menu;
final boolean connected = mService != null && mService.isConnected();
setMenuItemVisible(mMenu, R.id.network_unavailable, !connected);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch(item.getItemId()) {
case R.id.network_unavailable:
return true;
case R.id.network_preferences:
startActivity(new Intent(this, NetworkSettingsActivity.class));
return true;
case R.id.watchonly_preference:
startActivity(new Intent(PinActivity.this, WatchOnlyLoginActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void update(final Observable observable, final Object data) {
final GaService.State state = (GaService.State) data;
setMenuItemVisible(mMenu, R.id.network_unavailable,
!state.isConnected() && !state.isLoggedOrLoggingIn());
}
}