MultiBit HD / Technical library
INDEPENDENT KEYCHAINX RESOURCE
WalletManager: creation and loading
Coordinates creation, encrypted storage, wallet loading and wallet-type inference. Compare the legacy noncompliant creation method with the BIP32 method. Follow the encrypted-file and wallet-summary dependencies rather than reading this large class in isolation.
Source inspection only. Historical Java code; not executed or security-audited for this publication.
mbhd-core/src/main/java/org/multibit/hd/core/managers/WalletManager.java
View the exact upstream file · Read raw source · License notices
SHA-256: 58fbae9322d5ad7e1881985a4170f36dd7f31a4a504fc68ce3c923432df6cb93
1package org.multibit.hd.core.managers;23import com.google.common.base.Charsets;4import com.google.common.base.Optional;5import com.google.common.base.Preconditions;6import com.google.common.base.Strings;7import com.google.common.collect.ImmutableList;8import com.google.common.collect.Lists;9import com.google.common.io.Files;10import com.google.common.primitives.Bytes;11import com.google.common.util.concurrent.FutureCallback;12import com.google.common.util.concurrent.Futures;13import com.google.common.util.concurrent.ListenableFuture;14import com.google.common.util.concurrent.ListeningExecutorService;15import org.bitcoinj.core.*;16import org.bitcoinj.crypto.*;17import org.bitcoinj.script.Script;18import org.bitcoinj.store.BlockStore;19import org.bitcoinj.store.BlockStoreException;20import org.bitcoinj.store.UnreadableWalletException;21import org.bitcoinj.store.WalletProtobufSerializer;22import org.bitcoinj.wallet.DeterministicSeed;23import org.bitcoinj.wallet.Protos;24import org.joda.time.DateTime;25import org.multibit.commons.concurrent.SafeExecutors;26import org.multibit.commons.crypto.AESUtils;27import org.multibit.commons.files.SecureFiles;28import org.multibit.commons.utils.Dates;29import org.multibit.hd.brit.core.extensions.MatcherResponseWalletExtension;30import org.multibit.hd.brit.core.extensions.SendFeeDtoWalletExtension;31import org.multibit.hd.brit.core.seed_phrase.Bip39SeedPhraseGenerator;32import org.multibit.hd.brit.core.seed_phrase.SeedPhraseGenerator;33import org.multibit.hd.core.config.Configurations;34import org.multibit.hd.core.config.Yaml;35import org.multibit.hd.core.crypto.EncryptedFileReaderWriter;36import org.multibit.hd.core.dto.*;37import org.multibit.hd.core.error_reporting.ExceptionHandler;38import org.multibit.hd.core.events.CoreEvents;39import org.multibit.hd.core.events.ShutdownEvent;40import org.multibit.hd.core.events.TransactionSeenEvent;41import org.multibit.hd.core.events.WalletLoadEvent;42import org.multibit.hd.core.exceptions.WalletLoadException;43import org.multibit.hd.core.exceptions.WalletSaveException;44import org.multibit.hd.core.exceptions.WalletVersionException;45import org.multibit.hd.core.extensions.WalletTypeExtension;46import org.multibit.hd.core.files.EncryptedWalletFile;47import org.multibit.hd.core.services.BackupService;48import org.multibit.hd.core.services.BitcoinNetworkService;49import org.multibit.hd.core.services.CoreServices;50import org.multibit.hd.core.utils.BitcoinNetwork;51import org.multibit.hd.core.utils.Collators;52import org.multibit.hd.core.wallet.UnconfirmedTransactionDetector;53import org.slf4j.Logger;54import org.slf4j.LoggerFactory;55import org.spongycastle.crypto.params.KeyParameter;5657import javax.annotation.Nullable;58import java.io.*;59import java.security.NoSuchAlgorithmException;60import java.security.SecureRandom;61import java.security.SignatureException;62import java.util.*;63import java.util.concurrent.Callable;64import java.util.concurrent.TimeUnit;65import java.util.regex.Pattern;6667import static org.multibit.hd.core.dto.WalletId.*;6869/**70 * <p>Manager to provide the following to core users:</p>71 * <ul>72 * <li>create wallet</li>73 * <li>save wallet wallet</li>74 * <li>load wallet wallet</li>75 * <li>tracks the current wallet and the list of wallet directories</li>76 * </ul>77 * <p/>78 */79public enum WalletManager implements WalletEventListener {8081 INSTANCE {82 @Override83 public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {84 // Emit an event so that GUI elements can update as required85 Coin value = tx.getValue(wallet);86 log.debug("Received transaction {} with value {}", tx, value);8788 CoreEvents.fireTransactionSeenEvent(new TransactionSeenEvent(tx, value));89 }9091 @Override92 public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {93 // Emit an event so that GUI elements can update as required94 Coin value = tx.getValue(wallet);95 CoreEvents.fireTransactionSeenEvent(new TransactionSeenEvent(tx, value));96 }9798 @Override99 public void onReorganize(Wallet wallet) {100101 }102103 @Override104 public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {105 // Emit an event so that GUI elements can update as required106 if (tx != null) {107 Coin value = tx.getValue(wallet);108 CoreEvents.fireTransactionSeenEvent(new TransactionSeenEvent(tx, value));109 }110 }111112 @Override113 public void onWalletChanged(Wallet wallet) {114115 }116117 @Override118 public void onKeysAdded(List<ECKey> keys) {119120 }121122 @Override123 public void onScriptsChanged(Wallet wallet, List<Script> scripts, boolean isAddingScripts) {124125 }126 };127128 private static final int AUTO_SAVE_DELAY = 60000; // milliseconds129130 // TODO (GR) Refactor this to be injected131 private static final NetworkParameters networkParameters = BitcoinNetwork.current().get();132133 private static final Logger log = LoggerFactory.getLogger(WalletManager.class);134135 /**136 * The earliest possible HD wallet.137 * Set after discussions on the bitcoinj mailing list:138 * https://groups.google.com/forum/#!topic/bitcoinj/288mCHhLMrA139 */140 public static final String EARLIEST_HD_WALLET_DATE = "2014-05-01";141142 public static final String WALLET_DIRECTORY_PREFIX = "mbhd";143 // The format of the wallet directories is WALLET_DIRECTORY_PREFIX + a wallet id.144 // A wallet id is 5 groups of 4 bytes in lowercase hex, with a "-' separator e.g. mbhd-11111111-22222222-33333333-44444444-55555555145 private static final String REGEX_FOR_WALLET_DIRECTORY = "^"146 + WALLET_DIRECTORY_PREFIX147 + WALLET_ID_SEPARATOR148 + "[0-9a-f]{8}"149 + WALLET_ID_SEPARATOR150 + "[0-9a-f]{8}"151 + WALLET_ID_SEPARATOR152 + "[0-9a-f]{8}"153 + WALLET_ID_SEPARATOR154 + "[0-9a-f]{8}"155 + WALLET_ID_SEPARATOR156 + "[0-9a-f]{8}$";157158 private static final Pattern walletDirectoryPattern = Pattern.compile(REGEX_FOR_WALLET_DIRECTORY);159160 /**161 * The wallet version number for protobuf encrypted wallets - compatible with MultiBit Classic162 */163 public static final int MBHD_WALLET_VERSION = 1;164 public static final String MBHD_WALLET_PREFIX = "mbhd";165 public static final String MBHD_WALLET_SUFFIX = ".wallet";166 public static final String MBHD_AES_SUFFIX = ".aes";167 public static final String MBHD_SUMMARY_SUFFIX = ".yaml";168 public static final String MBHD_WALLET_NAME = MBHD_WALLET_PREFIX + MBHD_WALLET_SUFFIX;169 public static final String MBHD_SUMMARY_NAME = MBHD_WALLET_PREFIX + MBHD_SUMMARY_SUFFIX;170 public static final int LOOK_AHEAD_SIZE = 50; // A smaller look ahead size than the bitcoinj default of 100 (speeds up syncing as te bloom filters are smaller)171 public static final long MAXIMUM_WALLET_CREATION_DELTA = 180 * 1000; // 3 minutes in millis172173 private Optional<WalletSummary> currentWalletSummary = Optional.absent();174175 private static final SecureRandom random = new SecureRandom();176177 /**178 * The initialisation vector to use for AES encryption of output files (such as wallets)179 * There is no particular significance to the value of these bytes180 */181 private static final byte[] AES_INITIALISATION_VECTOR = new byte[]{(byte) 0xa3, (byte) 0x44, (byte) 0x39, (byte) 0x1f, (byte) 0x53, (byte) 0x83, (byte) 0x11,182 (byte) 0xb3, (byte) 0x29, (byte) 0x54, (byte) 0x86, (byte) 0x16, (byte) 0xc4, (byte) 0x89, (byte) 0x72, (byte) 0x3e};183184 /**185 * The salt used for deriving the KeyParameter from the credentials in AES encryption for wallets186 */187 private static final byte[] SCRYPT_SALT = new byte[]{(byte) 0x35, (byte) 0x51, (byte) 0x03, (byte) 0x80, (byte) 0x75, (byte) 0xa3, (byte) 0xb0, (byte) 0xc5};188189 private ListeningExecutorService walletExecutorService = null;190191 /**192 * @return A copy of the AES initialisation vector193 */194 public static byte[] deprecatedFixedAesInitializationVector() {195 return Arrays.copyOf(AES_INITIALISATION_VECTOR, AES_INITIALISATION_VECTOR.length);196 }197198 /**199 * @return A copy of the Scrypt salt200 */201 public static byte[] scryptSalt() {202 return Arrays.copyOf(SCRYPT_SALT, SCRYPT_SALT.length);203 }204205206 /**207 * A new wallet up to this amount of seconds old will have a regular sync performed on it and not be checkpointed.208 */209 private static final int ALLOWABLE_TIME_DELTA = 10;210211 /**212 * Open the given wallet and hook it up to the blockchain and peergroup so that it receives notifications213 *214 * @param applicationDataDirectory The application data directory215 * @param walletId The wallet ID to locate the wallet216 * @param password The credentials to use to decrypt the wallet217 *218 * @return The wallet summary if found219 */220 public Optional<WalletSummary> openWalletFromWalletId(File applicationDataDirectory, WalletId walletId, CharSequence password) throws WalletLoadException {221 log.debug("openWalletFromWalletId called");222 Preconditions.checkNotNull(walletId, "'walletId' must be present");223 Preconditions.checkNotNull(password, "'credentials' must be present");224225 this.currentWalletSummary = Optional.absent();226227 // Ensure BackupManager knows where the wallets are228 BackupManager.INSTANCE.setApplicationDataDirectory(applicationDataDirectory);229230 // Work out the list of available wallets in the application data directory231 List<File> walletDirectories = findWalletDirectories(applicationDataDirectory);232233 // If a wallet directory is present try to load the wallet234 if (!walletDirectories.isEmpty()) {235 String walletIdPath = walletId.toFormattedString();236 // Match the wallet directory to the wallet data237 for (File walletDirectory : walletDirectories) {238239 verifyWalletDirectory(walletDirectory);240241 String walletDirectoryPath = walletDirectory.getAbsolutePath();242 if (walletDirectoryPath.contains(walletIdPath)) {243 // Found the required wallet directory - attempt to present the wallet244 WalletSummary walletSummary = loadFromWalletDirectory(walletDirectory, password);245 setCurrentWalletSummary(walletSummary);246247 try {248 // Wallet is now created - finish off other configuration249 updateConfigurationAndCheckSync(createWalletRoot(walletId), walletDirectory, walletSummary, false, true);250 } catch (IOException ioe) {251 throw new WalletLoadException("Cannot load wallet with id: " + walletId, ioe);252 }253254 break;255 }256 }257 } else {258 currentWalletSummary = Optional.absent();259 }260261 return currentWalletSummary;262 }263264 /**265 * <h1>THIS METHOD DOES NOT PRODUCE BIP32 COMPLIANT WALLETS !</h1>266 * <h1>THIS METHOD DOES NOT PRODUCE BIP32 COMPLIANT WALLETS !</h1>267 * <h1>THIS METHOD DOES NOT PRODUCE BIP32 COMPLIANT WALLETS !</h1>268 *269 * See: https://github.com/keepkey/multibit-hd/issues/445270 *271 * <p>Create a MBHD soft wallet from a seed.</p>272 * <p>This is stored in the specified directory.</p>273 * <p>The name of the wallet directory is derived from the seed.</p>274 * <p>If the wallet file already exists it is loaded and returned</p>275 * <p>Auto-save is hooked up so that the wallet is saved on modification</p>276 * <p>Synchronization is begun if required</p>277 *278 * @param applicationDataDirectory The application data directory containing the wallet279 * @param seed The byte array corresponding to the seed phrase to initialise the wallet280 * @param creationTimeInSeconds The creation time of the wallet, in seconds since epoch281 * @param password The credentials to use to encrypt the wallet - if null then the wallet is not loaded282 * @param name The wallet name283 * @param notes Public notes associated with the wallet284 * @param performSynch True if the wallet should immediately begin synchronization285 *286 * @return Wallet summary containing the wallet object and the walletId (used in storage etc)287 *288 * @throws IllegalStateException if applicationDataDirectory is incorrect289 * @throws WalletLoadException if there is already a wallet created but it could not be loaded290 * @throws WalletVersionException if there is already a wallet but the wallet version cannot be understood291 */292 public WalletSummary badlyGetOrCreateMBHDSoftWalletSummaryFromSeed(293 File applicationDataDirectory,294 byte[] seed,295 long creationTimeInSeconds,296 String password,297 String name,298 String notes,299 boolean performSynch) throws WalletLoadException, WalletVersionException, IOException {300 log.debug("badlyGetOrCreateMBHDSoftWalletSummaryFromSeed called");301 final WalletSummary walletSummary;302303 // Create a wallet id from the seed to work out the wallet root directory304 final WalletId walletId = new WalletId(seed);305 String walletRoot = createWalletRoot(walletId);306307 final File walletDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, walletRoot);308 log.debug("Wallet directory:\n'{}'", walletDirectory.getAbsolutePath());309310 final File walletFile = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME);311 final File walletFileWithAES = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME + MBHD_AES_SUFFIX);312313 boolean saveWalletYaml = false;314 boolean createdNew = false;315 if (walletFileWithAES.exists()) {316 log.debug("Discovered AES encrypted wallet file. Loading...");317318 // There is already a wallet created with this root - if so load it and return that319 walletSummary = loadFromWalletDirectory(walletDirectory, password);320321 setCurrentWalletSummary(walletSummary);322 } else {323 // Wallet file does not exist so create it below the known good wallet directory324 log.debug("Creating new wallet file...");325326 // Create a wallet using the seed (no salt passphrase)327 // THIS METHOD CALL PRODUCES NON BIP32 COMPLIANT WALLETS !328 // The entropy should be passed in - not the seed bytes329 DeterministicSeed deterministicSeed = new DeterministicSeed(seed, "", creationTimeInSeconds);330 Wallet walletToReturn = Wallet.fromSeed(networkParameters, deterministicSeed);331 walletToReturn.setKeychainLookaheadSize(LOOK_AHEAD_SIZE);332 walletToReturn.encrypt(password);333 walletToReturn.setVersion(MBHD_WALLET_VERSION);334335 // Save it now to ensure it is on the disk336 walletToReturn.saveToFile(walletFile);337 EncryptedFileReaderWriter.makeAESEncryptedCopyAndDeleteOriginal(walletFile, password);338339 // Create a new wallet summary340 walletSummary = new WalletSummary(walletId, walletToReturn);341 walletSummary.setName(name);342 walletSummary.setNotes(notes);343 walletSummary.setWalletPassword(new WalletPassword(password, walletId));344 walletSummary.setWalletFile(walletFile);345 walletSummary.setWalletType(WalletType.MBHD_SOFT_WALLET);346 setCurrentWalletSummary(walletSummary);347348 // Save the wallet YAML349 saveWalletYaml = true;350 createdNew = true;351352 try {353 WalletManager.writeEncryptedPasswordAndBackupKey(walletSummary, seed, password);354 } catch (NoSuchAlgorithmException e) {355 throw new WalletLoadException("Could not store encrypted credentials and backup AES key", e);356 }357 }358359 // Set wallet type360 walletSummary.getWallet().addOrUpdateExtension(new WalletTypeExtension(WalletType.MBHD_SOFT_WALLET));361362 if (createdNew) {363 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), true, CoreMessageKey.WALLET_LOADED_OK, null, Optional.<File>absent()));364 }365366 // Wallet is now created - finish off other configuration and check if wallet needs syncing367 updateConfigurationAndCheckSync(walletRoot, walletDirectory, walletSummary, saveWalletYaml, performSynch);368369 return walletSummary;370 }371372 /**373 * <p>Create a MBHD soft wallet from a seed.</p>374 * <p>This is stored in the specified directory.</p>375 * <p>The name of the wallet directory is derived from the seed.</p>376 * <p>If the wallet file already exists it is loaded and returned</p>377 * <p>Auto-save is hooked up so that the wallet is saved on modification</p>378 * <p>Synchronization is begun if required</p>379 *380 * @param applicationDataDirectory The application data directory containing the wallet381 * @param entropy The entropy equivalent to the wallet words (seed phrase)382 * This is the byte array equivalent to the random number you are using383 * This is NOT the seed bytes, which have undergone Scrypt processing384 * @param seed The seed byte array (the seed phrase after Scrypt processing)385 * @param creationTimeInSeconds The creation time of the wallet, in seconds since epoch386 * @param password The credentials to use to encrypt the wallet - if null then the wallet is not loaded387 * @param name The wallet name388 * @param notes Public notes associated with the wallet389 * @param performSynch True if the wallet should immediately begin synchronization390 *391 * @return Wallet summary containing the wallet object and the walletId (used in storage etc)392 *393 * @throws IllegalStateException if applicationDataDirectory is incorrect394 * @throws WalletLoadException if there is already a wallet created but it could not be loaded395 * @throws WalletVersionException if there is already a wallet but the wallet version cannot be understood396 */397 public WalletSummary getOrCreateMBHDSoftWalletSummaryFromEntropy(398 File applicationDataDirectory,399 byte[] entropy,400 byte[] seed,401 long creationTimeInSeconds,402 String password,403 String name,404 String notes,405 boolean performSynch) throws WalletLoadException, WalletVersionException, IOException {406 log.debug("getOrCreateMBHDSoftWalletSummaryFromEntropy called, creation time: {}", new DateTime(creationTimeInSeconds * 1000));407 final WalletSummary walletSummary;408409 // Create a wallet id from the seed to work out the wallet root directory410 // The seed bytes are used for backwards compatibility411 final WalletId walletId = new WalletId(seed);412 String walletRoot = createWalletRoot(walletId);413414 final File walletDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, walletRoot);415 log.debug("Wallet directory:\n'{}'", walletDirectory.getAbsolutePath());416417 final File walletFile = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME);418 final File walletFileWithAES = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME + MBHD_AES_SUFFIX);419420 boolean saveWalletYaml = false;421 boolean createdNew = false;422 if (walletFileWithAES.exists()) {423 log.debug("Discovered AES encrypted wallet file. Loading...");424425 // There is already a wallet created with this root - if so load it and return that426 walletSummary = loadFromWalletDirectory(walletDirectory, password);427428 setCurrentWalletSummary(walletSummary);429 } else {430 // Wallet file does not exist so create it below the known good wallet directory431 log.debug("Creating new wallet file...");432433 // Create a wallet using the entropy434 // DeterministicSeed constructor expects ENTROPY here435 DeterministicSeed deterministicSeed = new DeterministicSeed(entropy, "", creationTimeInSeconds);436 Wallet walletToReturn = Wallet.fromSeed(networkParameters, deterministicSeed);437 walletToReturn.setKeychainLookaheadSize(LOOK_AHEAD_SIZE);438 walletToReturn.encrypt(password);439 walletToReturn.setVersion(MBHD_WALLET_VERSION);440441 // Save it now to ensure it is on the disk442 walletToReturn.saveToFile(walletFile);443 EncryptedFileReaderWriter.makeAESEncryptedCopyAndDeleteOriginal(walletFile, password);444445 // Create a new wallet summary446 walletSummary = new WalletSummary(walletId, walletToReturn);447 walletSummary.setName(name);448 walletSummary.setNotes(notes);449 walletSummary.setWalletPassword(new WalletPassword(password, walletId));450 walletSummary.setWalletFile(walletFile);451 walletSummary.setWalletType(WalletType.MBHD_SOFT_WALLET_BIP32);452 setCurrentWalletSummary(walletSummary);453454 // Save the wallet YAML455 saveWalletYaml = true;456 createdNew = true;457458 try {459 // The seed bytes are used as the secret to encrypt the password (mainly for backwards compatibility)460 WalletManager.writeEncryptedPasswordAndBackupKey(walletSummary, seed, password);461 } catch (NoSuchAlgorithmException e) {462 throw new WalletLoadException("Could not store encrypted credentials and backup AES key", e);463 }464 }465466 // Set wallet type467 walletSummary.getWallet().addOrUpdateExtension(new WalletTypeExtension(WalletType.MBHD_SOFT_WALLET_BIP32));468469 if (createdNew) {470 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), true, CoreMessageKey.WALLET_LOADED_OK, null, Optional.<File>absent()));471 }472473 // Wallet is now created - finish off other configuration and check if wallet needs syncing474 updateConfigurationAndCheckSync(walletRoot, walletDirectory, walletSummary, saveWalletYaml, performSynch);475476 return walletSummary;477 }478479 /**480 * Create a Trezor / KeepKey hard wallet from an HD root node.481 * <p/>482 * This is stored in the specified application directory.483 * The name of the wallet directory is derived from the rootNode.484 * <p/>485 * If the wallet file already exists it is loaded and returned486 * <p/>487 * Auto-save is hooked up so that the wallet is saved on modification488 *489 * @param applicationDataDirectory The application data directory containing the wallet490 * @param rootNode The root node that will be used to initialise the wallet (e.g. a BIP44 node)491 * @param creationTimeInSeconds The creation time of the wallet, in seconds since epoch492 * @param password The credentials to use to encrypt the wallet - if null then the wallet is not loaded493 * @param name The wallet name494 * @param notes Public notes associated with the wallet495 * @param performSync True if the wallet should immediately begin synchronization496 *497 * @return Wallet summary containing the wallet object and the walletId (used in storage etc)498 *499 * @throws IllegalStateException if applicationDataDirectory is incorrect500 * @throws WalletLoadException if there is already a wallet created but it could not be loaded501 * @throws WalletVersionException if there is already a wallet but the wallet version cannot be understood502 */503 public WalletSummary getOrCreateTrezorCloneHardWalletSummaryFromRootNode(504 File applicationDataDirectory,505 DeterministicKey rootNode,506 long creationTimeInSeconds,507 String password,508 String name,509 String notes,510 boolean performSync) throws WalletLoadException, WalletVersionException, IOException {511512 log.debug("getOrCreateTrezorCloneHardWalletSummaryFromRootNode called");513514 // Create a wallet id from the rootNode to work out the wallet root directory515 final WalletId walletId = new WalletId(rootNode.getIdentifier());516 String walletRoot = createWalletRoot(walletId);517518 final File walletDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, walletRoot);519 final File walletFile = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME);520 final File walletFileWithAES = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME + MBHD_AES_SUFFIX);521522 final WalletSummary walletSummary;523524 boolean createdNew = false;525526 if (walletFileWithAES.exists()) {527 try {528 // There is already a wallet created with this root - if so load it and return that529 log.debug("Opening AES wallet:\n'{}'", walletFileWithAES.getAbsolutePath());530 walletSummary = loadFromWalletDirectory(walletDirectory, password);531532 // Use any existing notes if none is specified533 if (Strings.isNullOrEmpty(notes) && !Strings.isNullOrEmpty(walletSummary.getNotes())) {534 notes = walletSummary.getNotes();535 }536 } catch (WalletLoadException e) {537 // Failed to decrypt the existing wallet/backups or something else went wrong538 log.error("Failed to load from wallet directory.");539 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), false, CoreMessageKey.WALLET_FAILED_TO_LOAD, e, Optional.<File>absent()));540 throw e;541 }542 } else {543 log.debug("Wallet file does not exist. Creating...");544545 // Create the containing directory if it does not exist546 if (!walletDirectory.exists()) {547 if (!walletDirectory.mkdir()) {548 IllegalStateException error = new IllegalStateException("The directory for the wallet '" + walletDirectory.getAbsoluteFile() + "' could not be created");549 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), false, CoreMessageKey.WALLET_FAILED_TO_LOAD, error, Optional.<File>absent()));550 throw error;551 }552 }553554 // Create a wallet using the root node555 DeterministicKey rootNodePubOnly = rootNode.dropPrivateBytes();556 log.debug("Watching wallet based on: {}", rootNodePubOnly);557558 rootNodePubOnly.setCreationTimeSeconds(creationTimeInSeconds);559560 Wallet walletToReturn = Wallet.fromWatchingKey(networkParameters, rootNodePubOnly, creationTimeInSeconds, rootNodePubOnly.getPath());561 walletToReturn.setKeychainLookaheadSize(LOOK_AHEAD_SIZE);562 walletToReturn.setVersion(MBHD_WALLET_VERSION);563564 // Save it now to ensure it is on the disk565 walletToReturn.saveToFile(walletFile);566 EncryptedFileReaderWriter.makeAESEncryptedCopyAndDeleteOriginal(walletFile, password);567568 // Create a new wallet summary569 walletSummary = new WalletSummary(walletId, walletToReturn);570571 log.debug("Created new wallet in {}", walletFile);572573 createdNew = true;574 }575576 // Wallet summary cannot be null at this point577 walletSummary.setWalletFile(walletFile);578 walletSummary.setName(name);579 walletSummary.setNotes(notes);580 walletSummary.setWalletPassword(new WalletPassword(password, walletId));581 walletSummary.setWalletType(WalletType.TREZOR_HARD_WALLET);582583 setCurrentWalletSummary(walletSummary);584585 // Set wallet type586 walletSummary.getWallet().addOrUpdateExtension(new WalletTypeExtension(WalletType.TREZOR_HARD_WALLET));587588589 try {590 // The entropy based password from the Trezor is used for both the wallet password and for the backup's password591 WalletManager.writeEncryptedPasswordAndBackupKey(walletSummary, password.getBytes(Charsets.UTF_8), password);592 } catch (NoSuchAlgorithmException e) {593 WalletLoadException error = new WalletLoadException("Could not store encrypted credentials and backup AES key", e);594 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), false, CoreMessageKey.WALLET_FAILED_TO_LOAD, error, Optional.<File>absent()));595 throw error;596 }597598 if (createdNew) {599 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), true, CoreMessageKey.WALLET_LOADED_OK, null, Optional.<File>absent()));600 }601602 // Wallet is now created - finish off other configuration and check if wallet needs syncing603 // (Always save the wallet yaml as there was a bug in early Trezor wallets where it was not written out)604 updateConfigurationAndCheckSync(walletRoot, walletDirectory, walletSummary, true, performSync);605606 return walletSummary;607 }608609 /**610 * Create a Trezor or KeepKey soft wallet from a seed phrase611 * <p/>612 * This is stored in the specified application directory.613 * The name of the wallet directory is derived from the rootNode.614 * <p/>615 * If the wallet file already exists it is loaded and returned616 * <p/>617 * Auto-save is hooked up so that the wallet is saved on modification618 *619 * @param applicationDataDirectory The application data directory containing the wallet620 * @param seedPhrase The BIP39 seed phrase to use to initialise the walelt621 * @param creationTimeInSeconds The creation time of the wallet, in seconds since epoch622 * @param password The credentials to use to encrypt the wallet - if null then the wallet is not loaded623 * @param name The wallet name624 * @param notes Public notes associated with the wallet625 * @param performSync True if the wallet should immediately begin synchronizing626 *627 * @return Wallet summary containing the wallet object and the walletId (used in storage etc)628 *629 * @throws IllegalStateException if applicationDataDirectory is incorrect630 * @throws WalletLoadException if there is already a wallet created but it could not be loaded631 * @throws WalletVersionException if there is already a wallet but the wallet version cannot be understood632 */633 public WalletSummary getOrCreateTrezorCloneSoftWalletSummaryFromSeedPhrase(634 File applicationDataDirectory,635 String seedPhrase,636 long creationTimeInSeconds,637 String password,638 String name,639 String notes,640 boolean performSync) throws UnreadableWalletException, WalletLoadException, WalletVersionException, IOException {641642 log.debug("getOrCreateTrezorCloneSoftWalletSummaryFromSeedPhrase called");643644 // Create a wallet id from the seed to work out the wallet root directory645 SeedPhraseGenerator seedGenerator = new Bip39SeedPhraseGenerator();646 List<String> seedPhraseList = Bip39SeedPhraseGenerator.split(seedPhrase);647 byte[] seed = seedGenerator.convertToSeed(seedPhraseList);648649 final WalletId walletId = new WalletId(seed, getWalletIdSaltUsedInScryptForTrezorSoftWallets());650 String walletRoot = createWalletRoot(walletId);651652 final File walletDirectory = WalletManager.getOrCreateWalletDirectory(applicationDataDirectory, walletRoot);653 final File walletFile = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME);654 final File walletFileWithAES = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME + MBHD_AES_SUFFIX);655656 final WalletSummary walletSummary;657658 boolean createdNew = false;659660 if (walletFileWithAES.exists()) {661 try {662 // There is already a wallet created with this root - if so load it and return that663 log.debug("A wallet with name {} exists. Opening...", walletFileWithAES.getAbsolutePath());664 walletSummary = loadFromWalletDirectory(walletDirectory, password);665 } catch (WalletLoadException e) {666 // Failed to decrypt the existing wallet/backups667 log.error("Failed to load from wallet directory.");668 IllegalStateException error = new IllegalStateException("The wallet could not be opened");669 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), false, CoreMessageKey.WALLET_FAILED_TO_LOAD, error, Optional.<File>absent()));670 throw error;671672 }673 } else {674 log.debug("Wallet file does not exist. Creating...");675676 // Create the containing directory if it does not exist677 if (!walletDirectory.exists()) {678 if (!walletDirectory.mkdir()) {679 throw new IllegalStateException("The directory for the wallet '" + walletDirectory.getAbsoluteFile() + "' could not be created");680 }681 }682683 // Trezor uses BIP-44684 // BIP-44 starts from M/44h/0h/0h for soft wallets685 List<ChildNumber> trezorRootNodePathList = new ArrayList<>();686 trezorRootNodePathList.add(new ChildNumber(44 | ChildNumber.HARDENED_BIT));687 trezorRootNodePathList.add(new ChildNumber(ChildNumber.HARDENED_BIT));688689 DeterministicKey trezorRootNode = HDKeyDerivation.createRootNodeWithPrivateKey(ImmutableList.copyOf(trezorRootNodePathList), seed);690 log.debug("Creating Trezor clone soft wallet with root node with path {}", trezorRootNode.getPath());691692 // Create a KeyCrypter to encrypt the waller693 KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(EncryptedFileReaderWriter.makeScryptParameters(WalletManager.SCRYPT_SALT));694695 // Create a wallet using the seed phrase and Trezor root node696 DeterministicSeed deterministicSeed = new DeterministicSeed(seed, seedPhraseList, creationTimeInSeconds);697698 Wallet walletToReturn = Wallet.fromSeed(networkParameters, deterministicSeed, trezorRootNode.getPath(), password, keyCrypterScrypt);699 walletToReturn.setKeychainLookaheadSize(LOOK_AHEAD_SIZE);700 walletToReturn.setVersion(MBHD_WALLET_VERSION);701702 // Save it now to ensure it is on the disk703 walletToReturn.saveToFile(walletFile);704 EncryptedFileReaderWriter.makeAESEncryptedCopyAndDeleteOriginal(walletFile, password);705706 // Create a new wallet summary707 walletSummary = new WalletSummary(walletId, walletToReturn);708709 createdNew = true;710 }711712 // Wallet summary cannot be null713 walletSummary.setWalletFile(walletFile);714 walletSummary.setName(name);715 walletSummary.setNotes(notes);716 walletSummary.setWalletPassword(new WalletPassword(password, walletId));717 walletSummary.setWalletType(WalletType.TREZOR_SOFT_WALLET);718719 setCurrentWalletSummary(walletSummary);720721 // Set wallet type722 walletSummary.getWallet().addOrUpdateExtension(new WalletTypeExtension(WalletType.TREZOR_SOFT_WALLET));723724 try {725 WalletManager.writeEncryptedPasswordAndBackupKey(walletSummary, seed, password);726 } catch (NoSuchAlgorithmException e) {727 throw new WalletLoadException("Could not store encrypted credentials and backup AES key", e);728 }729730 if (createdNew) {731 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), true, CoreMessageKey.WALLET_LOADED_OK, null, Optional.<File>absent()));732 }733734 // Wallet is now created - finish off other configuration and check if wallet needs syncing735 // Always save the wallet YAML as there was a bug in early Trezor wallets where it was not written out736 updateConfigurationAndCheckSync(walletRoot, walletDirectory, walletSummary, true, performSync);737738 return walletSummary;739 }740741742 /**743 * Update configuration with new wallet information744 */745 private void updateConfigurationAndCheckSync(746 String walletRoot,747 File walletDirectory,748 WalletSummary walletSummary,749 boolean saveWalletYaml,750 boolean performSync) throws IOException {751752 Preconditions.checkNotNull(walletRoot, "'walletRoot' must be present");753 Preconditions.checkNotNull(walletDirectory, "'walletDirectory' must be present");754 Preconditions.checkNotNull(walletSummary, "'walletSummary' must be present");755756 // Set the walletSummary walletType757 // This is stored in plain text in the wallet yaml and enables filtering before knowing the wallet password758 walletSummary.setWalletType(getWalletType(walletSummary.getWallet()));759760 if (saveWalletYaml) {761 File walletSummaryFile = WalletManager.getOrCreateWalletSummaryFile(walletDirectory);762 log.debug("Writing wallet YAML to file:\n'{}'", walletSummaryFile.getAbsolutePath());763 WalletManager.updateWalletSummary(walletSummaryFile, walletSummary);764 }765766 // Remember the current soft wallet root767 if (WalletType.MBHD_SOFT_WALLET == walletSummary.getWalletType() ||768 WalletType.MBHD_SOFT_WALLET_BIP32 == walletSummary.getWalletType() ||769 WalletType.TREZOR_SOFT_WALLET == walletSummary.getWalletType()) {770 if (Configurations.currentConfiguration != null) {771 Configurations.currentConfiguration.getWallet().setLastSoftWalletRoot(walletRoot);772 }773 }774775 // See if there is a checkpoints file - if not then get the InstallationManager to copy one in776 File checkpointsFile = new File(walletDirectory.getAbsolutePath() + File.separator + InstallationManager.MBHD_PREFIX + InstallationManager.CHECKPOINTS_SUFFIX);777 InstallationManager.copyCheckpointsTo(checkpointsFile);778779 // Set up auto-save on the wallet.780 addAutoSaveListener(walletSummary.getWallet(), walletSummary.getWalletFile());781782 // Remember the info required for the next backups783 BackupService backupService = CoreServices.getOrCreateBackupService();784 backupService.rememberWalletSummaryAndPasswordForRollingBackup(walletSummary, walletSummary.getWalletPassword().getPassword());785 backupService.rememberWalletIdAndPasswordForLocalZipBackup(walletSummary.getWalletId(), walletSummary.getWalletPassword().getPassword());786 backupService.rememberWalletIdAndPasswordForCloudZipBackup(walletSummary.getWalletId(), walletSummary.getWalletPassword().getPassword());787788 // Check if the wallet needs to synch (not required during FEST tests)789 if (performSync) {790 log.info("Wallet configured - performing synchronization");791 checkIfWalletNeedsToSync(walletSummary);792 } else {793 log.warn("Wallet configured - synchronization not selected - expect this during testing");794 }795 }796797 /**798 * Check if the wallet needs to sync and, if so, work out the sync date and fire off the synchronise799 *800 * @param walletSummary The wallet summary containing the wallet that may need syncing801 */802 private void checkIfWalletNeedsToSync(WalletSummary walletSummary) {803 // See if the wallet and blockstore are at the same height - in which case perform a regular download blockchain804 // Else perform a sync from the last seen block date to ensure all tx are seen805 log.debug("Seeing if wallet needs to sync");806 if (walletSummary != null) {807 Wallet walletBeingReturned = walletSummary.getWallet();808809 if (walletBeingReturned == null) {810 log.debug("There is no wallet to examine");811 } else {812813 boolean performRegularSync = false;814 Optional<DateTime> unconfirmedTransactionReplayDate = Optional.absent();815 BlockStore blockStore = null;816 try {817 // Get the bitcoin network service818 BitcoinNetworkService bitcoinNetworkService = CoreServices.getOrCreateBitcoinNetworkService();819 log.debug("bitcoinNetworkService: {}", bitcoinNetworkService);820821 int walletBlockHeight = walletBeingReturned.getLastBlockSeenHeight();822 Date walletLastSeenBlockTime = walletBeingReturned.getLastBlockSeenTime();823824 log.debug(825 "Wallet lastBlockSeenHeight: {}, lastSeenBlockTime: {}, earliestKeyCreationTime: {}",826 walletBlockHeight,827 walletLastSeenBlockTime,828 new DateTime(walletBeingReturned.getEarliestKeyCreationTime() * 1000));829830 // See if the bitcoinNetworkService already has an open blockstore831 blockStore = bitcoinNetworkService.getBlockStore();832833 if (blockStore == null) {834 // Open the blockstore with no checkpointing (this is to get the chain height)835 blockStore = bitcoinNetworkService.openBlockStore(836 InstallationManager.getOrCreateApplicationDataDirectory(),837 new ReplayConfig()838 );839 }840 log.debug("blockStore = {}", blockStore);841842 int blockStoreBlockHeight = -2; // -2 is just a dummy value843 if (blockStore != null) {844 StoredBlock chainHead = blockStore.getChainHead();845 blockStoreBlockHeight = chainHead == null ? -2 : chainHead.getHeight();846847 }848 log.debug("The blockStore is at height {}", blockStoreBlockHeight);849850 boolean keyCreationTimeIsInThePast = false;851 if (walletBeingReturned.getEarliestKeyCreationTime() != -1) {852 if (walletBeingReturned.getEarliestKeyCreationTime() < Dates.nowInSeconds() - ALLOWABLE_TIME_DELTA) {853 keyCreationTimeIsInThePast = true;854 }855 }856857 // Work out if the wallet has unconfirmed transactions in the time window of interest for replay858 unconfirmedTransactionReplayDate = UnconfirmedTransactionDetector.calculateReplayDate(walletBeingReturned, Dates.nowUtc());859860 // If (wallet and block store match or wallet is brand new) and861 // no sync is required due to unconfirmed transactions862 // then use regular sync863 if (((walletBlockHeight > 0 && walletBlockHeight == blockStoreBlockHeight) ||864 (walletLastSeenBlockTime == null && !keyCreationTimeIsInThePast)) && !unconfirmedTransactionReplayDate.isPresent()) {865 // Regular sync is ok - no need to use checkpoints / replayDate866 log.debug("Will perform a regular sync");867 performRegularSync = true;868 }869 } catch (BlockStoreException bse) {870 // Carry on - it's just logging871 log.warn("Block store exception", bse);872 } finally {873 // Close the blockstore - it will get opened again later but may or may not be checkpointed874 if (blockStore != null) {875 try {876 blockStore.close();877 } catch (BlockStoreException bse) {878 log.warn("Failed to close block store", bse);879 }880 }881 }882883 if (performRegularSync) {884 synchroniseWallet(Optional.<DateTime>absent());885 } else {886 // Work out the replay date based on the last block seen, the earliest key creation date, the earliest HD wallet date887 // and the unconfirmed transaction replay date888 DateTime replayDate = calculateReplayDateTime(walletBeingReturned, unconfirmedTransactionReplayDate);889 synchroniseWallet(Optional.of(replayDate));890 }891 }892 }893 }894895 /**896 * @param walletBeingReturned The wallet requiring replay897 * @param unconfirmedTransactionReplayDate The replayDate required due to there being unconfirmed transactions898 *899 * @return The most appropriate date time to being replay900 */901 private DateTime calculateReplayDateTime(Wallet walletBeingReturned, Optional<DateTime> unconfirmedTransactionReplayDate) {902903 DateTime replayDateTime = null;904905 // Start with the last block seen date906 if (walletBeingReturned.getLastBlockSeenTime() != null) {907 replayDateTime = new DateTime(walletBeingReturned.getLastBlockSeenTime());908 log.debug("Setting potential replay date from last block seen time of {}", replayDateTime);909 }910911 // If there is an unconfirmedTransactionReplayDate and it is earlier then we will go back further to that912 if (unconfirmedTransactionReplayDate.isPresent()) {913 DateTime candidateReplayDate = unconfirmedTransactionReplayDate.get();914 if (candidateReplayDate != null && candidateReplayDate.isBefore(replayDateTime)) {915 replayDateTime = candidateReplayDate;916 log.debug("Setting earlier potential replay date from unconfirmedTransaction replay date of {}", replayDateTime);917 }918 }919920 // Override with the earliest key creation date921 // Expect:922 // 0 for ECKey keys created before timestamp (triggers epoch) or923 // timestamp of "now" or "earliest key creation" in seconds since epoch924 long earliestKeyCreationSeconds = walletBeingReturned.getEarliestKeyCreationTime();925 if (earliestKeyCreationSeconds >= 0) {926 DateTime earliestKeyCreationDateTime = new DateTime(earliestKeyCreationSeconds * 1000); // Using seconds927 if (replayDateTime == null) {928 replayDateTime = earliestKeyCreationDateTime;929 log.debug("Setting potential replay date from earliestKeyCreationDateTime date (1) of {}", replayDateTime);930 }931 }932933 // Override with earliest HD wallet date (shared with other wallets)934 DateTime earliestHDWalletDate = DateTime.parse(EARLIEST_HD_WALLET_DATE);935 if (replayDateTime == null || replayDateTime.isBefore(earliestHDWalletDate)) {936 // Do not go further back than earliest HD wallet (this avoids epoch)937 replayDateTime = earliestHDWalletDate;938 log.debug("Setting potential replay date from earliest HDwallet date of {}", replayDateTime);939 }940941 // Cannot be null942 return replayDateTime;943 }944945 /**946 * Load a wallet from a file and decrypt it947 * (but don't hook it up to the Bitcoin network or sync it)948 *949 * @param walletFile wallet file to load950 * @param password password to use to decrypt the wallet951 *952 * @return the loaded wallet953 *954 * @throws IOException955 * @throws UnreadableWalletException956 */957 public Wallet loadWalletFromFile(File walletFile, CharSequence password) throws IOException, UnreadableWalletException {958959 // Read the encrypted file in and decrypt it.960 byte[] fileBytes = Files.toByteArray(walletFile);961 byte[] ivBytes = Arrays.copyOfRange(fileBytes, 0, 16);962 byte[] encryptedWalletBytes = Arrays.copyOfRange(fileBytes, 16, fileBytes.length);963 Preconditions.checkNotNull(encryptedWalletBytes, "'encryptedWalletBytes' must be present");964965 log.trace("Encrypted wallet bytes after load:\n{}", Utils.HEX.encode(encryptedWalletBytes));966 log.debug("Loaded the encrypted wallet bytes with length: {}", encryptedWalletBytes.length);967968 KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(EncryptedFileReaderWriter.makeScryptParameters(SCRYPT_SALT));969 KeyParameter keyParameter = keyCrypterScrypt.deriveKey(password);970971 // Decrypt the wallet bytes972973 byte [] decryptedBytes = AESUtils.decrypt(encryptedWalletBytes, keyParameter, ivBytes);974 if(!EncryptedWalletFile.isParseable(decryptedBytes)){975 decryptedBytes = AESUtils.decrypt(fileBytes, keyParameter, WalletManager.deprecatedFixedAesInitializationVector());976 }977 InputStream inputStream = new ByteArrayInputStream(decryptedBytes);978 Protos.Wallet walletProto = WalletProtobufSerializer.parseToProto(inputStream);979980 WalletExtension[] walletExtensions = new WalletExtension[]{new SendFeeDtoWalletExtension(), new MatcherResponseWalletExtension(), new WalletTypeExtension()};981 Wallet wallet = new WalletProtobufSerializer().readWallet(BitcoinNetwork.current().get(), walletExtensions, walletProto);982 wallet.setKeychainLookaheadSize(LOOK_AHEAD_SIZE);983984 // Try to infer the wallet type from the key structure to bootstrap missing WalletType values985 inferWalletType(wallet);986987 // Writing out a wallet to a clear text file is security risk988 // Do not do it except for debug989 // log.debug("Wallet loaded OK:\n{}\n", wallet);990991 return wallet;992993 }994 private void inferWalletType(Wallet wallet) {995 // Get the wallet type as defined by the wallet type extension996 WalletType walletType = getWalletType(wallet);997998 WalletType inferredWalletType = null;9991000 if (WalletType.UNKNOWN.equals(walletType)) {1001 // Attempt to infer the wallet type from the wallet key structure1002 if (wallet.getActiveKeychain() != null) {1003 List<DeterministicKey> leafKeys = wallet.getActiveKeychain().getLeafKeys();1004 if (leafKeys != null && !leafKeys.isEmpty()) {1005 DeterministicKey firstLeafKey = leafKeys.get(0);10061007 if (firstLeafKey != null) {1008 ImmutableList<ChildNumber> firstLeafKeyPath = firstLeafKey.getPath();10091010 if (firstLeafKeyPath != null && firstLeafKeyPath.size() > 0) {1011 // MBHD soft wallets start at m/0h1012 if (ChildNumber.ZERO_HARDENED.equals(firstLeafKeyPath.get(0))) {1013 inferredWalletType = WalletType.MBHD_SOFT_WALLET_BIP32;1014 } else if ((new ChildNumber(44 | ChildNumber.HARDENED_BIT)).equals(firstLeafKeyPath.get(0))) {1015 // Trezor wallet1016 if (firstLeafKey.isEncrypted()) {1017 // soft wallets only have encrypted private keys1018 inferredWalletType = WalletType.TREZOR_SOFT_WALLET;1019 } else {1020 inferredWalletType = WalletType.TREZOR_HARD_WALLET;1021 }1022 }1023 }1024 }1025 }1026 }10271028 // if we inferred the WalletType put it in the wallet1029 if (inferredWalletType != null) {1030 log.debug("Inferring the Wallet type of the wallet to be {}", inferredWalletType);1031 wallet.addOrUpdateExtension(new WalletTypeExtension(inferredWalletType));1032 }1033 }1034 }10351036 static public WalletType getWalletType(Wallet wallet) {1037 if (wallet == null) {1038 return WalletType.UNKNOWN;1039 } else {1040 Map<String, WalletExtension> walletExtensionMap = wallet.getExtensions();1041 WalletTypeExtension walletTypeExtension = (WalletTypeExtension) walletExtensionMap.get(WalletTypeExtension.WALLET_TYPE_WALLET_EXTENSION_ID);1042 if (walletTypeExtension == null) {1043 return WalletType.UNKNOWN;1044 } else {1045 return walletTypeExtension.getWalletType();1046 }1047 }1048 }10491050 /**1051 * <p>Load up an encrypted Wallet from a specified wallet directory.</p>1052 * <p>Reduced visibility for testing</p>1053 *1054 * @param walletDirectory The wallet directory containing the various wallet files to load1055 * @param password The credentials to use to decrypt the wallet1056 *1057 * @return Wallet - the loaded wallet1058 *1059 * @throws WalletLoadException If the wallet could not be loaded1060 * @throws WalletVersionException If the wallet has an unsupported version number1061 */1062 WalletSummary loadFromWalletDirectory(File walletDirectory, CharSequence password) throws WalletLoadException, WalletVersionException {10631064 Preconditions.checkNotNull(walletDirectory, "'walletDirectory' must be present");1065 Preconditions.checkNotNull(password, "'credentials' must be present");1066 verifyWalletDirectory(walletDirectory);10671068 try {1069 String walletFilenameNoAESSuffix = walletDirectory.getAbsolutePath() + File.separator + MBHD_WALLET_NAME;1070 File walletFile = new File(walletFilenameNoAESSuffix + MBHD_AES_SUFFIX);1071 WalletId walletId = parseWalletFilename(walletFile.getAbsolutePath());10721073 if (walletFile.exists() && isWalletSerialised(walletFile)) {1074 // Serialised wallets are no longer supported.1075 throw new WalletLoadException(1076 "Could not load wallet '"1077 + walletFile1078 + "'. Serialized wallets are no longer supported."1079 );1080 }10811082 Wallet wallet;1083 boolean backupFileLoaded = false;10841085 try {1086 wallet = loadWalletFromFile(walletFile, password);1087 } catch (WalletVersionException wve) {1088 // We want this exception to propagate out.1089 // Don't bother trying to load the rolling backups as they will most likely be an unreadable version too.1090 throw wve;1091 } catch (Exception e) {1092 // Log the initial error1093 log.error("WalletManager error: " + e.getClass().getCanonicalName() + " " + e.getMessage(), e);10941095 // Try loading one of the rolling backups - this will send a WalletLoadedEvent containing the backup file loaded1096 // If the rolling backups don't load then loadRollingBackup will throw a WalletLoadException which will propagate out1097 wallet = BackupManager.INSTANCE.loadRollingBackup(walletId, password);1098 backupFileLoaded = true;1099 }11001101 // Create the wallet summary with its wallet1102 WalletSummary walletSummary = getAndChangeWalletSummary(walletDirectory, walletId,password);1103 walletSummary.setWallet(wallet);1104 walletSummary.setWalletFile(new File(walletFilenameNoAESSuffix));1105 walletSummary.setWalletPassword(new WalletPassword(password, walletId));11061107 log.debug("Loaded the wallet successfully from \n{}", walletDirectory);11081109 // Fire a wallet loaded event indicating success (if a rolling backup was loaded this has already been sent so do not send another)1110 if (!backupFileLoaded) {1111 CoreEvents.fireWalletLoadEvent(new WalletLoadEvent(Optional.of(walletId), true, CoreMessageKey.WALLET_LOADED_OK, null, Optional.<File>absent()));1112 }1113 File walletSummaryFile = getOrCreateWalletSummaryFile(walletDirectory);1114 updateWalletSummary(walletSummaryFile,walletSummary);11151116 return walletSummary;11171118 } catch (WalletVersionException wve) {1119 // We want this to propagate out as is1120 throw wve;1121 } catch (Exception e) {1122 throw new WalletLoadException(e.getMessage(), e);1123 }1124 }11251126 /**1127 * Set up auto-save on the wallet.1128 * This ensures the wallet is saved on modification1129 * The listener has a 'after save' callback which ensures rolling backups and local/ cloud backups are also saved where necessary1130 *1131 * @param wallet The wallet to add the autosave listener to1132 * @param file The file to add the autoSaveListener to - this should be WITHOUT the AES suffix1133 */1134 private void addAutoSaveListener(Wallet wallet, File file) {1135 if (file != null) {1136 WalletAutoSaveListener walletAutoSaveListener = new WalletAutoSaveListener();1137 wallet.autosaveToFile(file, AUTO_SAVE_DELAY, TimeUnit.MILLISECONDS, walletAutoSaveListener);1138 log.debug("WalletAutoSaveListener {} on file\n'{}'\njust added to wallet {}", System.identityHashCode(this), file.getAbsolutePath(), System.identityHashCode(wallet));1139 } else {1140 log.debug("Not adding autoSaveListener to wallet {} as no wallet file is specified", System.identityHashCode(wallet));1141 }1142 }11431144 /**1145 * @param replayDate The date from which to replay the download (absent means no checkpoints)1146 */1147 private void synchroniseWallet(final Optional<DateTime> replayDate) {11481149 if (walletExecutorService == null) {1150 walletExecutorService = SafeExecutors.newSingleThreadExecutor("sync-wallet");1151 }11521153 // Start the Bitcoin network synchronization operation1154 ListenableFuture<Boolean> future = walletExecutorService.submit(1155 new Callable<Boolean>() {11561157 @Override1158 public Boolean call() throws Exception {1159 log.debug("Synchronizing wallet with replay date '{}'", replayDate.orNull());11601161 // Replay wallet, use fast catch up, no clearing mempool1162 CoreServices.getOrCreateBitcoinNetworkService().replayWallet(1163 InstallationManager.getOrCreateApplicationDataDirectory(),1164 replayDate,1165 true,1166 false1167 );1168 return true;11691170 }11711172 });1173 Futures.addCallback(1174 future, new FutureCallback<Boolean>() {1175 @Override1176 public void onSuccess(@Nullable Boolean result) {1177 // Do nothing this just means that the block chain download has begun1178 log.debug("Sync has begun");11791180 }11811182 @Override1183 public void onFailure(Throwable t) {1184 // Have a failure1185 log.debug("Sync failed, error was " + t.getClass().getCanonicalName() + " " + t.getMessage());11861187 }1188 });1189 }11901191 /**1192 * @param walletFile the wallet to test serialisation for1193 *1194 * @return true if the wallet file specified is serialised (this format is no longer supported)1195 */1196 private boolean isWalletSerialised(File walletFile) {11971198 Preconditions.checkNotNull(walletFile, "'walletFile' must be present");1199 Preconditions.checkState(walletFile.isFile(), "'walletFile' must be a file");12001201 boolean isWalletSerialised = false;1202 InputStream stream = null;1203 try {1204 // Determine what kind of wallet stream this is: Java serialization or protobuf format1205 stream = new BufferedInputStream(new FileInputStream(walletFile));1206 isWalletSerialised = stream.read() == 0xac && stream.read() == 0xed;1207 } catch (IOException e) {1208 log.error(e.getClass().getCanonicalName() + " " + e.getMessage());1209 } finally {1210 if (stream != null) {1211 try {1212 stream.close();1213 } catch (IOException e) {1214 log.error(e.getClass().getCanonicalName() + " " + e.getMessage());1215 }1216 }1217 }1218 return isWalletSerialised;1219 }12201221 /**1222 * Create the name of the directory in which the wallet is stored1223 *1224 * @param walletId The wallet id to use (e.g. "11111111-22222222-33333333-44444444-55555555")1225 *1226 * @return A wallet root1227 */1228 public static String createWalletRoot(WalletId walletId) {12291230 Preconditions.checkNotNull(walletId, "'walletId' must be present");12311232 return WALLET_DIRECTORY_PREFIX + WALLET_ID_SEPARATOR + walletId.toFormattedString();1233 }12341235 /**1236 * <p>Get or create the sub-directory of the given application directory with the given wallet root</p>1237 *1238 * @param applicationDataDirectory The application data directory containing the wallet1239 * @param walletRoot The wallet root from which to make a sub-directory (e.g. "mbhd-11111111-22222222-33333333-44444444-55555555")1240 *1241 * @return The directory composed of parent directory plus the wallet root1242 *1243 * @throws IllegalStateException if wallet could not be created1244 */1245 public static File getOrCreateWalletDirectory(File applicationDataDirectory, String walletRoot) {12461247 // Create wallet directory under application directory1248 File walletDirectory = SecureFiles.verifyOrCreateDirectory(applicationDataDirectory, walletRoot);12491250 // Sanity check the wallet directory name and existence1251 verifyWalletDirectory(walletDirectory);12521253 return walletDirectory;1254 }12551256 /**1257 * @return A list of wallet summaries based on the current application directory contents (never null)1258 */1259 public static List<WalletSummary> getWalletSummaries() {12601261 List<File> walletDirectories = findWalletDirectories(InstallationManager.getOrCreateApplicationDataDirectory());1262 Optional<String> walletRoot = INSTANCE.getCurrentWalletRoot();1263 return findWalletSummaries(walletDirectories, walletRoot);12641265 }12661267 /**1268 * <p>This list contains MBHD soft wallets and Trezor soft wallets</p>1269 *1270 * @param localeOptional the locale to sort results by1271 *1272 * @return A list of soft wallet summaries based on the current application directory contents (never null), ordered by wallet name1273 */1274 public static List<WalletSummary> getSoftWalletSummaries(final Optional<Locale> localeOptional) {12751276 List<File> walletDirectories = findWalletDirectories(InstallationManager.getOrCreateApplicationDataDirectory());1277 Optional<String> walletRoot = INSTANCE.getCurrentWalletRoot();1278 List<WalletSummary> allWalletSummaries = findWalletSummaries(walletDirectories, walletRoot);1279 List<WalletSummary> softWalletSummaries = Lists.newArrayList();12801281 for (WalletSummary walletSummary : allWalletSummaries) {1282 if (WalletType.MBHD_SOFT_WALLET == walletSummary.getWalletType()1283 || WalletType.MBHD_SOFT_WALLET_BIP32 == walletSummary.getWalletType()1284 || WalletType.TREZOR_SOFT_WALLET == walletSummary.getWalletType()) {1285 softWalletSummaries.add(walletSummary);1286 }1287 }12881289 // Sort by name of wallet1290 Collections.sort(1291 softWalletSummaries, new Comparator<WalletSummary>() {1292 @Override1293 public int compare(WalletSummary me, WalletSummary other) {1294 String myName = me.getName();1295 if (myName == null) {1296 myName = "";1297 }1298 String otherName = other.getName();1299 if (otherName == null) {1300 otherName = "";1301 }1302 return Collators.newCollator(localeOptional).compare(myName, otherName);1303 }1304 });13051306 return softWalletSummaries;1307 }13081309 /**1310 * <p>Work out what wallets are available in a directory (typically the user data directory).1311 * This is achieved by looking for directories with a name like <code>"mbhd-walletId"</code>1312 *1313 * @param directoryToSearch The directory to search1314 *1315 * @return A list of files of wallet directories (never null)1316 */1317 public static List<File> findWalletDirectories(File directoryToSearch) {13181319 Preconditions.checkNotNull(directoryToSearch);13201321 File[] files = directoryToSearch.listFiles();1322 List<File> walletDirectories = Lists.newArrayList();13231324 // Look for file names with format "mbhd"-"walletId" and are not empty1325 if (files != null) {1326 for (File file : files) {1327 if (file.isDirectory()) {1328 String filename = file.getName();1329 if (filename.matches(REGEX_FOR_WALLET_DIRECTORY)) {1330 // The name matches so add it1331 walletDirectories.add(file);1332 }1333 }1334 }1335 }13361337 return walletDirectories;1338 }13391340 /**1341 * <p>Find Wallet summaries for all the wallet directories provided</p>1342 *1343 * @param walletDirectories The candidate wallet directory references1344 * @param walletRoot The wallet root of the first entry1345 *1346 * @return A list of wallet summaries (never null)1347 */1348 public static List<WalletSummary> findWalletSummaries(List<File> walletDirectories, Optional walletRoot) {13491350 Preconditions.checkNotNull(walletDirectories, "'walletDirectories' must be present");13511352 List<WalletSummary> walletList = Lists.newArrayList();1353 for (File walletDirectory : walletDirectories) {1354 if (walletDirectory.isDirectory()) {1355 String directoryName = walletDirectory.getName();1356 if (directoryName.matches(REGEX_FOR_WALLET_DIRECTORY)) {13571358 // The name matches so process it1359 WalletId walletId = new WalletId(directoryName.substring(MBHD_WALLET_PREFIX.length() + 1));1360 WalletSummary walletSummary = getOrCreateWalletSummary(walletDirectory, walletId);13611362 // Check if the wallet root is present and matches the file name1363 if (walletRoot.isPresent() && directoryName.equals(walletRoot.get())) {1364 walletList.add(0, walletSummary);1365 } else {1366 walletList.add(walletSummary);1367 }1368 }1369 }1370 }13711372 return walletList;1373 }137413751376 /**1377 * Get the spendable balance of the current wallet1378 * This is Optional.absent() if there is no wallet1379 */1380 public Optional<Coin> getCurrentWalletBalance() {1381 Optional<WalletSummary> currentWalletSummary = getCurrentWalletSummary();1382 if (currentWalletSummary.isPresent()) {1383 // Use the real wallet data1384 return Optional.of(currentWalletSummary.get().getWallet().getBalance());1385 } else {1386 // Unknown at this time1387 return Optional.absent();1388 }1389 }13901391 /**1392 * Get the balance of the current wallet including unconfirmed1393 * This is Optional.absent() if there is no wallet1394 */1395 public Optional<Coin> getCurrentWalletBalanceWithUnconfirmed() {1396 Optional<WalletSummary> currentWalletSummary = getCurrentWalletSummary();1397 if (currentWalletSummary.isPresent()) {1398 // Use the real wallet data1399 return Optional.of(currentWalletSummary.get().getWallet().getBalance(Wallet.BalanceType.ESTIMATED));1400 } else {1401 // Unknown at this time1402 return Optional.absent();1403 }1404 }14051406 /**1407 * @return The current wallet summary (present only if a wallet has been unlocked)1408 */1409 public Optional<WalletSummary> getCurrentWalletSummary() {1410 return currentWalletSummary;1411 }14121413 /**1414 * @param walletSummary The current wallet summary (null if a reset is required)1415 */1416 public void setCurrentWalletSummary(WalletSummary walletSummary) {14171418 if (walletSummary != null && walletSummary.getWallet() != null) {14191420 // Remove the previous WalletEventListener1421 walletSummary.getWallet().removeEventListener(this);14221423 // Add the wallet event listener1424 walletSummary.getWallet().addEventListener(this);1425 }14261427 this.currentWalletSummary = Optional.fromNullable(walletSummary);14281429 }14301431 /**1432 * @return The current wallet file (e.g. "/User/example/Application Support/MultiBitHD/mbhd-1111-2222-3333-4444/mbhd.wallet")1433 */1434 public Optional<File> getCurrentWalletFile(File applicationDataDirectory) {14351436 if (applicationDataDirectory != null && currentWalletSummary.isPresent()) {14371438 String walletFilename =1439 applicationDataDirectory1440 + File.separator1441 + WALLET_DIRECTORY_PREFIX1442 + WALLET_ID_SEPARATOR1443 + currentWalletSummary.get().getWalletId().toFormattedString()1444 + File.separator1445 + MBHD_WALLET_NAME;1446 return Optional.of(new File(walletFilename));14471448 } else {1449 return Optional.absent();1450 }14511452 }14531454 /**1455 * @return The current wallet summary file (e.g. "/User/example/Application Support/MultiBitHD/mbhd-1111-2222-3333-4444/mbhd.yaml")1456 */1457 public Optional<File> getCurrentWalletSummaryFile(File applicationDataDirectory) {14581459 if (applicationDataDirectory != null && currentWalletSummary.isPresent()) {14601461 String walletFilename =1462 applicationDataDirectory1463 + File.separator1464 + WALLET_DIRECTORY_PREFIX1465 + WALLET_ID_SEPARATOR1466 + currentWalletSummary.get().getWalletId().toFormattedString()1467 + File.separator1468 + MBHD_SUMMARY_NAME;1469 return Optional.of(new File(walletFilename));14701471 } else {1472 return Optional.absent();1473 }14741475 }14761477 /**1478 * @param walletDirectory The wallet directory containing the various wallet files1479 *1480 * @return A wallet summary file1481 */1482 public static File getOrCreateWalletSummaryFile(File walletDirectory) {1483 return SecureFiles.verifyOrCreateFile(walletDirectory, MBHD_SUMMARY_NAME);1484 }14851486 /**1487 * @return The current wallet root as defined in the configuration, or absent1488 */1489 public Optional<String> getCurrentWalletRoot() {1490 return Optional.fromNullable(Configurations.currentConfiguration.getWallet().getLastSoftWalletRoot());1491 }14921493 /**1494 * @param walletSummary The wallet summary to write1495 */1496 public static void updateWalletSummary(File walletSummaryFile, WalletSummary walletSummary) {14971498 if (walletSummary == null) {1499 log.warn("WalletSummary is missing. The wallet configuration file is NOT being overwritten.");1500 return;1501 }15021503 // Persist the new configuration1504 try (FileOutputStream fos = new FileOutputStream(walletSummaryFile)) {15051506 Yaml.writeYaml(fos, walletSummary);15071508 } catch (IOException e) {1509 ExceptionHandler.handleThrowable(e);1510 }1511 }15121513 /**1514 * @param walletDirectory The wallet directory to read1515 *1516 * @return The wallet summary if present, or a default if not1517 */1518 public static WalletSummary getOrCreateWalletSummary(File walletDirectory, WalletId walletId) {15191520 verifyWalletDirectory(walletDirectory);15211522 Optional<WalletSummary> walletSummaryOptional = Optional.absent();15231524 File walletSummaryFile = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_SUMMARY_NAME);1525 if (walletSummaryFile.exists()) {1526 try (InputStream is = new FileInputStream(walletSummaryFile)) {1527 // Load configuration (providing a default if none exists)1528 walletSummaryOptional = Yaml.readYaml(is, WalletSummary.class);1529 } catch (IOException e) {1530 // A full stack trace is too much here1531 log.warn("Could not read wallet summary:\n'{}'\nException: {}", walletDirectory.getAbsolutePath(), e.getMessage());1532 }1533 }15341535 final WalletSummary walletSummary;1536 if (walletSummaryOptional.isPresent()) {1537 walletSummary = walletSummaryOptional.get();1538 } else {1539 walletSummary = new WalletSummary();1540 // TODO No localiser available in core to localise core_default_wallet_name.1541 String shortWalletDirectory = walletDirectory.getName().substring(0, 13); // The mbhd and the first group of digits1542 walletSummary.setName("Wallet (" + shortWalletDirectory + "...)");1543 walletSummary.setNotes("");1544 }1545 walletSummary.setWalletId(walletId);15461547 return walletSummary;15481549 }1550 public static WalletSummary getAndChangeWalletSummary(File walletDirectory, WalletId walletId, CharSequence password) {15511552 verifyWalletDirectory(walletDirectory);15531554 Optional<WalletSummary> walletSummaryOptional = Optional.absent();15551556 File walletSummaryFile = new File(walletDirectory.getAbsolutePath() + File.separator + MBHD_SUMMARY_NAME);1557 if (walletSummaryFile.exists()) {1558 try (InputStream is = new FileInputStream(walletSummaryFile)) {1559 // Load configuration (providing a default if none exists)1560 walletSummaryOptional = Yaml.readYaml(is, WalletSummary.class);1561 } catch (IOException e) {1562 // A full stack trace is too much here1563 log.warn("Could not read wallet summary:\n'{}'\nException: {}", walletDirectory.getAbsolutePath(), e.getMessage());1564 }1565 }15661567 final WalletSummary walletSummary;1568 if (walletSummaryOptional.isPresent()) {1569 walletSummary = walletSummaryOptional.get();1570 try {1571 changeEncryptedPasswordAndBackupKeyWithRandomIV(walletSummary,password);1572 } catch (NoSuchAlgorithmException e) {1573 e.printStackTrace();1574 }1575 } else {1576 walletSummary = new WalletSummary();1577 String shortWalletDirectory = walletDirectory.getName().substring(0, 13); // The mbhd and the first group of digits1578 walletSummary.setName("Wallet (" + shortWalletDirectory + "...)");1579 walletSummary.setNotes("");1580 }1581 walletSummary.setWalletId(walletId);15821583 return walletSummary;15841585 }15861587 /**1588 * Write the encrypted wallet credentials and backup AES key to the wallet configuration.1589 * You probably want to save it afterwards with an updateSummary1590 *1591 * @param walletSummary The wallet summary to write the encrypted details for1592 * @param secret The secret used to derive the AES encryption key. This is typically created deterministically from the wallet words1593 * @param password The password you want to store encrypted1594 */1595 public static void writeEncryptedPasswordAndBackupKey(WalletSummary walletSummary, byte[] secret, String password) throws NoSuchAlgorithmException {15961597 Preconditions.checkNotNull(walletSummary, "'walletSummary' must be present");1598 Preconditions.checkNotNull(secret, "'secret' must be present");1599 Preconditions.checkNotNull(password, "'password' must be present");16001601 // Save the wallet credentials, AES encrypted with a key derived from the wallet secret1602 KeyParameter secretDerivedAESKey = org.multibit.commons.crypto.AESUtils.createAESKey(secret, SCRYPT_SALT);1603 byte[] passwordBytes = password.getBytes(Charsets.UTF_8);1604 SecureRandom secureRandom = new SecureRandom();1605 byte[] ivBytes = new byte[16];1606 secureRandom.nextBytes(ivBytes);1607 walletSummary.setInitializationVector(ivBytes);1608 byte[] paddedPasswordBytes = padPasswordBytes(passwordBytes);1609 byte[] encryptedPaddedPassword = AESUtils.encrypt(paddedPasswordBytes, secretDerivedAESKey, ivBytes);1610 walletSummary.setEncryptedPassword(encryptedPaddedPassword);16111612 // Save the backupAESKey, AES encrypted with a key generated from the wallet password1613 KeyParameter walletPasswordDerivedAESKey = org.multibit.commons.crypto.AESUtils.createAESKey(passwordBytes, SCRYPT_SALT);1614 byte[] encryptedBackupAESKey = AESUtils.encrypt(secretDerivedAESKey.getKey(), walletPasswordDerivedAESKey,ivBytes);1615 walletSummary.setEncryptedBackupKey(encryptedBackupAESKey);1616 }1617 /**1618 * Write the encrypted wallet credentials and backup AES key to the wallet configuration.1619 * You probably want to save it afterwards with an updateSummary1620 *1621 * @param walletSummary The wallet summary to write the encrypted details for1622 * @param password The password you want to store encrypted1623 */1624 public static void changeEncryptedPasswordAndBackupKeyWithRandomIV(WalletSummary walletSummary,CharSequence password) throws NoSuchAlgorithmException {16251626 Preconditions.checkNotNull(walletSummary, "'walletSummary' must be present");1627 Preconditions.checkNotNull(password, "'password' must be present");16281629 // Save the wallet credentials, AES encrypted with a key derived from the wallet secret16301631 byte[] passwordBytes = password.toString().getBytes(Charsets.UTF_8);1632 KeyParameter walletPasswordDerivedAESKey = org.multibit.commons.crypto.AESUtils.createAESKey(passwordBytes, SCRYPT_SALT);1633 byte[] encryptedSecretDerivedAESkey = walletSummary.getEncryptedBackupKey();1634 KeyParameter secretDerivedAESKey = new KeyParameter(AESUtils.decrypt(encryptedSecretDerivedAESkey,walletPasswordDerivedAESKey,WalletManager.deprecatedFixedAesInitializationVector()));1635 byte[] randomIvBytes = generateRandomIv();1636 walletSummary.setInitializationVector(randomIvBytes);1637 byte[] paddedPasswordBytes = padPasswordBytes(passwordBytes);1638 byte[] encryptedPaddedPassword = AESUtils.encrypt(paddedPasswordBytes, secretDerivedAESKey,randomIvBytes);1639 walletSummary.setEncryptedPassword(encryptedPaddedPassword);16401641 // Save the backupAESKey, AES encrypted with a key generated from the wallet password16421643 byte[] encryptedBackupAESKey = AESUtils.encrypt(secretDerivedAESKey.getKey(), walletPasswordDerivedAESKey,randomIvBytes);1644 walletSummary.setEncryptedBackupKey(encryptedBackupAESKey);1645 }16461647 /**1648 * @param walletDirectory The candidate wallet directory (e.g. "/User/example/Application Support/MultiBitHD/mbhd-11111111-22222222-33333333-44444444-55555555")1649 *1650 * @throws IllegalStateException If the wallet directory is malformed1651 */1652 private static void verifyWalletDirectory(File walletDirectory) {16531654 log.trace("Verifying wallet directory: '{}'", walletDirectory.getAbsolutePath());16551656 Preconditions.checkState(walletDirectory.isDirectory(), "'walletDirectory' must be a directory: '" + walletDirectory.getAbsolutePath() + "'");16571658 // Use the pre-compiled regex1659 boolean result = walletDirectoryPattern.matcher(walletDirectory.getName()).matches();16601661 Preconditions.checkState(result, "'walletDirectory' is not named correctly: '" + walletDirectory.getAbsolutePath() + "'");16621663 log.trace("Wallet directory verified ok");16641665 }16661667 /**1668 * Method to determine whether a message is 'mine', meaning an existing address in the current wallet1669 *1670 * @param address The address to test for wallet inclusion1671 *1672 * @return true if address is in current wallet, false otherwise1673 */1674 public boolean isAddressMine(Address address) {1675 try {16761677 Optional<WalletSummary> walletSummaryOptional = WalletManager.INSTANCE.getCurrentWalletSummary();16781679 if (walletSummaryOptional.isPresent()) {1680 WalletSummary walletSummary = walletSummaryOptional.get();16811682 Wallet wallet = walletSummary.getWallet();1683 ECKey signingKey = wallet.findKeyFromPubHash(address.getHash160());16841685 return signingKey != null;1686 } else {1687 // No wallet present1688 return false;1689 }1690 } catch (Exception e) {1691 // Some other problem1692 return false;1693 }1694 }16951696 /**1697 * @return True if current wallet is unlocked and represents a Trezor "hard" wallet1698 */1699 public boolean isUnlockedTrezorHardWallet() {1700 try {17011702 Optional<WalletSummary> walletSummaryOptional = WalletManager.INSTANCE.getCurrentWalletSummary();17031704 if (walletSummaryOptional.isPresent()) {1705 WalletSummary walletSummary = walletSummaryOptional.get();17061707 return WalletType.TREZOR_HARD_WALLET.equals(walletSummary.getWalletType());17081709 } else {1710 // No wallet present1711 return false;1712 }1713 } catch (Exception e) {1714 // Some other problem1715 return false;1716 }1717 }17181719 /**1720 * <p>Method to sign a message</p>1721 *1722 * @param addressText Text address to use to sign (makes UI Address conversion code DRY)1723 * @param messageText The message to sign1724 * @param walletPassword The wallet credentials1725 *1726 * @return A "sign message result" describing the outcome1727 */1728 public SignMessageResult signMessage(String addressText, String messageText, String walletPassword) {1729 if (Strings.isNullOrEmpty(addressText)) {1730 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_ENTER_ADDRESS, null);1731 }17321733 if (Strings.isNullOrEmpty(messageText)) {1734 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_ENTER_MESSAGE, null);1735 }17361737 if (Strings.isNullOrEmpty(walletPassword)) {1738 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_ENTER_PASSWORD, null);1739 }17401741 try {1742 Address signingAddress = new Address(BitcoinNetwork.current().get(), addressText);17431744 Optional<WalletSummary> walletSummaryOptional = WalletManager.INSTANCE.getCurrentWalletSummary();17451746 if (walletSummaryOptional.isPresent()) {1747 WalletSummary walletSummary = walletSummaryOptional.get();17481749 Wallet wallet = walletSummary.getWallet();17501751 ECKey signingKey = wallet.findKeyFromPubHash(signingAddress.getHash160());1752 if (signingKey != null) {1753 if (signingKey.getKeyCrypter() != null) {1754 KeyParameter aesKey = signingKey.getKeyCrypter().deriveKey(walletPassword);1755 ECKey decryptedSigningKey = signingKey.decrypt(aesKey);17561757 String signatureBase64 = decryptedSigningKey.signMessage(messageText);1758 return new SignMessageResult(Optional.of(signatureBase64), true, CoreMessageKey.SIGN_MESSAGE_SUCCESS, null);1759 } else {1760 // The signing key is not encrypted but it should be1761 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_SIGNING_KEY_NOT_ENCRYPTED, null);1762 }1763 } else {1764 // No signing key found.1765 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_NO_SIGNING_KEY, new Object[]{addressText});1766 }1767 } else {1768 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_NO_WALLET, null);1769 }1770 } catch (KeyCrypterException e) {1771 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_NO_PASSWORD, null);1772 } catch (RuntimeException | AddressFormatException e) {1773 log.error("Sign message failure", e);1774 return new SignMessageResult(Optional.<String>absent(), false, CoreMessageKey.SIGN_MESSAGE_FAILURE, null);1775 }1776 }17771778 /**1779 * <p>Method to verify a message</p>1780 *1781 * @param addressText Text address to use to sign (makes UI Address conversion code DRY)1782 * @param messageText The message to sign1783 * @param signatureText The signature text (can include CRLF characters which will be stripped)1784 *1785 * @return A "verify message result" describing the outcome1786 */1787 public VerifyMessageResult verifyMessage(String addressText, String messageText, String signatureText) {1788 if (Strings.isNullOrEmpty(addressText)) {1789 return new VerifyMessageResult(false, CoreMessageKey.VERIFY_MESSAGE_ENTER_ADDRESS, null);1790 }17911792 if (Strings.isNullOrEmpty(messageText)) {1793 return new VerifyMessageResult(false, CoreMessageKey.VERIFY_MESSAGE_ENTER_MESSAGE, null);1794 }17951796 if (Strings.isNullOrEmpty(signatureText)) {1797 return new VerifyMessageResult(false, CoreMessageKey.VERIFY_MESSAGE_ENTER_SIGNATURE, null);1798 }17991800 try {1801 Address signingAddress = new Address(BitcoinNetwork.current().get(), addressText);18021803 // Strip CRLF from signature text1804 signatureText = signatureText.replaceAll("\n", "").replaceAll("\r", "");18051806 ECKey key = ECKey.signedMessageToKey(messageText, signatureText);1807 Address gotAddress = key.toAddress(BitcoinNetwork.current().get());1808 if (signingAddress.equals(gotAddress)) {1809 return new VerifyMessageResult(true, CoreMessageKey.VERIFY_MESSAGE_VERIFY_SUCCESS, null);1810 } else {1811 return new VerifyMessageResult(false, CoreMessageKey.VERIFY_MESSAGE_VERIFY_FAILURE, null);1812 }18131814 } catch (RuntimeException | AddressFormatException | SignatureException e) {1815 log.warn("Failed to verify the message", e.getClass().getCanonicalName() + " " + e.getMessage());1816 return new VerifyMessageResult(false, CoreMessageKey.VERIFY_MESSAGE_FAILURE, null);1817 }1818 }18191820 /**1821 * Password short passwords with extra bytes - this is done so that the existence of short passwords is not leaked by1822 * the length of the encrypted credentials (which is always a multiple of the AES block size (16 bytes).1823 *1824 * @param passwordBytes the credentials bytes to pad1825 *1826 * @return paddedPasswordBytes - this is guaranteed to be longer than 48 bytes. Byte 0 indicates the number of padding bytes,1827 * which are random bytes stored from byte 1 to byte <number of padding bytes). The real credentials is stored int he remaining bytes1828 */1829 public static byte[] padPasswordBytes(byte[] passwordBytes) {1830 if (passwordBytes.length > AESUtils.BLOCK_LENGTH * 3) {1831 // No padding required - add a zero to the beginning of the credentials bytes (to indicate no padding bytes)1832 return Bytes.concat(new byte[]{(byte) 0x0}, passwordBytes);1833 } else {1834 if (passwordBytes.length > AESUtils.BLOCK_LENGTH * 2) {1835 // Pad with 16 random bytes1836 byte[] paddingBytes = new byte[16];1837 random.nextBytes(paddingBytes);1838 return Bytes.concat(new byte[]{(byte) 0x10}, paddingBytes, passwordBytes);1839 } else {1840 if (passwordBytes.length > AESUtils.BLOCK_LENGTH) {1841 // Pad with 32 random bytes1842 byte[] paddingBytes = new byte[32];1843 random.nextBytes(paddingBytes);1844 return Bytes.concat(new byte[]{(byte) 0x20}, paddingBytes, passwordBytes);1845 } else {1846 // Pad with 48 random bytes1847 byte[] paddingBytes = new byte[48];1848 random.nextBytes(paddingBytes);1849 return Bytes.concat(new byte[]{(byte) 0x30}, paddingBytes, passwordBytes);1850 }1851 }1852 }1853 }18541855 /**1856 * Unpad credentials bytes, removing the random prefix bytes length marker byte and te random bytes themselves1857 */1858 public static byte[] unpadPasswordBytes(byte[] paddedPasswordBytes) {1859 Preconditions.checkNotNull(paddedPasswordBytes);1860 Preconditions.checkState(paddedPasswordBytes.length > 0);18611862 // Get the length of the pad1863 int lengthOfPad = (int) paddedPasswordBytes[0];18641865 if (lengthOfPad > paddedPasswordBytes.length - 1) {1866 throw new IllegalStateException("Stored encrypted credentials is not in the correct format");1867 }1868 return Arrays.copyOfRange(paddedPasswordBytes, 1 + lengthOfPad, paddedPasswordBytes.length);1869 }18701871 /**1872 * Generate the DeterministicKey from the private master key for a Trezor wallet1873 * <p/>1874 * For a real Trezor device this will be the result of a GetPublicKey of the M/44'/0'/0' path, received as an xpub and then converted to a DeterministicKey1875 *1876 * @param privateMasterKey the private master key derived from the wallet seed1877 *1878 * @return the public only DeterministicSeed corresponding to the root Trezor wallet node e.g. M/44'/0'/0'1879 */1880 public static DeterministicKey generateTrezorWalletRootNode(DeterministicKey privateMasterKey) {1881 DeterministicKey key_m_44h = HDKeyDerivation.deriveChildKey(privateMasterKey, new ChildNumber(44 | ChildNumber.HARDENED_BIT));1882 log.debug("key_m_44h deterministic key = " + key_m_44h);18831884 DeterministicKey key_m_44h_0h = HDKeyDerivation.deriveChildKey(key_m_44h, ChildNumber.ZERO_HARDENED);1885 log.debug("key_m_44h_0h deterministic key = " + key_m_44h_0h);18861887 DeterministicKey key_m_44h_0h_0h = HDKeyDerivation.deriveChildKey(key_m_44h_0h, ChildNumber.ZERO_HARDENED);1888 log.debug("key_m_44h_0h_0h = " + key_m_44h_0h_0h);18891890 return key_m_44h_0h_0h;1891 }18921893 /**1894 * @param shutdownType The shutdown type1895 */1896 public void shutdownNow(ShutdownEvent.ShutdownType shutdownType) {18971898 log.debug("Received shutdown: {}", shutdownType.name());18991900 // Writing out a wallet to a clear text file is security risk1901 // Do not do it except for debug1902 // log.debug("Wallet at shutdown:\n{}\n", getCurrentWalletSummary().isPresent() ? getCurrentWalletSummary().get().getWallet() : "");1903 currentWalletSummary = Optional.absent();19041905 }19061907 /**1908 * <p>Save the current wallet to application directory, create a rolling backup and a cloud backup</p>1909 */1910 public void saveWallet() {19111912 // Save the current wallet immediately1913 if (getCurrentWalletSummary().isPresent()) {19141915 WalletSummary walletSummary = WalletManager.INSTANCE.getCurrentWalletSummary().get();1916 WalletId walletId = walletSummary.getWalletId();1917 log.debug("Saving wallet with id : {}, height : {}", walletId, walletSummary.getWallet().getLastBlockSeenHeight());19181919 // Check that the password is the correct password for this wallet1920 if (!walletId.equals(walletSummary.getWalletPassword().getWalletId())) {1921 throw new WalletSaveException("The password specified is not the password for this wallet");1922 }19231924 try {1925 File applicationDataDirectory = InstallationManager.getOrCreateApplicationDataDirectory();1926 File currentWalletFile = WalletManager.INSTANCE.getCurrentWalletFile(applicationDataDirectory).get();19271928 walletSummary.getWallet().saveToFile(currentWalletFile);19291930 File encryptedAESCopy = EncryptedFileReaderWriter.makeAESEncryptedCopyAndDeleteOriginal(currentWalletFile, walletSummary.getWalletPassword().getPassword());1931 if (encryptedAESCopy == null) {1932 log.debug("Did not create AES encrypted wallet");1933 } else {1934 log.debug("Created AES encrypted wallet as file:\n'{}'\nSize: {} bytes", encryptedAESCopy.getAbsolutePath(), encryptedAESCopy.length());1935 }1936 BackupService backupService = CoreServices.getOrCreateBackupService();1937 backupService.rememberWalletSummaryAndPasswordForRollingBackup(walletSummary, walletSummary.getWalletPassword().getPassword());1938 backupService.rememberWalletIdAndPasswordForLocalZipBackup(walletSummary.getWalletId(), walletSummary.getWalletPassword().getPassword());1939 backupService.rememberWalletIdAndPasswordForCloudZipBackup(walletSummary.getWalletId(), walletSummary.getWalletPassword().getPassword());19401941 } catch (IOException ioe) {1942 log.error("Could not write wallet and backups for wallet with id '" + walletId + "' successfully. The error was '" + ioe.getMessage() + "'");1943 }1944 }19451946 }19471948 /**1949 * Closes the wallet1950 */1951 public void closeWallet() {19521953 if (WalletManager.INSTANCE.getCurrentWalletSummary().isPresent()) {1954 try {1955 Wallet wallet = WalletManager.INSTANCE.getCurrentWalletSummary().get().getWallet();1956 log.debug("Shutdown wallet autosave at height: {} ", wallet.getLastBlockSeenHeight());1957 wallet.shutdownAutosaveAndWait();1958 } catch (IllegalStateException ise) {1959 // If there is no autosaving set up yet then that is ok1960 if (!ise.getMessage().contains("Auto saving not enabled.")) {1961 throw ise;1962 }1963 }1964 } else {1965 log.info("No current wallet summary to provide wallet");1966 }1967 }1968 public static byte[] generateRandomIv(){1969 SecureRandom secureRandom = new SecureRandom();1970 byte[] ivBytes = new byte[16];1971 secureRandom.nextBytes(ivBytes);1972 return ivBytes;1973 }1974}Read in context
Coordinates creation, encrypted storage, wallet loading and wallet-type inference. Compare the legacy noncompliant creation method with the BIP32 method. Follow the encrypted-file and wallet-summary dependencies rather than reading this large class in isolation.
Compare the callers, dependency versions and corresponding tests. The pinned file is one part of a larger application. Do not execute write, prune or migration routines against your only copy of a wallet.