Skip to content

Commit

Permalink
Remove spending PIN before backup
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Nicol <sebastian.r.nicol@gmail.com>
Co-authored-by: Oliver Aemmer <oliver.aemmer@protonmail.ch>
  • Loading branch information
3 people committed May 17, 2024
1 parent 8e907a2 commit d3035a3
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 18 deletions.
31 changes: 31 additions & 0 deletions wallet/res/layout/backup_wallet_dialog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,36 @@
android:text="@string/backup_wallet_dialog_warning_encrypted"
android:textColor="@color/fg_less_significant"
android:textSize="@dimen/font_size_small" />

<LinearLayout
android:id="@+id/backup_wallet_dialog_spending_pin_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/list_entry_padding_vertical"
android:divider="@drawable/divider_field"
android:orientation="horizontal"
android:showDividers="middle"
android:visibility="gone">

<EditText
android:id="@+id/backup_wallet_dialog_spending_pin"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/private_key_password"
android:imeOptions="flagNoExtractUi"
android:inputType="numberPassword"
android:singleLine="true" />

<TextView
android:id="@+id/backup_wallet_dialog_bad_spending_pin"
android:layout_width="0px"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/private_key_bad_password"
android:textColor="@color/fg_error"
android:textStyle="bold"
android:visibility="invisible" />
</LinearLayout>
</LinearLayout>
</ScrollView>
3 changes: 2 additions & 1 deletion wallet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@
<string name="import_keys_dialog_failure">Wallet could not be restored:\n\n%s\n\nBad password?</string>
<string name="export_keys_dialog_title">Back up wallet</string>
<string name="backup_wallet_dialog_message">Your backup will be encrypted with the chosen password and written to external storage.</string>
<string name="backup_wallet_dialog_warning_encrypted">Your wallet is protected by a spending PIN. Make sure you remember the PIN in addition to the backup password!</string>
<string name="backup_wallet_dialog_warning_encrypted">Your spending PIN protection won\'t be present in the backup. Make sure you set the PIN again if you restore the wallet!</string>
<string name="backup_wallet_dialog_state_verifying">Verifying...</string>
<string name="export_keys_dialog_button_export">Back up</string>
<string name="export_keys_dialog_success"><![CDATA[<p>Your wallet has been backed up to <tt>%s</tt></p><p><b>If the only place your backup exists is on your device, you run the risk of losing both at the same time!</b></p><p>In any case, make sure you remember your backup password.</p>]]></string>
<string name="export_keys_dialog_failure">Your wallet could not be backed up:\n%s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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<String> createDocumentLauncher =
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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());
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand All @@ -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());
}
});
});
Expand All @@ -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);

Expand All @@ -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);
}
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,14 @@
* @author Andreas Schildbach
*/
public class BackupWalletViewModel extends ViewModel {

public enum State {
INPUT, CRYPTING, BADPIN, EXPORTING
}


public final MutableLiveData<State> state = new MutableLiveData<>(State.INPUT);

public final MutableLiveData<String> password = new MutableLiveData<>();
public final MutableLiveData<String> spendingPIN = new MutableLiveData<>();
}

0 comments on commit d3035a3

Please sign in to comment.