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.
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.