Refactor the decryption

This commit is contained in:
Maschell 2020-07-10 17:29:07 +02:00
parent 2604fe5eb9
commit 95039a42ef
46 changed files with 1170 additions and 433 deletions

View File

@ -1,7 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="test"/>
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
@ -12,5 +23,11 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -11,7 +11,7 @@
</properties>
<build>
<sourceDirectory>src</sourceDirectory>
<sourceDirectory>src/main/java</sourceDirectory>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>

View File

@ -19,6 +19,7 @@ package de.mas.wiiu.jnus;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -52,14 +53,14 @@ public final class ExtractionService {
}
private ExtractionService(NUSTitle nustitle) {
if (nustitle.getDataProvider() instanceof Parallelizable) {
if (nustitle.getDataProcessor().getDataProvider() instanceof Parallelizable) {
parallelizable = true;
}
this.NUSTitle = nustitle;
}
private NUSDataProvider getDataProvider() {
return getNUSTitle().getDataProvider();
return getNUSTitle().getDataProcessor().getDataProvider();
}
public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException {
@ -95,7 +96,7 @@ public final class ExtractionService {
}
}
public void extractEncryptedContentFilesTo(List<Content> list, String outputFolder, boolean withHashes) throws IOException {
public void extractEncryptedContentFilesTo(Collection<Content> list, String outputFolder, boolean withHashes) throws IOException {
Utils.createDir(outputFolder);
if (parallelizable && Settings.ALLOW_PARALLELISATION) {
try {

View File

@ -17,7 +17,6 @@
package de.mas.wiiu.jnus;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -27,10 +26,9 @@ import de.mas.wiiu.jnus.entities.TMD;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
import de.mas.wiiu.jnus.utils.FSTUtils;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
public class NUSTitle {
@ -39,12 +37,18 @@ public class NUSTitle {
@Getter private final TMD TMD;
@Getter private final NUSDataProvider dataProvider;
@Getter private final NUSDataProcessor dataProcessor;
public NUSTitle(@NonNull NUSDataProvider dataProvider) throws ParseException, IOException {
byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0));
this.TMD = de.mas.wiiu.jnus.entities.TMD.parseTMD(tmdData);
this.dataProvider = dataProvider;
private NUSTitle(TMD tmd, NUSDataProcessor dataProcessor) {
this.TMD = tmd;
this.dataProcessor = dataProcessor;
}
public static NUSTitle create(TMD tmd, NUSDataProcessor dataProcessor, Optional<Ticket> ticket, Optional<FST> fst) {
NUSTitle result = new NUSTitle(tmd, dataProcessor);
result.setTicket(ticket);
result.setFST(fst);
return result;
}
public Stream<FSTEntry> getAllFSTEntriesAsStream() {
@ -66,13 +70,13 @@ public class NUSTitle {
}
public void cleanup() throws IOException {
if (getDataProvider() != null) {
getDataProvider().cleanup();
if (getDataProcessor() != null && getDataProcessor().getDataProvider() != null) {
getDataProcessor().getDataProvider().cleanup();
}
}
@Override
public String toString() {
return "NUSTitle [dataProvider=" + dataProvider + "]";
return "NUSTitle [dataProcessor=" + dataProcessor + "]";
}
}

View File

@ -1,5 +1,5 @@
/****************************************************************************
* Copyright (C) 2016-2019 Maschell
* Copyright (C) 2016-2020 Maschell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,54 +16,78 @@
****************************************************************************/
package de.mas.wiiu.jnus;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchProviderException;
import java.text.ParseException;
import java.util.Optional;
import java.util.function.Supplier;
import de.mas.wiiu.jnus.entities.TMD;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
import de.mas.wiiu.jnus.interfaces.ContentEncryptor;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.StreamUtils;
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
import de.mas.wiiu.jnus.interfaces.TriFunction;
import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption;
import de.mas.wiiu.jnus.utils.cryptography.NUSEncryption;
public class NUSTitleLoader {
private NUSTitleLoader() {
// should be empty
}
public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier<NUSDataProvider> dataProviderFunction) throws IOException, ParseException {
public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier<NUSDataProvider> dataProviderFunction,
TriFunction<NUSDataProvider, Optional<ContentDecryptor>, Optional<ContentEncryptor>, NUSDataProcessor> dataProcessorFunction)
throws IOException, ParseException {
NUSDataProvider dataProvider = dataProviderFunction.get();
NUSTitle result = new NUSTitle(dataProvider);
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD().orElseThrow(() -> new FileNotFoundException("No TMD data found")));
if (config.isNoDecryption()) {
NUSTitle result = NUSTitle.create(tmd, dataProcessorFunction.apply(dataProvider, Optional.empty(), Optional.empty()), Optional.empty(),
Optional.empty());
return result;
}
Ticket ticket = null;
Optional<Ticket> ticket = Optional.empty();
Optional<ContentDecryptor> decryption = Optional.empty();
Optional<ContentEncryptor> encryption = Optional.empty();
if (config.isTicketNeeded()) {
ticket = config.getTicket();
if (ticket == null) {
Ticket ticketT = config.getTicket();
if (ticketT == null) {
Optional<byte[]> ticketOpt = dataProvider.getRawTicket();
if (ticketOpt.isPresent()) {
ticket = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey());
ticketT = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey());
}
}
if (ticket == null) {
new ParseException("Failed to get ticket data", 0);
if (ticketT == null) {
throw new ParseException("Failed to get ticket data", 0);
}
ticket = Optional.of(ticketT);
decryption = Optional.of(new NUSDecryption(ticketT));
try {
encryption = Optional.of(new NUSEncryption(ticketT));
} catch (NoSuchProviderException e) {
throw new IOException(e);
}
result.setTicket(Optional.of(ticket));
}
NUSDataProcessor dpp = dataProcessorFunction.apply(dataProvider, decryption, encryption);
// If we have just content, we don't have a FST.
if (result.getTMD().getAllContents().size() == 1) {
if (tmd.getAllContents().size() == 1) {
// The only way to check if the key is right, is by trying to decrypt the whole thing.
NUSTitle result = NUSTitle.create(tmd, dpp, ticket, Optional.empty());
FSTDataProvider dp = new FSTDataProviderNUSTitle(result);
for (FSTEntry children : dp.getRoot().getChildren()) {
dp.readFile(children);
@ -72,27 +96,16 @@ public class NUSTitleLoader {
return result;
}
// If we have more than one content, the index 0 is the FST.
Content fstContent = result.getTMD().getContentByIndex(0);
InputStream fstContentEncryptedStream = dataProvider.readContentAsStream(fstContent);
byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());
if (fstContent.isEncrypted()) {
AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]);
if (fstBytes.length % 0x10 != 0) {
throw new IOException("FST length is not align to 16");
}
fstBytes = aesDecryption.decrypt(fstBytes);
}
Content fstContent = tmd.getContentByIndex(0);
byte[] fstBytes = dpp.readPlainDecryptedContent(fstContent, true);
FST fst = FST.parseFST(fstBytes);
result.setFST(Optional.of(fst));
// The dataprovider may need the FST to calculate the offset of a content
// on the partition.
dataProvider.setFST(fst);
return result;
return NUSTitle.create(tmd, dpp, ticket, Optional.of(fst));
}
}

View File

@ -20,6 +20,7 @@ import java.io.IOException;
import java.text.ParseException;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderFST;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
@ -36,7 +37,7 @@ public final class NUSTitleLoaderFST {
NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey);
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderFST(dataProvider, base));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderFST(dataProvider, base), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
}
}

View File

@ -20,6 +20,7 @@ import java.io.IOException;
import java.text.ParseException;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal;
public final class NUSTitleLoaderLocal {
@ -43,7 +44,7 @@ public final class NUSTitleLoaderLocal {
throw new IOException("Ticket was null and no commonKey was given");
}
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
}
}

View File

@ -18,6 +18,7 @@
package de.mas.wiiu.jnus;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup;
public final class NUSTitleLoaderLocalBackup {
@ -35,7 +36,8 @@ public final class NUSTitleLoaderLocalBackup {
config.setNoDecryption(true);
}
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion),
(dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
}
}

