Skip to content

Commit

Permalink
Add Recovery Phrase (Backup/Restore)
Browse files Browse the repository at this point in the history
  • Loading branch information
HashEngineering committed May 6, 2017
1 parent 0556ce8 commit 6bbafb2
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 0 deletions.
8 changes: 8 additions & 0 deletions wallet/res/menu/wallet_safety_options.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
android:id="@+id/wallet_options_restore_wallet"
android:title="@string/import_keys_dialog_title"
app:showAsAction="never" />
<item
android:id="@+id/wallet_options_backup_wallet_to_seed"
android:title="@string/export_keys_dialog_title_to_seed"
app:showAsAction="never" />
<item
android:id="@+id/wallet_options_restore_wallet_from_seed"
android:title="@string/import_keys_dialog_title_from_seed"
app:showAsAction="never" />
<item
android:id="@+id/wallet_options_encrypt_keys"
android:title="@string/encrypt_keys_dialog_title"
Expand Down
8 changes: 8 additions & 0 deletions wallet/res/values/strings-extra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@
<string name="wallet_options_disconnect">Disconnect</string>

<string name="network_monitor_masternodes_title">Masternodes</string>
<string name="export_keys_dialog_title_to_seed">View Wallet Seed</string>
<string name="import_keys_dialog_title_from_seed">Restore Wallet from Seed</string>
<string name="backup_to_seed_wallet_dialog_message">This is your recovery phrase. Please write it down.</string>
<string name="backup_wallet_to_seed_dialog_warning">DO NOT let anyone see your recovery phrase or they can spend your Dash. This phrase is not compatible with other Dash Software.</string>
<string name="import_keys_from_seed_dialog_message">Enter your recovery phrase here.</string>
<string name="import_export_keys_dialog_password_phrase">Recovery phrase goes here.</string>


</resources>
4 changes: 4 additions & 0 deletions wallet/src/de/schildbach/wallet/WalletApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down
180 changes: 180 additions & 0 deletions wallet/src/de/schildbach/wallet/ui/WalletActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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<String> words = new ArrayList<String>(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<File> files = new LinkedList<File>();

// 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<File>() {
@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)
Expand Down Expand Up @@ -870,6 +998,51 @@ public void onClick(final DialogInterface dialog, final int id) {
}
}

private void restoreWalletFromSeed(final List<String> 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 {
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions wallet/src/de/schildbach/wallet/util/WalletUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -189,6 +192,30 @@ public static Wallet restoreWalletFromProtobuf(final InputStream is,
}
}

public static Wallet restoreWalletFromSeed(final List<String> 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));
Expand Down

0 comments on commit 6bbafb2

Please sign in to comment.