MultiBit HDTHE TECHNICAL RECOVERY LIBRARYmail@keychainx.io ↗

MultiBit HD / Technical library

INDEPENDENT KEYCHAINX RESOURCE

EncryptedFileReaderWriter: encryption boundaries

Reads an IV prefix, derives a password key and attempts decryption, with a validity-controlled legacy fallback. Separate backup-key and ordinary password-key methods are present. Several write methods replace or delete intermediate files, so code inspection is safer than experimenting on originals.

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/crypto/EncryptedFileReaderWriter.java

View the exact upstream file · Read raw source · License notices

SHA-256: 84da4d04adfc1ce75bb3cb89f776bbd182c184dcb2d3a797c32eea132cf0e1cb

1package org.multibit.hd.core.crypto;23import com.google.common.base.Charsets;4import com.google.common.base.Preconditions;5import com.google.common.collect.Lists;6import com.google.common.io.ByteStreams;7import com.google.common.io.Files;8import com.google.protobuf.ByteString;9import org.bitcoinj.crypto.KeyCrypterScrypt;10import org.bitcoinj.wallet.Protos;11import org.multibit.commons.crypto.AESUtils;12import org.multibit.hd.core.dto.WalletSummary;13import org.multibit.hd.core.exceptions.EncryptedFileReaderWriterException;14import org.multibit.commons.files.SecureFiles;15import org.multibit.hd.core.files.EncryptedFileListItem;16import org.multibit.hd.core.managers.WalletManager;17import org.slf4j.Logger;18import org.slf4j.LoggerFactory;19import org.spongycastle.crypto.params.KeyParameter;2021import java.io.*;22import java.security.SecureRandom;23import java.util.Arrays;24import java.util.List;2526/**27 * <p>Reader / Writer to provide the following to Services:<br>28 * <ul>29 * <li>load an AES encrypted file</li>30 * <li>write an AES encrypted file</li>31 * </ul>32 * Example:<br>33 * <pre>34 * </pre>35 * </p>36 */37public class EncryptedFileReaderWriter {38  private static final Logger log = LoggerFactory.getLogger(EncryptedFileReaderWriter.class);3940  private static final String TEMPORARY_FILE_EXTENSION = ".tmp";41  private static final String OLD_FILE_EXTENSION = ".old";42  private static final String NEW_FILE_EXTENSION = ".new";4344  /**45   * Decrypt an AES encrypted file and return it as an inputStream46   */47  public static ByteArrayInputStream readAndDecrypt(EncryptedFileListItem encryptedProtobufFile, CharSequence password) throws EncryptedFileReaderWriterException {48    return new ByteArrayInputStream(readAndDecryptToByteArray(encryptedProtobufFile, password));49  }5051  /**52   * Decrypt an AES encrypted file and return it as a byte array53   */54  public static byte[] readAndDecryptToByteArray(EncryptedFileListItem encryptedProtobufFile, CharSequence password) throws EncryptedFileReaderWriterException {55      Preconditions.checkNotNull(encryptedProtobufFile);56      Preconditions.checkNotNull(password);57      try {58          // Read the encrypted file in and decrypt it.59          log.debug("Encrypted file is of size {} bytes", encryptedProtobufFile.length());60          byte[] fileBytes = Files.toByteArray(encryptedProtobufFile);61          byte[] ivBytes = Arrays.copyOfRange(fileBytes, 0, 16);62          byte[] encryptedWalletBytes = Arrays.copyOfRange(fileBytes, 16, fileBytes.length);6364          KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(makeScryptParameters(WalletManager.scryptSalt()));65          KeyParameter keyParameter = keyCrypterScrypt.deriveKey(password);66          byte [] decryptedBytes = AESUtils.decrypt(encryptedWalletBytes,keyParameter,ivBytes);67          InputStream inputStream = new ByteArrayInputStream(decryptedBytes);68          if(!encryptedProtobufFile.isValidDecryption(inputStream)){69              decryptedBytes = AESUtils.decrypt(fileBytes,keyParameter,WalletManager.deprecatedFixedAesInitializationVector());70          }71          // Decrypt the file bytes72          return decryptedBytes;73      } catch (Exception e) {7475          throw new EncryptedFileReaderWriterException("Cannot read and decrypt the file '" + encryptedProtobufFile.getAbsolutePath() + "'", e);76      }77  }7879  /**80   * Encrypt a byte array and output to a file, using an intermediate temporary file81   */82  public static void encryptAndWrite(byte[] unencryptedBytes, CharSequence password, File outputFile) throws EncryptedFileReaderWriterException {83    try {84      byte[] encryptedBytes = encrypt(unencryptedBytes, password);8586      ByteArrayInputStream encryptedWalletByteArrayInputStream = new ByteArrayInputStream(encryptedBytes);87      File temporaryFile = new File(outputFile.getAbsolutePath() + TEMPORARY_FILE_EXTENSION);88      SecureFiles.writeFile(encryptedWalletByteArrayInputStream, temporaryFile, outputFile);89    } catch (Exception e) {90      throw new EncryptedFileReaderWriterException("Cannot encryptAndWrite", e);91    }92  }9394  /**95   * Encrypt a byte array and output directly to a file96   */97  public static void encryptAndWriteDirect(byte[] unencryptedBytes, CharSequence password, File outputFile) throws EncryptedFileReaderWriterException {98    try {99      byte[] encryptedBytes = encrypt(unencryptedBytes, password);100101      ByteArrayInputStream encryptedWalletByteArrayInputStream = new ByteArrayInputStream(encryptedBytes);102      SecureFiles.writeFile(encryptedWalletByteArrayInputStream, outputFile);103    } catch (Exception e) {104      throw new EncryptedFileReaderWriterException("Cannot encryptAndWriteDirect", e);105    }106  }107  /**108   * Encrypt the file specified using the backup AES key derived from the supplied credentials109   *110   * @param fileToEncrypt file to encrypt111   * @param password      credentials to use to do the encryption112   * @return the resultant encrypted file113   * @throws EncryptedFileReaderWriterException114   */115  public static File makeBackupAESEncryptedCopyAndDeleteOriginal(File fileToEncrypt, String password, WalletSummary walletSummary) throws EncryptedFileReaderWriterException {116    Preconditions.checkNotNull(fileToEncrypt);117    Preconditions.checkNotNull(password);118    Preconditions.checkNotNull(walletSummary.getEncryptedBackupKey());119    Preconditions.checkNotNull(walletSummary.getInitializationVector());120    try {121      // Decrypt the backup AES key stored in the wallet summary122      KeyParameter walletPasswordDerivedAESKey = AESUtils.createAESKey(password.getBytes(Charsets.UTF_8), WalletManager.scryptSalt());123      byte[] backupAESKeyBytes = AESUtils.decrypt(walletSummary.getEncryptedBackupKey(), walletPasswordDerivedAESKey,walletSummary.getInitializationVector());124      KeyParameter backupAESKey = new KeyParameter(backupAESKeyBytes);125      File destinationFile = new File(fileToEncrypt.getAbsoluteFile() + WalletManager.MBHD_AES_SUFFIX);126127      return encryptAndDeleteOriginal(fileToEncrypt, destinationFile, backupAESKey);128    } catch (Exception e) {129      throw new EncryptedFileReaderWriterException("Could not decrypt backup AES key", e);130    }131  }132133  /**134   * Encrypt the file specified using an AES key derived from the supplied credentials135   *136   * @param fileToEncrypt file to encrypt137   * @param password      credentials to use to do the encryption138   * @return the resultant encrypted file139   * @throws EncryptedFileReaderWriterException140   */141  public static File makeAESEncryptedCopyAndDeleteOriginal(File fileToEncrypt, CharSequence password) throws EncryptedFileReaderWriterException {142    Preconditions.checkNotNull(fileToEncrypt);143    Preconditions.checkNotNull(password);144145    File destinationFile = new File(fileToEncrypt.getAbsoluteFile() + WalletManager.MBHD_AES_SUFFIX);146    return makeAESEncryptedCopyAndDeleteOriginal(fileToEncrypt, destinationFile, password);147  }148149  /**150   * Encrypt the file specified using an AES key derived from the supplied credentials151   *152   * @param fileToEncrypt   file to encrypt153   * @param destinationFile destination file (if not set then fileToEncrypt + .aes154   * @param password        credentials to use to do the encryption155   * @return the resultant encrypted file156   * @throws EncryptedFileReaderWriterException157   */158  public static File makeAESEncryptedCopyAndDeleteOriginal(File fileToEncrypt, File destinationFile, CharSequence password) throws EncryptedFileReaderWriterException {159    Preconditions.checkNotNull(fileToEncrypt);160    Preconditions.checkNotNull(destinationFile);161    Preconditions.checkNotNull(password);162163    KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(makeScryptParameters(WalletManager.scryptSalt()));164    KeyParameter keyParameter = keyCrypterScrypt.deriveKey(password);165    return encryptAndDeleteOriginal(fileToEncrypt, destinationFile, keyParameter);166  }167168  /**169   * Change the encryption on Collection of files.170   * This method is split into two parts:171   * 1) changeEncryptionPrepare - change the encryption on the files, giving them the suffix ".new"172   * 2) changeEncryptionCommit - rename the files173   *174   * @param files       The List of files to change the encryption on175   * @param oldPassword The original password176   * @param newPassword The new password177   * @return newFiles   A list containing the newly encrypted files178   * @throws EncryptedFileReaderWriterException179   */180  public static List<EncryptedFileListItem> changeEncryptionPrepare(List<EncryptedFileListItem> files, CharSequence oldPassword, CharSequence newPassword) throws IOException {181      Preconditions.checkNotNull(files);182      Preconditions.checkNotNull(oldPassword);183      Preconditions.checkNotNull(newPassword);184185      // The files are expected to end with ".aes"186      for (EncryptedFileListItem fileToCheck : files) {187          Preconditions.checkState(fileToCheck.getAbsolutePath().endsWith(WalletManager.MBHD_AES_SUFFIX));188      }189190      List<EncryptedFileListItem> newFiles = Lists.newArrayList();191      KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(makeScryptParameters(WalletManager.scryptSalt()));192      KeyParameter oldKeyParameter = keyCrypterScrypt.deriveKey(oldPassword);193      KeyParameter newKeyParameter = keyCrypterScrypt.deriveKey(newPassword);194195      for (EncryptedFileListItem file : files) {196          log.debug("Processing file\n'{}'", file.getAbsolutePath());197          EncryptedFileListItem newFile = new EncryptedFileListItem(file.getAbsolutePath() + NEW_FILE_EXTENSION) {198              @Override199              public boolean isValidDecryption(InputStream inputStream) throws IOException {200                  return false;201              }202          };203          newFiles.add(newFile);204          if (file.exists()) {205              // Read in the file bytes that are encrypted with the old password206              byte[] oldFileBytes = Files.toByteArray(file);207              byte[] ivBytes = Arrays.copyOfRange(oldFileBytes, 0, 16);208              byte[] encryptedWalletBytes = Arrays.copyOfRange(oldFileBytes, 16, oldFileBytes.length);209              byte[] plainBytes = AESUtils.decrypt(encryptedWalletBytes, oldKeyParameter, ivBytes);210              InputStream byteArrayInputStream= new ByteArrayInputStream(plainBytes);211              if(!file.isValidDecryption(byteArrayInputStream)){212                  plainBytes = AESUtils.decrypt(oldFileBytes,oldKeyParameter,WalletManager.deprecatedFixedAesInitializationVector());213              }214              byte[] newEncryptedBytes = encrypt(plainBytes, newKeyParameter);215              // Write out the bytes to a file with the suffix ".new"216              SecureFiles.writeFile(new ByteArrayInputStream(newEncryptedBytes), newFile);217          }218      }219      return newFiles;220  }221222223  /**224   * Change the encryption on Collection of files.225   * This method is split into two parts:226   * 1) changeEncryptionPrepare - change the encryption on the files, giving them the suffix ".new"227   * 2) changeEncryptionCommit - rename the files to  ".old", rename the ".new" files to the original, secure delete the ".old"228   *229   * @param originalFiles The List of files to change the encryption on230   * @param newFiles      The list of new files, after their encryption has been changed231   * @throws EncryptedFileReaderWriterException232   */233  public static void changeEncryptionCommit(List<EncryptedFileListItem> originalFiles, List<EncryptedFileListItem> newFiles) throws EncryptedFileReaderWriterException {234    Preconditions.checkNotNull(originalFiles);235    Preconditions.checkNotNull(newFiles);236    Preconditions.checkState(originalFiles.size() == newFiles.size());237238    // Once all files have been written to the ".new" files, rename the files to have the suffix ".old"239    List<File> oldFiles = Lists.newArrayList();240    for (int index = 0; index < originalFiles.size(); index++) {241       try {242        // Rename the file, giving it the suffix ".old"243        File oldFile = new File(originalFiles.get(index).getAbsolutePath() + OLD_FILE_EXTENSION);244        oldFiles.add(oldFile);245         if (originalFiles.get(index).exists()){246           SecureFiles.rename(originalFiles.get(index), oldFile);247           log.debug("Renamed:\n'{}'\n'{}'", originalFiles.get(index).getAbsolutePath(), oldFile.getAbsolutePath());248         }249       } catch (IOException ioe) {250         throw new EncryptedFileReaderWriterException("Could not rename file " + originalFiles.get(index).getAbsolutePath() + " to " + oldFiles.get(index).getAbsolutePath());251       }252     }253254    // Rename all the new files to the original ones passed in255    for (int index = 0; index < originalFiles.size(); index++) {256      try {257        if (newFiles.get(index).exists()) {258          SecureFiles.rename(newFiles.get(index), originalFiles.get(index));259          log.debug("Renamed:\n'{}'\n'{}'", newFiles.get(index).getAbsolutePath(), originalFiles.get(index).getAbsolutePath());260        }261      } catch (IOException ioe) {262        throw new EncryptedFileReaderWriterException("Could not rename file " + newFiles.get(index).getAbsolutePath() + " to " + originalFiles.get(index).getAbsolutePath());263      }264    }265266    // Secure delete the old files267    for (File fileToDelete : oldFiles) {268      try {269        if (fileToDelete.exists()) {270          SecureFiles.secureDelete(fileToDelete);271        }272      } catch (IOException ioe) {273        throw new EncryptedFileReaderWriterException("Could not delete file " + fileToDelete);274      }275    }276  }277278  public static Protos.ScryptParameters makeScryptParameters(byte[] salt) {279    Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder().setSalt(ByteString.copyFrom(salt));280    return scryptParametersBuilder.build();281  }282283284  /**285   * Encrypt a file and delete the original286   * @param fileToEncrypt the file to encrypt287   * @param encryptedFilename the encrypted filename288   * @param keyParameter the KeyParameter used to encrypt the file289   * @return the encrypted file - will be null if no encryption was done290   * @throws EncryptedFileReaderWriterException291   */292  private static synchronized File encryptAndDeleteOriginal(File fileToEncrypt, File encryptedFilename, KeyParameter keyParameter) throws EncryptedFileReaderWriterException {293    Preconditions.checkNotNull(encryptedFilename);294    Preconditions.checkNotNull(keyParameter);295    if (fileToEncrypt == null || !fileToEncrypt.exists()) {296      log.debug("Not encrypting file {} as it does not exist", fileToEncrypt == null ? "null" : fileToEncrypt.getAbsolutePath());297      // Nothing to do298      return null;299    }300301    FileOutputStream encryptedWalletOutputStream = null;302    try {303      // Read in the file304      byte[] unencryptedBytes = Files.toByteArray(fileToEncrypt);305      byte[] encryptedBytes = encrypt(unencryptedBytes, keyParameter);306307      // Save encrypted bytes308      ByteArrayInputStream encryptedWalletByteArrayInputStream = new ByteArrayInputStream(encryptedBytes);309      encryptedWalletOutputStream = new FileOutputStream(encryptedFilename);310      ByteStreams.copy(encryptedWalletByteArrayInputStream, encryptedWalletOutputStream);311      encryptedWalletOutputStream.flush();312313      if (encryptedFilename.length() == encryptedBytes.length) {314        SecureFiles.secureDelete(fileToEncrypt);315      } else {316        // The saved file isn't the correct size - do not delete the original317        throw new EncryptedFileReaderWriterException("The saved file " + encryptedFilename + " is not the size of the encrypted bytes - not deleting the original file");318      }319320      return encryptedFilename;321322    } catch (Exception e) {323      throw new EncryptedFileReaderWriterException("Cannot make encrypted copy for file '" + fileToEncrypt.getAbsolutePath() + "'", e);324    } finally {325      if (encryptedWalletOutputStream != null) {326        try {327          encryptedWalletOutputStream.close();328          encryptedWalletOutputStream = null;329        } catch (IOException e) {330          log.error("Cannot close wallet output stream", e);331        }332      }333    }334  }335336  /**337   * Encrypt a byte array, returning the encrypted byte array.338   * this method checks the encryption is reversible339   *340   * @param unencryptedBytes the unencrypted bytes you want to encrypt341   * @param keyParameter     the KeyParameter to use342   * @return encryptedBytes the encryptedBytes343   */344  private static byte[] encrypt(byte[] unencryptedBytes, KeyParameter keyParameter) {345    try {346      // Create an AES encoded version of the unencryptedBytes, using the credentials347      byte[] randomIvBytes = WalletManager.generateRandomIv();348      byte[] encryptedBytes = AESUtils.encrypt(unencryptedBytes,keyParameter,randomIvBytes);349      byte[] rebornBytes = AESUtils.decrypt(encryptedBytes,keyParameter,randomIvBytes);350      byte[] fileBytes = appendByteArrays(randomIvBytes,encryptedBytes);351      if (Arrays.equals(unencryptedBytes, rebornBytes)) {352        return fileBytes;353      } else {354        throw new EncryptedFileReaderWriterException("The encryption was not reversible so aborting.");355      }356    } catch (Exception e) {357      throw new EncryptedFileReaderWriterException("Cannot encryptAndWrite", e);358    }359  }360361  /**362   * Encrypt a byte array, returning the encrypted byte array.363   * this method checks the encryption is reversible364   *365   * @param unencryptedBytes the unencrypted bytes you want to encrypt366   * @param password         the password to use367   * @return encryptedBytes the encryptedBytes368   */369  private static byte[] encrypt(byte[] unencryptedBytes, CharSequence password) {370    try {371      KeyCrypterScrypt keyCrypterScrypt = new KeyCrypterScrypt(makeScryptParameters(WalletManager.scryptSalt()));372      KeyParameter keyParameter = keyCrypterScrypt.deriveKey(password);373374      return encrypt(unencryptedBytes, keyParameter);375    } catch (Exception e) {376      throw new EncryptedFileReaderWriterException("Cannot encryptAndWrite", e);377    }378  }379380  private static byte[] appendByteArrays(byte [] firstByteArray,byte [] secondByteArray){381    byte [] resultByteArray = new byte[firstByteArray.length+secondByteArray.length];382    System.arraycopy(firstByteArray, 0, resultByteArray, 0, firstByteArray.length);383    // copy encrypted bytes into end of destination (from pos iv.length,encryptedBytes.length)384    System.arraycopy(secondByteArray, 0, resultByteArray, firstByteArray.length, secondByteArray.length);385    return resultByteArray;386  }387}

Read in context

Reads an IV prefix, derives a password key and attempts decryption, with a validity-controlled legacy fallback. Separate backup-key and ordinary password-key methods are present. Several write methods replace or delete intermediate files, so code inspection is safer than experimenting on originals.

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