View File

@ -20,6 +20,7 @@ import java.io.IOException;
import java.text.ParseException;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
public final class NUSTitleLoaderRemote {
@ -53,7 +54,7 @@ public final class NUSTitleLoaderRemote {
throw new IOException("Ticket was null and no commonKey was given");
}
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
}
}

View File

@ -24,6 +24,7 @@ import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy;
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
import de.mas.wiiu.jnus.implementations.woomy.WoomyParser;
@ -38,10 +39,10 @@ public final class NUSTitleLoaderWoomy {
NUSTitleConfig config = new NUSTitleConfig();
config.setTicketNeeded(false);
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWoomy(woomyInfo));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWoomy(woomyInfo), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
}
}

View File

@ -24,6 +24,7 @@ import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
@ -95,7 +96,7 @@ public final class WUDLoader {
final NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey);
gamePartition.getTmd();
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
}
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WUDInfo wudInfo, byte[] commonKey) throws IOException, ParseException {

View File

@ -11,6 +11,7 @@ import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.implementations.FSTDataProviderWumadDataPartition;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWumad;
@ -40,7 +41,7 @@ public class WumadLoader {
final NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey);
gamePartition.getTmd();
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile));
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile), (dp, cd, ce) -> new DefaultNUSDataProcessor(dp, cd));
}
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WumadInfo wumadInfo, byte[] commonKey) throws IOException, ParseException {
@ -57,7 +58,6 @@ public class WumadLoader {
}
return result;
}
}

View File

@ -37,6 +37,7 @@ public class Content implements Comparable<Content> {
public static final short CONTENT_FLAG_UNKWN1 = 0x4000;
public static final short CONTENT_HASHED = 0x0002;
public static final short CONTENT_ENCRYPTED = 0x0001;
public static final int CONTENT_SIZE = 0x30;
@Getter private final int ID;
@ -127,7 +128,7 @@ public class Content implements Comparable<Content> {
*/
public long getDecryptedFileSize() {
if (isHashed()) {
return getEncryptedFileSize() / 0x10000 * 0xFC00;
return (getEncryptedFileSize() / 0x10000) * 0xFC00;
} else {
return getEncryptedFileSize();
}

View File

@ -61,7 +61,7 @@ public final class FST {
int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
FST result = new FST(sectorSize, contentCount);
int contentfst_offset = 0x20;
int contentfst_size = 0x20 * contentCount;
@ -71,8 +71,8 @@ public final class FST {
int fst_size = fileCount * 0x10;
int nameOff = fst_offset + fst_size;
int nameSize = nameOff + 1;
int nameSize = fstData.length - nameOff;
// Get list with null-terminated Strings. Ends with \0\0.
for (int i = nameOff; i < fstData.length - 1; i++) {
if (fstData[i] == 0 && fstData[i + 1] == 0) {
@ -88,7 +88,7 @@ public final class FST {
byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size);
byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize);
FSTEntry root = result.getRoot();
FSTService.parseFST(root, fstSection, nameSection, sectorSize);

View File

@ -104,7 +104,6 @@ public final class FSTService {
while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) {
j++;
}
return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + j)));
}

View File

@ -0,0 +1,352 @@
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;
}
}

View File

