diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..140ab94
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ae3c172
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/bin/
diff --git a/.project b/.project
new file mode 100644
index 0000000..97d02eb
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+
+
+ JNUSLib
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/libs/lombok.jar b/libs/lombok.jar
new file mode 100644
index 0000000..23cc160
Binary files /dev/null and b/libs/lombok.jar differ
diff --git a/src/de/mas/jnus/lib/DecryptionService.java b/src/de/mas/jnus/lib/DecryptionService.java
new file mode 100644
index 0000000..7e86491
--- /dev/null
+++ b/src/de/mas/jnus/lib/DecryptionService.java
@@ -0,0 +1,336 @@
+package de.mas.jnus.lib;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.mas.jnus.lib.entities.TMD;
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.entities.fst.FSTEntry;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+import de.mas.jnus.lib.utils.HashUtil;
+import de.mas.jnus.lib.utils.StreamUtils;
+import de.mas.jnus.lib.utils.Utils;
+import de.mas.jnus.lib.utils.cryptography.NUSDecryption;
+import lombok.Getter;
+import lombok.Setter;
+
+public class DecryptionService {
+ private static Map instances = new HashMap<>();
+
+ public static DecryptionService getInstance(NUSTitle nustitle) {
+ if(!instances.containsKey(nustitle)){
+ instances.put(nustitle, new DecryptionService(nustitle));
+ }
+ return instances.get(nustitle);
+ }
+
+ @Getter @Setter private NUSTitle NUSTitle = null;
+
+ private DecryptionService(NUSTitle nustitle){
+ setNUSTitle(nustitle);
+ }
+
+ public Ticket getTicket() {
+ return getNUSTitle().getTicket();
+ }
+
+ public void decryptFSTEntryTo(boolean useFullPath,FSTEntry entry, String outputPath, boolean skipExistingFile) throws IOException {
+ if(entry.isNotInPackage() || entry.getContent() == null){
+ return;
+ }
+
+ String targetFilePath = new StringBuilder().append(outputPath).append("/").append(entry.getFilename()).toString();
+ String fullPath = new StringBuilder().append(outputPath).toString();
+
+ if(useFullPath){
+ targetFilePath = new StringBuilder().append(outputPath).append(entry.getFullPath()).toString();
+ fullPath = new StringBuilder().append(outputPath).append(entry.getPath()).toString();
+ if(entry.isDir()){ //If the entry is a directory. Create it and return.
+ Utils.createDir(targetFilePath);
+ return;
+ }
+ }else if(entry.isDir()){
+ return;
+ }
+
+ if(!Utils.createDir(fullPath)){
+ return;
+ }
+
+ File target = new File(targetFilePath);
+
+ if(skipExistingFile){
+ File targetFile = new File(targetFilePath);
+ if(targetFile.exists()){
+ if(entry.isDir()){
+ return;
+ }
+ if(targetFile.length() == entry.getFileSize()){
+ Content c = entry.getContent();
+ if(!c.isHashed()){
+ if(Arrays.equals(HashUtil.hashSHA1(target,(int) c.getEncryptedFileSize()), c.getSHA2Hash())){
+ System.out.println("File already exists: " + entry.getFilename());
+ return;
+ }else{
+ System.out.println("File already exists with the same filesize, but the hash doesn't match: " + entry.getFilename());
+ }
+ }else{
+ System.out.println("File already exists: " + entry.getFilename());
+ return;
+ }
+
+ }else{
+ System.out.println("File already exists but the filesize doesn't match: " + entry.getFilename());
+ }
+ }
+ }
+
+ FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath));
+
+ System.out.println("Decrypting " + entry.getFilename());
+ decryptFSTEntryToStream(entry, outputStream);
+ }
+
+ public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException {
+ if(entry.isNotInPackage() || entry.getContent() == null){
+ return;
+ }
+
+ Content c = entry.getContent();
+
+ long fileSize = entry.getFileSize();
+ long fileOffset = entry.getFileOffset();
+ long fileOffsetBlock = entry.getFileOffsetBlock();
+
+ NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
+ InputStream in = dataProvider.getInputStreamFromContent(c, fileOffsetBlock);
+
+ decryptFSTEntryFromStreams(in,outputStream,fileSize,fileOffset,c);
+ }
+
+
+ private void decryptFSTEntryFromStreams(InputStream inputStream, OutputStream outputStream,long filesize, long fileoffset, Content content) throws IOException{
+ decryptStreams(inputStream, outputStream, filesize, fileoffset, content);
+ }
+
+ private void decryptContentFromStream(InputStream inputStream, OutputStream outputStream,Content content) throws IOException {
+ long filesize = content.getDecryptedFileSize();
+ System.out.println("Decrypting Content " + String.format("%08X", content.getID()));
+ decryptStreams(inputStream, outputStream, filesize, 0L, content);
+ }
+
+
+ private void decryptStreams(InputStream inputStream, OutputStream outputStream,long size, long offset, Content content) throws IOException{
+ NUSDecryption nusdecryption = new NUSDecryption(getTicket());
+ short contentIndex = (short)content.getIndex();
+
+ long encryptedFileSize = content.getEncryptedFileSize();
+ if(!content.isEncrypted()){
+ StreamUtils.saveInputStreamToOutputStreamWithHash(inputStream, outputStream,size,content.getSHA2Hash(),encryptedFileSize);
+ }else{
+ if(content.isHashed()){
+ NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
+ byte[] h3 = dataProvider.getContentH3Hash(content);
+ nusdecryption.decryptFileStreamHashed(inputStream, outputStream, size, offset, (short) contentIndex, h3);
+ }else{
+ nusdecryption.decryptFileStream(inputStream, outputStream, size, (short)contentIndex,content.getSHA2Hash(),encryptedFileSize);
+ }
+ }
+
+ inputStream.close();
+ outputStream.close();
+ }
+
+
+ public void decryptContentTo(Content content,String outPath,boolean skipExistingFile) throws IOException {
+ String targetFilePath = outPath + File.separator + content.getFilenameDecrypted();
+ if(skipExistingFile){
+ File targetFile = new File(targetFilePath);
+ if(targetFile.exists()){
+ if(targetFile.length() == content.getDecryptedFileSize()){
+ System.out.println("File already exists : " + content.getFilenameDecrypted());
+ return;
+ }else{
+ System.out.println("File already exists but the filesize doesn't match: " +content.getFilenameDecrypted());
+ }
+ }
+ }
+
+ if(!Utils.createDir(outPath)){
+ return;
+ }
+
+ System.out.println("Decrypting Content " + String.format("%08X", content.getID()));
+
+ FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath));
+
+ decryptContentToStream(content, outputStream);
+ }
+
+
+ public void decryptContentToStream(Content content, OutputStream outputStream) throws IOException {
+ if(content == null){
+ return;
+ }
+
+ NUSDataProvider dataProvider = getNUSTitle().getDataProvider();
+ InputStream inputStream = dataProvider.getInputStreamFromContent(content, 0);
+
+ decryptContentFromStream(inputStream,outputStream,content);
+ }
+
+ public InputStream getDecryptedOutputAsInputStream(FSTEntry fstEntry) throws IOException {
+ PipedInputStream in = new PipedInputStream();
+ PipedOutputStream out = new PipedOutputStream(in);
+
+ new Thread(() -> {try {
+ decryptFSTEntryToStream(fstEntry, out);
+ } catch (IOException e) {e.printStackTrace();}}).start();
+
+ return in;
+ }
+ public InputStream getDecryptedContentAsInputStream(Content content) throws IOException {
+ PipedInputStream in = new PipedInputStream();
+ PipedOutputStream out = new PipedOutputStream(in);
+
+ new Thread(() -> {try {
+ decryptContentToStream(content, out);
+ } catch (IOException e) {e.printStackTrace();}}).start();
+
+ return in;
+ }
+
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ //Decrypt FSTEntry to OutputStream
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ public void decryptFSTEntryTo(String entryFullPath,OutputStream outputStream) throws IOException{
+ FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath);
+ if(entry == null){
+ System.out.println("File not found");
+ }
+
+ decryptFSTEntryToStream(entry, outputStream);
+ }
+
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ //Decrypt single FSTEntry to File
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ public void decryptFSTEntryTo(String entryFullPath,String outputFolder) throws IOException{
+ decryptFSTEntryTo(false,entryFullPath,outputFolder);
+ }
+
+ public void decryptFSTEntryTo(boolean fullPath,String entryFullPath,String outputFolder) throws IOException{
+ decryptFSTEntryTo(fullPath,entryFullPath,outputFolder,getNUSTitle().isSkipExistingFiles());
+ }
+
+ public void decryptFSTEntryTo(String entryFullPath,String outputFolder, boolean skipExistingFiles) throws IOException{
+ decryptFSTEntryTo(false,entryFullPath,outputFolder,getNUSTitle().isSkipExistingFiles());
+ }
+
+ public void decryptFSTEntryTo(boolean fullPath, String entryFullPath,String outputFolder, boolean skipExistingFiles) throws IOException{
+ FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath);
+ if(entry == null){
+ System.out.println("File not found");
+ return;
+ }
+
+ decryptFSTEntryTo(fullPath,entry, outputFolder,skipExistingFiles);
+ }
+
+ public void decryptFSTEntryTo(FSTEntry entry,String outputFolder) throws IOException{
+ decryptFSTEntryTo(false,entry, outputFolder);
+ }
+ public void decryptFSTEntryTo(boolean fullPath,FSTEntry entry,String outputFolder) throws IOException{
+ decryptFSTEntryTo(fullPath,entry,outputFolder,getNUSTitle().isSkipExistingFiles());
+ }
+
+ public void decryptFSTEntryTo(FSTEntry entry,String outputFolder, boolean skipExistingFiles) throws IOException{
+ decryptFSTEntryTo(false,entry,outputFolder,getNUSTitle().isSkipExistingFiles());
+ }
+
+ /*
+ public void decryptFSTEntryTo(boolean fullPath, FSTEntry entry,String outputFolder, boolean skipExistingFiles) throws IOException{
+ decryptFSTEntry(fullPath,entry,outputFolder,skipExistingFiles);
+ }*/
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ //Decrypt list of FSTEntry to Files
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ public void decryptAllFSTEntriesTo(String outputFolder) throws IOException {
+ decryptFSTEntriesTo(true, ".*", outputFolder);
+ }
+
+ public void decryptFSTEntriesTo(String regEx, String outputFolder) throws IOException {
+ decryptFSTEntriesTo(true,regEx, outputFolder);
+ }
+ public void decryptFSTEntriesTo(boolean fullPath,String regEx, String outputFolder) throws IOException {
+ List files = getNUSTitle().getAllFSTEntriesFlat();
+ Pattern p = Pattern.compile(regEx);
+
+ List result = new ArrayList<>();
+
+ for(FSTEntry f :files){
+ String match = f.getFullPath().replaceAll("\\\\", "/");
+ Matcher m = p.matcher(match);
+ if(m.matches()){
+ result.add(f);
+ }
+ }
+
+ decryptFSTEntryListTo(fullPath,result, outputFolder);
+ }
+ public void decryptFSTEntryListTo(List list,String outputFolder) throws IOException {
+ decryptFSTEntryListTo(true,list,outputFolder);
+ }
+ public void decryptFSTEntryListTo(boolean fullPath,List list,String outputFolder) throws IOException {
+ for(FSTEntry entry: list){
+ decryptFSTEntryTo(fullPath,entry, outputFolder, getNUSTitle().isSkipExistingFiles());
+ }
+ }
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ //Save decrypted contents
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ public void decryptPlainContentByID(int ID,String outputFolder) throws IOException {
+ decryptPlainContent(getTMDFromNUSTitle().getContentByID(ID),outputFolder);
+ }
+
+ public void decryptPlainContentByIndex(int index,String outputFolder) throws IOException {
+ decryptPlainContent(getTMDFromNUSTitle().getContentByIndex(index),outputFolder);
+ }
+
+ public void decryptPlainContent(Content c,String outputFolder) throws IOException {
+ decryptPlainContents(new ArrayList(Arrays.asList(c)), outputFolder);
+ }
+
+ public void decryptPlainContents(List list,String outputFolder) throws IOException {
+ for(Content c : list){
+ decryptContentTo(c,outputFolder,getNUSTitle().isSkipExistingFiles());
+ }
+ }
+ public void decryptAllPlainContents(String outputFolder) throws IOException {
+ decryptPlainContents(new ArrayList(getTMDFromNUSTitle().getAllContents().values()),outputFolder);
+ }
+
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ //Other
+ //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ private TMD getTMDFromNUSTitle(){
+ return getNUSTitle().getTMD();
+ }
+}
diff --git a/src/de/mas/jnus/lib/ExtractionService.java b/src/de/mas/jnus/lib/ExtractionService.java
new file mode 100644
index 0000000..588012c
--- /dev/null
+++ b/src/de/mas/jnus/lib/ExtractionService.java
@@ -0,0 +1,125 @@
+package de.mas.jnus.lib;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+import de.mas.jnus.lib.utils.FileUtils;
+import de.mas.jnus.lib.utils.Utils;
+import lombok.Getter;
+import lombok.Setter;
+
+public class ExtractionService {
+ private static Map instances = new HashMap<>();
+
+ public static ExtractionService getInstance(NUSTitle nustitle) {
+ if(!instances.containsKey(nustitle)){
+ instances.put(nustitle, new ExtractionService(nustitle));
+ }
+ return instances.get(nustitle);
+ }
+
+ @Getter @Setter private NUSTitle NUSTitle = null;
+
+ private ExtractionService(NUSTitle nustitle){
+ setNUSTitle(nustitle);
+ }
+
+ private NUSDataProvider getDataProvider(){
+ return getNUSTitle().getDataProvider();
+ }
+
+ public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException {
+ extractEncryptedContentHashesTo(new ArrayList(getNUSTitle().getTMD().getAllContents().values()),outputFolder);
+ }
+
+ public void extractEncryptedContentHashesTo(List list, String outputFolder) throws IOException {
+ Utils.createDir(outputFolder);
+ NUSDataProvider dataProvider = getDataProvider();
+ for(Content c : list){
+ dataProvider.saveContentH3Hash(c, outputFolder);
+ }
+ }
+
+ public void extractAllEncryptedContentFiles(String outputFolder) throws IOException {
+ extractAllEncryptedContentFilesWithHashesTo(outputFolder);
+ }
+
+ public void extractAllEncryptedContentFilesWithoutHashesTo(String outputFolder) throws IOException {
+ extractEncryptedContentFilesTo(new ArrayList(getNUSTitle().getTMD().getAllContents().values()),outputFolder,false);
+ }
+
+ public void extractAllEncryptedContentFilesWithHashesTo(String outputFolder) throws IOException {
+ extractEncryptedContentFilesTo(new ArrayList(getNUSTitle().getTMD().getAllContents().values()),outputFolder,true);
+ }
+
+ public void extractEncryptedContentFilesTo(List list,String outputFolder,boolean withHashes) throws IOException {
+ Utils.createDir(outputFolder);
+ NUSDataProvider dataProvider = getDataProvider();
+ for(Content c : list){
+ System.out.println("Saving " + c.getFilename());
+ if(withHashes){
+ dataProvider.saveEncryptedContentWithH3Hash(c, outputFolder);
+ }else{
+ dataProvider.saveEncryptedContent(c, outputFolder);
+ }
+ }
+ }
+
+ public void extractTMDTo(String output) throws IOException {
+ Utils.createDir(output);
+
+ byte[] rawTMD= getDataProvider().getRawTMD();
+
+ if(rawTMD != null && rawTMD.length == 0){
+ System.out.println("Couldn't write TMD: No TMD loaded");
+ return;
+ }
+ String tmd_path = output + File.separator + Settings.TMD_FILENAME;
+ System.out.println("Extracting TMD to: " + tmd_path);
+ FileUtils.saveByteArrayToFile(tmd_path,rawTMD);
+ }
+
+ public void extractTicketTo(String output) throws IOException {
+ Utils.createDir(output);
+
+ byte[] rawTicket= getDataProvider().getRawTicket();
+
+ if(rawTicket != null && rawTicket.length == 0){
+ System.out.println("Couldn't write Ticket: No Ticket loaded");
+ return;
+ }
+ String ticket_path = output + File.separator + Settings.TICKET_FILENAME;
+ System.out.println("Extracting Ticket to: " + ticket_path);
+ FileUtils.saveByteArrayToFile(ticket_path,rawTicket);
+ }
+
+ public void extractCertTo(String output) throws IOException {
+ Utils.createDir(output);
+
+ byte[] rawCert = getDataProvider().getRawCert();
+
+ if(rawCert != null && rawCert.length == 0){
+ System.out.println("Couldn't write Cert: No Cert loaded");
+ return;
+ }
+ String cert_path = output + File.separator + Settings.CERT_FILENAME;
+ System.out.println("Extracting Cert to: " + cert_path);
+ FileUtils.saveByteArrayToFile(cert_path,rawCert);
+ }
+
+ public void extractAll(String outputFolder) throws IOException {
+ Utils.createDir(outputFolder);
+ extractAllEncryptedContentFilesWithHashesTo(outputFolder);
+ extractCertTo(outputFolder);
+ extractTMDTo(outputFolder);
+ extractTicketTo(outputFolder);
+
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/NUSTitle.java b/src/de/mas/jnus/lib/NUSTitle.java
new file mode 100644
index 0000000..86efcf6
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitle.java
@@ -0,0 +1,103 @@
+package de.mas.jnus.lib;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map.Entry;
+
+import de.mas.jnus.lib.entities.TMD;
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.entities.content.ContentFSTInfo;
+import de.mas.jnus.lib.entities.fst.FST;
+import de.mas.jnus.lib.entities.fst.FSTEntry;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+import lombok.Getter;
+import lombok.Setter;
+
+public class NUSTitle {
+ @Getter @Setter private String inputPath = "";
+
+ @Getter @Setter private FST FST;
+ @Getter @Setter private TMD TMD;
+ @Getter @Setter private Ticket ticket;
+
+ @Getter @Setter private boolean skipExistingFiles = true;
+ @Getter @Setter private NUSDataProvider dataProvider = null;
+
+ public List getAllFSTEntriesFlatByContentID(short ID){
+ return getFSTEntriesFlatByContent(getTMD().getContentByID((int) ID));
+ }
+
+ public List getFSTEntriesFlatByContentIndex(int index){
+ return getFSTEntriesFlatByContent(getTMD().getContentByIndex(index));
+ }
+
+ public List getFSTEntriesFlatByContent(Content content){
+ return getFSTEntriesFlatByContents(new ArrayList(Arrays.asList(content)));
+ }
+
+ public List getFSTEntriesFlatByContents(List list){
+ List entries = new ArrayList<>();
+ for(Content c : list){
+ for(FSTEntry f : c.getEntries()){
+ entries.add(f);
+ }
+ }
+ return entries;
+ }
+
+ public List getAllFSTEntriesFlat(){
+ return getFSTEntriesFlatByContents(new ArrayList(getTMD().getAllContents().values()));
+ }
+
+ public FSTEntry getFSTEntryByFullPath(String givenFullPath) {
+ givenFullPath = givenFullPath.replaceAll("/", "\\\\");
+ if(!givenFullPath.startsWith("\\")) givenFullPath = "\\" +givenFullPath;
+ for(FSTEntry f :getAllFSTEntriesFlat()){
+ if(f.getFullPath().equals(givenFullPath)){
+ return f;
+ }
+ }
+ return null;
+ }
+
+ public void printFiles() {
+ getFST().getRoot().printRecursive(0);
+ }
+
+ public void printContentFSTInfos(){
+ for(Entry e : getFST().getContentFSTInfos().entrySet()){
+ System.out.println(String.format("%08X", e.getKey()) + ": " +e.getValue());
+ }
+ }
+
+ public void printContentInfos(){
+ for(Entry e : getTMD().getAllContents().entrySet()){
+
+ System.out.println(String.format("%08X", e.getKey()) + ": " +e.getValue());
+ System.out.println(e.getValue().getContentFSTInfo());
+ for(FSTEntry entry : e.getValue().getEntries()){
+ System.out.println(entry.getFullPath() + String.format(" size: %016X", entry.getFileSize())
+ + String.format(" offset: %016X", entry.getFileOffset())
+ + String.format(" flags: %04X", entry.getFlags()));
+ }
+ System.out.println("-");
+ }
+ }
+
+ public void cleanup() throws IOException{
+ if(getDataProvider() != null){
+ getDataProvider().cleanup();
+ }
+ }
+
+ public void printDetailedData() {
+ printFiles();
+ printContentFSTInfos();
+ printContentInfos();
+
+ System.out.println();
+ }
+}
diff --git a/src/de/mas/jnus/lib/NUSTitleConfig.java b/src/de/mas/jnus/lib/NUSTitleConfig.java
new file mode 100644
index 0000000..64edb97
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitleConfig.java
@@ -0,0 +1,18 @@
+package de.mas.jnus.lib;
+
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
+import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
+import lombok.Data;
+
+@Data
+public class NUSTitleConfig {
+ private String inputPath = "";
+ private WUDInfo WUDInfo = null;
+ private Ticket ticket = null;
+
+ private int version = Settings.LATEST_TMD_VERSION;
+ private long titleID = 0x0L;
+
+ private WoomyInfo woomyInfo;
+}
diff --git a/src/de/mas/jnus/lib/NUSTitleLoader.java b/src/de/mas/jnus/lib/NUSTitleLoader.java
new file mode 100644
index 0000000..4bbd725
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitleLoader.java
@@ -0,0 +1,67 @@
+package de.mas.jnus.lib;
+
+import java.io.InputStream;
+import java.util.Map;
+
+import de.mas.jnus.lib.entities.TMD;
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.entities.fst.FST;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+import de.mas.jnus.lib.utils.StreamUtils;
+import de.mas.jnus.lib.utils.cryptography.AESDecryption;
+
+abstract class NUSTitleLoader {
+ protected NUSTitleLoader(){
+
+ }
+
+ public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception{
+ NUSTitle result = new NUSTitle();
+
+ NUSDataProvider dataProvider = getDataProvider(config);
+ result.setDataProvider(dataProvider);
+ dataProvider.setNUSTitle(result);
+
+
+ TMD tmd = TMD.parseTMD(dataProvider.getRawTMD());
+ result.setTMD(tmd);
+
+ if(tmd == null){
+ System.out.println("TMD not found.");
+ throw new Exception();
+ }
+
+ Ticket ticket = config.getTicket();
+ if(ticket == null){
+ ticket = Ticket.parseTicket(dataProvider.getRawTicket());
+ }
+ result.setTicket(ticket);
+ System.out.println(ticket);
+
+ Content fstContent = tmd.getContentByIndex(0);
+
+
+ InputStream fstContentEncryptedStream = dataProvider.getInputStreamFromContent(fstContent, 0);
+ if(fstContentEncryptedStream == null){
+
+ return null;
+ }
+
+ byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());
+
+ if(fstContent.isEncrypted()){
+ AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]);
+ fstBytes = aesDecryption.decrypt(fstBytes);
+ }
+
+ Map contents = tmd.getAllContents();
+
+ FST fst = FST.parseFST(fstBytes, contents);
+ result.setFST(fst);
+
+ return result;
+ }
+
+ protected abstract NUSDataProvider getDataProvider(NUSTitleConfig config);
+}
diff --git a/src/de/mas/jnus/lib/NUSTitleLoaderLocal.java b/src/de/mas/jnus/lib/NUSTitleLoaderLocal.java
new file mode 100644
index 0000000..d22224b
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitleLoaderLocal.java
@@ -0,0 +1,35 @@
+package de.mas.jnus.lib;
+
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.implementations.NUSDataProviderLocal;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+
+public class NUSTitleLoaderLocal extends NUSTitleLoader {
+
+ private NUSTitleLoaderLocal(){
+ super();
+ }
+ public static NUSTitle loadNUSTitle(String inputPath) throws Exception{
+ return loadNUSTitle(inputPath, null);
+ }
+
+ public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket) throws Exception{
+ NUSTitleLoader loader = new NUSTitleLoaderLocal();
+ NUSTitleConfig config = new NUSTitleConfig();
+
+ if(ticket != null){
+ config.setTicket(ticket);
+ }
+ config.setInputPath(inputPath);
+
+ return loader.loadNusTitle(config);
+ }
+
+ @Override
+ protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
+ NUSDataProviderLocal result = new NUSDataProviderLocal();
+ result.setLocalPath(config.getInputPath());
+ return result;
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/NUSTitleLoaderRemote.java b/src/de/mas/jnus/lib/NUSTitleLoaderRemote.java
new file mode 100644
index 0000000..2d65ed7
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitleLoaderRemote.java
@@ -0,0 +1,43 @@
+package de.mas.jnus.lib;
+
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.implementations.NUSDataProviderRemote;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+
+public class NUSTitleLoaderRemote extends NUSTitleLoader{
+
+ private NUSTitleLoaderRemote(){
+ super();
+ }
+
+ public static NUSTitle loadNUSTitle(long titleID) throws Exception{
+ return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION ,null);
+ }
+
+ public static NUSTitle loadNUSTitle(long titleID, int version) throws Exception{
+ return loadNUSTitle(titleID, version,null);
+ }
+
+ public static NUSTitle loadNUSTitle(long titleID, Ticket ticket) throws Exception{
+ return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, ticket);
+ }
+
+ public static NUSTitle loadNUSTitle(long titleID,int version, Ticket ticket) throws Exception{
+ NUSTitleLoader loader = new NUSTitleLoaderRemote();
+ NUSTitleConfig config = new NUSTitleConfig();
+
+ config.setVersion(version);
+ config.setTitleID(titleID);
+
+ return loader.loadNusTitle(config);
+ }
+
+ @Override
+ protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
+ NUSDataProviderRemote result = new NUSDataProviderRemote();
+ result.setVersion(config.getVersion());
+ result.setTitleID(config.getTitleID());
+ return result;
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/NUSTitleLoaderWUD.java b/src/de/mas/jnus/lib/NUSTitleLoaderWUD.java
new file mode 100644
index 0000000..15f80e2
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitleLoaderWUD.java
@@ -0,0 +1,49 @@
+package de.mas.jnus.lib;
+import java.io.File;
+import java.nio.file.Files;
+
+import de.mas.jnus.lib.implementations.NUSDataProviderWUD;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+import de.mas.jnus.lib.implementations.wud.WUDImage;
+import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
+import de.mas.jnus.lib.implementations.wud.parser.WUDInfoParser;
+
+public class NUSTitleLoaderWUD extends NUSTitleLoader {
+
+ private NUSTitleLoaderWUD(){
+ super();
+ }
+
+ public static NUSTitle loadNUSTitle(String WUDPath) throws Exception{
+ return loadNUSTitle(WUDPath, null);
+ }
+ public static NUSTitle loadNUSTitle(String WUDPath, byte[] titleKey) throws Exception{
+ NUSTitleLoader loader = new NUSTitleLoaderWUD();
+ NUSTitleConfig config = new NUSTitleConfig();
+
+ File wudFile = new File(WUDPath);
+ if(!wudFile.exists()){
+ System.out.println(WUDPath + " does not exist.");
+ System.exit(1);
+ }
+
+ WUDImage image = new WUDImage(wudFile);
+ if(titleKey == null){
+ File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
+ titleKey = Files.readAllBytes(keyFile.toPath());
+ }
+ WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), titleKey);
+
+ config.setWUDInfo(wudInfo);
+
+ return loader.loadNusTitle(config);
+ }
+
+ @Override
+ protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
+ NUSDataProviderWUD result = new NUSDataProviderWUD();
+ result.setWUDInfo(config.getWUDInfo());
+ return result;
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java b/src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java
new file mode 100644
index 0000000..8fea3aa
--- /dev/null
+++ b/src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java
@@ -0,0 +1,33 @@
+package de.mas.jnus.lib;
+
+import java.io.File;
+
+import de.mas.jnus.lib.implementations.NUSDataProviderWoomy;
+import de.mas.jnus.lib.implementations.NUSDataProvider;
+import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
+import de.mas.jnus.lib.implementations.woomy.WoomyParser;
+import lombok.extern.java.Log;
+
+@Log
+public class NUSTitleLoaderWoomy extends NUSTitleLoader {
+
+ public static NUSTitle loadNUSTitle(String inputFile) throws Exception{
+ NUSTitleLoaderWoomy loader = new NUSTitleLoaderWoomy();
+ NUSTitleConfig config = new NUSTitleConfig();
+
+ WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
+ if(woomyInfo == null){
+ log.info("Created woomy is null.");
+ return null;
+ }
+ config.setWoomyInfo(woomyInfo);
+ return loader.loadNusTitle(config);
+ }
+ @Override
+ protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
+ NUSDataProviderWoomy dataProvider = new NUSDataProviderWoomy();
+ dataProvider.setWoomyInfo(config.getWoomyInfo());
+ return dataProvider;
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/Settings.java b/src/de/mas/jnus/lib/Settings.java
new file mode 100644
index 0000000..1d8532a
--- /dev/null
+++ b/src/de/mas/jnus/lib/Settings.java
@@ -0,0 +1,18 @@
+package de.mas.jnus.lib;
+
+public class Settings {
+ public static String URL_BASE = "http://ccs.cdn.wup.shop.nintendo.net/ccs/download";
+ public static final int LATEST_TMD_VERSION = 0;
+ public static final String TMD_FILENAME = "title.tmd";
+ public static final String TICKET_FILENAME = "title.tik";
+ public static final String CERT_FILENAME = "title.cert";
+
+ public static final String ENCRYPTED_CONTENT_EXTENTION = ".app";
+ public static final String DECRYPTED_CONTENT_EXTENTION = ".dec";
+ public static final String WUD_KEY_FILENAME = "game.key";
+ public static final String WOOMY_METADATA_FILENAME = "metadata.xml";
+ public static final String H3_EXTENTION = ".h3";
+
+ public static byte[] commonKey = new byte[0x10];
+ public static int WIIU_DECRYPTED_AREA_OFFSET = 0x18000;
+}
diff --git a/src/de/mas/jnus/lib/WUDService.java b/src/de/mas/jnus/lib/WUDService.java
new file mode 100644
index 0000000..9dddb96
--- /dev/null
+++ b/src/de/mas/jnus/lib/WUDService.java
@@ -0,0 +1,176 @@
+package de.mas.jnus.lib;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.sun.org.apache.xerces.internal.util.SynchronizedSymbolTable;
+
+import java.util.TreeMap;
+
+import de.mas.jnus.lib.implementations.wud.WUDImage;
+import de.mas.jnus.lib.implementations.wud.WUDImageCompressedInfo;
+import de.mas.jnus.lib.utils.ByteArrayBuffer;
+import de.mas.jnus.lib.utils.ByteArrayWrapper;
+import de.mas.jnus.lib.utils.HashUtil;
+import de.mas.jnus.lib.utils.StreamUtils;
+import de.mas.jnus.lib.utils.Utils;
+import lombok.extern.java.Log;
+
+@Log
+public class WUDService {
+ public static boolean compressWUDToWUX(WUDImage image,String outputFolder) throws IOException{
+ return compressWUDToWUX(image, outputFolder, "game.wux");
+ }
+ public static boolean compressWUDToWUX(WUDImage image,String outputFolder,String filename) throws IOException{
+ if(image.isCompressed()){
+ log.info("Given Image is already compressed");
+ return false;
+ }
+
+ if(image.getWUDFileSize() != WUDImage.WUD_FILESIZE)
+ {
+ log.info("Given WUD has not the expected filesize");
+ return false;
+ }
+
+ Utils.createDir(outputFolder);
+
+ String filePath;
+ if(!outputFolder.isEmpty()){
+ filePath = outputFolder+ File.separator + filename;
+ }else{
+ filePath = filename;
+ }
+ File outputFile = new File(filePath);
+
+ if(outputFile.exists()){
+ log.info("Couldn't compress wud, target file already exists (" + outputFile.getAbsolutePath() + ")");
+ //return false;
+ }
+
+ RandomAccessFile fileOutput = new RandomAccessFile(outputFile, "rw");
+
+ WUDImageCompressedInfo info = WUDImageCompressedInfo.getDefaultCompressedInfo();
+
+ byte[] header = info.getHeaderAsBytes();
+ log.info("Writing header");
+ fileOutput.write(header);
+
+ int sectorTableEntryCount = (int) ((image.getWUDFileSize()+ WUDImageCompressedInfo.SECTOR_SIZE-1) / (long)WUDImageCompressedInfo.SECTOR_SIZE);
+
+ long sectorTableStart = fileOutput.getFilePointer();
+ long sectorTableEnd = Utils.align(sectorTableEntryCount *0x04,WUDImageCompressedInfo.SECTOR_SIZE);
+ byte[] sectorTablePlaceHolder = new byte[(int) (sectorTableEnd-sectorTableStart)];
+
+ fileOutput.write(sectorTablePlaceHolder);
+
+ Map sectorHashes = new HashMap<>();
+ Map sectorMapping = new TreeMap<>();
+
+ InputStream in = image.getWUDDiscReader().readEncryptedToInputStream(0, image.getWUDFileSize());
+
+ int bufferSize = WUDImageCompressedInfo.SECTOR_SIZE;
+ byte[] blockBuffer = new byte[bufferSize];
+ ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
+
+
+ long written = 0;
+ int curSector = 0;
+ int realSector = 0;
+
+ log.info("Writing sectors");
+ Integer oldOffset = null;
+ do{
+ int read = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize);
+ ByteArrayWrapper hash = new ByteArrayWrapper(HashUtil.hashSHA1(blockBuffer));
+
+ if((oldOffset = sectorHashes.get(hash)) != null){
+ sectorMapping.put(curSector, oldOffset);
+ oldOffset = null;
+ }else{ //its a new sector
+ sectorMapping.put(curSector, realSector);
+ sectorHashes.put(hash, realSector);
+ fileOutput.write(blockBuffer);
+ realSector++;
+ }
+
+ written += read;
+ curSector++;
+ if(curSector % 1000 == 0){
+ double readMB = written / 1024.0 / 1024.0;
+ double writtenMB = ((long)realSector * (long)bufferSize) / 1024.0 / 1024.0;
+ double percent = ((double)written / image.getWUDFileSize())*100;
+ double ratio = 1 / (writtenMB / readMB);
+ System.out.println(String.format(Locale.ROOT,"Compressing (%05.2f%%) | Ratio: 1:%.2f | Read: %08.2fMB | Written: %08.2fMB",percent,ratio,readMB,writtenMB));
+ }
+ }while(written < image.getWUDFileSize());
+
+ log.info("Writing sector table");
+ fileOutput.seek(sectorTableStart);
+ ByteBuffer buffer = ByteBuffer.allocate(sectorTablePlaceHolder.length);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ for(Entry e: sectorMapping.entrySet()){
+ buffer.putInt(e.getValue());
+ }
+
+ fileOutput.write(buffer.array());
+
+ fileOutput.close();
+ return true;
+ }
+
+ public static boolean compareWUDImage(WUDImage firstImage,WUDImage secondImage) throws IOException{
+ if(firstImage.getWUDFileSize() != secondImage.getWUDFileSize()){
+ log.info("Filesize if different");
+ return false;
+ }
+ InputStream in1 = firstImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
+ InputStream in2 = secondImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
+
+ boolean result = true;
+ int bufferSize = 1024*1024+19;
+ long totalread = 0;
+ byte[] blockBuffer1 = new byte[bufferSize];
+ byte[] blockBuffer2 = new byte[bufferSize];
+ ByteArrayBuffer overflow1 = new ByteArrayBuffer(bufferSize);
+ ByteArrayBuffer overflow2 = new ByteArrayBuffer(bufferSize);
+ long curSector = 0;
+ do{
+ int read1 = StreamUtils.getChunkFromStream(in1, blockBuffer1, overflow1, bufferSize);
+ int read2 = StreamUtils.getChunkFromStream(in2, blockBuffer2, overflow2, bufferSize);
+ if(read1 != read2){
+ log.info("Verification error");
+ result = false;
+ break;
+ }
+
+ if(!Arrays.equals(blockBuffer1,blockBuffer2)){
+ log.info("Verification error");
+ result = false;
+ break;
+ }
+
+ totalread += read1;
+
+ curSector++;
+ if(curSector % 100 == 0){
+ System.out.println(String.format("Checked: %016X bytes (%.2f%%)", (long)curSector*(long)bufferSize,(((long)curSector*(long)bufferSize*1.0)/(WUDImage.WUD_FILESIZE))*100));
+ }
+ }while(totalread < WUDImage.WUD_FILESIZE);
+
+ in1.close();
+ in2.close();
+ log.info("Verfication done!");
+
+ return result;
+ }
+}
diff --git a/src/de/mas/jnus/lib/entities/TMD.java b/src/de/mas/jnus/lib/entities/TMD.java
new file mode 100644
index 0000000..67dee4c
--- /dev/null
+++ b/src/de/mas/jnus/lib/entities/TMD.java
@@ -0,0 +1,167 @@
+package de.mas.jnus.lib.entities;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.entities.content.ContentInfo;
+import lombok.Getter;
+import lombok.Setter;
+
+public class TMD {
+ @Getter @Setter private int signatureType; // 0x000
+ @Getter @Setter private byte[] signature = new byte[0x100]; // 0x004
+ @Getter @Setter private byte[] issuer = new byte[0x40]; // 0x140
+ @Getter @Setter private byte version; // 0x180
+ @Getter @Setter private byte CACRLVersion; // 0x181
+ @Getter @Setter private byte signerCRLVersion; // 0x182
+ @Getter @Setter private long systemVersion; // 0x184
+ @Getter @Setter private long titleID; // 0x18C
+ @Getter @Setter private int titleType; // 0x194
+ @Getter @Setter private short groupID; // 0x198
+ @Getter @Setter private byte[] reserved = new byte[62]; // 0x19A
+ @Getter @Setter private int accessRights; // 0x1D8
+ @Getter @Setter private short titleVersion; // 0x1DC
+ @Getter @Setter private short contentCount; // 0x1DE
+ @Getter @Setter private short bootIndex; // 0x1E0
+ @Getter @Setter private byte[] SHA2 = new byte[0x20]; // 0x1E4
+ @Getter @Setter private ContentInfo[] contentInfos = new ContentInfo[0x40];
+ Map contentToIndex = new HashMap<>();
+ Map contentToID = new HashMap<>();
+
+ @Getter @Setter private byte[] rawTMD = new byte[0];
+ private TMD(){
+
+ }
+
+ public static TMD parseTMD(File tmd) throws IOException {
+ if(tmd == null || !tmd.exists()){
+ System.out.println("TMD input file null or doesn't exist.");
+ return null;
+ }
+ return parseTMD(Files.readAllBytes(tmd.toPath()));
+ }
+
+ public static TMD parseTMD(byte[] input) {
+
+ TMD result = new TMD();
+ result.setRawTMD(Arrays.copyOf(input,input.length));
+ byte[] signature = new byte[0x100];
+ byte[] issuer = new byte[0x40];
+ byte[] reserved = new byte[62];
+ byte[] SHA2 = new byte[0x20];
+
+ ContentInfo[] contentInfos = result.getContentInfos();
+
+ ByteBuffer buffer = ByteBuffer.allocate(input.length);
+ buffer.put(input);
+
+ //Get Signature
+ buffer.position(0x00);
+ int signatureType = buffer.getInt();
+ buffer.get(signature, 0, 0x100);
+
+ //Get Issuer
+ buffer.position(0x140);
+ buffer.get(issuer, 0, 0x40);
+
+ //Get CACRLVersion and signerCRLVersion
+ buffer.position(0x180);
+ byte version = buffer.get();
+ byte CACRLVersion = buffer.get();
+ byte signerCRLVersion = buffer.get();
+
+ //Get title information
+ buffer.position(0x184);
+ long systemVersion = buffer.getLong();
+ long titleID = buffer.getLong();
+ int titleType = buffer.getInt();
+ short groupID = buffer.getShort();
+ buffer.position(0x19A);
+
+ //Get title information
+ buffer.get(reserved, 0, 62);
+
+ //Get accessRights,titleVersion,contentCount,bootIndex
+ buffer.position(0x1D8);
+ int accessRights = buffer.getInt();
+ short titleVersion = buffer.getShort();
+ short contentCount = buffer.getShort();
+ short bootIndex = buffer.getShort();
+
+ //Get hash
+ buffer.position(0x1E4);
+ buffer.get(SHA2, 0, 0x20);
+
+ //Get contentInfos
+ buffer.position(0x204);
+ for(int i =0;i<64;i++){
+ byte[] contentInfo = new byte[0x24];
+ buffer.get(contentInfo, 0, 0x24);
+ contentInfos[i] = ContentInfo.parseContentInfo(contentInfo);
+ }
+
+ //Get Contents
+ for(int i =0;i getAllContents() {
+ return contentToIndex;
+ }
+
+ public void printContents() {
+ for(Content c: contentToIndex.values()){
+ System.out.println(c);
+ }
+ }
+}
diff --git a/src/de/mas/jnus/lib/entities/Ticket.java b/src/de/mas/jnus/lib/entities/Ticket.java
new file mode 100644
index 0000000..4879c82
--- /dev/null
+++ b/src/de/mas/jnus/lib/entities/Ticket.java
@@ -0,0 +1,122 @@
+package de.mas.jnus.lib.entities;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.utils.Utils;
+import de.mas.jnus.lib.utils.cryptography.AESDecryption;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.java.Log;
+
+@Log
+public class Ticket {
+ @Getter @Setter private byte[] encryptedKey = new byte[0x10];
+ @Getter @Setter private byte[] decryptedKey = new byte[0x10];
+
+ @Getter @Setter private byte[] IV = new byte[0x10];
+
+ @Getter @Setter private byte[] cert0 = new byte[0x300];
+ @Getter @Setter private byte[] cert1 = new byte[0x400];
+
+ @Getter @Setter private byte[] rawTicket = new byte[0];
+
+ private Ticket(){
+
+ }
+
+ public static Ticket parseTicket(File ticket) throws IOException {
+ if(ticket == null || !ticket.exists()){
+ log.warning("Ticket input file null or doesn't exist.");
+ return null;
+ }
+ return parseTicket(Files.readAllBytes(ticket.toPath()));
+ }
+
+ public static Ticket parseTicket(byte[] ticket) throws IOException {
+ if(ticket == null){
+ return null;
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(ticket.length);
+ buffer.put(ticket);
+
+ //read key
+ byte[] encryptedKey = new byte[0x10];
+ buffer.position(0x1BF);
+ buffer.get(encryptedKey,0x00,0x10);
+
+ //read titleID
+ buffer.position(0x1DC);
+ long titleID = buffer.getLong();
+
+ Ticket result = createTicket(encryptedKey,titleID);
+ result.setRawTicket(Arrays.copyOf(ticket, ticket.length));
+
+ //read certs.
+ byte[] cert0 = new byte[0x300];
+ byte[] cert1 = new byte[0x400];
+
+ if(ticket.length >= 0x650){
+ buffer.position(0x350);
+ buffer.get(cert0,0x00,0x300);
+ }
+ if(ticket.length >= 0xA50){
+ buffer.position(0x650);
+ buffer.get(cert1,0x00,0x400);
+ }
+
+ result.setCert0(cert0);
+ result.setCert1(cert1);
+
+ return result;
+ }
+
+ public static Ticket createTicket(byte[] encryptedKey, long titleID) {
+ Ticket result = new Ticket();
+ result.encryptedKey = encryptedKey;
+
+ byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
+ result.decryptedKey = calculateDecryptedKey(result.encryptedKey,IV);
+ result.setIV(IV);
+
+ return result;
+ }
+
+ private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV) {
+ AESDecryption decryption = new AESDecryption(Settings.commonKey, IV){};
+ return decryption.decrypt(encryptedKey);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + Arrays.hashCode(encryptedKey);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Ticket other = (Ticket) obj;
+ if (!Arrays.equals(encryptedKey, other.encryptedKey))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey="
+ + Utils.ByteArrayToString(decryptedKey) + "]";
+ }
+}
diff --git a/src/de/mas/jnus/lib/entities/content/Content.java b/src/de/mas/jnus/lib/entities/content/Content.java
new file mode 100644
index 0000000..67f2d56
--- /dev/null
+++ b/src/de/mas/jnus/lib/entities/content/Content.java
@@ -0,0 +1,149 @@
+package de.mas.jnus.lib.entities.content;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.fst.FSTEntry;
+import de.mas.jnus.lib.utils.Utils;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Represents a Content
+ * @author Maschell
+ *
+ */
+public class Content{
+ public static final short CONTENT_HASHED = 0x0002;
+ public static final short CONTENT_ENCRYPTED = 0x0001;
+
+ @Getter @Setter private int ID = 0x00;
+ @Getter @Setter private short index = 0x00;
+ @Getter @Setter private short type = 0x0000;
+
+ @Getter @Setter private long encryptedFileSize = 0;
+ @Getter @Setter private byte[] SHA2Hash = new byte[0x14];
+
+ @Getter private List entries = new ArrayList<>();
+
+ @Getter @Setter private ContentFSTInfo contentFSTInfo = null;
+
+ /**
+ * Creates a new Content object given be the raw byte data
+ * @param input 0x30 byte of data from the TMD (starting at 0xB04)
+ * @return content object
+ */
+ public static Content parseContent(byte[] input) {
+ if(input == null || input.length != 0x30){
+ System.out.println("Error: invalid Content byte[] input");
+ return null;
+ }
+ ByteBuffer buffer = ByteBuffer.allocate(input.length);
+ buffer.put(input);
+ buffer.position(0);
+ int ID = buffer.getInt(0x00);
+ short index = buffer.getShort(0x04);
+ short type = buffer.getShort(0x06);
+ long encryptedFileSize = buffer.getLong(0x08);
+ buffer.position(0x10);
+ byte[] hash = new byte[0x14];
+ buffer.get(hash, 0x00, 0x14);
+ return new Content(ID, index,type,encryptedFileSize, hash);
+ }
+
+ public Content(int ID, short index, short type, long encryptedFileSize, byte[] hash) {
+ setID(ID);
+ setIndex(index);
+ setType(type);
+ setEncryptedFileSize(encryptedFileSize);
+ setSHA2Hash(hash);
+ }
+
+ /**
+ * Returns if the content is hashed
+ * @return true if hashed
+ */
+ public boolean isHashed() {
+ return (type & CONTENT_HASHED) == CONTENT_HASHED;
+ }
+ /**
+ * Returns if the content is encrypted
+ * @return true if encrypted
+ */
+ public boolean isEncrypted() {
+ return (type & CONTENT_ENCRYPTED) == CONTENT_ENCRYPTED;
+ }
+
+ /**
+ * Return the filename of the encrypted content.
+ * It's the ID as hex with an extension
+ * For example: 00000000.app
+ * @return filename of the encrypted content
+ */
+ public String getFilename(){
+ return String.format("%08X%s", getID(),Settings.ENCRYPTED_CONTENT_EXTENTION);
+ }
+
+ /**
+ * Adds a content to the internal entry list.
+ * @param entry that will be added to the content list
+ */
+ public void addEntry(FSTEntry entry) {
+ getEntries().add(entry);
+ }
+
+ /**
+ * Returns the size of the decrypted content.
+ * @return size of the decrypted content
+ */
+ public long getDecryptedFileSize() {
+ if(isHashed()){
+ return getEncryptedFileSize()/0x10000*0xFC00;
+ }else{
+ return getEncryptedFileSize();
+ }
+ }
+
+ /**
+ * Return the filename of the decrypted content.
+ * It's the ID as hex with an extension
+ * For example: 00000000.dec
+ * @return filename of the decrypted content
+ */
+ public String getFilenameDecrypted() {
+ return String.format("%08X%s", getID(),Settings.DECRYPTED_CONTENT_EXTENTION);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ID;
+ result = prime * result + Arrays.hashCode(SHA2Hash);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Content other = (Content) obj;
+ if (ID != other.ID)
+ return false;
+ if (!Arrays.equals(SHA2Hash, other.SHA2Hash))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Content [ID=" + Integer.toHexString(ID) + ", index=" + Integer.toHexString(index) + ", type=" + String.format("%04X", type) + ", encryptedFileSize=" + encryptedFileSize + ", SHA2Hash=" + Utils.ByteArrayToString(SHA2Hash) + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java b/src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java
new file mode 100644
index 0000000..47dcbbc
--- /dev/null
+++ b/src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java
@@ -0,0 +1,85 @@
+package de.mas.jnus.lib.entities.content;
+
+import java.nio.ByteBuffer;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@EqualsAndHashCode
+/**
+ * Representation on an Object of the first section
+ * of an FST.
+ * @author Maschell
+ *
+ */
+public class ContentFSTInfo {
+ @Getter @Setter private long offsetSector;
+ @Getter @Setter private long sizeSector;
+ @Getter @Setter private long ownerTitleID;
+ @Getter @Setter private int groupID;
+ @Getter @Setter private byte unkown;
+
+ private static int SECTOR_SIZE = 0x8000;
+
+ private ContentFSTInfo(){
+
+ }
+
+
+ /**
+ * Creates a new ContentFSTInfo object given be the raw byte data
+ * @param input 0x20 byte of data from the FST (starting at 0x20)
+ * @return ContentFSTInfo object
+ */
+ public static ContentFSTInfo parseContentFST(byte[] input) {
+ if(input == null ||input.length != 0x20){
+ System.out.println("Error: invalid ContentFSTInfo byte[] input");
+ return null;
+ }
+ ContentFSTInfo cFSTInfo = new ContentFSTInfo();
+ ByteBuffer buffer = ByteBuffer.allocate(input.length);
+ buffer.put(input);
+
+ buffer.position(0);
+ int offset = buffer.getInt();
+ int size = buffer.getInt();
+ long ownerTitleID = buffer.getLong();
+ int groupID = buffer.getInt();
+ byte unkown = buffer.get();
+
+ cFSTInfo.setOffsetSector(offset);
+ cFSTInfo.setSizeSector(size);
+ cFSTInfo.setOwnerTitleID(ownerTitleID);
+ cFSTInfo.setGroupID(groupID);
+ cFSTInfo.setUnkown(unkown);
+
+ return cFSTInfo;
+ }
+
+ /**
+ * Returns the offset of of the Content in the partition
+ * @return offset of the content in the partition in bytes
+ */
+ public long getOffset() {
+ long result = (getOffsetSector() * SECTOR_SIZE) - SECTOR_SIZE;
+ if(result < 0){
+ return 0;
+ }
+ return result;
+ }
+
+ /**
+ * Returns the size in bytes, not in sectors
+ * @return size in bytes
+ */
+ public int getSize() {
+ return (int) (getSizeSector() * SECTOR_SIZE);
+ }
+
+ @Override
+ public String toString() {
+ return "ContentFSTInfo [offset=" + String.format("%08X", offsetSector) + ", size=" + String.format("%08X", sizeSector) + ", ownerTitleID=" + String.format("%016X", ownerTitleID) + ", groupID="
+ + String.format("%08X", groupID) + ", unkown=" + unkown + "]";
+ }
+}
diff --git a/src/de/mas/jnus/lib/entities/content/ContentInfo.java b/src/de/mas/jnus/lib/entities/content/ContentInfo.java
new file mode 100644
index 0000000..0edc6b1
--- /dev/null
+++ b/src/de/mas/jnus/lib/entities/content/ContentInfo.java
@@ -0,0 +1,65 @@
+package de.mas.jnus.lib.entities.content;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@EqualsAndHashCode
+/**
+ * Represents a Object from the TMD before the actual Content Section.
+ * @author Maschell
+ *
+ */
+public class ContentInfo{
+ @Getter @Setter private short indexOffset = 0x00;
+ @Getter @Setter private short commandCount = 0x00;
+ @Getter @Setter private byte[] SHA2Hash = new byte[0x20];
+
+ public ContentInfo() {
+ this((short) 0);
+ }
+
+ public ContentInfo(short contentCount) {
+ this((short) 0,contentCount);
+ }
+ public ContentInfo(short indexOffset,short commandCount) {
+ this(indexOffset,commandCount,null);
+ }
+ public ContentInfo(short indexOffset,short commandCount,byte[] SHA2Hash) {
+ setIndexOffset(indexOffset);
+ setCommandCount(commandCount);
+ setSHA2Hash(SHA2Hash);
+ }
+
+ /**
+ * Creates a new ContentInfo object given be the raw byte data
+ * @param input 0x24 byte of data from the TMD (starting at 0x208)
+ * @return ContentFSTInfo object
+ */
+ public static ContentInfo parseContentInfo(byte[] input){
+ if(input == null || input.length != 0x24){
+ System.out.println("Error: invalid ContentInfo byte[] input");
+ return null;
+ }
+
+ ByteBuffer buffer = ByteBuffer.allocate(input.length);
+ buffer.put(input);
+ buffer.position(0);
+ short indexOffset = buffer.getShort(0x00);
+ short commandCount = buffer.getShort(0x02);
+
+ byte[] sha2hash = new byte[0x20];
+ buffer.position(0x04);
+ buffer.get(sha2hash, 0x00, 0x20);
+
+ return new ContentInfo(indexOffset, commandCount, sha2hash);
+ }
+
+ @Override
+ public String toString() {
+ return "ContentInfo [indexOffset=" + indexOffset + ", commandCount=" + commandCount + ", SHA2Hash=" + Arrays.toString(SHA2Hash) + "]";
+ }
+}
diff --git a/src/de/mas/jnus/lib/entities/fst/FST.java b/src/de/mas/jnus/lib/entities/fst/FST.java
new file mode 100644
index 0000000..f7ed1ac
--- /dev/null
+++ b/src/de/mas/jnus/lib/entities/fst/FST.java
@@ -0,0 +1,81 @@
+package de.mas.jnus.lib.entities.fst;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.entities.content.ContentFSTInfo;
+import de.mas.jnus.lib.utils.ByteUtils;
+import lombok.Getter;
+import lombok.Setter;
+/**
+ * Represents the FST
+ * @author Maschell
+ *
+ */
+public class FST {
+ @Getter @Setter private FSTEntry root = FSTEntry.getRootFSTEntry();
+
+ @Getter @Setter private int unknown;
+ @Getter @Setter private int contentCount = 0;
+
+ @Getter @Setter private Map contentFSTInfos = new HashMap<>();
+
+ private FST(){
+
+ }
+
+ /**
+ * Creates a FST by the given raw byte data
+ * @param fstData raw decrypted FST data
+ * @param contentsMappedByIndex map of index/content
+ * @return
+ */
+ public static FST parseFST(byte[] fstData,Map contentsMappedByIndex){
+ if(!Arrays.equals(Arrays.copyOfRange(fstData, 0, 3), new byte[]{0x46,0x53,0x54})){
+ throw new IllegalArgumentException("Not a FST. Maybe a wrong key?");
+ }
+ FST result = new FST();
+
+ int unknownValue = ByteUtils.getIntFromBytes(fstData, 0x04);
+ int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08);
+
+ int contentfst_offset = 0x20;
+ int contentfst_size = 0x20*contentCount;
+
+ int fst_offset = contentfst_offset+contentfst_size;
+ int fileCount = ByteUtils.getIntFromBytes(fstData, fst_offset + 0x08);
+ int fst_size = fileCount*0x10;
+
+ int nameOff = fst_offset + fst_size;
+ int nameSize = nameOff +1;
+
+ //Get list with null-terminated Strings. Ends with \0\0.
+ for(int i = nameOff;i contentFSTInfos = result.getContentFSTInfos();
+ for(int i = 0;i children = null;
+
+ @Getter @Setter private short flags;
+
+ @Getter @Setter private long fileSize = 0;
+ @Getter @Setter private long fileOffset = 0;
+
+ @Getter private Content content = null;
+
+ @Getter @Setter private byte[] hash = new byte[0x14];
+
+ @Getter @Setter private boolean isDir = false;
+ @Getter @Setter private boolean isRoot = false;
+ @Getter @Setter private boolean notInPackage = false;
+
+ @Getter @Setter private short contentFSTID = 0;
+
+ public FSTEntry(){
+
+ }
+
+ /**
+ * Creates and returns a new FST Entry
+ * @return
+ */
+ public static FSTEntry getRootFSTEntry(){
+ FSTEntry entry = new FSTEntry();
+ entry.setRoot(true);
+ return entry;
+ }
+
+ public String getFullPath() {
+ return getPath() + getFilename();
+ }
+
+ public int getEntryCount() {
+ int count = 1;
+ for(FSTEntry entry : getChildren()){
+ count += entry.getEntryCount();
+ }
+ return count;
+ }
+
+ public void addChildren(FSTEntry fstEntry) {
+ getChildren().add(fstEntry);
+ fstEntry.setParent(this);
+ }
+
+ public List getChildren() {
+ if(children == null){
+ children = new ArrayList<>();
+ }
+ return children;
+ }
+
+ public List getDirChildren(){
+ return getDirChildren(false);
+ }
+
+ public List getDirChildren(boolean all){
+ List result = new ArrayList<>();
+ for(FSTEntry child : getChildren()){
+ if(child.isDir() && (all || !child.isNotInPackage())){
+ result.add(child);
+ }
+ }
+ return result;
+ }
+
+ public List getFileChildren(){
+ return getFileChildren(false);
+ }
+
+ public List getFileChildren(boolean all){
+ List result = new ArrayList<>();
+ for(FSTEntry child : getChildren()){
+ if((all && !child.isDir() || !child.isDir())){
+ result.add(child);
+ }
+ }
+ return result;
+ }
+
+ public List getFSTEntriesByContent(Content content) {
+ List entries = new ArrayList<>();
+ if(this.content == null){
+ log.warning("Error in getFSTEntriesByContent, content null");
+ System.exit(0);
+ }else{
+ if(this.content.equals(content)){
+ entries.add(this);
+ }
+ }
+ for(FSTEntry child : getChildren()){
+ entries.addAll(child.getFSTEntriesByContent(content));
+ }
+ return entries;
+ }
+
+ public void setContent(Content content) {
+ if(content == null){
+ log.warning("Can't set content for "+ getFilename() + ": Content it null");
+ System.out.println();
+ return;
+ }
+ this.content = content;
+ content.addEntry(this);
+ }
+
+ public long getFileOffsetBlock() {
+ if(getContent().isHashed()){
+ return (getFileOffset()/0xFC00) * 0x10000;
+ }else{
+ return getFileOffset();
+ }
+ }
+
+ public void printRecursive(int space){
+ for(int i = 0;i contentsByIndex,Map contentsFSTByIndex) {
+ int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08);
+
+ int level = 0;
+ int[] LEntry = new int[16];
+ int[] Entry = new int[16];
+
+ HashMap fstEntryToOffsetMap = new HashMap<>();
+
+ rootEntry.setDir(true);
+ Entry[level] = 0;
+ LEntry[level++] = 0;
+
+ fstEntryToOffsetMap.put(0,rootEntry);
+
+ for(int i = 1;i< totalEntries;i++){
+ int entryOffset = i;
+ if( level > 0){
+ while( LEntry[level-1] == i ){level--;}
+ }
+
+ byte[] curEntry = Arrays.copyOfRange(fstSection,i*0x10,(i+1)*0x10);
+
+ FSTEntry entry = new FSTEntry();
+
+ String path = getFullPath(level, fstSection, namesSection, Entry);
+ String filename = getName(curEntry,namesSection);
+
+ long fileOffset = ByteUtils.getIntFromBytes(curEntry, 0x04);
+ long fileSize = ByteUtils.getUnsingedIntFromBytes(curEntry, 0x08);
+
+ short flags = ByteUtils.getShortFromBytes(curEntry, 0x0C);
+ short contentIndex = ByteUtils.getShortFromBytes(curEntry, 0x0E);
+
+ if((curEntry[0] & FSTEntry.FSTEntry_notInNUS) == FSTEntry.FSTEntry_notInNUS){
+ entry.setNotInPackage(true);
+ }
+
+ if((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR){
+ entry.setDir(true);
+ int parentOffset = (int) fileOffset;
+ int nextOffset = (int) fileSize;
+
+ FSTEntry parent = fstEntryToOffsetMap.get(parentOffset);
+ if(parent != null){
+ log.info("no parent found for a FSTEntry");
+ parent.addChildren(entry);
+ }
+
+ Entry[level] = i;
+ LEntry[level++] = nextOffset ;
+
+ if( level > 15 ){
+ log.warning("level > 15");
+ break;
+ }
+ }else{
+ entry.setFileOffset(fileOffset<<5);
+ entry.setFileSize(fileSize);
+ FSTEntry parent = fstEntryToOffsetMap.get(Entry[level-1]);
+ if(parent != null){
+ parent.addChildren(entry);
+ }else{
+ log.warning(entryOffset +"couldn't find parent @ " + Entry[level-1]);
+ }
+ }
+
+ entry.setFlags(flags);
+ entry.setFilename(filename);
+ entry.setPath(path);
+
+ if(contentsByIndex != null){
+ Content content = contentsByIndex.get((int)contentIndex);
+ if(content == null){
+ log.warning("Content for FST Entry not found");
+ }else{
+ entry.setContent(content);
+
+ ContentFSTInfo contentFSTInfo = contentsFSTByIndex.get((int)contentIndex);
+ if(contentFSTInfo == null){
+ log.warning("ContentFSTInfo for FST Entry not found");
+ }else{
+ content.setContentFSTInfo(contentFSTInfo);
+ }
+ }
+ }
+
+ entry.setContentFSTID(contentIndex);
+
+ fstEntryToOffsetMap.put(entryOffset, entry);
+ }
+ }
+
+ private static int getNameOffset(byte[] curEntry) {
+ //Its a 24bit number. We overwrite the first byte, then we can read it as an Integer.
+ //But at first we make a copy.
+ byte[] entryData = Arrays.copyOf(curEntry, curEntry.length);
+ entryData[0] = 0;
+ return ByteUtils.getIntFromBytes(entryData, 0);
+ }
+
+ public static String getName(byte[] data, byte[] namesSection){
+ int nameOffset = getNameOffset(data);
+
+ int j = 0;
+
+ while(namesSection[nameOffset + j] != 0){j++;}
+ return(new String(Arrays.copyOfRange(namesSection,nameOffset, nameOffset + j)));
+ }
+
+
+ public static String getFullPath(int level,byte[] fstSection,byte[] namesSection, int[] Entry){
+ StringBuilder sb = new StringBuilder();
+ for(int i=0; i < level; i++){
+ int entryOffset = Entry[i]*0x10;
+ byte[] entryData = Arrays.copyOfRange(fstSection, entryOffset,entryOffset + 10);
+ String entryName = getName(entryData,namesSection);
+
+ sb.append(entryName).append(File.separator);
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProvider.java b/src/de/mas/jnus/lib/implementations/NUSDataProvider.java
new file mode 100644
index 0000000..00ba2af
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/NUSDataProvider.java
@@ -0,0 +1,115 @@
+package de.mas.jnus.lib.implementations;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.sun.istack.internal.NotNull;
+
+import de.mas.jnus.lib.NUSTitle;
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.utils.FileUtils;
+import de.mas.jnus.lib.utils.Utils;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.java.Log;
+
+@Log
+/**
+ * Service Methods for loading NUS/Content data from
+ * different sources
+ * @author Maschell
+ *
+ */
+public abstract class NUSDataProvider {
+
+ @Getter @Setter private NUSTitle NUSTitle = null;
+
+ public NUSDataProvider () {
+ }
+
+ /**
+ * Saves the given content encrypted with his .h3 file in the given directory.
+ * The Target directory will be created if it's missing.
+ * If the content is not hashed, no .h3 will be saved
+ * @param content Content that should be saved
+ * @param outputFolder Target directory where the files will be stored in.
+ * @throws IOException
+ */
+ public void saveEncryptedContentWithH3Hash(@NotNull Content content,@NotNull String outputFolder) throws IOException {
+ saveContentH3Hash(content, outputFolder);
+ saveEncryptedContent(content, outputFolder);
+ }
+
+ /**
+ * Saves the .h3 file of the given content into the given directory.
+ * The Target directory will be created if it's missing.
+ * If the content is not hashed, no .h3 will be saved
+ * @param content The content of which the h3 hashes should be saved
+ * @param outputFolder
+ * @throws IOException
+ */
+ public void saveContentH3Hash(@NotNull Content content,@NotNull String outputFolder) throws IOException {
+ if(!content.isHashed()){
+ return;
+ }
+ byte[] hash = getContentH3Hash(content);
+ if(hash == null){
+ return;
+ }
+ String h3Filename = String.format("%08X%s", content.getID(),Settings.H3_EXTENTION);
+ File output = new File(outputFolder + File.separator + h3Filename);
+ if(output.exists() && output.length() == hash.length){
+ return;
+ }
+
+ log.info("Saving " + h3Filename +" ");
+
+ FileUtils.saveByteArrayToFile(output, hash);
+ }
+
+ /**
+ * Saves the given content encrypted in the given directory.
+ * The Target directory will be created if it's missing.
+ * If the content is not encrypted at all, it will be just saved anyway.
+ * @param content Content that should be saved
+ * @param outputFolder Target directory where the files will be stored in.
+ * @throws IOException
+ */
+ public void saveEncryptedContent(@NotNull Content content,@NotNull String outputFolder) throws IOException {
+ Utils.createDir(outputFolder);
+ InputStream inputStream = getInputStreamFromContent(content, 0);
+ if(inputStream == null){
+ log.info("Couldn't save encrypted content. Input stream was null");
+ return;
+ }
+
+ File output = new File(outputFolder + File.separator + content.getFilename());
+ if(output.exists()){
+ if(output.length() == content.getEncryptedFileSize()){
+ log.info("Encrypted content alreadys exists, skipped");
+ return;
+ }else{
+ log.info("Encrypted content alreadys exists, but the length is not as expected. Saving it again");
+ }
+ }
+ FileUtils.saveInputStreamToFile(output,inputStream,content.getEncryptedFileSize());
+ }
+
+ /**
+ *
+ * @param content
+ * @param offset
+ * @return
+ * @throws IOException
+ */
+ public abstract InputStream getInputStreamFromContent(Content content,long offset) throws IOException ;
+
+ public abstract byte[] getContentH3Hash(Content content) throws IOException;
+ public abstract byte[] getRawTMD() throws IOException;
+ public abstract byte[] getRawTicket() throws IOException;
+ public abstract byte[] getRawCert() throws IOException;
+
+ public abstract void cleanup() throws IOException;
+}
diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProviderLocal.java b/src/de/mas/jnus/lib/implementations/NUSDataProviderLocal.java
new file mode 100644
index 0000000..744af0e
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/NUSDataProviderLocal.java
@@ -0,0 +1,73 @@
+package de.mas.jnus.lib.implementations;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.content.Content;
+import lombok.Getter;
+import lombok.Setter;
+
+public class NUSDataProviderLocal extends NUSDataProvider {
+ @Getter @Setter private String localPath = "";
+
+ public NUSDataProviderLocal() {
+ }
+
+ public String getFilePathOnDisk(Content c) {
+ return getLocalPath() + File.separator + c.getFilename();
+ }
+
+ @Override
+ public InputStream getInputStreamFromContent(Content content, long offset) throws IOException {
+ File filepath = new File(getFilePathOnDisk(content));
+ if(!filepath.exists()){
+
+ return null;
+ }
+ InputStream in = new FileInputStream(filepath);
+ in.skip(offset);
+ return in;
+ }
+
+ @Override
+ public 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()){
+ return null;
+ }
+ return Files.readAllBytes(h3File.toPath());
+ }
+
+ @Override
+ public byte[] getRawTMD() throws IOException {
+ String inputPath = getLocalPath();
+ String tmdPath = inputPath + File.separator + Settings.TMD_FILENAME;
+ File tmdFile = new File(tmdPath);
+ return Files.readAllBytes(tmdFile.toPath());
+ }
+
+ @Override
+ public byte[] getRawTicket() throws IOException {
+ String inputPath = getLocalPath();
+ String ticketPath = inputPath + File.separator + Settings.TICKET_FILENAME;
+ File ticketFile = new File(ticketPath);
+ return Files.readAllBytes(ticketFile.toPath());
+ }
+
+ @Override
+ public void cleanup() throws IOException {
+ }
+
+ @Override
+ public byte[] getRawCert() throws IOException {
+ String inputPath = getLocalPath();
+ String certPath = inputPath + File.separator + Settings.CERT_FILENAME;
+ File certFile = new File(certPath);
+ return Files.readAllBytes(certFile.toPath());
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProviderRemote.java b/src/de/mas/jnus/lib/implementations/NUSDataProviderRemote.java
new file mode 100644
index 0000000..af56753
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/NUSDataProviderRemote.java
@@ -0,0 +1,62 @@
+package de.mas.jnus.lib.implementations;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.utils.download.NUSDownloadService;
+import lombok.Getter;
+import lombok.Setter;
+
+public class NUSDataProviderRemote extends NUSDataProvider {
+ @Getter @Setter private int version = Settings.LATEST_TMD_VERSION;
+ @Getter @Setter private long titleID = 0L;
+
+ @Override
+ public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
+ NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
+ InputStream in = downloadService.getInputStreamForURL(getRemoteURL(content),fileOffsetBlock);
+ return in;
+ }
+
+ private String getRemoteURL(Content content) {
+ return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(),content.getID());
+ }
+
+ @Override
+ public byte[] getContentH3Hash(Content content) throws IOException {
+ NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
+ String url = getRemoteURL(content) + ".h3";
+ return downloadService.downloadToByteArray(url);
+ }
+
+ @Override
+ public byte[] getRawTMD() throws IOException {
+ NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
+
+ long titleID = getTitleID();
+ int version = getVersion();
+
+ return downloadService.downloadTMDToByteArray(titleID, version);
+ }
+
+ @Override
+ public byte[] getRawTicket() throws IOException {
+ NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
+
+ long titleID = getNUSTitle().getTMD().getTitleID();
+
+ return downloadService.downloadTicketToByteArray(titleID);
+ }
+
+ @Override
+ public void cleanup() throws IOException {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public byte[] getRawCert() throws IOException {
+ return nulL;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java b/src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java
new file mode 100644
index 0000000..f468f60
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java
@@ -0,0 +1,102 @@
+package de.mas.jnus.lib.implementations;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.TMD;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
+import de.mas.jnus.lib.implementations.wud.parser.WUDPartition;
+import de.mas.jnus.lib.implementations.wud.parser.WUDPartitionHeader;
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.java.Log;
+@Log
+public class NUSDataProviderWUD extends NUSDataProvider {
+ @Getter @Setter private WUDInfo WUDInfo = null;
+
+ @Setter(AccessLevel.PRIVATE) private TMD TMD = null;
+
+ public NUSDataProviderWUD() {
+ super();
+ }
+
+ public long getOffsetInWUD(Content content) {
+ if(content.getContentFSTInfo() == null){
+ return getAbsoluteReadOffset();
+ }else{
+ return getAbsoluteReadOffset() + content.getContentFSTInfo().getOffset();
+ }
+ }
+
+ public long getAbsoluteReadOffset(){
+ return (long)Settings.WIIU_DECRYPTED_AREA_OFFSET + getGamePartition().getPartitionOffset();
+ }
+
+ @Override
+ public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
+ WUDDiscReader discReader = getDiscReader();
+ long offset = getOffsetInWUD(content) + fileOffsetBlock;
+ return discReader.readEncryptedToInputStream(offset, content.getEncryptedFileSize());
+ }
+
+ @Override
+ public byte[] getContentH3Hash(Content content) throws IOException {
+
+ if(getGamePartitionHeader() == null){
+ log.warning("GamePartitionHeader is null");
+ return null;
+ }
+
+ if(!getGamePartitionHeader().isCalculatedHashes()){
+ log.info("Calculating h3 hashes");
+ getGamePartitionHeader().calculateHashes(getTMD().getAllContents());
+
+ }
+ return getGamePartitionHeader().getH3Hash(content);
+
+ }
+
+ private TMD getTMD() {
+ if(TMD == null){
+ setTMD(de.mas.jnus.lib.entities.TMD.parseTMD(getRawTMD()));
+ }
+ return TMD;
+ }
+
+ @Override
+ public byte[] getRawTMD() {
+ return getWUDInfo().getGamePartition().getRawTMD();
+ }
+
+ @Override
+ public byte[] getRawTicket() {
+ return getWUDInfo().getGamePartition().getRawTicket();
+ }
+
+ @Override
+ public byte[] getRawCert() throws IOException {
+ return getWUDInfo().getGamePartition().getRawCert();
+ }
+
+
+ public WUDPartition getGamePartition(){
+ return getWUDInfo().getGamePartition();
+ }
+
+ public WUDPartitionHeader getGamePartitionHeader(){
+ return getGamePartition().getPartitionHeader();
+ }
+
+ public WUDDiscReader getDiscReader(){
+ return getWUDInfo().getWUDDiscReader();
+ }
+
+ @Override
+ public void cleanup() {
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProviderWoomy.java b/src/de/mas/jnus/lib/implementations/NUSDataProviderWoomy.java
new file mode 100644
index 0000000..0097a75
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/NUSDataProviderWoomy.java
@@ -0,0 +1,95 @@
+package de.mas.jnus.lib.implementations;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
+import de.mas.jnus.lib.implementations.woomy.WoomyZipFile;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.extern.java.Log;
+
+@Log
+public class NUSDataProviderWoomy extends NUSDataProvider{
+ @Getter @Setter private WoomyInfo woomyInfo;
+ @Setter private WoomyZipFile woomyZipFile;
+
+ @Override
+ public InputStream getInputStreamFromContent(@NonNull Content content, long fileOffsetBlock) throws IOException {
+ WoomyZipFile zipFile = getSharedWoomyZipFile();
+ ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
+ if(entry == null){
+ log.warning("Inputstream for " + content.getFilename() + " not found");
+ System.exit(1);
+ }
+ return zipFile.getInputStream(entry);
+ }
+
+ @Override
+ public byte[] getContentH3Hash(Content content) throws IOException {
+ ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
+ if(entry != null){
+ WoomyZipFile zipFile = getNewWoomyZipFile();
+ byte[] result = zipFile.getEntryAsByte(entry);
+ zipFile.close();
+ return result;
+ }
+ return null;
+ }
+
+ @Override
+ public byte[] getRawTMD() throws IOException {
+ ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TMD_FILENAME);
+ if(entry == null){
+ log.warning(Settings.TMD_FILENAME + " not found in woomy file");
+ System.exit(1);
+ }
+ WoomyZipFile zipFile = getNewWoomyZipFile();
+ byte[] result = zipFile.getEntryAsByte(entry);
+ zipFile.close();
+ return result;
+ }
+
+ @Override
+ public byte[] getRawTicket() throws IOException {
+ ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TICKET_FILENAME);
+ if(entry == null){
+ log.warning(Settings.TICKET_FILENAME + " not found in woomy file");
+ System.exit(1);
+ }
+
+ WoomyZipFile zipFile = getNewWoomyZipFile();
+ byte[] result = zipFile.getEntryAsByte(entry);
+ zipFile.close();
+ return result;
+ }
+
+ public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException {
+ if(woomyZipFile == null || woomyZipFile.isClosed()){
+ woomyZipFile = getNewWoomyZipFile();
+ }
+ return woomyZipFile;
+ }
+
+ private WoomyZipFile getNewWoomyZipFile() throws ZipException, IOException {
+ return new WoomyZipFile(getWoomyInfo().getWoomyFile());
+ }
+
+ @Override
+ public void cleanup() throws IOException {
+ if(woomyZipFile != null && woomyZipFile.isClosed()){
+ woomyZipFile.close();
+ }
+ }
+
+ @Override
+ public byte[] getRawCert() throws IOException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java b/src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java
new file mode 100644
index 0000000..5f59a51
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java
@@ -0,0 +1,14 @@
+package de.mas.jnus.lib.implementations.woomy;
+
+import java.io.File;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+
+import lombok.Data;
+
+@Data
+public class WoomyInfo {
+ private String name;
+ private File woomyFile;
+ private Map contentFiles;
+}
diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java b/src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java
new file mode 100644
index 0000000..02586cc
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java
@@ -0,0 +1,39 @@
+package de.mas.jnus.lib.implementations.woomy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.Data;
+
+@Data
+public class WoomyMeta {
+ private String name;
+ private int icon;
+ private List entries;
+
+ public void addEntry(String name,String folder, int entryCount){
+ WoomyEntry entry = new WoomyEntry(name, folder, entryCount);
+ getEntries().add(entry);
+ }
+
+ public List getEntries(){
+ if(entries == null){
+ setEntries(new ArrayList<>());
+ }
+ return entries;
+ }
+
+ @Data
+ public class WoomyEntry {
+
+ public WoomyEntry(String name, String folder, int entryCount) {
+ setName(name);
+ setFolder(folder);
+ setEntryCount(entryCount);
+ }
+
+ private String name;
+ private String folder;
+ private int entryCount;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyMetaParser.java b/src/de/mas/jnus/lib/implementations/woomy/WoomyMetaParser.java
new file mode 100644
index 0000000..66b3f99
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/woomy/WoomyMetaParser.java
@@ -0,0 +1,62 @@
+package de.mas.jnus.lib.implementations.woomy;
+
+import java.io.InputStream;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import de.mas.jnus.lib.utils.XMLParser;
+import lombok.extern.java.Log;
+
+@Log
+public class WoomyMetaParser extends XMLParser{
+ private static final String WOOMY_METADATA_NAME = "name";
+ private static final String WOOMY_METADATA_ICON = "icon";
+
+ private static final String WOOMY_METADATA_ENTRIES = "entries";
+ private static final String WOOMY_METADATA_ENTRY_NAME = "name";
+ private static final String WOOMY_METADATA_ENTRY_FOLDER = "folder";
+ private static final String WOOMY_METADATA_ENTRY_ENTRIES = "entries";
+ /**
+ * Overwrite the default constructor to force the user to use the factory.
+ */
+ private WoomyMetaParser(){
+ }
+
+ public static WoomyMeta parseMeta(InputStream data){
+ XMLParser parser = new WoomyMetaParser();
+ WoomyMeta result = new WoomyMeta();
+ try {
+ parser.loadDocument(data);
+ } catch(Exception e){
+ log.info("Error while loading the data into the WoomyMetaParser");
+ return null;
+ }
+
+ String name = parser.getValueOfElement(WOOMY_METADATA_NAME);
+ if(name != null && !name.isEmpty()){
+ result.setName(name);
+ }
+
+ String icon = parser.getValueOfElement(WOOMY_METADATA_ICON);
+ if(icon != null && !icon.isEmpty()){
+ int icon_val = Integer.parseInt(icon);
+ result.setIcon(icon_val);
+ }
+ Node entries_node = parser.getNodeByValue(WOOMY_METADATA_ENTRIES);
+
+ NodeList entry_list = entries_node.getChildNodes();
+ for(int i = 0;i contentFiles = loadFileList(zipFile,regEx);
+ result.setContentFiles(contentFiles);
+
+ }catch(ZipException e){
+
+ }
+ return result;
+ }
+
+ private static Map loadFileList(@NotNull ZipFile zipFile, @NotNull String regEx) {
+ Enumeration extends ZipEntry> zipEntries = zipFile.entries();
+ Map result = new HashMap<>();
+ Pattern pattern = Pattern.compile(regEx);
+ while (zipEntries.hasMoreElements()) {
+ ZipEntry entry = (ZipEntry) zipEntries.nextElement();
+ if(!entry.isDirectory()){
+ String entryName = entry.getName();
+ Matcher matcher = pattern.matcher(entryName);
+ if(matcher.matches()){
+ String[] tokens = entryName.split("[\\\\|/]"); //We only want the filename!
+ String filename = tokens[tokens.length - 1];
+ result.put(filename.toLowerCase(), entry);
+ }
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java b/src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java
new file mode 100644
index 0000000..f50759f
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java
@@ -0,0 +1,35 @@
+package de.mas.jnus.lib.implementations.woomy;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+import de.mas.jnus.lib.utils.StreamUtils;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Woomy files are just zip files. This class is just the
+ * normal ZipFile class extended by an "isClosed" attribute.
+ * @author Maschell
+ *
+ */
+public class WoomyZipFile extends ZipFile {
+ @Getter @Setter boolean isClosed;
+ public WoomyZipFile(File file) throws ZipException, IOException {
+ super(file);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ setClosed(true);
+ }
+
+ public byte[] getEntryAsByte(ZipEntry entry) throws IOException {
+ return StreamUtils.getBytesFromStream(getInputStream(entry),(int) entry.getSize());
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/WUDImage.java b/src/de/mas/jnus/lib/implementations/wud/WUDImage.java
new file mode 100644
index 0000000..bef9b73
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/WUDImage.java
@@ -0,0 +1,108 @@
+package de.mas.jnus.lib.implementations.wud;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.Map;
+
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderCompressed;
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderSplitted;
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderUncompressed;
+import de.mas.jnus.lib.utils.ByteUtils;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.java.Log;
+
+@Log
+public class WUDImage {
+ public static long WUD_FILESIZE = 0x5D3A00000L;
+
+ @Getter @Setter private File fileHandle = null;
+ @Getter @Setter private WUDImageCompressedInfo compressedInfo = null;
+
+ @Getter @Setter private boolean isCompressed = false;
+ @Getter @Setter private boolean isSplitted = false;
+ private long inputFileSize = 0L;
+ @Setter private WUDDiscReader WUDDiscReader = null;
+
+ public WUDImage(File file) throws IOException{
+ if(file == null || !file.exists()){
+ System.out.println("WUD file is null or does not exist");
+ System.exit(1);
+ }
+
+ RandomAccessFile fileStream = new RandomAccessFile(file,"r");
+ fileStream.seek(0);
+ byte[] wuxheader = new byte[WUDImageCompressedInfo.WUX_HEADER_SIZE];
+ fileStream.read(wuxheader);
+ WUDImageCompressedInfo compressedInfo = new WUDImageCompressedInfo(wuxheader);
+
+ if(compressedInfo.isWUX()){
+ log.info("Image is compressed");
+ setCompressed(true);
+ Map indexTable = new HashMap<>();
+ long offsetIndexTable = compressedInfo.getOffsetIndexTable();
+ fileStream.seek(offsetIndexTable);
+
+ byte[] tableData = new byte[(int) (compressedInfo.getIndexTableEntryCount() * 0x04)];
+ fileStream.read(tableData);
+ int cur_offset = 0x00;
+ for(long i = 0;i indexTable = new HashMap<>();
+
+ public WUDImageCompressedInfo(byte[] headData){
+ if(headData.length < WUX_HEADER_SIZE){
+ System.out.println("WUX header length wrong");
+ System.exit(1);
+ }
+ setMagic0(ByteUtils.getIntFromBytes(headData, 0x00,ByteOrder.LITTLE_ENDIAN));
+ setMagic1(ByteUtils.getIntFromBytes(headData, 0x04,ByteOrder.LITTLE_ENDIAN));
+ setSectorSize(ByteUtils.getIntFromBytes(headData, 0x08,ByteOrder.LITTLE_ENDIAN));
+ setFlags(ByteUtils.getIntFromBytes(headData, 0x0C,ByteOrder.LITTLE_ENDIAN));
+ setUncompressedSize(ByteUtils.getLongFromBytes(headData, 0x10,ByteOrder.LITTLE_ENDIAN));
+
+ calculateOffsets();
+ }
+
+ public static WUDImageCompressedInfo getDefaultCompressedInfo(){
+ return new WUDImageCompressedInfo(SECTOR_SIZE, 0, WUDImage.WUD_FILESIZE);
+ }
+
+ public WUDImageCompressedInfo(int sectorSize,int flags, long uncompressedSize) {
+ setMagic0(WUX_MAGIC_0);
+ setMagic1(WUX_MAGIC_1);
+ setSectorSize(sectorSize);
+ setFlags(flags);
+ setUncompressedSize(uncompressedSize);
+ }
+
+ private void calculateOffsets() {
+ long indexTableEntryCount = (getUncompressedSize()+ getSectorSize()-1) / getSectorSize();
+ setIndexTableEntryCount(indexTableEntryCount);
+ setOffsetIndexTable(0x20);
+ long offsetSectorArray = (getOffsetIndexTable() + ((long)getIndexTableEntryCount() * 0x04L));
+ // align to SECTOR_SIZE
+ offsetSectorArray = (offsetSectorArray + (long)(getSectorSize()-1));
+ offsetSectorArray = offsetSectorArray - (offsetSectorArray%(long)getSectorSize());
+ setOffsetSectorArray(offsetSectorArray);
+ // read index table
+ setIndexTableSize(0x04 * getIndexTableEntryCount());
+ }
+
+ public boolean isWUX() {
+ return (getMagic0() == WUX_MAGIC_0 && getMagic1() == WUX_MAGIC_1);
+ }
+
+ @Override
+ public String toString() {
+ return "WUDImageCompressedInfo [magic0=" + String.format("0x%08X", magic0) + ", magic1=" + String.format("0x%08X", magic1) + ", sectorSize=" + String.format("0x%08X", sectorSize)
+ + ", uncompressedSize=" + String.format("0x%016X", uncompressedSize) + ", flags=" + String.format("0x%08X", flags) + ", indexTableEntryCount="
+ + indexTableEntryCount + ", offsetIndexTable=" + offsetIndexTable + ", offsetSectorArray="
+ + offsetSectorArray + ", indexTableSize=" + indexTableSize + "]";
+ }
+
+ public long getSectorIndex(int sectorIndex) {
+ return getIndexTable().get(sectorIndex);
+ }
+
+ public void setIndexTable(Map indexTable) {
+ this.indexTable = indexTable;
+ }
+
+ public byte[] getHeaderAsBytes() {
+ ByteBuffer result = ByteBuffer.allocate(WUX_HEADER_SIZE);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ result.putInt(getMagic0());
+ result.putInt(getMagic1());
+ result.putInt(getSectorSize());
+ result.putInt(getFlags());
+ result.putLong(getUncompressedSize());
+ return result.array();
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/parser/WUDGamePartition.java b/src/de/mas/jnus/lib/implementations/wud/parser/WUDGamePartition.java
new file mode 100644
index 0000000..d6f94a7
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/parser/WUDGamePartition.java
@@ -0,0 +1,12 @@
+package de.mas.jnus.lib.implementations.wud.parser;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper=true)
+public class WUDGamePartition extends WUDPartition {
+ private byte[] rawTMD;
+ private byte[] rawCert;
+ private byte[] rawTicket;
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java b/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java
new file mode 100644
index 0000000..b6e30c2
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java
@@ -0,0 +1,41 @@
+package de.mas.jnus.lib.implementations.wud.parser;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+
+@Data
+public class WUDInfo {
+ private byte[] titleKey = null;
+
+ private WUDDiscReader WUDDiscReader = null;
+ private Map partitions = null;
+
+ @Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PROTECTED)
+ private String gamePartitionName;
+
+ WUDInfo(){
+ }
+
+
+ private WUDGamePartition cachedGamePartition = null;
+ public WUDGamePartition getGamePartition(){
+ if(cachedGamePartition == null){
+ cachedGamePartition = findGamePartition();
+ }
+ return cachedGamePartition;
+ }
+ private WUDGamePartition findGamePartition() {
+ for(Entry e: getPartitions().entrySet()){
+ if(e.getKey().equals(getGamePartitionName())){
+ return (WUDGamePartition) e.getValue();
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfoParser.java b/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfoParser.java
new file mode 100644
index 0000000..5702b06
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfoParser.java
@@ -0,0 +1,155 @@
+package de.mas.jnus.lib.implementations.wud.parser;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import de.mas.jnus.lib.Settings;
+import de.mas.jnus.lib.entities.content.ContentFSTInfo;
+import de.mas.jnus.lib.entities.fst.FST;
+import de.mas.jnus.lib.entities.fst.FSTEntry;
+import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
+import de.mas.jnus.lib.utils.ByteUtils;
+import de.mas.jnus.lib.utils.FileUtils;
+import de.mas.jnus.lib.utils.Utils;
+import lombok.extern.java.Log;
+
+@Log
+public class WUDInfoParser {
+ public static byte[] DECRYPTED_AREA_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0xA6, (byte) 0xE6, 0x7B };
+ public static byte[] PARTITION_FILE_TABLE_SIGNATURE = new byte[] { 0x46, 0x53, 0x54, 0x00 }; // "FST"
+ public final static int PARTITION_TOC_OFFSET = 0x800;
+ public final static int PARTITION_TOC_ENTRY_SIZE = 0x80;
+
+ public static final String WUD_TMD_FILENAME = "title.tmd";
+ public static final String WUD_TICKET_FILENAME = "title.tik";
+ public static final String WUD_CERT_FILENAME = "title.cert";
+
+ public static WUDInfo createAndLoad(WUDDiscReader discReader,byte[] titleKey) throws IOException {
+ WUDInfo result = new WUDInfo();
+
+ result.setTitleKey(titleKey);
+ result.setWUDDiscReader(discReader);
+
+ byte[] PartitionTocBlock = discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET, 0, 0x8000, titleKey, null);
+
+ // verify DiscKey before proceeding
+ if(!Arrays.equals(Arrays.copyOfRange(PartitionTocBlock, 0, 4),DECRYPTED_AREA_SIGNATURE)){
+ System.out.println("Decryption of PartitionTocBlock failed");
+ return null;
+ }
+
+ Map partitions = readPartitions(result,PartitionTocBlock);
+ result.setPartitions(partitions);
+ //parsePartitions(wudInfo,partitions);
+
+ return result;
+ }
+
+
+ private static Map readPartitions(WUDInfo wudInfo,byte[] partitionTocBlock) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(partitionTocBlock.length);
+
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ buffer.put(partitionTocBlock);
+ buffer.position(0);
+
+ int partitionCount = (int) ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, 0x1C,ByteOrder.BIG_ENDIAN);
+
+ Map partitions = new HashMap<>();
+
+ WUDGamePartition gamePartition = new WUDGamePartition();
+
+ String realGamePartitionName = null;
+ // populate partition information from decrypted TOC
+ for (int i = 0; i < partitionCount; i++){
+ WUDPartition partition = new WUDPartition();
+
+ int offset = (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE));
+ byte[] partitionIdentifier = Arrays.copyOfRange(partitionTocBlock, offset, offset+ 0x19);
+ int j = 0;
+ for(j = 0;j>16).array();
+
+ return discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + (long)partition.getPartitionOffset() + (long)info.getOffset(),entry.getFileOffset(), (int) entry.getFileSize(), key, IV);
+ }
+
+ private static FSTEntry getEntryByName(FSTEntry root,String name){
+ for(FSTEntry cur : root.getFileChildren()){
+ if(cur.getFilename().equals(name)){
+ return cur;
+ }
+ }
+ for(FSTEntry cur : root.getDirChildren()){
+ FSTEntry dir_result = getEntryByName(cur,name);
+ if(dir_result != null){
+ return dir_result;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartition.java b/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartition.java
new file mode 100644
index 0000000..ea47926
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartition.java
@@ -0,0 +1,10 @@
+package de.mas.jnus.lib.implementations.wud.parser;
+
+import lombok.Data;
+@Data
+public class WUDPartition {
+ private String partitionName = "";
+ private long partitionOffset = 0;
+
+ private WUDPartitionHeader partitionHeader;
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartitionHeader.java b/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartitionHeader.java
new file mode 100644
index 0000000..b6b5f01
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartitionHeader.java
@@ -0,0 +1,96 @@
+package de.mas.jnus.lib.implementations.wud.parser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.mas.jnus.lib.entities.content.Content;
+import de.mas.jnus.lib.utils.ByteUtils;
+import de.mas.jnus.lib.utils.HashUtil;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.java.Log;
+
+@Log
+public class WUDPartitionHeader {
+ @Getter @Setter
+ private boolean calculatedHashes = false;
+ private HashMap h3Hashes;
+ @Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PRIVATE)
+ private byte[] rawData;
+
+ private WUDPartitionHeader(){
+ }
+
+ //TODO: real processing. Currently we are ignoring everything except the hashes
+ public static WUDPartitionHeader parseHeader(byte[] header) {
+ WUDPartitionHeader result = new WUDPartitionHeader();
+ result.setRawData(header);
+ return result;
+ }
+
+ public HashMap getH3Hashes() {
+ if(h3Hashes == null){
+ h3Hashes = new HashMap<>();
+ }
+ return h3Hashes;
+ }
+
+ public void addH3Hashes(short index, byte[] hash) {
+ getH3Hashes().put(index, hash);
+ }
+
+ public byte[] getH3Hash(Content content) {
+ if(content == null){
+ log.info("Can't find h3 hash, given content is null.");
+ return null;
+ }
+
+ return getH3Hashes().get(content.getIndex());
+ }
+
+ public void calculateHashes(Map allContents) {
+ byte[] header = getRawData();
+
+ //Calculating offset for the hashes
+ int cnt = ByteUtils.getIntFromBytes(header,0x10);
+ int start_offset = 0x40 + cnt*0x04;
+
+ int offset = 0;
+
+ //We have to make sure, that the list is ordered by index
+ List contents = new ArrayList<>(allContents.values());
+ Collections.sort( contents, new Comparator() {
+ @Override
+ public int compare(Content o1, Content o2) {
+ return Short.compare(o1.getIndex(), o2.getIndex());
+ }
+ } );
+
+ for(Content c : allContents.values()){
+ if(!c.isHashed() || !c.isEncrypted()){
+ continue;
+ }
+
+ //The encrypted content are splitted in 0x10000 chunk. For each 0x1000 chunk we need one entry in the h3
+ int cnt_hashes = (int) (c.getEncryptedFileSize()/0x10000/0x1000)+1;
+
+ byte[] hash = Arrays.copyOfRange(header, start_offset+ offset*0x14, start_offset+ (offset+cnt_hashes)*0x14);
+
+ //Checking the hash of the h3 file.
+ if(!Arrays.equals(HashUtil.hashSHA1(hash), c.getSHA2Hash())){
+ log.info("h3 incorrect from WUD");
+ }
+
+ addH3Hashes(c.getIndex(), hash);
+ offset += cnt_hashes;
+ }
+
+ setCalculatedHashes(true);
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReader.java b/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReader.java
new file mode 100644
index 0000000..14830ec
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReader.java
@@ -0,0 +1,127 @@
+package de.mas.jnus.lib.implementations.wud.reader;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+
+import de.mas.jnus.lib.implementations.wud.WUDImage;
+import de.mas.jnus.lib.utils.cryptography.AESDecryption;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.java.Log;
+@Log
+public abstract class WUDDiscReader {
+ @Getter @Setter private WUDImage image = null;
+
+ public WUDDiscReader(WUDImage image){
+ setImage(image);
+ }
+
+ public InputStream readEncryptedToInputStream(long offset,long size) throws IOException {
+ PipedInputStream in = new PipedInputStream();
+ PipedOutputStream out = new PipedOutputStream(in);
+
+ new Thread(() -> {try {
+ readEncryptedToOutputStream(out,offset,size);
+ } catch (IOException e) {e.printStackTrace();}}).start();
+
+ return in;
+ }
+
+ public byte[] readEncryptedToByteArray(long offset,long fileoffset, long size) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ readEncryptedToOutputStream(out,offset,size);
+ return out.toByteArray();
+ }
+
+ public InputStream readDecryptedToInputStream(long offset,long fileoffset, long size, byte[] key,byte[] iv) throws IOException {
+ PipedInputStream in = new PipedInputStream();
+ PipedOutputStream out = new PipedOutputStream(in);
+
+ new Thread(() -> {try {
+ readDecryptedToOutputStream(out,offset,fileoffset,size,key,iv);
+ } catch (IOException e) {e.printStackTrace();}}).start();
+
+ return in;
+ }
+
+ public byte[] readDecryptedToByteArray(long offset,long fileoffset, long size, byte[] key,byte[] iv) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ readDecryptedToOutputStream(out,offset,fileoffset,size,key,iv);
+ return out.toByteArray();
+ }
+
+ protected abstract void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException;
+
+ /**
+ *
+ * @param readOffset Needs to be aligned to 0x8000
+ * @param key
+ * @param IV
+ * @return
+ * @throws IOException
+ */
+ public byte[] readDecryptedChunk(long readOffset,byte[] key, byte[]IV) throws IOException{
+ int chunkSize = 0x8000;
+
+ byte[] encryptedChunk = readEncryptedToByteArray(readOffset, 0, chunkSize);
+ byte[] decryptedChunk = new byte[chunkSize];
+
+ AESDecryption aesDecryption = new AESDecryption(key, IV);
+ decryptedChunk = aesDecryption.decrypt(encryptedChunk);
+
+ return decryptedChunk;
+ }
+
+ public void readDecryptedToOutputStream(OutputStream outputStream,long clusterOffset, long fileOffset, long size,byte[] key,byte[] IV) throws IOException {
+ if(IV == null){
+ IV = new byte[0x10];
+ }
+
+ byte[] buffer;
+
+ long maxCopySize;
+ long copySize;
+
+ long readOffset;
+
+ int blockSize = 0x8000;
+ long totalread = 0;
+
+ do{
+ long blockNumber = (fileOffset / blockSize);
+ long blockOffset = (fileOffset % blockSize);
+
+ readOffset = clusterOffset + (blockNumber * blockSize);
+ // (long)WiiUDisc.WIIU_DECRYPTED_AREA_OFFSET + volumeOffset + clusterOffset + (blockStructure.getBlockNumber() * 0x8000);
+
+ buffer = readDecryptedChunk(readOffset,key, IV);
+ maxCopySize = 0x8000 - blockOffset;
+ copySize = (size > maxCopySize) ? maxCopySize : size;
+
+ outputStream.write(Arrays.copyOfRange(buffer, (int) blockOffset, (int) copySize));
+ totalread += copySize;
+
+ // update counters
+ size -= copySize;
+ fileOffset += copySize;
+ }while(totalread < size);
+
+ outputStream.close();
+ }
+
+ public RandomAccessFile getRandomAccessFileStream() throws FileNotFoundException{
+ if(getImage() == null || getImage().getFileHandle() == null){
+ log.warning("No image or image filehandle set.");
+ System.exit(1);
+ }
+ return new RandomAccessFile(getImage().getFileHandle(), "r");
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderCompressed.java b/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderCompressed.java
new file mode 100644
index 0000000..66a23b8
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderCompressed.java
@@ -0,0 +1,69 @@
+package de.mas.jnus.lib.implementations.wud.reader;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.Arrays;
+
+import de.mas.jnus.lib.implementations.wud.WUDImage;
+import de.mas.jnus.lib.implementations.wud.WUDImageCompressedInfo;
+import lombok.extern.java.Log;
+
+@Log
+public class WUDDiscReaderCompressed extends WUDDiscReader{
+
+ public WUDDiscReaderCompressed(WUDImage image) {
+ super(image);
+ }
+/**
+ * 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
+ protected void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException {
+ // make sure there is no out-of-bounds read
+ WUDImageCompressedInfo info = getImage().getCompressedInfo();
+
+ long fileBytesLeft = info.getUncompressedSize() - offset;
+ if( fileBytesLeft <= 0 ){
+ log.warning("offset too big");
+ System.exit(1);
+ }
+ if( fileBytesLeft < size ){
+ size = fileBytesLeft;
+ }
+ // compressed read must be handled on a per-sector level
+
+ int bufferSize = 0x8000;
+ byte[] buffer = new byte[bufferSize];
+
+ RandomAccessFile input = getRandomAccessFileStream();
+ while( size > 0 ){
+ long sectorOffset = (offset % info.getSectorSize());
+ long remainingSectorBytes = info.getSectorSize() - sectorOffset;
+ long sectorIndex = (offset / info.getSectorSize());
+ int bytesToRead = (int) ((remainingSectorBytes= WUD_SPLITTED_FILE_SIZE){ //Will we read above the part?
+ long toRead = WUD_SPLITTED_FILE_SIZE - offsetInFile;
+ if(toRead == 0){ //just load the new file
+ input.close();
+ input = getFileByOffset(curOffset);
+ part++;
+ offsetInFile = getOffsetInFilePart(part, curOffset);
+ }else{
+ curReadSize = (int) toRead; //And first only read until the part ends
+ }
+ }
+
+ int read = input.read(buffer,0,curReadSize);
+ if(read < 0) break;
+ if(totalread + read > size){
+ read = (int) (size - totalread);
+ }
+ try{
+ outputStream.write(Arrays.copyOfRange(buffer, 0, read));
+ }catch(IOException e){
+ if(e.getMessage().equals("Pipe closed")){
+ break;
+ }else{
+ input.close();
+ throw e;
+ }
+ }
+ totalread += read;
+ curOffset += read;
+ }while(totalread < size);
+
+
+ input.close();
+ outputStream.close();
+ }
+
+ private int getFilePartByOffset(long offset){
+ return (int) (offset/WUD_SPLITTED_FILE_SIZE)+1;
+ }
+
+ private long getOffsetInFilePart(int part,long offset){
+ return offset - ((long)(part-1) * WUD_SPLITTED_FILE_SIZE);
+ }
+
+ private RandomAccessFile getFileByOffset(long offset) throws IOException{
+ File filehandlePart1 = getImage().getFileHandle();
+ String pathToFiles = filehandlePart1.getParentFile().getAbsolutePath();
+
+ int filePart = getFilePartByOffset(offset);
+
+ String filePartPath = pathToFiles + File.separator + String.format(WUD_SPLITTED_DEFAULT_FILEPATTERN, filePart);
+
+ File part = new File(filePartPath);
+
+ if(!part.exists()){
+ System.out.println("File does not exist");
+ return null;
+ }
+ RandomAccessFile result = new RandomAccessFile(part, "r");
+ result.seek(getOffsetInFilePart(filePart, offset));
+ return result;
+ }
+}
diff --git a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderUncompressed.java b/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderUncompressed.java
new file mode 100644
index 0000000..3dfb174
--- /dev/null
+++ b/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderUncompressed.java
@@ -0,0 +1,45 @@
+package de.mas.jnus.lib.implementations.wud.reader;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import de.mas.jnus.lib.implementations.wud.WUDImage;
+
+public class WUDDiscReaderUncompressed extends WUDDiscReader {
+ public WUDDiscReaderUncompressed(WUDImage image) {
+ super(image);
+ }
+
+ @Override
+ protected void readEncryptedToOutputStream(OutputStream outputStream, long offset,long size) throws IOException{
+
+ FileInputStream input = new FileInputStream(getImage().getFileHandle());
+ input.skip(offset);
+ int bufferSize = 0x8000;
+ byte[] buffer = new byte[bufferSize];
+ long totalread = 0;
+ do{
+ int read = input.read(buffer);
+ if(read < 0) break;
+ if(totalread + read > size){
+ read = (int) (size - totalread);
+ }
+ try{
+ outputStream.write(Arrays.copyOfRange(buffer, 0, read));
+ }catch(IOException e){
+ if(e.getMessage().equals("Pipe closed")){
+ break;
+ }else{
+ input.close();
+ throw e;
+ }
+ }
+ totalread += read;
+ }while(totalread < size);
+ input.close();
+ outputStream.close();
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/utils/ByteArrayBuffer.java b/src/de/mas/jnus/lib/utils/ByteArrayBuffer.java
new file mode 100644
index 0000000..0e7d844
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/ByteArrayBuffer.java
@@ -0,0 +1,25 @@
+package de.mas.jnus.lib.utils;
+
+import lombok.Getter;
+import lombok.Setter;
+
+public class ByteArrayBuffer{
+ @Getter public byte[] buffer;
+ @Getter @Setter int lengthOfDataInBuffer;
+
+ public ByteArrayBuffer(int length){
+ buffer = new byte[(int) length];
+ }
+
+ public int getSpaceLeft() {
+ return buffer.length - getLengthOfDataInBuffer();
+ }
+
+ public void addLengthOfDataInBuffer(int bytesRead) {
+ lengthOfDataInBuffer += bytesRead;
+ }
+
+ public void resetLengthOfDataInBuffer() {
+ setLengthOfDataInBuffer(0);
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/ByteArrayWrapper.java b/src/de/mas/jnus/lib/utils/ByteArrayWrapper.java
new file mode 100644
index 0000000..5af776b
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/ByteArrayWrapper.java
@@ -0,0 +1,33 @@
+package de.mas.jnus.lib.utils;
+
+import java.util.Arrays;
+
+public final class ByteArrayWrapper
+{
+ private final byte[] data;
+
+ public ByteArrayWrapper(byte[] data)
+ {
+ if (data == null)
+ {
+ throw new NullPointerException();
+ }
+ this.data = data;
+ }
+
+ @Override
+ public boolean equals(Object other)
+ {
+ if (!(other instanceof ByteArrayWrapper))
+ {
+ return false;
+ }
+ return Arrays.equals(data, ((ByteArrayWrapper)other).data);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Arrays.hashCode(data);
+ }
+}
\ No newline at end of file
diff --git a/src/de/mas/jnus/lib/utils/ByteUtils.java b/src/de/mas/jnus/lib/utils/ByteUtils.java
new file mode 100644
index 0000000..aa1d200
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/ByteUtils.java
@@ -0,0 +1,76 @@
+package de.mas.jnus.lib.utils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+public class ByteUtils {
+ public static int getIntFromBytes(byte[] input,int offset){
+ return getIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
+ }
+ public static int getIntFromBytes(byte[] input,int offset,ByteOrder bo){
+ ByteBuffer buffer = ByteBuffer.allocate(4);
+ buffer.order(bo);
+ Arrays.copyOfRange(input,offset, offset+4);
+ buffer.put(Arrays.copyOfRange(input,offset, offset+4));
+
+ return buffer.getInt(0);
+ }
+
+ public static long getUnsingedIntFromBytes(byte[] input,int offset){
+ return getUnsingedIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
+ }
+
+ public static long getUnsingedIntFromBytes(byte[] input,int offset,ByteOrder bo){
+ ByteBuffer buffer = ByteBuffer.allocate(8);
+ buffer.order(bo);
+ if(bo.equals(ByteOrder.BIG_ENDIAN)){
+ buffer.position(4);
+ }else{
+ buffer.position(0);
+ }
+ buffer.put(Arrays.copyOfRange(input,offset, offset+4));
+
+ return buffer.getLong(0);
+ }
+
+ public static long getLongFromBytes(byte[] input,int offset){
+ return getLongFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
+ }
+
+ public static long getLongFromBytes(byte[] input,int offset,ByteOrder bo){
+ return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+8)).order(bo).getLong(0);
+ }
+
+ public static short getShortFromBytes(byte[] input,int offset){
+ return getShortFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
+ }
+ public static short getShortFromBytes(byte[] input, int offset,ByteOrder bo) {
+ return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+2)).order(bo).getShort();
+ }
+
+ public static byte[] getBytesFromLong(long value) {
+ return getBytesFromLong(value,ByteOrder.BIG_ENDIAN);
+ }
+ public static byte[] getBytesFromLong(long value,ByteOrder bo) {
+ byte[] result = new byte[0x08];
+ ByteBuffer.allocate(8).order(bo).putLong(value).get(result);
+ return result;
+ }
+
+ public static byte[] getBytesFromInt(int value) {
+ return getBytesFromInt(value,ByteOrder.BIG_ENDIAN);
+ }
+ public static byte[] getBytesFromInt(int value,ByteOrder bo) {
+ byte[] result = new byte[0x04];
+ ByteBuffer.allocate(4).order(bo).putInt(value).get(result);
+ return result;
+ }
+
+ public static byte[] getBytesFromShort(short value) {
+ byte[] result = new byte[0x02];
+ ByteBuffer.allocate(2).putShort(value).get(result);
+ return result;
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/utils/FileUtils.java b/src/de/mas/jnus/lib/utils/FileUtils.java
new file mode 100644
index 0000000..dfbd121
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/FileUtils.java
@@ -0,0 +1,52 @@
+package de.mas.jnus.lib.utils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import lombok.NonNull;
+
+public class FileUtils {
+
+ public static boolean saveByteArrayToFile(String filePath,byte[] data) throws IOException {
+ File target = new File(filePath);
+ if(target.isDirectory()){
+ return false;
+ }
+ File parent = target.getParentFile();
+ if(parent != null){
+ Utils.createDir(parent.getAbsolutePath());
+ }
+ return saveByteArrayToFile(target,data);
+ }
+
+ /**
+ * Saves a byte array to a file (and overwrite it if its already exists)
+ * DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
+ * @param output
+ * @param data
+ * @return
+ * @throws IOException
+ */
+ public static boolean saveByteArrayToFile(@NonNull File output,byte[] data) throws IOException {
+ FileOutputStream out = new FileOutputStream(output);
+ out.write(data);
+ out.close();
+ return true;
+ }
+
+ /**
+ * Saves a byte array to a file (and overwrite it if its already exists)
+ * DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
+ * @param output
+ * @param inputStream
+ * @throws IOException
+ */
+ public static void saveInputStreamToFile(@NonNull File output,InputStream inputStream,long filesize) throws IOException {
+
+ FileOutputStream out = new FileOutputStream(output);
+ StreamUtils.saveInputStreamToOutputStream(inputStream,out,filesize);
+ }
+
+}
diff --git a/src/de/mas/jnus/lib/utils/HashUtil.java b/src/de/mas/jnus/lib/utils/HashUtil.java
new file mode 100644
index 0000000..ea30b97
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/HashUtil.java
@@ -0,0 +1,185 @@
+package de.mas.jnus.lib.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import lombok.extern.java.Log;
+@Log
+public class HashUtil {
+ public static byte[] hashSHA256(byte[] data){
+ MessageDigest sha256;
+ try {
+ sha256 = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return new byte[0x20];
+ }
+
+ return sha256.digest(data);
+ }
+
+ public static byte[] hashSHA256(File file) {
+ return hashSHA256(file, 0);
+ }
+
+ //TODO: testing
+ public static byte[] hashSHA256(File file,int aligmnent) {
+ byte[] hash = new byte[0x20];
+ MessageDigest sha1 = null;
+ try {
+ InputStream in = new FileInputStream(file);
+ sha1 = MessageDigest.getInstance("SHA-256");
+ hash = hash(sha1,in,file.length(),0x8000,aligmnent);
+ } catch (NoSuchAlgorithmException | FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return hash;
+ }
+
+ public static byte[] hashSHA1(byte[] data){
+ MessageDigest sha1;
+ try {
+ sha1 = MessageDigest.getInstance("SHA1");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return new byte[0x14];
+ }
+
+ return sha1.digest(data);
+ }
+
+ public static byte[] hashSHA1(File file) {
+ return hashSHA1(file, 0);
+ }
+
+ public static byte[] hashSHA1(File file,int aligmnent) {
+ byte[] hash = new byte[0x14];
+ MessageDigest sha1 = null;
+ try {
+ InputStream in = new FileInputStream(file);
+ sha1 = MessageDigest.getInstance("SHA1");
+ hash = hash(sha1,in,file.length(),0x8000,aligmnent);
+ } catch (NoSuchAlgorithmException | FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return hash;
+ }
+
+ public static byte [] hash(MessageDigest digest, InputStream in,long inputSize1, int bufferSize,int alignment) throws IOException {
+ long target_size = alignment == 0 ? inputSize1: Utils.align(inputSize1, alignment);
+ long cur_position = 0;
+ int inBlockBufferRead = 0;
+ byte[] blockBuffer = new byte[bufferSize];
+ ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
+ do{
+ if(cur_position + bufferSize > target_size){
+ int expectedSize = (int) (target_size - cur_position);
+ ByteBuffer buffer = ByteBuffer.allocate(expectedSize);
+ buffer.position(0);
+ inBlockBufferRead = StreamUtils.getChunkFromStream(in,blockBuffer,overflow,expectedSize);
+ buffer.put(Arrays.copyOfRange(blockBuffer, 0, inBlockBufferRead));
+ blockBuffer = buffer.array();
+ inBlockBufferRead = blockBuffer.length;
+ }else{
+ int expectedSize = bufferSize;
+ inBlockBufferRead = StreamUtils.getChunkFromStream(in,blockBuffer,overflow,expectedSize);
+ }
+ if(inBlockBufferRead == 0){
+ inBlockBufferRead = (int) (target_size - cur_position);
+ blockBuffer = new byte[inBlockBufferRead];
+ }
+ if(inBlockBufferRead <= 0) break;
+
+ digest.update(blockBuffer, 0, inBlockBufferRead);
+ cur_position += inBlockBufferRead;
+
+ }while(cur_position < target_size);
+
+ in.close();
+
+ return digest.digest();
+ }
+
+ public static void checkFileChunkHashes(byte[] hashes,byte[] h3Hashes, byte[] output,int block) {
+ int H0_start = (block % 16) * 20;
+ int H1_start = (16 + (block / 16) % 16) * 20;
+ int H2_start = (32 + (block / 256) % 16) * 20;
+ int H3_start = ((block / 4096) % 16) * 20;
+
+ byte[] real_h0_hash = HashUtil.hashSHA1(output);
+ byte[] expected_h0_hash = Arrays.copyOfRange(hashes,H0_start,H0_start + 20);
+
+ if(!Arrays.equals(real_h0_hash,expected_h0_hash)){
+ System.out.println("h0 checksum failed");
+ System.out.println("real hash :" + Utils.ByteArrayToString(real_h0_hash));
+ System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h0_hash));
+ System.exit(2);
+ //throw new IllegalArgumentException("h0 checksumfail");
+ }else{
+ log.finest("h1 checksum right!");
+ }
+
+ if ((block % 16) == 0){
+ byte[] expected_h1_hash = Arrays.copyOfRange(hashes,H1_start,H1_start + 20);
+ byte[] real_h1_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H0_start,H0_start + (16*20)));
+
+ if(!Arrays.equals(expected_h1_hash, real_h1_hash)){
+ System.out.println("h1 checksum failed");
+ System.out.println("real hash :" + Utils.ByteArrayToString(real_h1_hash));
+ System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h1_hash));
+ System.exit(2);
+ //throw new IllegalArgumentException("h1 checksumfail");
+ }else{
+ log.finer("h1 checksum right!");
+ }
+ }
+
+ if ((block % 256) == 0){
+ byte[] expected_h2_hash = Arrays.copyOfRange(hashes,H2_start,H2_start + 20);
+ byte[] real_h2_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H1_start,H1_start + (16*20)));
+
+ if(!Arrays.equals(expected_h2_hash, real_h2_hash)){
+ System.out.println("h2 checksum failed");
+ System.out.println("real hash :" + Utils.ByteArrayToString(real_h2_hash));
+ System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h2_hash));
+ System.exit(2);
+ //throw new IllegalArgumentException("h2 checksumfail");
+
+ }else{
+ log.fine("h2 checksum right!");
+ }
+ }
+
+ if(h3Hashes == null){
+ log.info("didn't check the h3, its missing.");
+ return;
+ }
+ if ((block % 4096) == 0){
+ byte[] expected_h3_hash = Arrays.copyOfRange(h3Hashes,H3_start,H3_start + 20);
+ byte[] real_h3_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H2_start,H2_start + (16*20)));
+
+ if(!Arrays.equals(expected_h3_hash, real_h3_hash)){
+ System.out.println("h3 checksum failed");
+ System.out.println("real hash :" + Utils.ByteArrayToString(real_h3_hash));
+ System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h3_hash));
+ System.exit(2);
+ //throw new IllegalArgumentException("h3 checksumfail");
+ }else{
+ log.fine("h3 checksum right!");
+ }
+ }
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/StreamUtils.java b/src/de/mas/jnus/lib/utils/StreamUtils.java
new file mode 100644
index 0000000..df3d9a7
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/StreamUtils.java
@@ -0,0 +1,127 @@
+package de.mas.jnus.lib.utils;
+
+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 lombok.extern.java.Log;
+
+@Log
+public class StreamUtils {
+ public static byte[] getBytesFromStream(InputStream in,int size) throws IOException{
+ byte[] result = new byte[size];
+ byte[] buffer = new byte[0x8000];
+ int totalRead = 0;
+ do{
+ int read = in.read(buffer);
+ if(read < 0) break;
+ System.arraycopy(buffer, 0, result, totalRead, read);
+ totalRead += read;
+ }while(totalRead < size);
+ in.close();
+ return result;
+ }
+
+ public static int getChunkFromStream(InputStream inputStream,byte[] output, ByteArrayBuffer overflowbuffer,int BLOCKSIZE) throws IOException {
+ int bytesRead = -1;
+ int inBlockBuffer = 0;
+ byte[] overflowbuf = overflowbuffer.getBuffer();
+ do{
+ try{
+ bytesRead = inputStream.read(overflowbuf,overflowbuffer.getLengthOfDataInBuffer(),overflowbuffer.getSpaceLeft());
+ }catch(IOException e){
+ if(!e.getMessage().equals("Write end dead")){
+ throw e;
+ }
+ bytesRead = -1;
+ }
+ if(bytesRead <= 0){
+ if(overflowbuffer.getLengthOfDataInBuffer() > 0){
+ System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer());
+ inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer();
+ }
+ break;
+ }
+
+ overflowbuffer.addLengthOfDataInBuffer(bytesRead);
+
+ if(inBlockBuffer + overflowbuffer.getLengthOfDataInBuffer() > BLOCKSIZE){
+ int tooMuch = (inBlockBuffer + bytesRead) - BLOCKSIZE;
+ int toRead = BLOCKSIZE - inBlockBuffer;
+
+ System.arraycopy(overflowbuf, 0, output, inBlockBuffer, toRead);
+ inBlockBuffer += toRead;
+
+ System.arraycopy(overflowbuf, toRead, overflowbuf, 0, tooMuch);
+ overflowbuffer.setLengthOfDataInBuffer(tooMuch);
+ }else{
+ System.arraycopy(overflowbuf, 0, output, inBlockBuffer, overflowbuffer.getLengthOfDataInBuffer());
+ inBlockBuffer +=overflowbuffer.getLengthOfDataInBuffer();
+ overflowbuffer.resetLengthOfDataInBuffer();
+ }
+ }while(inBlockBuffer != BLOCKSIZE);
+ return inBlockBuffer;
+ }
+
+ public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
+ saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null,0L);
+ }
+
+ public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream,long filesize, byte[] hash,long expectedSizeForHash) throws IOException {
+ MessageDigest sha1 = null;
+ if(hash != null){
+ try {
+ sha1 = MessageDigest.getInstance("SHA1");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int BUFFER_SIZE = 0x8000;
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int read = 0;
+ long totalRead = 0;
+ long written = 0;
+ do{
+ read = inputStream.read(buffer);
+ if(read < 0){
+ break;
+ }
+ totalRead +=read;
+
+ if(totalRead > filesize){
+ read = (int) (read - (totalRead - filesize));
+ }
+
+ outputStream.write(buffer, 0, read);
+ written += read;
+
+ if(sha1 != null){
+ sha1.update(buffer,0,read);
+ }
+ }while(written < filesize);
+
+ if(sha1 != null){
+ long missingInHash = expectedSizeForHash - written;
+ if(missingInHash > 0){
+ sha1.update(new byte[(int) missingInHash]);
+ }
+
+ byte[] calculated_hash = sha1.digest();
+ byte[] expected_hash = hash;
+ if(!Arrays.equals(calculated_hash, expected_hash)){
+ log.info(Utils.ByteArrayToString(calculated_hash));
+ log.info(Utils.ByteArrayToString(expected_hash));
+ log.info("Hash doesn't match saves output stream.");
+ }else{
+ //log.warning("Hash DOES match saves output stream.");
+ }
+ }
+
+ outputStream.close();
+ inputStream.close();
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/Utils.java b/src/de/mas/jnus/lib/utils/Utils.java
new file mode 100644
index 0000000..22dfd9d
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/Utils.java
@@ -0,0 +1,66 @@
+package de.mas.jnus.lib.utils;
+
+import java.io.File;
+
+public class Utils {
+
+ public static long align(long numToRound, int multiple){
+ if((multiple>0) && ((multiple & (multiple -1)) == 0)){
+ return alignPower2(numToRound, multiple);
+ }else{
+ return alignGeneriv(numToRound,multiple);
+ }
+ }
+
+ private static long alignGeneriv(long numToRound,int multiple){
+ int isPositive = 0;
+ if(numToRound >=0){
+ isPositive = 1;
+ }
+ return ((numToRound + isPositive * (multiple - 1)) / multiple) * multiple;
+ }
+
+ private static long alignPower2(long numToRound, int multiple)
+ {
+ if(!((multiple>0) && ((multiple & (multiple -1)) == 0))) return 0L;
+ return (numToRound + (multiple - 1)) & ~(multiple - 1);
+ }
+
+ public static String ByteArrayToString(byte[] ba)
+ {
+ if(ba == null) return null;
+ StringBuilder hex = new StringBuilder(ba.length * 2);
+ for(byte b : ba){
+ hex.append(String.format("%02X", b));
+ }
+ return hex.toString();
+ }
+
+ public static byte[] StringToByteArray(String s) {
+ int len = s.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i+1), 16));
+ }
+ return data;
+ }
+
+ public static boolean createDir(String path){
+ if(path == null || path.isEmpty()){
+ return false;
+ }
+ File pathFile = new File(path);
+ if(!pathFile.exists()){
+ boolean succes = new File(path).mkdirs();
+ if(!succes){
+ System.out.println("Creating directory \"" +path + "\" failed.");
+ return false;
+ }
+ }else if(!pathFile.isDirectory()){
+ System.out.println("\"" +path + "\" already exists but is no directoy");
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/XMLParser.java b/src/de/mas/jnus/lib/utils/XMLParser.java
new file mode 100644
index 0000000..8842219
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/XMLParser.java
@@ -0,0 +1,77 @@
+package de.mas.jnus.lib.utils;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import com.sun.istack.internal.NotNull;
+
+public class XMLParser {
+ private Document document;
+
+ public void loadDocument(InputStream inputStream) throws ParserConfigurationException, SAXException, IOException{
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ Document document = builder.parse(inputStream);
+ this.document = document;
+
+ }
+
+ public long getValueOfElementAsInt(String element,int index){
+ return Integer.parseInt(getValueOfElement(element,index));
+ }
+
+ public long getValueOfElementAsLong(String element,int index){
+ return Long.parseLong(getValueOfElement(element,index));
+ }
+
+ public String getValueOfElement(String element){
+ return getValueOfElement(element,0);
+ }
+
+ public Node getNodeByValue(String element){
+ return getNodeByValue(element, 0);
+ }
+ public Node getNodeByValue(String element,int index){
+ if(document == null){
+ System.out.println("Please load the document first.");
+ }
+ NodeList list = document.getElementsByTagName(element);
+ if(list == null){
+ return null;
+ }
+ return list.item(index);
+ }
+
+ public String getValueOfElementAttribute(String element,int index,String attribute){
+ Node node = getNodeByValue(element, index);
+ if(node == null){
+ //System.out.println("Node is null");
+ return "";
+ }
+ return getAttributeValueFromNode(node,attribute);
+ }
+
+ public static String getAttributeValueFromNode(@NotNull Node element,String attribute){
+ return element.getAttributes().getNamedItem(attribute).getTextContent().toString();
+ }
+
+ public String getValueOfElement(String element,int index){
+ Node node = getNodeByValue(element, index);
+ if(node == null){
+ //System.out.println("Node is null");
+ return "";
+ }
+
+ return node.getTextContent().toString();
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java b/src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java
new file mode 100644
index 0000000..f63256f
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java
@@ -0,0 +1,71 @@
+package de.mas.jnus.lib.utils.cryptography;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+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;
+
+public class AESDecryption {
+ private Cipher cipher;
+
+ @Getter @Setter private byte[] AESKey;
+ @Getter @Setter private byte[] IV;
+
+ public AESDecryption(byte[] AESKey, byte[] IV) {
+ try {
+ cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ e.printStackTrace();
+ }
+ setAESKey(AESKey);
+ setIV(IV);
+ init();
+ }
+
+ protected void init() {
+ init(getAESKey(),getIV());
+ }
+
+ protected void init(byte[] decryptedKey,byte[] iv){
+ SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ System.exit(2);
+ }
+ }
+
+ public byte[] decrypt(byte[] input){
+ try {
+ return cipher.doFinal(input);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ e.printStackTrace();
+ System.exit(2);
+ }
+ return input;
+ }
+
+ public byte[] decrypt(byte[] input,int len){
+ return decrypt(input,0,len);
+ }
+
+ public byte[] decrypt(byte[] input,int offset,int len){
+ try {
+ return cipher.doFinal(input, offset, len);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ e.printStackTrace();
+ System.exit(2);
+ }
+ return input;
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java b/src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java
new file mode 100644
index 0000000..468086b
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java
@@ -0,0 +1,171 @@
+package de.mas.jnus.lib.utils.cryptography;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import de.mas.jnus.lib.entities.Ticket;
+import de.mas.jnus.lib.utils.ByteArrayBuffer;
+import de.mas.jnus.lib.utils.HashUtil;
+import de.mas.jnus.lib.utils.StreamUtils;
+import de.mas.jnus.lib.utils.Utils;
+import lombok.extern.java.Log;
+@Log
+public class NUSDecryption extends AESDecryption{
+ public NUSDecryption(byte[] AESKey, byte[] IV) {
+ super(AESKey, IV);
+ }
+
+ public NUSDecryption(Ticket ticket) {
+ this(ticket.getDecryptedKey(),ticket.getIV());
+ }
+
+ private byte[] decryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) {
+ return decryptFileChunk(blockBuffer,0,BLOCKSIZE, IV);
+ }
+
+ private byte[] decryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) {
+ if(IV != null){
+ setIV(IV);
+ init();
+ }
+ return decrypt(blockBuffer,offset,BLOCKSIZE);
+ }
+
+ public void decryptFileStream(InputStream inputStream,OutputStream outputStream,long filesize,short contentIndex,byte[] h3hash,long expectedSizeForHash) throws IOException {
+ MessageDigest sha1 = null;
+ if(h3hash != null){
+ try {
+ sha1 = MessageDigest.getInstance("SHA1");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int BLOCKSIZE = 0x8000;
+ long dlFileLength = filesize;
+ if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){
+ dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE;
+ }
+
+ byte[] IV = new byte[0x10];
+
+ IV[0] = (byte)((contentIndex >> 8) & 0xFF);
+ IV[1] = (byte)(contentIndex);
+
+ byte[] blockBuffer = new byte[BLOCKSIZE];
+
+ int inBlockBuffer;
+ long written = 0;
+
+ ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
+
+ do{
+ inBlockBuffer = StreamUtils.getChunkFromStream(inputStream,blockBuffer,overflow,BLOCKSIZE);
+
+ byte[] output = decryptFileChunk(blockBuffer,BLOCKSIZE,IV);
+ IV = Arrays.copyOfRange(blockBuffer,BLOCKSIZE-16, BLOCKSIZE);
+
+ if((written + inBlockBuffer) > filesize){
+ inBlockBuffer = (int) (filesize- written);
+ }
+
+ written += inBlockBuffer;
+
+ outputStream.write(output, 0, inBlockBuffer);
+
+ if(sha1 != null){
+ sha1.update(output,0,inBlockBuffer);
+ }
+ }while(inBlockBuffer == BLOCKSIZE);
+
+ if(sha1 != null){
+ long missingInHash = expectedSizeForHash - written;
+ if(missingInHash > 0){
+ sha1.update(new byte[(int) missingInHash]);
+ }
+
+ byte[] calculated_hash = sha1.digest();
+ byte[] expected_hash = h3hash;
+ if(!Arrays.equals(calculated_hash, expected_hash)){
+ log.info(Utils.ByteArrayToString(calculated_hash));
+ log.info(Utils.ByteArrayToString(expected_hash));
+ log.info("Hash doesn't match decrypted content.");
+ }else{
+ //log.warning("###################################################Hash DOES match saves output stream.");
+ }
+ }
+
+ outputStream.close();
+ inputStream.close();
+ }
+
+ public void decryptFileStreamHashed(InputStream inputStream,OutputStream outputStream,long filesize,long fileoffset,short contentIndex,byte[] h3Hash) throws IOException{
+ int BLOCKSIZE = 0x10000;
+ int HASHBLOCKSIZE = 0xFC00;
+
+ long writeSize = HASHBLOCKSIZE;
+
+ long block = (fileoffset / HASHBLOCKSIZE);
+ long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
+
+ if( soffset+filesize > writeSize )
+ writeSize = writeSize - soffset;
+
+ byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
+ ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
+
+ long wrote = 0;
+ int inBlockBuffer;
+ do{
+ inBlockBuffer = StreamUtils.getChunkFromStream(inputStream,encryptedBlockBuffer,overflow,BLOCKSIZE);
+
+ if( writeSize > filesize )
+ writeSize = filesize;
+
+ byte[] output = decryptFileChunkHash(encryptedBlockBuffer, (int) block,contentIndex,h3Hash);
+
+ if((wrote + writeSize) > filesize){
+ writeSize = (int) (filesize- wrote);
+ }
+
+ outputStream.write(output, (int)(0+soffset), (int)writeSize);
+
+ wrote +=writeSize;
+
+ block++;
+
+ if( soffset > 0){
+ writeSize = HASHBLOCKSIZE;
+ soffset = 0;
+ }
+ }while(wrote < filesize && (inBlockBuffer == BLOCKSIZE));
+
+ outputStream.close();
+ inputStream.close();
+ }
+
+ private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, int contentIndex,byte[] h3_hashes){
+ int hashSize = 0x400;
+ int blocksize = 0xFC00;
+ byte[] IV = ByteBuffer.allocate(16).putShort((short) contentIndex).array();
+
+ byte[] hashes = decryptFileChunk(blockBuffer,hashSize,IV);
+
+ hashes[0] ^= (byte)((contentIndex >> 8) & 0xFF);
+ hashes[1] ^= (byte)(contentIndex & 0xFF);
+
+ int H0_start = (block % 16) * 20;
+
+ 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;
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/download/Downloader.java b/src/de/mas/jnus/lib/utils/download/Downloader.java
new file mode 100644
index 0000000..3013105
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/download/Downloader.java
@@ -0,0 +1,38 @@
+package de.mas.jnus.lib.utils.download;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class Downloader {
+ public static byte[] downloadFileToByteArray(String fileURL) throws IOException {
+ int BUFFER_SIZE = 0x800;
+ URL url = new URL(fileURL);
+ HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
+ int responseCode = httpConn.getResponseCode();
+
+ byte[] file = null;
+
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ int contentLength = httpConn.getContentLength();
+
+ file = new byte[contentLength];
+
+ InputStream inputStream = httpConn.getInputStream();
+
+ int bytesRead = -1;
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int filePostion = 0;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ System.arraycopy(buffer, 0, file, filePostion,bytesRead);
+ filePostion+=bytesRead;
+ }
+ inputStream.close();
+ }else{
+ System.out.println("File not found: " + fileURL);
+ }
+ httpConn.disconnect();
+ return file;
+ }
+}
diff --git a/src/de/mas/jnus/lib/utils/download/NUSDownloadService.java b/src/de/mas/jnus/lib/utils/download/NUSDownloadService.java
new file mode 100644
index 0000000..760a196
--- /dev/null
+++ b/src/de/mas/jnus/lib/utils/download/NUSDownloadService.java
@@ -0,0 +1,70 @@
+package de.mas.jnus.lib.utils.download;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import de.mas.jnus.lib.Settings;
+import lombok.Setter;
+
+public class NUSDownloadService extends Downloader{
+ private static NUSDownloadService defaultInstance;
+
+ public static NUSDownloadService getDefaultInstance(){
+ if(defaultInstance == null){
+ defaultInstance = new NUSDownloadService();
+ defaultInstance.setURL_BASE(Settings.URL_BASE);
+ }
+ return defaultInstance;
+ }
+
+ public static NUSDownloadService getInstance(String URL){
+ NUSDownloadService instance = new NUSDownloadService();
+ instance.setURL_BASE(URL);
+ return instance;
+ }
+
+ private NUSDownloadService(){
+
+ }
+
+ @Setter private String URL_BASE = "";
+
+
+ public byte[] downloadTMDToByteArray(long titleID, int version) throws IOException {
+ String version_suf = "";
+ if(version > Settings.LATEST_TMD_VERSION) version_suf = "." + version;
+ String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" +version_suf;
+ return downloadFileToByteArray(URL);
+ }
+
+ public byte[] downloadTicketToByteArray(long titleID) throws IOException {
+ String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
+ return downloadFileToByteArray(URL);
+ }
+
+ public byte[] downloadToByteArray(String url) throws IOException {
+ String URL = URL_BASE + "/" + url;
+ return downloadFileToByteArray(URL);
+ }
+
+ public InputStream getInputStream(String URL,long offset) throws IOException{
+ URL url_obj = new URL(URL);
+ HttpURLConnection connection = (HttpURLConnection) url_obj.openConnection();
+ connection.setRequestProperty("Range", "bytes=" + offset +"-");
+ try{
+ connection.connect();
+ }catch(Exception e){
+ e.printStackTrace();
+ }
+
+ return connection.getInputStream();
+ }
+
+ public InputStream getInputStreamForURL(String url, long offset) throws IOException {
+ String URL = URL_BASE + "/" + url;
+
+ return getInputStream(URL,offset);
+ }
+}