mirror of
https://github.com/Maschell/JNUSLib.git
synced 2024-11-21 23:49:17 +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"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="test"/>
|
||||
<classpathentry kind="src" path="src/main/java"/>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
@ -12,5 +23,11 @@
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
|
2
pom.xml
2
pom.xml
@ -11,7 +11,7 @@
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
|
@ -19,6 +19,7 @@ package de.mas.wiiu.jnus;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -52,14 +53,14 @@ public final class ExtractionService {
|
||||
}
|
||||
|
||||
private ExtractionService(NUSTitle nustitle) {
|
||||
if (nustitle.getDataProvider() instanceof Parallelizable) {
|
||||
if (nustitle.getDataProcessor().getDataProvider() instanceof Parallelizable) {
|
||||
parallelizable = true;
|
||||
}
|
||||
this.NUSTitle = nustitle;
|
||||
}
|
||||
|
||||
private NUSDataProvider getDataProvider() {
|
||||
return getNUSTitle().getDataProvider();
|
||||
return getNUSTitle().getDataProcessor().getDataProvider();
|
||||
}
|
||||
|
||||
public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException {
|
||||
@ -95,7 +96,7 @@ public final class ExtractionService {
|
||||
}
|
||||
}
|
||||
|
||||
public void extractEncryptedContentFilesTo(List<Content> list, String outputFolder, boolean withHashes) throws IOException {
|
||||
public void extractEncryptedContentFilesTo(Collection<Content> list, String outputFolder, boolean withHashes) throws IOException {
|
||||
Utils.createDir(outputFolder);
|
||||
if (parallelizable && Settings.ALLOW_PARALLELISATION) {
|
||||
try {
|
||||
|
@ -17,7 +17,6 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -27,10 +26,9 @@ import de.mas.wiiu.jnus.entities.TMD;
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
|
||||
public class NUSTitle {
|
||||
@ -39,12 +37,18 @@ public class NUSTitle {
|
||||
|
||||
@Getter private final TMD TMD;
|
||||
|
||||
@Getter private final NUSDataProvider dataProvider;
|
||||
@Getter private final NUSDataProcessor dataProcessor;
|
||||
|
||||
public NUSTitle(@NonNull NUSDataProvider dataProvider) throws ParseException, IOException {
|
||||
byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0));
|
||||
this.TMD = de.mas.wiiu.jnus.entities.TMD.parseTMD(tmdData);
|
||||
this.dataProvider = dataProvider;
|
||||
private NUSTitle(TMD tmd, NUSDataProcessor dataProcessor) {
|
||||
this.TMD = tmd;
|
||||
this.dataProcessor = dataProcessor;
|
||||
}
|
||||
|
||||
public static NUSTitle create(TMD tmd, NUSDataProcessor dataProcessor, Optional<Ticket> ticket, Optional<FST> fst) {
|
||||
NUSTitle result = new NUSTitle(tmd, dataProcessor);
|
||||
result.setTicket(ticket);
|
||||
result.setFST(fst);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Stream<FSTEntry> getAllFSTEntriesAsStream() {
|
||||
@ -66,13 +70,13 @@ public class NUSTitle {
|
||||
}
|
||||
|
||||
public void cleanup() throws IOException {
|
||||
if (getDataProvider() != null) {
|
||||
getDataProvider().cleanup();
|
||||
if (getDataProcessor() != null && getDataProcessor().getDataProvider() != null) {
|
||||
getDataProcessor().getDataProvider().cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NUSTitle [dataProvider=" + dataProvider + "]";
|
||||
return "NUSTitle [dataProcessor=" + dataProcessor + "]";
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/****************************************************************************
|
||||
* Copyright (C) 2016-2019 Maschell
|
||||
* Copyright (C) 2016-2020 Maschell
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -16,54 +16,78 @@
|
||||
****************************************************************************/
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.TMD;
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.fst.FST;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
||||
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
||||
import de.mas.wiiu.jnus.interfaces.ContentEncryptor;
|
||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.AESDecryption;
|
||||
import de.mas.wiiu.jnus.interfaces.TriFunction;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.NUSEncryption;
|
||||
|
||||
public class NUSTitleLoader {
|
||||
private NUSTitleLoader() {
|
||||
// should be empty
|
||||
}
|
||||
|
||||
public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier<NUSDataProvider> dataProviderFunction) throws IOException, ParseException {
|
||||
public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier<NUSDataProvider> dataProviderFunction,
|
||||
TriFunction<NUSDataProvider, Optional<ContentDecryptor>, Optional<ContentEncryptor>, NUSDataProcessor> dataProcessorFunction)
|
||||
throws IOException, ParseException {
|
||||
NUSDataProvider dataProvider = dataProviderFunction.get();
|
||||
|
||||
NUSTitle result = new NUSTitle(dataProvider);
|
||||
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD().orElseThrow(() -> new FileNotFoundException("No TMD data found")));
|
||||
|
||||
if (config.isNoDecryption()) {
|
||||
NUSTitle result = NUSTitle.create(tmd, dataProcessorFunction.apply(dataProvider, Optional.empty(), Optional.empty()), Optional.empty(),
|
||||
Optional.empty());
|
||||
return result;
|
||||
}
|
||||
|
||||
Ticket ticket = null;
|
||||
Optional<Ticket> ticket = Optional.empty();
|
||||
Optional<ContentDecryptor> decryption = Optional.empty();
|
||||
Optional<ContentEncryptor> encryption = Optional.empty();
|
||||
if (config.isTicketNeeded()) {
|
||||
ticket = config.getTicket();
|
||||
if (ticket == null) {
|
||||
Ticket ticketT = config.getTicket();
|
||||
if (ticketT == null) {
|
||||
Optional<byte[]> ticketOpt = dataProvider.getRawTicket();
|
||||
if (ticketOpt.isPresent()) {
|
||||
ticket = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey());
|
||||
ticketT = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey());
|
||||
}
|
||||
}
|
||||
if (ticket == null) {
|
||||
new ParseException("Failed to get ticket data", 0);
|
||||
if (ticketT == null) {
|
||||
throw new ParseException("Failed to get ticket data", 0);
|
||||
}
|
||||
|
||||
ticket = Optional.of(ticketT);
|
||||
|
||||
decryption = Optional.of(new NUSDecryption(ticketT));
|
||||
try {
|
||||
encryption = Optional.of(new NUSEncryption(ticketT));
|
||||
} catch (NoSuchProviderException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
result.setTicket(Optional.of(ticket));
|
||||
}
|
||||
|
||||
NUSDataProcessor dpp = dataProcessorFunction.apply(dataProvider, decryption, encryption);
|
||||
|
||||
// If we have just content, we don't have a FST.
|
||||
if (result.getTMD().getAllContents().size() == 1) {
|
||||
if (tmd.getAllContents().size() == 1) {
|
||||
// The only way to check if the key is right, is by trying to decrypt the whole thing.
|
||||
NUSTitle result = NUSTitle.create(tmd, dpp, ticket, Optional.empty());
|
||||
|
||||
FSTDataProvider dp = new FSTDataProviderNUSTitle(result);
|
||||
for (FSTEntry children : dp.getRoot().getChildren()) {
|
||||
dp.readFile(children);
|
||||
@ -72,27 +96,16 @@ public class NUSTitleLoader {
|
||||
return result;
|
||||
}
|
||||
// If we have more than one content, the index 0 is the FST.
|
||||
Content fstContent = result.getTMD().getContentByIndex(0);
|
||||
|
||||
InputStream fstContentEncryptedStream = dataProvider.readContentAsStream(fstContent);
|
||||
|
||||
byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());
|
||||
|
||||
if (fstContent.isEncrypted()) {
|
||||
AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]);
|
||||
if (fstBytes.length % 0x10 != 0) {
|
||||
throw new IOException("FST length is not align to 16");
|
||||
}
|
||||
fstBytes = aesDecryption.decrypt(fstBytes);
|
||||
}
|
||||
Content fstContent = tmd.getContentByIndex(0);
|
||||
|
||||
byte[] fstBytes = dpp.readPlainDecryptedContent(fstContent, true);
|
||||
FST fst = FST.parseFST(fstBytes);
|
||||
result.setFST(Optional.of(fst));
|
||||
|
||||
// The dataprovider may need the FST to calculate the offset of a content
|
||||
// on the partition.
|
||||
dataProvider.setFST(fst);
|
||||
|
||||
return result;
|
||||
return NUSTitle.create(tmd, dpp, ticket, Optional.of(fst));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderFST;
|
||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||
|
||||
@ -36,7 +37,7 @@ public final class NUSTitleLoaderFST {
|
||||
NUSTitleConfig config = new NUSTitleConfig();
|
||||
config.setCommonKey(commonKey);
|
||||
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderFST(dataProvider, base));
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderFST(dataProvider, base), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal;
|
||||
|
||||
public final class NUSTitleLoaderLocal {
|
||||
@ -43,7 +44,7 @@ public final class NUSTitleLoaderLocal {
|
||||
throw new IOException("Ticket was null and no commonKey was given");
|
||||
}
|
||||
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath));
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
package de.mas.wiiu.jnus;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup;
|
||||
|
||||
public final class NUSTitleLoaderLocalBackup {
|
||||
@ -35,7 +36,8 @@ public final class NUSTitleLoaderLocalBackup {
|
||||
config.setNoDecryption(true);
|
||||
}
|
||||
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion));
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion),
|
||||
(dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote;
|
||||
|
||||
public final class NUSTitleLoaderRemote {
|
||||
@ -53,7 +54,7 @@ public final class NUSTitleLoaderRemote {
|
||||
throw new IOException("Ticket was null and no commonKey was given");
|
||||
}
|
||||
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID));
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo;
|
||||
import de.mas.wiiu.jnus.implementations.woomy.WoomyParser;
|
||||
@ -41,7 +42,7 @@ public final class NUSTitleLoaderWoomy {
|
||||
|
||||
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.List;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD;
|
||||
@ -95,7 +96,7 @@ public final class WUDLoader {
|
||||
final NUSTitleConfig config = new NUSTitleConfig();
|
||||
config.setCommonKey(commonKey);
|
||||
gamePartition.getTmd();
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader));
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd));
|
||||
}
|
||||
|
||||
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WUDInfo wudInfo, byte[] commonKey) throws IOException, ParseException {
|
||||
|
@ -11,6 +11,7 @@ import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor;
|
||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle;
|
||||
import de.mas.wiiu.jnus.implementations.FSTDataProviderWumadDataPartition;
|
||||
import de.mas.wiiu.jnus.implementations.NUSDataProviderWumad;
|
||||
@ -40,7 +41,7 @@ public class WumadLoader {
|
||||
final NUSTitleConfig config = new NUSTitleConfig();
|
||||
config.setCommonKey(commonKey);
|
||||
gamePartition.getTmd();
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile));
|
||||
return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile), (dp, cd, ce) -> new DefaultNUSDataProcessor(dp, cd));
|
||||
}
|
||||
|
||||
public static List<FSTDataProvider> getPartitonsAsFSTDataProvider(@NonNull WumadInfo wumadInfo, byte[] commonKey) throws IOException, ParseException {
|
||||
@ -57,7 +58,6 @@ public class WumadLoader {
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ public class Content implements Comparable<Content> {
|
||||
public static final short CONTENT_FLAG_UNKWN1 = 0x4000;
|
||||
public static final short CONTENT_HASHED = 0x0002;
|
||||
public static final short CONTENT_ENCRYPTED = 0x0001;
|
||||
|
||||
public static final int CONTENT_SIZE = 0x30;
|
||||
|
||||
@Getter private final int ID;
|
||||
@ -127,7 +128,7 @@ public class Content implements Comparable<Content> {
|
||||
*/
|
||||
public long getDecryptedFileSize() {
|
||||
if (isHashed()) {
|
||||
return getEncryptedFileSize() / 0x10000 * 0xFC00;
|
||||
return (getEncryptedFileSize() / 0x10000) * 0xFC00;
|
||||
} else {
|
||||
return getEncryptedFileSize();
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ public final class FST {
|
||||
int fst_size = fileCount * 0x10;
|
||||
|
||||
int nameOff = fst_offset + fst_size;
|
||||
int nameSize = nameOff + 1;
|
||||
int nameSize = fstData.length - nameOff;
|
||||
|
||||
// Get list with null-terminated Strings. Ends with \0\0.
|
||||
for (int i = nameOff; i < fstData.length - 1; i++) {
|
||||
|
@ -104,7 +104,6 @@ public final class FSTService {
|
||||
while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) {
|
||||
j++;
|
||||
}
|
||||
|
||||
return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + j)));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.mas.wiiu.jnus.NUSTitle;
|
||||
@ -28,22 +25,20 @@ import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||
import de.mas.wiiu.jnus.interfaces.HasNUSTitle;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProcessor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
|
||||
private final NUSDataProcessor dataProcessor;
|
||||
private final NUSTitle title;
|
||||
private final FSTEntry rootEntry;
|
||||
@Getter @Setter private String name;
|
||||
|
||||
public FSTDataProviderNUSTitle(NUSTitle title) throws IOException {
|
||||
this.dataProcessor = title.getDataProcessor();
|
||||
this.title = title;
|
||||
this.name = String.format("%016X", title.getTMD().getTitleID());
|
||||
|
||||
@ -66,150 +61,18 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
||||
try {
|
||||
return decryptFSTEntryToStream(entry, out, offset, size);
|
||||
} catch (CheckSumWrongException | NoSuchAlgorithmException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean decryptFSTEntryToStreamHashed(FSTEntry entry, OutputStream outputStream, long offset, long size)
|
||||
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
|
||||
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
|
||||
|
||||
long payloadOffset = entry.getFileOffset() + offset;
|
||||
long streamOffset = payloadOffset;
|
||||
long streamFilesize = 0;
|
||||
|
||||
streamOffset = (payloadOffset / 0xFC00) * 0x10000;
|
||||
long offsetInBlock = payloadOffset - ((streamOffset / 0x10000) * 0xFC00);
|
||||
if (offsetInBlock + size < 0xFC00) {
|
||||
streamFilesize = 0x10000L;
|
||||
} else {
|
||||
long curVal = 0x10000;
|
||||
long missing = (size - (0xFC00 - offsetInBlock));
|
||||
|
||||
curVal += (missing / 0xFC00) * 0x10000;
|
||||
|
||||
if (missing % 0xFC00 > 0) {
|
||||
curVal += 0x10000;
|
||||
}
|
||||
|
||||
streamFilesize = curVal;
|
||||
}
|
||||
|
||||
NUSDataProvider dataProvider = title.getDataProvider();
|
||||
InputStream in = dataProvider.readContentAsStream(c, streamOffset, streamFilesize);
|
||||
|
||||
NUSDecryption nusdecryption = new NUSDecryption(title.getTicket().get());
|
||||
|
||||
return nusdecryption.decryptStreamsHashed(in, outputStream, payloadOffset, size, dataProvider.getContentH3Hash(c));
|
||||
}
|
||||
|
||||
private boolean decryptFSTEntryToStreamNonHashed(FSTEntry entry, OutputStream outputStream, long offset, long size)
|
||||
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
|
||||
|
||||
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
|
||||
|
||||
byte[] IV = new byte[0x10];
|
||||
IV[0] = (byte) ((c.getIndex() >> 8) & 0xFF);
|
||||
IV[1] = (byte) (c.getIndex() & 0xFF);
|
||||
|
||||
long payloadOffset = entry.getFileOffset() + offset;
|
||||
long streamOffset = payloadOffset;
|
||||
long streamFilesize = c.getEncryptedFileSize();
|
||||
|
||||
// if we have an offset we can't calculate the hash anymore
|
||||
// we need a new IV
|
||||
if (streamOffset > 0) {
|
||||
streamFilesize = size;
|
||||
|
||||
streamOffset -= 16;
|
||||
streamFilesize += 16;
|
||||
|
||||
// We need to get the current IV as soon as we get the InputStream.
|
||||
IV = null;
|
||||
}
|
||||
|
||||
NUSDataProvider dataProvider = title.getDataProvider();
|
||||
InputStream in = dataProvider.readContentAsStream(c, streamOffset, streamFilesize);
|
||||
|
||||
if (IV == null) {
|
||||
// If we read with an offset > 16 we need the previous 16 bytes because they are the IV.
|
||||
// The input stream has been prepared to start 16 bytes earlier on this case.
|
||||
int toRead = 16;
|
||||
byte[] data = new byte[toRead];
|
||||
int readTotal = 0;
|
||||
while (readTotal < toRead) {
|
||||
int res = in.read(data, readTotal, toRead - readTotal);
|
||||
if (res < 0) {
|
||||
// This should NEVER happen.
|
||||
throw new IOException();
|
||||
}
|
||||
readTotal += res;
|
||||
}
|
||||
IV = Arrays.copyOfRange(data, 0, toRead);
|
||||
}
|
||||
NUSDecryption nusdecryption = new NUSDecryption(title.getTicket().get());
|
||||
|
||||
return nusdecryption.decryptStreamsNonHashed(in, outputStream, payloadOffset, size, c, IV, size != entry.getFileSize());
|
||||
}
|
||||
|
||||
private boolean decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream, long offset, long size)
|
||||
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
|
||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
||||
if (entry.isNotInPackage()) {
|
||||
if (entry.isNotInPackage()) {
|
||||
log.info("Decryption not possible because the FSTEntry is not in this package");
|
||||
}
|
||||
outputStream.close();
|
||||
return false;
|
||||
}
|
||||
if (offset % 16 != 0) {
|
||||
throw new IOException("The offset for decryption need to be aligned to 16");
|
||||
out.close();
|
||||
return -1;
|
||||
}
|
||||
|
||||
Content c = title.getTMD().getContentByIndex(entry.getContentIndex());
|
||||
|
||||
try {
|
||||
if (c.isEncrypted()) {
|
||||
if (!title.getTicket().isPresent()) {
|
||||
log.info("Decryption not possible because no ticket was set.");
|
||||
outputStream.close();
|
||||
return false;
|
||||
}
|
||||
if (c.isHashed()) {
|
||||
return decryptFSTEntryToStreamHashed(entry, outputStream, offset, size);
|
||||
} else {
|
||||
return decryptFSTEntryToStreamNonHashed(entry, outputStream, offset, size);
|
||||
}
|
||||
} else {
|
||||
InputStream in = title.getDataProvider().readContentAsStream(c, offset, size);
|
||||
|
||||
try {
|
||||
StreamUtils.saveInputStreamToOutputStreamWithHash(in, outputStream, size, c.getSHA2Hash(), c.getEncryptedFileSize(),
|
||||
size != entry.getFileSize());
|
||||
return true;
|
||||
} finally {
|
||||
StreamUtils.closeAll(in, outputStream);
|
||||
}
|
||||
}
|
||||
} catch (CheckSumWrongException e) {
|
||||
if (c.isUNKNWNFlag1Set()) {
|
||||
log.info("Hash doesn't match. But file is optional. Don't worry.");
|
||||
} else {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Hash doesn't match").append(System.lineSeparator());
|
||||
sb.append("Detailed info:").append(System.lineSeparator());
|
||||
sb.append(entry).append(System.lineSeparator());
|
||||
sb.append(String.format("%016x", title.getTMD().getTitleID()));
|
||||
sb.append(e.getMessage() + " Calculated Hash: " + Utils.ByteArrayToString(e.getGivenHash()) + ", expected hash: "
|
||||
+ Utils.ByteArrayToString(e.getExpectedHash()));
|
||||
log.info(sb.toString());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getFileOffset(), size, size == entry.getFileSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -49,7 +49,7 @@ public class FSTDataProviderWUDDataPartition implements FSTDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
||||
ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex())
|
||||
.orElseThrow(() -> new IOException("Failed to find FSTInfo"));
|
||||
if (titleKey == null) {
|
||||
|
@ -32,15 +32,15 @@ public class FSTDataProviderWumadDataPartition implements FSTDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
||||
StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
|
||||
return true;
|
||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException {
|
||||
return StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException {
|
||||
ZipEntry zipEntry = zipFile.stream()
|
||||
.filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex()))).findFirst()
|
||||
.filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex())))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new FileNotFoundException());
|
||||
|
||||
InputStream in = zipFile.getInputStream(zipEntry);
|
||||
|
@ -26,6 +26,7 @@ import java.util.Optional;
|
||||
import de.mas.wiiu.jnus.Settings;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.entities.fst.FSTEntry;
|
||||
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
||||
import de.mas.wiiu.jnus.interfaces.FSTDataProvider;
|
||||
import de.mas.wiiu.jnus.interfaces.NUSDataProvider;
|
||||
import de.mas.wiiu.jnus.utils.FSTUtils;
|
||||
@ -39,12 +40,12 @@ public class NUSDataProviderFST implements NUSDataProvider {
|
||||
this.fstDataProvider = fstDataProvider;
|
||||
}
|
||||
|
||||
public NUSDataProviderFST(FSTDataProvider fstDataProvider) {
|
||||
public NUSDataProviderFST(FSTDataProvider fstDataProvider, ContentDecryptor decryptor) {
|
||||
this(fstDataProvider, fstDataProvider.getRoot());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
String filename = content.getFilename();
|
||||
Optional<FSTEntry> contentFileOpt = FSTUtils.getChildOfDirectory(base, filename);
|
||||
FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found."));
|
||||
|
@ -45,7 +45,7 @@ public final class NUSDataProviderLocal implements NUSDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
File filepath = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), content.getFilename());
|
||||
if (filepath == null || !filepath.exists()) {
|
||||
String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + content.getFilename() + "\", file does not exist";
|
||||
|
@ -48,7 +48,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
File filepath = new File(getFilePathOnDisk(content));
|
||||
if (!filepath.exists()) {
|
||||
throw new FileNotFoundException(filepath.getAbsolutePath() + " was not found.");
|
||||
@ -62,7 +62,9 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider {
|
||||
public Optional<byte[]> getContentH3Hash(Content content) throws IOException {
|
||||
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
|
||||
File h3File = new File(h3Path);
|
||||
|
||||
if (!h3File.exists()) {
|
||||
throw new FileNotFoundException(h3File.getAbsolutePath() + " was not found.");
|
||||
}
|
||||
return Optional.of(Files.readAllBytes(h3File.toPath()));
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size);
|
||||
}
|
||||
@ -56,7 +56,6 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
|
||||
if (resOpt == null) {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
String url = getRemoteURL(content) + Settings.H3_EXTENTION;
|
||||
System.out.println(url);
|
||||
|
||||
byte[] res = downloadService.downloadToByteArray(url);
|
||||
if (res == null || res.length == 0) {
|
||||
@ -69,29 +68,39 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable {
|
||||
return resOpt;
|
||||
}
|
||||
|
||||
Optional<byte[]> tmdCache = null;
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getRawTMD() throws IOException {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
if (tmdCache == null) {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
|
||||
long titleID = getTitleID();
|
||||
int version = getVersion();
|
||||
long titleID = getTitleID();
|
||||
int version = getVersion();
|
||||
|
||||
byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
|
||||
byte[] res = downloadService.downloadTMDToByteArray(titleID, version);
|
||||
|
||||
if (res == null || res.length == 0) {
|
||||
return Optional.empty();
|
||||
if (res == null || res.length == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
tmdCache = Optional.of(res);
|
||||
}
|
||||
return Optional.of(res);
|
||||
return tmdCache;
|
||||
}
|
||||
|
||||
Optional<byte[]> ticketCache = null;
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getRawTicket() throws IOException {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
byte[] res = downloadService.downloadTicketToByteArray(titleID);
|
||||
if (res == null || res.length == 0) {
|
||||
return Optional.empty();
|
||||
if (ticketCache == null) {
|
||||
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
|
||||
byte[] res = downloadService.downloadTicketToByteArray(titleID);
|
||||
if (res == null || res.length == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ticketCache = Optional.of(res);
|
||||
}
|
||||
return Optional.of(res);
|
||||
return ticketCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -56,7 +56,7 @@ public class NUSDataProviderWUD implements NUSDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException {
|
||||
WUDDiscReader discReader = getDiscReader();
|
||||
long offset = getOffsetInWUD(content) + fileOffsetBlock;
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
****************************************************************************/
|
||||
package de.mas.wiiu.jnus.implementations;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@ -46,7 +45,7 @@ public class NUSDataProviderWoomy implements NUSDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(@NonNull Content content, long offset, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(@NonNull Content content, long offset, long size) throws IOException {
|
||||
WoomyZipFile zipFile = getSharedWoomyZipFile();
|
||||
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
|
||||
if (entry == null) {
|
||||
|
@ -61,7 +61,7 @@ public class NUSDataProviderWumad implements NUSDataProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException {
|
||||
ZipEntry entry = files.values().stream().filter(e -> e.getName().startsWith("p" + partition.getPartitionName() + "."))
|
||||
.filter(e -> e.getName().endsWith(content.getFilename().toLowerCase())).findFirst().orElseThrow(() -> new FileNotFoundException());
|
||||
InputStream in = wumad.getInputStream(entry);
|
||||
|
@ -54,7 +54,7 @@ public abstract class WUDDiscReader {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public abstract boolean readEncryptedToStream(OutputStream out, long offset, long size) throws IOException;
|
||||
public abstract long readEncryptedToStream(OutputStream out, long offset, long size) throws IOException;
|
||||
|
||||
public InputStream readEncryptedToStream(long offset, long size) throws IOException {
|
||||
PipedOutputStream out = new PipedOutputStream();
|
||||
@ -72,6 +72,23 @@ public abstract class WUDDiscReader {
|
||||
return in;
|
||||
}
|
||||
|
||||
public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV,
|
||||
boolean useFixedIV) throws IOException {
|
||||
PipedOutputStream out = new PipedOutputStream();
|
||||
PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x8000);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
readDecryptedToOutputStream(out, offset, fileOffset, size, key, IV, useFixedIV);
|
||||
in.throwException(null);
|
||||
} catch (Exception e) {
|
||||
in.throwException(e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param readOffset
|
||||
@ -93,7 +110,7 @@ public abstract class WUDDiscReader {
|
||||
return decryptedChunk;
|
||||
}
|
||||
|
||||
public boolean readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV,
|
||||
public long readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV,
|
||||
boolean useFixedIV) throws IOException {
|
||||
byte[] usedIV = null;
|
||||
if (useFixedIV) {
|
||||
@ -153,7 +170,7 @@ public abstract class WUDDiscReader {
|
||||
StreamUtils.closeAll(outputStream);
|
||||
}
|
||||
|
||||
return totalread >= size;
|
||||
return totalread;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,7 +35,7 @@ public class WUDDiscReaderCompressed extends WUDDiscReader {
|
||||
* Expects the .wux format by Exzap. You can more infos about it here. https://gbatemp.net/threads/wii-u-image-wud-compression-tool.397901/
|
||||
*/
|
||||
@Override
|
||||
public boolean readEncryptedToStream(OutputStream out, long offset, long size) throws IOException {
|
||||
public long readEncryptedToStream(OutputStream out, long offset, long size) throws IOException {
|
||||
// make sure there is no out-of-bounds read
|
||||
WUDImageCompressedInfo info = getImage().getCompressedInfo();
|
||||
|
||||
@ -91,6 +91,6 @@ public class WUDDiscReaderCompressed extends WUDDiscReader {
|
||||
} finally {
|
||||
StreamUtils.closeAll(input, out);
|
||||
}
|
||||
return usedSize == 0;
|
||||
return size - usedSize;
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
|
||||
public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
|
||||
RandomAccessFile input = getFileByOffset(offset);
|
||||
|
||||
int bufferSize = 0x8000;
|
||||
@ -86,7 +86,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader {
|
||||
|
||||
input.close();
|
||||
outputStream.close();
|
||||
return totalread >= size;
|
||||
return totalread;
|
||||
}
|
||||
|
||||
private int getFilePartByOffset(long offset) {
|
||||
|
@ -31,7 +31,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
|
||||
public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException {
|
||||
|
||||
FileInputStream input = new FileInputStream(getImage().getFileHandle());
|
||||
|
||||
@ -62,7 +62,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader {
|
||||
} while (totalread < size);
|
||||
input.close();
|
||||
outputStream.close();
|
||||
return totalread >= size;
|
||||
return totalread;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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);
|
||||
} catch (Exception e) {
|
||||
in.throwException(e);
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
default public boolean readFileToStream(OutputStream out, FSTEntry entry) throws IOException {
|
||||
default public long readFileToStream(OutputStream out, FSTEntry entry) throws IOException {
|
||||
return readFileToStream(out, entry, 0, entry.getFileSize());
|
||||
}
|
||||
|
||||
default public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException {
|
||||
default public long readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException {
|
||||
return readFileToStream(out, entry, offset, entry.getFileSize());
|
||||
}
|
||||
|
||||
public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException;
|
||||
public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException;
|
||||
|
||||
}
|
||||
|
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;
|
||||
|
||||
public interface NUSDataProvider {
|
||||
default public byte[] readContent(Content content, long offset, int size) throws IOException {
|
||||
return StreamUtils.getBytesFromStream(readContentAsStream(content, offset, size), size);
|
||||
default byte[] readRawContent(Content content, long offset, int size) throws IOException {
|
||||
return StreamUtils.getBytesFromStream(readRawContentAsStream(content, offset, size), size);
|
||||
}
|
||||
|
||||
default public InputStream readContentAsStream(Content content) throws IOException {
|
||||
return readContentAsStream(content, 0);
|
||||
default InputStream readRawContentAsStream(Content content) throws IOException {
|
||||
return readRawContentAsStream(content, 0);
|
||||
}
|
||||
|
||||
default public InputStream readContentAsStream(Content content, long offset) throws IOException {
|
||||
return readContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset);
|
||||
default InputStream readRawContentAsStream(Content content, long offset) throws IOException {
|
||||
return readRawContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset);
|
||||
}
|
||||
|
||||
public InputStream readContentAsStream(Content content, long offset, long size) throws IOException;
|
||||
public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException;
|
||||
|
||||
public Optional<byte[]> getContentH3Hash(Content content) throws IOException;
|
||||
|
||||
@ -53,5 +53,4 @@ public interface NUSDataProvider {
|
||||
default public void setFST(FST fst) {
|
||||
|
||||
}
|
||||
|
||||
}
|
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -98,4 +100,10 @@ public final class ByteUtils {
|
||||
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);
|
||||
InputStream inputStream = dataProvider.readContentAsStream(content);
|
||||
InputStream inputStream = dataProvider.readRawContentAsStream(content);
|
||||
if (inputStream == null) {
|
||||
log.warning(content.getFilename() + " Couldn't save encrypted content. Input stream was null");
|
||||
return;
|
||||
|
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) {
|
||||
System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer());
|
||||
inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer();
|
||||
} else {
|
||||
if (inBlockBuffer == 0) {
|
||||
return bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -124,19 +127,21 @@ public final class StreamUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
|
||||
public static long saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
|
||||
try {
|
||||
saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true);
|
||||
return saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true);
|
||||
} catch (CheckSumWrongException e) {
|
||||
// Should never happen because the hash is not set. Lets print it anyway.
|
||||
e.printStackTrace();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash,
|
||||
public static long saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash,
|
||||
long expectedSizeForHash, boolean partial) throws IOException, CheckSumWrongException {
|
||||
synchronized (inputStream) {
|
||||
long written = 0;
|
||||
|
||||
synchronized (inputStream) {
|
||||
MessageDigest sha1 = null;
|
||||
if (hash != null && !partial) {
|
||||
try {
|
||||
@ -150,7 +155,6 @@ public final class StreamUtils {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int read = 0;
|
||||
long totalRead = 0;
|
||||
long written = 0;
|
||||
|
||||
try {
|
||||
do {
|
||||
@ -189,6 +193,7 @@ public final class StreamUtils {
|
||||
StreamUtils.closeAll(inputStream, outputStream);
|
||||
}
|
||||
}
|
||||
return written > 0 ? written : -1;
|
||||
}
|
||||
|
||||
public static void skipExactly(InputStream in, long offset) throws IOException {
|
||||
|
@ -23,12 +23,14 @@ import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Arrays;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogManager;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
||||
@ -238,4 +240,31 @@ public final class Utils {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Long getLastModifiedURL(HttpURLConnection connectionForURL, int timeout) throws IOException {
|
||||
HttpURLConnection connection = connectionForURL;
|
||||
connection.setRequestProperty("User-Agent", Settings.USER_AGENT);
|
||||
connection.setConnectTimeout(timeout);
|
||||
connection.setReadTimeout(timeout);
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
if (responseCode == HttpsURLConnection.HTTP_OK) {
|
||||
InputStream inputStream = connection.getInputStream();
|
||||
byte[] buffer = new byte[0x10];
|
||||
inputStream.read(buffer);
|
||||
inputStream.close();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
Long dateTime = connection.getLastModified();
|
||||
|
||||
if (200 <= responseCode && responseCode <= 399) {
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Synchronized;
|
||||
|
||||
public class AESDecryption {
|
||||
private Cipher cipher;
|
||||
@ -51,6 +52,7 @@ public class AESDecryption {
|
||||
init(getAESKey(), getIV());
|
||||
}
|
||||
|
||||
@Synchronized("cipher")
|
||||
protected void init(byte[] decryptedKey, byte[] iv) {
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
|
||||
try {
|
||||
@ -61,6 +63,7 @@ public class AESDecryption {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized("cipher")
|
||||
public byte[] decrypt(byte[] input) {
|
||||
try {
|
||||
return cipher.doFinal(input);
|
||||
@ -75,6 +78,7 @@ public class AESDecryption {
|
||||
return decrypt(input, 0, len);
|
||||
}
|
||||
|
||||
@Synchronized("cipher")
|
||||
public byte[] decrypt(byte[] input, int offset, int len) {
|
||||
try {
|
||||
return cipher.doFinal(input, offset, len);
|
||||
|
@ -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;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.mas.wiiu.jnus.entities.Ticket;
|
||||
import de.mas.wiiu.jnus.entities.content.Content;
|
||||
import de.mas.wiiu.jnus.interfaces.ContentDecryptor;
|
||||
import de.mas.wiiu.jnus.utils.ByteArrayBuffer;
|
||||
import de.mas.wiiu.jnus.utils.CheckSumWrongException;
|
||||
import de.mas.wiiu.jnus.utils.HashUtil;
|
||||
import de.mas.wiiu.jnus.utils.StreamUtils;
|
||||
import de.mas.wiiu.jnus.utils.Utils;
|
||||
import lombok.extern.java.Log;
|
||||
|
||||
@Log
|
||||
public class NUSDecryption extends AESDecryption {
|
||||
public class NUSDecryption extends AESDecryption implements ContentDecryptor {
|
||||
public NUSDecryption(byte[] AESKey, byte[] IV) {
|
||||
super(AESKey, IV);
|
||||
}
|
||||
@ -56,144 +48,82 @@ public class NUSDecryption extends AESDecryption {
|
||||
return decrypt(blockBuffer, offset, BLOCKSIZE);
|
||||
}
|
||||
|
||||
public void decryptFileStream(InputStream inputStream, OutputStream outputStream, long fileOffset, long filesize, byte[] IV, byte[] h3hash,
|
||||
long expectedSizeForHash) throws IOException, CheckSumWrongException {
|
||||
MessageDigest sha1 = null;
|
||||
MessageDigest sha1fallback = null;
|
||||
|
||||
if (h3hash != null) {
|
||||
try {
|
||||
sha1 = MessageDigest.getInstance("SHA1");
|
||||
sha1fallback = MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
int BLOCKSIZE = 0x8000;
|
||||
|
||||
byte[] blockBuffer = new byte[BLOCKSIZE];
|
||||
|
||||
int inBlockBuffer;
|
||||
long written = 0;
|
||||
long writtenFallback = 0;
|
||||
|
||||
try {
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
// We can only decrypt multiples of 16. So we need to align it.
|
||||
long toRead = Utils.align(filesize, 16);
|
||||
|
||||
do {
|
||||
|
||||
int curReadSize = BLOCKSIZE;
|
||||
if (toRead < BLOCKSIZE) {
|
||||
curReadSize = (int) toRead;
|
||||
}
|
||||
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, curReadSize);
|
||||
|
||||
byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV);
|
||||
|
||||
if (inBlockBuffer == BLOCKSIZE) {
|
||||
IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE);
|
||||
}
|
||||
|
||||
int toWrite = inBlockBuffer;
|
||||
|
||||
if ((written + inBlockBuffer) > filesize) {
|
||||
toWrite = (int) (filesize - written);
|
||||
}
|
||||
|
||||
written += toWrite;
|
||||
toRead -= toWrite;
|
||||
|
||||
outputStream.write(output, 0, toWrite);
|
||||
|
||||
if (sha1 != null && sha1fallback != null) {
|
||||
sha1.update(output, 0, toWrite);
|
||||
|
||||
// In some cases it's using the hash of the whole .app file instead of the part
|
||||
// that's been actually used.
|
||||
long toFallback = inBlockBuffer;
|
||||
if (writtenFallback + toFallback > expectedSizeForHash) {
|
||||
toFallback = expectedSizeForHash - writtenFallback;
|
||||
}
|
||||
sha1fallback.update(output, 0, (int) toFallback);
|
||||
writtenFallback += toFallback;
|
||||
}
|
||||
|
||||
if (written >= filesize && h3hash == null) {
|
||||
break;
|
||||
}
|
||||
} while (inBlockBuffer == BLOCKSIZE);
|
||||
|
||||
if (sha1 != null && sha1fallback != null) {
|
||||
long missingInHash = expectedSizeForHash - writtenFallback;
|
||||
if (missingInHash > 0) {
|
||||
sha1fallback.update(new byte[(int) missingInHash]);
|
||||
}
|
||||
|
||||
byte[] calculated_hash1 = sha1.digest();
|
||||
byte[] calculated_hash2 = sha1fallback.digest();
|
||||
byte[] expected_hash = h3hash;
|
||||
if (!Arrays.equals(calculated_hash1, expected_hash) && !Arrays.equals(calculated_hash2, expected_hash)) {
|
||||
throw new CheckSumWrongException("hash checksum failed ", calculated_hash1, expected_hash);
|
||||
} else {
|
||||
log.finest("Hash DOES match saves output stream.");
|
||||
}
|
||||
|
||||
}
|
||||
} finally {
|
||||
StreamUtils.closeAll(inputStream, outputStream);
|
||||
}
|
||||
if (written < filesize) {
|
||||
throw new IOException("Failed to read. Missing " + (filesize - written));
|
||||
}
|
||||
}
|
||||
|
||||
public void decryptFileStreamHashed(InputStream inputStream, OutputStream outputStream, long fileoffset, long filesize, byte[] h3Hash)
|
||||
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
|
||||
@Override
|
||||
public long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes)
|
||||
throws IOException {
|
||||
int BLOCKSIZE = 0x10000;
|
||||
int HASHBLOCKSIZE = 0xFC00;
|
||||
int HASHEDBLOCKSIZE = 0xFC00;
|
||||
int HASHSIZE = BLOCKSIZE - HASHEDBLOCKSIZE;
|
||||
|
||||
long writeSize = HASHBLOCKSIZE;
|
||||
long block = (offset / BLOCKSIZE);
|
||||
long writeSize = BLOCKSIZE;
|
||||
|
||||
long block = (fileoffset / HASHBLOCKSIZE);
|
||||
long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
|
||||
|
||||
if (soffset + filesize > writeSize) {
|
||||
writeSize = writeSize - soffset;
|
||||
|
||||
}
|
||||
long soffset = payloadOffset;
|
||||
|
||||
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
long wrote = 0;
|
||||
int inBlockBuffer = 0;
|
||||
|
||||
try {
|
||||
do {
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, encryptedBlockBuffer, overflow, BLOCKSIZE);
|
||||
if (writeSize > filesize) writeSize = filesize;
|
||||
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(in, encryptedBlockBuffer, overflow, BLOCKSIZE);
|
||||
if (inBlockBuffer < 0) {
|
||||
return wrote;
|
||||
}
|
||||
if (inBlockBuffer != BLOCKSIZE) {
|
||||
|
||||
throw new IOException("wasn't able to read " + BLOCKSIZE);
|
||||
}
|
||||
|
||||
byte[] output;
|
||||
try {
|
||||
output = decryptFileChunkHash(encryptedBlockBuffer, (int) block, h3Hash);
|
||||
} catch (CheckSumWrongException | NoSuchAlgorithmException e) {
|
||||
throw e;
|
||||
}
|
||||
byte[] hashes = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, new byte[16]);
|
||||
|
||||
if ((wrote + writeSize) > filesize) {
|
||||
writeSize = (int) (filesize - wrote);
|
||||
}
|
||||
int H0_start = (int) (((int) block % 16) * 20);
|
||||
|
||||
byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16);
|
||||
byte[] output = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, HASHEDBLOCKSIZE, IV);
|
||||
|
||||
try {
|
||||
outputStream.write(output, (int) (0 + soffset), (int) writeSize);
|
||||
if (writeSize > size) {
|
||||
writeSize = size;
|
||||
}
|
||||
if (writeSize + wrote > size) {
|
||||
writeSize = size - wrote;
|
||||
}
|
||||
|
||||
long toBeWritten = writeSize;
|
||||
|
||||
if (soffset <= HASHSIZE) {
|
||||
long writeHashSize = HASHSIZE;
|
||||
if (writeSize < HASHSIZE) {
|
||||
writeHashSize = writeSize;
|
||||
}
|
||||
if (writeHashSize + soffset > HASHSIZE) {
|
||||
writeHashSize = HASHSIZE - soffset;
|
||||
}
|
||||
out.write(hashes, (int) (0 + soffset), (int) writeHashSize);
|
||||
wrote += writeHashSize;
|
||||
toBeWritten -= writeHashSize;
|
||||
|
||||
if (toBeWritten > 0) {
|
||||
if (toBeWritten > HASHEDBLOCKSIZE) {
|
||||
toBeWritten = HASHEDBLOCKSIZE;
|
||||
writeSize = toBeWritten - HASHEDBLOCKSIZE;
|
||||
}
|
||||
out.write(output, 0, (int) toBeWritten);
|
||||
wrote += toBeWritten;
|
||||
}
|
||||
} else {
|
||||
soffset -= 0x400;
|
||||
long writeThisTime = writeSize;
|
||||
if (writeSize + soffset > HASHEDBLOCKSIZE) {
|
||||
writeThisTime = HASHEDBLOCKSIZE - soffset;
|
||||
}
|
||||
out.write(output, (int) (0 + soffset), (int) writeThisTime);
|
||||
wrote += writeThisTime;
|
||||
}
|
||||
writeSize = BLOCKSIZE;
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage().equals("Pipe closed")) {
|
||||
break;
|
||||
@ -201,62 +131,76 @@ public class NUSDecryption extends AESDecryption {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
wrote += writeSize;
|
||||
|
||||
block++;
|
||||
|
||||
if (soffset > 0) {
|
||||
writeSize = HASHBLOCKSIZE;
|
||||
soffset = 0;
|
||||
}
|
||||
} while (wrote < filesize && (inBlockBuffer == BLOCKSIZE));
|
||||
log.finest("Decryption okay");
|
||||
} while (wrote < size && (inBlockBuffer == BLOCKSIZE));
|
||||
} finally {
|
||||
StreamUtils.closeAll(inputStream, outputStream);
|
||||
StreamUtils.closeAll(in, out);
|
||||
}
|
||||
return wrote > 0 ? wrote : -1;
|
||||
}
|
||||
|
||||
private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, byte[] h3_hashes) throws CheckSumWrongException, NoSuchAlgorithmException {
|
||||
int hashSize = 0x400;
|
||||
int blocksize = 0xFC00;
|
||||
@Override
|
||||
public long readDecryptedContentToStreamNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, long payloadOffset, byte[] IV)
|
||||
throws IOException {
|
||||
int BLOCKSIZE = 0x80000;
|
||||
|
||||
byte[] hashes = decryptFileChunk(blockBuffer, hashSize, new byte[16]);
|
||||
byte[] blockBuffer = new byte[BLOCKSIZE];
|
||||
|
||||
int H0_start = (block % 16) * 20;
|
||||
int inBlockBuffer;
|
||||
long written = 0;
|
||||
long read = 0;
|
||||
|
||||
byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16);
|
||||
byte[] output = decryptFileChunk(blockBuffer, hashSize, blocksize, IV);
|
||||
|
||||
HashUtil.checkFileChunkHashes(hashes, h3_hashes, output, block);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public boolean decryptStreamsHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Optional<byte[]> h3HashHashed)
|
||||
throws IOException, CheckSumWrongException, NoSuchAlgorithmException {
|
||||
try {
|
||||
byte[] h3 = h3HashHashed.orElseThrow(() -> new FileNotFoundException("h3 hash not found."));
|
||||
decryptFileStreamHashed(inputStream, outputStream, offset, size, h3);
|
||||
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
|
||||
|
||||
// We can only decrypt multiples of 16. So we need to align it.
|
||||
long toRead = Utils.align(size, 16);
|
||||
|
||||
do {
|
||||
long writeOffset = Math.max(0, payloadOffset - read);
|
||||
int curReadSize = BLOCKSIZE;
|
||||
if (toRead < BLOCKSIZE) {
|
||||
curReadSize = (int) (toRead + writeOffset);
|
||||
}
|
||||
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, (int) Utils.align(curReadSize, 16));
|
||||
|
||||
if (inBlockBuffer < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV);
|
||||
|
||||
if (inBlockBuffer > 16) {
|
||||
IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE);
|
||||
}
|
||||
|
||||
long writeLength = Math.min((output.length - writeOffset), (size - written));
|
||||
|
||||
try {
|
||||
read += inBlockBuffer;
|
||||
outputStream.write(output, (int) writeOffset, (int) writeLength);
|
||||
written += writeLength;
|
||||
toRead -= writeLength;
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage().equals("Pipe closed")) {
|
||||
break;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (written >= size) {
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
} finally {
|
||||
StreamUtils.closeAll(inputStream, outputStream);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean decryptStreamsNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Content content, byte[] IV,
|
||||
boolean partial) throws IOException, CheckSumWrongException {
|
||||
try {
|
||||
byte[] h3Hash = content.getSHA2Hash();
|
||||
// Ignore the h3hash if we don't read the whole file.
|
||||
if (partial) {
|
||||
h3Hash = null;
|
||||
}
|
||||
decryptFileStream(inputStream, outputStream, offset, size, IV, h3Hash, content.getEncryptedFileSize());
|
||||
} finally {
|
||||
StreamUtils.closeAll(inputStream, outputStream);
|
||||
}
|
||||
|
||||
return true;
|
||||
return written > 0 ? written : -1;
|
||||
}
|
||||
}
|
||||
|
@ -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