@ -17,10 +17,7 @@
package de.mas.wiiu.jnus.implementations;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.stream.Collectors;
import de.mas.wiiu.jnus.NUSTitle;
@ -28,22 +25,20 @@ import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.HasNUSTitle;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
import de.mas.wiiu.jnus.utils.StreamUtils;
import de.mas.wiiu.jnus.utils.Utils;
import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption;
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
private final NUSDataProcessor dataProcessor;
private final NUSTitle title;
private final FSTEntry rootEntry;
@Getter @Setter private String name;
public FSTDataProviderNUSTitle(NUSTitle title) throws IOException {
this.dataProcessor = title.getDataProcessor();
this.title = title;
this.name = String.format("%016X", title.getTMD().getTitleID());
@ -59,157 +54,25 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
throw new IOException("No FST root entry was found");
}
}
@Override
public FSTEntry getRoot() {
return rootEntry;
}
@Override
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
try {
return decryptFSTEntryToStream(entry, out, offset, size);
} catch (CheckSumWrongException | NoSuchAlgorithmException e) {
throw new IOException(e);
}
}
private boolean decryptFSTEntryToStreamHashed(FSTEntry entry, OutputStream outputStream, long offset, long size)
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
long payloadOffset = entry.getFileOffset() + 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;
}
NUSDataProvider dataProvider = title.getDataProvider();
InputStream in = dataProvider.readContentAsStream(c, streamOffset, streamFilesize);
NUSDecryption nusdecryption = new NUSDecryption(title.getTicket().get());
return nusdecryption.decryptStreamsHashed(in, outputStream, payloadOffset, size, dataProvider.getContentH3Hash(c));
}
private boolean decryptFSTEntryToStreamNonHashed(FSTEntry entry, OutputStream outputStream, long offset, long size)
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
byte[] IV = new byte[0x10];
IV[0] = (byte) ((c.getIndex() >> 8) & 0xFF);
IV[1] = (byte) (c.getIndex() & 0xFF);
long payloadOffset = entry.getFileOffset() + offset;
long streamOffset = payloadOffset;
long streamFilesize = c.getEncryptedFileSize();
// if we have an offset we can't calculate the hash anymore
// we need a new IV
if (streamOffset > 0) {
streamFilesize = size;
streamOffset -= 16;
streamFilesize += 16;
// We need to get the current IV as soon as we get the InputStream.
IV = null;
}
NUSDataProvider dataProvider = title.getDataProvider();
InputStream in = dataProvider.readContentAsStream(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) {
// This should NEVER happen.
throw new IOException();
}
readTotal += res;
}
IV = Arrays.copyOfRange(data, 0, toRead);
}
NUSDecryption nusdecryption = new NUSDecryption(title.getTicket().get());
return nusdecryption.decryptStreamsNonHashed(in, outputStream, payloadOffset, size, c, IV, size != entry.getFileSize());
}
private boolean decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream, long offset, long size)
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
if (entry.isNotInPackage()) {
if (entry.isNotInPackage()) {
log.info("Decryption not possible because the FSTEntry is not in this package");
}
outputStream.close();
return false;
}
if (offset % 16 != 0) {
throw new IOException("The offset for decryption need to be aligned to 16");
out.close();
return -1;
}
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
try {
if (c.isEncrypted()) {
if (!title.getTicket().isPresent()) {
log.info("Decryption not possible because no ticket was set.");
outputStream.close();
return false;
}
if (c.isHashed()) {
return decryptFSTEntryToStreamHashed(entry, outputStream, offset, size);
} else {
return decryptFSTEntryToStreamNonHashed(entry, outputStream, offset, size);
}
} else {
InputStream in = title.getDataProvider().readContentAsStream(c, offset, size);
try {
StreamUtils.saveInputStreamToOutputStreamWithHash(in, outputStream, size, c.getSHA2Hash(), c.getEncryptedFileSize(),
size != entry.getFileSize());
return true;
} finally {
StreamUtils.closeAll(in, outputStream);
}
}
} catch (CheckSumWrongException e) {
if (c.isUNKNWNFlag1Set()) {
log.info("Hash doesn't match. But file is optional. Don't worry.");
} else {
StringBuilder sb = new StringBuilder();
sb.append("Hash doesn't match").append(System.lineSeparator());
sb.append("Detailed info:").append(System.lineSeparator());
sb.append(entry).append(System.lineSeparator());
sb.append(String.format("%016x", title.getTMD().getTitleID()));
sb.append(e.getMessage() + " Calculated Hash: " + Utils.ByteArrayToString(e.getGivenHash()) + ", expected hash: "
+ Utils.ByteArrayToString(e.getExpectedHash()));
log.info(sb.toString());
throw e;
}
}
return false;
return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getFileOffset(), size, size == entry.getFileSize());
}
@Override

View File

@ -49,7 +49,7 @@ public class FSTDataProviderWUDDataPartition implements FSTDataProvider {
}
@Override
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
.orElseThrow(() -> new IOException("Failed to find FSTInfo"));
if (titleKey == null) {

View File

@ -32,15 +32,15 @@ public class FSTDataProviderWumadDataPartition implements FSTDataProvider {
}
@Override
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
return true;
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
return StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
}
@Override
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
ZipEntry zipEntry = zipFile.stream()
.filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex()))).findFirst()
.filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex())))
.findFirst()
.orElseThrow(() -> new FileNotFoundException());
InputStream in = zipFile.getInputStream(zipEntry);

View File

@ -26,6 +26,7 @@ import java.util.Optional;
import de.mas.wiiu.jnus.Settings;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.FSTUtils;
@ -39,12 +40,12 @@ public class NUSDataProviderFST implements NUSDataProvider {
this.fstDataProvider = fstDataProvider;
}
public NUSDataProviderFST(FSTDataProvider fstDataProvider) {
public NUSDataProviderFST(FSTDataProvider fstDataProvider, ContentDecryptor decryptor) {
this(fstDataProvider, fstDataProvider.getRoot());
}
@Override
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
String filename = content.getFilename();
Optional<FSTEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));

View File

