JNUSLib/src/main/java/de/mas/wiiu/jnus/implementations/DefaultNUSDataProcessor.java

353 lines
13 KiB
Java

package de.mas.wiiu.jnus.implementations;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.ByteArrayBuffer;
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
import de.mas.wiiu.jnus.utils.HashUtil;
import de.mas.wiiu.jnus.utils.PipedInputStreamWithException;
import de.mas.wiiu.jnus.utils.StreamUtils;
import de.mas.wiiu.jnus.utils.Utils;
import lombok.extern.java.Log;
@Log
public class DefaultNUSDataProcessor implements NUSDataProcessor {
protected final NUSDataProvider dataProvider;
private final Optional<ContentDecryptor> decryptor;
public DefaultNUSDataProcessor(NUSDataProvider dataProvider, Optional<ContentDecryptor> decryptor) {
this.dataProvider = dataProvider;
this.decryptor = decryptor;
}
@Override
public InputStream readContentAsStream(Content c, long offset, long size) throws IOException {
return dataProvider.readRawContentAsStream(c, offset, size);
}
@Override
public InputStream readDecryptedContentAsStream(Content c, long offset, long size) throws IOException {
if (!c.isEncrypted()) {
return dataProvider.readRawContentAsStream(c, offset, size);
}
PipedOutputStream out = new PipedOutputStream();
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000);
new Thread(() -> {
try {
readDecryptedContentToStream(out, c, offset, size);
in.throwException(null);
} catch (Exception e) {
in.throwException(e);
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}).start();
return in;
}
@Override
public long readDecryptedContentToStream(OutputStream out, Content c, long offset, long size) throws IOException {
if (!c.isEncrypted()) {
InputStream in = dataProvider.readRawContentAsStream(c, offset, size);
return StreamUtils.saveInputStreamToOutputStream(in, out, size);
}
if (!decryptor.isPresent()) {
throw new IOException("Decryptor was null. Maybe the ticket is missing?");
}
if (c.isHashed()) {
long stream_offset = (offset / 0x10000) * 0x10000;
InputStream in = dataProvider.readRawContentAsStream(c, stream_offset, size + offset - stream_offset);
return decryptor.get().readDecryptedContentToStreamHashed(in, out, offset, size, offset - stream_offset, dataProvider.getContentH3Hash(c).get());
} else {
byte[] IV = new byte[0x10];
IV[0] = (byte) ((c.getIndex() >> 8) & 0xFF);
IV[1] = (byte) (c.getIndex() & 0xFF);
long streamOffset = (offset / 16) * 16;
long streamFilesize = size;
// if we have an offset we can't calculate the hash anymore
// we need a new IV
if (streamOffset > 15) {
streamFilesize = size;
streamOffset -= 16;
streamFilesize += 16;
// We need to get the current IV as soon as we get the InputStream.
IV = null;
} else if ((offset > 0 && offset < 16) && size < 16) {
streamFilesize = 16;
}
long curStreamOffset = streamOffset;
InputStream in = dataProvider.readRawContentAsStream(c, streamOffset, streamFilesize);
if (IV == null) {
// If we read with an offset > 16 we need the previous 16 bytes because they are the IV.
// The input stream has been prepared to start 16 bytes earlier on this case.
int toRead = 16;
byte[] data = new byte[toRead];
int readTotal = 0;
while (readTotal < toRead) {
int res = in.read(data, readTotal, toRead - readTotal);
if (res < 0) {
StreamUtils.closeAll(in, out);
return -1;
}
readTotal += res;
}
IV = Arrays.copyOfRange(data, 0, toRead);
curStreamOffset = streamOffset + 16;
}
long res = decryptor.get().readDecryptedContentToStreamNonHashed(in, out, curStreamOffset, size, offset - curStreamOffset, IV);
return res;
}
}
@Override
public InputStream readPlainDecryptedContentAsStream(Content c, long offset, long size, boolean forceCheckHash) throws IOException {
PipedOutputStream out = new PipedOutputStream();
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000);
new Thread(() -> {
try {
readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash);
in.throwException(null);
} catch (Exception e) {
in.throwException(e);
try {
in.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}).start();
return in;
}
@Override
public long readPlainDecryptedContentToStream(OutputStream out, Content c, long offset, long size, boolean forceCheckHash) throws IOException {
if (c.isHashed()) {
long payloadOffset = offset;
long streamOffset = payloadOffset;
long streamFilesize = 0;
streamOffset = (payloadOffset / 0xFC00) * 0x10000;
long offsetInBlock = payloadOffset - ((streamOffset / 0x10000) * 0xFC00);
if (offsetInBlock + size < 0xFC00) {
streamFilesize = 0x10000L;
} else {
long curVal = 0x10000;
long missing = (size - (0xFC00 - offsetInBlock));
curVal += (missing / 0xFC00) * 0x10000;
if (missing % 0xFC00 > 0) {
curVal += 0x10000;
}
streamFilesize = curVal;
}
InputStream in = readDecryptedContentAsStream(c, streamOffset, streamFilesize);
try {
return processHashedStream(in, out, (int) (offset / 0xFC00), size, offsetInBlock, dataProvider.getContentH3Hash(c).get());
} catch (NoSuchAlgorithmException | CheckSumWrongException e) {
throw new IOException(e);
}
} else {
InputStream in = readDecryptedContentAsStream(c, offset, size);
byte[] hash = null;
if (forceCheckHash) {
hash = c.getSHA2Hash();
}
try {
return processNonHashedStream(in, out, offset, size, hash, c.getEncryptedFileSize());
} catch (CheckSumWrongException e) {
throw new IOException(e);
}
}
}
private long processNonHashedStream(InputStream inputStream, OutputStream outputStream, long payloadOffset, long filesize, byte[] hash,
long expectedSizeForHash) throws IOException, CheckSumWrongException {
MessageDigest sha1 = null;
MessageDigest sha1fallback = null;
if (hash != null) {
try {
sha1 = MessageDigest.getInstance("SHA1");
sha1fallback = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
int BLOCKSIZE = 0x8000;
byte[] blockBuffer = new byte[BLOCKSIZE];
int inBlockBuffer;
long written = 0;
long writtenFallback = 0;
try {
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
// We can only decrypt multiples of 16. So we need to align it.
long toRead = Utils.align(filesize, 16);
do {
int curReadSize = BLOCKSIZE;
if (toRead < BLOCKSIZE) {
curReadSize = (int) toRead;
}
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, curReadSize);
if (inBlockBuffer <= 0) {
break;
}
byte[] output = blockBuffer;
int toWrite = inBlockBuffer;
if ((written + inBlockBuffer) > filesize) {
toWrite = (int) (filesize - written);
}
written += toWrite;
toRead -= toWrite;
outputStream.write(output, 0, toWrite);
if (sha1 != null && sha1fallback != null) {
sha1.update(output, 0, toWrite);
// In some cases it's using the hash of the whole .app file instead of the part
// that's been actually used.
long toFallback = inBlockBuffer;
if (writtenFallback + toFallback > expectedSizeForHash) {
toFallback = expectedSizeForHash - writtenFallback;
}
sha1fallback.update(output, 0, (int) toFallback);
writtenFallback += toFallback;
}
if (written >= filesize && hash == null) {
break;
}
} while (inBlockBuffer == BLOCKSIZE);
if (sha1 != null && sha1fallback != null) {
long missingInHash = expectedSizeForHash - writtenFallback;
if (missingInHash > 0) {
sha1fallback.update(new byte[(int) missingInHash]);
}
byte[] calculated_hash1 = sha1.digest();
byte[] calculated_hash2 = sha1fallback.digest();
byte[] expected_hash = hash;
if (!Arrays.equals(calculated_hash1, expected_hash) && !Arrays.equals(calculated_hash2, expected_hash)) {
throw new CheckSumWrongException("hash checksum failed ", calculated_hash1, expected_hash);
} else {
log.fine("Hash DOES match saves output stream.");
}
}
} finally {
StreamUtils.closeAll(inputStream, outputStream);
}
return written;
}
private long processHashedStream(InputStream inputStream, OutputStream outputStream, int block, long filesize, long payloadOffset, byte[] h3_hashes)
throws IOException, NoSuchAlgorithmException, CheckSumWrongException {
int BLOCKSIZE = 0x10000;
int HASHBLOCKSIZE = 0xFC00;
int HASHSIZE = BLOCKSIZE - HASHBLOCKSIZE;
long curBlock = block;
byte[] blockBuffer = new byte[BLOCKSIZE];
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
long written = 0;
int inBlockBuffer = 0;
long writeOffset = payloadOffset;
try {
do {
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, BLOCKSIZE);
if (inBlockBuffer < 0) {
break;
}
if (inBlockBuffer != BLOCKSIZE) {
throw new IOException("buffer was not " + BLOCKSIZE + " bytes");
}
byte[] hashes = null;
byte[] output = null;
hashes = Arrays.copyOfRange(blockBuffer, 0, HASHSIZE);
output = Arrays.copyOfRange(blockBuffer, HASHSIZE, BLOCKSIZE);
HashUtil.checkFileChunkHashes(hashes, h3_hashes, output, (int) curBlock);
try {
long writeLength = Math.min((output.length - writeOffset), (filesize - written));
outputStream.write(output, (int) writeOffset, (int) writeLength);
written += writeLength;
} catch (IOException e) {
if (e.getMessage().equals("Pipe closed")) {
break;
}
e.printStackTrace();
throw e;
}
writeOffset = 0;
curBlock++;
} while (written < filesize);
log.finest("Decryption okay");
} finally {
StreamUtils.closeAll(inputStream, outputStream);
}
return written > 0 ? written : -1;
}
@Override
public NUSDataProvider getDataProvider() {
return dataProvider;
}
}