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

View File

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

View File

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

View File

@ -17,7 +17,6 @@
package de.mas.wiiu.jnus; package de.mas.wiiu.jnus;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; 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.Ticket;
import de.mas.wiiu.jnus.entities.fst.FST; import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.entities.fst.FSTEntry; 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 de.mas.wiiu.jnus.utils.FSTUtils;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
public class NUSTitle { public class NUSTitle {
@ -39,12 +37,18 @@ public class NUSTitle {
@Getter private final TMD TMD; @Getter private final TMD TMD;
@Getter private final NUSDataProvider dataProvider; @Getter private final NUSDataProcessor dataProcessor;
public NUSTitle(@NonNull NUSDataProvider dataProvider) throws ParseException, IOException { private NUSTitle(TMD tmd, NUSDataProcessor dataProcessor) {
byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0)); this.TMD = tmd;
this.TMD = de.mas.wiiu.jnus.entities.TMD.parseTMD(tmdData); this.dataProcessor = dataProcessor;
this.dataProvider = dataProvider; }
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() { public Stream<FSTEntry> getAllFSTEntriesAsStream() {
@ -66,13 +70,13 @@ public class NUSTitle {
} }
public void cleanup() throws IOException { public void cleanup() throws IOException {
if (getDataProvider() != null) { if (getDataProcessor() != null && getDataProcessor().getDataProvider() != null) {
getDataProvider().cleanup(); getDataProcessor().getDataProvider().cleanup();
} }
} }
@Override @Override
public String toString() { 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 * 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 * it under the terms of the GNU General Public License as published by
@ -16,54 +16,78 @@
****************************************************************************/ ****************************************************************************/
package de.mas.wiiu.jnus; package de.mas.wiiu.jnus;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.security.NoSuchProviderException;
import java.text.ParseException; import java.text.ParseException;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; 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.Ticket;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FST; import de.mas.wiiu.jnus.entities.fst.FST;
import de.mas.wiiu.jnus.entities.fst.FSTEntry; import de.mas.wiiu.jnus.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle; 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.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider; import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.StreamUtils; import de.mas.wiiu.jnus.interfaces.TriFunction;
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption; import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption;
import de.mas.wiiu.jnus.utils.cryptography.NUSEncryption;
public class NUSTitleLoader { public class NUSTitleLoader {
private NUSTitleLoader() { private NUSTitleLoader() {
// should be empty // 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(); 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()) { if (config.isNoDecryption()) {
NUSTitle result = NUSTitle.create(tmd, dataProcessorFunction.apply(dataProvider, Optional.empty(), Optional.empty()), Optional.empty(),
Optional.empty());
return result; return result;
} }
Ticket ticket = null; Optional<Ticket> ticket = Optional.empty();
Optional<ContentDecryptor> decryption = Optional.empty();
Optional<ContentEncryptor> encryption = Optional.empty();
if (config.isTicketNeeded()) { if (config.isTicketNeeded()) {
ticket = config.getTicket(); Ticket ticketT = config.getTicket();
if (ticket == null) { if (ticketT == null) {
Optional<byte[]> ticketOpt = dataProvider.getRawTicket(); Optional<byte[]> ticketOpt = dataProvider.getRawTicket();
if (ticketOpt.isPresent()) { if (ticketOpt.isPresent()) {
ticket = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey()); ticketT = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey());
} }
} }
if (ticket == null) { if (ticketT == null) {
new ParseException("Failed to get ticket data", 0); throw new ParseException("Failed to get ticket data", 0);
}
result.setTicket(Optional.of(ticket));
} }
ticket = Optional.of(ticketT);
decryption = Optional.of(new NUSDecryption(ticketT));
try {
encryption = Optional.of(new NUSEncryption(ticketT));
} catch (NoSuchProviderException e) {
throw new IOException(e);
}
}
NUSDataProcessor dpp = dataProcessorFunction.apply(dataProvider, decryption, encryption);
// If we have just content, we don't have a FST. // 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. // 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); FSTDataProvider dp = new FSTDataProviderNUSTitle(result);
for (FSTEntry children : dp.getRoot().getChildren()) { for (FSTEntry children : dp.getRoot().getChildren()) {
dp.readFile(children); dp.readFile(children);
@ -72,27 +96,16 @@ public class NUSTitleLoader {
return result; return result;
} }
// If we have more than one content, the index 0 is the FST. // If we have more than one content, the index 0 is the FST.
Content fstContent = result.getTMD().getContentByIndex(0); Content fstContent = tmd.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);
}
byte[] fstBytes = dpp.readPlainDecryptedContent(fstContent, true);
FST fst = FST.parseFST(fstBytes); FST fst = FST.parseFST(fstBytes);
result.setFST(Optional.of(fst));
// The dataprovider may need the FST to calculate the offset of a content // The dataprovider may need the FST to calculate the offset of a content
// on the partition. // on the partition.
dataProvider.setFST(fst); 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 java.text.ParseException;
import de.mas.wiiu.jnus.entities.fst.FSTEntry; 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.implementations.NUSDataProviderFST;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider; import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
@ -36,7 +37,7 @@ public final class NUSTitleLoaderFST {
NUSTitleConfig config = new NUSTitleConfig(); NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey); 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 java.text.ParseException;
import de.mas.wiiu.jnus.entities.Ticket; import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal; import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal;
public final class NUSTitleLoaderLocal { public final class NUSTitleLoaderLocal {
@ -43,7 +44,7 @@ public final class NUSTitleLoaderLocal {
throw new IOException("Ticket was null and no commonKey was given"); 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; package de.mas.wiiu.jnus;
import de.mas.wiiu.jnus.entities.Ticket; import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup; import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup;
public final class NUSTitleLoaderLocalBackup { public final class NUSTitleLoaderLocalBackup {
@ -35,7 +36,8 @@ public final class NUSTitleLoaderLocalBackup {
config.setNoDecryption(true); 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 java.text.ParseException;
import de.mas.wiiu.jnus.entities.Ticket; import de.mas.wiiu.jnus.entities.Ticket;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote; import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
public final class NUSTitleLoaderRemote { public final class NUSTitleLoaderRemote {
@ -53,7 +54,7 @@ public final class NUSTitleLoaderRemote {
throw new IOException("Ticket was null and no commonKey was given"); 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 org.xml.sax.SAXException;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy; import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy;
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo; import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
import de.mas.wiiu.jnus.implementations.woomy.WoomyParser; import de.mas.wiiu.jnus.implementations.woomy.WoomyParser;
@ -41,7 +42,7 @@ public final class NUSTitleLoaderWoomy {
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile)); 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.ArrayList;
import java.util.List; import java.util.List;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle; import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition; import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD; import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
@ -95,7 +96,7 @@ public final class WUDLoader {
final NUSTitleConfig config = new NUSTitleConfig(); final NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey); config.setCommonKey(commonKey);
gamePartition.getTmd(); 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 { 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 org.xml.sax.SAXException;
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle; import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
import de.mas.wiiu.jnus.implementations.FSTDataProviderWumadDataPartition; import de.mas.wiiu.jnus.implementations.FSTDataProviderWumadDataPartition;
import de.mas.wiiu.jnus.implementations.NUSDataProviderWumad; import de.mas.wiiu.jnus.implementations.NUSDataProviderWumad;
@ -40,7 +41,7 @@ public class WumadLoader {
final NUSTitleConfig config = new NUSTitleConfig(); final NUSTitleConfig config = new NUSTitleConfig();
config.setCommonKey(commonKey); config.setCommonKey(commonKey);
gamePartition.getTmd(); 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 { public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WumadInfo wumadInfo, byte[] commonKey) throws IOException, ParseException {
@ -57,7 +58,6 @@ public class WumadLoader {
} }
return result; 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_FLAG_UNKWN1 = 0x4000;
public static final short CONTENT_HASHED = 0x0002; public static final short CONTENT_HASHED = 0x0002;
public static final short CONTENT_ENCRYPTED = 0x0001; public static final short CONTENT_ENCRYPTED = 0x0001;
public static final int CONTENT_SIZE = 0x30; public static final int CONTENT_SIZE = 0x30;
@Getter private final int ID; @Getter private final int ID;
@ -127,7 +128,7 @@ public class Content implements Comparable<Content> {
*/ */
public long getDecryptedFileSize() { public long getDecryptedFileSize() {
if (isHashed()) { if (isHashed()) {
return getEncryptedFileSize() / 0x10000 * 0xFC00; return (getEncryptedFileSize() / 0x10000) * 0xFC00;
} else { } else {
return getEncryptedFileSize(); return getEncryptedFileSize();
} }

View File

@ -71,7 +71,7 @@ public final class FST {
int fst_size = fileCount * 0x10; int fst_size = fileCount * 0x10;
int nameOff = fst_offset + fst_size; 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. // Get list with null-terminated Strings. Ends with \0\0.
for (int i = nameOff; i < fstData.length - 1; i++) { for (int i = nameOff; i < fstData.length - 1; i++) {

View File

@ -104,7 +104,6 @@ public final class FSTService {
while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) { while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) {
j++; j++;
} }
return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + 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; package de.mas.wiiu.jnus.implementations;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import de.mas.wiiu.jnus.NUSTitle; 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.entities.fst.FSTEntry;
import de.mas.wiiu.jnus.interfaces.FSTDataProvider; import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.HasNUSTitle; import de.mas.wiiu.jnus.interfaces.HasNUSTitle;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider; import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
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 lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.java.Log; import lombok.extern.java.Log;
@Log @Log
public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle { public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
private final NUSDataProcessor dataProcessor;
private final NUSTitle title; private final NUSTitle title;
private final FSTEntry rootEntry; private final FSTEntry rootEntry;
@Getter @Setter private String name; @Getter @Setter private String name;
public FSTDataProviderNUSTitle(NUSTitle title) throws IOException { public FSTDataProviderNUSTitle(NUSTitle title) throws IOException {
this.dataProcessor = title.getDataProcessor();
this.title = title; this.title = title;
this.name = String.format("%016X", title.getTMD().getTitleID()); this.name = String.format("%016X", title.getTMD().getTitleID());
@ -66,150 +61,18 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
} }
@Override @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 {
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 {
if (entry.isNotInPackage()) { if (entry.isNotInPackage()) {
if (entry.isNotInPackage()) { if (entry.isNotInPackage()) {
log.info("Decryption not possible because the FSTEntry is not in this package"); log.info("Decryption not possible because the FSTEntry is not in this package");
} }
outputStream.close(); out.close();
return false; return -1;
}
if (offset % 16 != 0) {
throw new IOException("The offset for decryption need to be aligned to 16");
} }
Content c = title.getTMD().getContentByIndex(entry.getContentIndex()); Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
try { return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getFileOffset(), size, size == entry.getFileSize());
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;
} }
@Override @Override

View File

@ -49,7 +49,7 @@ public class FSTDataProviderWUDDataPartition implements FSTDataProvider {
} }
@Override @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()) ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
.orElseThrow(() -> new IOException("Failed to find FSTInfo")); .orElseThrow(() -> new IOException("Failed to find FSTInfo"));
if (titleKey == null) { if (titleKey == null) {

View File

@ -32,15 +32,15 @@ public class FSTDataProviderWumadDataPartition implements FSTDataProvider {
} }
@Override @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 {
StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size); return StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
return true;
} }
@Override @Override
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException { public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
ZipEntry zipEntry = zipFile.stream() 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()); .orElseThrow(() -> new FileNotFoundException());
InputStream in = zipFile.getInputStream(zipEntry); 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.Settings;
import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.content.Content;
import de.mas.wiiu.jnus.entities.fst.FSTEntry; 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.FSTDataProvider;
import de.mas.wiiu.jnus.interfaces.NUSDataProvider; import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
import de.mas.wiiu.jnus.utils.FSTUtils; import de.mas.wiiu.jnus.utils.FSTUtils;
@ -39,12 +40,12 @@ public class NUSDataProviderFST implements NUSDataProvider {
this.fstDataProvider = fstDataProvider; this.fstDataProvider = fstDataProvider;
} }
public NUSDataProviderFST(FSTDataProvider fstDataProvider) { public NUSDataProviderFST(FSTDataProvider fstDataProvider, ContentDecryptor decryptor) {
this(fstDataProvider, fstDataProvider.getRoot()); this(fstDataProvider, fstDataProvider.getRoot());
} }
@Override @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(); String filename = content.getFilename();
Optional<FSTEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename); Optional<FSTEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found.")); FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));

View File

@ -45,7 +45,7 @@ public final class NUSDataProviderLocal implements NUSDataProvider {
} }
@Override @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()); File filepath = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), content.getFilename());
if (filepath == null || !filepath.exists()) { if (filepath == null || !filepath.exists()) {
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + content.getFilename() + "\", file does not exist"; String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + content.getFilename() + "\", file does not exist";

View File

@ -48,7 +48,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
} }
@Override @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)); File filepath = new File(getFilePathOnDisk(content));
if (!filepath.exists()) { if (!filepath.exists()) {
throw new FileNotFoundException(filepath.getAbsolutePath() + " was not found."); 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 { public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID()); String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
File h3File = new File(h3Path); File h3File = new File(h3Path);
if (!h3File.exists()) {
throw new FileNotFoundException(h3File.getAbsolutePath() + " was not found.");
}
return Optional.of(Files.readAllBytes(h3File.toPath())); return Optional.of(Files.readAllBytes(h3File.toPath()));
} }

View File

@ -39,7 +39,7 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
} }
@Override @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(); NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size); return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size);
} }
@ -56,7 +56,6 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
if (resOpt == null) { if (resOpt == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
String url = getRemoteURL(content) + Settings.H3_EXTENTION; String url = getRemoteURL(content) + Settings.H3_EXTENTION;
System.out.println(url);
byte[] res = downloadService.downloadToByteArray(url); byte[] res = downloadService.downloadToByteArray(url);
if (res == null || res.length == 0) { if (res == null || res.length == 0) {
@ -69,8 +68,11 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
return resOpt; return resOpt;
} }
Optional<byte[]> tmdCache = null;
@Override @Override
public Optional<byte[]> getRawTMD() throws IOException { public Optional<byte[]> getRawTMD() throws IOException {
if (tmdCache == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
long titleID = getTitleID(); long titleID = getTitleID();
@ -81,17 +83,24 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
if (res == null || res.length == 0) { if (res == null || res.length == 0) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(res); tmdCache = Optional.of(res);
} }
return tmdCache;
}
Optional<byte[]> ticketCache = null;
@Override @Override
public Optional<byte[]> getRawTicket() throws IOException { public Optional<byte[]> getRawTicket() throws IOException {
if (ticketCache == null) {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
byte[] res = downloadService.downloadTicketToByteArray(titleID); byte[] res = downloadService.downloadTicketToByteArray(titleID);
if (res == null || res.length == 0) { if (res == null || res.length == 0) {
return Optional.empty(); return Optional.empty();
} }
return Optional.of(res); ticketCache = Optional.of(res);
}
return ticketCache;
} }
@Override @Override

View File

@ -56,7 +56,7 @@ public class NUSDataProviderWUD implements NUSDataProvider {
} }
@Override @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(); WUDDiscReader discReader = getDiscReader();
long offset = getOffsetInWUD(content) + fileOffsetBlock; long offset = getOffsetInWUD(content) + fileOffsetBlock;

View File

@ -16,7 +16,6 @@
****************************************************************************/ ****************************************************************************/
package de.mas.wiiu.jnus.implementations; package de.mas.wiiu.jnus.implementations;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -46,7 +45,7 @@ public class NUSDataProviderWoomy implements NUSDataProvider {
} }
@Override @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(); WoomyZipFile zipFile = getSharedWoomyZipFile();
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase()); ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
if (entry == null) { if (entry == null) {

View File

@ -61,7 +61,7 @@ public class NUSDataProviderWumad implements NUSDataProvider {
} }
@Override @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() + ".")) 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()); .filter(e -> e.getName().endsWith(content.getFilename().toLowerCase())).findFirst().orElseThrow(() -> new FileNotFoundException());
InputStream in = wumad.getInputStream(entry); InputStream in = wumad.getInputStream(entry);

