From 14c987456d8652bf45339a8aeba58ca4c029cfbe Mon Sep 17 00:00:00 2001 From: Maschell Date: Wed, 17 May 2017 19:20:42 +0200 Subject: [PATCH] Code clean up, changed to a maven project --- .classpath | 22 +- .gitignore | 1 + .project | 6 + eclipse_code_convention.xml | 295 ++++++++++++++ pom.xml | 41 ++ src/de/mas/jnus/lib/DecryptionService.java | 336 ---------------- src/de/mas/jnus/lib/NUSTitle.java | 103 ----- src/de/mas/jnus/lib/NUSTitleConfig.java | 18 - src/de/mas/jnus/lib/NUSTitleLoader.java | 67 ---- src/de/mas/jnus/lib/NUSTitleLoaderLocal.java | 35 -- src/de/mas/jnus/lib/NUSTitleLoaderRemote.java | 43 -- src/de/mas/jnus/lib/NUSTitleLoaderWUD.java | 56 --- src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java | 33 -- src/de/mas/jnus/lib/WUDService.java | 184 --------- src/de/mas/jnus/lib/entities/TMD.java | 167 -------- src/de/mas/jnus/lib/entities/Ticket.java | 122 ------ .../jnus/lib/entities/content/Content.java | 149 ------- .../lib/entities/content/ContentFSTInfo.java | 85 ---- src/de/mas/jnus/lib/entities/fst/FST.java | 81 ---- .../mas/jnus/lib/entities/fst/FSTService.java | 137 ------- .../implementations/NUSDataProviderWUD.java | 102 ----- .../lib/implementations/woomy/WoomyMeta.java | 39 -- .../lib/implementations/wud/WUDImage.java | 108 ----- .../wud/WUDImageCompressedInfo.java | 100 ----- .../wud/parser/WUDGamePartition.java | 12 - .../implementations/wud/parser/WUDInfo.java | 41 -- .../wud/parser/WUDInfoParser.java | 152 ------- .../wud/parser/WUDPartition.java | 10 - .../wud/parser/WUDPartitionHeader.java | 96 ----- .../wud/reader/WUDDiscReader.java | 127 ------ .../mas/jnus/lib/utils/ByteArrayBuffer.java | 25 -- .../mas/jnus/lib/utils/ByteArrayWrapper.java | 33 -- src/de/mas/jnus/lib/utils/ByteUtils.java | 76 ---- src/de/mas/jnus/lib/utils/HashUtil.java | 185 --------- src/de/mas/jnus/lib/utils/Utils.java | 66 ---- .../lib/utils/cryptography/AESDecryption.java | 71 ---- .../lib/utils/cryptography/NUSDecryption.java | 171 -------- .../utils/download/NUSDownloadService.java | 70 ---- src/de/mas/wiiu/jnus/DecryptionService.java | 374 ++++++++++++++++++ .../lib => wiiu/jnus}/ExtractionService.java | 110 +++--- .../wiiu/jnus/InputStreamWithException.java | 7 + src/de/mas/wiiu/jnus/NUSTitle.java | 123 ++++++ src/de/mas/wiiu/jnus/NUSTitleConfig.java | 18 + src/de/mas/wiiu/jnus/NUSTitleLoader.java | 66 ++++ src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java | 34 ++ .../mas/wiiu/jnus/NUSTitleLoaderRemote.java | 41 ++ src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java | 57 +++ src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java | 32 ++ .../jnus/PipedInputStreamWithException.java | 64 +++ .../mas/{jnus/lib => wiiu/jnus}/Settings.java | 4 +- src/de/mas/wiiu/jnus/WUDService.java | 251 ++++++++++++ src/de/mas/wiiu/jnus/entities/TMD.java | 226 +++++++++++ src/de/mas/wiiu/jnus/entities/Ticket.java | 95 +++++ .../wiiu/jnus/entities/content/Content.java | 192 +++++++++ .../jnus/entities/content/ContentFSTInfo.java | 105 +++++ .../jnus}/entities/content/ContentInfo.java | 58 +-- src/de/mas/wiiu/jnus/entities/fst/FST.java | 85 ++++ .../jnus}/entities/fst/FSTEntry.java | 105 ++--- .../wiiu/jnus/entities/fst/FSTService.java | 157 ++++++++ .../implementations/NUSDataProvider.java | 94 +++-- .../implementations/NUSDataProviderLocal.java | 53 +-- .../NUSDataProviderRemote.java | 44 ++- .../implementations/NUSDataProviderWUD.java | 104 +++++ .../implementations/NUSDataProviderWoomy.java | 71 ++-- .../implementations/woomy/WoomyInfo.java | 4 +- .../jnus/implementations/woomy/WoomyMeta.java | 25 ++ .../woomy/WoomyMetaParser.java | 47 ++- .../implementations/woomy/WoomyParser.java | 50 +-- .../implementations/woomy/WoomyZipFile.java | 11 +- .../jnus/implementations/wud/WUDImage.java | 110 ++++++ .../wud/WUDImageCompressedInfo.java | 98 +++++ .../wud/parser/WUDGamePartition.java | 19 + .../implementations/wud/parser/WUDInfo.java | 43 ++ .../wud/parser/WUDInfoParser.java | 151 +++++++ .../wud/parser/WUDPartition.java | 11 + .../wud/parser/WUDPartitionHeader.java | 87 ++++ .../wud/reader/WUDDiscReader.java | 139 +++++++ .../wud/reader/WUDDiscReaderCompressed.java | 66 ++-- .../wud/reader/WUDDiscReaderSplitted.java | 88 +++-- .../wud/reader/WUDDiscReaderUncompressed.java | 33 +- .../mas/wiiu/jnus/utils/ByteArrayBuffer.java | 25 ++ .../mas/wiiu/jnus/utils/ByteArrayWrapper.java | 27 ++ src/de/mas/wiiu/jnus/utils/ByteUtils.java | 85 ++++ .../jnus/utils/CheckSumWrongException.java | 20 + .../lib => wiiu/jnus}/utils/FileUtils.java | 34 +- src/de/mas/wiiu/jnus/utils/HashResult.java | 16 + src/de/mas/wiiu/jnus/utils/HashUtil.java | 265 +++++++++++++ .../lib => wiiu/jnus}/utils/StreamUtils.java | 131 +++--- src/de/mas/wiiu/jnus/utils/Utils.java | 149 +++++++ .../lib => wiiu/jnus}/utils/XMLParser.java | 16 +- .../utils/cryptography/AESDecryption.java | 71 ++++ .../utils/cryptography/NUSDecryption.java | 184 +++++++++ .../jnus}/utils/download/Downloader.java | 33 +- .../utils/download/NUSDownloadService.java | 69 ++++ 94 files changed, 4567 insertions(+), 3575 deletions(-) create mode 100644 eclipse_code_convention.xml create mode 100644 pom.xml delete mode 100644 src/de/mas/jnus/lib/DecryptionService.java delete mode 100644 src/de/mas/jnus/lib/NUSTitle.java delete mode 100644 src/de/mas/jnus/lib/NUSTitleConfig.java delete mode 100644 src/de/mas/jnus/lib/NUSTitleLoader.java delete mode 100644 src/de/mas/jnus/lib/NUSTitleLoaderLocal.java delete mode 100644 src/de/mas/jnus/lib/NUSTitleLoaderRemote.java delete mode 100644 src/de/mas/jnus/lib/NUSTitleLoaderWUD.java delete mode 100644 src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java delete mode 100644 src/de/mas/jnus/lib/WUDService.java delete mode 100644 src/de/mas/jnus/lib/entities/TMD.java delete mode 100644 src/de/mas/jnus/lib/entities/Ticket.java delete mode 100644 src/de/mas/jnus/lib/entities/content/Content.java delete mode 100644 src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java delete mode 100644 src/de/mas/jnus/lib/entities/fst/FST.java delete mode 100644 src/de/mas/jnus/lib/entities/fst/FSTService.java delete mode 100644 src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java delete mode 100644 src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/WUDImage.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/WUDImageCompressedInfo.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/parser/WUDGamePartition.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/parser/WUDInfoParser.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/parser/WUDPartition.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/parser/WUDPartitionHeader.java delete mode 100644 src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReader.java delete mode 100644 src/de/mas/jnus/lib/utils/ByteArrayBuffer.java delete mode 100644 src/de/mas/jnus/lib/utils/ByteArrayWrapper.java delete mode 100644 src/de/mas/jnus/lib/utils/ByteUtils.java delete mode 100644 src/de/mas/jnus/lib/utils/HashUtil.java delete mode 100644 src/de/mas/jnus/lib/utils/Utils.java delete mode 100644 src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java delete mode 100644 src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java delete mode 100644 src/de/mas/jnus/lib/utils/download/NUSDownloadService.java create mode 100644 src/de/mas/wiiu/jnus/DecryptionService.java rename src/de/mas/{jnus/lib => wiiu/jnus}/ExtractionService.java (57%) create mode 100644 src/de/mas/wiiu/jnus/InputStreamWithException.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitle.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitleConfig.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitleLoader.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java create mode 100644 src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java create mode 100644 src/de/mas/wiiu/jnus/PipedInputStreamWithException.java rename src/de/mas/{jnus/lib => wiiu/jnus}/Settings.java (85%) create mode 100644 src/de/mas/wiiu/jnus/WUDService.java create mode 100644 src/de/mas/wiiu/jnus/entities/TMD.java create mode 100644 src/de/mas/wiiu/jnus/entities/Ticket.java create mode 100644 src/de/mas/wiiu/jnus/entities/content/Content.java create mode 100644 src/de/mas/wiiu/jnus/entities/content/ContentFSTInfo.java rename src/de/mas/{jnus/lib => wiiu/jnus}/entities/content/ContentInfo.java (51%) create mode 100644 src/de/mas/wiiu/jnus/entities/fst/FST.java rename src/de/mas/{jnus/lib => wiiu/jnus}/entities/fst/FSTEntry.java (59%) create mode 100644 src/de/mas/wiiu/jnus/entities/fst/FSTService.java rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/NUSDataProvider.java (58%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/NUSDataProviderLocal.java (65%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/NUSDataProviderRemote.java (67%) create mode 100644 src/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/NUSDataProviderWoomy.java (52%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/woomy/WoomyInfo.java (65%) create mode 100644 src/de/mas/wiiu/jnus/implementations/woomy/WoomyMeta.java rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/woomy/WoomyMetaParser.java (68%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/woomy/WoomyParser.java (63%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/woomy/WoomyZipFile.java (85%) create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/WUDImage.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/WUDImageCompressedInfo.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/parser/WUDGamePartition.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfo.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfoParser.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartition.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartitionHeader.java create mode 100644 src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/wud/reader/WUDDiscReaderCompressed.java (52%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/wud/reader/WUDDiscReaderSplitted.java (55%) rename src/de/mas/{jnus/lib => wiiu/jnus}/implementations/wud/reader/WUDDiscReaderUncompressed.java (63%) create mode 100644 src/de/mas/wiiu/jnus/utils/ByteArrayBuffer.java create mode 100644 src/de/mas/wiiu/jnus/utils/ByteArrayWrapper.java create mode 100644 src/de/mas/wiiu/jnus/utils/ByteUtils.java create mode 100644 src/de/mas/wiiu/jnus/utils/CheckSumWrongException.java rename src/de/mas/{jnus/lib => wiiu/jnus}/utils/FileUtils.java (56%) create mode 100644 src/de/mas/wiiu/jnus/utils/HashResult.java create mode 100644 src/de/mas/wiiu/jnus/utils/HashUtil.java rename src/de/mas/{jnus/lib => wiiu/jnus}/utils/StreamUtils.java (50%) create mode 100644 src/de/mas/wiiu/jnus/utils/Utils.java rename src/de/mas/{jnus/lib => wiiu/jnus}/utils/XMLParser.java (85%) create mode 100644 src/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java create mode 100644 src/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java rename src/de/mas/{jnus/lib => wiiu/jnus}/utils/download/Downloader.java (69%) create mode 100644 src/de/mas/wiiu/jnus/utils/download/NUSDownloadService.java diff --git a/.classpath b/.classpath index 140ab94..d524ae2 100644 --- a/.classpath +++ b/.classpath @@ -1,7 +1,21 @@ - - - - + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore index ae3c172..09e3bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /bin/ +/target/ diff --git a/.project b/.project index 97d02eb..997a70d 100644 --- a/.project +++ b/.project @@ -10,8 +10,14 @@ + + org.eclipse.m2e.core.maven2Builder + + + + org.eclipse.m2e.core.maven2Nature org.eclipse.jdt.core.javanature diff --git a/eclipse_code_convention.xml b/eclipse_code_convention.xml new file mode 100644 index 0000000..d476ca2 --- /dev/null +++ b/eclipse_code_convention.xml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..592d500 --- /dev/null +++ b/pom.xml @@ -0,0 +1,41 @@ + + 4.0.0 + de.mas.wiiu.jnus + library + 0.0.1-SNAPSHOT + + 1.8 + 1.8 + + + + src + + + maven-compiler-plugin + 3.5.1 + + + + + + + + + + org.projectlombok + lombok + 1.16.12 + + + junit + junit + 4.12 + + + commons-io + commons-io + 2.4 + + + \ No newline at end of file diff --git a/src/de/mas/jnus/lib/DecryptionService.java b/src/de/mas/jnus/lib/DecryptionService.java deleted file mode 100644 index 7e86491..0000000 --- a/src/de/mas/jnus/lib/DecryptionService.java +++ /dev/null @@ -1,336 +0,0 @@ -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/NUSTitle.java b/src/de/mas/jnus/lib/NUSTitle.java deleted file mode 100644 index 86efcf6..0000000 --- a/src/de/mas/jnus/lib/NUSTitle.java +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index 64edb97..0000000 --- a/src/de/mas/jnus/lib/NUSTitleConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 4bbd725..0000000 --- a/src/de/mas/jnus/lib/NUSTitleLoader.java +++ /dev/null @@ -1,67 +0,0 @@ -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 deleted file mode 100644 index d22224b..0000000 --- a/src/de/mas/jnus/lib/NUSTitleLoaderLocal.java +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 2d65ed7..0000000 --- a/src/de/mas/jnus/lib/NUSTitleLoaderRemote.java +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index b220680..0000000 --- a/src/de/mas/jnus/lib/NUSTitleLoaderWUD.java +++ /dev/null @@ -1,56 +0,0 @@ -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(new File(wudFile.getAbsolutePath()).getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME); - if(!keyFile.exists()){ - System.out.println(keyFile.getAbsolutePath() + " does not exist and no title key was provided."); - return null; - } - titleKey = Files.readAllBytes(keyFile.toPath()); - } - WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), titleKey); - if(wudInfo == null){ - return null; - } - - 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 deleted file mode 100644 index 8fea3aa..0000000 --- a/src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java +++ /dev/null @@ -1,33 +0,0 @@ -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/WUDService.java b/src/de/mas/jnus/lib/WUDService.java deleted file mode 100644 index c1061d5..0000000 --- a/src/de/mas/jnus/lib/WUDService.java +++ /dev/null @@ -1,184 +0,0 @@ -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 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 File compressWUDToWUX(WUDImage image,String outputFolder) throws IOException{ - return compressWUDToWUX(image, outputFolder, "game.wux",false); - } - - public static File compressWUDToWUX(WUDImage image,String outputFolder,boolean overwrite) throws IOException{ - return compressWUDToWUX(image, outputFolder, "game.wux",overwrite); - } - - public static File compressWUDToWUX(WUDImage image,String outputFolder,String filename,boolean overwrite) throws IOException{ - if(image.isCompressed()){ - log.info("Given image is already compressed"); - return null; - } - - if(image.getWUDFileSize() != WUDImage.WUD_FILESIZE) - { - log.info("Given WUD has not the expected filesize"); - return null; - } - - Utils.createDir(outputFolder); - - String filePath; - if(outputFolder == null) outputFolder = ""; - - if(!outputFolder.isEmpty()){ - filePath = outputFolder+ File.separator + filename; - }else{ - filePath = filename; - } - File outputFile = new File(filePath); - - if(outputFile.exists() && !overwrite){ - log.info("Couldn't compress wud, target file already exists (" + outputFile.getAbsolutePath() + ")"); - return null; - } - - log.info("Writing compressed file to: " + outputFile.getAbsolutePath() ); - 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 % 10 == 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.print(String.format(Locale.ROOT,"\rCompressing into .wux | Progress %.2f%% | Ratio: 1:%.2f | Read: %.2fMB | Written: %.2fMB\t",percent,ratio,readMB,writtenMB)); - } - }while(written < image.getWUDFileSize()); - System.out.println(); - System.out.println("Sectors compressed."); - 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 outputFile; - } - - public static boolean compareWUDImage(WUDImage firstImage,WUDImage secondImage) throws IOException{ - if(firstImage.getWUDFileSize() != secondImage.getWUDFileSize()){ - log.info("Filesize is 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 % 1 == 0){ - double readMB = totalread / 1024.0 / 1024.0; - double percent = ((double)totalread / WUDImage.WUD_FILESIZE)*100; - System.out.print(String.format("\rVerification: %.2fMB done (%.2f%%)", readMB,percent)); - } - }while(totalread < WUDImage.WUD_FILESIZE); - System.out.println(); - System.out.print("Verfication done!"); - in1.close(); - in2.close(); - - return result; - } -} diff --git a/src/de/mas/jnus/lib/entities/TMD.java b/src/de/mas/jnus/lib/entities/TMD.java deleted file mode 100644 index 67dee4c..0000000 --- a/src/de/mas/jnus/lib/entities/TMD.java +++ /dev/null @@ -1,167 +0,0 @@ -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 deleted file mode 100644 index 4879c82..0000000 --- a/src/de/mas/jnus/lib/entities/Ticket.java +++ /dev/null @@ -1,122 +0,0 @@ -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 deleted file mode 100644 index 67f2d56..0000000 --- a/src/de/mas/jnus/lib/entities/content/Content.java +++ /dev/null @@ -1,149 +0,0 @@ -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 deleted file mode 100644 index 47dcbbc..0000000 --- a/src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java +++ /dev/null @@ -1,85 +0,0 @@ -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/fst/FST.java b/src/de/mas/jnus/lib/entities/fst/FST.java deleted file mode 100644 index f7ed1ac..0000000 --- a/src/de/mas/jnus/lib/entities/fst/FST.java +++ /dev/null @@ -1,81 +0,0 @@ -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 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.fine("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/NUSDataProviderWUD.java b/src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java deleted file mode 100644 index f468f60..0000000 --- a/src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java +++ /dev/null @@ -1,102 +0,0 @@ -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/woomy/WoomyMeta.java b/src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java deleted file mode 100644 index 02586cc..0000000 --- a/src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java +++ /dev/null @@ -1,39 +0,0 @@ -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/wud/WUDImage.java b/src/de/mas/jnus/lib/implementations/wud/WUDImage.java deleted file mode 100644 index 5f40d36..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/WUDImage.java +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index d6f94a7..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/parser/WUDGamePartition.java +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index b6e30c2..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index f5fdf09..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/parser/WUDInfoParser.java +++ /dev/null @@ -1,152 +0,0 @@ -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 deleted file mode 100644 index ea47926..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartition.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index b6b5f01..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/parser/WUDPartitionHeader.java +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index 14830ec..0000000 --- a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReader.java +++ /dev/null @@ -1,127 +0,0 @@ -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/utils/ByteArrayBuffer.java b/src/de/mas/jnus/lib/utils/ByteArrayBuffer.java deleted file mode 100644 index 0e7d844..0000000 --- a/src/de/mas/jnus/lib/utils/ByteArrayBuffer.java +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 5af776b..0000000 --- a/src/de/mas/jnus/lib/utils/ByteArrayWrapper.java +++ /dev/null @@ -1,33 +0,0 @@ -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 deleted file mode 100644 index aa1d200..0000000 --- a/src/de/mas/jnus/lib/utils/ByteUtils.java +++ /dev/null @@ -1,76 +0,0 @@ -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/HashUtil.java b/src/de/mas/jnus/lib/utils/HashUtil.java deleted file mode 100644 index ea30b97..0000000 --- a/src/de/mas/jnus/lib/utils/HashUtil.java +++ /dev/null @@ -1,185 +0,0 @@ -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/Utils.java b/src/de/mas/jnus/lib/utils/Utils.java deleted file mode 100644 index 22dfd9d..0000000 --- a/src/de/mas/jnus/lib/utils/Utils.java +++ /dev/null @@ -1,66 +0,0 @@ -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/cryptography/AESDecryption.java b/src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java deleted file mode 100644 index f63256f..0000000 --- a/src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index 468086b..0000000 --- a/src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java +++ /dev/null @@ -1,171 +0,0 @@ -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/NUSDownloadService.java b/src/de/mas/jnus/lib/utils/download/NUSDownloadService.java deleted file mode 100644 index 760a196..0000000 --- a/src/de/mas/jnus/lib/utils/download/NUSDownloadService.java +++ /dev/null @@ -1,70 +0,0 @@ -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); - } -} diff --git a/src/de/mas/wiiu/jnus/DecryptionService.java b/src/de/mas/wiiu/jnus/DecryptionService.java new file mode 100644 index 0000000..6944a89 --- /dev/null +++ b/src/de/mas/wiiu/jnus/DecryptionService.java @@ -0,0 +1,374 @@ +package de.mas.wiiu.jnus; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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.concurrent.Callable; +import java.util.concurrent.ForkJoinPool; + +import de.mas.wiiu.jnus.entities.TMD; +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.entities.fst.FSTEntry; +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.utils.CheckSumWrongException; +import de.mas.wiiu.jnus.utils.HashUtil; +import de.mas.wiiu.jnus.utils.StreamUtils; +import de.mas.wiiu.jnus.utils.Utils; +import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption; +import lombok.Getter; +import lombok.extern.java.Log; + +@Log +public final class DecryptionService { + private static Map instances = new HashMap<>(); + @Getter private final NUSTitle NUSTitle; + + public static DecryptionService getInstance(NUSTitle nustitle) { + if (!instances.containsKey(nustitle)) { + instances.put(nustitle, new DecryptionService(nustitle)); + } + return instances.get(nustitle); + } + + private DecryptionService(NUSTitle nustitle) { + this.NUSTitle = nustitle; + } + + public Ticket getTicket() { + return getNUSTitle().getTicket(); + } + + public void decryptFSTEntryTo(boolean useFullPath, FSTEntry entry, String outputPath, boolean skipExistingFile) throws IOException, CheckSumWrongException { + if (entry.isNotInPackage() || entry.getContent() == null) { + return; + } + + //log.info("Decrypting " + entry.getFilename()); + + 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()) { + log.info("File already exists: " + entry.getFilename()); + return; + } else { + if (Arrays.equals(HashUtil.hashSHA1(target, (int) c.getDecryptedFileSize()), c.getSHA2Hash())) { + log.info("File already exists: " + entry.getFilename()); + return; + } else { + log.info("File already exists with the same filesize, but the hash doesn't match: " + entry.getFilename()); + } + } + + } else { + log.info("File already exists but the filesize doesn't match: " + entry.getFilename()); + } + } + } + + FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath)); + try { + decryptFSTEntryToStream(entry, outputStream); + } catch (CheckSumWrongException e) { + if (entry.getFilename().endsWith(".xml") && Utils.checkXML(new File(targetFilePath))) { + log.info("Hash doesn't match, but it's an XML file and it looks okay."); + } else { + log.info("Hash doesn't match!"); + throw e; + } + } + } + + public void decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream) throws IOException, CheckSumWrongException { + 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); + + try { + decryptFSTEntryFromStreams(in, outputStream, fileSize, fileOffset, c); + } catch (CheckSumWrongException e) { + log.info("Hash doesn't match"); + if (entry.getFilename().endsWith(".xml")) { + if (outputStream instanceof PipedOutputStream) { + log.info("Hash doesn't match. Please check the data for " + entry.getFullPath()); + } else { + throw e; + } + } else if (entry.getContent().isUNKNWNFlag1Set()) { + log.info("But file is optional. Don't worry."); + } else { + StringBuilder sb = new StringBuilder(); + sb.append("Detailed info:").append(System.lineSeparator()); + sb.append(entry).append(System.lineSeparator()); + sb.append(entry.getContent()).append(System.lineSeparator()); + sb.append(String.format("%016x", this.NUSTitle.getTMD().getTitleID())); + sb.append(e.getMessage() + " Calculated Hash: " + Utils.ByteArrayToString(e.getGivenHash()) + ", expected hash: " + + Utils.ByteArrayToString(e.getExpectedHash())); + log.info(sb.toString()); + throw e; + } + } + } + + private void decryptFSTEntryFromStreams(InputStream inputStream, OutputStream outputStream, long filesize, long fileoffset, Content content) + throws IOException, CheckSumWrongException { + decryptStreams(inputStream, outputStream, filesize, fileoffset, content); + } + + private void decryptContentFromStream(InputStream inputStream, OutputStream outputStream, Content content) throws IOException, CheckSumWrongException { + long filesize = content.getDecryptedFileSize(); + log.info("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, CheckSumWrongException { + NUSDecryption nusdecryption = new NUSDecryption(getTicket()); + short contentIndex = (short) content.getIndex(); + + long encryptedFileSize = content.getEncryptedFileSize(); + + if (content.isEncrypted()) { + 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); + } + } else { + StreamUtils.saveInputStreamToOutputStreamWithHash(inputStream, outputStream, size, content.getSHA2Hash(), encryptedFileSize); + } + + inputStream.close(); + outputStream.close(); + } + + public void decryptContentTo(Content content, String outPath, boolean skipExistingFile) throws IOException, CheckSumWrongException { + String targetFilePath = outPath + File.separator + content.getFilenameDecrypted(); + if (skipExistingFile) { + File targetFile = new File(targetFilePath); + if (targetFile.exists()) { + if (targetFile.length() == content.getDecryptedFileSize()) { + log.info("File already exists : " + content.getFilenameDecrypted()); + return; + } else { + log.info("File already exists but the filesize doesn't match: " + content.getFilenameDecrypted()); + } + } + } + + if (!Utils.createDir(outPath)) { + return; + } + + log.info("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, CheckSumWrongException { + if (content == null) { + return; + } + + NUSDataProvider dataProvider = getNUSTitle().getDataProvider(); + InputStream inputStream = dataProvider.getInputStreamFromContent(content, 0); + + decryptContentFromStream(inputStream, outputStream, content); + } + + public PipedInputStreamWithException getDecryptedOutputAsInputStream(FSTEntry fstEntry) throws IOException { + PipedInputStreamWithException in = new PipedInputStreamWithException(); + PipedOutputStream out = new PipedOutputStream(in); + + new Thread(() -> { + try { // Throwing it in both cases is EXTREMLY important. Otherwise it'll end in a deadlock + decryptFSTEntryToStream(fstEntry, out); + in.throwException(null); + } catch (Exception e) { + in.throwException(e); + } + + }).start(); + + return in; + } + + public PipedInputStreamWithException getDecryptedContentAsInputStream(Content content) throws IOException, CheckSumWrongException { + PipedInputStreamWithException in = new PipedInputStreamWithException(); + PipedOutputStream out = new PipedOutputStream(in); + + new Thread(() -> { + try {// Throwing it in both cases is EXTREMLY important. Otherwise it'll end in a deadlock + decryptContentToStream(content, out); + in.throwException(null); + } catch (Exception e) { + in.throwException(e); + } + }).start(); + + return in; + } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Decrypt FSTEntry to OutputStream + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + public void decryptFSTEntryTo(String entryFullPath, OutputStream outputStream) throws IOException, CheckSumWrongException { + FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath); + if (entry == null) { + log.info("File not found"); + } + + decryptFSTEntryToStream(entry, outputStream); + } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Decrypt single FSTEntry to File + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + public void decryptFSTEntryTo(String entryFullPath, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntryTo(false, entryFullPath, outputFolder); + } + + public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntryTo(fullPath, entryFullPath, outputFolder, getNUSTitle().isSkipExistingFiles()); + } + + public void decryptFSTEntryTo(String entryFullPath, String outputFolder, boolean skipExistingFiles) throws IOException, CheckSumWrongException { + decryptFSTEntryTo(false, entryFullPath, outputFolder, getNUSTitle().isSkipExistingFiles()); + } + + public void decryptFSTEntryTo(boolean fullPath, String entryFullPath, String outputFolder, boolean skipExistingFiles) + throws IOException, CheckSumWrongException { + FSTEntry entry = getNUSTitle().getFSTEntryByFullPath(entryFullPath); + if (entry == null) { + log.info("File not found"); + return; + } + + decryptFSTEntryTo(fullPath, entry, outputFolder, skipExistingFiles); + } + + public void decryptFSTEntryTo(FSTEntry entry, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntryTo(false, entry, outputFolder); + } + + public void decryptFSTEntryTo(boolean fullPath, FSTEntry entry, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntryTo(fullPath, entry, outputFolder, getNUSTitle().isSkipExistingFiles()); + } + + public void decryptFSTEntryTo(FSTEntry entry, String outputFolder, boolean skipExistingFiles) throws IOException, CheckSumWrongException { + 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, CheckSumWrongException { + Utils.createDir(outputFolder + File.separator + "code"); + Utils.createDir(outputFolder + File.separator + "content"); + Utils.createDir(outputFolder + File.separator + "meta"); + decryptFSTEntriesTo(true, ".*", outputFolder); + } + + public void decryptFSTEntriesTo(String regEx, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntriesTo(true, regEx, outputFolder); + } + + public void decryptFSTEntriesTo(boolean fullPath, String regEx, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntryListTo(fullPath, getNUSTitle().getFSTEntriesByRegEx(regEx), outputFolder); + } + + public void decryptFSTEntryListTo(List list, String outputFolder) throws IOException, CheckSumWrongException { + decryptFSTEntryListTo(true, list, outputFolder); + } + + public void decryptFSTEntryListTo(boolean fullPath, List list, String outputFolder) throws IOException, CheckSumWrongException { + for (FSTEntry entry : list) { + decryptFSTEntryTo(fullPath, entry, outputFolder, getNUSTitle().isSkipExistingFiles()); + } + } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Save decrypted contents + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!! + public void decryptPlainContentByID(int ID, String outputFolder) throws IOException, CheckSumWrongException { + decryptPlainContent(getTMDFromNUSTitle().getContentByID(ID), outputFolder); + } + + public void decryptPlainContentByIndex(int index, String outputFolder) throws IOException, CheckSumWrongException { + decryptPlainContent(getTMDFromNUSTitle().getContentByIndex(index), outputFolder); + } + + public void decryptPlainContent(Content c, String outputFolder) throws IOException, CheckSumWrongException { + decryptPlainContents(new ArrayList(Arrays.asList(c)), outputFolder); + } + + public void decryptPlainContents(List list, String outputFolder) throws IOException, CheckSumWrongException { + for (Content c : list) { + decryptContentTo(c, outputFolder, getNUSTitle().isSkipExistingFiles()); + } + } + + public void decryptAllPlainContents(String outputFolder) throws IOException, CheckSumWrongException { + 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/wiiu/jnus/ExtractionService.java similarity index 57% rename from src/de/mas/jnus/lib/ExtractionService.java rename to src/de/mas/wiiu/jnus/ExtractionService.java index b8a0550..b4f0968 100644 --- a/src/de/mas/jnus/lib/ExtractionService.java +++ b/src/de/mas/wiiu/jnus/ExtractionService.java @@ -1,4 +1,4 @@ -package de.mas.jnus.lib; +package de.mas.wiiu.jnus; import java.io.File; import java.io.IOException; @@ -7,41 +7,42 @@ 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 de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.utils.FileUtils; +import de.mas.wiiu.jnus.utils.Utils; import lombok.Getter; -import lombok.Setter; +import lombok.extern.java.Log; + +@Log +public final class ExtractionService { + private static Map instances = new HashMap<>(); + + @Getter private final NUSTitle NUSTitle; -public class ExtractionService { - private static Map instances = new HashMap<>(); - public static ExtractionService getInstance(NUSTitle nustitle) { - if(!instances.containsKey(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 ExtractionService(NUSTitle nustitle) { + this.NUSTitle = nustitle; } - - private NUSDataProvider getDataProvider(){ + + private NUSDataProvider getDataProvider() { return getNUSTitle().getDataProvider(); } - + public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException { - extractEncryptedContentHashesTo(new ArrayList(getNUSTitle().getTMD().getAllContents().values()),outputFolder); + 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){ + for (Content c : list) { dataProvider.saveContentH3Hash(c, outputFolder); } } @@ -49,22 +50,23 @@ public class ExtractionService { 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); + extractEncryptedContentFilesTo(new ArrayList(getNUSTitle().getTMD().getAllContents().values()), outputFolder, false); } - public void extractEncryptedContentFilesTo(List list,String outputFolder,boolean withHashes) throws IOException { + 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){ - if(withHashes){ + for (Content c : list) { + log.info("Saving " + c.getFilename()); + if (withHashes) { dataProvider.saveEncryptedContentWithH3Hash(c, outputFolder); - }else{ + } else { dataProvider.saveEncryptedContent(c, outputFolder); } } @@ -72,44 +74,44 @@ public class ExtractionService { 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"); + + byte[] rawTMD = getDataProvider().getRawTMD(); + + if (rawTMD == null || rawTMD.length == 0) { + log.info("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); + log.info("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"); + + byte[] rawTicket = getDataProvider().getRawTicket(); + + if (rawTicket == null || rawTicket.length == 0) { + log.info("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); + log.info("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"); + + byte[] rawCert = getDataProvider().getRawCert(); + + if (rawCert == null || rawCert.length == 0) { + log.info("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); + log.info("Extracting Cert to: " + cert_path); + FileUtils.saveByteArrayToFile(cert_path, rawCert); } public void extractAll(String outputFolder) throws IOException { @@ -118,7 +120,7 @@ public class ExtractionService { extractCertTo(outputFolder); extractTMDTo(outputFolder); extractTicketTo(outputFolder); - + } } diff --git a/src/de/mas/wiiu/jnus/InputStreamWithException.java b/src/de/mas/wiiu/jnus/InputStreamWithException.java new file mode 100644 index 0000000..f1b0bd1 --- /dev/null +++ b/src/de/mas/wiiu/jnus/InputStreamWithException.java @@ -0,0 +1,7 @@ +package de.mas.wiiu.jnus; + +import java.io.Closeable; + +public interface InputStreamWithException extends Closeable { + public void checkForException() throws Exception; +} diff --git a/src/de/mas/wiiu/jnus/NUSTitle.java b/src/de/mas/wiiu/jnus/NUSTitle.java new file mode 100644 index 0000000..f035a63 --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitle.java @@ -0,0 +1,123 @@ +package de.mas.wiiu.jnus; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.mas.wiiu.jnus.entities.TMD; +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.entities.content.ContentFSTInfo; +import de.mas.wiiu.jnus.entities.fst.FST; +import de.mas.wiiu.jnus.entities.fst.FSTEntry; +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import lombok.Getter; +import lombok.Setter; + +public class NUSTitle { + @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) { + String fullPath = givenFullPath.replaceAll("/", "\\\\"); + if (!fullPath.startsWith("\\")) fullPath = "\\" + fullPath; + for (FSTEntry f : getAllFSTEntriesFlat()) { + if (f.getFullPath().equals(fullPath)) { + return f; + } + } + return null; + } + + public List getFSTEntriesByRegEx(String regEx) { + List files = 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); + } + } + return result; + } + + 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(); + } + + @Override + public String toString() { + return "NUSTitle [dataProvider=" + dataProvider + "]"; + } +} diff --git a/src/de/mas/wiiu/jnus/NUSTitleConfig.java b/src/de/mas/wiiu/jnus/NUSTitleConfig.java new file mode 100644 index 0000000..e00e4ee --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitleConfig.java @@ -0,0 +1,18 @@ +package de.mas.wiiu.jnus; + +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo; +import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo; +import lombok.Data; + +@Data +public class NUSTitleConfig { + private String inputPath; + private WUDInfo WUDInfo; + private Ticket ticket; + + private int version = Settings.LATEST_TMD_VERSION; + private long titleID = 0x0L; + + private WoomyInfo woomyInfo; +} diff --git a/src/de/mas/wiiu/jnus/NUSTitleLoader.java b/src/de/mas/wiiu/jnus/NUSTitleLoader.java new file mode 100644 index 0000000..fde445a --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitleLoader.java @@ -0,0 +1,66 @@ +package de.mas.wiiu.jnus; + +import java.io.InputStream; +import java.util.Map; + +import de.mas.wiiu.jnus.entities.TMD; +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.entities.fst.FST; +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.utils.StreamUtils; +import de.mas.wiiu.jnus.utils.cryptography.AESDecryption; +import lombok.extern.java.Log; + +@Log +abstract class NUSTitleLoader { + protected NUSTitleLoader() { + // should be empty + } + + public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception { + NUSTitle result = new NUSTitle(); + + NUSDataProvider dataProvider = getDataProvider(result, config); + result.setDataProvider(dataProvider); + + TMD tmd = TMD.parseTMD(dataProvider.getRawTMD()); + result.setTMD(tmd); + + if (tmd == null) { + log.info("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(NUSTitle title, NUSTitleConfig config); +} diff --git a/src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java b/src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java new file mode 100644 index 0000000..7b4761a --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java @@ -0,0 +1,34 @@ +package de.mas.wiiu.jnus; + +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal; + +public final 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(NUSTitle title, NUSTitleConfig config) { + return new NUSDataProviderLocal(title, config.getInputPath()); + } + +} diff --git a/src/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java b/src/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java new file mode 100644 index 0000000..dffe8a1 --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java @@ -0,0 +1,41 @@ +package de.mas.wiiu.jnus; + +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote; + +public final 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); + config.setTicket(ticket); + + return loader.loadNusTitle(config); + } + + @Override + protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) { + return new NUSDataProviderRemote(title, config.getVersion(), config.getTitleID()); + } + +} diff --git a/src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java b/src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java new file mode 100644 index 0000000..cd2ce99 --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitleLoaderWUD.java @@ -0,0 +1,57 @@ +package de.mas.wiiu.jnus; + +import java.io.File; +import java.nio.file.Files; + +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD; +import de.mas.wiiu.jnus.implementations.wud.WUDImage; +import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo; +import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfoParser; +import lombok.extern.java.Log; + +@Log +public final 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(); + byte[] usedTitleKey = titleKey; + File wudFile = new File(WUDPath); + if (!wudFile.exists()) { + log.info(WUDPath + " does not exist."); + System.exit(1); + } + + WUDImage image = new WUDImage(wudFile); + if (usedTitleKey == null) { + File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME); + if (!keyFile.exists()) { + log.info(keyFile.getAbsolutePath() + " does not exist and no title key was provided."); + return null; + } + usedTitleKey = Files.readAllBytes(keyFile.toPath()); + } + WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), usedTitleKey); + if (wudInfo == null) { + return null; + } + + config.setWUDInfo(wudInfo); + + return loader.loadNusTitle(config); + } + + @Override + protected NUSDataProvider getDataProvider(NUSTitle title, NUSTitleConfig config) { + return new NUSDataProviderWUD(title, config.getWUDInfo()); + } +} diff --git a/src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java b/src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java new file mode 100644 index 0000000..91752a3 --- /dev/null +++ b/src/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java @@ -0,0 +1,32 @@ +package de.mas.wiiu.jnus; + +import java.io.File; + +import de.mas.wiiu.jnus.implementations.NUSDataProvider; +import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy; +import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo; +import de.mas.wiiu.jnus.implementations.woomy.WoomyParser; +import lombok.extern.java.Log; + +@Log +public final 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(NUSTitle title, NUSTitleConfig config) { + return new NUSDataProviderWoomy(title, config.getWoomyInfo()); + } + +} diff --git a/src/de/mas/wiiu/jnus/PipedInputStreamWithException.java b/src/de/mas/wiiu/jnus/PipedInputStreamWithException.java new file mode 100644 index 0000000..244356d --- /dev/null +++ b/src/de/mas/wiiu/jnus/PipedInputStreamWithException.java @@ -0,0 +1,64 @@ +package de.mas.wiiu.jnus; + +import java.io.IOException; +import java.io.PipedInputStream; + +import de.mas.wiiu.jnus.utils.Utils; + +public class PipedInputStreamWithException extends PipedInputStream implements InputStreamWithException { + private Exception e = null; + private boolean exceptionSet = false; + private boolean closed = false; + private Object lock = new Object(); + + @Override + public void close() throws IOException { + super.close(); + synchronized (lock) { + closed = true; + } + } + + public void throwException(Exception e) { + synchronized (lock) { + exceptionSet = true; + this.e = e; + } + } + + public boolean isClosed() { + boolean isClosed = false; + synchronized (lock) { + isClosed = closed; + } + return isClosed; + } + + @Override + public void checkForException() throws Exception { + if (isClosed()) { + boolean waiting = true; + int tries = 0; + while (waiting) { + synchronized (lock) { + waiting = !exceptionSet; + } + if (waiting) { + Utils.sleep(10); + } + if (tries > 100) { + // TODO: warning? + break; + } + } + } + synchronized (lock) { + if (e != null) { + Exception tmp = e; + e = null; + exceptionSet = true; + throw tmp; + } + } + } +} diff --git a/src/de/mas/jnus/lib/Settings.java b/src/de/mas/wiiu/jnus/Settings.java similarity index 85% rename from src/de/mas/jnus/lib/Settings.java rename to src/de/mas/wiiu/jnus/Settings.java index 1d8532a..43444f6 100644 --- a/src/de/mas/jnus/lib/Settings.java +++ b/src/de/mas/wiiu/jnus/Settings.java @@ -1,7 +1,7 @@ -package de.mas.jnus.lib; +package de.mas.wiiu.jnus; public class Settings { - public static String URL_BASE = "http://ccs.cdn.wup.shop.nintendo.net/ccs/download"; + public static String URL_BASE = "http://ccs.cdn.c.shop.nintendowifi.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"; diff --git a/src/de/mas/wiiu/jnus/WUDService.java b/src/de/mas/wiiu/jnus/WUDService.java new file mode 100644 index 0000000..2280b72 --- /dev/null +++ b/src/de/mas/wiiu/jnus/WUDService.java @@ -0,0 +1,251 @@ +package de.mas.wiiu.jnus; + +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.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import de.mas.wiiu.jnus.implementations.wud.WUDImage; +import de.mas.wiiu.jnus.implementations.wud.WUDImageCompressedInfo; +import de.mas.wiiu.jnus.utils.ByteArrayBuffer; +import de.mas.wiiu.jnus.utils.ByteArrayWrapper; +import de.mas.wiiu.jnus.utils.HashResult; +import de.mas.wiiu.jnus.utils.HashUtil; +import de.mas.wiiu.jnus.utils.StreamUtils; +import de.mas.wiiu.jnus.utils.Utils; +import lombok.extern.java.Log; + +@Log +public final class WUDService { + private WUDService() { + // Just an utility class + } + + public static File compressWUDToWUX(WUDImage image, String outputFolder) throws IOException { + return compressWUDToWUX(image, outputFolder, "game.wux", false); + } + + public static File compressWUDToWUX(WUDImage image, String outputFolder, boolean overwrite) throws IOException { + return compressWUDToWUX(image, outputFolder, "game.wux", overwrite); + } + + public static File compressWUDToWUX(WUDImage image, String outputFolder, String filename, boolean overwrite) throws IOException { + if (image.isCompressed()) { + log.info("Given image is already compressed"); + return null; + } + + if (image.getWUDFileSize() != WUDImage.WUD_FILESIZE) { + log.info("Given WUD has not the expected filesize"); + return null; + } + + String usedOutputFolder = outputFolder; + if (usedOutputFolder == null) usedOutputFolder = ""; + Utils.createDir(usedOutputFolder); + + String filePath; + if (usedOutputFolder.isEmpty()) { + filePath = filename; + } else { + filePath = usedOutputFolder + File.separator + filename; + } + + File outputFile = new File(filePath); + + if (outputFile.exists() && !overwrite) { + log.info("Couldn't compress wud, target file already exists (" + outputFile.getAbsolutePath() + ")"); + return null; + } + + log.info("Writing compressed file to: " + outputFile.getAbsolutePath()); + 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, realSector); + sectorHashes.put(hash, realSector); + fileOutput.write(blockBuffer); + realSector++; + } else { + sectorMapping.put(curSector, oldOffset); + oldOffset = null; + } + + written += read; + curSector++; + if (curSector % 10 == 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.print(String.format(Locale.ROOT, "\rCompressing into .wux | Progress %.2f%% | Ratio: 1:%.2f | Read: %.2fMB | Written: %.2fMB\t", + percent, ratio, readMB, writtenMB)); + } + } while (written < image.getWUDFileSize()); + System.out.println(); + System.out.println("Sectors compressed."); + 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 outputFile; + } + + public static boolean compareWUDImage(WUDImage firstImage, WUDImage secondImage) throws IOException { + if (firstImage.getWUDFileSize() != secondImage.getWUDFileSize()) { + log.info("Filesize is 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 % 1 == 0) { + double readMB = totalread / 1024.0 / 1024.0; + double percent = ((double) totalread / WUDImage.WUD_FILESIZE) * 100; + System.out.print(String.format("\rVerification: %.2fMB done (%.2f%%)", readMB, percent)); + } + } while (totalread < WUDImage.WUD_FILESIZE); + System.out.println(); + System.out.print("Verfication done!"); + in1.close(); + in2.close(); + + return result; + } + + public static HashResult hashWUDImage(WUDImage image) throws IOException { + if (image == null) { + log.info("Failed to calculate the hash of the given image: input was null."); + return null; + } + + if (image.isCompressed()) { + log.info("The input file is compressed. The calculated hash is the hash of the corresponding .wud file, not this .wux!"); + } else if (image.isSplitted()) { + log.info("The input file is splitted. The calculated hash is the hash of the corresponding .wud file, not this splitted .wud"); + } + + InputStream in = image.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE); + + int bufferSize = 1024 * 1024 * 10; + long totalread = 0; + byte[] blockBuffer1 = new byte[bufferSize]; + ByteArrayBuffer overflow1 = new ByteArrayBuffer(bufferSize); + long curSector = 0; + + MessageDigest sha1 = null; + MessageDigest md5 = null; + Checksum checksumEngine = new CRC32(); + + try { + sha1 = MessageDigest.getInstance("SHA1"); + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + do { + int read1 = StreamUtils.getChunkFromStream(in, blockBuffer1, overflow1, bufferSize); + sha1.update(blockBuffer1, 0, read1); + md5.update(blockBuffer1, 0, read1); + checksumEngine.update(blockBuffer1, 0, read1); + + totalread += read1; + + curSector++; + if (curSector % 10 == 0) { + double readMB = totalread / 1024.0 / 1024.0; + double percent = ((double) totalread / WUDImage.WUD_FILESIZE) * 100; + System.out.print(String.format("\rHashing: %.2fMB done (%.2f%%)", readMB, percent)); + } + } while (totalread < WUDImage.WUD_FILESIZE); + double readMB = totalread / 1024.0 / 1024.0; + double percent = ((double) totalread / WUDImage.WUD_FILESIZE) * 100; + + System.out.println(String.format("\rHashing: %.2fMB done (%.2f%%)", readMB, percent)); + + HashResult result = new HashResult(sha1.digest(), md5.digest(), Utils.StringToByteArray(Long.toHexString(checksumEngine.getValue()))); + + in.close(); + + return result; + } +} diff --git a/src/de/mas/wiiu/jnus/entities/TMD.java b/src/de/mas/wiiu/jnus/entities/TMD.java new file mode 100644 index 0000000..04005d8 --- /dev/null +++ b/src/de/mas/wiiu/jnus/entities/TMD.java @@ -0,0 +1,226 @@ +package de.mas.wiiu.jnus.entities; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.entities.content.ContentInfo; +import lombok.Data; +import lombok.Getter; +import lombok.extern.java.Log; + +@Log +public final class TMD { + private static final int SIGNATURE_LENGTH = 0x100; + private static final int ISSUER_LENGTH = 0x40; + private static final int RESERVED_LENGTH = 0x3E; + private static final int SHA2_LENGTH = 0x20; + + private static final int POSITION_SIGNATURE = 0x04; + private static final int POSITION_ISSUER = 0x140; + private static final int POSITION_RESERVED = 0x19A; + private static final int POSITION_SHA2 = 0x1E4; + + private static final int CONTENT_INFO_ARRAY_SIZE = 0x40; + + private static final int CONTENT_INFO_OFFSET = 0x204; + private static final int CONTENT_OFFSET = 0xB04; + + @Getter private final int signatureType; // 0x000 + @Getter private final byte[] signature; // 0x004 + @Getter private final byte[] issuer; // 0x140 + @Getter private final byte version; // 0x180 + @Getter private final byte CACRLVersion; // 0x181 + @Getter private final byte signerCRLVersion; // 0x182 + @Getter private final long systemVersion; // 0x184 + @Getter private final long titleID; // 0x18C + @Getter private final int titleType; // 0x194 + @Getter private final short groupID; // 0x198 + @Getter private final byte[] reserved; // 0x19A + @Getter private final int accessRights; // 0x1D8 + @Getter private final short titleVersion; // 0x1DC + @Getter private final short contentCount; // 0x1DE + @Getter private final short bootIndex; // 0x1E0 + @Getter private final byte[] SHA2; // 0x1E4 + @Getter private final ContentInfo[] contentInfos; + private final Map contentToIndex = new HashMap<>(); + private final Map contentToID = new HashMap<>(); + + private TMD(TMDParam param) { + super(); + this.signatureType = param.getSignatureType(); + this.signature = param.getSignature(); + this.issuer = param.getIssuer(); + this.version = param.getVersion(); + this.CACRLVersion = param.getCACRLVersion(); + this.signerCRLVersion = param.getSignerCRLVersion(); + this.systemVersion = param.getSystemVersion(); + this.titleID = param.getTitleID(); + this.titleType = param.getTitleType(); + this.groupID = param.getGroupID(); + this.reserved = param.getReserved(); + this.accessRights = param.getAccessRights(); + this.titleVersion = param.getTitleVersion(); + this.contentCount = param.getContentCount(); + this.bootIndex = param.getBootIndex(); + this.SHA2 = param.getSHA2(); + this.contentInfos = param.getContentInfos(); + } + + public static TMD parseTMD(File tmd) throws IOException { + if (tmd == null || !tmd.exists()) { + log.info("TMD input file null or doesn't exist."); + return null; + } + return parseTMD(Files.readAllBytes(tmd.toPath())); + } + + public static TMD parseTMD(byte[] input) { + byte[] signature = new byte[SIGNATURE_LENGTH]; + byte[] issuer = new byte[ISSUER_LENGTH]; + byte[] reserved = new byte[RESERVED_LENGTH]; + byte[] SHA2 = new byte[SHA2_LENGTH]; + + ContentInfo[] contentInfos = new ContentInfo[CONTENT_INFO_ARRAY_SIZE]; + + ByteBuffer buffer = ByteBuffer.allocate(input.length); + buffer.put(input); + + // Get Signature + buffer.position(0); + int signatureType = buffer.getInt(); + buffer.position(POSITION_SIGNATURE); + buffer.get(signature, 0, SIGNATURE_LENGTH); + + // Get Issuer + buffer.position(POSITION_ISSUER); + buffer.get(issuer, 0, ISSUER_LENGTH); + + // 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(); + + // Get other information + buffer.position(POSITION_RESERVED); + buffer.get(reserved, 0, RESERVED_LENGTH); + + // 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(POSITION_SHA2); + buffer.get(SHA2, 0, SHA2_LENGTH); + + // Get contentInfos + buffer.position(CONTENT_INFO_OFFSET); + for (int i = 0; i < CONTENT_INFO_ARRAY_SIZE; i++) { + byte[] contentInfo = new byte[ContentInfo.CONTENT_INFO_SIZE]; + buffer.get(contentInfo, 0, ContentInfo.CONTENT_INFO_SIZE); + contentInfos[i] = ContentInfo.parseContentInfo(contentInfo); + } + + TMDParam param = new TMDParam(); + param.setSignatureType(signatureType); + param.setSignature(signature); + param.setVersion(version); + param.setCACRLVersion(CACRLVersion); + param.setSignerCRLVersion(signerCRLVersion); + param.setSystemVersion(systemVersion); + param.setTitleID(titleID); + param.setTitleType(titleType); + param.setGroupID(groupID); + param.setAccessRights(accessRights); + param.setTitleVersion(titleVersion); + param.setContentCount(contentCount); + param.setBootIndex(bootIndex); + param.setSHA2(SHA2); + param.setContentInfos(contentInfos); + + TMD result = new TMD(param); + + // Get Contents + for (int i = 0; i < contentCount; i++) { + buffer.position(CONTENT_OFFSET + (Content.CONTENT_SIZE * i)); + byte[] content = new byte[Content.CONTENT_SIZE]; + buffer.get(content, 0, Content.CONTENT_SIZE); + Content c = Content.parseContent(content); + result.setContentToIndex(c.getIndex(), c); + result.setContentToID(c.getID(), c); + } + + return result; + } + + public Content getContentByIndex(int index) { + return contentToIndex.get(index); + } + + private void setContentToIndex(int index, Content content) { + contentToIndex.put(index, content); + } + + public Content getContentByID(int id) { + return contentToID.get(id); + } + + private void setContentToID(int id, Content content) { + contentToID.put(id, content); + } + + /** + * Returns all contents mapped by index + * + * @return Map of Content, index/content pairs + */ + public Map getAllContents() { + return contentToIndex; + } + + public void printContents() { + long totalSize = 0; + for (Content c : contentToIndex.values()) { + totalSize += c.getEncryptedFileSize(); + System.out.println(c); + } + System.out.println("Total size: " + totalSize); + + } + + @Data + private static class TMDParam { + private int signatureType; // 0x000 + private byte[] signature; // 0x004 + private byte[] issuer; // 0x140 + private byte version; // 0x180 + private byte CACRLVersion; // 0x181 + private byte signerCRLVersion; // 0x182 + private long systemVersion; // 0x184 + private long titleID; // 0x18C + private int titleType; // 0x194 + private short groupID; // 0x198 + private byte[] reserved; // 0x19A + private int accessRights; // 0x1D8 + private short titleVersion; // 0x1DC + private short contentCount; // 0x1DE + private short bootIndex; // 0x1E0 + private byte[] SHA2; // 0x1E4 + private ContentInfo[] contentInfos; // + } +} diff --git a/src/de/mas/wiiu/jnus/entities/Ticket.java b/src/de/mas/wiiu/jnus/entities/Ticket.java new file mode 100644 index 0000000..4a58c6f --- /dev/null +++ b/src/de/mas/wiiu/jnus/entities/Ticket.java @@ -0,0 +1,95 @@ +package de.mas.wiiu.jnus.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.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.utils.Utils; +import de.mas.wiiu.jnus.utils.cryptography.AESDecryption; +import lombok.Getter; +import lombok.extern.java.Log; + +@Log +public final class Ticket { + private static final int POSITION_KEY = 0x1BF; + private static final int POSITION_TITLEID = 0x1DC; + + @Getter private final byte[] encryptedKey; + @Getter private final byte[] decryptedKey; + + @Getter private final byte[] IV; + + private Ticket(byte[] encryptedKey, byte[] decryptedKey, byte[] IV) { + this.encryptedKey = encryptedKey; + this.decryptedKey = decryptedKey; + this.IV = IV; + } + + 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(POSITION_KEY); + buffer.get(encryptedKey, 0x00, 0x10); + + // read titleID + buffer.position(POSITION_TITLEID); + long titleID = buffer.getLong(); + + Ticket result = createTicket(encryptedKey, titleID); + + return result; + } + + public static Ticket createTicket(byte[] encryptedKey, long titleID) { + byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array(); + byte[] decryptedKey = calculateDecryptedKey(encryptedKey, IV); + + return new Ticket(encryptedKey, decryptedKey, IV); + } + + 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; + return Arrays.equals(encryptedKey, other.encryptedKey); + } + + @Override + public String toString() { + return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey=" + Utils.ByteArrayToString(decryptedKey) + "]"; + } +} diff --git a/src/de/mas/wiiu/jnus/entities/content/Content.java b/src/de/mas/wiiu/jnus/entities/content/Content.java new file mode 100644 index 0000000..1a213c5 --- /dev/null +++ b/src/de/mas/wiiu/jnus/entities/content/Content.java @@ -0,0 +1,192 @@ +package de.mas.wiiu.jnus.entities.content; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import de.mas.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.entities.fst.FSTEntry; +import de.mas.wiiu.jnus.utils.Utils; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.java.Log; + +/** + * Represents a Content + * + * @author Maschell + * + */ +@Log +public class Content { + public static final short CONTENT_FLAG_UNKWN1 = 0x4000; + public static final short CONTENT_HASHED = 0x0002; + public static final short CONTENT_ENCRYPTED = 0x0001; + public static final int CONTENT_SIZE = 0x30; + + @Getter private final int ID; + @Getter private final short index; + @Getter private final short type; + + @Getter private final long encryptedFileSize; + @Getter private final byte[] SHA2Hash; + + @Getter private final List entries = new ArrayList<>(); + + @Getter @Setter private ContentFSTInfo contentFSTInfo; + + private Content(ContentParam param) { + this.ID = param.getID(); + this.index = param.getIndex(); + this.type = param.getType(); + this.encryptedFileSize = param.getEncryptedFileSize(); + this.SHA2Hash = param.getSHA2Hash(); + } + + /** + * 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 != CONTENT_SIZE) { + log.info("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); + byte[] hash2 = new byte[0x06]; + buffer.get(hash2, 0x00, 0x06); + + ContentParam param = new ContentParam(); + param.setID(ID); + param.setIndex(index); + param.setType(type); + param.setEncryptedFileSize(encryptedFileSize); + param.setSHA2Hash(hash); + + return new Content(param); + } + + /** + * 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; + } + + public boolean isUNKNWNFlag1Set() { + return (type & CONTENT_FLAG_UNKWN1) == CONTENT_FLAG_UNKWN1; + } + + /** + * 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; + return Arrays.equals(SHA2Hash, other.SHA2Hash); + } + + public long getEncryptedFileSizeAligned() { + return Utils.align(encryptedFileSize, 16); + } + + @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) + "]"; + } + + @Data + private static class ContentParam { + private int ID; + private short index; + private short type; + + private long encryptedFileSize; + private byte[] SHA2Hash; + + private ContentFSTInfo contentFSTInfo; + } + +} \ No newline at end of file diff --git a/src/de/mas/wiiu/jnus/entities/content/ContentFSTInfo.java b/src/de/mas/wiiu/jnus/entities/content/ContentFSTInfo.java new file mode 100644 index 0000000..1f39e2c --- /dev/null +++ b/src/de/mas/wiiu/jnus/entities/content/ContentFSTInfo.java @@ -0,0 +1,105 @@ +package de.mas.wiiu.jnus.entities.content; + +import java.nio.ByteBuffer; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.java.Log; + +@EqualsAndHashCode +/** + * Representation on an Object of the first section + * of an FST. + * + * @author Maschell + * + */ +@Log +public final class ContentFSTInfo { + @Getter private final long offsetSector; + @Getter private final long sizeSector; + @Getter private final long ownerTitleID; + @Getter private final int groupID; + @Getter private final byte unkown; + + private static int SECTOR_SIZE = 0x8000; + + private ContentFSTInfo(ContentFSTInfoParam param) { + this.offsetSector = param.getOffsetSector(); + this.sizeSector = param.getSizeSector(); + this.ownerTitleID = param.getOwnerTitleID(); + this.groupID = param.getGroupID(); + this.unkown = param.getUnkown(); + } + + /** + * 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) { + log.info("Error: invalid ContentFSTInfo byte[] input"); + return null; + } + ContentFSTInfoParam param = new ContentFSTInfoParam(); + 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(); + + param.setOffsetSector(offset); + param.setSizeSector(size); + param.setOwnerTitleID(ownerTitleID); + param.setGroupID(groupID); + param.setUnkown(unkown); + + return new ContentFSTInfo(param); + } + + /** + * 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 + "]"; + } + + @Data + private static class ContentFSTInfoParam { + private long offsetSector; + private long sizeSector; + private long ownerTitleID; + private int groupID; + private byte unkown; + } + +} diff --git a/src/de/mas/jnus/lib/entities/content/ContentInfo.java b/src/de/mas/wiiu/jnus/entities/content/ContentInfo.java similarity index 51% rename from src/de/mas/jnus/lib/entities/content/ContentInfo.java rename to src/de/mas/wiiu/jnus/entities/content/ContentInfo.java index 0edc6b1..3beafc3 100644 --- a/src/de/mas/jnus/lib/entities/content/ContentInfo.java +++ b/src/de/mas/wiiu/jnus/entities/content/ContentInfo.java @@ -1,63 +1,71 @@ -package de.mas.jnus.lib.entities.content; +package de.mas.wiiu.jnus.entities.content; import java.nio.ByteBuffer; import java.util.Arrays; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; +import lombok.extern.java.Log; @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]; - +@Log +public class ContentInfo { + public static final int CONTENT_INFO_SIZE = 0x24; + + @Getter private final short indexOffset; + @Getter private final short commandCount; + @Getter private final byte[] SHA2Hash; + public ContentInfo() { this((short) 0); } - + public ContentInfo(short contentCount) { - this((short) 0,contentCount); + this((short) 0, contentCount); } - public ContentInfo(short indexOffset,short commandCount) { - this(indexOffset,commandCount,null); + + public ContentInfo(short indexOffset, short commandCount) { + this(indexOffset, commandCount, null); } - public ContentInfo(short indexOffset,short commandCount,byte[] SHA2Hash) { - setIndexOffset(indexOffset); - setCommandCount(commandCount); - setSHA2Hash(SHA2Hash); + + public ContentInfo(short indexOffset, short commandCount, byte[] SHA2Hash) { + this.indexOffset = indexOffset; + this.commandCount = commandCount; + this.SHA2Hash = SHA2Hash; } - + /** * Creates a new ContentInfo object given be the raw byte data - * @param input 0x24 byte of data from the TMD (starting at 0x208) + * + * @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"); + public static ContentInfo parseContentInfo(byte[] input) { + if (input == null || input.length != CONTENT_INFO_SIZE) { + log.info("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]; + byte[] sha2hash = new byte[0x20]; buffer.position(0x04); buffer.get(sha2hash, 0x00, 0x20); - - return new ContentInfo(indexOffset, commandCount, sha2hash); + + 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/wiiu/jnus/entities/fst/FST.java b/src/de/mas/wiiu/jnus/entities/fst/FST.java new file mode 100644 index 0000000..a6fd7f7 --- /dev/null +++ b/src/de/mas/wiiu/jnus/entities/fst/FST.java @@ -0,0 +1,85 @@ +package de.mas.wiiu.jnus.entities.fst; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.entities.content.ContentFSTInfo; +import de.mas.wiiu.jnus.utils.ByteUtils; +import lombok.Getter; + +/** + * Represents the FST + * + * @author Maschell + * + */ +public final class FST { + @Getter private final FSTEntry root = FSTEntry.getRootFSTEntry(); + + @Getter private final int unknown; + @Getter private final int contentCount; + + @Getter private final Map contentFSTInfos = new HashMap<>(); + + private FST(int unknown, int contentCount) { + this.unknown = unknown; + this.contentCount = contentCount; + } + + /** + * 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 NullPointerException(); + // return null; + // throw new IllegalArgumentException("Not a FST. Maybe a wrong key?"); + } + + int unknownValue = ByteUtils.getIntFromBytes(fstData, 0x04); + int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08); + + FST result = new FST(unknownValue, contentCount); + + 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 < fstData.length - 1; i++) { + if (fstData[i] == 0 && fstData[i + 1] == 0) { + nameSize = i - nameOff; + } + } + + Map contentFSTInfos = result.getContentFSTInfos(); + for (int i = 0; i < contentCount; i++) { + byte contentFST[] = Arrays.copyOfRange(fstData, contentfst_offset + (i * 0x20), contentfst_offset + ((i + 1) * 0x20)); + contentFSTInfos.put(i, ContentFSTInfo.parseContentFST(contentFST)); + } + + byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size); + byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize); + + FSTEntry root = result.getRoot(); + + FSTService.parseFST(root, fstSection, nameSection, contentsMappedByIndex, contentFSTInfos); + + return result; + } +} diff --git a/src/de/mas/jnus/lib/entities/fst/FSTEntry.java b/src/de/mas/wiiu/jnus/entities/fst/FSTEntry.java similarity index 59% rename from src/de/mas/jnus/lib/entities/fst/FSTEntry.java rename to src/de/mas/wiiu/jnus/entities/fst/FSTEntry.java index c806d3b..aa5c7e1 100644 --- a/src/de/mas/jnus/lib/entities/fst/FSTEntry.java +++ b/src/de/mas/wiiu/jnus/entities/fst/FSTEntry.java @@ -1,11 +1,11 @@ -package de.mas.jnus.lib.entities.fst; +package de.mas.wiiu.jnus.entities.fst; import java.util.ArrayList; import java.util.List; -import de.mas.jnus.lib.entities.content.Content; +import de.mas.wiiu.jnus.entities.content.Content; +import lombok.Data; import lombok.Getter; -import lombok.Setter; import lombok.extern.java.Log; @Log @@ -18,29 +18,43 @@ public class FSTEntry{ public static final byte FSTEntry_DIR = (byte)0x01; public static final byte FSTEntry_notInNUS = (byte)0x80; - @Getter @Setter private String filename = ""; - @Getter @Setter private String path = ""; - @Getter @Setter private FSTEntry parent = null; + @Getter private final String filename; + @Getter private final String path; + @Getter private final FSTEntry parent; - private List children = null; + @Getter private final List children = new ArrayList<>(); - @Getter @Setter private short flags; + @Getter private final short flags; - @Getter @Setter private long fileSize = 0; - @Getter @Setter private long fileOffset = 0; + @Getter private final long fileSize; + @Getter private final long fileOffset; - @Getter private Content content = null; + @Getter private final Content content; - @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 private final boolean isDir; + @Getter private final boolean isRoot; + @Getter private final boolean isNotInPackage; - @Getter @Setter private short contentFSTID = 0; + @Getter private final short contentFSTID; - public FSTEntry(){ - + protected FSTEntry(FSTEntryParam fstParam){ + this.filename = fstParam.getFilename(); + this.path = fstParam.getPath(); + this.flags = fstParam.getFlags(); + this.parent = fstParam.getParent(); + if(parent != null){ + parent.children.add(this); + } + this.fileSize = fstParam.getFileSize(); + this.fileOffset = fstParam.getFileOffset(); + this.content = fstParam.getContent(); + if(content != null){ + content.addEntry(this); + } + this.isDir = fstParam.isDir(); + this.isRoot = fstParam.isRoot(); + this.isNotInPackage = fstParam.isNotInPackage(); + this.contentFSTID = fstParam.getContentFSTID(); } /** @@ -48,9 +62,10 @@ public class FSTEntry{ * @return */ public static FSTEntry getRootFSTEntry(){ - FSTEntry entry = new FSTEntry(); - entry.setRoot(true); - return entry; + FSTEntryParam param = new FSTEntryParam(); + param.setRoot(true); + param.setDir(true); + return new FSTEntry(param); } public String getFullPath() { @@ -65,18 +80,6 @@ public class FSTEntry{ 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); } @@ -120,16 +123,6 @@ public class FSTEntry{ } 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()){ @@ -160,6 +153,28 @@ public class FSTEntry{ public String toString() { return "FSTEntry [filename=" + filename + ", path=" + path + ", flags=" + flags + ", filesize=" + fileSize + ", fileoffset=" + fileOffset + ", content=" + content + ", isDir=" + isDir + ", isRoot=" + isRoot - + ", notInPackage=" + notInPackage + "]"; + + ", notInPackage=" + isNotInPackage + "]"; } + + @Data + protected static class FSTEntryParam { + private String filename = ""; + private String path = ""; + + private FSTEntry parent = null; + + private short flags; + + private long fileSize = 0; + private long fileOffset = 0; + + private Content content = null; + + private boolean isDir = false; + private boolean isRoot = false; + private boolean notInPackage = false; + + private short contentFSTID = 0; + } + } diff --git a/src/de/mas/wiiu/jnus/entities/fst/FSTService.java b/src/de/mas/wiiu/jnus/entities/fst/FSTService.java new file mode 100644 index 0000000..dcd404c --- /dev/null +++ b/src/de/mas/wiiu/jnus/entities/fst/FSTService.java @@ -0,0 +1,157 @@ +package de.mas.wiiu.jnus.entities.fst; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.entities.content.ContentFSTInfo; +import de.mas.wiiu.jnus.entities.fst.FSTEntry.FSTEntryParam; +import de.mas.wiiu.jnus.utils.ByteUtils; +import lombok.extern.java.Log; + +@Log +public final class FSTService { + private FSTService() { + } + + public static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection, Map contentsByIndex, + Map contentsFSTByIndex) { + int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08); + + int level = 0; + int[] LEntry = new int[16]; + int[] Entry = new int[16]; + String[] pathStrings = new String[16]; + for (int i = 0; i < 16; i++) { + pathStrings[i] = ""; + } + + HashMap fstEntryToOffsetMap = new HashMap<>(); + Entry[level] = 0; + LEntry[level++] = 0; + + fstEntryToOffsetMap.put(0, rootEntry); + + int lastlevel = level; + String path = "\\"; + + FSTEntry last = null; + 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); + + FSTEntryParam entryParam = new FSTEntry.FSTEntryParam(); + + if (lastlevel != level) { + path = pathStrings[level] + getFullPath(level - 1, level, fstSection, namesSection, Entry); + lastlevel = level; + } + + 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) { + entryParam.setNotInPackage(true); + } + FSTEntry parent = null; + if ((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR) { + entryParam.setDir(true); + int parentOffset = (int) fileOffset; + int nextOffset = (int) fileSize; + + parent = fstEntryToOffsetMap.get(parentOffset); + Entry[level] = i; + LEntry[level++] = nextOffset; + pathStrings[level] = path; + + if (level > 15) { + log.warning("level > 15"); + break; + } + } else { + entryParam.setFileOffset(fileOffset << 5); + + entryParam.setFileSize(fileSize); + parent = fstEntryToOffsetMap.get(Entry[level - 1]); + } + + entryParam.setFlags(flags); + entryParam.setFilename(filename); + entryParam.setPath(path); + + if (contentsByIndex != null) { + Content content = contentsByIndex.get((int) contentIndex); + if (content == null) { + log.warning("Content for FST Entry not found"); + } else { + + if (content.isHashed() && (content.getDecryptedFileSize() < (fileOffset << 5))) { // TODO: Figure out how this works... + entryParam.setFileOffset(fileOffset); + } + + entryParam.setContent(content); + + ContentFSTInfo contentFSTInfo = contentsFSTByIndex.get((int) contentIndex); + if (contentFSTInfo == null) { + log.warning("ContentFSTInfo for FST Entry not found"); + } else { + content.setContentFSTInfo(contentFSTInfo); + } + } + } + + entryParam.setContentFSTID(contentIndex); + entryParam.setParent(parent); + + FSTEntry entry = new FSTEntry(entryParam); + last = entry; + 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 ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) { + j++; + } + + return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + j))); + } + + public static String getFullPath(int startlevel, int endlevel, byte[] fstSection, byte[] namesSection, int[] Entry) { + StringBuilder sb = new StringBuilder(); + for (int i = startlevel; i < endlevel; 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/wiiu/jnus/implementations/NUSDataProvider.java similarity index 58% rename from src/de/mas/jnus/lib/implementations/NUSDataProvider.java rename to src/de/mas/wiiu/jnus/implementations/NUSDataProvider.java index 80ddc27..624b89b 100644 --- a/src/de/mas/jnus/lib/implementations/NUSDataProvider.java +++ b/src/de/mas/wiiu/jnus/implementations/NUSDataProvider.java @@ -1,103 +1,108 @@ -package de.mas.jnus.lib.implementations; +package de.mas.wiiu.jnus.implementations; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.util.concurrent.SynchronousQueue; -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 de.mas.wiiu.jnus.NUSTitle; +import de.mas.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.utils.FileUtils; +import de.mas.wiiu.jnus.utils.Utils; import lombok.Getter; -import lombok.Setter; +import lombok.NonNull; 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 () { - } - + @Getter private final NUSTitle NUSTitle; + + public NUSDataProvider(NUSTitle title) { + this.NUSTitle = title; + } + /** * 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. + * + * @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 { + public void saveEncryptedContentWithH3Hash(@NonNull Content content, @NonNull 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 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()){ + public void saveContentH3Hash(@NonNull Content content, @NonNull String outputFolder) throws IOException { + if (!content.isHashed()) { return; } byte[] hash = getContentH3Hash(content); - if(hash == null){ + if (hash == null || hash.length == 0) { return; } - String h3Filename = String.format("%08X%s", content.getID(),Settings.H3_EXTENTION); + 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){ - System.out.println(h3Filename + " already exists"); + if (output.exists() && output.length() == hash.length) { + log.info(h3Filename + " already exists"); return; } - System.out.println("Saving " + h3Filename +" "); - + 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. + * + * @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 { + public void saveEncryptedContent(@NonNull Content content, @NonNull String outputFolder) throws IOException { Utils.createDir(outputFolder); InputStream inputStream = getInputStreamFromContent(content, 0); - if(inputStream == null){ + 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()){ + if (output.exists()) { + if (output.length() == content.getEncryptedFileSizeAligned()) { log.info("Encrypted content alreadys exists, skipped"); return; - }else{ + } else { log.info("Encrypted content alreadys exists, but the length is not as expected. Saving it again"); } } - System.out.println("Saving " + content.getFilename()); - FileUtils.saveInputStreamToFile(output,inputStream,content.getEncryptedFileSize()); + FileUtils.saveInputStreamToFile(output, inputStream, content.getEncryptedFileSizeAligned()); } /** @@ -107,12 +112,17 @@ public abstract class NUSDataProvider { * @return * @throws IOException */ - public abstract InputStream getInputStreamFromContent(Content content,long offset) throws IOException ; + public abstract InputStream getInputStreamFromContent(Content content, long offset) throws IOException; + // TODO: JavaDocs 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/wiiu/jnus/implementations/NUSDataProviderLocal.java similarity index 65% rename from src/de/mas/jnus/lib/implementations/NUSDataProviderLocal.java rename to src/de/mas/wiiu/jnus/implementations/NUSDataProviderLocal.java index 744af0e..774765c 100644 --- a/src/de/mas/jnus/lib/implementations/NUSDataProviderLocal.java +++ b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderLocal.java @@ -1,4 +1,4 @@ -package de.mas.jnus.lib.implementations; +package de.mas.wiiu.jnus.implementations; import java.io.File; import java.io.FileInputStream; @@ -6,30 +6,33 @@ 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 de.mas.wiiu.jnus.NUSTitle; +import de.mas.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.utils.StreamUtils; import lombok.Getter; -import lombok.Setter; -public class NUSDataProviderLocal extends NUSDataProvider { - @Getter @Setter private String localPath = ""; - - public NUSDataProviderLocal() { +public final class NUSDataProviderLocal extends NUSDataProvider { + @Getter private final String localPath; + + public NUSDataProviderLocal(NUSTitle nustitle, String localPath) { + super(nustitle); + this.localPath = localPath; } - + 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()){ - + if (!filepath.exists()) { + return null; } InputStream in = new FileInputStream(filepath); - in.skip(offset); + StreamUtils.skipExactly(in, offset); return in; } @@ -37,8 +40,8 @@ public class NUSDataProviderLocal extends NUSDataProvider { 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; + if (!h3File.exists()) { + return new byte[0]; } return Files.readAllBytes(h3File.toPath()); } @@ -52,22 +55,28 @@ public class NUSDataProviderLocal extends NUSDataProvider { } @Override - public byte[] getRawTicket() throws IOException { + public byte[] getRawTicket() throws IOException { String inputPath = getLocalPath(); String ticketPath = inputPath + File.separator + Settings.TICKET_FILENAME; - File ticketFile = new File(ticketPath); + 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); + File certFile = new File(certPath); return Files.readAllBytes(certFile.toPath()); } + + @Override + public void cleanup() throws IOException { + // We don't need this + } + + @Override + public String toString() { + return "NUSDataProviderLocal [localPath=" + localPath + "]"; + } } diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProviderRemote.java b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java similarity index 67% rename from src/de/mas/jnus/lib/implementations/NUSDataProviderRemote.java rename to src/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java index 4b52658..b094814 100644 --- a/src/de/mas/jnus/lib/implementations/NUSDataProviderRemote.java +++ b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java @@ -1,27 +1,31 @@ -package de.mas.jnus.lib.implementations; +package de.mas.wiiu.jnus.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 de.mas.wiiu.jnus.NUSTitle; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.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; - + @Getter private final int version; + @Getter private final long titleID; + + public NUSDataProviderRemote(NUSTitle title, int version, long titleID) { + super(title); + this.version = version; + this.titleID = titleID; + } + @Override public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException { NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); - InputStream in = downloadService.getInputStreamForURL(getRemoteURL(content),fileOffsetBlock); - return in; + return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock); } - private String getRemoteURL(Content content) { - return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(),content.getID()); + private String getRemoteURL(Content content) { + return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(), content.getID()); } @Override @@ -34,29 +38,29 @@ public class NUSDataProviderRemote extends NUSDataProvider { @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 + public byte[] getRawCert() throws IOException { + return new byte[0]; // TODO: needs to be implemented } @Override - public byte[] getRawCert() throws IOException { - return null; + public void cleanup() throws IOException { + // We don't need this } } diff --git a/src/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java new file mode 100644 index 0000000..3b61da7 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java @@ -0,0 +1,104 @@ +package de.mas.wiiu.jnus.implementations; + +import java.io.IOException; +import java.io.InputStream; + +import de.mas.wiiu.jnus.NUSTitle; +import de.mas.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.entities.TMD; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.implementations.wud.parser.WUDGamePartition; +import de.mas.wiiu.jnus.implementations.wud.parser.WUDInfo; +import de.mas.wiiu.jnus.implementations.wud.parser.WUDPartitionHeader; +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader; +import lombok.Getter; +import lombok.extern.java.Log; + +@Log +public class NUSDataProviderWUD extends NUSDataProvider { + @Getter private final WUDInfo WUDInfo; + + private final TMD tmd; + + public NUSDataProviderWUD(NUSTitle title, WUDInfo wudinfo) { + super(title); + this.WUDInfo = wudinfo; + this.tmd = TMD.parseTMD(getRawTMD()); + } + + 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); + } + + public TMD getTMD() { + return tmd; + } + + @Override + public byte[] getRawTMD() { + return getGamePartition().getRawTMD(); + } + + @Override + public byte[] getRawTicket() { + return getGamePartition().getRawTicket(); + } + + @Override + public byte[] getRawCert() throws IOException { + return getGamePartition().getRawCert(); + } + + public WUDGamePartition getGamePartition() { + return getWUDInfo().getGamePartition(); + } + + public WUDPartitionHeader getGamePartitionHeader() { + return getGamePartition().getPartitionHeader(); + } + + public WUDDiscReader getDiscReader() { + return getWUDInfo().getWUDDiscReader(); + } + + @Override + public void cleanup() { + // We don't need it + } + + @Override + public String toString() { + return "NUSDataProviderWUD [WUDInfo=" + WUDInfo + "]"; + } +} diff --git a/src/de/mas/jnus/lib/implementations/NUSDataProviderWoomy.java b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java similarity index 52% rename from src/de/mas/jnus/lib/implementations/NUSDataProviderWoomy.java rename to src/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java index 0097a75..e78e004 100644 --- a/src/de/mas/jnus/lib/implementations/NUSDataProviderWoomy.java +++ b/src/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java @@ -1,95 +1,102 @@ -package de.mas.jnus.lib.implementations; +package de.mas.wiiu.jnus.implementations; +import java.io.FileNotFoundException; 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 de.mas.wiiu.jnus.NUSTitle; +import de.mas.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo; +import de.mas.wiiu.jnus.implementations.woomy.WoomyZipFile; +import lombok.AccessLevel; 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; - +public class NUSDataProviderWoomy extends NUSDataProvider { + @Getter private final WoomyInfo woomyInfo; + @Setter(AccessLevel.PRIVATE) private WoomyZipFile woomyZipFile; + + public NUSDataProviderWoomy(NUSTitle title, WoomyInfo woomyInfo) { + super(title); + this.woomyInfo = woomyInfo; + } + @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){ + if (entry == null) { log.warning("Inputstream for " + content.getFilename() + " not found"); - System.exit(1); + throw new FileNotFoundException("Inputstream for " + content.getFilename() + " not found"); } - return zipFile.getInputStream(entry); + 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(); + if (entry != null) { + WoomyZipFile zipFile = getNewWoomyZipFile(); byte[] result = zipFile.getEntryAsByte(entry); zipFile.close(); return result; } - return null; + return new byte[0]; } @Override public byte[] getRawTMD() throws IOException { ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TMD_FILENAME); - if(entry == null){ + if (entry == null) { log.warning(Settings.TMD_FILENAME + " not found in woomy file"); - System.exit(1); + throw new FileNotFoundException(Settings.TMD_FILENAME + " not found in woomy file"); } - WoomyZipFile zipFile = getNewWoomyZipFile(); + WoomyZipFile zipFile = getNewWoomyZipFile(); byte[] result = zipFile.getEntryAsByte(entry); zipFile.close(); - return result; + return result; } @Override public byte[] getRawTicket() throws IOException { ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TICKET_FILENAME); - if(entry == null){ + if (entry == null) { log.warning(Settings.TICKET_FILENAME + " not found in woomy file"); - System.exit(1); + throw new FileNotFoundException(Settings.TICKET_FILENAME + " not found in woomy file"); } - - WoomyZipFile zipFile = getNewWoomyZipFile(); + + WoomyZipFile zipFile = getNewWoomyZipFile(); byte[] result = zipFile.getEntryAsByte(entry); zipFile.close(); - return result; + return result; } public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException { - if(woomyZipFile == null || woomyZipFile.isClosed()){ - woomyZipFile = getNewWoomyZipFile(); + if (this.woomyZipFile == null || this.woomyZipFile.isClosed()) { + this.woomyZipFile = getNewWoomyZipFile(); } - return woomyZipFile; + return this.woomyZipFile; } private WoomyZipFile getNewWoomyZipFile() throws ZipException, IOException { - return new WoomyZipFile(getWoomyInfo().getWoomyFile()); + return new WoomyZipFile(getWoomyInfo().getWoomyFile()); } @Override public void cleanup() throws IOException { - if(woomyZipFile != null && woomyZipFile.isClosed()){ - woomyZipFile.close(); + if (this.woomyZipFile != null && this.woomyZipFile.isClosed()) { + this.woomyZipFile.close(); } } @Override public byte[] getRawCert() throws IOException { - // TODO Auto-generated method stub - return null; + return new byte[0]; } } diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyInfo.java similarity index 65% rename from src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java rename to src/de/mas/wiiu/jnus/implementations/woomy/WoomyInfo.java index 5f59a51..2472442 100644 --- a/src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java +++ b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyInfo.java @@ -1,4 +1,4 @@ -package de.mas.jnus.lib.implementations.woomy; +package de.mas.wiiu.jnus.implementations.woomy; import java.io.File; import java.util.Map; @@ -10,5 +10,5 @@ import lombok.Data; public class WoomyInfo { private String name; private File woomyFile; - private Map contentFiles; + private Map contentFiles; } diff --git a/src/de/mas/wiiu/jnus/implementations/woomy/WoomyMeta.java b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyMeta.java new file mode 100644 index 0000000..f98699a --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyMeta.java @@ -0,0 +1,25 @@ +package de.mas.wiiu.jnus.implementations.woomy; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Data; + +@Data +public class WoomyMeta { + private final String name; + private final int icon; + private final List entries = new ArrayList<>(); + + public void addEntry(String name, String folder, int entryCount) { + WoomyEntry entry = new WoomyEntry(name, folder, entryCount); + getEntries().add(entry); + } + + @Data + public class WoomyEntry { + private final String name; + private final String folder; + private final int entryCount; + } +} diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyMetaParser.java b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyMetaParser.java similarity index 68% rename from src/de/mas/jnus/lib/implementations/woomy/WoomyMetaParser.java rename to src/de/mas/wiiu/jnus/implementations/woomy/WoomyMetaParser.java index 66b3f99..7780ec9 100644 --- a/src/de/mas/jnus/lib/implementations/woomy/WoomyMetaParser.java +++ b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyMetaParser.java @@ -1,61 +1,66 @@ -package de.mas.jnus.lib.implementations.woomy; +package de.mas.wiiu.jnus.implementations.woomy; import java.io.InputStream; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import de.mas.jnus.lib.utils.XMLParser; +import de.mas.wiiu.jnus.utils.XMLParser; import lombok.extern.java.Log; @Log -public class WoomyMetaParser extends XMLParser{ +public final 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(){ + private WoomyMetaParser() { } - - public static WoomyMeta parseMeta(InputStream data){ - XMLParser parser = new WoomyMetaParser(); - WoomyMeta result = new WoomyMeta(); + + public static WoomyMeta parseMeta(InputStream data) { + XMLParser parser = new WoomyMetaParser(); + String resultName = ""; + int resultIcon = 0; try { parser.loadDocument(data); - } catch(Exception e){ + } 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); + if (name != null && !name.isEmpty()) { + resultName = name; } - + String icon = parser.getValueOfElement(WOOMY_METADATA_ICON); - if(icon != null && !icon.isEmpty()){ - int icon_val = Integer.parseInt(icon); - result.setIcon(icon_val); + if (icon != null && !icon.isEmpty()) { + int icon_val = Integer.parseInt(icon); + resultIcon = icon_val; } + + WoomyMeta result = new WoomyMeta(resultName, resultIcon); + Node entries_node = parser.getNodeByValue(WOOMY_METADATA_ENTRIES); - + NodeList entry_list = entries_node.getChildNodes(); - for(int i = 0;i contentFiles = loadFileList(zipFile,regEx); + String regEx = entry.getFolder() + ".*"; // We want all files in the entry fodler + Map contentFiles = loadFileList(zipFile, regEx); result.setContentFiles(contentFiles); - - }catch(ZipException e){ - + + } catch (ZipException e) { + log.info("Caught Execption : " + e.getMessage()); } return result; } - private static Map loadFileList(@NotNull ZipFile zipFile, @NotNull String regEx) { + private static Map loadFileList(@NonNull ZipFile zipFile, @NonNull String regEx) { Enumeration zipEntries = zipFile.entries(); - Map result = new HashMap<>(); + Map result = new HashMap<>(); Pattern pattern = Pattern.compile(regEx); while (zipEntries.hasMoreElements()) { ZipEntry entry = (ZipEntry) zipEntries.nextElement(); - if(!entry.isDirectory()){ + if (!entry.isDirectory()) { String entryName = entry.getName(); Matcher matcher = pattern.matcher(entryName); - if(matcher.matches()){ - String[] tokens = entryName.split("[\\\\|/]"); //We only want the filename! + if (matcher.matches()) { + String[] tokens = entryName.split("[\\\\|/]"); // We only want the filename! String filename = tokens[tokens.length - 1]; - result.put(filename.toLowerCase(), entry); + result.put(filename.toLowerCase(Locale.ENGLISH), entry); } } - } + } return result; } } diff --git a/src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyZipFile.java similarity index 85% rename from src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java rename to src/de/mas/wiiu/jnus/implementations/woomy/WoomyZipFile.java index f50759f..7147c54 100644 --- a/src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java +++ b/src/de/mas/wiiu/jnus/implementations/woomy/WoomyZipFile.java @@ -1,4 +1,4 @@ -package de.mas.jnus.lib.implementations.woomy; +package de.mas.wiiu.jnus.implementations.woomy; import java.io.File; import java.io.IOException; @@ -6,22 +6,24 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; -import de.mas.jnus.lib.utils.StreamUtils; +import de.mas.wiiu.jnus.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(); @@ -29,7 +31,6 @@ public class WoomyZipFile extends ZipFile { } public byte[] getEntryAsByte(ZipEntry entry) throws IOException { - return StreamUtils.getBytesFromStream(getInputStream(entry),(int) entry.getSize()); + return StreamUtils.getBytesFromStream(getInputStream(entry), (int) entry.getSize()); } - } diff --git a/src/de/mas/wiiu/jnus/implementations/wud/WUDImage.java b/src/de/mas/wiiu/jnus/implementations/wud/WUDImage.java new file mode 100644 index 0000000..c09057f --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/WUDImage.java @@ -0,0 +1,110 @@ +package de.mas.wiiu.jnus.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.wiiu.jnus.implementations.wud.reader.WUDDiscReader; +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderCompressed; +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderSplitted; +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReaderUncompressed; +import de.mas.wiiu.jnus.utils.ByteUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.java.Log; + +@Log +public class WUDImage { + public static long WUD_FILESIZE = 0x5D3A00000L; + + @Getter private final File fileHandle; + @Getter @Setter private WUDImageCompressedInfo compressedInfo = null; + + @Getter private final boolean isCompressed; + @Getter private final boolean isSplitted; + + private long inputFileSize = 0L; + @Getter private final WUDDiscReader WUDDiscReader; + + public WUDImage(File file) throws IOException { + if (file == null || !file.exists()) { + log.info("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"); + this.isCompressed = true; + this.isSplitted = false; + 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 < compressedInfo.getIndexTableEntryCount(); i++) { + indexTable.put((int) i, ByteUtils.getUnsingedIntFromBytes(tableData, (int) cur_offset, ByteOrder.LITTLE_ENDIAN)); + cur_offset += 0x04; + } + compressedInfo.setIndexTable(indexTable); + setCompressedInfo(compressedInfo); + } else { + this.isCompressed = false; + if (file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1)) + && (file.length() == WUDDiscReaderSplitted.WUD_SPLITTED_FILE_SIZE)) { + this.isSplitted = true; + log.info("Image is splitted"); + } else { + this.isSplitted = false; + } + } + + if (isCompressed()) { + this.WUDDiscReader = new WUDDiscReaderCompressed(this); + } else if (isSplitted()) { + this.WUDDiscReader = new WUDDiscReaderSplitted(this); + } else { + this.WUDDiscReader = new WUDDiscReaderUncompressed(this); + } + + fileStream.close(); + this.fileHandle = file; + } + + public long getWUDFileSize() { + if (inputFileSize == 0) { + if (isSplitted()) { + inputFileSize = calculateSplittedFileSize(); + } else if (isCompressed()) { + inputFileSize = getCompressedInfo().getUncompressedSize(); + } else { + inputFileSize = getFileHandle().length(); + } + } + return inputFileSize; + } + + private long calculateSplittedFileSize() { + long result = 0; + File filehandlePart1 = getFileHandle(); + String pathToFiles = filehandlePart1.getParentFile().getAbsolutePath(); + for (int i = 1; i <= WUDDiscReaderSplitted.NUMBER_OF_FILES; i++) { + String filePartPath = pathToFiles + File.separator + String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, i); + File part = new File(filePartPath); + if (part.exists()) { + result += part.length(); + } + } + return result; + } +} diff --git a/src/de/mas/wiiu/jnus/implementations/wud/WUDImageCompressedInfo.java b/src/de/mas/wiiu/jnus/implementations/wud/WUDImageCompressedInfo.java new file mode 100644 index 0000000..e3d6c04 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/WUDImageCompressedInfo.java @@ -0,0 +1,98 @@ +package de.mas.wiiu.jnus.implementations.wud; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; + +import de.mas.wiiu.jnus.utils.ByteUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.java.Log; + +@Log +public class WUDImageCompressedInfo { + public static final int WUX_HEADER_SIZE = 0x20; + public static final int WUX_MAGIC_0 = 0x30585557; + public static final int WUX_MAGIC_1 = 0x1099d02e; + public static final int SECTOR_SIZE = 0x8000; + + @Getter private final int sectorSize; + @Getter private final long uncompressedSize; + @Getter private final int flags; + + @Getter @Setter private long indexTableEntryCount; + @Getter private final long offsetIndexTable = WUX_HEADER_SIZE; + @Getter @Setter private long offsetSectorArray; + @Getter @Setter private long indexTableSize; + + private final boolean valid; + + @Getter private Map indexTable = new HashMap<>(); + + public WUDImageCompressedInfo(byte[] headData) { + if (headData.length < WUX_HEADER_SIZE) { + log.info("WUX header length wrong"); + System.exit(1); + } + int magic0 = ByteUtils.getIntFromBytes(headData, 0x00, ByteOrder.LITTLE_ENDIAN); + int magic1 = ByteUtils.getIntFromBytes(headData, 0x04, ByteOrder.LITTLE_ENDIAN); + if (magic0 == WUX_MAGIC_0 && magic1 == WUX_MAGIC_1) { + valid = true; + } else { + valid = false; + } + this.sectorSize = ByteUtils.getIntFromBytes(headData, 0x08, ByteOrder.LITTLE_ENDIAN); + this.flags = ByteUtils.getIntFromBytes(headData, 0x0C, ByteOrder.LITTLE_ENDIAN); + this.uncompressedSize = 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) { + this.sectorSize = sectorSize; + this.flags = flags; + this.uncompressedSize = uncompressedSize; + valid = true; + calculateOffsets(); + } + + private void calculateOffsets() { + long indexTableEntryCount = (getUncompressedSize() + getSectorSize() - 1) / getSectorSize(); + setIndexTableEntryCount(indexTableEntryCount); + 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 valid; + } + + 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(WUX_MAGIC_0); + result.putInt(WUX_MAGIC_1); + result.putInt(getSectorSize()); + result.putInt(getFlags()); + result.putLong(getUncompressedSize()); + return result.array(); + } +} diff --git a/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDGamePartition.java b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDGamePartition.java new file mode 100644 index 0000000..b8c0978 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDGamePartition.java @@ -0,0 +1,19 @@ +package de.mas.wiiu.jnus.implementations.wud.parser; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class WUDGamePartition extends WUDPartition { + private final byte[] rawTMD; + private final byte[] rawCert; + private final byte[] rawTicket; + + public WUDGamePartition(String partitionName, long partitionOffset, byte[] rawTMD, byte[] rawCert, byte[] rawTicket) { + super(partitionName, partitionOffset); + this.rawTMD = rawTMD; + this.rawCert = rawCert; + this.rawTicket = rawTicket; + } +} diff --git a/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfo.java b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfo.java new file mode 100644 index 0000000..1bfad01 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfo.java @@ -0,0 +1,43 @@ +package de.mas.wiiu.jnus.implementations.wud.parser; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +@Data +public class WUDInfo { + private final byte[] titleKey; + + private final WUDDiscReader WUDDiscReader; + private final Map partitions = new HashMap<>(); + + @Getter(AccessLevel.PRIVATE) @Setter(AccessLevel.PROTECTED) private String gamePartitionName; + + private WUDGamePartition cachedGamePartition = null; + + public void addPartion(String partitionName, WUDGamePartition partition) { + getPartitions().put(partitionName, partition); + } + + 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/wiiu/jnus/implementations/wud/parser/WUDInfoParser.java b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfoParser.java new file mode 100644 index 0000000..ebadf07 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDInfoParser.java @@ -0,0 +1,151 @@ +package de.mas.wiiu.jnus.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.wiiu.jnus.Settings; +import de.mas.wiiu.jnus.entities.content.ContentFSTInfo; +import de.mas.wiiu.jnus.entities.fst.FST; +import de.mas.wiiu.jnus.entities.fst.FSTEntry; +import de.mas.wiiu.jnus.implementations.wud.reader.WUDDiscReader; +import de.mas.wiiu.jnus.utils.ByteUtils; +import de.mas.wiiu.jnus.utils.Utils; +import lombok.extern.java.Log; + +@Log +// TODO: reduce magic numbers +public final 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"; + + private WUDInfoParser() { + // + } + + public static WUDInfo createAndLoad(WUDDiscReader discReader, byte[] titleKey) throws IOException { + WUDInfo result = new WUDInfo(titleKey, 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)) { + log.info("Decryption of PartitionTocBlock failed"); + return null; + } + + Map partitions = readPartitions(result, PartitionTocBlock); + result.getPartitions().clear(); + result.getPartitions().putAll(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<>(); + + byte[] gamePartitionTMD = new byte[0]; + byte[] gamePartitionTicket = new byte[0]; + byte[] gamePartitionCert = new byte[0]; + + String realGamePartitionName = null; + // populate partition information from decrypted TOC + for (int i = 0; i < partitionCount; i++) { + + 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 < partitionIdentifier.length; j++) { + if (partitionIdentifier[j] == 0) { + break; + } + } + String partitionName = new String(Arrays.copyOfRange(partitionIdentifier, 0, j)); + + // calculate partition offset (relative from WIIU_DECRYPTED_AREA_OFFSET) from decrypted TOC + long tmp = ByteUtils.getUnsingedIntFromBytes(partitionTocBlock, (PARTITION_TOC_OFFSET + (i * PARTITION_TOC_ENTRY_SIZE) + 0x20), + ByteOrder.BIG_ENDIAN); + + long partitionOffset = ((tmp * (long) 0x8000) - 0x10000); + + WUDPartition partition = new WUDPartition(partitionName, partitionOffset); + + if (partitionName.startsWith("SI")) { + byte[] fileTableBlock = wudInfo.getWUDDiscReader().readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET + partitionOffset, 0, 0x8000, + wudInfo.getTitleKey(), null); + if (!Arrays.equals(Arrays.copyOfRange(fileTableBlock, 0, 4), PARTITION_FILE_TABLE_SIGNATURE)) { + log.info("FST Decrpytion failed"); + continue; + } + + FST fst = FST.parseFST(fileTableBlock, null); + + byte[] rawTIK = getFSTEntryAsByte(WUD_TICKET_FILENAME, partition, fst, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey()); + byte[] rawTMD = getFSTEntryAsByte(WUD_TMD_FILENAME, partition, fst, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey()); + byte[] rawCert = getFSTEntryAsByte(WUD_CERT_FILENAME, partition, fst, wudInfo.getWUDDiscReader(), wudInfo.getTitleKey()); + + gamePartitionTMD = rawTMD; + gamePartitionTicket = rawTIK; + gamePartitionCert = rawCert; + + // We want to use the real game partition + realGamePartitionName = partitionName = "GM" + Utils.ByteArrayToString(Arrays.copyOfRange(rawTIK, 0x1DC, 0x1DC + 0x08)); + } else if (partitionName.startsWith(realGamePartitionName)) { + wudInfo.setGamePartitionName(partitionName); + partition = new WUDGamePartition(partitionName, partitionOffset, gamePartitionTMD, gamePartitionCert, gamePartitionTicket); + } + byte[] header = wudInfo.getWUDDiscReader().readEncryptedToByteArray(partition.getPartitionOffset() + 0x10000, 0, 0x8000); + WUDPartitionHeader partitionHeader = WUDPartitionHeader.parseHeader(header); + partition.setPartitionHeader(partitionHeader); + + partitions.put(partitionName, partition); + } + + return partitions; + } + + private static byte[] getFSTEntryAsByte(String filename, WUDPartition partition, FST fst, WUDDiscReader discReader, byte[] key) throws IOException { + FSTEntry entry = getEntryByName(fst.getRoot(), filename); + ContentFSTInfo info = fst.getContentFSTInfos().get((int) entry.getContentFSTID()); + + // Calculating the IV + ByteBuffer byteBuffer = ByteBuffer.allocate(0x10); + byteBuffer.position(0x08); + byte[] IV = byteBuffer.putLong(entry.getFileOffset() >> 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/wiiu/jnus/implementations/wud/parser/WUDPartition.java b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartition.java new file mode 100644 index 0000000..5861e07 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartition.java @@ -0,0 +1,11 @@ +package de.mas.wiiu.jnus.implementations.wud.parser; + +import lombok.Data; + +@Data +public class WUDPartition { + private final String partitionName; + private final long partitionOffset; + + private WUDPartitionHeader partitionHeader; +} diff --git a/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartitionHeader.java b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartitionHeader.java new file mode 100644 index 0000000..14cb642 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/parser/WUDPartitionHeader.java @@ -0,0 +1,87 @@ +package de.mas.wiiu.jnus.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.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.utils.ByteUtils; +import de.mas.wiiu.jnus.utils.HashUtil; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.java.Log; + +@Log +public final class WUDPartitionHeader { + @Getter @Setter private boolean calculatedHashes = false; + @Getter private final HashMap h3Hashes = new HashMap<>(); + @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 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/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java b/src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java new file mode 100644 index 0000000..f6272e3 --- /dev/null +++ b/src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java @@ -0,0 +1,139 @@ +package de.mas.wiiu.jnus.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.wiiu.jnus.implementations.wud.WUDImage; +import de.mas.wiiu.jnus.utils.cryptography.AESDecryption; +import lombok.Getter; +import lombok.extern.java.Log; + +@Log +public abstract class WUDDiscReader { + @Getter private final WUDImage image; + + public WUDDiscReader(WUDImage image) { + this.image = 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(); + } + },"readEncryptedToInputStream@" + this.hashCode()).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(); + } + },"readDecryptedToInputStream@" + this.hashCode()).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 { + byte[] usedIV = IV; + if (usedIV == null) { + usedIV = new byte[0x10]; + } + long usedSize = size; + long usedFileOffset = fileOffset; + byte[] buffer; + + long maxCopySize; + long copySize; + + long readOffset; + + int blockSize = 0x8000; + long totalread = 0; + + do { + long blockNumber = (usedFileOffset / blockSize); + long blockOffset = (usedFileOffset % blockSize); + + readOffset = clusterOffset + (blockNumber * blockSize); + // (long)WiiUDisc.WIIU_DECRYPTED_AREA_OFFSET + volumeOffset + clusterOffset + (blockStructure.getBlockNumber() * 0x8000); + + buffer = readDecryptedChunk(readOffset, key, usedIV); + maxCopySize = 0x8000 - blockOffset; + copySize = (usedSize > maxCopySize) ? maxCopySize : usedSize; + + outputStream.write(Arrays.copyOfRange(buffer, (int) blockOffset, (int) copySize)); + totalread += copySize; + + // update counters + usedSize -= copySize; + usedFileOffset += copySize; + } while (totalread < usedSize); + + outputStream.close(); + } + + public RandomAccessFile getRandomAccessFileStream() throws FileNotFoundException { + if (getImage() == null || getImage().getFileHandle() == null) { + log.warning("No image or image filehandle set."); + System.exit(1); //TODO: NOOOOOOOOOOOOO + } + return new RandomAccessFile(getImage().getFileHandle(), "r"); + } +} diff --git a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderCompressed.java b/src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java similarity index 52% rename from src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderCompressed.java rename to src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java index 66a23b8..baf6a9c 100644 --- a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderCompressed.java +++ b/src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java @@ -1,69 +1,73 @@ -package de.mas.jnus.lib.implementations.wud.reader; +package de.mas.wiiu.jnus.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; +import de.mas.wiiu.jnus.implementations.wud.WUDImage; +import de.mas.wiiu.jnus.implementations.wud.WUDImageCompressedInfo; +import lombok.extern.java.Log; @Log -public class WUDDiscReaderCompressed extends WUDDiscReader{ - +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/ - */ + + /** + * 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 ){ + + long usedOffset = offset; + long usedSize = size; + + if (fileBytesLeft <= 0) { log.warning("offset too big"); System.exit(1); } - if( fileBytesLeft < size ){ - size = fileBytesLeft; + if (fileBytesLeft < usedSize) { + usedSize = 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()); + while (usedSize > 0) { + long sectorOffset = (usedOffset % 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 + if ((offsetInFile + bufferSize) >= 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 + } 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){ + + int read = input.read(buffer, 0, curReadSize); + if (read < 0) break; + if (totalread + read > size) { read = (int) (size - totalread); } - try{ + try { outputStream.write(Arrays.copyOfRange(buffer, 0, read)); - }catch(IOException e){ - if(e.getMessage().equals("Pipe closed")){ + } catch (IOException e) { + if (e.getMessage().equals("Pipe closed")) { break; - }else{ + } else { input.close(); throw e; } } totalread += read; curOffset += read; - }while(totalread < size); - - + } while (totalread < size); + input.close(); outputStream.close(); } - - private int getFilePartByOffset(long offset){ - return (int) (offset/WUD_SPLITTED_FILE_SIZE)+1; + + 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 long getOffsetInFilePart(int part, long offset) { + return offset - ((long) (part - 1) * WUD_SPLITTED_FILE_SIZE); } - - private RandomAccessFile getFileByOffset(long offset) throws IOException{ - File filehandlePart1 = new File(getImage().getFileHandle().getAbsolutePath()); //Create copy + + 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"); + + if (!part.exists()) { + log.info("File does not exist"); return null; } - RandomAccessFile result = new RandomAccessFile(part, "r"); + 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/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java similarity index 63% rename from src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderUncompressed.java rename to src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java index 3dfb174..d0f720a 100644 --- a/src/de/mas/jnus/lib/implementations/wud/reader/WUDDiscReaderUncompressed.java +++ b/src/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java @@ -1,45 +1,48 @@ -package de.mas.jnus.lib.implementations.wud.reader; +package de.mas.wiiu.jnus.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; +import de.mas.wiiu.jnus.implementations.wud.WUDImage; +import de.mas.wiiu.jnus.utils.StreamUtils; public class WUDDiscReaderUncompressed extends WUDDiscReader { public WUDDiscReaderUncompressed(WUDImage image) { super(image); } - + @Override - protected void readEncryptedToOutputStream(OutputStream outputStream, long offset,long size) throws IOException{ - + protected void readEncryptedToOutputStream(OutputStream outputStream, long offset, long size) throws IOException { + FileInputStream input = new FileInputStream(getImage().getFileHandle()); - input.skip(offset); + + StreamUtils.skipExactly(input, offset); + int bufferSize = 0x8000; byte[] buffer = new byte[bufferSize]; long totalread = 0; - do{ + do { int read = input.read(buffer); - if(read < 0) break; - if(totalread + read > size){ + if (read < 0) break; + if (totalread + read > size) { read = (int) (size - totalread); } - try{ + try { outputStream.write(Arrays.copyOfRange(buffer, 0, read)); - }catch(IOException e){ - if(e.getMessage().equals("Pipe closed")){ + } catch (IOException e) { + if (e.getMessage().equals("Pipe closed")) { break; - }else{ + } else { input.close(); throw e; } } totalread += read; - }while(totalread < size); + } while (totalread < size); input.close(); outputStream.close(); } - + } diff --git a/src/de/mas/wiiu/jnus/utils/ByteArrayBuffer.java b/src/de/mas/wiiu/jnus/utils/ByteArrayBuffer.java new file mode 100644 index 0000000..4f80330 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/ByteArrayBuffer.java @@ -0,0 +1,25 @@ +package de.mas.wiiu.jnus.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/wiiu/jnus/utils/ByteArrayWrapper.java b/src/de/mas/wiiu/jnus/utils/ByteArrayWrapper.java new file mode 100644 index 0000000..3b69fda --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/ByteArrayWrapper.java @@ -0,0 +1,27 @@ +package de.mas.wiiu.jnus.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/wiiu/jnus/utils/ByteUtils.java b/src/de/mas/wiiu/jnus/utils/ByteUtils.java new file mode 100644 index 0000000..4cb22c7 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/ByteUtils.java @@ -0,0 +1,85 @@ +package de.mas.wiiu.jnus.utils; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +public final class ByteUtils { + + private ByteUtils() { + // Utility Class + } + + 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/wiiu/jnus/utils/CheckSumWrongException.java b/src/de/mas/wiiu/jnus/utils/CheckSumWrongException.java new file mode 100644 index 0000000..c76d2fc --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/CheckSumWrongException.java @@ -0,0 +1,20 @@ +package de.mas.wiiu.jnus.utils; + +import lombok.Getter; + +@Getter +public class CheckSumWrongException extends Exception { + /** + * + */ + private static final long serialVersionUID = 5781223264453732269L; + private final byte[] givenHash; + private final byte[] expectedHash; + + public CheckSumWrongException(String string, byte[] given, byte[] expected) { + super(string); + this.givenHash = given; + this.expectedHash = expected; + + } +} diff --git a/src/de/mas/jnus/lib/utils/FileUtils.java b/src/de/mas/wiiu/jnus/utils/FileUtils.java similarity index 56% rename from src/de/mas/jnus/lib/utils/FileUtils.java rename to src/de/mas/wiiu/jnus/utils/FileUtils.java index 9a699cf..f84c762 100644 --- a/src/de/mas/jnus/lib/utils/FileUtils.java +++ b/src/de/mas/wiiu/jnus/utils/FileUtils.java @@ -1,4 +1,4 @@ -package de.mas.jnus.lib.utils; +package de.mas.wiiu.jnus.utils; import java.io.File; import java.io.FileOutputStream; @@ -7,29 +7,32 @@ import java.io.InputStream; import lombok.NonNull; -public class FileUtils { +public final class FileUtils { + private FileUtils() { + // Utility Class + } - public static boolean saveByteArrayToFile(String filePath,byte[] data) throws IOException { + public static boolean saveByteArrayToFile(String filePath, byte[] data) throws IOException { File target = new File(filePath); - if(target.isDirectory()){ + if (target.isDirectory()) { return false; } - File parent = new File(target.getAbsolutePath()).getParentFile(); - if(parent != null){ + File parent = target.getParentFile(); + if (parent != null) { Utils.createDir(parent.getAbsolutePath()); } - return saveByteArrayToFile(target,data); + 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 + * 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 { + public static boolean saveByteArrayToFile(@NonNull File output, byte[] data) throws IOException { FileOutputStream out = new FileOutputStream(output); out.write(data); out.close(); @@ -37,16 +40,15 @@ public class FileUtils { } /** - * 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 + * 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 { - + public static void saveInputStreamToFile(@NonNull File output, InputStream inputStream, long filesize) throws IOException { FileOutputStream out = new FileOutputStream(output); - StreamUtils.saveInputStreamToOutputStream(inputStream,out,filesize); + StreamUtils.saveInputStreamToOutputStream(inputStream, out, filesize); } } diff --git a/src/de/mas/wiiu/jnus/utils/HashResult.java b/src/de/mas/wiiu/jnus/utils/HashResult.java new file mode 100644 index 0000000..d6f69c5 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/HashResult.java @@ -0,0 +1,16 @@ +package de.mas.wiiu.jnus.utils; + +import lombok.Data; + +@Data +public class HashResult { + private final byte[] SHA1; + private final byte[] MD5; + private final byte[] CRC32; + + @Override + public String toString() { + return "HashResult [SHA1=" + Utils.ByteArrayToString(SHA1) + ", MD5=" + Utils.ByteArrayToString(MD5) + ", CRC32=" + Utils.ByteArrayToString(CRC32) + + "]"; + } +} diff --git a/src/de/mas/wiiu/jnus/utils/HashUtil.java b/src/de/mas/wiiu/jnus/utils/HashUtil.java new file mode 100644 index 0000000..87dcad0 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/HashUtil.java @@ -0,0 +1,265 @@ +package de.mas.wiiu.jnus.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import lombok.extern.java.Log; + +@Log +public final class HashUtil { + private HashUtil() { + // Utility class + } + + 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(InputStream in, long length) { + return hashSHA1(in, length, 0); + } + + public static byte[] hashSHA1(InputStream in, long length, int aligmnent) { + byte[] hash = new byte[0x14]; + MessageDigest sha1 = null; + try { + sha1 = MessageDigest.getInstance("SHA1"); + hash = hash(sha1, in, length, 0x8000, aligmnent); + } catch (NoSuchAlgorithmException | FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return hash; + } + + public static byte[] hashSHA1(File file) { + return hashSHA1(file, 0); + } + + public static byte[] hashSHA1(File file, int aligmnent) { + InputStream in; + try { + in = new FileInputStream(file); + return hashSHA1(in, file.length(), aligmnent); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + 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 { + inBlockBufferRead = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize); + + if (inBlockBufferRead <= 0) break; + + digest.update(blockBuffer, 0, inBlockBufferRead); + cur_position += inBlockBufferRead; + + } while (cur_position < target_size); + long missing_bytes = target_size - cur_position; + if(missing_bytes > 0){ + byte[] missing = new byte[(int) missing_bytes]; + digest.update(missing, 0, (int) missing_bytes); + } + + in.close(); + + return digest.digest(); + } + + public static boolean compareHashFolder(File input1, File input2) { + List expectedFiles = getInputFilesForFolder(input1); + List givenFiles = getInputFilesForFolder(input2); + String regexInput = input1.getAbsolutePath().toLowerCase(); + String regexOutput = input2.getAbsolutePath().toLowerCase(); + + List exptectedFileWithAdjustedPath = new ArrayList<>(); + for (File f : expectedFiles) { + String newPath = Utils.replaceStringInStringEscaped(f.getAbsolutePath().toLowerCase(), regexInput, regexOutput).toLowerCase(); + exptectedFileWithAdjustedPath.add(new File(newPath.toLowerCase())); + } + + if (!givenFiles.equals(exptectedFileWithAdjustedPath)) { + log.info("Folder doesn't contain the same files."); + List additionalFileList = new ArrayList<>(givenFiles); + List missingFileList = new ArrayList<>(exptectedFileWithAdjustedPath); + additionalFileList.removeAll(exptectedFileWithAdjustedPath); + missingFileList.removeAll(givenFiles); + if (!additionalFileList.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("Additional files:\n"); + for (File f : additionalFileList) { + sb.append(f.toString() + "\n"); + } + log.info(sb.toString()); + } + + if (!missingFileList.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("Missing files:\n"); + for (File f : missingFileList) { + sb.append(f.toString() + "\n"); + } + log.info(sb.toString()); + } + return false; + } + + boolean result = true; + for (File f : expectedFiles) { + File expected = f; + String newPath = Utils.replaceStringInStringEscaped(f.getAbsolutePath().toLowerCase(), regexInput, regexOutput).toLowerCase(); + File given = new File(newPath); + if (!compareHashFile(expected, given)) { + result = false; + break; + } + } + return result; + } + + private static boolean compareHashFile(File file1, File file2) { + if (file1 == null || !file1.exists() || file2 == null || !file2.exists()) { + return false; + } + if (file1.isDirectory() && file2.isDirectory()) { + return true; + } else if (file1.isDirectory() || file2.isDirectory()) { + return false; + } + byte[] hash1 = HashUtil.hashSHA1(file1); + byte[] hash2 = HashUtil.hashSHA1(file2); + boolean result = Arrays.equals(hash1, hash2); + if (!result) { + log.info("Hash doesn't match for " + file1.getAbsolutePath() + "(" + Utils.ByteArrayToString(hash1) + ") and " + file2.getAbsolutePath() + "(" + + Utils.ByteArrayToString(hash2) + ")!"); + } + return result; + } + + private static List getInputFilesForFolder(File input1) { + if (input1 == null || !input1.exists()) return new ArrayList<>(); + List result = new ArrayList<>(); + for (File f : input1.listFiles()) { + if (f.isDirectory()) { + result.add(f); + result.addAll(getInputFilesForFolder(f)); + } else { + result.add(f); + } + } + return result; + } + + public static void checkFileChunkHashes(byte[] hashes, byte[] h3Hashes, byte[] output, int block) throws CheckSumWrongException { + 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)) { + if (!Arrays.equals(expected_h0_hash, new byte[20])) { // Fix for /meta/WUP-N-HASP-EUR.bfma in 0005001b10059200 + throw new CheckSumWrongException("h0 checksumfail", real_h0_hash, expected_h0_hash); + } + } 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)) { + throw new CheckSumWrongException("h1 checksumfail", real_h1_hash, expected_h1_hash); + } 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)) { + throw new CheckSumWrongException("h2 checksumfail", real_h2_hash, expected_h2_hash); + } 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)) { + throw new CheckSumWrongException("h3 checksumfail", real_h3_hash, expected_h3_hash); + } else { + log.fine("h3 checksum right!"); + } + } + } +} diff --git a/src/de/mas/jnus/lib/utils/StreamUtils.java b/src/de/mas/wiiu/jnus/utils/StreamUtils.java similarity index 50% rename from src/de/mas/jnus/lib/utils/StreamUtils.java rename to src/de/mas/wiiu/jnus/utils/StreamUtils.java index df3d9a7..ad5e7a4 100644 --- a/src/de/mas/jnus/lib/utils/StreamUtils.java +++ b/src/de/mas/wiiu/jnus/utils/StreamUtils.java @@ -1,5 +1,6 @@ -package de.mas.jnus.lib.utils; +package de.mas.wiiu.jnus.utils; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -10,118 +11,142 @@ import java.util.Arrays; import lombok.extern.java.Log; @Log -public class StreamUtils { - public static byte[] getBytesFromStream(InputStream in,int size) throws IOException{ +public final class StreamUtils { + private StreamUtils() { + // Utility class + } + + public static byte[] getBytesFromStream(InputStream in, int size) throws IOException { byte[] result = new byte[size]; byte[] buffer = new byte[0x8000]; int totalRead = 0; - do{ + do { int read = in.read(buffer); - if(read < 0) break; + if (read < 0) break; System.arraycopy(buffer, 0, result, totalRead, read); totalRead += read; - }while(totalRead < size); + } while (totalRead < size); in.close(); return result; } - - public static int getChunkFromStream(InputStream inputStream,byte[] output, ByteArrayBuffer overflowbuffer,int BLOCKSIZE) throws IOException { + + 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")){ + do { + try { + bytesRead = inputStream.read(overflowbuf, overflowbuffer.getLengthOfDataInBuffer(), overflowbuffer.getSpaceLeft()); + + } catch (IOException e) { + log.info(e.getMessage()); + if (!e.getMessage().equals("Write end dead")) { throw e; } bytesRead = -1; } - if(bytesRead <= 0){ - if(overflowbuffer.getLengthOfDataInBuffer() > 0){ + + 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){ + + 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(); + } else { + System.arraycopy(overflowbuf, 0, output, inBlockBuffer, overflowbuffer.getLengthOfDataInBuffer()); + inBlockBuffer += overflowbuffer.getLengthOfDataInBuffer(); overflowbuffer.resetLengthOfDataInBuffer(); } - }while(inBlockBuffer != BLOCKSIZE); + } while (inBlockBuffer != BLOCKSIZE); return inBlockBuffer; } - + public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException { - saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null,0L); + try { + saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L); + } catch (CheckSumWrongException e) { + // Should never happen because the hash is not set. Lets print it anyway. + e.printStackTrace(); + } } - public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream,long filesize, byte[] hash,long expectedSizeForHash) throws IOException { - MessageDigest sha1 = null; - if(hash != null){ + public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash, + long expectedSizeForHash) throws IOException, CheckSumWrongException { + 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{ + do { read = inputStream.read(buffer); - if(read < 0){ + if (read < 0) { break; } - totalRead +=read; - - if(totalRead > filesize){ + 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); + + if (sha1 != null) { + sha1.update(buffer, 0, read); } - }while(written < filesize); - - if(sha1 != null){ - long missingInHash = expectedSizeForHash - written; - if(missingInHash > 0){ + } while (written < filesize); + + if (sha1 != null && hash != 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."); + if (!Arrays.equals(calculated_hash, expected_hash)) { + outputStream.close(); + inputStream.close(); + throw new CheckSumWrongException("Hash doesn't match saves output stream.", calculated_hash, expected_hash); } } - - outputStream.close(); + + outputStream.close(); inputStream.close(); } + + public static void skipExactly(InputStream in, long offset) throws IOException { + long n = offset; + while (n != 0) { + long skipped = in.skip(n); + if (skipped == 0) { + in.close(); + throw new EOFException(); + } + n -= skipped; + } + } } diff --git a/src/de/mas/wiiu/jnus/utils/Utils.java b/src/de/mas/wiiu/jnus/utils/Utils.java new file mode 100644 index 0000000..cc48043 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/Utils.java @@ -0,0 +1,149 @@ +package de.mas.wiiu.jnus.utils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import lombok.extern.java.Log; + +@Log +public final class Utils { + private Utils() { + // Utility class + } + + public static long align(long numToRound, int multiple) { + if ((multiple > 0) && ((multiple & (multiple - 1)) == 0)) { + return alignPower2(numToRound, multiple); + } else { + return alignGeneric(numToRound, multiple); + } + } + + private static long alignGeneric(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 success = new File(path).mkdirs(); + if (!success) { + log.fine("Creating directory \"" + path + "\" failed."); + return false; + } + } else if (!pathFile.isDirectory()) { + log.info("\"" + path + "\" already exists but is no directoy"); + return false; + } + return true; + } + + public static void deleteDir(File path) { + if (path == null || !path.exists()) return; + for (File file : path.listFiles()) { + if (file.isDirectory()) deleteDir(file); + file.delete(); + } + path.delete(); + } + + public static String replaceStringInStringEscaped(String input, String replace, String replaceWith) { + return input.replaceAll(Pattern.quote(replace), Matcher.quoteReplacement(replaceWith)); + } + + public static boolean checkXML(InputSource in) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + + builder.setErrorHandler(new ErrorHandler() { + + @Override + public void warning(SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void error(SAXParseException exception) throws SAXException { + throw exception; + + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throw exception; + } + + }); + + builder.parse(in); + return true; + } catch (Exception e) { + return false; + } + } + + public static boolean checkXML(InputStream in) { + return checkXML(new InputSource(in)); + } + + public static boolean checkXML(byte[] data) { + return checkXML(new ByteArrayInputStream(data)); + } + + public static boolean checkXML(File file) { + return checkXML(new InputSource(file.getAbsolutePath())); + } + + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + +} diff --git a/src/de/mas/jnus/lib/utils/XMLParser.java b/src/de/mas/wiiu/jnus/utils/XMLParser.java similarity index 85% rename from src/de/mas/jnus/lib/utils/XMLParser.java rename to src/de/mas/wiiu/jnus/utils/XMLParser.java index 8842219..a002a46 100644 --- a/src/de/mas/jnus/lib/utils/XMLParser.java +++ b/src/de/mas/wiiu/jnus/utils/XMLParser.java @@ -1,4 +1,4 @@ -package de.mas.jnus.lib.utils; +package de.mas.wiiu.jnus.utils; import java.io.IOException; @@ -13,8 +13,10 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -import com.sun.istack.internal.NotNull; +import lombok.NonNull; +import lombok.extern.java.Log; +@Log public class XMLParser { private Document document; @@ -43,7 +45,7 @@ public class XMLParser { } public Node getNodeByValue(String element,int index){ if(document == null){ - System.out.println("Please load the document first."); + log.info("Please load the document first."); } NodeList list = document.getElementsByTagName(element); if(list == null){ @@ -55,23 +57,23 @@ public class XMLParser { public String getValueOfElementAttribute(String element,int index,String attribute){ Node node = getNodeByValue(element, index); if(node == null){ - //System.out.println("Node is null"); + //log.info("Node is null"); return ""; } return getAttributeValueFromNode(node,attribute); } - public static String getAttributeValueFromNode(@NotNull Node element,String attribute){ + public static String getAttributeValueFromNode(@NonNull 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"); + //log.info("Node is null"); return ""; } - return node.getTextContent().toString(); + return node.getTextContent().toString(); } } diff --git a/src/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java b/src/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java new file mode 100644 index 0000000..6912bf5 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java @@ -0,0 +1,71 @@ +package de.mas.wiiu.jnus.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 final 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/wiiu/jnus/utils/cryptography/NUSDecryption.java b/src/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java new file mode 100644 index 0000000..3b7cea6 --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java @@ -0,0 +1,184 @@ +package de.mas.wiiu.jnus.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.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.utils.ByteArrayBuffer; +import de.mas.wiiu.jnus.utils.CheckSumWrongException; +import de.mas.wiiu.jnus.utils.HashUtil; +import de.mas.wiiu.jnus.utils.StreamUtils; +import de.mas.wiiu.jnus.utils.Utils; + +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, CheckSumWrongException { + MessageDigest sha1 = null; + MessageDigest sha1fallback = null; + if (h3hash != null) { + try { + sha1 = MessageDigest.getInstance("SHA1"); + sha1fallback = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + int BLOCKSIZE = 0x8000; + // 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, (int) Utils.align(inBlockBuffer, 16), IV); + + IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE); + + int toWrite = inBlockBuffer; + if ((written + inBlockBuffer) > filesize) { + toWrite = (int) (filesize - written); + } + + written += toWrite; + + outputStream.write(output, 0, toWrite); + + if (sha1 != null && sha1fallback != null) { + sha1.update(output, 0, toWrite); + sha1fallback.update(output, 0, toWrite); + } + } while (inBlockBuffer == BLOCKSIZE); + + if (sha1 != null && sha1fallback != null) { + long missingInHash = expectedSizeForHash - written; + if (missingInHash > 0) { + sha1fallback.update(new byte[(int) missingInHash]); + } + + byte[] calculated_hash1 = sha1.digest(); + byte[] calculated_hash2 = sha1fallback.digest(); + byte[] expected_hash = h3hash; + if (!Arrays.equals(calculated_hash1, expected_hash) && !Arrays.equals(calculated_hash2, expected_hash)) { + outputStream.close(); + inputStream.close(); + throw new CheckSumWrongException("hash checksum failed", calculated_hash1, expected_hash); + } 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, CheckSumWrongException { + 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; + try { + output = decryptFileChunkHash(encryptedBlockBuffer, (int) block, contentIndex, h3Hash); + } catch (CheckSumWrongException e) { + outputStream.close(); + inputStream.close(); + throw e; + } + + 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)); + // System.out.println("Decryption okay"); + outputStream.close(); + inputStream.close(); + } + + private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, int contentIndex, byte[] h3_hashes) throws CheckSumWrongException { + 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/wiiu/jnus/utils/download/Downloader.java similarity index 69% rename from src/de/mas/jnus/lib/utils/download/Downloader.java rename to src/de/mas/wiiu/jnus/utils/download/Downloader.java index 3013105..5901a25 100644 --- a/src/de/mas/jnus/lib/utils/download/Downloader.java +++ b/src/de/mas/wiiu/jnus/utils/download/Downloader.java @@ -1,36 +1,43 @@ -package de.mas.jnus.lib.utils.download; +package de.mas.wiiu.jnus.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; +import lombok.extern.java.Log; + +@Log +public abstract class Downloader { + public 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; + System.arraycopy(buffer, 0, file, filePostion, bytesRead); + filePostion += bytesRead; } inputStream.close(); - }else{ - System.out.println("File not found: " + fileURL); + } else { + log.info("File not found: " + fileURL); } httpConn.disconnect(); return file; diff --git a/src/de/mas/wiiu/jnus/utils/download/NUSDownloadService.java b/src/de/mas/wiiu/jnus/utils/download/NUSDownloadService.java new file mode 100644 index 0000000..3f3e11e --- /dev/null +++ b/src/de/mas/wiiu/jnus/utils/download/NUSDownloadService.java @@ -0,0 +1,69 @@ +package de.mas.wiiu.jnus.utils.download; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import de.mas.wiiu.jnus.Settings; + +public final class NUSDownloadService extends Downloader { + private static NUSDownloadService defaultInstance = new NUSDownloadService(Settings.URL_BASE); + private static Map instances = new HashMap<>(); + + private final String URL_BASE; + + private NUSDownloadService(String URL) { + this.URL_BASE = URL; + } + + public static NUSDownloadService getDefaultInstance() { + return defaultInstance; + } + + public static NUSDownloadService getInstance(String URL) { + if (!instances.containsKey(URL)) { + NUSDownloadService instance = new NUSDownloadService(URL); + instances.put(URL, instance); + } + return instances.get(URL); + } + + 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); + } +}