/**
* Copyright 2012 multibit.org
*
* Licensed under the MIT license (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/mit-license.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.multibit.viewsystem.swing.action;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.multibit.controller.bitcoin.BitcoinController;
import org.multibit.file.*;
import org.multibit.message.Message;
import org.multibit.model.bitcoin.WalletBusyListener;
import org.multibit.model.bitcoin.WalletData;
import org.multibit.network.ReplayManager;
import org.multibit.network.ReplayTask;
import org.multibit.utils.DateUtils;
import org.multibit.viewsystem.swing.MultiBitFrame;
import org.multibit.viewsystem.swing.view.panels.ImportPrivateKeysPanel;
import org.multibit.viewsystem.swing.view.walletlist.SingleWalletPanel;
import org.multibit.viewsystem.swing.view.walletlist.WalletListPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.*;
import java.util.List;
/**
* This {@link Action} imports the private keys to the active wallet.
*/
public class ImportPrivateKeysSubmitAction extends MultiBitSubmitAction implements WalletBusyListener {
private static final Logger log = LoggerFactory.getLogger(ImportPrivateKeysSubmitAction.class);
private static final long serialVersionUID = 1923492087598757765L;
private MultiBitFrame mainFrame;
private ImportPrivateKeysPanel importPrivateKeysPanel;
private JPasswordField walletPasswordField;
private JPasswordField passwordField;
private JPasswordField passwordField2;
private boolean performReplay = true;
private File privateKeysBackupFile;
private static final long NUMBER_OF_MILLISECONDS_IN_A_SECOND = 1000;
/**
* Creates a new {@link ImportPrivateKeysSubmitAction}.
*/
public ImportPrivateKeysSubmitAction(BitcoinController bitcoinController, MultiBitFrame mainFrame, ImportPrivateKeysPanel importPrivateKeysPanel,
ImageIcon icon, JPasswordField walletPasswordField, JPasswordField passwordField1, JPasswordField passwordField2) {
super(bitcoinController, "importPrivateKeysSubmitAction.text", "importPrivateKeysSubmitAction.tooltip",
"importPrivateKeysSubmitAction.mnemonicKey", icon);
this.mainFrame = mainFrame;
this.importPrivateKeysPanel = importPrivateKeysPanel;
this.walletPasswordField = walletPasswordField;
this.passwordField = passwordField1;
this.passwordField2 = passwordField2;
// This action is a WalletBusyListener.
super.bitcoinController.registerWalletBusyListener(this);
walletBusyChange(super.bitcoinController.getModel().getActivePerWalletModelData().isBusy());
}
/**
* Import the private keys and replay the blockchain.
*/
@Override
public void actionPerformed(ActionEvent event) {
privateKeysBackupFile = null;
if (abort()) {
return;
}
String importFilename = importPrivateKeysPanel.getOutputFilename();
if (importFilename == null || importFilename.equals("")) {
// No import file - nothing to do.
importPrivateKeysPanel.setMessageText1(controller.getLocaliser().getString(
"importPrivateKeysSubmitAction.privateKeysNothingToDo"));
importPrivateKeysPanel.setMessageText2(" ");
return;
}
// See if a wallet password is required and present.
if (super.bitcoinController.getModel().getActiveWallet() != null) {
KeyCrypter keyCrypter = super.bitcoinController.getModel().getActiveWallet().getKeyCrypter();
if (keyCrypter != null && keyCrypter.getUnderstoodEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES) {
if (walletPasswordField.getPassword() == null || walletPasswordField.getPassword().length == 0) {
importPrivateKeysPanel.setMessageText1(controller.getLocaliser().getString(
"showExportPrivateKeysAction.youMustEnterTheWalletPassword"));
importPrivateKeysPanel.setMessageText2(" ");
return;
}
try {
// See if the password is the correct wallet password.
if (!super.bitcoinController.getModel().getActiveWallet()
.checkPassword(CharBuffer.wrap(walletPasswordField.getPassword()))) {
// The password supplied is incorrect.
importPrivateKeysPanel.setMessageText1(controller.getLocaliser().getString(
"createNewReceivingAddressSubmitAction.passwordIsIncorrect"));
importPrivateKeysPanel.setMessageText2(" ");
return;
}
} catch (KeyCrypterException ede) {
log.debug(ede.getClass().getCanonicalName() + " " + ede.getMessage());
// The password supplied is probably incorrect.
importPrivateKeysPanel.setMessageText1(controller.getLocaliser().getString(
"createNewReceivingAddressSubmitAction.passwordIsIncorrect"));
importPrivateKeysPanel.setMessageText2(" ");
return;
}
}
}
setEnabled(false);
log.debug("Importing from file '" + importFilename + "'.");
File importFile = new File(importFilename);
CharSequence passwordCharSequence = CharBuffer.wrap(passwordField.getPassword());
try {
if (importPrivateKeysPanel.multiBitFileChooser.accept(importFile)) {
log.debug("Regular MultiBit import.");
PrivateKeysHandler privateKeysHandler = new PrivateKeysHandler(super.bitcoinController.getModel().getNetworkParameters());
importPrivateKeysPanel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
Collection<PrivateKeyAndDate> privateKeyAndDateArray = privateKeysHandler.readInPrivateKeys(importFile,
passwordCharSequence);
changeWalletBusyAndImportInBackground(privateKeyAndDateArray, CharBuffer.wrap(walletPasswordField.getPassword()));
importPrivateKeysPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
} else {
log.error("The wallet import file was not a recognised type.");
}
} catch (Exception e) {
log.error(e.getClass().getName() + " " + e.getMessage());
setEnabled(true);
importPrivateKeysPanel.setMessageText1(controller.getLocaliser().getString(
"importPrivateKeysSubmitAction.privateKeysUnlockFailure", new Object[] { e.getMessage() }));
importPrivateKeysPanel.setMessageText2(" ");
} finally {
importPrivateKeysPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
private void changeWalletBusyAndImportInBackground(final Collection<PrivateKeyAndDate> privateKeyAndDateArray,
final CharSequence walletPassword) {
// Double check wallet is not busy then declare that the active wallet
// is busy with the task
WalletData perWalletModelData = super.bitcoinController.getModel().getActivePerWalletModelData();
if (!perWalletModelData.isBusy()) {
perWalletModelData.setBusy(true);
perWalletModelData.setBusyTaskKey("importPrivateKeysSubmitAction.text");
perWalletModelData.setBusyTaskVerbKey("importPrivateKeysSubmitAction.verb");
importPrivateKeysPanel.setMessageText1(controller.getLocaliser().getString(
"importPrivateKeysSubmitAction.importingPrivateKeys"));
importPrivateKeysPanel.setMessageText2(" ");
super.bitcoinController.fireWalletBusyChange(true);
importPrivateKeysInBackground(privateKeyAndDateArray, walletPassword);
}
}
/**
* Import the private keys in a background Swing worker thread.
*/
private void importPrivateKeysInBackground(final Collection<PrivateKeyAndDate> privateKeyAndDateArray,
final CharSequence walletPassword) {
final WalletData finalPerWalletModelData = super.bitcoinController.getModel().getActivePerWalletModelData();
final ImportPrivateKeysPanel finalImportPanel = importPrivateKeysPanel;
final BitcoinController finalBitcoinController = super.bitcoinController;
SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
private String uiMessage = null;
@Override
protected Boolean doInBackground() throws Exception {
Boolean successMeasure = Boolean.FALSE;
boolean keyEncryptionRequired = false;
try {
Wallet walletToAddKeysTo = finalPerWalletModelData.getWallet();
Collection<byte[]> unencryptedWalletPrivateKeys = new ArrayList<byte[]>();
Date earliestTransactionDate = new Date(DateUtils.nowUtc().getMillis());
if (walletToAddKeysTo.getEncryptionType() != EncryptionType.UNENCRYPTED) {
keyEncryptionRequired = true;
}
try {
if (walletToAddKeysTo != null) {
synchronized (walletToAddKeysTo.getKeychain()) {
// Work out what the unencrypted private keys are.
KeyCrypter walletKeyCrypter = walletToAddKeysTo.getKeyCrypter();
KeyParameter aesKey = null;
if (keyEncryptionRequired) {
if (walletKeyCrypter == null) {
log.error("Missing KeyCrypter. Could not decrypt private keys.");
}
aesKey = walletKeyCrypter.deriveKey(CharBuffer.wrap(walletPassword));
}
for (ECKey ecKey : walletToAddKeysTo.getKeychain()) {
if (keyEncryptionRequired) {
if (ecKey.getEncryptedPrivateKey() == null
|| ecKey.getEncryptedPrivateKey().getEncryptedBytes() == null
|| ecKey.getEncryptedPrivateKey().getEncryptedBytes().length == 0) {
log.error("Missing encrypted private key bytes for key " + ecKey.toString()
+ ", enc.priv = "
+ Utils.bytesToHexString(ecKey.getEncryptedPrivateKey().getEncryptedBytes()));
} else {
byte[] decryptedPrivateKey = ecKey.getKeyCrypter().decrypt(
ecKey.getEncryptedPrivateKey(), aesKey);
unencryptedWalletPrivateKeys.add(decryptedPrivateKey);
}
} else {
// Wallet is not encrypted.
unencryptedWalletPrivateKeys.add(ecKey.getPrivKeyBytes());
}
}
// Keep track of earliest transaction date go backwards from now.
if (privateKeyAndDateArray != null) {
for (PrivateKeyAndDate privateKeyAndDate : privateKeyAndDateArray) {
ECKey keyToAdd = privateKeyAndDate.getKey();
if (keyToAdd != null) {
if (privateKeyAndDate.getDate() != null) {
keyToAdd.setCreationTimeSeconds(privateKeyAndDate.getDate().getTime()
/ NUMBER_OF_MILLISECONDS_IN_A_SECOND);
}
if (!keyChainContainsPrivateKey(unencryptedWalletPrivateKeys, keyToAdd, walletPassword)) {
if (keyEncryptionRequired) {
ECKey encryptedKey = new ECKey(walletKeyCrypter.encrypt(
keyToAdd.getPrivKeyBytes(), aesKey), keyToAdd.getPubKey(),
walletKeyCrypter);
walletToAddKeysTo.addKey(encryptedKey);
} else {
walletToAddKeysTo.addKey(keyToAdd);
}
// Update earliest transaction date.
if (privateKeyAndDate.getDate() == null) {
// Need to go back to the genesis block.
earliestTransactionDate = null;
} else {
if (earliestTransactionDate != null) {
earliestTransactionDate = earliestTransactionDate.before(privateKeyAndDate
.getDate()) ? earliestTransactionDate : privateKeyAndDate.getDate();
}
}
}
}
}
}
}
}
} finally {
// Wipe the work collection of private key bytes to remove it from memory.
for (byte[] privateKeyBytes : unencryptedWalletPrivateKeys) {
if (privateKeyBytes != null) {
for (int i = 0; i < privateKeyBytes.length; i++) {
privateKeyBytes[i] = 0;
}
}
}
}
log.debug(walletToAddKeysTo.toString());
finalBitcoinController.getFileHandler().savePerWalletModelData(finalPerWalletModelData, false);
finalBitcoinController.getModel().createAddressBookReceivingAddresses(finalPerWalletModelData.getWalletFilename());
// Import was successful.
uiMessage = finalBitcoinController.getLocaliser().getString("importPrivateKeysSubmitAction.privateKeysImportSuccess");
// Recalculate the bloom filter.
if (bitcoinController.getMultiBitService() != null) {
bitcoinController.getMultiBitService().recalculateFastCatchupAndFilter();
}
// Backup the private keys.
privateKeysBackupFile = finalBitcoinController.getFileHandler().backupPrivateKeys(CharBuffer.wrap(walletPassword));
// Backup the wallet and wallet info.
BackupManager.INSTANCE.backupPerWalletModelData(finalBitcoinController.getFileHandler(), finalPerWalletModelData);
// Begin blockchain replay - returns quickly - just kicks it off.
log.debug("Starting replay from date = " + earliestTransactionDate);
if (performReplay) {
List<WalletData> perWalletModelDataList = new ArrayList<WalletData>();
perWalletModelDataList.add(finalPerWalletModelData);
// Initialise the message shown in the SingleWalletPanel
if (mainFrame != null) {
WalletListPanel walletListPanel = mainFrame.getWalletsView();
if (walletListPanel != null) {
SingleWalletPanel singleWalletPanel = walletListPanel.findWalletPanelByFilename(finalPerWalletModelData.getWalletFilename());
if (singleWalletPanel != null) {
singleWalletPanel.setSyncMessage(controller.getLocaliser().getString("importPrivateKeysSubmitAction.verb"), Message.NOT_RELEVANT_PERCENTAGE_COMPLETE);
}
}
}
ReplayTask replayTask = new ReplayTask(perWalletModelDataList, earliestTransactionDate, ReplayTask.UNKNOWN_START_HEIGHT);
ReplayManager.INSTANCE.offerReplayTask(replayTask);
successMeasure = Boolean.TRUE;
}
} catch (WalletSaveException wse) {
logError(wse);
} catch (KeyCrypterException kce) {
logError(kce);
} catch (PrivateKeysHandlerException pkhe) {
logError(pkhe);
} catch (Exception e) {
logError(e);
}
return successMeasure;
}
private void logError(Exception e) {
log.error(e.getClass().getName() + " " + e.getMessage());
e.printStackTrace();
uiMessage = controller.getLocaliser().getString("importPrivateKeysSubmitAction.privateKeysImportFailure",
new Object[] { e.getMessage() });
}
@Override
protected void done() {
try {
Boolean wasSuccessful = get();
if (finalImportPanel != null && uiMessage != null) {
finalImportPanel.setMessageText1(uiMessage);
}
if (privateKeysBackupFile != null) {
try {
finalImportPanel.setMessageText2(controller.getLocaliser().getString(
"changePasswordPanel.keysBackupSuccess",
new Object[] { privateKeysBackupFile.getCanonicalPath() }));
} catch (IOException e1) {
log.debug(e1.getClass().getCanonicalName() + " " + e1.getMessage());
}
}
if (wasSuccessful) {
finalImportPanel.clearPasswords();
}
} catch (Exception e) {
// Not really used but caught so that SwingWorker shuts down cleanly.
log.error(e.getClass() + " " + e.getMessage());
}
}
};
log.debug("Importing private keys in background SwingWorker thread");
worker.execute();
}
/**
* Determine whether the key is already in the wallet.
* @throws KeyCrypterException
*/
private boolean keyChainContainsPrivateKey(Collection<byte[]> unencryptedPrivateKeys, ECKey keyToAdd, CharSequence walletPassword) throws KeyCrypterException {
if (unencryptedPrivateKeys == null || keyToAdd == null) {
return false;
} else {
byte[] unencryptedKeyToAdd = new byte[0];
if (keyToAdd.isEncrypted()) {
unencryptedKeyToAdd = keyToAdd.getKeyCrypter().decrypt(keyToAdd.getEncryptedPrivateKey(), keyToAdd.getKeyCrypter().deriveKey(walletPassword));
}
for (byte[] loopEncryptedPrivateKey : unencryptedPrivateKeys) {
if (Arrays.equals(unencryptedKeyToAdd, loopEncryptedPrivateKey)) {
return true;
}
}
return false;
}
}
// Used in testing.
public void setPerformReplay(boolean performReplay) {
this.performReplay = performReplay;
}
@Override
public void walletBusyChange(boolean newWalletIsBusy) {
// Update the enable status of the action to match the wallet busy status.
if (super.bitcoinController.getModel().getActivePerWalletModelData().isBusy()) {
// Wallet is busy with another operation that may change the private keys - Action is disabled.
putValue(SHORT_DESCRIPTION, controller.getLocaliser().getString("multiBitSubmitAction.walletIsBusy",
new Object[]{controller.getLocaliser().getString(this.bitcoinController.getModel().getActivePerWalletModelData().getBusyTaskKey())}));
setEnabled(false);
} else {
// Enable
putValue(SHORT_DESCRIPTION, controller.getLocaliser().getString("importPrivateKeysSubmitAction.text"));
setEnabled(true);
}
}
}