@ -45,7 +45,7 @@ public final class NUSDataProviderLocal implements NUSDataProvider {
}
@Override
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
File filepath = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), content.getFilename());
if (filepath == null || !filepath.exists()) {
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + content.getFilename() + "\", file does not exist";

View File

@ -40,7 +40,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
public NUSDataProviderLocalBackup(String localPath, short version) {
this.localPath = localPath;
this.titleVersion = version;
this.titleVersion = version;
}
private String getFilePathOnDisk(Content c) {
@ -48,7 +48,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
}
@Override
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
File filepath = new File(getFilePathOnDisk(content));
if (!filepath.exists()) {
throw new FileNotFoundException(filepath.getAbsolutePath() + " was not found.");
@ -62,7 +62,9 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
File h3File = new File(h3Path);
if (!h3File.exists()) {
throw new FileNotFoundException(h3File.getAbsolutePath() + " was not found.");
}
return Optional.of(Files.readAllBytes(h3File.toPath()));
}

View File

@ -39,7 +39,7 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
}
@Override
public InputStream readContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size);
}
@ -56,7 +56,6 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
if (resOpt == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
String url = getRemoteURL(content) + Settings.H3_EXTENTION;
System.out.println(url);
byte[] res = downloadService.downloadToByteArray(url);
if (res == null || res.length == 0) {
@ -69,29 +68,39 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
return resOpt;
}
Optional<byte[]> tmdCache = null;
@Override
public Optional<byte[]> getRawTMD() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
if (tmdCache == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
long titleID = getTitleID();
int version = getVersion();
long titleID = getTitleID();
int version = getVersion();
byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
if (res == null || res.length == 0) {
return Optional.empty();
if (res == null || res.length == 0) {
return Optional.empty();
}
tmdCache = Optional.of(res);
}
return Optional.of(res);
return tmdCache;
}
Optional<byte[]> ticketCache = null;
@Override
public Optional<byte[]> getRawTicket() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
byte[] res = downloadService.downloadTicketToByteArray(titleID);
if (res == null || res.length == 0) {
return Optional.empty();
if (ticketCache == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
byte[] res = downloadService.downloadTicketToByteArray(titleID);
if (res == null || res.length == 0) {
return Optional.empty();
}
ticketCache = Optional.of(res);
}
return Optional.of(res);
return ticketCache;
}
@Override

View File

@ -56,7 +56,7 @@ public class NUSDataProviderWUD implements NUSDataProvider {
}
@Override
public InputStream readContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
WUDDiscReader discReader = getDiscReader();
long offset = getOffsetInWUD(content) + fileOffsetBlock;

View File

@ -16,7 +16,6 @@
****************************************************************************/
package de.mas.wiiu.jnus.implementations;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@ -46,7 +45,7 @@ public class NUSDataProviderWoomy implements NUSDataProvider {
}
@Override
public InputStream readContentAsStream(@NonNull Content content, long offset, long size) throws IOException {
public InputStream readRawContentAsStream(@NonNull Content content, long offset, long size) throws IOException {
WoomyZipFile zipFile = getSharedWoomyZipFile();
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
if (entry == null) {

View File

@ -61,7 +61,7 @@ public class NUSDataProviderWumad implements NUSDataProvider {
}
@Override
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
ZipEntry entry = files.values().stream().filter(e -> e.getName().startsWith("p" + partition.getPartitionName() + "."))
.filter(e -> e.getName().endsWith(content.getFilename().toLowerCase())).findFirst().orElseThrow(() -> new FileNotFoundException());
InputStream in = wumad.getInputStream(entry);

View File

@ -54,7 +54,7 @@ public abstract class WUDDiscReader {
return out.toByteArray();
}
public abstract boolean readEncryptedToStream(OutputStream out, long offset, long size) throws IOException;
public abstract long readEncryptedToStream(OutputStream out, long offset, long size) throws IOException;
public InputStream readEncryptedToStream(long offset, long size) throws IOException {
PipedOutputStream out = new PipedOutputStream();
@ -72,6 +72,23 @@ public abstract class WUDDiscReader {
return in;
}
public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV,
boolean useFixedIV) throws IOException {
PipedOutputStream out = new PipedOutputStream();
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x8000);
new Thread(() -> {
try {
readDecryptedToOutputStream(out, offset, fileOffset, size, key, IV, useFixedIV);
in.throwException(null);
} catch (Exception e) {
in.throwException(e);
}
}).start();
return in;
}
/**
*
* @param readOffset
@ -93,7 +110,7 @@ public abstract class WUDDiscReader {
return decryptedChunk;
}
public boolean readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV,
public long readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV,
boolean useFixedIV) throws IOException {
byte[] usedIV = null;
if (useFixedIV) {
@ -153,7 +170,7 @@ public abstract class WUDDiscReader {
StreamUtils.closeAll(outputStream);
}
return totalread >= size;
return totalread;
}
/**

View File

@ -35,7 +35,7 @@ public class WUDDiscReaderCompressed extends WUDDiscReader {
* Expects the .wux format by Exzap. You can more infos about it here. https://gbatemp.net/threads/wii-u-image-wud-compression-tool.397901/
*/
@Override
public boolean readEncryptedToStream(OutputStream out, long offset, long size) throws IOException {
public long readEncryptedToStream(OutputStream out, long offset, long size) throws IOException {
// make sure there is no out-of-bounds read
WUDImageCompressedInfo info = getImage().getCompressedInfo();
@ -91,6 +91,6 @@ public class WUDDiscReaderCompressed extends WUDDiscReader {
} finally {
StreamUtils.closeAll(input, out);
}
return usedSize == 0;
return size - usedSize;
}
}

View File

@ -37,7 +37,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader {
}
@Override
public boolean readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
RandomAccessFile input = getFileByOffset(offset);
int bufferSize = 0x8000;
@ -86,7 +86,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader {
input.close();
outputStream.close();
return totalread >= size;
return totalread;
}
private int getFilePartByOffset(long offset) {

View File

@ -31,7 +31,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
}
@Override
public boolean readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
FileInputStream input = new FileInputStream(getImage().getFileHandle());
@ -62,7 +62,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
} while (totalread < size);
input.close();
outputStream.close();
return totalread >= size;
return totalread;
}
@Override

View File

@ -0,0 +1,24 @@
package de.mas.wiiu.jnus.interfaces;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public interface ContentDecryptor {
/**
*
* @param in InputStream of the Encrypted Data with hashed
* @param out OutputStream of the decrypted data with hashes
* @param offset absolute offset in this Content stream
* @param size size of the payload that will be written to the outputstream
* @param payloadOffset relative offset to the start of the inputstream
* @param h3_hashes level 3 hashes of the content file
* @return
* @throws IOException
*/
long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes) throws IOException;
long readDecryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV) throws IOException;
}

View File

@ -0,0 +1,15 @@
package de.mas.wiiu.jnus.interfaces;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import de.mas.wiiu.jnus.utils.IVCache;
public interface ContentEncryptor {
long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException;
long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, IVCache ivcache) throws IOException;
}

View File

