/* * Copyright (C) 2017 Vincent Breitmoser * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.sufficientlysecure.keychain.ui.transfer.presenter; import java.io.IOException; import java.net.URISyntaxException; import java.util.List; import android.content.Context; import android.graphics.Bitmap; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.support.annotation.RequiresApi; import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; import org.openintents.openpgp.util.OpenPgpUtils; import org.openintents.openpgp.util.OpenPgpUtils.UserId; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing; import org.sufficientlysecure.keychain.network.KeyTransferInteractor; import org.sufficientlysecure.keychain.network.KeyTransferInteractor.KeyTransferCallback; import org.sufficientlysecure.keychain.operations.results.ImportKeyResult; import org.sufficientlysecure.keychain.operations.results.OperationResult; import org.sufficientlysecure.keychain.pgp.UncachedKeyRing; import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; import org.sufficientlysecure.keychain.provider.KeyRepository; import org.sufficientlysecure.keychain.provider.KeyRepository.NotFoundException; import org.sufficientlysecure.keychain.service.ImportKeyringParcel; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper.Callback; import org.sufficientlysecure.keychain.ui.transfer.loader.SecretKeyLoader.SecretKeyItem; import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.OnClickImportKeyListener; import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyAdapter; import org.sufficientlysecure.keychain.ui.transfer.view.ReceivedSecretKeyList.ReceivedKeyItem; import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.OnClickTransferKeyListener; import org.sufficientlysecure.keychain.ui.transfer.view.TransferSecretKeyList.TransferKeyAdapter; import org.sufficientlysecure.keychain.ui.util.QrCodeUtils; import org.sufficientlysecure.keychain.util.Log; @RequiresApi(api = VERSION_CODES.LOLLIPOP) public class TransferPresenter implements KeyTransferCallback, LoaderCallbacks>, OnClickTransferKeyListener, OnClickImportKeyListener { private static final String DELIMITER_START = "-----BEGIN PGP PRIVATE KEY BLOCK-----"; private static final String DELIMITER_END = "-----END PGP PRIVATE KEY BLOCK-----"; private static final String BACKSTACK_TAG_TRANSFER = "transfer"; private final Context context; private final TransferMvpView view; private final LoaderManager loaderManager; private final int loaderId; private final KeyRepository databaseInteractor; private final TransferKeyAdapter secretKeyAdapter; private final ReceivedKeyAdapter receivedKeyAdapter; private KeyTransferInteractor keyTransferClientInteractor; private KeyTransferInteractor keyTransferServerInteractor; private boolean wasConnected = false; private boolean sentData = false; private boolean waitingForWifi = false; private Long confirmingMasterKeyId; public TransferPresenter(Context context, LoaderManager loaderManager, int loaderId, TransferMvpView view) { this.context = context; this.view = view; this.loaderManager = loaderManager; this.loaderId = loaderId; this.databaseInteractor = KeyRepository.createDatabaseInteractor(context); secretKeyAdapter = new TransferKeyAdapter(context, LayoutInflater.from(context), this); view.setSecretKeyAdapter(secretKeyAdapter); receivedKeyAdapter = new ReceivedKeyAdapter(context, LayoutInflater.from(context), this); view.setReceivedKeyAdapter(receivedKeyAdapter); } public void onUiInitFromIntentUri(final Uri initUri) { connectionStartConnect(initUri.toString()); } public void onUiStart() { loaderManager.restartLoader(loaderId, null, this); if (keyTransferServerInteractor == null && keyTransferClientInteractor == null && !wasConnected) { checkWifiResetAndStartListen(); } } public void onUiStop() { connectionClear(); if (wasConnected) { view.showViewDisconnected(); view.dismissConfirmationIfExists(); secretKeyAdapter.setAllDisabled(true); } } public void onUiClickScan() { connectionClear(); view.scanQrCode(); } public void onUiClickScanAgain() { onUiClickScan(); } public void onUiClickDone() { view.finishFragmentOrActivity(); } public void onUiQrCodeScanned(String qrCodeContent) { connectionStartConnect(qrCodeContent); } public void onUiBackStackPop() { if (wasConnected) { checkWifiResetAndStartListen(); } } @Override public void onUiClickTransferKey(long masterKeyId) { if (sentData) { prepareAndSendKey(masterKeyId); } else { confirmingMasterKeyId = masterKeyId; view.showConfirmSendDialog(); } } public void onUiClickConfirmSend() { if (confirmingMasterKeyId == null) { return; } long masterKeyId = confirmingMasterKeyId; confirmingMasterKeyId = null; prepareAndSendKey(masterKeyId); } @Override public void onUiClickImportKey(final long masterKeyId, String keyData) { receivedKeyAdapter.focusItem(masterKeyId); final ImportKeyringParcel importKeyringParcel = ImportKeyringParcel.createImportKeyringParcel( ParcelableKeyRing.createFromEncodedBytes(keyData.getBytes())); CryptoOperationHelper op = view.createCryptoOperationHelper(new Callback() { @Override public ImportKeyringParcel createOperationInput() { return importKeyringParcel; } @Override public void onCryptoOperationSuccess(ImportKeyResult result) { receivedKeyAdapter.focusItem(null); receivedKeyAdapter.addToFinishedItems(masterKeyId); view.releaseCryptoOperationHelper(); view.showResultNotification(result); } @Override public void onCryptoOperationCancelled() { view.releaseCryptoOperationHelper(); receivedKeyAdapter.focusItem(null); } @Override public void onCryptoOperationError(ImportKeyResult result) { receivedKeyAdapter.focusItem(null); view.releaseCryptoOperationHelper(); view.showResultNotification(result); } @Override public boolean onCryptoSetProgress(String msg, int progress, int max) { return false; } }); op.cryptoOperation(); } public void onWifiConnected() { if (waitingForWifi) { resetAndStartListen(); } } @Override public void onServerStarted(String qrCodeData) { Bitmap qrCodeBitmap = QrCodeUtils.getQRCodeBitmap(Uri.parse(qrCodeData)); view.setQrImage(qrCodeBitmap); } @Override public void onConnectionEstablished(String otherName) { wasConnected = true; secretKeyAdapter.clearFinishedItems(); secretKeyAdapter.focusItem(null); secretKeyAdapter.setAllDisabled(false); receivedKeyAdapter.clear(); view.showConnectionEstablished(otherName); view.setShowDoneIcon(true); view.addFakeBackStackItem(BACKSTACK_TAG_TRANSFER); } @Override public void onConnectionLost() { if (!wasConnected) { checkWifiResetAndStartListen(); view.showErrorConnectionFailed(); } else { connectionClear(); view.dismissConfirmationIfExists(); view.showViewDisconnected(); secretKeyAdapter.setAllDisabled(true); } } @Override public void onDataReceivedOk(String receivedData) { if (sentData) { Log.d(Constants.TAG, "received data, but we already sent a key! race condition, or other side misbehaving?"); return; } Log.d(Constants.TAG, "received data"); UncachedKeyRing uncachedKeyRing; try { uncachedKeyRing = UncachedKeyRing.decodeFromData(receivedData.getBytes()); } catch (PgpGeneralException | IOException | RuntimeException e) { Log.e(Constants.TAG, "error parsing incoming key", e); view.showErrorBadKey(); return; } String primaryUserId = uncachedKeyRing.getPublicKey().getPrimaryUserIdWithFallback(); UserId userId = OpenPgpUtils.splitUserId(primaryUserId); ReceivedKeyItem receivedKeyItem = new ReceivedKeyItem(receivedData, uncachedKeyRing.getMasterKeyId(), uncachedKeyRing.getCreationTime(), userId.name, userId.email); receivedKeyAdapter.addItem(receivedKeyItem); view.showReceivingKeys(); } @Override public void onDataSentOk(String passthrough) { Log.d(Constants.TAG, "data sent ok!"); final long masterKeyId = Long.parseLong(passthrough); new Handler().postDelayed(new Runnable() { @Override public void run() { secretKeyAdapter.focusItem(null); secretKeyAdapter.addToFinishedItems(masterKeyId); } }, 750); } @Override public void onConnectionErrorConnect() { view.showWaitingForConnection(); view.showErrorConnectionFailed(); resetAndStartListen(); } @Override public void onConnectionErrorNoRouteToHost(String wifiSsid) { connectionClear(); String ownWifiSsid = getConnectedWifiSsid(); if (!wifiSsid.equalsIgnoreCase(ownWifiSsid)) { view.showWifiError(wifiSsid); } else { view.showWaitingForConnection(); view.showErrorConnectionFailed(); resetAndStartListen(); } } @Override public void onConnectionErrorListen() { view.showErrorListenFailed(); } @Override public void onConnectionError(String errorMessage) { view.showErrorConnectionError(errorMessage); connectionClear(); if (wasConnected) { view.showViewDisconnected(); secretKeyAdapter.setAllDisabled(true); } } private void connectionStartConnect(String qrCodeContent) { connectionClear(); view.showEstablishingConnection(); keyTransferClientInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END); try { keyTransferClientInteractor.connectToServer(qrCodeContent, TransferPresenter.this); } catch (URISyntaxException e) { view.showErrorConnectionFailed(); } } private void checkWifiResetAndStartListen() { if (!isWifiConnected()) { waitingForWifi = true; view.showNotOnWifi(); return; } resetAndStartListen(); } private void resetAndStartListen() { waitingForWifi = false; wasConnected = false; sentData = false; connectionClear(); String wifiSsid = getConnectedWifiSsid(); keyTransferServerInteractor = new KeyTransferInteractor(DELIMITER_START, DELIMITER_END); keyTransferServerInteractor.startServer(this, wifiSsid); view.showWaitingForConnection(); view.setShowDoneIcon(false); } private boolean isWifiConnected() { ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo wifiNetwork = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); return wifiNetwork.isConnected(); } private String getConnectedWifiSsid() { WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); if (wifiManager == null) { return null; } WifiInfo info = wifiManager.getConnectionInfo(); if (info == null) { return null; } // getSSID will return the ssid in quotes if it is valid utf-8. we only return it in that case. String ssid = info.getSSID(); if (ssid.charAt(0) != '"') { return null; } return ssid.substring(1, ssid.length() -1); } private void connectionClear() { if (keyTransferServerInteractor != null) { keyTransferServerInteractor.closeConnection(); keyTransferServerInteractor = null; } if (keyTransferClientInteractor != null) { keyTransferClientInteractor.closeConnection(); keyTransferClientInteractor = null; } } private void prepareAndSendKey(long masterKeyId) { try { byte[] armoredSecretKey = databaseInteractor.getSecretKeyRingAsArmoredData(masterKeyId); secretKeyAdapter.focusItem(masterKeyId); connectionSend(armoredSecretKey, Long.toString(masterKeyId)); } catch (IOException | NotFoundException | PgpGeneralException e) { // TODO e.printStackTrace(); } } private void connectionSend(byte[] armoredSecretKey, String passthrough) { sentData = true; if (keyTransferClientInteractor != null) { keyTransferClientInteractor.sendData(armoredSecretKey, passthrough); } else if (keyTransferServerInteractor != null) { keyTransferServerInteractor.sendData(armoredSecretKey, passthrough); } } @Override public Loader> onCreateLoader(int id, Bundle args) { return secretKeyAdapter.createLoader(context); } @Override public void onLoadFinished(Loader> loader, List data) { secretKeyAdapter.setData(data); view.setShowSecretKeyEmptyView(data.isEmpty()); } @Override public void onLoaderReset(Loader> loader) { secretKeyAdapter.setData(null); } public interface TransferMvpView { void showNotOnWifi(); void showWaitingForConnection(); void showEstablishingConnection(); void showConnectionEstablished(String hostname); void showWifiError(String wifiSsid); void showReceivingKeys(); void showViewDisconnected(); void scanQrCode(); void setQrImage(Bitmap qrCode); void releaseCryptoOperationHelper(); void showErrorBadKey(); void showErrorConnectionFailed(); void showErrorListenFailed(); void showErrorConnectionError(String errorMessage); void showResultNotification(ImportKeyResult result); void setShowDoneIcon(boolean showDoneIcon); void setSecretKeyAdapter(Adapter adapter); void setShowSecretKeyEmptyView(boolean isEmpty); void setReceivedKeyAdapter(Adapter secretKeyAdapter); CryptoOperationHelper createCryptoOperationHelper(Callback callback); void addFakeBackStackItem(String tag); void finishFragmentOrActivity(); void showConfirmSendDialog(); void dismissConfirmationIfExists(); } }