From d3035a3a56c6791535a2cae43d8955322c2bfb61 Mon Sep 17 00:00:00 2001 From: Sebastian Nicol Date: Fri, 17 May 2024 18:38:10 +0200 Subject: [PATCH 1/3] Remove spending PIN before backup Co-authored-by: Sebastian Nicol Co-authored-by: Oliver Aemmer --- wallet/res/layout/backup_wallet_dialog.xml | 31 +++ wallet/res/values/strings.xml | 3 +- .../ui/backup/BackupWalletDialogFragment.java | 186 ++++++++++++++++-- .../ui/backup/BackupWalletViewModel.java | 9 + 4 files changed, 211 insertions(+), 18 deletions(-) diff --git a/wallet/res/layout/backup_wallet_dialog.xml b/wallet/res/layout/backup_wallet_dialog.xml index 7f3d800e46..9bdb91755f 100644 --- a/wallet/res/layout/backup_wallet_dialog.xml +++ b/wallet/res/layout/backup_wallet_dialog.xml @@ -91,5 +91,36 @@ android:text="@string/backup_wallet_dialog_warning_encrypted" android:textColor="@color/fg_less_significant" android:textSize="@dimen/font_size_small" /> + + + + + + + diff --git a/wallet/res/values/strings.xml b/wallet/res/values/strings.xml index 7b678296d2..4335ac0ce9 100644 --- a/wallet/res/values/strings.xml +++ b/wallet/res/values/strings.xml @@ -205,7 +205,8 @@ Wallet could not be restored:\n\n%s\n\nBad password? Back up wallet Your backup will be encrypted with the chosen password and written to external storage. - Your wallet is protected by a spending PIN. Make sure you remember the PIN in addition to the backup password! + Your spending PIN protection won\'t be present in the backup. Make sure you set the PIN again if you restore the wallet! + Verifying... Back up Your wallet has been backed up to %s

If the only place your backup exists is on your device, you run the risk of losing both at the same time!

