mirror of
https://github.com/Maschell/JNUSLib.git
synced 2024-11-22 07:59:19 +01:00
Refactor the decryption
This commit is contained in:
parent
2604fe5eb9
commit
95039a42ef
21
.classpath
21
.classpath
@ -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>
|
||||||
|
2
pom.xml
2
pom.xml
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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 + "]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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++) {
|
||||||
|
@ -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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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."));
|
||||||
|
@ -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";
|
||||||
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,29 +68,39 @@ 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 {
|
||||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
if (tmdCache == null) {
|
||||||
|
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||||
|
|
||||||
long titleID = getTitleID();
|
long titleID = getTitleID();
|
||||||
int version = getVersion();
|
int version = getVersion();
|
||||||
|
|
||||||
byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
|
byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
|
||||||
|
|
||||||
if (res == null || res.length == 0) {
|
if (res == null || res.length == 0) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
tmdCache = Optional.of(res);
|
||||||
}
|
}
|
||||||
return Optional.of(res);
|
return tmdCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Optional<byte[]> ticketCache = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<byte[]> getRawTicket() throws IOException {
|
public Optional<byte[]> getRawTicket() throws IOException {
|
||||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
if (ticketCache == null) {
|
||||||
byte[] res = downloadService.downloadTicketToByteArray(titleID);
|
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||||
if (res == null || res.length == 0) {
|
byte[] res = downloadService.downloadTicketToByteArray(titleID);
|
||||||
return Optional.empty();
|
if (res == null || res.length == 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
ticketCache = Optional.of(res);
|
||||||
}
|
}
|
||||||
return Optional.of(res);
|
return ticketCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
111
src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProcessor.java
Normal file
111
src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProcessor.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
17
src/main/java/de/mas/wiiu/jnus/interfaces/TriFunction.java
Normal file
17
src/main/java/de/mas/wiiu/jnus/interfaces/TriFunction.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
38
src/main/java/de/mas/wiiu/jnus/utils/IVCache.java
Normal file
38
src/main/java/de/mas/wiiu/jnus/utils/IVCache.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
9
src/main/java/de/mas/wiiu/jnus/utils/Pair.java
Normal file
9
src/main/java/de/mas/wiiu/jnus/utils/Pair.java
Normal 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;
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
|
// 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 {
|
} finally {
|
||||||
StreamUtils.closeAll(inputStream, outputStream);
|
StreamUtils.closeAll(inputStream, outputStream);
|
||||||
}
|
}
|
||||||
|
return written > 0 ? written : -1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user