/**
* 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.file;
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;
import org.bitcoinj.wallet.Protos;
import org.bitcoinj.wallet.Protos.ScryptParameters;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
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.multibit.store.WalletVersionException;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Iterator;
import static junit.framework.Assert.*;
public class FileHandlerTest {
private static final String WALLET_TESTDATA_DIRECTORY = "wallets";
private static final String WALLET_FUTURE = "future.wallet";
private static final String TEST_CREATE_UNENCRYPTED_PROTOBUF_PREFIX = "testCreateUnencryptedProtobuf";
private static final String TEST_CREATE_ENCRYPTED_PROTOBUF_PREFIX = "testCreateEncryptedProtobuf";
private static final String TEST_CREATE_PROTOBUF_PREFIX = "testCreateProtobuf";
private static final String TEST_WALLET_VERSION_PREFIX = "testCannotFutureWalletVersions";
private static final String TEST_WALLET_VERSION_2_PREFIX = "testWalletVersion";
private static final String TEST_SCRYPT_PARAMETERS = "testScryptParameters";
private final CharSequence WALLET_PASSWORD = "horatio nelson 123";
private SecureRandom secureRandom;
private BitcoinController controller;
private FileHandler fileHandler;
@Before
public void setUp() throws Exception {
secureRandom = new SecureRandom();
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));
ScryptParameters scryptParameters = scryptParametersBuilder.build();
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
fileHandler = new FileHandler(controller);
}
@Test
public void testCreateProtobufUnencryptedWallet() throws IOException {
File temporaryWallet = File.createTempFile(TEST_CREATE_UNENCRYPTED_PROTOBUF_PREFIX, ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
// Create a new unencrypted (vanilla) 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);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(newWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_CREATE_UNENCRYPTED_PROTOBUF_PREFIX);
// Check the wallet status before it is written out and reborn.
assertEquals(MultiBitWalletVersion.PROTOBUF, perWalletModelData.getWalletInfo().getWalletVersion());
assertTrue("Wallet is not UNENCRYPTED when it should be", perWalletModelData.getWallet().getEncryptionType() == EncryptionType.UNENCRYPTED);
// Save the wallet and then read it back in.
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Check the wallet and wallet info file exists.
File newWalletFile = new File(newWalletFilename);
assertTrue(newWalletFile.exists());
String walletInfoFileAsString = WalletInfoData.createWalletInfoFilename(newWalletFilename);
File walletInfoFile = new File(walletInfoFileAsString);
assertTrue(walletInfoFile.exists());
// Check wallet can be loaded and is still protobuf and unencrypted.
// Note - when reborn it is reborn as an EncryptableWallet.
WalletData perWalletModelDataReborn = fileHandler.loadFromFile(newWalletFile);
assertNotNull(perWalletModelDataReborn);
assertEquals(BigInteger.ZERO, perWalletModelDataReborn.getWallet().getBalance());
assertEquals(TEST_CREATE_UNENCRYPTED_PROTOBUF_PREFIX, perWalletModelDataReborn.getWalletDescription());
assertEquals(2, perWalletModelDataReborn.getWallet().getKeychain().size());
assertEquals(MultiBitWalletVersion.PROTOBUF, perWalletModelDataReborn.getWalletInfo().getWalletVersion());
assertTrue("Wallet is not UNENCRYPTED when it should be", perWalletModelDataReborn.getWallet().getEncryptionType() == EncryptionType.UNENCRYPTED);
deleteWalletAndCheckDeleted(perWalletModelDataReborn, newWalletFile, walletInfoFile);
}
@Test
public void testCreateProtobufEncryptedWallet() throws Exception {
// Create an encrypted wallet.
File temporaryWallet = File.createTempFile(TEST_CREATE_ENCRYPTED_PROTOBUF_PREFIX, ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
KeyCrypterScrypt initialKeyCrypter = new KeyCrypterScrypt();
System.out.println("InitialKeyCrypter = " + initialKeyCrypter);
Wallet newWallet = new Wallet(NetworkParameters.prodNet(), initialKeyCrypter);
assertEquals(MultiBitWalletVersion.PROTOBUF_ENCRYPTED, newWallet.getVersion());
ECKey newKey = new ECKey();
// Copy the private key bytes for checking later.
byte[] originalPrivateKeyBytes1 = new byte[32];
System.arraycopy(newKey.getPrivKeyBytes(), 0, originalPrivateKeyBytes1, 0, 32);
System.out.println("EncryptableECKeyTest - Original private key 1 = " + Utils.bytesToHexString(originalPrivateKeyBytes1));
newKey = newKey.encrypt(newWallet.getKeyCrypter(), newWallet.getKeyCrypter().deriveKey(WALLET_PASSWORD));
newWallet.addKey(newKey);
newKey = new ECKey();
byte[] originalPrivateKeyBytes2 = new byte[32];
System.arraycopy(newKey.getPrivKeyBytes(), 0, originalPrivateKeyBytes2, 0, 32);
System.out.println("EncryptableECKeyTest - Original private key 2 = " + Utils.bytesToHexString(originalPrivateKeyBytes2));
newKey = newKey.encrypt(newWallet.getKeyCrypter(), newWallet.getKeyCrypter().deriveKey(WALLET_PASSWORD));
newWallet.addKey(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_CREATE_ENCRYPTED_PROTOBUF_PREFIX);
// Check the wallet status before it is written out and reborn.
assertEquals(MultiBitWalletVersion.PROTOBUF_ENCRYPTED, perWalletModelData.getWalletInfo().getWalletVersion());
assertTrue("Wallet is not ENCRYPTED when it should be", perWalletModelData.getWallet().getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Get the keys of the wallet and check that all the keys are encrypted.
Collection<ECKey> keys = newWallet.getKeychain();
for (ECKey key : keys) {
assertTrue("Key is not encrypted when it should be", key.isEncrypted());
}
// Save the wallet and read it back in again.
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Check the wallet and wallet info file exists.
File newWalletFile = new File(newWalletFilename);
assertTrue(newWalletFile.exists());
String walletInfoFileAsString = WalletInfoData.createWalletInfoFilename(newWalletFilename);
File walletInfoFile = new File(walletInfoFileAsString);
assertTrue(walletInfoFile.exists());
// Check wallet can be loaded and is still protobuf and encrypted.
WalletData perWalletModelDataReborn = fileHandler.loadFromFile(newWalletFile);
assertNotNull(perWalletModelDataReborn);
assertEquals(BigInteger.ZERO, perWalletModelDataReborn.getWallet().getBalance());
assertEquals(TEST_CREATE_ENCRYPTED_PROTOBUF_PREFIX, perWalletModelDataReborn.getWalletDescription());
assertEquals(2, perWalletModelDataReborn.getWallet().getKeychain().size());
assertEquals(MultiBitWalletVersion.PROTOBUF_ENCRYPTED, perWalletModelDataReborn.getWalletInfo().getWalletVersion());
assertTrue("Wallet is not of type ENCRYPTED when it should be", perWalletModelDataReborn.getWallet().getEncryptionType() == EncryptionType.ENCRYPTED_SCRYPT_AES);
// Get the keys out the reborn wallet and check that all the keys are encrypted.
Collection<ECKey> rebornEncryptedKeys = perWalletModelDataReborn.getWallet().getKeychain();
for (ECKey key : rebornEncryptedKeys) {
assertTrue("Key is not encrypted when it should be", key.isEncrypted());
}
System.out.println("Reborn KeyCrypter = " + perWalletModelDataReborn.getWallet().getKeyCrypter());
// Decrypt the reborn wallet.
perWalletModelDataReborn.getWallet().decrypt(perWalletModelDataReborn.getWallet().getKeyCrypter().deriveKey(WALLET_PASSWORD));
// Get the keys out the reborn wallet and check that all the keys match.
Collection<ECKey> rebornKeys = perWalletModelDataReborn.getWallet().getKeychain();
assertEquals("Wrong number of keys in reborn wallet", 2, rebornKeys.size());
Iterator<ECKey> iterator = rebornKeys.iterator();
ECKey firstRebornKey = iterator.next();
assertTrue("firstRebornKey should now de decrypted but is not", !firstRebornKey.isEncrypted());
// The reborn unencrypted private key bytes should match the original private key.
byte[] firstRebornPrivateKeyBytes = firstRebornKey.getPrivKeyBytes();
System.out.println("FileHandlerTest - Reborn decrypted first private key = " + Utils.bytesToHexString(firstRebornPrivateKeyBytes));
for (int i = 0; i < firstRebornPrivateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the reborn first private key did not match the original", originalPrivateKeyBytes1[i], firstRebornPrivateKeyBytes[i]);
}
ECKey secondRebornKey = iterator.next();
assertTrue("secondRebornKey should now de decrypted but is not", !secondRebornKey.isEncrypted());
// The reborn unencrypted private key bytes should match the original private key.
byte[] secondRebornPrivateKeyBytes = secondRebornKey.getPrivKeyBytes();
System.out.println("FileHandlerTest - Reborn decrypted second private key = " + Utils.bytesToHexString(secondRebornPrivateKeyBytes));
for (int i = 0; i < secondRebornPrivateKeyBytes.length; i++) {
assertEquals("Byte " + i + " of the reborn second private key did not match the original", originalPrivateKeyBytes2[i], secondRebornPrivateKeyBytes[i]);
}
deleteWalletAndCheckDeleted(perWalletModelDataReborn, newWalletFile, walletInfoFile);
}
@Test
public void testDefaultScryptParameters() throws Exception {
// Create an encrypted wallet with default scrypt parameters.
File temporaryWallet = File.createTempFile(TEST_SCRYPT_PARAMETERS + "1", ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
KeyCrypter testKeyCrypter = new KeyCrypterScrypt();
Wallet newWallet = new Wallet(NetworkParameters.prodNet(), testKeyCrypter);
ECKey newKey = new ECKey();
newKey = newKey.encrypt(newWallet.getKeyCrypter(), newWallet.getKeyCrypter().deriveKey(WALLET_PASSWORD));
newWallet.addKey(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_SCRYPT_PARAMETERS);
// Save the wallet and read it back in again.
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Check the wallet and wallet info file exists.
File newWalletFile = new File(newWalletFilename);
String walletInfoFileAsString = WalletInfoData.createWalletInfoFilename(newWalletFilename);
File walletInfoFile = new File(walletInfoFileAsString);
// Load the wallet and check the default scrypt parameters
WalletData perWalletModelDataReborn = fileHandler.loadFromFile(newWalletFile);
assertNotNull(perWalletModelDataReborn);
KeyCrypter rebornEncrypterDecrypter = perWalletModelDataReborn.getWallet().getKeyCrypter();
assertNotNull("There was no encrypterDecrypter after round trip", rebornEncrypterDecrypter);
assertTrue("EncrypterDecrypter was not an EncrypterDecrypterScrypt", rebornEncrypterDecrypter instanceof KeyCrypterScrypt);
KeyCrypterScrypt rebornEncrypterDecrypterScrypt = (KeyCrypterScrypt)rebornEncrypterDecrypter;
assertEquals("Wrong N parameter", 16384, rebornEncrypterDecrypterScrypt.getScryptParameters().getN());
assertEquals("Wrong R parameter", 8, rebornEncrypterDecrypterScrypt.getScryptParameters().getR());
assertEquals("Wrong P parameter", 1, rebornEncrypterDecrypterScrypt.getScryptParameters().getP());
deleteWalletAndCheckDeleted(perWalletModelDataReborn, newWalletFile, walletInfoFile);
}
@Test
public void testNonDefaultScryptParameters() throws Exception {
// Non default scrypt parameters.
int n = 32768;
int r = 8;
int p = 3;
byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH];
secureRandom.nextBytes(salt);
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder()
.setSalt(ByteString.copyFrom(salt)).setN(n).setR(r).setP(p);
ScryptParameters scryptParameters = scryptParametersBuilder.build();
KeyCrypter testKeyCrypter = new KeyCrypterScrypt(scryptParameters);
// Create an encrypted wallet with nondefault scrypt parameters.
File temporaryWallet = File.createTempFile(TEST_SCRYPT_PARAMETERS + "2", ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
Wallet newWallet = new Wallet(NetworkParameters.prodNet(), testKeyCrypter);
ECKey newKey = new ECKey();
newKey = newKey.encrypt(newWallet.getKeyCrypter(), newWallet.getKeyCrypter().deriveKey(WALLET_PASSWORD));
newWallet.addKey(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_SCRYPT_PARAMETERS + "2");
// Save the wallet and read it back in again.
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Check the wallet and wallet info file exists.
File newWalletFile = new File(newWalletFilename);
String walletInfoFileAsString = WalletInfoData.createWalletInfoFilename(newWalletFilename);
File walletInfoFile = new File(walletInfoFileAsString);
// Load the wallet and check the default scrypt parameters
WalletData perWalletModelDataReborn = fileHandler.loadFromFile(newWalletFile);
assertNotNull(perWalletModelDataReborn);
KeyCrypter rebornKeyCrypter = perWalletModelDataReborn.getWallet().getKeyCrypter();
assertNotNull("There was no keyCrypter after round trip", rebornKeyCrypter);
assertTrue("EncrypterDecrypter was not an KeyCrypterScrypt", rebornKeyCrypter instanceof KeyCrypterScrypt);
KeyCrypterScrypt rebornKeyCrypterScrypt = (KeyCrypterScrypt)rebornKeyCrypter;
assertEquals("Wrong N parameter", n, rebornKeyCrypterScrypt.getScryptParameters().getN());
assertEquals("Wrong R parameter", r, rebornKeyCrypterScrypt.getScryptParameters().getR());
assertEquals("Wrong P parameter", p, rebornKeyCrypterScrypt.getScryptParameters().getP());
deleteWalletAndCheckDeleted(perWalletModelDataReborn, newWalletFile, walletInfoFile);
}
private void deleteWalletAndCheckDeleted(WalletData perWalletModelData, File walletFile, File walletInfoFile) {
// Delete wallet and check it is deleted.
fileHandler.deleteWalletAndWalletInfo(perWalletModelData);
assertTrue(!walletFile.exists());
assertTrue(!walletInfoFile.exists());
}
@Test
public void testCannotLoadOrSaveFutureWalletVersions() throws IOException {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
File temporaryWallet = File.createTempFile(TEST_WALLET_VERSION_PREFIX, ".wallet");
temporaryWallet.deleteOnExit();
String newWalletFilename = temporaryWallet.getAbsolutePath();
// Create a new protobuf wallet with a future wallet version
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.FUTURE);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(newWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_CREATE_PROTOBUF_PREFIX);
// Should not be able to save a wallet version from the future.
try {
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
fail("Could save a wallet version from the future but should not be able to");
} catch (WalletVersionException wve) {
// Expected result.
}
// Check a wallet from the future cannot be loaded.
File directory = new File(".");
String currentPath = directory.getAbsolutePath();
String futureWalletName = currentPath + File.separator + Constants.TESTDATA_DIRECTORY + File.separator
+ WALLET_TESTDATA_DIRECTORY + File.separator + WALLET_FUTURE;
try {
File futureWalletFile = new File(futureWalletName);
fileHandler.loadFromFile(futureWalletFile);
fail("Could load a wallet version from the future but should not be able to");
} catch (WalletVersionException wve) {
// Expected result.
}
}
@Test
public void testWalletVersion2a() throws IOException {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
File temporaryWallet = File.createTempFile(TEST_WALLET_VERSION_2_PREFIX, ".wallet");
temporaryWallet.deleteOnExit();
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);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(newWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_WALLET_VERSION_2_PREFIX);
// Save the wallet
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Check the version gets round tripped.
WalletData perWalletModelDataReborn = fileHandler.loadFromFile(new File(newWalletFilename));
assertNotNull(perWalletModelDataReborn);
WalletInfoData rebornWalletInfo = perWalletModelDataReborn.getWalletInfo();
assertEquals("Wallet version was not roundtripped", MultiBitWalletVersion.PROTOBUF, rebornWalletInfo.getWalletVersion());;
}
@Test
public void testWalletVersion2b() throws IOException {
// Create MultiBit controller.
final CreateControllers.Controllers controllers = CreateControllers.createControllers();
controller = controllers.bitcoinController;
File temporaryWallet = File.createTempFile(TEST_WALLET_VERSION_2_PREFIX, ".wallet");
temporaryWallet.deleteOnExit();
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();
// The wallet info incorrectly states it is encrypted but the wallet is not encrypted.
WalletInfoData walletInfo = new WalletInfoData(newWalletFilename, newWallet, MultiBitWalletVersion.PROTOBUF_ENCRYPTED);
perWalletModelData.setWalletInfo(walletInfo);
perWalletModelData.setWallet(newWallet);
perWalletModelData.setWalletFilename(newWalletFilename);
perWalletModelData.setWalletDescription(TEST_WALLET_VERSION_2_PREFIX);
// Save the wallet
controller.getFileHandler().savePerWalletModelData(perWalletModelData, true);
// Check the version gets round tripped.
WalletData perWalletModelDataReborn = fileHandler.loadFromFile(new File(newWalletFilename));
assertNotNull(perWalletModelDataReborn);
WalletInfoData rebornWalletInfo = perWalletModelDataReborn.getWalletInfo();
assertEquals("Wallet version was incorrect.", MultiBitWalletVersion.PROTOBUF, rebornWalletInfo.getWalletVersion());;
}
}