In any case, make sure you remember your backup password.

]]>
Your wallet could not be backed up:\n%s diff --git a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java index 819d90c6cd..2529442a84 100644 --- a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java +++ b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java @@ -51,9 +51,13 @@ import de.schildbach.wallet.util.Crypto; import de.schildbach.wallet.util.Toast; import de.schildbach.wallet.util.WalletUtils; + +import org.bitcoinj.crypto.KeyCrypter; +import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletProtobufSerializer; +import org.bouncycastle.crypto.params.KeyParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,12 +68,12 @@ import java.io.Reader; import java.io.Writer; import java.nio.charset.StandardCharsets; -import java.text.DateFormat; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.Arrays; -import java.util.Date; import java.util.TimeZone; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import static com.google.common.base.Preconditions.checkState; @@ -92,11 +96,16 @@ public static void show(final FragmentManager fm) { private View passwordMismatchView; private CheckBox showView; private TextView warningView; + private View spendingPINViewGroup; + private EditText spendingPINView; + private TextView badSpendingPINView; private Button positiveButton, negativeButton; - + private boolean spendingPINSet = false; + private String pin; + private Executor executor = Executors.newSingleThreadExecutor(); + private Wallet wallet; private AbstractWalletActivityViewModel walletActivityViewModel; private BackupWalletViewModel viewModel; - private static final Logger log = LoggerFactory.getLogger(BackupWalletDialogFragment.class); private final ActivityResultLauncher createDocumentLauncher = @@ -136,6 +145,10 @@ public void onChanged(final Wallet wallet) { log.error("problem backing up wallet to " + uri, x); ErrorDialogFragment.showDialog(getParentFragmentManager(), x.toString()); return; + } finally { + if (badEncryptionState()) { + reEncryptWallet(); + } } try (final Reader cipherIn = new InputStreamReader( @@ -169,7 +182,7 @@ public void onChanged(final Wallet wallet) { } }); - private final TextWatcher textWatcher = new TextWatcher() { + private final TextWatcher passwordTextWatcher = new TextWatcher() { @Override public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { viewModel.password.postValue(s.toString().trim()); @@ -184,11 +197,27 @@ public void afterTextChanged(final Editable s) { } }; + private final TextWatcher spendingPINTextWatcher = new TextWatcher() { + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + viewModel.spendingPIN.postValue(s.toString().trim()); + } + + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + } + + @Override + public void afterTextChanged(final Editable s) { + } + }; + @Override public void onAttach(final Context context) { super.onAttach(context); this.activity = (AbstractWalletActivity) context; this.application = activity.getWalletApplication(); + this.wallet = application.getWallet(); } @Override @@ -216,8 +245,18 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) { showView = view.findViewById(R.id.backup_wallet_dialog_show); + spendingPINViewGroup = view.findViewById(R.id.backup_wallet_dialog_spending_pin_group); + spendingPINView = view.findViewById(R.id.backup_wallet_dialog_spending_pin); + badSpendingPINView = view.findViewById(R.id.backup_wallet_dialog_bad_spending_pin); + warningView = view.findViewById(R.id.backup_wallet_dialog_warning_encrypted); + if (wallet.isEncrypted()) { + warningView.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE); + spendingPINViewGroup.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE); + spendingPINSet =wallet.isEncrypted(); + } + final DialogBuilder builder = DialogBuilder.custom(activity, R.string.export_keys_dialog_title, view); // dummies, just to make buttons show builder.setPositiveButton(R.string.export_keys_dialog_button_export, null); @@ -237,13 +276,23 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) { activity.finish(); }); - passwordView.addTextChangedListener(textWatcher); - passwordAgainView.addTextChangedListener(textWatcher); - + passwordView.addTextChangedListener(passwordTextWatcher); + passwordAgainView.addTextChangedListener(passwordTextWatcher); + spendingPINView.addTextChangedListener(spendingPINTextWatcher); showView.setOnCheckedChangeListener(new ShowPasswordCheckListener(passwordView, passwordAgainView)); - walletActivityViewModel.wallet.observe(BackupWalletDialogFragment.this, - wallet -> warningView.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE)); + viewModel.spendingPIN.observe(BackupWalletDialogFragment.this, spendingPIN -> { + badSpendingPINView.setVisibility(View.INVISIBLE); + if (positiveButton != null) { + positiveButton.setEnabled(preConditionsSatisfied()); + } + + }); + + viewModel.state.observe(BackupWalletDialogFragment.this, state -> { + updateView(); + }); + viewModel.password.observe(BackupWalletDialogFragment.this, password -> { passwordMismatchView.setVisibility(View.INVISIBLE); @@ -268,10 +317,7 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) { } if (positiveButton != null) { - final Wallet wallet = walletActivityViewModel.wallet.getValue(); - final boolean hasPassword = !password.isEmpty(); - final boolean hasPasswordAgain = !passwordAgainView.getText().toString().trim().isEmpty(); - positiveButton.setEnabled(wallet != null && hasPassword && hasPasswordAgain); + positiveButton.setEnabled(preConditionsSatisfied()); } }); }); @@ -281,8 +327,9 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) { @Override public void onDismiss(final DialogInterface dialog) { - passwordView.removeTextChangedListener(textWatcher); - passwordAgainView.removeTextChangedListener(textWatcher); + passwordView.removeTextChangedListener(passwordTextWatcher); + passwordAgainView.removeTextChangedListener(passwordTextWatcher); + spendingPINView.removeTextChangedListener(spendingPINTextWatcher); showView.setOnCheckedChangeListener(null); @@ -293,6 +340,11 @@ public void onDismiss(final DialogInterface dialog) { @Override public void onCancel(final DialogInterface dialog) { + // re-encrypt wallet if it was previously decrypted + if (badEncryptionState()) { + reEncryptWallet(); + } + activity.finish(); super.onCancel(dialog); } @@ -302,15 +354,115 @@ private void handleGo() { final String passwordAgain = passwordAgainView.getText().toString().trim(); if (passwordAgain.equals(password)) { - backupWallet(); + + if (wallet.isEncrypted()) { + setState(BackupWalletViewModel.State.CRYPTING); + + executor.execute(() -> { + final String inputPIN = spendingPINView.getText().toString().trim(); + + try { + final KeyCrypter keyCrypter = wallet.getKeyCrypter(); + final KeyParameter derivedAesKey = keyCrypter.deriveKey(inputPIN); + wallet.decrypt(derivedAesKey); + pin = inputPIN; + + log.info("wallet successfully decrypted for back up"); + setState(BackupWalletViewModel.State.EXPORTING); + backupWallet(); + + } catch (final Wallet.BadWalletEncryptionKeyException x) { + log.info("wallet decryption failed, bad spending password: " + x.getMessage()); + setState(BackupWalletViewModel.State.BADPIN); + } + }); + } else { + setState(BackupWalletViewModel.State.EXPORTING); + backupWallet(); + setState(BackupWalletViewModel.State.INPUT); + } } else { passwordMismatchView.setVisibility(View.VISIBLE); + setState(BackupWalletViewModel.State.INPUT); + } + } + + private void setState(final BackupWalletViewModel.State state) { + viewModel.state.postValue(state); + } + + private void updateView() { + + BackupWalletViewModel.State currentState = viewModel.state.getValue(); + + if (currentState == BackupWalletViewModel.State.INPUT || currentState ==BackupWalletViewModel.State.BADPIN) { + showView.setEnabled(true); + negativeButton.setEnabled(true); // prevent the user from cancelling in a decrypted wallet state + positiveButton.setEnabled(preConditionsSatisfied()); + positiveButton.setText(R.string.export_keys_dialog_button_export); + spendingPINView.setEnabled(true); + passwordView.setEnabled(true); + passwordAgainView.setEnabled(true); + if (currentState ==BackupWalletViewModel.State.BADPIN) { + badSpendingPINView.setVisibility(View.VISIBLE); + } + } else if (currentState == BackupWalletViewModel.State.CRYPTING) { + negativeButton.setEnabled(false); // prevent the user from cancelling in a decrypted wallet state + showView.setEnabled(false); + positiveButton.setEnabled(false); + positiveButton.setText(R.string.backup_wallet_dialog_state_verifying); + spendingPINView.setEnabled(false); + passwordView.setEnabled(false); + passwordAgainView.setEnabled(false); + } else if (currentState == BackupWalletViewModel.State.EXPORTING) { + negativeButton.setEnabled(true); + positiveButton.setEnabled(false); + positiveButton.setText(R.string.backup_wallet_dialog_state_verifying); + spendingPINView.setEnabled(false); + passwordView.setEnabled(false); + passwordAgainView.setEnabled(false); } } + private boolean isSpendingPINPlausible() { + if (wallet == null) + return false; + if (!wallet.isEncrypted()) + return true; + return !spendingPINView.getText().toString().trim().isEmpty(); + } + private void reEncryptWallet() { + executor.execute(() -> { + checkState(!wallet.isEncrypted()); + + try { + checkState(!pin.isEmpty()); + final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(application.scryptIterationsTarget()); + final KeyParameter aesKey = keyCrypter.deriveKey(pin); + + wallet.encrypt(keyCrypter , aesKey); + log.info("wallet successfully decrypted"); + } catch (final Wallet.BadWalletEncryptionKeyException x) { + log.error("wallet decryption failed, bad spending password"); + } + + }); + } + + private boolean badEncryptionState() { + return !spendingPINSet ? false : !wallet.isEncrypted(); + } + + private boolean preConditionsSatisfied() { + final boolean hasPassword = !passwordView.getText().toString().trim().isEmpty(); + final boolean hasPasswordAgain = !passwordAgainView.getText().toString().trim().isEmpty(); + return wallet != null && hasPassword && hasPasswordAgain && isSpendingPINPlausible(); + } + private void wipePasswords() { passwordView.setText(null); passwordAgainView.setText(null); + spendingPINView.setText(null); } private void backupWallet() { diff --git a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java index a62a23ae28..4c83f02568 100644 --- a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java +++ b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java @@ -24,5 +24,14 @@ * @author Andreas Schildbach */ public class BackupWalletViewModel extends ViewModel { + + public enum State { + INPUT, CRYPTING, BADPIN, EXPORTING + } + + + public final MutableLiveData state = new MutableLiveData<>(State.INPUT); + public final MutableLiveData password = new MutableLiveData<>(); + public final MutableLiveData spendingPIN = new MutableLiveData<>(); } From 736109d3d8018945c9fec0071e6f1a5fc911d65f Mon Sep 17 00:00:00 2001 From: Sebastian Nicol Date: Tue, 21 May 2024 13:27:05 +0200 Subject: [PATCH 2/3] rework backup logic to create a wallet clone, remove spending PIN and then backup Co-authored-by: Sebastian Nicol Co-authored-by: Oliver Aemmer --- .../ui/backup/BackupWalletDialogFragment.java | 85 +++++++------------ 1 file changed, 29 insertions(+), 56 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java index 2529442a84..8529400084 100644 --- a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java +++ b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java @@ -53,8 +53,8 @@ import de.schildbach.wallet.util.WalletUtils; import org.bitcoinj.crypto.KeyCrypter; -import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.wallet.Protos; +import org.bitcoinj.wallet.UnreadableWalletException; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletProtobufSerializer; import org.bouncycastle.crypto.params.KeyParameter; @@ -100,10 +100,8 @@ public static void show(final FragmentManager fm) { private EditText spendingPINView; private TextView badSpendingPINView; private Button positiveButton, negativeButton; - private boolean spendingPINSet = false; - private String pin; private Executor executor = Executors.newSingleThreadExecutor(); - private Wallet wallet; + private Wallet backupWallet; private AbstractWalletActivityViewModel walletActivityViewModel; private BackupWalletViewModel viewModel; private static final Logger log = LoggerFactory.getLogger(BackupWalletDialogFragment.class); @@ -120,6 +118,7 @@ public void onChanged(final Wallet wallet) { final String targetProvider = WalletUtils.uriToProvider(uri); final String password = passwordView.getText().toString().trim(); checkState(!password.isEmpty()); + checkState(backupWallet != null); wipePasswords(); dismiss(); @@ -128,7 +127,7 @@ public void onChanged(final Wallet wallet) { activity.getContentResolver().openOutputStream(uri), StandardCharsets.UTF_8)) { final Protos.Wallet walletProto = - new WalletProtobufSerializer().walletToProto(wallet); + new WalletProtobufSerializer().walletToProto(backupWallet); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); walletProto.writeTo(baos); baos.close(); @@ -145,10 +144,6 @@ public void onChanged(final Wallet wallet) { log.error("problem backing up wallet to " + uri, x); ErrorDialogFragment.showDialog(getParentFragmentManager(), x.toString()); return; - } finally { - if (badEncryptionState()) { - reEncryptWallet(); - } } try (final Reader cipherIn = new InputStreamReader( @@ -217,7 +212,6 @@ public void onAttach(final Context context) { super.onAttach(context); this.activity = (AbstractWalletActivity) context; this.application = activity.getWalletApplication(); - this.wallet = application.getWallet(); } @Override @@ -251,12 +245,6 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) { warningView = view.findViewById(R.id.backup_wallet_dialog_warning_encrypted); - if (wallet.isEncrypted()) { - warningView.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE); - spendingPINViewGroup.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE); - spendingPINSet =wallet.isEncrypted(); - } - final DialogBuilder builder = DialogBuilder.custom(activity, R.string.export_keys_dialog_title, view); // dummies, just to make buttons show builder.setPositiveButton(R.string.export_keys_dialog_button_export, null); @@ -281,6 +269,11 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) { spendingPINView.addTextChangedListener(spendingPINTextWatcher); showView.setOnCheckedChangeListener(new ShowPasswordCheckListener(passwordView, passwordAgainView)); + walletActivityViewModel.wallet.observe(BackupWalletDialogFragment.this, wallet ->{ + warningView.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE); + spendingPINViewGroup.setVisibility(wallet.isEncrypted() ? View.VISIBLE : View.GONE); + }); + viewModel.spendingPIN.observe(BackupWalletDialogFragment.this, spendingPIN -> { badSpendingPINView.setVisibility(View.INVISIBLE); if (positiveButton != null) { @@ -340,11 +333,6 @@ public void onDismiss(final DialogInterface dialog) { @Override public void onCancel(final DialogInterface dialog) { - // re-encrypt wallet if it was previously decrypted - if (badEncryptionState()) { - reEncryptWallet(); - } - activity.finish(); super.onCancel(dialog); } @@ -354,31 +342,38 @@ private void handleGo() { final String passwordAgain = passwordAgainView.getText().toString().trim(); if (passwordAgain.equals(password)) { - + final Wallet wallet = walletActivityViewModel.wallet.getValue(); if (wallet.isEncrypted()) { setState(BackupWalletViewModel.State.CRYPTING); - executor.execute(() -> { final String inputPIN = spendingPINView.getText().toString().trim(); try { - final KeyCrypter keyCrypter = wallet.getKeyCrypter(); - final KeyParameter derivedAesKey = keyCrypter.deriveKey(inputPIN); - wallet.decrypt(derivedAesKey); - pin = inputPIN; + backupWallet = new WalletProtobufSerializer() + .readWallet(Constants.NETWORK_PARAMETERS, null, + new WalletProtobufSerializer().walletToProto(wallet)); - log.info("wallet successfully decrypted for back up"); + final KeyCrypter keyCrypter = backupWallet.getKeyCrypter(); + final KeyParameter derivedAesKey = keyCrypter.deriveKey(inputPIN); + backupWallet.decrypt(derivedAesKey); + log.info("backup wallet successfully decrypted for back up"); setState(BackupWalletViewModel.State.EXPORTING); backupWallet(); - } catch (final Wallet.BadWalletEncryptionKeyException x) { - log.info("wallet decryption failed, bad spending password: " + x.getMessage()); + } catch (final Wallet.BadWalletEncryptionKeyException e) { + log.info("wallet decryption failed, bad spending password: " + e.getMessage()); + backupWallet = null; setState(BackupWalletViewModel.State.BADPIN); + } catch (UnreadableWalletException e) { + log.info("wallet deserialization failed: " + e.getMessage()); + ErrorDialogFragment.showDialog(getParentFragmentManager(), e.toString()); } }); } else { setState(BackupWalletViewModel.State.EXPORTING); + backupWallet = walletActivityViewModel.wallet.getValue(); backupWallet(); + backupWallet = null; setState(BackupWalletViewModel.State.INPUT); } } else { @@ -397,7 +392,7 @@ private void updateView() { if (currentState == BackupWalletViewModel.State.INPUT || currentState ==BackupWalletViewModel.State.BADPIN) { showView.setEnabled(true); - negativeButton.setEnabled(true); // prevent the user from cancelling in a decrypted wallet state + negativeButton.setEnabled(true); positiveButton.setEnabled(preConditionsSatisfied()); positiveButton.setText(R.string.export_keys_dialog_button_export); spendingPINView.setEnabled(true); @@ -407,7 +402,7 @@ private void updateView() { badSpendingPINView.setVisibility(View.VISIBLE); } } else if (currentState == BackupWalletViewModel.State.CRYPTING) { - negativeButton.setEnabled(false); // prevent the user from cancelling in a decrypted wallet state + negativeButton.setEnabled(true); showView.setEnabled(false); positiveButton.setEnabled(false); positiveButton.setText(R.string.backup_wallet_dialog_state_verifying); @@ -425,35 +420,16 @@ private void updateView() { } private boolean isSpendingPINPlausible() { + final Wallet wallet = walletActivityViewModel.wallet.getValue(); if (wallet == null) return false; if (!wallet.isEncrypted()) return true; return !spendingPINView.getText().toString().trim().isEmpty(); } - private void reEncryptWallet() { - executor.execute(() -> { - checkState(!wallet.isEncrypted()); - - try { - checkState(!pin.isEmpty()); - final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(application.scryptIterationsTarget()); - final KeyParameter aesKey = keyCrypter.deriveKey(pin); - - wallet.encrypt(keyCrypter , aesKey); - log.info("wallet successfully decrypted"); - } catch (final Wallet.BadWalletEncryptionKeyException x) { - log.error("wallet decryption failed, bad spending password"); - } - - }); - } - - private boolean badEncryptionState() { - return !spendingPINSet ? false : !wallet.isEncrypted(); - } private boolean preConditionsSatisfied() { + final Wallet wallet = walletActivityViewModel.wallet.getValue(); final boolean hasPassword = !passwordView.getText().toString().trim().isEmpty(); final boolean hasPasswordAgain = !passwordAgainView.getText().toString().trim().isEmpty(); return wallet != null && hasPassword && hasPasswordAgain && isSpendingPINPlausible(); @@ -466,9 +442,6 @@ private void wipePasswords() { } private void backupWallet() { - passwordView.setEnabled(false); - passwordAgainView.setEnabled(false); - final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm"); final StringBuilder filename = new StringBuilder(Constants.Files.EXTERNAL_WALLET_BACKUP); filename.append('-'); From 48dc908d83295523abbcba132fcc1b3cae7eccd3 Mon Sep 17 00:00:00 2001 From: Oliver Aemmer Date: Wed, 5 Jun 2024 11:41:59 +0200 Subject: [PATCH 3/3] change walletToBackup to MutableLiveData --- .../ui/backup/BackupWalletDialogFragment.java | 58 +++++++++---------- .../ui/backup/BackupWalletViewModel.java | 2 + 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java index 8529400084..31c70dabfa 100644 --- a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java +++ b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletDialogFragment.java @@ -72,8 +72,10 @@ import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.TimeZone; -import java.util.concurrent.Executor; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import static com.google.common.base.Preconditions.checkState; @@ -100,8 +102,7 @@ public static void show(final FragmentManager fm) { private EditText spendingPINView; private TextView badSpendingPINView; private Button positiveButton, negativeButton; - private Executor executor = Executors.newSingleThreadExecutor(); - private Wallet backupWallet; + private ExecutorService executor = Executors.newSingleThreadExecutor(); private AbstractWalletActivityViewModel walletActivityViewModel; private BackupWalletViewModel viewModel; private static final Logger log = LoggerFactory.getLogger(BackupWalletDialogFragment.class); @@ -110,15 +111,14 @@ public static void show(final FragmentManager fm) { registerForActivityResult(new ActivityResultContracts.CreateDocument(Constants.MIMETYPE_WALLET_BACKUP), uri -> { if (uri != null) { - walletActivityViewModel.wallet.observe(this, new Observer() { + viewModel.walletToBackup.observe(this, new Observer() { @Override - public void onChanged(final Wallet wallet) { + public void onChanged(final Wallet walletToBackup) { walletActivityViewModel.wallet.removeObserver(this); final String targetProvider = WalletUtils.uriToProvider(uri); final String password = passwordView.getText().toString().trim(); checkState(!password.isEmpty()); - checkState(backupWallet != null); wipePasswords(); dismiss(); @@ -127,7 +127,7 @@ public void onChanged(final Wallet wallet) { activity.getContentResolver().openOutputStream(uri), StandardCharsets.UTF_8)) { final Protos.Wallet walletProto = - new WalletProtobufSerializer().walletToProto(backupWallet); + new WalletProtobufSerializer().walletToProto(walletToBackup); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); walletProto.writeTo(baos); baos.close(); @@ -165,7 +165,6 @@ public void onChanged(final Wallet wallet) { } catch (final IOException x) { log.error("problem verifying backup from " + uri, x); ErrorDialogFragment.showDialog(getParentFragmentManager(), x.toString()); - return; } } }); @@ -345,35 +344,36 @@ private void handleGo() { final Wallet wallet = walletActivityViewModel.wallet.getValue(); if (wallet.isEncrypted()) { setState(BackupWalletViewModel.State.CRYPTING); - executor.execute(() -> { - final String inputPIN = spendingPINView.getText().toString().trim(); + final String inputPIN = spendingPINView.getText().toString().trim(); - try { - backupWallet = new WalletProtobufSerializer() - .readWallet(Constants.NETWORK_PARAMETERS, null, - new WalletProtobufSerializer().walletToProto(wallet)); + try { + final Protos.Wallet protosWallet = new WalletProtobufSerializer().walletToProto(wallet); + final Wallet backupWallet = new WalletProtobufSerializer() + .readWallet(Constants.NETWORK_PARAMETERS, null, protosWallet); - final KeyCrypter keyCrypter = backupWallet.getKeyCrypter(); + final KeyCrypter keyCrypter = backupWallet.getKeyCrypter(); + Future future = executor.submit(() -> { + org.bitcoinj.core.Context.propagate(Constants.CONTEXT); final KeyParameter derivedAesKey = keyCrypter.deriveKey(inputPIN); backupWallet.decrypt(derivedAesKey); log.info("backup wallet successfully decrypted for back up"); - setState(BackupWalletViewModel.State.EXPORTING); - backupWallet(); - - } catch (final Wallet.BadWalletEncryptionKeyException e) { - log.info("wallet decryption failed, bad spending password: " + e.getMessage()); - backupWallet = null; - setState(BackupWalletViewModel.State.BADPIN); - } catch (UnreadableWalletException e) { - log.info("wallet deserialization failed: " + e.getMessage()); - ErrorDialogFragment.showDialog(getParentFragmentManager(), e.toString()); - } - }); + }); + future.get(); + viewModel.walletToBackup.setValue(backupWallet); + setState(BackupWalletViewModel.State.EXPORTING); + backupWallet(); + + } catch (final Wallet.BadWalletEncryptionKeyException e) { + log.info("wallet decryption failed, bad spending password: " + e.getMessage()); + setState(BackupWalletViewModel.State.BADPIN); + } catch (UnreadableWalletException | ExecutionException | InterruptedException e) { + log.info("wallet deserialization failed: " + e.getMessage()); + ErrorDialogFragment.showDialog(getParentFragmentManager(), e.toString()); + } } else { + viewModel.walletToBackup.setValue(wallet); setState(BackupWalletViewModel.State.EXPORTING); - backupWallet = walletActivityViewModel.wallet.getValue(); backupWallet(); - backupWallet = null; setState(BackupWalletViewModel.State.INPUT); } } else { diff --git a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java index 4c83f02568..99e2e8905a 100644 --- a/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java +++ b/wallet/src/de/schildbach/wallet/ui/backup/BackupWalletViewModel.java @@ -19,6 +19,7 @@ import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import org.bitcoinj.wallet.Wallet; /** * @author Andreas Schildbach @@ -34,4 +35,5 @@ public enum State { public final MutableLiveData password = new MutableLiveData<>(); public final MutableLiveData spendingPIN = new MutableLiveData<>(); + public final MutableLiveData walletToBackup = new MutableLiveData<>(); }