MultiBit HDTHE TECHNICAL RECOVERY LIBRARYmail@keychainx.io ↗

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.

By KeychainX · 20 September 2026 · Source revision 24a12b199a01

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.

Sources at the reviewed revision