package com.greenaddress.greenbits.ui; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.btchip.BTChipDongle; import com.btchip.BTChipDongle.BTChipPublicKey; import com.btchip.comm.BTChipTransportFactory; import com.btchip.comm.BTChipTransportFactoryCallback; import com.btchip.utils.Dump; import com.btchip.utils.KeyUtils; 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.LoginData; import com.greenaddress.greenapi.LoginFailed; import com.greenaddress.greenapi.Network; import com.greenaddress.greenbits.ui.preferences.NetworkSettingsActivity; import com.greenaddress.greenbits.wallets.BTChipHWWallet; import com.ledger.tbase.comm.LedgerTransportTEEProxy; import com.ledger.tbase.comm.LedgerTransportTEEProxyFactory; import java.util.concurrent.Callable; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; public class FirstScreenActivity extends LoginActivity { private static final String NVM_PATH = "nvm.bin"; private static final String TAG = FirstScreenActivity.class.getSimpleName(); private static boolean tuiCall; private BTChipTransportFactory transportFactory; private static final int CONNECT_TIMEOUT = 2000; @Override protected int getMainViewId() { return R.layout.activity_first_screen; } @Override protected void onCreateWithService(final Bundle savedInstanceState) { UI.mapClick(this, R.id.firstLogInButton, new Intent(this, MnemonicActivity.class)); UI.mapClick(this, R.id.firstSignUpButton, new Intent(this, SignUpActivity.class)); final Uri homepage = Uri.parse("https://greenaddress.it"); UI.mapClick(this, R.id.firstMadeByText, new Intent(Intent.ACTION_VIEW, homepage)); Log.d(TAG, "Create FirstScreenActivity : TUI " + tuiCall); if (tuiCall || (transportFactory != null)) return; // Check if a TEE is supported mService.getExecutor().submit(new Callable<Object>() { @Override public Object call() { transportFactory = new LedgerTransportTEEProxyFactory(getApplicationContext()); final LedgerTransportTEEProxy teeTransport = (LedgerTransportTEEProxy) transportFactory.getTransport(); final byte[] nvm = teeTransport.loadNVM(NVM_PATH); teeTransport.setDebug(BuildConfig.DEBUG); if (nvm != null) { teeTransport.setNVM(nvm); } boolean initialized = false; // Check if the TEE can be connected final LinkedBlockingQueue<Boolean> waitConnected = new LinkedBlockingQueue<>(1); final boolean result = transportFactory.connect(FirstScreenActivity.this, new BTChipTransportFactoryCallback() { @Override public void onConnected(final boolean success) { try { waitConnected.put(success); } catch (final InterruptedException e) { } } }); if (result) { try { initialized = waitConnected.poll(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS); } catch (final InterruptedException e) { } if (initialized) initialized = teeTransport.init(); } Log.d(TAG, "TEE init " + initialized); if (initialized) { final BTChipDongle dongle = new BTChipDongle(teeTransport, true); // Prompt for use (or use immediately if an NVM file exists and the application is ready) // If ok, attempt setup, then verify PIN, then login to gait backend boolean teeReady = false; if (nvm != null) try { final int attempts = dongle.getVerifyPinRemainingAttempts(); teeReady = (attempts != 0); } catch (final Exception e) { } Log.d(TAG, "TEE ready " + teeReady); if (teeReady) proceedTEE(teeTransport, dongle, false); else { FirstScreenActivity.this.runOnUiThread(new Runnable() { public void run() { UI.popup(FirstScreenActivity.this, "Ledger Wallet Trustlet") .content("Ledger Wallet Trustlet is available - do you want to use it to register ?") .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(final MaterialDialog dialog, final DialogAction which) { proceedTEE(teeTransport, dongle, true); } }) .onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(final MaterialDialog dialog, final DialogAction which) { try { teeTransport.close(); } catch (final Exception e) { } } }).build().show(); } }); } } return null; } }); } private void proceedTEE(final LedgerTransportTEEProxy transport, final BTChipDongle dongle, final boolean setup) { mService.getExecutor().submit(new Callable<Object>() { @Override public Object call() { tuiCall = true; BTChipPublicKey masterPublicKey = null, loginPublicKey = null; Log.d(TAG, "TEE setup " + setup); if (setup) { try { // Not setup ? Create the wallet dongle.setup(new BTChipDongle.OperationMode[]{BTChipDongle.OperationMode.WALLET}, new BTChipDongle.Feature[]{BTChipDongle.Feature.RFC6979}, // TEE doesn't need NO_2FA_P2SH Network.NETWORK.getAddressHeader(), Network.NETWORK.getP2SHHeader(), new byte[4], null, null, null, null); // Save the encrypted image transport.writeNVM(NVM_PATH, transport.requestNVM().get()); } catch (final Exception e) { Log.d(TAG, "Setup exception", e); try { transport.close(); } catch (final Exception e1) { } FirstScreenActivity.this.toast("Trustlet setup failed"); tuiCall = false; return null; } // FIXME reopen transport - more stable // (Should not be necessary anyway with the new transport API) /* try { byte[] nvm = transport.requestNVM().get(); transport.close(); transport.setNVM(nvm); transport.init(); } catch(final Exception e) { Log.d(TAG, "Transport reinitialization failed", e); tuiCall = false; return null; } */ } // Verify the PIN try { // TODO : handle terminated PIN Log.d(TAG, "verify pin"); dongle.verifyPin(new byte[4]); Log.d(TAG, "write NVM after verify pin"); transport.writeNVM(NVM_PATH, transport.requestNVM().get()); } catch (final Exception e) { Log.d(TAG, "PIN exception", e); try { transport.writeNVM(NVM_PATH, transport.requestNVM().get()); } catch (final Exception e1) { } try { transport.close(); } catch (final Exception e1) { } FirstScreenActivity.this.toast("Trustlet PIN validation failed"); tuiCall = false; return null; } // If a new key was set up, register it if (setup) { try { masterPublicKey = dongle.getWalletPublicKey(""); loginPublicKey = dongle.getWalletPublicKey("18241'"); Log.d(TAG, "TEE derived MPK " + Dump.dump(masterPublicKey.getPublicKey()) + ' ' + Dump.dump(masterPublicKey.getChainCode())); Log.d(TAG, "TEE derived LPK " + Dump.dump(loginPublicKey.getPublicKey()) + ' ' + Dump.dump(loginPublicKey.getChainCode())); } catch (final Exception e) { FirstScreenActivity.this.toast("Trustlet login failed"); tuiCall = false; return null; } } // And finally login final BTChipPublicKey masterPublicKeyFixed = masterPublicKey; final BTChipPublicKey loginPublicKeyFixed = loginPublicKey; Futures.addCallback(Futures.transform(mService.onConnected, new AsyncFunction<Void, LoginData>() { @Override public ListenableFuture<LoginData> apply(final Void input) throws Exception { if (!setup) { Log.d(TAG, "TEE login"); return mService.login(new BTChipHWWallet(dongle)); } else { Log.d(TAG, "TEE signup"); return mService.signup(new BTChipHWWallet(dongle), KeyUtils.compressPublicKey(masterPublicKeyFixed.getPublicKey()), masterPublicKeyFixed.getChainCode(), KeyUtils.compressPublicKey(loginPublicKeyFixed.getPublicKey()), loginPublicKeyFixed.getChainCode()); } } }), new FutureCallback<LoginData>() { @Override public void onSuccess(final LoginData result) { Log.d(TAG, "Success"); onLoginSuccess(); } @Override public void onFailure(final Throwable t) { Log.d(TAG, "login failed", t); if (!(Throwables.getRootCause(t) instanceof LoginFailed)) finishOnUiThread(); } }); tuiCall = false; return null; } }); } @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.preauth_menu, menu); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. switch (item.getItemId()) { case R.id.watchonly_preference: startActivity(new Intent(FirstScreenActivity.this, WatchOnlyLoginActivity.class)); return true; case R.id.network_preferences: startActivity(new Intent(FirstScreenActivity.this, NetworkSettingsActivity.class)); return true; case R.id.action_settings: return true; } return super.onOptionsItemSelected(item); } @Override public void onResumeWithService() { // Make a note if the user cancelled PIN entry final boolean userCancelled = mService.getUserCancelledPINEntry(); mService.setUserCancelledPINEntry(false); //FIXME : recheck state, properly handle TEE link anyway if (mService.isLoggedIn()) { onLoginSuccess(); } else if (mService.cfg("pin").getString("ident", null) != null && !userCancelled) { startActivity(new Intent(this, PinActivity.class)); finish(); } } }