/**
* Copyright 2013 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.file;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.List;
import junit.framework.TestCase;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.junit.Before;
import org.junit.Test;
import org.multibit.Constants;
import org.multibit.CreateControllers;
import org.multibit.controller.bitcoin.BitcoinController;
import org.multibit.model.bitcoin.WalletData;
import org.multibit.model.bitcoin.WalletInfoData;
import org.multibit.store.MultiBitWalletVersion;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.Arrays;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.protobuf.ByteString;
public class BackupManagerTest extends TestCase {
private static final String TEST_FILE_COPY_AND_ENCRYPT = "testFileCopyAndEncrypt";
private static final String TEST_BACKUP_WALLET_UNENCRYPTED = "testBackupWalletUnencrypted";
private static final String TEST_BACKUP_WALLET_ENCRYPTED = "testBackupWalletEncrypted";
private final CharSequence WALLET_PASSWORD = "horatio nelson 123";
public static final String CIPHER_TESTDATA_DIRECTORY = "cipher";
public static final String CIPHER_WALLET_VERSION_0_FILENAME = "qwertyuiop-version-0.wallet.cipher";
public static final String CIPHER_WALLET_VERSION_FF_FILENAME = "qwertyuiop-version-ff.wallet.cipher";
public static final String CIPHER_WALLET_PASSWORD = "qwertyuiop";
private BitcoinController controller;
@Before
@Override
public void setUp() throws Exception {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
}
@Test
public void testFileCopyAndEncrypt() throws IOException {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
File temporaryWallet = File.createTempFile(TEST_FILE_COPY_AND_ENCRYPT, ".wallet");
temporaryWallet.deleteOnExit();
File temporaryWalletCopy = File.createTempFile(TEST_FILE_COPY_AND_ENCRYPT, ".wallet.cipher");
temporaryWalletCopy.deleteOnExit();
temporaryWalletCopy.delete();
String newWalletFilename = temporaryWallet.getAbsolutePath();
// Create a new protobuf wallet.
Wallet newWallet = new Wallet(NetworkParameters.prodNet());
ECKey newKey = new ECKey();
newWallet.getKeychain().add(newKey);
newKey = new ECKey();
newWallet.getKeychain().add(newKey);
WalletData perWalletModelData = new WalletData();
WalletInfoData walletInfo = new WalletInfoData(newWalletFilename, newWallet, MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(newWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_FILE_COPY_AND_ENCRYPT);
// Save the wallet
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Copy the wallet and encrypt the whole file.
BackupManager.INSTANCE.copyFileAndEncrypt(temporaryWallet, temporaryWalletCopy, WALLET_PASSWORD);
// Read the file back and decrypt it.
byte[] decryptedWalletBytes = BackupManager.INSTANCE.readFileAndDecrypt(temporaryWalletCopy, WALLET_PASSWORD);
// The decrypted bytes should match what is in the temporaryWallet file.
byte[] sourceBytes = FileHandler.read(temporaryWallet);
assertEquals("Wrong length of file after encrypt save roundtrip", sourceBytes.length, decryptedWalletBytes.length);
assertTrue("The wallet after the encrypt save roundtrip has changed", Arrays.areEqual(sourceBytes, decryptedWalletBytes));
}
@Test
public void checkSaltAndIVLength() {
// If something changes in the KeyCrypterScrypt it would cause backwards compatibility problems reading and writing
// the encrypted backup files so check them.
assertTrue("The salt length seems to have changed from length 8 bytes", BackupManager.EXPECTED_LENGTH_OF_SALT == KeyCrypterScrypt.SALT_LENGTH);
assertTrue("The initialisation vector length seems to have changed from length 16 bytes", BackupManager.EXPECTED_LENGTH_OF_IV == KeyCrypterScrypt.BLOCK_LENGTH);
}
@Test
public void testReadCipherVersion0() throws Exception {
File directory = new File(".");
String currentPath = directory.getAbsolutePath();
String testDirectory = currentPath + File.separator + Constants.TESTDATA_DIRECTORY + File.separator
+ CIPHER_TESTDATA_DIRECTORY;
File walletFile = new File (testDirectory + File.separator + CIPHER_WALLET_VERSION_0_FILENAME);
// Read in the version 0 wallet and decrypt it.
byte[] walletBytes = BackupManager.INSTANCE.readFileAndDecrypt(walletFile, CIPHER_WALLET_PASSWORD);
InputStream walletInputStream = new ByteArrayInputStream(walletBytes);
Wallet wallet = null;
try {
wallet = Wallet.loadFromFileStream(walletInputStream);
} finally {
walletInputStream.close();
}
// Check the private keys are reborn ok.
assertNotNull(wallet);
assertEquals("Wrong number of private keys in decrypted wallet file", 3, wallet.getKeychainSize());
List<ECKey> keys = wallet.getKeychain();
assertEquals("Wrong private key 0", "0ec932ea4f6b305247c12c0a4fb310d839a688ac0011d69724e6a32bc35fcda8", Utils.bytesToHexString(keys.get(0).getPrivKeyBytes()));
assertEquals("Wrong private key 1", "02ac2c94e44fc2edab9585111480d6d438ebe8c96faf92e561957a48d2bbdacc", Utils.bytesToHexString(keys.get(1).getPrivKeyBytes()));
assertEquals("Wrong private key 2", "b01a936b78b6a649ea0ede2182eb73629d5ca3200d36a48b1006eb6729c1bc15", Utils.bytesToHexString(keys.get(2).getPrivKeyBytes()));
}
@Test
/**
* Check that future versions of encrypted files cannot be loaded.
* (This test wallet has a version of 0xff in it).
*/
public void testReadCipherVersionff() throws Exception {
File directory = new File(".");
String currentPath = directory.getAbsolutePath();
String testDirectory = currentPath + File.separator + Constants.TESTDATA_DIRECTORY + File.separator
+ CIPHER_TESTDATA_DIRECTORY;
File walletFile = new File (testDirectory + File.separator + CIPHER_WALLET_VERSION_FF_FILENAME);
// Read in the version ff wallet and decrypt it.
InputStream walletInputStream = null;
try {
byte[] walletBytes = BackupManager.INSTANCE.readFileAndDecrypt(walletFile, CIPHER_WALLET_PASSWORD);
walletInputStream = new ByteArrayInputStream(walletBytes);
Wallet.loadFromFileStream(walletInputStream);
fail("Wallet with a future encrypted file version loaded but it should not");
} catch(IOException ioe) {
// Success.
} finally {
if (walletInputStream != null) {
walletInputStream.close();
}
}
}
@Test
public void testBackupWalletUnencrypted() throws IOException {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
File temporaryWallet = File.createTempFile(TEST_BACKUP_WALLET_UNENCRYPTED, ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
// Create a new protobuf wallet - unencrypted.
Wallet newWallet = new Wallet(NetworkParameters.prodNet());
ECKey newKey = new ECKey();
newWallet.getKeychain().add(newKey);
newKey = new ECKey();
newWallet.getKeychain().add(newKey);
WalletData perWalletModelData = new WalletData();
WalletInfoData walletInfo = new WalletInfoData(newWalletFilename, newWallet, MultiBitWalletVersion.PROTOBUF);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(newWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_BACKUP_WALLET_UNENCRYPTED);
// Check there are initially no backup wallets.
List<File> backupWallets = BackupManager.INSTANCE.getWalletsInBackupDirectory(newWalletFilename, "wallet-unenc-backup");
assertNotNull("Null backupWallets list returned", backupWallets);
assertEquals("Wring number of backup wallets", 0, backupWallets.size());
// Save the wallet.
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Backup the wallet.
BackupManager.INSTANCE.backupPerWalletModelData(controller.getFileHandler(), perWalletModelData);
// Check that a backup copy has been saved in the data/wallet-unenc-backup directory
backupWallets = BackupManager.INSTANCE.getWalletsInBackupDirectory(newWalletFilename, "wallet-unenc-backup");
assertNotNull("Null backupWallets list returned", backupWallets);
assertEquals("Wring number of backup wallets", 1, backupWallets.size());
// Read the originally saved wallet back in.
byte[] originalBytes = FileHandler.read(temporaryWallet);
// Read the backup wallet back in.
byte[] backupBytes = FileHandler.read(backupWallets.get(0));
assertNotNull("The originally saved wallet was not read back in ok.1", originalBytes);
assertTrue("The originally saved wallet was not read back in ok.2", originalBytes.length > 0);
assertEquals("Wrong length of file after backup", originalBytes.length, backupBytes.length);
assertTrue("The wallet after the backup has changed", Arrays.areEqual(originalBytes, backupBytes));
}
@Test
public void testBackupWalletEncrypted() throws IOException {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
File temporaryWallet = File.createTempFile(TEST_BACKUP_WALLET_ENCRYPTED, ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
// Create a new protobuf wallet - encrypted.
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
ScryptParameters scryptParameters = scryptParametersBuilder.build();
KeyCrypter keyCrypter = new KeyCrypterScrypt(scryptParameters);
Wallet encryptedWallet = new Wallet(NetworkParameters.prodNet(), keyCrypter);
WalletData perWalletModelData = new WalletData();
WalletInfoData walletInfo = new WalletInfoData(newWalletFilename, encryptedWallet, MultiBitWalletVersion.PROTOBUF);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(encryptedWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_BACKUP_WALLET_ENCRYPTED);
// Check there are initially no backup wallets.
List<File> backupWallets = BackupManager.INSTANCE.getWalletsInBackupDirectory(newWalletFilename, "wallet-backup");
assertNotNull("Null backupWallets list returned", backupWallets);
assertEquals("Wring number of backup wallets", 0, backupWallets.size());
// Save the wallet.
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Backup the wallet.
BackupManager.INSTANCE.backupPerWalletModelData(controller.getFileHandler(), perWalletModelData);
// Check that a backup copy has been saved in the data/wallet-backup directory
backupWallets = BackupManager.INSTANCE.getWalletsInBackupDirectory(newWalletFilename, "wallet-backup");
assertNotNull("Null backupWallets list returned", backupWallets);
assertEquals("Wring number of backup wallets", 1, backupWallets.size());
// Read the originally saved wallet back in.
byte[] originalBytes = FileHandler.read(temporaryWallet);
// Read the backup wallet back in.
byte[] backupBytes = FileHandler.read(backupWallets.get(0));
assertNotNull("The originally saved wallet was not read back in ok.1", originalBytes);
assertTrue("The originally saved wallet was not read back in ok.2", originalBytes.length > 0);
assertEquals("Wrong length of file after backup", originalBytes.length, backupBytes.length);
assertTrue("The wallet after the backup has changed", Arrays.areEqual(originalBytes, backupBytes));
}
}