Java tutorial
/* * The MIT License * * Copyright 2015 Ahseya. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.github.horrorho.liquiddonkey.cloud.file; import com.github.horrorho.liquiddonkey.exception.BadDataException; import com.google.protobuf.ByteString; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.READ; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; import java.util.Arrays; import net.jcip.annotations.NotThreadSafe; import org.bouncycastle.crypto.BufferedBlockCipher; import org.bouncycastle.crypto.DataLengthException; import org.bouncycastle.crypto.digests.GeneralDigest; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.modes.CBCBlockCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Decrypts encrypted files. * * @author Ahseya */ @NotThreadSafe public final class FileDecrypter { private static final Logger logger = LoggerFactory.getLogger(FileDecrypter.class); /** * Returns a new instance. * * @return a new instance, not null */ public static FileDecrypter create() { return FileDecrypter.from(new BufferedBlockCipher(new CBCBlockCipher(new AESEngine())), new SHA1Digest()); } static FileDecrypter from(BufferedBlockCipher cbcAes, SHA1Digest sha1) { return new FileDecrypter(cbcAes, sha1); } private final BufferedBlockCipher cbcAes; private final GeneralDigest digest; private final byte[] in = new byte[0x1000]; private final byte[] out = new byte[0x1000]; FileDecrypter(BufferedBlockCipher cbcAes, GeneralDigest digest) { this.cbcAes = cbcAes; this.digest = digest; } /** * Decrypts a file. * <p> * Decrypts the specified file. The file is temporarily renamed with a .encrypted suffix. The un-encrypted data is * written to a fresh file of the same name. The temporary file is deleted. File timestamps are altered. * <p> * In the presence of exceptions the temporary file may remain undeleted, the original or the un-encrypted file may * not exist. * <p> * iOS 5 files remain untested. * * @param path the Path to the file * @param key the file key * @param decryptedSize the expected decrypted size, a value of 0 indicates iOS 5 format * @throws BadDataException if a cipher exception occurred * @throws IOException */ public void decrypt(Path path, ByteString key, long decryptedSize) throws BadDataException, IOException { Path encrypted = null; try { if (Files.size(path) == 0) { logger.warn("--decrypt() > cannot decrypt an empty file: {}", path); return; } ParametersWithIV ivKey = deriveIvKey(key); KeyParameter fileKey = deriveFileKey(key); long blockCount = Files.size(path) + (decryptedSize > 0 ? 0x0FFF : 0) >> 12; encrypted = path.getParent().resolve(path.getFileName() + ".encrypted"); Files.move(path, encrypted, StandardCopyOption.REPLACE_EXISTING); try (InputStream input = Files.newInputStream(encrypted, READ); OutputStream output = Files.newOutputStream(path, CREATE, WRITE, TRUNCATE_EXISTING)) { byte[] checksum = decrypt(input, output, blockCount, ivKey, fileKey); if (decryptedSize == 0) { // iOS 5 decryptedSize = trailer(input, checksum); if (decryptedSize == -1) { logger.warn("-- decrypt() > bad trailer/ checksum"); } } } long size = Files.size(path); if (decryptedSize > 0 && size > decryptedSize) { logger.debug("-- decrypt() > truncating to: {} from: {}", decryptedSize, size); Files.newByteChannel(path, WRITE).truncate(decryptedSize).close(); } else if (Files.size(path) < decryptedSize) { logger.warn("-- decrypt() > short output size: {} expected: {}", Files.size(path), decryptedSize); } } catch (BufferUnderflowException | DataLengthException ex) { throw new BadDataException("Cipher exception", ex); } finally { if (encrypted != null) { try { Files.deleteIfExists(encrypted); } catch (IOException ex) { logger.warn("-- decrypt() > unable to deleted temporary encrypted file: ", ex); } } } } byte[] decrypt(InputStream input, OutputStream output, long blockCount, ParametersWithIV ivKey, KeyParameter fileKey) throws IOException { byte[] hash = new byte[digest.getDigestSize()]; digest.reset(); for (int block = 0; block < blockCount; block++) { int length = input.read(in); if (length == -1) { logger.warn("-- decrypt() > empty block"); break; } digest.update(in, 0, length); decryptBlock(fileKey, deriveIv(ivKey, block), in, length, out); output.write(out, 0, length); } digest.doFinal(hash, 0); return hash; } void decryptBlock(KeyParameter fileKey, byte[] iv, byte[] in, int length, byte[] out) { cbcAes.init(false, new ParametersWithIV(fileKey, iv)); cbcAes.processBytes(in, 0, length, out, 0); } long trailer(InputStream input, byte[] checksum) throws IOException { byte[] trailer = new byte[0x1C]; int length = input.read(trailer); if (length == -1) { logger.warn("-- trailer() > missing trailer"); return -1; } ByteBuffer buffer = ByteBuffer.wrap(trailer); long decryptedSize = buffer.getLong(); ByteBuffer expectedChecksum = buffer.slice(); if (!ByteBuffer.wrap(checksum).equals(expectedChecksum)) { logger.warn("-- trailer() - bad checksum"); return -1; } return decryptedSize; } byte[] deriveIv(ParametersWithIV ivKey, int block) { byte[] blockHash = blockHash(block); byte[] iv = new byte[0x10]; cbcAes.init(true, ivKey); cbcAes.processBytes(blockHash, 0, blockHash.length, iv, 0); return iv; } ParametersWithIV deriveIvKey(ByteString key) { byte[] hash = new byte[digest.getDigestSize()]; digest.reset(); digest.update(key.toByteArray(), 0, key.size()); digest.doFinal(hash, 0); return new ParametersWithIV(new KeyParameter(Arrays.copyOfRange(hash, 0, 16)), new byte[16]); } KeyParameter deriveFileKey(ByteString key) { return new KeyParameter(key.toByteArray()); } byte[] blockHash(int block) { int offset = block << 12; byte[] hash = new byte[0x10]; ByteBuffer buffer = ByteBuffer.wrap(hash); buffer.order(ByteOrder.LITTLE_ENDIAN); for (int i = 0; i < 4; i++) { offset = ((offset & 1) == 1) ? 0x80000061 ^ (offset >>> 1) : offset >>> 1; buffer.putInt(offset); } return hash; } }