From e46b2138159a059202d387020f205ba54875b1db Mon Sep 17 00:00:00 2001 From: JunZhang Date: Tue, 9 Jun 2020 17:17:47 +0800 Subject: [PATCH] add fee attack checking (#32) --- .../java/com/cobo/cold/DataRepository.java | 4 + .../main/java/com/cobo/cold/db/dao/TxDao.java | 3 + .../cobo/cold/ui/fragment/BaseFragment.java | 4 + .../ui/fragment/main/FeeAttackChecking.java | 82 +++++++++++++++++++ .../fragment/main/PsbtSignedTxFragment.java | 3 +- .../ui/fragment/main/TxConfirmFragment.java | 19 +++++ .../cold/ui/fragment/main/TxFragment.java | 12 ++- .../main/electrum/SignedTxFragment.java | 11 ++- .../main/electrum/UnsignedTxFragment.java | 22 +++++ .../cold/viewmodel/TxConfirmViewModel.java | 39 ++++++++- .../main/res/navigation/nav_graph_main.xml | 24 +++++- app/src/main/res/values-zh-rCN/strings.xml | 5 ++ app/src/main/res/values/strings.xml | 4 + 13 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java diff --git a/app/src/main/java/com/cobo/cold/DataRepository.java b/app/src/main/java/com/cobo/cold/DataRepository.java index bb852ae..e2a022d 100644 --- a/app/src/main/java/com/cobo/cold/DataRepository.java +++ b/app/src/main/java/com/cobo/cold/DataRepository.java @@ -140,6 +140,10 @@ public class DataRepository { return mDb.txDao().loadWasabiTxsSync(coinId); } + public List loadAllTxSync(String coinId) { + return mDb.txDao().loadTxsSync(coinId); + } + public LiveData loadTx(String txId) { return mDb.txDao().load(txId); } diff --git a/app/src/main/java/com/cobo/cold/db/dao/TxDao.java b/app/src/main/java/com/cobo/cold/db/dao/TxDao.java index ca16ba1..fadd677 100644 --- a/app/src/main/java/com/cobo/cold/db/dao/TxDao.java +++ b/app/src/main/java/com/cobo/cold/db/dao/TxDao.java @@ -39,6 +39,9 @@ public interface TxDao { @Query("SELECT * FROM txs where coinId = :coinId and signId = 'wasabi_sign_id' ORDER BY timeStamp DESC") List loadWasabiTxsSync(String coinId); + @Query("SELECT * FROM txs where coinId = :coinId") + List loadTxsSync(String coinId); + @Insert(onConflict = OnConflictStrategy.REPLACE) void insert(TxEntity tx); diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java index d72bb7b..a6c3c01 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/BaseFragment.java @@ -146,5 +146,9 @@ public abstract class BaseFragment extends Fragment { public void navigate(@IdRes int id, Bundle data) { NavHostFragment.findNavController(this).navigate(id, data); } + + public AppCompatActivity getHostActivity() { + return mActivity; + } } diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java new file mode 100644 index 0000000..d73aae5 --- /dev/null +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/FeeAttackChecking.java @@ -0,0 +1,82 @@ +package com.cobo.cold.ui.fragment.main; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.databinding.DataBindingUtil; +import androidx.navigation.Navigation; + +import com.cobo.cold.R; +import com.cobo.cold.databinding.CommonModalBinding; +import com.cobo.cold.db.entity.TxEntity; +import com.cobo.cold.ui.fragment.BaseFragment; +import com.cobo.cold.ui.modal.ModalDialog; + +import java.util.Objects; + +import static com.cobo.cold.ui.fragment.main.TxFragment.KEY_TX_ID; +import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID; +import static com.cobo.cold.viewmodel.PsbtViewModel.WASABI_SIGN_ID; + +public class FeeAttackChecking { + + public static final String KEY_DUPLICATE_TX = "key_duplicate_tx"; + public interface FeeAttackCheckingResult { + + int NORMAL = 1; + int DUPLICATE_TX = 2; + int SAME_OUTPUTS = 3; + } + private BaseFragment fragment; + + public FeeAttackChecking(BaseFragment fragment) { + this.fragment = fragment; + } + + public void showFeeAttackWarning() { + ModalDialog modalDialog = ModalDialog.newInstance(); + CommonModalBinding binding = DataBindingUtil.inflate( + LayoutInflater.from(fragment.getHostActivity()), R.layout.common_modal, + null, false); + modalDialog.setBinding(binding); + binding.title.setText(R.string.abnormal_tx); + binding.subTitle.setText(R.string.fee_attack_warning); + binding.confirm.setText(R.string.know); + binding.confirm.setOnClickListener(v -> modalDialog.dismiss()); + modalDialog.show(fragment.getHostActivity().getSupportFragmentManager(),""); + } + + private void navigateToSignedTx(String txId, String signId) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_TX_ID, txId); + bundle.putBoolean(KEY_DUPLICATE_TX,true); + if (ELECTRUM_SIGN_ID.equals(signId)) { + Navigation.findNavController(Objects.requireNonNull(fragment.getView())) + .navigate(R.id.action_to_electrumTxFragment, bundle); + } else if(WASABI_SIGN_ID.equals(signId)){ + Navigation.findNavController(Objects.requireNonNull(fragment.getView())) + .navigate(R.id.action_to_psbtSignedTxFragment, bundle); + } else { + Navigation.findNavController(Objects.requireNonNull(fragment.getView())) + .navigate(R.id.action_to_txFragment, bundle); + } + } + + public void showDuplicateTx(TxEntity tx) { + ModalDialog modalDialog = ModalDialog.newInstance(); + CommonModalBinding binding = DataBindingUtil.inflate( + LayoutInflater.from(fragment.getHostActivity()), R.layout.common_modal, + null, false); + modalDialog.setBinding(binding); + binding.title.setText(R.string.broadcast_tx); + binding.close.setVisibility(View.GONE); + binding.subTitle.setText(R.string.already_signed); + binding.confirm.setText(R.string.broadcast_tx); + binding.confirm.setOnClickListener(v -> { + modalDialog.dismiss(); + navigateToSignedTx(tx.getTxId(), tx.getSignId()); + }); + modalDialog.show(fragment.getHostActivity().getSupportFragmentManager(),""); + } +} diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtSignedTxFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtSignedTxFragment.java index f7560ed..53bd70a 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtSignedTxFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/PsbtSignedTxFragment.java @@ -19,6 +19,7 @@ package com.cobo.cold.ui.fragment.main; import android.view.View; +import com.cobo.cold.R; import com.cobo.cold.db.entity.TxEntity; import com.cobo.cold.ui.fragment.main.electrum.SignedTxFragment; @@ -34,6 +35,6 @@ public class PsbtSignedTxFragment extends SignedTxFragment { @Override protected void showExportDialog() { showExportPsbtDialog(mActivity, txEntity.getTxId(), - txEntity.getSignedHex(), this::navigateUp); + txEntity.getSignedHex(), () -> popBackStack(R.id.assetFragment, false)); } } diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java index 21dcdc6..f456b70 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxConfirmFragment.java @@ -57,6 +57,9 @@ import java.util.Objects; import static com.cobo.cold.ui.fragment.Constants.KEY_NAV_ID; import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.DUPLICATE_TX; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.NORMAL; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.SAME_OUTPUTS; public class TxConfirmFragment extends BaseFragment { @@ -72,6 +75,8 @@ public class TxConfirmFragment extends BaseFragment private SigningDialog signingDialog; private TxEntity txEntity; private ModalDialog addingAddressDialog; + private int feeAttackCheckingState; + private FeeAttackChecking feeAttackChecking; @Override protected int setView() { @@ -93,6 +98,10 @@ public class TxConfirmFragment extends BaseFragment } private void handleSign() { + if (feeAttackCheckingState == SAME_OUTPUTS) { + feeAttackChecking.showFeeAttackWarning(); + return; + } boolean fingerprintSignEnable = Utilities.isFingerprintSignEnable(mActivity); if (txEntity != null) { if (FeatureFlags.ENABLE_WHITE_LIST) { @@ -169,6 +178,16 @@ public class TxConfirmFragment extends BaseFragment navigateUp(); } }); + + viewModel.feeAttackChecking().observe(this, state -> { + feeAttackCheckingState = state; + if (state != NORMAL) { + feeAttackChecking = new FeeAttackChecking(this); + } + if(state == DUPLICATE_TX) { + feeAttackChecking.showDuplicateTx(viewModel.getPreviousSignTx()); + } + }); } private void refreshAmount() { diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java index 2903d7b..b5a634d 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/TxFragment.java @@ -26,6 +26,7 @@ import android.text.style.ForegroundColorSpan; import android.view.View; import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.fragment.NavHostFragment; import com.cobo.coinlib.utils.Coins; import com.cobo.cold.R; @@ -44,6 +45,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.KEY_DUPLICATE_TX; + public class TxFragment extends BaseFragment { @@ -58,7 +61,14 @@ public class TxFragment extends BaseFragment { @Override protected void init(View view) { Bundle data = Objects.requireNonNull(getArguments()); - mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp()); + mBinding.toolbar.setNavigationOnClickListener(v -> { + if (data.getBoolean(KEY_DUPLICATE_TX)) { + NavHostFragment.findNavController(this) + .popBackStack(R.id.assetFragment, false); + } else { + navigateUp(); + } + }); CoinListViewModel viewModel = ViewModelProviders.of(mActivity).get(CoinListViewModel.class); viewModel.loadTx(data.getString(KEY_TX_ID)).observe(this, txEntity -> { mBinding.setTx(txEntity); diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/SignedTxFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/SignedTxFragment.java index 6e3a596..e58b80c 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/SignedTxFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/SignedTxFragment.java @@ -25,6 +25,7 @@ import android.text.style.ForegroundColorSpan; import android.view.View; import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.fragment.NavHostFragment; import com.cobo.coinlib.utils.Base43; import com.cobo.coinlib.utils.Coins; @@ -47,6 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.KEY_DUPLICATE_TX; import static com.cobo.cold.ui.fragment.main.electrum.ElectrumBroadcastTxFragment.showElectrumInfo; import static com.cobo.cold.ui.fragment.main.electrum.UnsignedTxFragment.showExportTxnDialog; @@ -65,7 +67,14 @@ public class SignedTxFragment extends BaseFragment { @Override protected void init(View view) { Bundle data = Objects.requireNonNull(getArguments()); - mBinding.toolbar.setNavigationOnClickListener(v -> navigateUp()); + mBinding.toolbar.setNavigationOnClickListener(v -> { + if (data.getBoolean(KEY_DUPLICATE_TX)) { + NavHostFragment.findNavController(this) + .popBackStack(R.id.assetFragment, false); + } else { + navigateUp(); + } + }); String walletName = SupportedWatchWallet.getSupportedWatchWallet(mActivity) .getWalletName(mActivity); mBinding.txDetail.watchWallet.setText(walletName); diff --git a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/UnsignedTxFragment.java b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/UnsignedTxFragment.java index b23f05c..f51b55b 100644 --- a/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/UnsignedTxFragment.java +++ b/app/src/main/java/com/cobo/cold/ui/fragment/main/electrum/UnsignedTxFragment.java @@ -42,6 +42,7 @@ import com.cobo.cold.databinding.ProgressModalBinding; import com.cobo.cold.db.entity.TxEntity; import com.cobo.cold.encryptioncore.utils.ByteFormatter; import com.cobo.cold.ui.fragment.BaseFragment; +import com.cobo.cold.ui.fragment.main.FeeAttackChecking; import com.cobo.cold.ui.fragment.main.TransactionItem; import com.cobo.cold.ui.fragment.main.TransactionItemAdapter; import com.cobo.cold.ui.modal.ModalDialog; @@ -68,10 +69,15 @@ import java.util.Objects; import static com.cobo.cold.ui.fragment.Constants.KEY_NAV_ID; import static com.cobo.cold.ui.fragment.main.BroadcastTxFragment.KEY_TXID; + import static com.cobo.cold.viewmodel.GlobalViewModel.exportSuccess; import static com.cobo.cold.viewmodel.GlobalViewModel.hasSdcard; import static com.cobo.cold.viewmodel.GlobalViewModel.showNoSdcardModal; import static com.cobo.cold.viewmodel.GlobalViewModel.writeToSdcard; + +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.DUPLICATE_TX; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.NORMAL; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.SAME_OUTPUTS; import static com.cobo.cold.viewmodel.TxConfirmViewModel.STATE_NONE; public class UnsignedTxFragment extends BaseFragment { @@ -87,6 +93,8 @@ public class UnsignedTxFragment extends BaseFragment changeAddress = new ArrayList<>(); + private int feeAttackCheckingState; + private FeeAttackChecking feeAttackChecking; static void showExportTxnDialog(AppCompatActivity activity, String txId, String hex, Runnable onExportSuccess) { @@ -156,6 +164,10 @@ public class UnsignedTxFragment extends BaseFragment { + feeAttackCheckingState = state; + if (state != NORMAL) { + feeAttackChecking = new FeeAttackChecking(this); + } + if(state == DUPLICATE_TX) { + feeAttackChecking.showDuplicateTx(viewModel.getPreviousSignTx()); + } + }); } private void observeAddAddress() { diff --git a/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java b/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java index 361e7c3..c330a0a 100644 --- a/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java +++ b/app/src/main/java/com/cobo/cold/viewmodel/TxConfirmViewModel.java @@ -79,6 +79,9 @@ import java.util.stream.Stream; import static com.cobo.coinlib.coins.BTC.Electrum.TxUtils.isMasterPublicKeyMatch; import static com.cobo.cold.viewmodel.AddAddressViewModel.AddAddressTask.getAddressType; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.DUPLICATE_TX; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.NORMAL; +import static com.cobo.cold.ui.fragment.main.FeeAttackChecking.FeeAttackCheckingResult.SAME_OUTPUTS; import static com.cobo.cold.viewmodel.ElectrumViewModel.ELECTRUM_SIGN_ID; import static com.cobo.cold.viewmodel.ElectrumViewModel.adapt; import static com.cobo.cold.viewmodel.GlobalViewModel.getAccount; @@ -95,10 +98,13 @@ public class TxConfirmViewModel extends AndroidViewModel { private final MutableLiveData observableTx = new MutableLiveData<>(); private final MutableLiveData parseTxException = new MutableLiveData<>(); private final MutableLiveData addingAddress = new MutableLiveData<>(); + private final MutableLiveData feeAttachCheckingResult = new MutableLiveData<>(); private AbsTx transaction; private String coinCode; private final MutableLiveData signState = new MutableLiveData<>(); private AuthenticateModal.OnVerify.VerifyToken token; + private TxEntity previousSignedTx; + public TxConfirmViewModel(@NonNull Application application) { super(application); @@ -128,18 +134,49 @@ public class TxConfirmViewModel extends AndroidViewModel { if (transaction instanceof UtxoTx) { if (!checkChangeAddress(transaction)) { observableTx.postValue(null); - parseTxException.postValue(new InvalidTransactionException("invlid change address")); + parseTxException.postValue(new InvalidTransactionException("invalid change address")); return; } } TxEntity tx = generateTxEntity(object); observableTx.postValue(tx); + if (Coins.BTC.coinCode().equals(transaction.getCoinCode())) { + feeAttackChecking(tx); + } } catch (JSONException e) { e.printStackTrace(); } }); } + public TxEntity getPreviousSignTx() { + return previousSignedTx; + } + + private void feeAttackChecking(TxEntity txEntity) { + AppExecutors.getInstance().diskIO().execute(() -> { + String inputs = txEntity.getFrom(); + String outputs = txEntity.getTo(); + List txs = mRepository.loadAllTxSync(Coins.BTC.coinId()); + for (TxEntity tx : txs) { + if (inputs.equals(tx.getFrom()) && outputs.equals(tx.getTo())) { + previousSignedTx = tx; + feeAttachCheckingResult.postValue(DUPLICATE_TX); + break; + } else if (outputs.equals(tx.getTo())) { + feeAttachCheckingResult.postValue(SAME_OUTPUTS); + break; + } else { + feeAttachCheckingResult.postValue(NORMAL); + } + } + }); + } + + public LiveData feeAttackChecking() { + return feeAttachCheckingResult; + } + private TxEntity generateTxEntity(JSONObject object) throws JSONException { TxEntity tx = new TxEntity(); NumberFormat nf = NumberFormat.getInstance(); diff --git a/app/src/main/res/navigation/nav_graph_main.xml b/app/src/main/res/navigation/nav_graph_main.xml index d4bd1f1..b9f1b07 100644 --- a/app/src/main/res/navigation/nav_graph_main.xml +++ b/app/src/main/res/navigation/nav_graph_main.xml @@ -320,7 +320,11 @@ + android:label="PsbtTxConfirmFragment"> + + + + + + + + 3kgdahdkhghahdgkhaldshg]]> bc1akjhdfhkashdhdhsaghlh]]> + + 异常交易 + 检测到该笔交易有风险,不能对其签名,您可前往:https://cobo.com/hardware-wallet/XXX 了解详情,您也可邮件联系客服:support@cobo.com + 检测到您已对该笔交易签名,您可直接广播该交易 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2577cd..ba42803 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -454,4 +454,8 @@ Once you toggle the address format, your receiving addresses, exported wallet, and wallet info will all be changed to this format. You will need to export your wallet information again using the new address format if you have not already. Do you still want to change the format? Cancel Confirm + Abnormal Transaction + Detect that the transaction is risky and cannot be signed. You can go to: https// for details, or contact us: support@cobo.com + You have signed the transaction, can broadcast the transaction directly +