@ -56,20 +56,25 @@ public interface FSTDataProvider {
in.throwException(null);
} catch (Exception e) {
in.throwException(e);
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}).start();
return in;
}
default public boolean readFileToStream(OutputStream out, FSTEntry entry) throws IOException {
default public long readFileToStream(OutputStream out, FSTEntry entry) throws IOException {
return readFileToStream(out, entry, 0, entry.getFileSize());
}
default public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException {
default public long readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException {
return readFileToStream(out, entry, offset, entry.getFileSize());
}
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException;
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException;
}

View File

@ -0,0 +1,111 @@
package de.mas.wiiu.jnus.interfaces;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.utils.StreamUtils;
public interface NUSDataProcessor {
public NUSDataProvider getDataProvider();
default public byte[] readContent(Content c) throws IOException {
return readContent(c, 0, c.getEncryptedFileSizeAligned());
}
default public byte[] readContent(Content c, long offset, long size) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
readContentToStream(out, c, offset, size);
return out.toByteArray();
}
default public InputStream readContentAsStream(Content c) throws IOException {
return readContentAsStream(c, 0, c.getEncryptedFileSizeAligned());
}
public InputStream readContentAsStream(Content c, long offset, long size) throws IOException;
default public long readContentToStream(OutputStream out, Content entry) throws IOException {
return readContentToStream(out, entry, 0, entry.getEncryptedFileSizeAligned());
}
default public long readContentToStream(OutputStream out, Content entry, long offset) throws IOException {
return readContentToStream(out, entry, offset, entry.getEncryptedFileSizeAligned());
}
default public long readContentToStream(OutputStream out, Content c, long offset, long size) throws IOException {
InputStream in = readContentAsStream(c, offset, size);
return StreamUtils.saveInputStreamToOutputStream(in, out, size);
}
default public byte[] readDecryptedContent(Content c) throws IOException {
return readDecryptedContent(c, 0, c.getEncryptedFileSizeAligned());
}
default public byte[] readDecryptedContent(Content c, long offset, long size) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
long len = readDecryptedContentToStream(out, c, offset, size);
if(len < 0) {
return new byte[0];
}
return out.toByteArray();
}
default public InputStream readDecryptedContentAsStream(Content c) throws IOException {
return readDecryptedContentAsStream(c, 0, c.getEncryptedFileSizeAligned());
}
public InputStream readDecryptedContentAsStream(Content c, long offset, long size) throws IOException;
default public long readDecryptedContentToStream(OutputStream out, Content c) throws IOException {
return readDecryptedContentToStream(out, c, 0, c.getEncryptedFileSizeAligned());
}
default public long readDecryptedContentToStream(OutputStream out, Content c, long offset) throws IOException {
return readDecryptedContentToStream(out, c, offset, c.getEncryptedFileSizeAligned());
}
default public long readDecryptedContentToStream(OutputStream out, Content c, long offset, long size) throws IOException {
InputStream in = readDecryptedContentAsStream(c, offset, size);
return StreamUtils.saveInputStreamToOutputStream(in, out, size);
}
default public byte[] readPlainDecryptedContent(Content c, boolean forceCheckHash) throws IOException {
return readPlainDecryptedContent(c, 0, c.getEncryptedFileSizeAligned(), forceCheckHash);
}
default public byte[] readPlainDecryptedContent(Content c, long offset, long size, boolean forceCheckHash) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
long len = readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash);
if(len < 0) {
return new byte[0];
}
return out.toByteArray();
}
default public InputStream readPlainDecryptedContentAsStream(Content c, boolean forceCheckHash) throws IOException {
return readPlainDecryptedContentAsStream(c, 0, c.getEncryptedFileSizeAligned(), forceCheckHash);
}
public InputStream readPlainDecryptedContentAsStream(Content c, long offset, long size, boolean forceCheckHash) throws IOException;
default public long readPlainDecryptedContentToStream(OutputStream out, Content entry, boolean forceCheckHash) throws IOException {
return readPlainDecryptedContentToStream(out, entry, 0, entry.getEncryptedFileSizeAligned(), forceCheckHash);
}
default public long readPlainDecryptedContentToStream(OutputStream out, Content entry, long offset, boolean forceCheckHash) throws IOException {
return readPlainDecryptedContentToStream(out, entry, offset, entry.getEncryptedFileSizeAligned(), forceCheckHash);
}
default public long readPlainDecryptedContentToStream(OutputStream out, Content c, long offset, long size, boolean forceCheckHash) throws IOException {
InputStream in = readPlainDecryptedContentAsStream(c, offset, size, forceCheckHash);
return StreamUtils.saveInputStreamToOutputStream(in, out, size);
}
}

View File