View File

@ -54,7 +54,7 @@ public abstract class WUDDiscReader {
return out.toByteArray(); 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 { public InputStream readEncryptedToStream(long offset, long size) throws IOException {
PipedOutputStream out = new PipedOutputStream(); PipedOutputStream out = new PipedOutputStream();
@ -72,6 +72,23 @@ public abstract class WUDDiscReader {
return in; 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 * @param readOffset
@ -93,7 +110,7 @@ public abstract class WUDDiscReader {
return decryptedChunk; 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 { boolean useFixedIV) throws IOException {
byte[] usedIV = null; byte[] usedIV = null;
if (useFixedIV) { if (useFixedIV) {
@ -153,7 +170,7 @@ public abstract class WUDDiscReader {
StreamUtils.closeAll(outputStream); 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/ * 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 @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 // make sure there is no out-of-bounds read
WUDImageCompressedInfo info = getImage().getCompressedInfo(); WUDImageCompressedInfo info = getImage().getCompressedInfo();
@ -91,6 +91,6 @@ public class WUDDiscReaderCompressed extends WUDDiscReader {
} finally { } finally {
StreamUtils.closeAll(input, out); StreamUtils.closeAll(input, out);
} }
return usedSize == 0; return size - usedSize;
} }
} }

View File

@ -37,7 +37,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader {
} }
@Override @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); RandomAccessFile input = getFileByOffset(offset);
int bufferSize = 0x8000; int bufferSize = 0x8000;
@ -86,7 +86,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader {
input.close(); input.close();
outputStream.close(); outputStream.close();
return totalread >= size; return totalread;
} }
private int getFilePartByOffset(long offset) { private int getFilePartByOffset(long offset) {

View File

@ -31,7 +31,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
} }
@Override @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()); FileInputStream input = new FileInputStream(getImage().getFileHandle());
@ -62,7 +62,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
} while (totalread < size); } while (totalread < size);
input.close(); input.close();
outputStream.close(); outputStream.close();
return totalread >= size; return totalread;
} }
@Override @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); in.throwException(null);
} catch (Exception e) { } catch (Exception e) {
in.throwException(e); in.throwException(e);
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
} }
}).start(); }).start();
return in; 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()); 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()); 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; import de.mas.wiiu.jnus.utils.StreamUtils;
public interface NUSDataProvider { public interface NUSDataProvider {
default public byte[] readContent(Content content, long offset, int size) throws IOException { default byte[] readRawContent(Content content, long offset, int size) throws IOException {
return StreamUtils.getBytesFromStream(readContentAsStream(content, offset, size), size); return StreamUtils.getBytesFromStream(readRawContentAsStream(content, offset, size), size);
} }
default public InputStream readContentAsStream(Content content) throws IOException { default InputStream readRawContentAsStream(Content content) throws IOException {
return readContentAsStream(content, 0); return readRawContentAsStream(content, 0);
} }
default public InputStream readContentAsStream(Content content, long offset) throws IOException { default InputStream readRawContentAsStream(Content content, long offset) throws IOException {
return readContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset); 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; public Optional<byte[]> getContentH3Hash(Content content) throws IOException;
@ -53,5 +53,4 @@ public interface NUSDataProvider {
default public void setFST(FST fst) { 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) { public static byte[] getBytesFromInt(int value, ByteOrder bo) {
byte[] result = new byte[0x04]; 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; return result;
} }
@ -98,4 +100,10 @@ public final class ByteUtils {
return 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); Utils.createDir(outputFolder);
InputStream inputStream = dataProvider.readContentAsStream(content); InputStream inputStream = dataProvider.readRawContentAsStream(content);
if (inputStream == null) { if (inputStream == null) {
log.warning(content.getFilename() + " Couldn't save encrypted content. Input stream was null"); log.warning(content.getFilename() + " Couldn't save encrypted content. Input stream was null");
return; 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) { if (overflowbuffer.getLengthOfDataInBuffer() > 0) {
System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer()); System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer());
inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer(); inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer();
} else {
if (inBlockBuffer == 0) {
return bytesRead;
}
} }
break; 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 { try {
saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true); return saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true);
} catch (CheckSumWrongException e) { } catch (CheckSumWrongException e) {
// Should never happen because the hash is not set. Lets print it anyway. // Should never happen because the hash is not set. Lets print it anyway.
e.printStackTrace(); 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 { long expectedSizeForHash, boolean partial) throws IOException, CheckSumWrongException {
synchronized (inputStream) { long written = 0;
synchronized (inputStream) {
MessageDigest sha1 = null; MessageDigest sha1 = null;
if (hash != null && !partial) { if (hash != null && !partial) {
try { try {
@ -150,7 +155,6 @@ public final class StreamUtils {
byte[] buffer = new byte[BUFFER_SIZE]; byte[] buffer = new byte[BUFFER_SIZE];
int read = 0; int read = 0;
long totalRead = 0; long totalRead = 0;
long written = 0;
try { try {
do { do {
@ -189,6 +193,7 @@ public final class StreamUtils {
StreamUtils.closeAll(inputStream, outputStream); StreamUtils.closeAll(inputStream, outputStream);
} }
} }
return written > 0 ? written : -1;
} }
public static void skipExactly(InputStream in, long offset) throws IOException { 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.math.BigInteger;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays; import java.util.Arrays;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.LogManager; import java.util.logging.LogManager;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
@ -238,4 +240,31 @@ public final class Utils {
return null; 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.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.Synchronized;
public class AESDecryption { public class AESDecryption {
private Cipher cipher; private Cipher cipher;
@ -51,6 +52,7 @@ public class AESDecryption {
init(getAESKey(), getIV()); init(getAESKey(), getIV());
} }
@Synchronized("cipher")
protected void init(byte[] decryptedKey, byte[] iv) { protected void init(byte[] decryptedKey, byte[] iv) {
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES"); SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
try { try {
@ -61,6 +63,7 @@ public class AESDecryption {
} }
} }
@Synchronized("cipher")
public byte[] decrypt(byte[] input) { public byte[] decrypt(byte[] input) {
try { try {
return cipher.doFinal(input); return cipher.doFinal(input);
@ -75,6 +78,7 @@ public class AESDecryption {
return decrypt(input, 0, len); return decrypt(input, 0, len);
} }
@Synchronized("cipher")
public byte[] decrypt(byte[] input, int offset, int len) { public byte[] decrypt(byte[] input, int offset, int len) {
try { try {
return cipher.doFinal(input, offset, len); 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; package de.mas.wiiu.jnus.utils.cryptography;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional;
import de.mas.wiiu.jnus.entities.Ticket; 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.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.StreamUtils;
import de.mas.wiiu.jnus.utils.Utils; import de.mas.wiiu.jnus.utils.Utils;
import lombok.extern.java.Log;
@Log public class NUSDecryption extends AESDecryption implements ContentDecryptor {
public class NUSDecryption extends AESDecryption {
public NUSDecryption(byte[] AESKey, byte[] IV) { public NUSDecryption(byte[] AESKey, byte[] IV) {
super(AESKey, IV); super(AESKey, IV);
} }
@ -56,144 +48,82 @@ public class NUSDecryption extends AESDecryption {
return decrypt(blockBuffer, offset, BLOCKSIZE); return decrypt(blockBuffer, offset, BLOCKSIZE);
} }
public void decryptFileStream(InputStream inputStream, OutputStream outputStream, long fileOffset, long filesize, byte[] IV, byte[] h3hash, @Override
long expectedSizeForHash) throws IOException, CheckSumWrongException { public long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes)
MessageDigest sha1 = null; throws IOException {
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 {
int BLOCKSIZE = 0x10000; 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 = payloadOffset;
long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
if (soffset + filesize > writeSize) {
writeSize = writeSize - soffset;
}
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE]; byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE); ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
long wrote = 0; long wrote = 0;
int inBlockBuffer = 0; int inBlockBuffer = 0;
try { try {
do { do {
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, encryptedBlockBuffer, overflow, BLOCKSIZE); inBlockBuffer = StreamUtils.getChunkFromStream(in, encryptedBlockBuffer, overflow, BLOCKSIZE);
if (writeSize > filesize) writeSize = filesize; if (inBlockBuffer < 0) {
return wrote;
}
if (inBlockBuffer != BLOCKSIZE) { if (inBlockBuffer != BLOCKSIZE) {
throw new IOException("wasn't able to read " + BLOCKSIZE); throw new IOException("wasn't able to read " + BLOCKSIZE);
} }
byte[] output; byte[] hashes = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, new byte[16]);
try {
output = decryptFileChunkHash(encryptedBlockBuffer, (int) block, h3Hash);
} catch (CheckSumWrongException | NoSuchAlgorithmException e) {
throw e;
}
if ((wrote + writeSize) > filesize) { int H0_start = (int) (((int) block % 16) * 20);
writeSize = (int) (filesize - wrote);
} byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16);
byte[] output = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, HASHEDBLOCKSIZE, IV);
try { 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) { } catch (IOException e) {
if (e.getMessage().equals("Pipe closed")) { if (e.getMessage().equals("Pipe closed")) {
break; break;
@ -201,62 +131,76 @@ public class NUSDecryption extends AESDecryption {
e.printStackTrace(); e.printStackTrace();
throw e; throw e;
} }
wrote += writeSize;
block++; block++;
if (soffset > 0) { if (soffset > 0) {
writeSize = HASHBLOCKSIZE;
soffset = 0; soffset = 0;
} }
} while (wrote < filesize && (inBlockBuffer == BLOCKSIZE)); } while (wrote < size && (inBlockBuffer == BLOCKSIZE));
log.finest("Decryption okay");
} finally { } 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 { @Override
int hashSize = 0x400; public long readDecryptedContentToStreamNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, long payloadOffset, byte[] IV)
int blocksize = 0xFC00; 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 { try {
byte[] h3 = h3HashHashed.orElseThrow(() -> new FileNotFoundException("h3 hash not found.")); ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
decryptFileStreamHashed(inputStream, outputStream, offset, size, h3);
} finally { // We can only decrypt multiples of 16. So we need to align it.
StreamUtils.closeAll(inputStream, outputStream); 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;
} }
return true; byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV);
if (inBlockBuffer > 16) {
IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE);
} }
public boolean decryptStreamsNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Content content, byte[] IV, long writeLength = Math.min((output.length - writeOffset), (size - written));
boolean partial) throws IOException, CheckSumWrongException {
try { try {
byte[] h3Hash = content.getSHA2Hash(); read += inBlockBuffer;
// Ignore the h3hash if we don't read the whole file. outputStream.write(output, (int) writeOffset, (int) writeLength);
if (partial) { written += writeLength;
h3Hash = null; toRead -= writeLength;
} catch (IOException e) {
if (e.getMessage().equals("Pipe closed")) {
break;
} else {
throw e;
} }
decryptFileStream(inputStream, outputStream, offset, size, IV, h3Hash, content.getEncryptedFileSize()); }
if (written >= size) {
break;
}
} while (true);
} finally { } finally {
StreamUtils.closeAll(inputStream, outputStream); StreamUtils.closeAll(inputStream, outputStream);
} }
return written > 0 ? written : -1;
return true;
} }
} }

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;
}
}