-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove the spending PIN encryption before backup #1133
base: main
Are you sure you want to change the base?
Changes from 2 commits
d3035a3
736109d
48dc908
7c0aafb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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.wallet.Protos; | ||
import org.bitcoinj.wallet.UnreadableWalletException; | ||
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,14 @@ 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 Executor executor = Executors.newSingleThreadExecutor(); | ||
private Wallet backupWallet; | ||
private AbstractWalletActivityViewModel walletActivityViewModel; | ||
private BackupWalletViewModel viewModel; | ||
|
||
private static final Logger log = LoggerFactory.getLogger(BackupWalletDialogFragment.class); | ||
|
||
private final ActivityResultLauncher<String> createDocumentLauncher = | ||
|
@@ -111,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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In line 113 above, you'd observe that |
||
wipePasswords(); | ||
dismiss(); | ||
|
||
|
@@ -119,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(); | ||
|
@@ -169,7 +177,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,6 +192,21 @@ 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); | ||
|
@@ -216,6 +239,10 @@ 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); | ||
|
||
final DialogBuilder builder = DialogBuilder.custom(activity, R.string.export_keys_dialog_title, view); | ||
|
@@ -237,13 +264,28 @@ 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)); | ||
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) { | ||
positiveButton.setEnabled(preConditionsSatisfied()); | ||
} | ||
|
||
}); | ||
|
||
viewModel.state.observe(BackupWalletDialogFragment.this, state -> { | ||
updateView(); | ||
}); | ||
|
||
viewModel.password.observe(BackupWalletDialogFragment.this, password -> { | ||
passwordMismatchView.setVisibility(View.INVISIBLE); | ||
|
||
|
@@ -268,10 +310,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 +320,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); | ||
|
||
|
@@ -302,21 +342,106 @@ private void handleGo() { | |
final String passwordAgain = passwordAgainView.getText().toString().trim(); | ||
|
||
if (passwordAgain.equals(password)) { | ||
backupWallet(); | ||
final Wallet wallet = walletActivityViewModel.wallet.getValue(); | ||
if (wallet.isEncrypted()) { | ||
setState(BackupWalletViewModel.State.CRYPTING); | ||
executor.execute(() -> { | ||
final String inputPIN = spendingPINView.getText().toString().trim(); | ||
|
||
try { | ||
backupWallet = new WalletProtobufSerializer() | ||
.readWallet(Constants.NETWORK_PARAMETERS, null, | ||
new WalletProtobufSerializer().walletToProto(wallet)); | ||
|
||
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 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if the removal of the reference to the decrypted wallet is needed in general. We need to trust the garbage collector anyway. If you prefer to keep it, I'd move it towards the end of the |
||
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); | ||
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(true); | ||
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() { | ||
final Wallet wallet = walletActivityViewModel.wallet.getValue(); | ||
if (wallet == null) | ||
return false; | ||
if (!wallet.isEncrypted()) | ||
return true; | ||
return !spendingPINView.getText().toString().trim().isEmpty(); | ||
} | ||
|
||
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(); | ||
} | ||
|
||
private void wipePasswords() { | ||
passwordView.setText(null); | ||
passwordAgainView.setText(null); | ||
spendingPINView.setText(null); | ||
} | ||
|
||
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('-'); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the (decrypted) backupWallet should be kept in a
MutableLiveData<Wallet>
inBackupWalletViewModel
, rather than here where it can be collected at any time (e.g. while you're picking the file).And I'd use the name
walletToBackUp
, maybe evenwalletToBeBackedUp
ordecryptedWalletToBeBackedUp
.