@ -26,19 +26,19 @@ import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.utils.StreamUtils;
public interface NUSDataProvider {
default public byte[] readContent(Content content, long offset, int size) throws IOException {
return StreamUtils.getBytesFromStream(readContentAsStream(content, offset, size), size);
default byte[] readRawContent(Content content, long offset, int size) throws IOException {
return StreamUtils.getBytesFromStream(readRawContentAsStream(content, offset, size), size);
}
default public InputStream readContentAsStream(Content content) throws IOException {
return readContentAsStream(content, 0);
default InputStream readRawContentAsStream(Content content) throws IOException {
return readRawContentAsStream(content, 0);
}
default public InputStream readContentAsStream(Content content, long offset) throws IOException {
return readContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset);
default InputStream readRawContentAsStream(Content content, long offset) throws IOException {
return readRawContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset);
}
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException;
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException;
public Optional<byte[]> getContentH3Hash(Content content) throws IOException;
@ -53,5 +53,4 @@ public interface NUSDataProvider {
default public void setFST(FST fst) {
}
}

View File

@ -0,0 +1,17 @@
package de.mas.wiiu.jnus.interfaces;
import java.util.Objects;
import java.util.function.Function;
@FunctionalInterface
public interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
default <V> TriFunction<A, B, C, V> andThen(
Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (A a, B b, C c) -> after.apply(apply(a, b, c));
}
}

View File

@ -88,7 +88,9 @@ public final class ByteUtils {
public static byte[] getBytesFromInt(int value, ByteOrder bo) {
byte[] result = new byte[0x04];
ByteBuffer.allocate(4).order(bo).putInt(value).get(result);
ByteBuffer buffer = ByteBuffer.allocate(4).order(bo).putInt(value);
buffer.position(0);
buffer.get(result);
return result;
}
@ -97,5 +99,11 @@ public final class ByteUtils {
ByteBuffer.allocate(2).putShort(value).get(result);
return result;
}
public static short getByteFromBytes(byte[] input, int offset) {
ByteBuffer buffer = ByteBuffer.allocate(2).put(Arrays.copyOfRange(input, offset, offset + 1)).order(ByteOrder.BIG_ENDIAN);
buffer.position(0);
return (short) ((buffer.getShort() & 0xFF00) >> 8);
}
}

View File

@ -133,7 +133,7 @@ public class DataProviderUtils {
}
Utils.createDir(outputFolder);
InputStream inputStream = dataProvider.readContentAsStream(content);
InputStream inputStream = dataProvider.readRawContentAsStream(content);
if (inputStream == null) {
log.warning(content.getFilename() + " Couldn't save encrypted content. Input stream was null");
return;

View File

@ -0,0 +1,38 @@
package de.mas.wiiu.jnus.utils;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import lombok.val;
public class IVCache {
private final Map<Long, byte[]> cache = new TreeMap<>();
public IVCache(long first, byte[] IV) {
if (!addForOffset(first, IV)) {
throw new IllegalArgumentException("IV was null or not 16 bytes big");
}
}
public boolean addForOffset(long offset, byte[] IV) {
if (IV == null || IV.length != 16) {
return false;
}
cache.put(offset, IV);
return true;
}
public Optional<Pair<Long, byte[]>> getNearestForOffset(long offset) {
Optional<Pair<Long, byte[]>> result = Optional.empty();
for (val e : cache.entrySet()) {
if (e.getKey().longValue() <= offset) {
result = Optional.of(new Pair<>(e.getKey(), e.getValue()));
} else {
break;
}
}
return result;
}
}

View File

@ -0,0 +1,9 @@
package de.mas.wiiu.jnus.utils;
import lombok.Data;
@Data
public class Pair<T1, T2> {
public final T1 k;
public final T2 v;
}

View File

@ -89,8 +89,11 @@ public final class StreamUtils {
if (overflowbuffer.getLengthOfDataInBuffer() > 0) {
System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer());
inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer();
} else {
if (inBlockBuffer == 0) {
return bytesRead;
}
}
break;
}
@ -124,19 +127,21 @@ public final class StreamUtils {
}
}
public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
public static long saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
try {
saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true);
return saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true);
} catch (CheckSumWrongException e) {
// Should never happen because the hash is not set. Lets print it anyway.
e.printStackTrace();
}
return -1;
}
public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash,
public static long saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash,
long expectedSizeForHash, boolean partial) throws IOException, CheckSumWrongException {
synchronized (inputStream) {
long written = 0;
synchronized (inputStream) {
MessageDigest sha1 = null;
if (hash != null && !partial) {
try {
@ -150,7 +155,6 @@ public final class StreamUtils {
byte[] buffer = new byte[BUFFER_SIZE];
int read = 0;
long totalRead = 0;
long written = 0;
try {
do {
@ -189,6 +193,7 @@ public final class StreamUtils {
StreamUtils.closeAll(inputStream, outputStream);
}
}
return written > 0 ? written : -1;
}
public static void skipExactly(InputStream in, long offset) throws IOException {

View File

@ -23,12 +23,14 @@ import java.io.InputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
@ -238,4 +240,31 @@ public final class Utils {
return null;
}
public static Long getLastModifiedURL(HttpURLConnection connectionForURL, int timeout) throws IOException {
HttpURLConnection connection = connectionForURL;
connection.setRequestProperty("User-Agent", Settings.USER_AGENT);
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
int responseCode = connection.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
InputStream inputStream = connection.getInputStream();
byte[] buffer = new byte[0x10];
inputStream.read(buffer);
inputStream.close();
} else {
return null;
}
Long dateTime = connection.getLastModified();
if (200 <= responseCode && responseCode <= 399) {
return dateTime;
}
return null;
}
}

View File

@ -29,6 +29,7 @@ import javax.crypto.spec.SecretKeySpec;
import lombok.Getter;
import lombok.Setter;
import lombok.Synchronized;
public class AESDecryption {
private Cipher cipher;
@ -51,6 +52,7 @@ public class AESDecryption {
init(getAESKey(), getIV());
}
@Synchronized("cipher")
protected void init(byte[] decryptedKey, byte[] iv) {
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
try {
@ -61,6 +63,7 @@ public class AESDecryption {
}
}
@Synchronized("cipher")
public byte[] decrypt(byte[] input) {
try {
return cipher.doFinal(input);
@ -75,6 +78,7 @@ public class AESDecryption {
return decrypt(input, 0, len);
}
@Synchronized("cipher")
public byte[] decrypt(byte[] input, int offset, int len) {
try {
return cipher.doFinal(input, offset, len);

View File

@ -0,0 +1,92 @@
/****************************************************************************
* Copyright (C) 2016-2019 Maschell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
****************************************************************************/
package de.mas.wiiu.jnus.utils.cryptography;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import lombok.Getter;
import lombok.Setter;
import lombok.Synchronized;
public class AESEncryption {
private Cipher cipher;
@Getter @Setter private byte[] AESKey;
@Getter @Setter private byte[] IV;
public AESEncryption(byte[] AESKey, byte[] IV) throws NoSuchProviderException {
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
e.printStackTrace();
}
setAESKey(AESKey);
setIV(IV);
init();
}
protected final void init() {
init(getAESKey(), getIV());
}
@Synchronized("cipher")
protected void init(byte[] decryptedKey, byte[] iv) {
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
System.exit(2);
}
}
@Synchronized("cipher")
public byte[] encrypt(byte[] input) {
try {
return cipher.doFinal(input);
} catch (IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
System.exit(2);
}
return input;
}
public byte[] encrypt(byte[] input, int len) {
return encrypt(input, 0, len);
}
@Synchronized("cipher")
public byte[] encrypt(byte[] input, int offset, int len) {
try {
return cipher.doFinal(input, offset, len);
} catch (IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
System.exit(2);
}
return input;
}
}

View File

@ -16,26 +16,18 @@
****************************************************************************/
package de.mas.wiiu.jnus.utils.cryptography;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
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.StreamUtils;
import de.mas.wiiu.jnus.utils.Utils;
import lombok.extern.java.Log;
@Log
public class NUSDecryption extends AESDecryption {
public class NUSDecryption extends AESDecryption implements ContentDecryptor {
public NUSDecryption(byte[] AESKey, byte[] IV) {
super(AESKey, IV);
}
@ -56,144 +48,82 @@ public class NUSDecryption extends AESDecryption {
return decrypt(blockBuffer, offset, BLOCKSIZE);
}
public void decryptFileStream(InputStream inputStream, OutputStream outputStream, long fileOffset, long filesize, byte[] IV, byte[] h3hash,
long expectedSizeForHash) throws IOException, CheckSumWrongException {
MessageDigest sha1 = null;
MessageDigest sha1fallback = null;
if (h3hash != 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);
byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV);
if (inBlockBuffer == BLOCKSIZE) {
IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE);
}
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 && h3hash == 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 = h3hash;
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.finest("Hash DOES match saves output stream.");
}
}
} finally {
StreamUtils.closeAll(inputStream, outputStream);
}
if (written < filesize) {
throw new IOException("Failed to read. Missing " + (filesize - written));
}
}
public void decryptFileStreamHashed(InputStream inputStream, OutputStream outputStream, long fileoffset, long filesize, byte[] h3Hash)
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
@Override
public long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes)
throws IOException {
int BLOCKSIZE = 0x10000;
int HASHBLOCKSIZE = 0xFC00;
int HASHEDBLOCKSIZE = 0xFC00;
int HASHSIZE = BLOCKSIZE - HASHEDBLOCKSIZE;
long writeSize = HASHBLOCKSIZE;
long block = (offset / BLOCKSIZE);
long writeSize = BLOCKSIZE;
long block = (fileoffset / HASHBLOCKSIZE);
long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
if (soffset + filesize > writeSize) {
writeSize = writeSize - soffset;
}
long soffset = payloadOffset;
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
long wrote = 0;
int inBlockBuffer = 0;
try {
do {
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, encryptedBlockBuffer, overflow, BLOCKSIZE);
if (writeSize > filesize) writeSize = filesize;
inBlockBuffer = StreamUtils.getChunkFromStream(in, encryptedBlockBuffer, overflow, BLOCKSIZE);
if (inBlockBuffer < 0) {
return wrote;
}
if (inBlockBuffer != BLOCKSIZE) {
throw new IOException("wasn't able to read " + BLOCKSIZE);
}
byte[] output;
try {
output = decryptFileChunkHash(encryptedBlockBuffer, (int) block, h3Hash);
} catch (CheckSumWrongException | NoSuchAlgorithmException e) {
throw e;
}
byte[] hashes = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, new byte[16]);
if ((wrote + writeSize) > filesize) {
writeSize = (int) (filesize - wrote);
}
int H0_start = (int) (((int) block % 16) * 20);
byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16);
byte[] output = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, HASHEDBLOCKSIZE, IV);
try {
outputStream.write(output, (int) (0 + soffset), (int) writeSize);
if (writeSize > size) {
writeSize = size;
}
if (writeSize + wrote > size) {
writeSize = size - wrote;
}
long toBeWritten = writeSize;
if (soffset <= HASHSIZE) {
long writeHashSize = HASHSIZE;
if (writeSize < HASHSIZE) {
writeHashSize = writeSize;
}
if (writeHashSize + soffset > HASHSIZE) {
writeHashSize = HASHSIZE - soffset;
}
out.write(hashes, (int) (0 + soffset), (int) writeHashSize);
wrote += writeHashSize;
toBeWritten -= writeHashSize;
if (toBeWritten > 0) {
if (toBeWritten > HASHEDBLOCKSIZE) {
toBeWritten = HASHEDBLOCKSIZE;
writeSize = toBeWritten - HASHEDBLOCKSIZE;
}
out.write(output, 0, (int) toBeWritten);
wrote += toBeWritten;
}
} else {
soffset -= 0x400;
long writeThisTime = writeSize;
if (writeSize + soffset > HASHEDBLOCKSIZE) {
writeThisTime = HASHEDBLOCKSIZE - soffset;
}
out.write(output, (int) (0 + soffset), (int) writeThisTime);
wrote += writeThisTime;
}
writeSize = BLOCKSIZE;
} catch (IOException e) {
if (e.getMessage().equals("Pipe closed")) {
break;
@ -201,62 +131,76 @@ public class NUSDecryption extends AESDecryption {
e.printStackTrace();
throw e;
}
wrote += writeSize;
block++;
if (soffset > 0) {
writeSize = HASHBLOCKSIZE;
soffset = 0;
}
} while (wrote < filesize && (inBlockBuffer == BLOCKSIZE));
log.finest("Decryption okay");
} while (wrote < size && (inBlockBuffer == BLOCKSIZE));
} finally {
StreamUtils.closeAll(inputStream, outputStream);
StreamUtils.closeAll(in, out);
}
return wrote > 0 ? wrote : -1;
}
private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, byte[] h3_hashes) throws CheckSumWrongException, NoSuchAlgorithmException {
int hashSize = 0x400;
int blocksize = 0xFC00;
@Override
public long readDecryptedContentToStreamNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, long payloadOffset, byte[] IV)
throws IOException {
int BLOCKSIZE = 0x80000;
byte[] hashes = decryptFileChunk(blockBuffer, hashSize, new byte[16]);
byte[] blockBuffer = new byte[BLOCKSIZE];
int H0_start = (block % 16) * 20;
int inBlockBuffer;
long written = 0;
long read = 0;
byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16);
byte[] output = decryptFileChunk(blockBuffer, hashSize, blocksize, IV);
HashUtil.checkFileChunkHashes(hashes, h3_hashes, output, block);
return output;
}
public boolean decryptStreamsHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Optional<byte[]> h3HashHashed)
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
try {
byte[] h3 = h3HashHashed.orElseThrow(() -> new FileNotFoundException("h3 hash not found."));
decryptFileStreamHashed(inputStream, outputStream, offset, size, h3);
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
// We can only decrypt multiples of 16. So we need to align it.
long toRead = Utils.align(size, 16);
do {
long writeOffset = Math.max(0, payloadOffset - read);
int curReadSize = BLOCKSIZE;
if (toRead < BLOCKSIZE) {
curReadSize = (int) (toRead + writeOffset);
}
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, (int) Utils.align(curReadSize, 16));
if (inBlockBuffer < 0) {
break;
}
byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV);
if (inBlockBuffer > 16) {
IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE);
}
long writeLength = Math.min((output.length - writeOffset), (size - written));
try {
read += inBlockBuffer;
outputStream.write(output, (int) writeOffset, (int) writeLength);
written += writeLength;
toRead -= writeLength;
} catch (IOException e) {
if (e.getMessage().equals("Pipe closed")) {
break;
} else {
throw e;
}
}
if (written >= size) {
break;
}
} while (true);
} finally {
StreamUtils.closeAll(inputStream, outputStream);
}
return true;
}
public boolean decryptStreamsNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Content content, byte[] IV,
boolean partial) throws IOException, CheckSumWrongException {
try {
byte[] h3Hash = content.getSHA2Hash();
// Ignore the h3hash if we don't read the whole file.
if (partial) {
h3Hash = null;
}
decryptFileStream(inputStream, outputStream, offset, size, IV, h3Hash, content.getEncryptedFileSize());
} finally {
StreamUtils.closeAll(inputStream, outputStream);
}
return true;
return written > 0 ? written : -1;
}
}

