Skip to content

Commit

Permalink
feat: support header-encrypted archive
Browse files Browse the repository at this point in the history
  • Loading branch information
sunny-shu committed Aug 3, 2020
1 parent 84c98d3 commit c2a24f3
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 107 deletions.
95 changes: 49 additions & 46 deletions src/main/java/com/github/junrar/Archive.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
*/
package com.github.junrar;

import com.github.junrar.crypt.Rijndael;
import com.github.junrar.exception.BadRarArchiveException;
import com.github.junrar.exception.CorruptHeaderException;
import com.github.junrar.exception.CrcErrorException;
import com.github.junrar.exception.HeaderNotInArchiveException;
import com.github.junrar.exception.InitDeciphererFailedException;
import com.github.junrar.exception.MainHeaderNullException;
import com.github.junrar.exception.NotRarArchiveException;
import com.github.junrar.exception.RarException;
import com.github.junrar.exception.UnsupportedRarEncryptedException;
import com.github.junrar.exception.UnsupportedRarV5Exception;
import com.github.junrar.io.RawDataIo;
import com.github.junrar.io.SeekableReadOnlyByteChannel;
import com.github.junrar.rarfile.AVHeader;
import com.github.junrar.rarfile.BaseBlock;
Expand All @@ -53,6 +56,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -172,7 +176,7 @@ private void setChannel(final SeekableReadOnlyByteChannel channel, final long le
this.channel = channel;
try {
readHeaders(length);
} catch (UnsupportedRarEncryptedException | UnsupportedRarV5Exception | CorruptHeaderException e) {
} catch (UnsupportedRarEncryptedException | UnsupportedRarV5Exception | CorruptHeaderException | BadRarArchiveException e) {
logger.warn("exception in archive constructor maybe file is encrypted, corrupt or support not yet implemented", e);
throw e;
} catch (final Exception e) {
Expand Down Expand Up @@ -275,14 +279,28 @@ private void readHeaders(final long fileLength) throws IOException, RarException
this.headers.clear();
this.currentHeaderIndex = 0;
int toRead = 0;

//keep track of positions already processed for
//more robustness against corrupt files
final Set<Long> processedPositions = new HashSet<>();
while (true) {
int size = 0;
long newpos = 0;
RawDataIo rawData = new RawDataIo(channel);
final byte[] baseBlockBuffer = safelyAllocate(BaseBlock.BaseBlockSize, MAX_HEADER_SIZE);

// if header is encrypted,there is a 8-byte salt before each header
if (newMhd != null && newMhd.isEncrypted()) {
byte[] salt = new byte[8];
rawData.readFully(salt, 8);
try {
Cipher cipher = Rijndael.buildDecipherer(password, salt);
rawData.setCipher(cipher);
} catch (Exception e) {
throw new InitDeciphererFailedException(e);
}
}

final long position = this.channel.getPosition();

// Weird, but is trying to read beyond the end of the file
Expand All @@ -291,7 +309,8 @@ private void readHeaders(final long fileLength) throws IOException, RarException
}

// logger.info("\n--------reading header--------");
size = this.channel.readFully(baseBlockBuffer, BaseBlock.BaseBlockSize);
size = rawData.readFully(baseBlockBuffer, baseBlockBuffer.length);

if (size == 0) {
break;
}
Expand Down Expand Up @@ -324,50 +343,42 @@ private void readHeaders(final long fileLength) throws IOException, RarException
toRead = block.hasEncryptVersion() ? MainHeader.mainHeaderSizeWithEnc
: MainHeader.mainHeaderSize;
final byte[] mainbuff = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(mainbuff, toRead);
rawData.readFully(mainbuff, mainbuff.length);
final MainHeader mainhead = new MainHeader(block, mainbuff);
this.headers.add(mainhead);
this.newMhd = mainhead;
if (this.newMhd.isEncrypted()) {
throw new UnsupportedRarEncryptedException();
}
// mainhead.print();
break;

case SignHeader:
toRead = SignHeader.signHeaderSize;
final byte[] signBuff = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(signBuff, toRead);
rawData.readFully(signBuff, signBuff.length);
final SignHeader signHead = new SignHeader(block, signBuff);
this.headers.add(signHead);
// logger.info("HeaderType: SignHeader");

break;

case AvHeader:
toRead = AVHeader.avHeaderSize;
final byte[] avBuff = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(avBuff, toRead);
rawData.readFully(avBuff, avBuff.length);
final AVHeader avHead = new AVHeader(block, avBuff);
this.headers.add(avHead);
// logger.info("headertype: AVHeader");
break;

case CommHeader:
toRead = CommentHeader.commentHeaderSize;
final byte[] commBuff = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(commBuff, toRead);
rawData.readFully(commBuff, commBuff.length);
final CommentHeader commHead = new CommentHeader(block, commBuff);
this.headers.add(commHead);
// logger.info("method: "+commHead.getUnpMethod()+"; 0x"+
// Integer.toHexString(commHead.getUnpMethod()));
newpos = commHead.getPositionInFile()
+ commHead.getHeaderSize();

newpos = commHead.getPositionInFile() + commHead.getHeaderSize(isEncrypted());
this.channel.setPosition(newpos);

if (processedPositions.contains(newpos)) {
throw new BadRarArchiveException();
}
processedPositions.add(newpos);
this.channel.setPosition(newpos);

break;
case EndArcHeader:
Expand All @@ -382,73 +393,66 @@ private void readHeaders(final long fileLength) throws IOException, RarException
EndArcHeader endArcHead;
if (toRead > 0) {
final byte[] endArchBuff = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(endArchBuff, toRead);
rawData.readFully(endArchBuff, endArchBuff.length);
endArcHead = new EndArcHeader(block, endArchBuff);
// logger.info("HeaderType: endarch\ndatacrc:"+
// endArcHead.getArchiveDataCRC());
} else {
// logger.info("HeaderType: endarch - no Data");
endArcHead = new EndArcHeader(block, null);
}
this.headers.add(endArcHead);
// logger.info("\n--------end header--------");
return;

default:
final byte[] blockHeaderBuffer = safelyAllocate(BlockHeader.blockHeaderSize, MAX_HEADER_SIZE);
this.channel.readFully(blockHeaderBuffer, BlockHeader.blockHeaderSize);
rawData.readFully(blockHeaderBuffer, blockHeaderBuffer.length);
final BlockHeader blockHead = new BlockHeader(block,
blockHeaderBuffer);

switch (blockHead.getHeaderType()) {
case NewSubHeader:
case FileHeader:
toRead = blockHead.getHeaderSize()
toRead = blockHead.getHeaderSize(false)
- BlockHeader.BaseBlockSize
- BlockHeader.blockHeaderSize;
final byte[] fileHeaderBuffer = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(fileHeaderBuffer, toRead);
rawData.readFully(fileHeaderBuffer, fileHeaderBuffer.length);

final FileHeader fh = new FileHeader(blockHead, fileHeaderBuffer);
this.headers.add(fh);
newpos = fh.getPositionInFile() + fh.getHeaderSize()
+ fh.getFullPackSize();
newpos = fh.getPositionInFile() + fh.getHeaderSize(isEncrypted()) + fh.getFullPackSize();
this.channel.setPosition(newpos);

if (processedPositions.contains(newpos)) {
throw new BadRarArchiveException();
}
processedPositions.add(newpos);
this.channel.setPosition(newpos);
break;

case ProtectHeader:
toRead = blockHead.getHeaderSize()
toRead = blockHead.getHeaderSize(false)
- BlockHeader.BaseBlockSize
- BlockHeader.blockHeaderSize;
final byte[] protectHeaderBuffer = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(protectHeaderBuffer, toRead);
final ProtectHeader ph = new ProtectHeader(blockHead,
protectHeaderBuffer);
newpos = ph.getPositionInFile() + ph.getHeaderSize()
+ ph.getDataSize();
rawData.readFully(protectHeaderBuffer, protectHeaderBuffer.length);
final ProtectHeader ph = new ProtectHeader(blockHead, protectHeaderBuffer);
newpos = ph.getPositionInFile() + ph.getHeaderSize(isEncrypted()) + ph.getDataSize();
this.channel.setPosition(newpos);

if (processedPositions.contains(newpos)) {
throw new BadRarArchiveException();
}
processedPositions.add(newpos);
this.channel.setPosition(newpos);
break;

case SubHeader: {
final byte[] subHeadbuffer = safelyAllocate(SubBlockHeader.SubBlockHeaderSize, MAX_HEADER_SIZE);
this.channel.readFully(subHeadbuffer,
SubBlockHeader.SubBlockHeaderSize);
rawData.readFully(subHeadbuffer, subHeadbuffer.length);
final SubBlockHeader subHead = new SubBlockHeader(blockHead,
subHeadbuffer);
subHead.print();
switch (subHead.getSubType()) {
case MAC_HEAD: {
final byte[] macHeaderbuffer = safelyAllocate(MacInfoHeader.MacInfoHeaderSize, MAX_HEADER_SIZE);
this.channel.readFully(macHeaderbuffer,
MacInfoHeader.MacInfoHeaderSize);
rawData.readFully(macHeaderbuffer, macHeaderbuffer.length);
final MacInfoHeader macHeader = new MacInfoHeader(subHead,
macHeaderbuffer);
macHeader.print();
Expand All @@ -461,7 +465,7 @@ private void readHeaders(final long fileLength) throws IOException, RarException
break;
case EA_HEAD: {
final byte[] eaHeaderBuffer = safelyAllocate(EAHeader.EAHeaderSize, MAX_HEADER_SIZE);
this.channel.readFully(eaHeaderBuffer, EAHeader.EAHeaderSize);
rawData.readFully(eaHeaderBuffer, eaHeaderBuffer.length);
final EAHeader eaHeader = new EAHeader(subHead,
eaHeaderBuffer);
eaHeader.print();
Expand All @@ -474,12 +478,12 @@ private void readHeaders(final long fileLength) throws IOException, RarException
case STREAM_HEAD:
break;
case UO_HEAD:
toRead = subHead.getHeaderSize();
toRead = subHead.getHeaderSize(false);
toRead -= BaseBlock.BaseBlockSize;
toRead -= BlockHeader.blockHeaderSize;
toRead -= SubBlockHeader.SubBlockHeaderSize;
final byte[] uoHeaderBuffer = safelyAllocate(toRead, MAX_HEADER_SIZE);
this.channel.readFully(uoHeaderBuffer, toRead);
rawData.readFully(uoHeaderBuffer, uoHeaderBuffer.length);
final UnixOwnersHeader uoHeader = new UnixOwnersHeader(
subHead, uoHeaderBuffer);
uoHeader.print();
Expand Down Expand Up @@ -541,10 +545,9 @@ public void extractFile(final FileHeader hd, final OutputStream os) throws RarEx
*
* @param hd the header to be extracted
* @return inputstream
* @throws RarException .
* @throws IOException if any IO error occur
* @throws IOException if any IO error occur
*/
public InputStream getInputStream(final FileHeader hd) throws RarException, IOException {
public InputStream getInputStream(final FileHeader hd) throws IOException {
final PipedInputStream in = new PipedInputStream(32 * 1024);
final PipedOutputStream out = new PipedOutputStream(in);

Expand Down
6 changes: 0 additions & 6 deletions src/main/java/com/github/junrar/Junrar.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,6 @@ private static void validateRarPath(final File rar) {
}

private static List<File> extractArchiveTo(final Archive arch, final LocalFolderExtractor destination) throws IOException, RarException {
if (arch.isEncrypted()) {
logger.warn("archive is encrypted cannot extract");
arch.close();
return new ArrayList<>();
}

final List<File> extractedFiles = new ArrayList<>();
try {
for (final FileHeader fh : arch) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/github/junrar/crypt/Rijndael.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
public class Rijndael {
public static Cipher buildDecipherer(final String password, byte[] salt) throws IOException,
NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, NoSuchPaddingException {
if (password == null) {
throw new InvalidAlgorithmParameterException("password should be specified");
}
byte[] AESInit = new byte[16];
byte[] AESKey = new byte[16];
int rawLength = 2 * password.length();
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/com/github/junrar/io/RawDataIo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.github.junrar.io;

import javax.crypto.Cipher;
import java.io.IOException;
import java.util.LinkedList;

public class RawDataIo implements SeekableReadOnlyByteChannel {
private Cipher cipher = null;
private final SeekableReadOnlyByteChannel underlyingByteChannel;
private boolean isEncrypted = false;
private final LinkedList<Byte> dataPool = new LinkedList<>();
private final byte[] reused = new byte[1];

public RawDataIo(SeekableReadOnlyByteChannel channel) {
this.underlyingByteChannel = channel;
}

public Cipher getCipher() {
return cipher;
}

public void setCipher(Cipher cipher) {
this.cipher = cipher;
isEncrypted = true;
}

@Override
public long getPosition() throws IOException {
return underlyingByteChannel.getPosition();
}

@Override
public void setPosition(long pos) throws IOException {
underlyingByteChannel.setPosition(pos);
}

@Override
public int read() throws IOException {
read(reused, 0, 1);
return reused[0];
}

@Override
public int read(byte[] buffer, int off, int count) throws IOException {
byte[] tmp = new byte[count];
int size = readFully(tmp, count);
System.arraycopy(tmp, 0, buffer, off, count);
return size;
}

@Override
public int readFully(byte[] buffer, int count) throws IOException {
if (isEncrypted) {
int remainingSize = dataPool.size();
int toRead = count - remainingSize;
int realRead = toRead + ((~toRead + 1) & 0xF);
byte[] tmp = new byte[realRead];

if (realRead > 0) {
underlyingByteChannel.readFully(tmp, realRead);
byte[] decrypted = cipher.update(tmp);
for (int i = 0; i < decrypted.length; i++) {
dataPool.add(decrypted[i]);
}
}

int realReadSize = 0;
for (int i = 0; i < count && !dataPool.isEmpty(); i++) {
buffer[i] = dataPool.poll();
realReadSize++;
}
return realReadSize;

} else {
return underlyingByteChannel.readFully(buffer, count);
}
}

@Override
public void close() throws IOException {
this.underlyingByteChannel.close();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public SeekableReadOnlyFile(final File file) throws FileNotFoundException {

@Override
public int readFully(final byte[] buffer, final int count) throws IOException {
assert count > 0 : count;
assert count >= 0 : count;
file.readFully(buffer, 0, count);
return count;
}
Expand Down
Loading

0 comments on commit c2a24f3

Please sign in to comment.