package org.multibit.file; import com.google.bitcoin.core.Utils; import com.google.bitcoin.core.Wallet; import com.google.bitcoin.crypto.EncryptedPrivateKey; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterScrypt; import com.google.protobuf.ByteString; import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Protos.ScryptParameters; import org.bitcoinj.wallet.Protos.Wallet.EncryptionType; import org.multibit.model.bitcoin.BitcoinModel; import org.multibit.model.bitcoin.WalletData; import org.multibit.model.bitcoin.WalletInfoData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.util.Arrays; import java.io.*; import java.security.SecureRandom; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; /** * Class to manage creation and reading back of the wallet backups. */ public enum BackupManager { INSTANCE; private static final Logger log = LoggerFactory.getLogger(BackupManager.class); transient private static SecureRandom secureRandom = new SecureRandom(); public static final String BACKUP_SUFFIX_FORMAT = "yyyyMMddHHmmss"; private static final String SEPARATOR = "-"; private DateFormat dateFormat; private Date dateForBackupName = null; public static final String TOP_LEVEL_WALLET_BACKUP_SUFFIX = "-data"; public static final String PRIVATE_KEY_BACKUP_DIRECTORY_NAME = "key-backup"; public static final String ROLLING_WALLET_BACKUP_DIRECTORY_NAME = "rolling-backup"; public static final String ENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME = "wallet-backup"; public static final String UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME = "wallet-unenc-backup"; public static final int MAXIMUM_NUMBER_OF_BACKUPS = 60; // Chosen so that you will have about weekly backups for a year, fortnightly over two years. public static final int NUMBER_OF_FIRST_WALLETS_TO_ALWAYS_KEEP = 2; public static final int NUMBER_OF_LAST_WALLETS_TO_ALWAYS_KEEP = 8; // Must be at least 1. public static final String REGEX_FOR_WALLET_SUFFIX = ".*\\.wallet$"; public static final String REGEX_FOR_TIMESTAMP_AND_KEY_SUFFIX = ".*-\\d{" + BACKUP_SUFFIX_FORMAT.length() + "}\\.key$"; public static final String REGEX_FOR_TIMESTAMP_AND_WALLET_SUFFIX = ".*-\\d{" + BACKUP_SUFFIX_FORMAT.length() + "}\\.wallet$"; public static final String REGEX_FOR_TIMESTAMP_AND_INFO_SUFFIX = ".*-\\d{" + BACKUP_SUFFIX_FORMAT.length() + "}\\.info$"; public static final String REGEX_FOR_TIMESTAMP_AND_WALLET_AND_CIPHER_SUFFIX = ".*-\\d{" + BACKUP_SUFFIX_FORMAT.length() + "}\\.wallet\\.cipher$"; public static final int EXPECTED_LENGTH_OF_SALT = 8; public static final int EXPECTED_LENGTH_OF_IV = 16; public static final String INFO_FILE_SUFFIX_STRING = "info"; public static final String FILE_ENCRYPTED_WALLET_SUFFIX = "cipher"; public static final byte FILE_ENCRYPTED_VERSION_NUMBER = (byte) 0x00; public static final byte[] ENCRYPTED_FILE_FORMAT_MAGIC_BYTES = new byte[]{(byte) 0x6D, (byte) 0x65, (byte) 0x6E, (byte) 0x64, (byte) 0x6F, (byte) 0x7A, (byte) 0x61}; // mendoza in ASCII /** * Backup the perWalletModelData to the <wallet>-data/wallet-backup (encrypted) or wallet-unenc-backup (unencrypted) directories. * * @param perWalletModelData */ public void backupPerWalletModelData(FileHandler fileHandler, WalletData perWalletModelData) { if (perWalletModelData == null || fileHandler == null) { return; } // Write to backup files. try { String backupSuffixText; if (EncryptionType.UNENCRYPTED == perWalletModelData.getWallet().getEncryptionType()) { backupSuffixText = UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME; } else { backupSuffixText = ENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME; } String walletBackupFilename = createBackupFilename(new File(perWalletModelData.getWalletFilename()), backupSuffixText, true, false, BitcoinModel.WALLET_FILE_EXTENSION); perWalletModelData.setWalletBackupFilename(walletBackupFilename); String walletInfoBackupFilename = walletBackupFilename.replaceAll(BitcoinModel.WALLET_FILE_EXTENSION + "$", INFO_FILE_SUFFIX_STRING); perWalletModelData.setWalletInfoBackupFilename(walletInfoBackupFilename); // If the backup directory is needs thinning, do so. thinBackupDirectory(perWalletModelData.getWalletFilename(), backupSuffixText); fileHandler.saveWalletAndWalletInfoSimple(perWalletModelData, walletBackupFilename, walletInfoBackupFilename); log.info("Written backup wallet files to '" + walletBackupFilename + "', '" + walletInfoBackupFilename + "'"); } catch (IOException ioe) { log.error(ioe.getClass().getCanonicalName() + " " + ioe.getMessage()); throw new WalletSaveException("Cannot backup wallet '" + perWalletModelData.getWalletFilename(), ioe); } catch (Exception e) { log.error(e.getClass().getCanonicalName() + " " + e.getMessage()); throw new WalletSaveException("Cannot backup wallet '" + perWalletModelData.getWalletFilename(), e); } } public void fileLevelEncryptUnencryptedWalletBackups(WalletData perWalletModelData, CharSequence passwordToUse) { // See if there are any unencrypted wallet backups. Collection<File> unencryptedWalletBackups = getWalletsInBackupDirectory(perWalletModelData.getWalletFilename(), UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME); // Copy and encrypt each file and secure delete the original. for (File loopFile : unencryptedWalletBackups) { try { String encryptedFilename = loopFile.getAbsolutePath() + "." + FILE_ENCRYPTED_WALLET_SUFFIX; copyFileAndEncrypt(loopFile, new File(encryptedFilename), passwordToUse); SecureFiles.secureDelete(loopFile); } catch (IOException | IllegalArgumentException | IllegalStateException | KeyCrypterException ioe) { log.error(ioe.getClass().getName() + " " + ioe.getMessage()); } } } /** * Create a backup filename the format is: original file: filename.suffix. * backup file: * (without subDirectorySuffix) filename-yyyymmddhhmmss.suffix * (with subDirectorySuffix) filename-data/subDirectorySuffix/filename-yyyymmddhhmmss.suffix * * (Any intermediate directories are automatically created if necessary) * * @param file * @param subDirectorySuffix - subdirectory to add to backup file e.g key-backup. null for no subdirectory. * @param saveBackupDate - save the backup date for use later * @param reusePreviousBackupDate * Reuse the previously created backup date so that wallet and wallet info names match * @param suffixToUse * the suffix text to use * @return String the name of the created filename. * @throws IOException */ String createBackupFilename(File file, String subDirectorySuffix, boolean saveBackupDate, boolean reusePreviousBackupDate, String suffixToUse) throws IOException { String filenameLong = file.getAbsolutePath(); // Full path. String filenameShort = file.getName(); // Just the filename. String topLevelBackupDirectoryName = calculateTopLevelBackupDirectoryName(file); createDirectoryIfNecessary(topLevelBackupDirectoryName); // Find suffix and stems of filename. int suffixSeparatorLong = filenameLong.lastIndexOf('.'); String stemLong = filenameLong.substring(0, suffixSeparatorLong); int suffixSeparatorShort= filenameShort.lastIndexOf('.'); String stemShort = filenameShort.substring(0, suffixSeparatorShort); String suffix; if (suffixToUse != null) { suffix = "." + suffixToUse; } else { suffix = filenameLong.substring(suffixSeparatorLong); // Includes separating dot. } Date backupDateToUse = new Date(); if (saveBackupDate) { dateForBackupName = backupDateToUse; } if (reusePreviousBackupDate) { backupDateToUse = dateForBackupName; } String backupFilename; if (dateFormat == null) { dateFormat = new SimpleDateFormat(BACKUP_SUFFIX_FORMAT); } if (subDirectorySuffix != null && subDirectorySuffix.length() > 0) { String backupFilenameShort = stemShort + SEPARATOR + dateFormat.format(backupDateToUse) + suffix; String subDirectoryName = topLevelBackupDirectoryName + File.separator + subDirectorySuffix; createDirectoryIfNecessary(subDirectoryName); backupFilename = subDirectoryName + File.separator + backupFilenameShort; } else { backupFilename = stemLong + SEPARATOR + dateFormat.format(backupDateToUse) + suffix; } return backupFilename; } /** * Thin the wallet backups when they reach the MAXIMUM_NUMBER_OF_BACKUPS setting. * Thinning is done by removing the most quickly replaced backup, except for the first and last few * (as they are considered to be more valuable backups). * * @param backupDirectoryName */ void thinBackupDirectory(String walletFilename, String backupSuffixText) { if (dateFormat == null) { dateFormat = new SimpleDateFormat(BACKUP_SUFFIX_FORMAT); } if (walletFilename == null || backupSuffixText == null) { return; } // Find out how many wallet backups there are. List<File> backupWallets = getWalletsInBackupDirectory(walletFilename, backupSuffixText); if (backupWallets.size() < MAXIMUM_NUMBER_OF_BACKUPS) { // No thinning required. return; } // Work out the date the backup was made for each of the wallet. // This is done using the timestamp rather than the write time of the file. Map<File, Date> mapOfFileToBackupTimes = new HashMap<File, Date>(); for (int i = 0; i < backupWallets.size(); i++) { String filename = backupWallets.get(i).getName(); if (filename.length() > 22) { // 22 = 1 for hyphen + 14 for timestamp + 1 for dot + 6 for wallet. int startOfTimestamp = filename.length() - 21; // 21 = 14 for timestamp + 1 for dot + 6 for wallet. String timestampText = filename.substring(startOfTimestamp, startOfTimestamp + BACKUP_SUFFIX_FORMAT.length() ); try { Date parsedTimestamp = dateFormat.parse(timestampText); mapOfFileToBackupTimes.put(backupWallets.get(i), parsedTimestamp); } catch (ParseException pe) { // Cannot parse text - may be some other type of file the user has put in the directory. log.debug("For wallet '" + filename + " could not parse the timestamp of '" + timestampText + "'."); } } } // See which wallet is most quickly replaced by another backup - this will be thinned. int walletBackupToDeleteIndex = -1; // Not set yet. long walletBackupToDeleteReplacementTimeMillis = Integer.MAX_VALUE; // How quickly the wallet was replaced by a later one. for (int i = 0; i < backupWallets.size(); i++) { if ((i < NUMBER_OF_FIRST_WALLETS_TO_ALWAYS_KEEP) || (i >= backupWallets.size() - NUMBER_OF_LAST_WALLETS_TO_ALWAYS_KEEP)) { // Keep the very first and last wallets always. } else { // If there is a data directory for the backup then it may have been opened // in MultiBit so we will skip considering it for deletion. String possibleDataDirectory = calculateTopLevelBackupDirectoryName(backupWallets.get(i)); boolean theWalletHasADataDirectory = (new File(possibleDataDirectory)).exists(); // Work out how quickly the wallet is replaced by the next backup. Date thisWalletTimestamp = mapOfFileToBackupTimes.get(backupWallets.get(i)); Date nextWalletTimestamp = mapOfFileToBackupTimes.get(backupWallets.get(i + 1)); if (thisWalletTimestamp != null && nextWalletTimestamp != null) { long deltaTimeMillis = nextWalletTimestamp.getTime() - thisWalletTimestamp.getTime(); if (deltaTimeMillis < walletBackupToDeleteReplacementTimeMillis && !theWalletHasADataDirectory) { // This is the best candidate for deletion so far. walletBackupToDeleteIndex = i; walletBackupToDeleteReplacementTimeMillis = deltaTimeMillis; } } } } if (walletBackupToDeleteIndex > -1) { try { // Secure delete the chosen backup wallet and its info file if present. log.debug("To save space, secure deleting backup wallet '" + backupWallets.get(walletBackupToDeleteIndex).getAbsolutePath() + "'."); SecureFiles.secureDelete(backupWallets.get(walletBackupToDeleteIndex)); String walletInfoBackupFilename = backupWallets.get(walletBackupToDeleteIndex).getAbsolutePath() .replaceAll(BitcoinModel.WALLET_FILE_EXTENSION + "$", INFO_FILE_SUFFIX_STRING); File walletInfoBackup = new File(walletInfoBackupFilename); if (walletInfoBackup.exists()) { log.debug("To save space, secure deleting backup info file '" + walletInfoBackup.getAbsolutePath() + "'."); SecureFiles.secureDelete(walletInfoBackup); } } catch (IOException ioe) { log.error(ioe.getClass().getName() + " " + ioe.getMessage()); } } } void copyFileAndEncrypt(File sourceFile, File destinationFile, CharSequence passwordToUse) throws IOException { if (passwordToUse == null || passwordToUse.length() == 0) { throw new IllegalArgumentException("Password cannot be blank"); } if (destinationFile.exists()) { throw new IllegalArgumentException("The destination file '" + destinationFile.getAbsolutePath() + "' already exists."); } else { // Attempt to create it if (!destinationFile.createNewFile()) { throw new IllegalArgumentException("The destination file '" + destinationFile.getAbsolutePath() + "' could not be created. Check permissions."); } } // Read in the source file. byte[] sourceFileUnencrypted = FileHandler.read(sourceFile); // Encrypt the data. byte[] salt = new byte[KeyCrypterScrypt.SALT_LENGTH]; secureRandom.nextBytes(salt); Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder() .setSalt(ByteString.copyFrom(salt)); ScryptParameters scryptParameters = scryptParametersBuilder.build(); KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(scryptParameters); EncryptedPrivateKey encryptedData = keyCrypter.encrypt(sourceFileUnencrypted, keyCrypter.deriveKey(passwordToUse)); // The format of the encrypted data is: // 7 magic bytes 'mendoza' in ASCII. // 1 byte version number of format - initially set to 0 // 8 bytes salt // 16 bytes iv // rest of file is the encrypted byte data FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(destinationFile); fileOutputStream.write(ENCRYPTED_FILE_FORMAT_MAGIC_BYTES); // file format version. fileOutputStream.write(FILE_ENCRYPTED_VERSION_NUMBER); fileOutputStream.write(salt); // 8 bytes. fileOutputStream.write(encryptedData.getInitialisationVector()); // 16 bytes. System.out.println(Utils.bytesToHexString(encryptedData.getInitialisationVector())); fileOutputStream.write(encryptedData.getEncryptedBytes()); System.out.println(Utils.bytesToHexString(encryptedData.getEncryptedBytes())); } finally { if (fileOutputStream != null) { fileOutputStream.flush(); fileOutputStream.close(); } } // Read in the file again and decrypt it to make sure everything was ok. byte[] phoenix = readFileAndDecrypt(destinationFile, passwordToUse); if (!Arrays.areEqual(sourceFileUnencrypted, phoenix)) { throw new IOException("File '" + sourceFile.getAbsolutePath() + "' was not correctly encrypted to file '" + destinationFile.getAbsolutePath()); } } public byte[] readFileAndDecrypt(File encryptedFile, CharSequence passwordToUse) throws IOException { // Read in the encrypted file. byte[] sourceFileEncrypted = FileHandler.read(encryptedFile); // Check the first bytes match the magic number. if (!Arrays.areEqual(ENCRYPTED_FILE_FORMAT_MAGIC_BYTES, Arrays.copyOfRange(sourceFileEncrypted, 0, ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length))) { throw new IOException("File '" + encryptedFile.getAbsolutePath() + "' did not start with the correct magic bytes."); } // If the file is too short don't process it. if (sourceFileEncrypted.length < ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length + 1 + KeyCrypterScrypt.SALT_LENGTH + KeyCrypterScrypt.BLOCK_LENGTH) { throw new IOException("File '" + encryptedFile.getAbsolutePath() + "' is too short to decrypt. It is " + sourceFileEncrypted.length + " bytes long."); } // Check the format version. String versionNumber = "" + sourceFileEncrypted[ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length]; //System.out.println("FileHandler - versionNumber = " + versionNumber); if (!("0".equals(versionNumber))) { throw new IOException("File '" + encryptedFile.getAbsolutePath() + "' did not have the expected version number of 0. It was " + versionNumber); } // Extract the salt. byte[] salt = Arrays.copyOfRange(sourceFileEncrypted, ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length + 1, ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length + 1 + KeyCrypterScrypt.SALT_LENGTH); //System.out.println("FileHandler - salt = " + Utils.bytesToHexString(salt)); // Extract the IV. byte[] iv = Arrays.copyOfRange(sourceFileEncrypted, ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length + 1 + KeyCrypterScrypt.SALT_LENGTH , ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length + 1 + KeyCrypterScrypt.SALT_LENGTH + KeyCrypterScrypt.BLOCK_LENGTH); //System.out.println("FileHandler - iv = " + Utils.bytesToHexString(iv)); // Extract the encrypted bytes. byte[] encryptedBytes = Arrays.copyOfRange(sourceFileEncrypted, ENCRYPTED_FILE_FORMAT_MAGIC_BYTES.length + 1 + KeyCrypterScrypt.SALT_LENGTH + KeyCrypterScrypt.BLOCK_LENGTH , sourceFileEncrypted.length); //System.out.println("FileHandler - encryptedBytes = " + Utils.bytesToHexString(encryptedBytes)); // Decrypt the data. Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt)); ScryptParameters scryptParameters = scryptParametersBuilder.build(); KeyCrypter keyCrypter = new KeyCrypterScrypt(scryptParameters); EncryptedPrivateKey encryptedPrivateKey = new EncryptedPrivateKey(iv, encryptedBytes); return keyCrypter.decrypt(encryptedPrivateKey, keyCrypter.deriveKey(passwordToUse)); } void createBackupDirectories(File walletFile) { if (walletFile == null) { return; } // Create the top-level directory for the wallet specific data, if necessary. String topLevelBackupDirectoryName = calculateTopLevelBackupDirectoryName(walletFile); createDirectoryIfNecessary(topLevelBackupDirectoryName); // Create the backup directories for the private keys, rolling backup and wallets. String privateKeysBackupDirectoryName = topLevelBackupDirectoryName + File.separator + PRIVATE_KEY_BACKUP_DIRECTORY_NAME; createDirectoryIfNecessary(privateKeysBackupDirectoryName); String rollingWalletBackupDirectoryName = topLevelBackupDirectoryName + File.separator + ROLLING_WALLET_BACKUP_DIRECTORY_NAME; createDirectoryIfNecessary(rollingWalletBackupDirectoryName); String unencryptedWalletBackupDirectoryName = topLevelBackupDirectoryName + File.separator + UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME; createDirectoryIfNecessary(unencryptedWalletBackupDirectoryName); String encryptedWalletBackupDirectoryName = topLevelBackupDirectoryName + File.separator + ENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME; createDirectoryIfNecessary(encryptedWalletBackupDirectoryName); } /** * Work out the best wallet backups to try to load * @param walletFile * @return Collection<String> The best wallets to try to load, in order of goodness. */ Collection<String> calculateBestWalletBackups(File walletFile, WalletInfoData walletInfo) { Collection<String> backupWalletsToTry = new ArrayList<String>(); // Get the name of the rolling backup file. String walletBackupFilenameLong = walletInfo.getProperty(BitcoinModel.WALLET_BACKUP_FILE); String walletBackupFilenameShort = null; if (walletBackupFilenameLong != null && !"".equals(walletBackupFilenameLong)) { File walletBackupFile = new File(walletBackupFilenameLong); walletBackupFilenameShort = walletBackupFile.getName(); if (!walletBackupFile.exists()) { walletBackupFilenameLong = null; walletBackupFilenameShort = null; } } else { // No backup file was listed in the info file. Maybe it is damaged so take the most recent // file in the rolling backup directory, if there is one. Collection<File> rollingWalletBackups = getWalletsInBackupDirectory(walletFile.getAbsolutePath(), ROLLING_WALLET_BACKUP_DIRECTORY_NAME); if (rollingWalletBackups != null && !rollingWalletBackups.isEmpty()) { List<String> rollingWalletBackupFilenames = new ArrayList<String>(); for (File file : rollingWalletBackups) { rollingWalletBackupFilenames.add(file.getAbsolutePath()); } Collections.sort(rollingWalletBackupFilenames); walletBackupFilenameLong = rollingWalletBackupFilenames.get(rollingWalletBackupFilenames.size() - 1); walletBackupFilenameShort = (new File(walletBackupFilenameLong)).getName(); } } Collection<File> unencryptedWalletBackups = getWalletsInBackupDirectory(walletFile.getAbsolutePath(), UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME); Collection<File> encryptedWalletBackups = getWalletsInBackupDirectory(walletFile.getAbsolutePath(), ENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME); // Make a list of ALL the unencrypted and encrypted backup names and sort them. // Because the backups have a timestamp YYYYMMDDHHMMSS sort in ascending order gives most recent - we will use this one. List<String> encryptedAndUnencryptedFilenames = new ArrayList<String>(); // Sorting is done by the filename, keep track of the corresponding absolute path. Map<String, String> shortNamesToLongMap = new HashMap<String, String>(); if (unencryptedWalletBackups != null) { for (File file : unencryptedWalletBackups) { encryptedAndUnencryptedFilenames.add(file.getName()); shortNamesToLongMap.put(file.getName(), file.getAbsolutePath()); } } if (encryptedWalletBackups != null) { for (File file : encryptedWalletBackups) { encryptedAndUnencryptedFilenames.add(file.getName()); // If there is a duplicate, encrypted wallets are preferred. shortNamesToLongMap.put(file.getName(), file.getAbsolutePath()); } } Collections.sort(encryptedAndUnencryptedFilenames); String bestCandidateShort = null; String bestCandidateLong = null; if (encryptedAndUnencryptedFilenames.size() > 0) { bestCandidateShort = encryptedAndUnencryptedFilenames.get(encryptedAndUnencryptedFilenames.size() - 1); if (bestCandidateShort != null) { bestCandidateLong = shortNamesToLongMap.get(bestCandidateShort); } } log.debug("For wallet '" + walletFile + "' the rolling backup file was '" + walletBackupFilenameLong + "' and the best encrypted/ unencrypted backup was '" + bestCandidateLong + "'"); if (walletBackupFilenameLong == null) { if (bestCandidateLong == null) { // No backups to try. } else { // bestCandidate only. backupWalletsToTry.add(bestCandidateLong); } } else { if (bestCandidateLong == null) { // WalletBackupFilename only. backupWalletsToTry.add(walletBackupFilenameLong); } else { // Have both. Try the most recent first (preferring the backups to the rolling backups if there is a tie). if (walletBackupFilenameShort.compareTo(bestCandidateShort) <= 0) { backupWalletsToTry.add(bestCandidateLong); backupWalletsToTry.add(walletBackupFilenameLong); } else { backupWalletsToTry.add(walletBackupFilenameLong); backupWalletsToTry.add(bestCandidateLong); } } } return backupWalletsToTry; } public String calculateTopLevelBackupDirectoryName(File walletFile) { // Work out the name of the top level wallet backup directory. String walletPath = walletFile.getAbsolutePath(); // Remove any trailing ".wallet" or .info text. String walletSuffixSearchText = "." + BitcoinModel.WALLET_FILE_EXTENSION; if (walletPath.endsWith(walletSuffixSearchText)) { walletPath = walletPath.substring(0, walletPath.length() - walletSuffixSearchText.length()); } walletSuffixSearchText = "." + INFO_FILE_SUFFIX_STRING; if (walletPath.endsWith(walletSuffixSearchText)) { walletPath = walletPath.substring(0, walletPath.length() - walletSuffixSearchText.length()); } // Create the top-level directory for the wallet specific data return walletPath + TOP_LEVEL_WALLET_BACKUP_SUFFIX; } public void moveSiblingTimestampedKeyAndWalletBackups(String walletFilename) { // Work out the stem of the wallet i.e. stripped of a .wallet suffix File walletFile = new File(walletFilename); String stemShort= walletFile.getName(); if (walletFilename.matches(REGEX_FOR_WALLET_SUFFIX)) { stemShort = walletFile.getName().substring(0, walletFile.getName().length() - ("." + BitcoinModel.WALLET_FILE_EXTENSION).length()); } String topLevelWalletDirectory = BackupManager.INSTANCE.calculateTopLevelBackupDirectoryName(new File(walletFilename)); // Get the files in the directory. File containingDirectory = walletFile.getParentFile(); File[] siblingFiles = containingDirectory.listFiles(); // See if there are any files matching stem + timestamp + .key, .wallet and .info. Collection<String> privateKeyFilesShort = new ArrayList<String>(); Collection<String> walletFilesShort = new ArrayList<String>(); Collection<String> infoFilesShort = new ArrayList<String>(); if (siblingFiles != null) { for (int i = 0; i < siblingFiles.length; i++) { // Dont process directories. if (!siblingFiles[i].isDirectory()) { String siblingFilenameShort = siblingFiles[i].getName(); if (siblingFilenameShort.matches(REGEX_FOR_TIMESTAMP_AND_KEY_SUFFIX)) { // It has a timestamp and the key suffix. if (siblingFilenameShort.startsWith(stemShort) && siblingFilenameShort.length() == (stemShort.length() + 19)) { // 19 = length of hyphen + timestamp + dot + key privateKeyFilesShort.add(siblingFilenameShort); } } else if (siblingFilenameShort.matches(REGEX_FOR_TIMESTAMP_AND_WALLET_SUFFIX)) { // It has a timestamp and the wallet suffix. if (siblingFilenameShort.startsWith(stemShort) && siblingFilenameShort.length() == (stemShort.length() + 22)) { // 22 = length of hyphen + timestamp + dot + wallet walletFilesShort.add(siblingFilenameShort); } } else if (siblingFilenameShort.matches(REGEX_FOR_TIMESTAMP_AND_INFO_SUFFIX)) { // It has a timestamp and the info suffix. if (siblingFilenameShort.startsWith(stemShort) && siblingFilenameShort.length() == (stemShort.length() + 20)) { // 20 = length of hyphen + timestamp + dot + info infoFilesShort.add(siblingFilenameShort); } } } } // Move the sibling key files to the data/key-backup directory. for (String keyFilename : privateKeyFilesShort) { File sourceFile = new File(containingDirectory + File.separator + keyFilename); File destinationFile = new File(topLevelWalletDirectory + File.separator + PRIVATE_KEY_BACKUP_DIRECTORY_NAME + File.separator + keyFilename); try { sourceFile.renameTo(destinationFile); } catch (SecurityException se) { // Just log the error message. log.error(se.getClass().getName() + " " + se.getMessage()); } catch (NullPointerException npe) { // Just log the error message. log.error(npe.getClass().getName() + " " + npe.getMessage()); } } // Move the sibling wallet files (and their info files) to the data/wallet-backup or data/wallet-unenc-backup directory. for (String loopWalletFilename : walletFilesShort) { File walletSourceFile = new File(containingDirectory + File.separator + loopWalletFilename); File walletDestinationFileUnencrypted = new File(topLevelWalletDirectory + File.separator + UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME + File.separator + loopWalletFilename); File walletDestinationFileEncrypted = new File(topLevelWalletDirectory + File.separator + ENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME + File.separator + loopWalletFilename); // See if there is a matching info file for the wallet. String infoFilenameShort = WalletInfoData.createWalletInfoFilename(loopWalletFilename); boolean alsoRenameInfoFile = infoFilesShort.contains(infoFilenameShort); File infoFileSourceFile = new File(containingDirectory + File.separator + infoFilenameShort); File infoFileDestinationFileUnencrypted = new File(topLevelWalletDirectory + File.separator + UNENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME + File.separator + infoFilenameShort); File infoFileDestinationFileEncrypted = new File(topLevelWalletDirectory + File.separator + ENCRYPTED_WALLET_BACKUP_DIRECTORY_NAME + File.separator + infoFilenameShort); File destinationWalletFile = null; File destinationInfoFile = null; FileInputStream fileInputStream = null; InputStream stream = null; try { // Try to load the wallet to see if it is encrypted or not. fileInputStream = new FileInputStream(walletSourceFile); stream = new BufferedInputStream(fileInputStream); Wallet loadedWallet = Wallet.loadFromFileStream(stream); if (loadedWallet != null) { if (EncryptionType.UNENCRYPTED == loadedWallet.getEncryptionType()) { destinationWalletFile = walletDestinationFileUnencrypted; destinationInfoFile = infoFileDestinationFileUnencrypted; } else { destinationWalletFile = walletDestinationFileEncrypted; destinationInfoFile = infoFileDestinationFileEncrypted; } } } catch (Exception e) { // Just log the error message - we will treat the wallet as // unencrypted. log.error(e.getClass().getName() + " " + e.getMessage()); } finally { if (stream != null) { try { stream.close(); stream = null; } catch (IOException e) { log.error(e.getClass().getName() + " " + e.getMessage()); } } if (fileInputStream != null) { try { fileInputStream.close(); fileInputStream = null; } catch (IOException e) { log.error(e.getClass().getName() + " " + e.getMessage()); } } try { // Rename the wallet. if (destinationWalletFile != null) { walletSourceFile.renameTo(destinationWalletFile); } // Rename the info file. if (alsoRenameInfoFile && destinationInfoFile != null) { infoFileSourceFile.renameTo(destinationInfoFile); } } catch (SecurityException se) { // Just log the error message. log.error(se.getClass().getName() + " " + se.getMessage()); } catch (NullPointerException npe) { // Just log the error message. log.error(npe.getClass().getName() + " " + npe.getMessage()); } } } } } List<File> getWalletsInBackupDirectory(String walletFilename, String directorySuffix) { // See if there are any wallet backups. String topLevelBackupDirectoryName = calculateTopLevelBackupDirectoryName(new File(walletFilename)); String walletBackupDirectoryName = topLevelBackupDirectoryName + File.separator + directorySuffix; File walletBackupDirectory = new File(walletBackupDirectoryName); File[] listOfFiles = walletBackupDirectory.listFiles(); List<File> walletBackups = new ArrayList<File>(); // Look for filenames with format "text"-YYYYMMDDHHMMSS.wallet<eol> and are not empty. if (listOfFiles != null) { for (int i = 0; i < listOfFiles.length; i++) { if (listOfFiles[i].isFile()) { if (listOfFiles[i].getName().matches(REGEX_FOR_TIMESTAMP_AND_WALLET_SUFFIX)) { if (listOfFiles[i].length() > 0) { walletBackups.add(listOfFiles[i]); } } } } } return walletBackups; } private void createDirectoryIfNecessary(String directoryName) { File directory = new File(directoryName); if (!directory.exists()) { boolean createSuccess = directory.mkdir(); log.debug("Result of create of directory + '" + directoryName + "' was " + createSuccess); } } }