View File

@ -0,0 +1,152 @@
package de.mas.wiiu.jnus.utils.cryptography;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.interfaces.ContentEncryptor;
import de.mas.wiiu.jnus.utils.ByteArrayBuffer;
import de.mas.wiiu.jnus.utils.IVCache;
import de.mas.wiiu.jnus.utils.StreamUtils;
import lombok.Synchronized;
public class NUSEncryption extends AESEncryption implements ContentEncryptor {
public NUSEncryption(byte[] AESKey, byte[] IV) throws NoSuchProviderException {
super(AESKey, IV);
}
public NUSEncryption(Ticket ticket) throws NoSuchProviderException {
this(ticket.getDecryptedKey(), ticket.getIV());
}
@Synchronized
private byte[] encryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) {
return encryptFileChunk(blockBuffer, 0, BLOCKSIZE, IV);
}
@Synchronized
private byte[] encryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) {
if (IV != null) {
setIV(IV);
init();
}
return encrypt(blockBuffer, offset, BLOCKSIZE);
}
@Override
public long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException {
int BLOCKSIZE = 0x10000;
int HASHBLOCKSIZE = 0x400;
int HASHEDBLOCKSIZE = 0xFC00;
int buffer_size = BLOCKSIZE;
byte[] decryptedBlockBuffer = new byte[buffer_size];
ByteArrayBuffer overflowbuffer = new ByteArrayBuffer(buffer_size);
int block = (int) (offset / 0x10000);
int inBlockBuffer = 0;
long read = 0;
long written = 0;
try {
do {
inBlockBuffer = StreamUtils.getChunkFromStream(in, decryptedBlockBuffer, overflowbuffer, BLOCKSIZE);
read += inBlockBuffer;
if (read - offset < payloadOffset) {
continue;
}
if (inBlockBuffer != buffer_size) {
break;
}
long curOffset = Math.max(0, payloadOffset - offset - read);
byte[] IV = new byte[16];
if (curOffset < HASHBLOCKSIZE) {
byte[] encryptedhashes = encryptFileChunk(Arrays.copyOfRange(decryptedBlockBuffer, 0, HASHBLOCKSIZE), HASHBLOCKSIZE, IV);
long writeLength = Math.min((encryptedhashes.length - curOffset), (size - written));
out.write(encryptedhashes, (int) curOffset, (int) writeLength);
written += writeLength;
} else {
curOffset = curOffset > HASHBLOCKSIZE ? curOffset - HASHBLOCKSIZE : 0;
}
if(curOffset < HASHEDBLOCKSIZE) {
int iv_start = (block % 16) * 20;
IV = Arrays.copyOfRange(decryptedBlockBuffer, iv_start, iv_start + 16);
byte[] encryptedContent = encryptFileChunk(Arrays.copyOfRange(decryptedBlockBuffer, HASHBLOCKSIZE, HASHEDBLOCKSIZE + HASHBLOCKSIZE),
HASHEDBLOCKSIZE, IV);
long writeLength = Math.min((encryptedContent.length - curOffset), (size - written));
out.write(encryptedContent, (int) curOffset, (int) writeLength);
written += writeLength;
}
block++;
} while (inBlockBuffer == buffer_size);
} finally {
StreamUtils.closeAll(in, out);
}
return written > 0 ? written : -1;
}
@Override
public long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV,
IVCache ivcache) throws IOException {
int BLOCKSIZE = 0x08000;
int buffer_size = BLOCKSIZE;
byte[] decryptedBlockBuffer = new byte[buffer_size];
ByteArrayBuffer overflowbuffer = new ByteArrayBuffer(buffer_size);
int inBlockBuffer = 0;
setIV(IV);
init();
long read = 0;
long written = 0;
long curPos = offset;
try {
do {
int curReadLength = (int) (curPos % buffer_size);
if (curReadLength == 0) {
curReadLength = buffer_size;
}
inBlockBuffer = StreamUtils.getChunkFromStream(in, decryptedBlockBuffer, overflowbuffer, curReadLength);
if (inBlockBuffer < 0) {
break;
}
curPos += inBlockBuffer;
read += inBlockBuffer;
byte[] output = encrypt(decryptedBlockBuffer, 0, inBlockBuffer);
byte[] curIV = Arrays.copyOfRange(output, output.length - 16, output.length);
ivcache.addForOffset(curPos, curIV);
setIV(Arrays.copyOfRange(output, BLOCKSIZE - 16, BLOCKSIZE));
init();
if (read < payloadOffset) {
continue;
}
long writeOffset = Math.max(0, payloadOffset - offset - read);
long writeLength = Math.min((output.length - writeOffset), (size - written));
out.write(output, (int) writeOffset, (int) writeLength);
written += writeLength;
} while (written < size);
} finally {
StreamUtils.closeAll(in, out);
}
return written > 0 ? written : -1;
}
}