diff --git a/wallet/res/menu/wallet_safety_options.xml b/wallet/res/menu/wallet_safety_options.xml index 2d5ab8e826..672274944b 100644 --- a/wallet/res/menu/wallet_safety_options.xml +++ b/wallet/res/menu/wallet_safety_options.xml @@ -13,6 +13,14 @@ android:id="@+id/wallet_options_restore_wallet" android:title="@string/import_keys_dialog_title" app:showAsAction="never" /> + + Disconnect Masternodes + View Wallet Seed + Restore Wallet from Seed + This is your recovery phrase. Please write it down. + DO NOT let anyone see your recovery phrase or they can spend your Dash. This phrase is not compatible with other Dash Software. + Enter your recovery phrase here. + Recovery phrase goes here. + + diff --git a/wallet/src/de/schildbach/wallet/WalletApplication.java b/wallet/src/de/schildbach/wallet/WalletApplication.java index ccd8b7a69a..d7a3e1e34c 100644 --- a/wallet/src/de/schildbach/wallet/WalletApplication.java +++ b/wallet/src/de/schildbach/wallet/WalletApplication.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.List; import java.util.concurrent.TimeUnit; import org.bitcoinj.core.CoinDefinition; @@ -31,6 +32,7 @@ import org.bitcoinj.core.VersionMessage; import org.bitcoinj.crypto.LinuxSecureRandom; import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.store.FlatDB; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.Protos; @@ -69,6 +71,8 @@ import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; +import static de.schildbach.wallet.Constants.HEX; + /** * @author Andreas Schildbach */ diff --git a/wallet/src/de/schildbach/wallet/ui/WalletActivity.java b/wallet/src/de/schildbach/wallet/ui/WalletActivity.java index 6f065ef015..5110fcbbd2 100644 --- a/wallet/src/de/schildbach/wallet/ui/WalletActivity.java +++ b/wallet/src/de/schildbach/wallet/ui/WalletActivity.java @@ -24,6 +24,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; @@ -32,6 +34,8 @@ import org.bitcoinj.core.Transaction; import org.bitcoinj.core.VerificationException; import org.bitcoinj.core.VersionedChecksummedBytes; +import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.Wallet.BalanceType; @@ -104,6 +108,7 @@ public final class WalletActivity extends AbstractWalletActivity implements Acti private static final int DIALOG_TIMESKEW_ALERT = 3; private static final int DIALOG_VERSION_ALERT = 4; private static final int DIALOG_LOW_STORAGE_ALERT = 5; + private static final int DIALOG_RESTORE_WALLET_FROM_SEED = 12; private WalletApplication application; private Configuration config; @@ -400,6 +405,18 @@ public void handleRestoreWallet() { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE_RESTORE_WALLET); } + public void handleBackupWalletToSeed() { + //if (ContextCompat.checkSelfPermission(this, + // Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) + BackupWalletToSeedDialogFragment.show(getFragmentManager()); + //else + // ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + // REQUEST_CODE_BACKUP_WALLET); + } + + public void handleRestoreWalletFromSeed() { + showDialog(DIALOG_RESTORE_WALLET_FROM_SEED); + } public void handleEncryptKeys() { EncryptKeysDialogFragment.show(getFragmentManager()); @@ -449,6 +466,8 @@ else if (id == DIALOG_VERSION_ALERT) return createVersionAlertDialog(); else if (id == DIALOG_LOW_STORAGE_ALERT) return createLowStorageAlertDialog(); + else if (id == DIALOG_RESTORE_WALLET_FROM_SEED) + return createRestoreWalletFromSeedDialog(); else throw new IllegalArgumentException(); } @@ -457,6 +476,8 @@ else if (id == DIALOG_LOW_STORAGE_ALERT) protected void onPrepareDialog(final int id, final Dialog dialog) { if (id == DIALOG_RESTORE_WALLET) prepareRestoreWalletDialog(dialog); + else if(id == DIALOG_RESTORE_WALLET_FROM_SEED) + prepareRestoreWalletFromSeedDialog(dialog); } private Dialog createBackupWalletPermissionDialog() { @@ -615,7 +636,114 @@ protected boolean needsPassword() { final CheckBox showView = (CheckBox) alertDialog.findViewById(R.id.import_keys_from_storage_show); showView.setOnCheckedChangeListener(new ShowPasswordCheckListener(passwordView)); } + private Dialog createRestoreWalletFromSeedDialog() { + final View view = getLayoutInflater().inflate(R.layout.restore_wallet_from_seed_dialog, null); + final TextView messageView = (TextView) view.findViewById(R.id.restore_wallet_dialog_message); + //final Spinner fileView = (Spinner) view.findViewById(R.id.import_keys_from_storage_file); + final EditText passwordView = (EditText) view.findViewById(R.id.import_seed_recovery_phrase); + + final DialogBuilder dialog = new DialogBuilder(this); + dialog.setTitle(R.string.import_keys_dialog_title_from_seed); + dialog.setView(view); + dialog.setPositiveButton(R.string.import_keys_dialog_button_import, new OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + //final File file = (File) fileView.getSelectedItem(); + final String password = passwordView.getText().toString().trim(); + List words = new ArrayList(Arrays.asList(password.split(" "))); + + passwordView.setText(null); // get rid of it asap + + //if (WalletUtils.BACKUP_FILE_FILTER.accept(file)) + // restoreWalletFromProtobuf(file); + //else if (WalletUtils.KEYS_FILE_FILTER.accept(file)) + // restorePrivateKeysFromBase58(file); + //else if (Crypto.OPENSSL_FILE_FILTER.accept(file)) + // restoreWalletFromEncrypted(file, password); + restoreWalletFromSeed(words); + + } + }); + dialog.setNegativeButton(R.string.button_cancel, new OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + passwordView.setText(null); // get rid of it asap + } + }); + dialog.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(final DialogInterface dialog) { + passwordView.setText(null); // get rid of it asap + } + }); + /*final String path; + final String backupPath = Constants.Files.EXTERNAL_WALLET_BACKUP_DIR.getAbsolutePath(); + final String storagePath = Constants.Files.EXTERNAL_STORAGE_DIR.getAbsolutePath(); + if (backupPath.startsWith(storagePath)) + path = backupPath.substring(storagePath.length()); + else + path = backupPath; + */ + messageView.setText(getString(R.string.import_keys_from_seed_dialog_message)); + + //fileView.setAdapter(adapter); + + return dialog.create(); + } + + private void prepareRestoreWalletFromSeedDialog(final Dialog dialog) { + final AlertDialog alertDialog = (AlertDialog) dialog; + + final List files = new LinkedList(); + + // external storage + final File[] externalFiles = Constants.Files.EXTERNAL_WALLET_BACKUP_DIR.listFiles(); + if (externalFiles != null) + for (final File file : externalFiles) + if (Crypto.OPENSSL_FILE_FILTER.accept(file)) + files.add(file); + + // internal storage + for (final String filename : fileList()) + if (filename.startsWith(Constants.Files.WALLET_KEY_BACKUP_PROTOBUF + '.')) + files.add(new File(getFilesDir(), filename)); + + // sort + Collections.sort(files, new Comparator() { + @Override + public int compare(final File lhs, final File rhs) { + return lhs.getName().compareToIgnoreCase(rhs.getName()); + } + }); + + final View replaceWarningView = alertDialog + .findViewById(R.id.restore_wallet_from_storage_dialog_replace_warning); + final boolean hasCoins = wallet.getBalance(BalanceType.ESTIMATED).signum() > 0; + replaceWarningView.setVisibility(hasCoins ? View.VISIBLE : View.GONE); + + final EditText passwordView = (EditText) alertDialog.findViewById(R.id.import_seed_recovery_phrase); + passwordView.setText(null); + + final ImportDialogButtonEnablerListener dialogButtonEnabler = new ImportDialogButtonEnablerListener( + passwordView, alertDialog) { + + @Override + protected boolean hasFile() + { + return true; + } + @Override + protected boolean needsPassword() { + return true; + } + }; + passwordView.addTextChangedListener(dialogButtonEnabler); + //fileView.setOnItemSelectedListener(dialogButtonEnabler); + + //final CheckBox showView = (CheckBox) alertDialog.findViewById(R.id.import_keys_from_storage_show); + //showView.setOnCheckedChangeListener(new ShowPasswordCheckListener(passwordView)); + } private void checkLowStorageAlert() { final Intent stickyIntent = registerReceiver(null, new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW)); if (stickyIntent != null) @@ -870,6 +998,51 @@ public void onClick(final DialogInterface dialog, final int id) { } } + private void restoreWalletFromSeed(final List words) { + FileInputStream is = null; + try { + //is = new FileInputStream(file); + MnemonicCode.INSTANCE.check(words); + restoreWallet(WalletUtils.restoreWalletFromSeed(words, Constants.NETWORK_PARAMETERS)); + + log.info("successfully restored wallet from seed: {}", words.size()); + } catch (final IOException x) { + final DialogBuilder dialog = DialogBuilder.warn(this, R.string.import_export_keys_dialog_failure_title); + dialog.setMessage(getString(R.string.import_keys_dialog_failure, x.getMessage())); + dialog.setPositiveButton(R.string.button_dismiss, null); + dialog.setNegativeButton(R.string.button_retry, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int id) { + showDialog(DIALOG_RESTORE_WALLET); + } + }); + dialog.show(); + + log.info("problem restoring wallet from seed: ", x); + } catch (MnemonicException x) { + final DialogBuilder dialog = DialogBuilder.warn(this, R.string.import_export_keys_dialog_failure_title); + dialog.setMessage(getString(R.string.import_keys_dialog_failure, x.getMessage())); + dialog.setPositiveButton(R.string.button_dismiss, null); + dialog.setNegativeButton(R.string.button_retry, new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int id) { + showDialog(DIALOG_RESTORE_WALLET); + } + }); + dialog.show(); + + log.info("problem restoring wallet from seed: ", x); + } finally { + if (is != null) { + try { + is.close(); + } catch (final IOException x2) { + // swallow + } + } + } + } + private void restorePrivateKeysFromBase58(final File file) { FileInputStream is = null; try { @@ -957,6 +1130,13 @@ public boolean onContextItemSelected(MenuItem item) { case R.id.wallet_options_encrypt_keys: handleEncryptKeys(); return true; + case R.id.wallet_options_backup_wallet_to_seed: + handleBackupWalletToSeed(); + return true; + + case R.id.wallet_options_restore_wallet_from_seed: + handleRestoreWalletFromSeed(); + return true; } return super.onContextItemSelected(item); diff --git a/wallet/src/de/schildbach/wallet/util/WalletUtils.java b/wallet/src/de/schildbach/wallet/util/WalletUtils.java index c4a0e5ad1e..fce693c385 100644 --- a/wallet/src/de/schildbach/wallet/util/WalletUtils.java +++ b/wallet/src/de/schildbach/wallet/util/WalletUtils.java @@ -46,7 +46,10 @@ import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.DeterministicSeed; import org.bitcoinj.wallet.KeyChainGroup; import org.bitcoinj.wallet.UnreadableWalletException; import org.bitcoinj.wallet.Wallet; @@ -189,6 +192,30 @@ public static Wallet restoreWalletFromProtobuf(final InputStream is, } } + public static Wallet restoreWalletFromSeed(final List words, + final NetworkParameters expectedNetworkParameters) throws IOException { + try { + final Wallet wallet = new Wallet(Constants.NETWORK_PARAMETERS, new KeyChainGroup(Constants.NETWORK_PARAMETERS, new DeterministicSeed(words, null,"", 1474771804)));//new WalletProtobufSerializer().readWallet(is, true, null); + + if (!wallet.getParams().equals(expectedNetworkParameters)) + throw new IOException("bad wallet backup network parameters: " + wallet.getParams().getId()); + if (!wallet.isConsistent()) + throw new IOException("inconsistent wallet backup"); + + return wallet; + } /*catch (final MnemonicException.MnemonicLengthException x) { + throw new IOException("Mnemonic Seed has the incorrect number of words", x); + } catch (final MnemonicException.MnemonicChecksumException x) { + throw new IOException("Mnemonic Seed has the incorrect checksum", x); + } catch (final MnemonicException.MnemonicWordException x) { + throw new IOException("Mnemonic Seed has an invalid word", x); + }*/ + finally { + + } + + } + public static Wallet restorePrivateKeysFromBase58(final InputStream is, final NetworkParameters expectedNetworkParameters) throws IOException { final BufferedReader keyReader = new BufferedReader(new InputStreamReader(is, Charsets.UTF_8));