first commit, have fun

This commit is contained in:
Maschell 2016-12-12 21:01:12 +01:00
parent 26d41a1611
commit a75fe2e6bd
56 changed files with 4527 additions and 0 deletions

7
.classpath Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="lib" path="libs/lombok.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="output" path="bin"/>
</classpath>

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/bin/

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>JNUSLib</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

BIN
libs/lombok.jar Normal file

Binary file not shown.

View File

@ -0,0 +1,336 @@
package de.mas.jnus.lib;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.mas.jnus.lib.entities.TMD;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.fst.FSTEntry;
import de.mas.jnus.lib.implementations.NUSDataProvider;
import de.mas.jnus.lib.utils.HashUtil;
import de.mas.jnus.lib.utils.StreamUtils;
import de.mas.jnus.lib.utils.Utils;
import de.mas.jnus.lib.utils.cryptography.NUSDecryption;
import lombok.Getter;
import lombok.Setter;
public class DecryptionService {
private static Map<NUSTitle,DecryptionService> 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<FSTEntry> files = getNUSTitle().getAllFSTEntriesFlat();
Pattern p = Pattern.compile(regEx);
List<FSTEntry> 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<FSTEntry> list,String outputFolder) throws IOException {
decryptFSTEntryListTo(true,list,outputFolder);
}
public void decryptFSTEntryListTo(boolean fullPath,List<FSTEntry> 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<Content>(Arrays.asList(c)), outputFolder);
}
public void decryptPlainContents(List<Content> list,String outputFolder) throws IOException {
for(Content c : list){
decryptContentTo(c,outputFolder,getNUSTitle().isSkipExistingFiles());
}
}
public void decryptAllPlainContents(String outputFolder) throws IOException {
decryptPlainContents(new ArrayList<Content>(getTMDFromNUSTitle().getAllContents().values()),outputFolder);
}
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//Other
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
private TMD getTMDFromNUSTitle(){
return getNUSTitle().getTMD();
}
}

View File

@ -0,0 +1,125 @@
package de.mas.jnus.lib;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.implementations.NUSDataProvider;
import de.mas.jnus.lib.utils.FileUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.Getter;
import lombok.Setter;
public class ExtractionService {
private static Map<NUSTitle,ExtractionService> instances = new HashMap<>();
public static ExtractionService getInstance(NUSTitle nustitle) {
if(!instances.containsKey(nustitle)){
instances.put(nustitle, new ExtractionService(nustitle));
}
return instances.get(nustitle);
}
@Getter @Setter private NUSTitle NUSTitle = null;
private ExtractionService(NUSTitle nustitle){
setNUSTitle(nustitle);
}
private NUSDataProvider getDataProvider(){
return getNUSTitle().getDataProvider();
}
public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException {
extractEncryptedContentHashesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()),outputFolder);
}
public void extractEncryptedContentHashesTo(List<Content> list, String outputFolder) throws IOException {
Utils.createDir(outputFolder);
NUSDataProvider dataProvider = getDataProvider();
for(Content c : list){
dataProvider.saveContentH3Hash(c, outputFolder);
}
}
public void extractAllEncryptedContentFiles(String outputFolder) throws IOException {
extractAllEncryptedContentFilesWithHashesTo(outputFolder);
}
public void extractAllEncryptedContentFilesWithoutHashesTo(String outputFolder) throws IOException {
extractEncryptedContentFilesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()),outputFolder,false);
}
public void extractAllEncryptedContentFilesWithHashesTo(String outputFolder) throws IOException {
extractEncryptedContentFilesTo(new ArrayList<Content>(getNUSTitle().getTMD().getAllContents().values()),outputFolder,true);
}
public void extractEncryptedContentFilesTo(List<Content> list,String outputFolder,boolean withHashes) throws IOException {
Utils.createDir(outputFolder);
NUSDataProvider dataProvider = getDataProvider();
for(Content c : list){
System.out.println("Saving " + c.getFilename());
if(withHashes){
dataProvider.saveEncryptedContentWithH3Hash(c, outputFolder);
}else{
dataProvider.saveEncryptedContent(c, outputFolder);
}
}
}
public void extractTMDTo(String output) throws IOException {
Utils.createDir(output);
byte[] rawTMD= getDataProvider().getRawTMD();
if(rawTMD != null && rawTMD.length == 0){
System.out.println("Couldn't write TMD: No TMD loaded");
return;
}
String tmd_path = output + File.separator + Settings.TMD_FILENAME;
System.out.println("Extracting TMD to: " + tmd_path);
FileUtils.saveByteArrayToFile(tmd_path,rawTMD);
}
public void extractTicketTo(String output) throws IOException {
Utils.createDir(output);
byte[] rawTicket= getDataProvider().getRawTicket();
if(rawTicket != null && rawTicket.length == 0){
System.out.println("Couldn't write Ticket: No Ticket loaded");
return;
}
String ticket_path = output + File.separator + Settings.TICKET_FILENAME;
System.out.println("Extracting Ticket to: " + ticket_path);
FileUtils.saveByteArrayToFile(ticket_path,rawTicket);
}
public void extractCertTo(String output) throws IOException {
Utils.createDir(output);
byte[] rawCert = getDataProvider().getRawCert();
if(rawCert != null && rawCert.length == 0){
System.out.println("Couldn't write Cert: No Cert loaded");
return;
}
String cert_path = output + File.separator + Settings.CERT_FILENAME;
System.out.println("Extracting Cert to: " + cert_path);
FileUtils.saveByteArrayToFile(cert_path,rawCert);
}
public void extractAll(String outputFolder) throws IOException {
Utils.createDir(outputFolder);
extractAllEncryptedContentFilesWithHashesTo(outputFolder);
extractCertTo(outputFolder);
extractTMDTo(outputFolder);
extractTicketTo(outputFolder);
}
}

View File

@ -0,0 +1,103 @@
package de.mas.jnus.lib;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import de.mas.jnus.lib.entities.TMD;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
import de.mas.jnus.lib.entities.fst.FST;
import de.mas.jnus.lib.entities.fst.FSTEntry;
import de.mas.jnus.lib.implementations.NUSDataProvider;
import lombok.Getter;
import lombok.Setter;
public class NUSTitle {
@Getter @Setter private String inputPath = "";
@Getter @Setter private FST FST;
@Getter @Setter private TMD TMD;
@Getter @Setter private Ticket ticket;
@Getter @Setter private boolean skipExistingFiles = true;
@Getter @Setter private NUSDataProvider dataProvider = null;
public List<FSTEntry> getAllFSTEntriesFlatByContentID(short ID){
return getFSTEntriesFlatByContent(getTMD().getContentByID((int) ID));
}
public List<FSTEntry> getFSTEntriesFlatByContentIndex(int index){
return getFSTEntriesFlatByContent(getTMD().getContentByIndex(index));
}
public List<FSTEntry> getFSTEntriesFlatByContent(Content content){
return getFSTEntriesFlatByContents(new ArrayList<Content>(Arrays.asList(content)));
}
public List<FSTEntry> getFSTEntriesFlatByContents(List<Content> list){
List<FSTEntry> entries = new ArrayList<>();
for(Content c : list){
for(FSTEntry f : c.getEntries()){
entries.add(f);
}
}
return entries;
}
public List<FSTEntry> getAllFSTEntriesFlat(){
return getFSTEntriesFlatByContents(new ArrayList<Content>(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<Integer,ContentFSTInfo> e : getFST().getContentFSTInfos().entrySet()){
System.out.println(String.format("%08X", e.getKey()) + ": " +e.getValue());
}
}
public void printContentInfos(){
for(Entry<Integer, Content> 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();
}
}

View File

@ -0,0 +1,18 @@
package de.mas.jnus.lib;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
import lombok.Data;
@Data
public class NUSTitleConfig {
private String inputPath = "";
private WUDInfo WUDInfo = null;
private Ticket ticket = null;
private int version = Settings.LATEST_TMD_VERSION;
private long titleID = 0x0L;
private WoomyInfo woomyInfo;
}

View File

@ -0,0 +1,67 @@
package de.mas.jnus.lib;
import java.io.InputStream;
import java.util.Map;
import de.mas.jnus.lib.entities.TMD;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.fst.FST;
import de.mas.jnus.lib.implementations.NUSDataProvider;
import de.mas.jnus.lib.utils.StreamUtils;
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
abstract class NUSTitleLoader {
protected NUSTitleLoader(){
}
public NUSTitle loadNusTitle(NUSTitleConfig config) throws Exception{
NUSTitle result = new NUSTitle();
NUSDataProvider dataProvider = getDataProvider(config);
result.setDataProvider(dataProvider);
dataProvider.setNUSTitle(result);
TMD tmd = TMD.parseTMD(dataProvider.getRawTMD());
result.setTMD(tmd);
if(tmd == null){
System.out.println("TMD not found.");
throw new Exception();
}
Ticket ticket = config.getTicket();
if(ticket == null){
ticket = Ticket.parseTicket(dataProvider.getRawTicket());
}
result.setTicket(ticket);
System.out.println(ticket);
Content fstContent = tmd.getContentByIndex(0);
InputStream fstContentEncryptedStream = dataProvider.getInputStreamFromContent(fstContent, 0);
if(fstContentEncryptedStream == null){
return null;
}
byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize());
if(fstContent.isEncrypted()){
AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]);
fstBytes = aesDecryption.decrypt(fstBytes);
}
Map<Integer,Content> contents = tmd.getAllContents();
FST fst = FST.parseFST(fstBytes, contents);
result.setFST(fst);
return result;
}
protected abstract NUSDataProvider getDataProvider(NUSTitleConfig config);
}

View File

@ -0,0 +1,35 @@
package de.mas.jnus.lib;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.implementations.NUSDataProviderLocal;
import de.mas.jnus.lib.implementations.NUSDataProvider;
public class NUSTitleLoaderLocal extends NUSTitleLoader {
private NUSTitleLoaderLocal(){
super();
}
public static NUSTitle loadNUSTitle(String inputPath) throws Exception{
return loadNUSTitle(inputPath, null);
}
public static NUSTitle loadNUSTitle(String inputPath, Ticket ticket) throws Exception{
NUSTitleLoader loader = new NUSTitleLoaderLocal();
NUSTitleConfig config = new NUSTitleConfig();
if(ticket != null){
config.setTicket(ticket);
}
config.setInputPath(inputPath);
return loader.loadNusTitle(config);
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderLocal result = new NUSDataProviderLocal();
result.setLocalPath(config.getInputPath());
return result;
}
}

View File

@ -0,0 +1,43 @@
package de.mas.jnus.lib;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.implementations.NUSDataProviderRemote;
import de.mas.jnus.lib.implementations.NUSDataProvider;
public class NUSTitleLoaderRemote extends NUSTitleLoader{
private NUSTitleLoaderRemote(){
super();
}
public static NUSTitle loadNUSTitle(long titleID) throws Exception{
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION ,null);
}
public static NUSTitle loadNUSTitle(long titleID, int version) throws Exception{
return loadNUSTitle(titleID, version,null);
}
public static NUSTitle loadNUSTitle(long titleID, Ticket ticket) throws Exception{
return loadNUSTitle(titleID, Settings.LATEST_TMD_VERSION, ticket);
}
public static NUSTitle loadNUSTitle(long titleID,int version, Ticket ticket) throws Exception{
NUSTitleLoader loader = new NUSTitleLoaderRemote();
NUSTitleConfig config = new NUSTitleConfig();
config.setVersion(version);
config.setTitleID(titleID);
return loader.loadNusTitle(config);
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderRemote result = new NUSDataProviderRemote();
result.setVersion(config.getVersion());
result.setTitleID(config.getTitleID());
return result;
}
}

View File

@ -0,0 +1,49 @@
package de.mas.jnus.lib;
import java.io.File;
import java.nio.file.Files;
import de.mas.jnus.lib.implementations.NUSDataProviderWUD;
import de.mas.jnus.lib.implementations.NUSDataProvider;
import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
import de.mas.jnus.lib.implementations.wud.parser.WUDInfoParser;
public class NUSTitleLoaderWUD extends NUSTitleLoader {
private NUSTitleLoaderWUD(){
super();
}
public static NUSTitle loadNUSTitle(String WUDPath) throws Exception{
return loadNUSTitle(WUDPath, null);
}
public static NUSTitle loadNUSTitle(String WUDPath, byte[] titleKey) throws Exception{
NUSTitleLoader loader = new NUSTitleLoaderWUD();
NUSTitleConfig config = new NUSTitleConfig();
File wudFile = new File(WUDPath);
if(!wudFile.exists()){
System.out.println(WUDPath + " does not exist.");
System.exit(1);
}
WUDImage image = new WUDImage(wudFile);
if(titleKey == null){
File keyFile = new File(wudFile.getParentFile().getPath() + File.separator + Settings.WUD_KEY_FILENAME);
titleKey = Files.readAllBytes(keyFile.toPath());
}
WUDInfo wudInfo = WUDInfoParser.createAndLoad(image.getWUDDiscReader(), titleKey);
config.setWUDInfo(wudInfo);
return loader.loadNusTitle(config);
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderWUD result = new NUSDataProviderWUD();
result.setWUDInfo(config.getWUDInfo());
return result;
}
}

View File

@ -0,0 +1,33 @@
package de.mas.jnus.lib;
import java.io.File;
import de.mas.jnus.lib.implementations.NUSDataProviderWoomy;
import de.mas.jnus.lib.implementations.NUSDataProvider;
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
import de.mas.jnus.lib.implementations.woomy.WoomyParser;
import lombok.extern.java.Log;
@Log
public class NUSTitleLoaderWoomy extends NUSTitleLoader {
public static NUSTitle loadNUSTitle(String inputFile) throws Exception{
NUSTitleLoaderWoomy loader = new NUSTitleLoaderWoomy();
NUSTitleConfig config = new NUSTitleConfig();
WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile));
if(woomyInfo == null){
log.info("Created woomy is null.");
return null;
}
config.setWoomyInfo(woomyInfo);
return loader.loadNusTitle(config);
}
@Override
protected NUSDataProvider getDataProvider(NUSTitleConfig config) {
NUSDataProviderWoomy dataProvider = new NUSDataProviderWoomy();
dataProvider.setWoomyInfo(config.getWoomyInfo());
return dataProvider;
}
}

View File

@ -0,0 +1,18 @@
package de.mas.jnus.lib;
public class Settings {
public static String URL_BASE = "http://ccs.cdn.wup.shop.nintendo.net/ccs/download";
public static final int LATEST_TMD_VERSION = 0;
public static final String TMD_FILENAME = "title.tmd";
public static final String TICKET_FILENAME = "title.tik";
public static final String CERT_FILENAME = "title.cert";
public static final String ENCRYPTED_CONTENT_EXTENTION = ".app";
public static final String DECRYPTED_CONTENT_EXTENTION = ".dec";
public static final String WUD_KEY_FILENAME = "game.key";
public static final String WOOMY_METADATA_FILENAME = "metadata.xml";
public static final String H3_EXTENTION = ".h3";
public static byte[] commonKey = new byte[0x10];
public static int WIIU_DECRYPTED_AREA_OFFSET = 0x18000;
}

View File

@ -0,0 +1,176 @@
package de.mas.jnus.lib;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import com.sun.org.apache.xerces.internal.util.SynchronizedSymbolTable;
import java.util.TreeMap;
import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.implementations.wud.WUDImageCompressedInfo;
import de.mas.jnus.lib.utils.ByteArrayBuffer;
import de.mas.jnus.lib.utils.ByteArrayWrapper;
import de.mas.jnus.lib.utils.HashUtil;
import de.mas.jnus.lib.utils.StreamUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.extern.java.Log;
@Log
public class WUDService {
public static boolean compressWUDToWUX(WUDImage image,String outputFolder) throws IOException{
return compressWUDToWUX(image, outputFolder, "game.wux");
}
public static boolean compressWUDToWUX(WUDImage image,String outputFolder,String filename) throws IOException{
if(image.isCompressed()){
log.info("Given Image is already compressed");
return false;
}
if(image.getWUDFileSize() != WUDImage.WUD_FILESIZE)
{
log.info("Given WUD has not the expected filesize");
return false;
}
Utils.createDir(outputFolder);
String filePath;
if(!outputFolder.isEmpty()){
filePath = outputFolder+ File.separator + filename;
}else{
filePath = filename;
}
File outputFile = new File(filePath);
if(outputFile.exists()){
log.info("Couldn't compress wud, target file already exists (" + outputFile.getAbsolutePath() + ")");
//return false;
}
RandomAccessFile fileOutput = new RandomAccessFile(outputFile, "rw");
WUDImageCompressedInfo info = WUDImageCompressedInfo.getDefaultCompressedInfo();
byte[] header = info.getHeaderAsBytes();
log.info("Writing header");
fileOutput.write(header);
int sectorTableEntryCount = (int) ((image.getWUDFileSize()+ WUDImageCompressedInfo.SECTOR_SIZE-1) / (long)WUDImageCompressedInfo.SECTOR_SIZE);
long sectorTableStart = fileOutput.getFilePointer();
long sectorTableEnd = Utils.align(sectorTableEntryCount *0x04,WUDImageCompressedInfo.SECTOR_SIZE);
byte[] sectorTablePlaceHolder = new byte[(int) (sectorTableEnd-sectorTableStart)];
fileOutput.write(sectorTablePlaceHolder);
Map<ByteArrayWrapper,Integer> sectorHashes = new HashMap<>();
Map<Integer,Integer> sectorMapping = new TreeMap<>();
InputStream in = image.getWUDDiscReader().readEncryptedToInputStream(0, image.getWUDFileSize());
int bufferSize = WUDImageCompressedInfo.SECTOR_SIZE;
byte[] blockBuffer = new byte[bufferSize];
ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
long written = 0;
int curSector = 0;
int realSector = 0;
log.info("Writing sectors");
Integer oldOffset = null;
do{
int read = StreamUtils.getChunkFromStream(in, blockBuffer, overflow, bufferSize);
ByteArrayWrapper hash = new ByteArrayWrapper(HashUtil.hashSHA1(blockBuffer));
if((oldOffset = sectorHashes.get(hash)) != null){
sectorMapping.put(curSector, oldOffset);
oldOffset = null;
}else{ //its a new sector
sectorMapping.put(curSector, realSector);
sectorHashes.put(hash, realSector);
fileOutput.write(blockBuffer);
realSector++;
}
written += read;
curSector++;
if(curSector % 1000 == 0){
double readMB = written / 1024.0 / 1024.0;
double writtenMB = ((long)realSector * (long)bufferSize) / 1024.0 / 1024.0;
double percent = ((double)written / image.getWUDFileSize())*100;
double ratio = 1 / (writtenMB / readMB);
System.out.println(String.format(Locale.ROOT,"Compressing (%05.2f%%) | Ratio: 1:%.2f | Read: %08.2fMB | Written: %08.2fMB",percent,ratio,readMB,writtenMB));
}
}while(written < image.getWUDFileSize());
log.info("Writing sector table");
fileOutput.seek(sectorTableStart);
ByteBuffer buffer = ByteBuffer.allocate(sectorTablePlaceHolder.length);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for(Entry<Integer,Integer> e: sectorMapping.entrySet()){
buffer.putInt(e.getValue());
}
fileOutput.write(buffer.array());
fileOutput.close();
return true;
}
public static boolean compareWUDImage(WUDImage firstImage,WUDImage secondImage) throws IOException{
if(firstImage.getWUDFileSize() != secondImage.getWUDFileSize()){
log.info("Filesize if different");
return false;
}
InputStream in1 = firstImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
InputStream in2 = secondImage.getWUDDiscReader().readEncryptedToInputStream(0, WUDImage.WUD_FILESIZE);
boolean result = true;
int bufferSize = 1024*1024+19;
long totalread = 0;
byte[] blockBuffer1 = new byte[bufferSize];
byte[] blockBuffer2 = new byte[bufferSize];
ByteArrayBuffer overflow1 = new ByteArrayBuffer(bufferSize);
ByteArrayBuffer overflow2 = new ByteArrayBuffer(bufferSize);
long curSector = 0;
do{
int read1 = StreamUtils.getChunkFromStream(in1, blockBuffer1, overflow1, bufferSize);
int read2 = StreamUtils.getChunkFromStream(in2, blockBuffer2, overflow2, bufferSize);
if(read1 != read2){
log.info("Verification error");
result = false;
break;
}
if(!Arrays.equals(blockBuffer1,blockBuffer2)){
log.info("Verification error");
result = false;
break;
}
totalread += read1;
curSector++;
if(curSector % 100 == 0){
System.out.println(String.format("Checked: %016X bytes (%.2f%%)", (long)curSector*(long)bufferSize,(((long)curSector*(long)bufferSize*1.0)/(WUDImage.WUD_FILESIZE))*100));
}
}while(totalread < WUDImage.WUD_FILESIZE);
in1.close();
in2.close();
log.info("Verfication done!");
return result;
}
}

View File

@ -0,0 +1,167 @@
package de.mas.jnus.lib.entities;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.content.ContentInfo;
import lombok.Getter;
import lombok.Setter;
public class TMD {
@Getter @Setter private int signatureType; // 0x000
@Getter @Setter private byte[] signature = new byte[0x100]; // 0x004
@Getter @Setter private byte[] issuer = new byte[0x40]; // 0x140
@Getter @Setter private byte version; // 0x180
@Getter @Setter private byte CACRLVersion; // 0x181
@Getter @Setter private byte signerCRLVersion; // 0x182
@Getter @Setter private long systemVersion; // 0x184
@Getter @Setter private long titleID; // 0x18C
@Getter @Setter private int titleType; // 0x194
@Getter @Setter private short groupID; // 0x198
@Getter @Setter private byte[] reserved = new byte[62]; // 0x19A
@Getter @Setter private int accessRights; // 0x1D8
@Getter @Setter private short titleVersion; // 0x1DC
@Getter @Setter private short contentCount; // 0x1DE
@Getter @Setter private short bootIndex; // 0x1E0
@Getter @Setter private byte[] SHA2 = new byte[0x20]; // 0x1E4
@Getter @Setter private ContentInfo[] contentInfos = new ContentInfo[0x40];
Map<Integer,Content> contentToIndex = new HashMap<>();
Map<Integer,Content> 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<contentCount;i++){
buffer.position(0xB04+(0x30*i));
byte[] content = new byte[0x30];
buffer.get(content, 0, 0x30);
Content c = Content.parseContent(content);
result.setContentToIndex(c.getIndex(),c);
result.setContentToID(c.getID(), c);
}
result.setSignatureType(signatureType);
result.setSignature(signature);
result.setVersion(version);
result.setCACRLVersion(CACRLVersion);
result.setSignerCRLVersion(signerCRLVersion);
result.setSystemVersion(systemVersion);
result.setTitleID(titleID);
result.setTitleType(titleType);
result.setGroupID(groupID);
result.setAccessRights(accessRights);
result.setTitleVersion(titleVersion);
result.setContentCount(contentCount);
result.setBootIndex(bootIndex);
result.setSHA2(SHA2);
result.setContentInfos(contentInfos);
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<Integer, Content> getAllContents() {
return contentToIndex;
}
public void printContents() {
for(Content c: contentToIndex.values()){
System.out.println(c);
}
}
}

View File

@ -0,0 +1,122 @@
package de.mas.jnus.lib.entities;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Arrays;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.utils.Utils;
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class Ticket {
@Getter @Setter private byte[] encryptedKey = new byte[0x10];
@Getter @Setter private byte[] decryptedKey = new byte[0x10];
@Getter @Setter private byte[] IV = new byte[0x10];
@Getter @Setter private byte[] cert0 = new byte[0x300];
@Getter @Setter private byte[] cert1 = new byte[0x400];
@Getter @Setter private byte[] rawTicket = new byte[0];
private Ticket(){
}
public static Ticket parseTicket(File ticket) throws IOException {
if(ticket == null || !ticket.exists()){
log.warning("Ticket input file null or doesn't exist.");
return null;
}
return parseTicket(Files.readAllBytes(ticket.toPath()));
}
public static Ticket parseTicket(byte[] ticket) throws IOException {
if(ticket == null){
return null;
}
ByteBuffer buffer = ByteBuffer.allocate(ticket.length);
buffer.put(ticket);
//read key
byte[] encryptedKey = new byte[0x10];
buffer.position(0x1BF);
buffer.get(encryptedKey,0x00,0x10);
//read titleID
buffer.position(0x1DC);
long titleID = buffer.getLong();
Ticket result = createTicket(encryptedKey,titleID);
result.setRawTicket(Arrays.copyOf(ticket, ticket.length));
//read certs.
byte[] cert0 = new byte[0x300];
byte[] cert1 = new byte[0x400];
if(ticket.length >= 0x650){
buffer.position(0x350);
buffer.get(cert0,0x00,0x300);
}
if(ticket.length >= 0xA50){
buffer.position(0x650);
buffer.get(cert1,0x00,0x400);
}
result.setCert0(cert0);
result.setCert1(cert1);
return result;
}
public static Ticket createTicket(byte[] encryptedKey, long titleID) {
Ticket result = new Ticket();
result.encryptedKey = encryptedKey;
byte[] IV = ByteBuffer.allocate(0x10).putLong(titleID).array();
result.decryptedKey = calculateDecryptedKey(result.encryptedKey,IV);
result.setIV(IV);
return result;
}
private static byte[] calculateDecryptedKey(byte[] encryptedKey, byte[] IV) {
AESDecryption decryption = new AESDecryption(Settings.commonKey, IV){};
return decryption.decrypt(encryptedKey);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(encryptedKey);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Ticket other = (Ticket) obj;
if (!Arrays.equals(encryptedKey, other.encryptedKey))
return false;
return true;
}
@Override
public String toString() {
return "Ticket [encryptedKey=" + Utils.ByteArrayToString(encryptedKey) + ", decryptedKey="
+ Utils.ByteArrayToString(decryptedKey) + "]";
}
}

View File

@ -0,0 +1,149 @@
package de.mas.jnus.lib.entities.content;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.fst.FSTEntry;
import de.mas.jnus.lib.utils.Utils;
import lombok.Getter;
import lombok.Setter;
/**
* Represents a Content
* @author Maschell
*
*/
public class Content{
public static final short CONTENT_HASHED = 0x0002;
public static final short CONTENT_ENCRYPTED = 0x0001;
@Getter @Setter private int ID = 0x00;
@Getter @Setter private short index = 0x00;
@Getter @Setter private short type = 0x0000;
@Getter @Setter private long encryptedFileSize = 0;
@Getter @Setter private byte[] SHA2Hash = new byte[0x14];
@Getter private List<FSTEntry> 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) + "]";
}
}

View File

@ -0,0 +1,85 @@
package de.mas.jnus.lib.entities.content;
import java.nio.ByteBuffer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@EqualsAndHashCode
/**
* Representation on an Object of the first section
* of an FST.
* @author Maschell
*
*/
public class ContentFSTInfo {
@Getter @Setter private long offsetSector;
@Getter @Setter private long sizeSector;
@Getter @Setter private long ownerTitleID;
@Getter @Setter private int groupID;
@Getter @Setter private byte unkown;
private static int SECTOR_SIZE = 0x8000;
private ContentFSTInfo(){
}
/**
* Creates a new ContentFSTInfo object given be the raw byte data
* @param input 0x20 byte of data from the FST (starting at 0x20)
* @return ContentFSTInfo object
*/
public static ContentFSTInfo parseContentFST(byte[] input) {
if(input == null ||input.length != 0x20){
System.out.println("Error: invalid ContentFSTInfo byte[] input");
return null;
}
ContentFSTInfo cFSTInfo = new ContentFSTInfo();
ByteBuffer buffer = ByteBuffer.allocate(input.length);
buffer.put(input);
buffer.position(0);
int offset = buffer.getInt();
int size = buffer.getInt();
long ownerTitleID = buffer.getLong();
int groupID = buffer.getInt();
byte unkown = buffer.get();
cFSTInfo.setOffsetSector(offset);
cFSTInfo.setSizeSector(size);
cFSTInfo.setOwnerTitleID(ownerTitleID);
cFSTInfo.setGroupID(groupID);
cFSTInfo.setUnkown(unkown);
return cFSTInfo;
}
/**
* Returns the offset of of the Content in the partition
* @return offset of the content in the partition in bytes
*/
public long getOffset() {
long result = (getOffsetSector() * SECTOR_SIZE) - SECTOR_SIZE;
if(result < 0){
return 0;
}
return result;
}
/**
* Returns the size in bytes, not in sectors
* @return size in bytes
*/
public int getSize() {
return (int) (getSizeSector() * SECTOR_SIZE);
}
@Override
public String toString() {
return "ContentFSTInfo [offset=" + String.format("%08X", offsetSector) + ", size=" + String.format("%08X", sizeSector) + ", ownerTitleID=" + String.format("%016X", ownerTitleID) + ", groupID="
+ String.format("%08X", groupID) + ", unkown=" + unkown + "]";
}
}

View File

@ -0,0 +1,65 @@
package de.mas.jnus.lib.entities.content;
import java.nio.ByteBuffer;
import java.util.Arrays;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@EqualsAndHashCode
/**
* Represents a Object from the TMD before the actual Content Section.
* @author Maschell
*
*/
public class ContentInfo{
@Getter @Setter private short indexOffset = 0x00;
@Getter @Setter private short commandCount = 0x00;
@Getter @Setter private byte[] SHA2Hash = new byte[0x20];
public ContentInfo() {
this((short) 0);
}
public ContentInfo(short contentCount) {
this((short) 0,contentCount);
}
public ContentInfo(short indexOffset,short commandCount) {
this(indexOffset,commandCount,null);
}
public ContentInfo(short indexOffset,short commandCount,byte[] SHA2Hash) {
setIndexOffset(indexOffset);
setCommandCount(commandCount);
setSHA2Hash(SHA2Hash);
}
/**
* Creates a new ContentInfo object given be the raw byte data
* @param input 0x24 byte of data from the TMD (starting at 0x208)
* @return ContentFSTInfo object
*/
public static ContentInfo parseContentInfo(byte[] input){
if(input == null || input.length != 0x24){
System.out.println("Error: invalid ContentInfo byte[] input");
return null;
}
ByteBuffer buffer = ByteBuffer.allocate(input.length);
buffer.put(input);
buffer.position(0);
short indexOffset = buffer.getShort(0x00);
short commandCount = buffer.getShort(0x02);
byte[] sha2hash = new byte[0x20];
buffer.position(0x04);
buffer.get(sha2hash, 0x00, 0x20);
return new ContentInfo(indexOffset, commandCount, sha2hash);
}
@Override
public String toString() {
return "ContentInfo [indexOffset=" + indexOffset + ", commandCount=" + commandCount + ", SHA2Hash=" + Arrays.toString(SHA2Hash) + "]";
}
}

View File

@ -0,0 +1,81 @@
package de.mas.jnus.lib.entities.fst;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
import de.mas.jnus.lib.utils.ByteUtils;
import lombok.Getter;
import lombok.Setter;
/**
* Represents the FST
* @author Maschell
*
*/
public class FST {
@Getter @Setter private FSTEntry root = FSTEntry.getRootFSTEntry();
@Getter @Setter private int unknown;
@Getter @Setter private int contentCount = 0;
@Getter @Setter private Map<Integer,ContentFSTInfo> 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<Integer,Content> 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<fstData.length-1;i++){
if(fstData[i] == 0 && fstData[i+1] == 0){
nameSize = i - nameOff-1;
}
}
Map<Integer,ContentFSTInfo> 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);
result.setContentCount(contentCount);
result.setUnknown(unknownValue);
result.setContentFSTInfos(contentFSTInfos);
return result;
}
}

View File

@ -0,0 +1,165 @@
package de.mas.jnus.lib.entities.fst;
import java.util.ArrayList;
import java.util.List;
import de.mas.jnus.lib.entities.content.Content;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
/**
* Represents one FST Entry
* @author Maschell
*
*/
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;
private List<FSTEntry> children = null;
@Getter @Setter private short flags;
@Getter @Setter private long fileSize = 0;
@Getter @Setter private long fileOffset = 0;
@Getter private Content content = null;
@Getter @Setter private byte[] hash = new byte[0x14];
@Getter @Setter private boolean isDir = false;
@Getter @Setter private boolean isRoot = false;
@Getter @Setter private boolean notInPackage = false;
@Getter @Setter private short contentFSTID = 0;
public FSTEntry(){
}
/**
* Creates and returns a new FST Entry
* @return
*/
public static FSTEntry getRootFSTEntry(){
FSTEntry entry = new FSTEntry();
entry.setRoot(true);
return entry;
}
public String getFullPath() {
return getPath() + getFilename();
}
public int getEntryCount() {
int count = 1;
for(FSTEntry entry : getChildren()){
count += entry.getEntryCount();
}
return count;
}
public void addChildren(FSTEntry fstEntry) {
getChildren().add(fstEntry);
fstEntry.setParent(this);
}
public List<FSTEntry> getChildren() {
if(children == null){
children = new ArrayList<>();
}
return children;
}
public List<FSTEntry> getDirChildren(){
return getDirChildren(false);
}
public List<FSTEntry> getDirChildren(boolean all){
List<FSTEntry> result = new ArrayList<>();
for(FSTEntry child : getChildren()){
if(child.isDir() && (all || !child.isNotInPackage())){
result.add(child);
}
}
return result;
}
public List<FSTEntry> getFileChildren(){
return getFileChildren(false);
}
public List<FSTEntry> getFileChildren(boolean all){
List<FSTEntry> result = new ArrayList<>();
for(FSTEntry child : getChildren()){
if((all && !child.isDir() || !child.isDir())){
result.add(child);
}
}
return result;
}
public List<FSTEntry> getFSTEntriesByContent(Content content) {
List<FSTEntry> entries = new ArrayList<>();
if(this.content == null){
log.warning("Error in getFSTEntriesByContent, content null");
System.exit(0);
}else{
if(this.content.equals(content)){
entries.add(this);
}
}
for(FSTEntry child : getChildren()){
entries.addAll(child.getFSTEntriesByContent(content));
}
return entries;
}
public void setContent(Content content) {
if(content == null){
log.warning("Can't set content for "+ getFilename() + ": Content it null");
System.out.println();
return;
}
this.content = content;
content.addEntry(this);
}
public long getFileOffsetBlock() {
if(getContent().isHashed()){
return (getFileOffset()/0xFC00) * 0x10000;
}else{
return getFileOffset();
}
}
public void printRecursive(int space){
for(int i = 0;i<space;i++){
System.out.print(" ");
}
System.out.print(getFilename());
if(isNotInPackage()){
System.out.print(" (not in package)");
}
System.out.println();
for(FSTEntry child : getDirChildren(true)){
child.printRecursive(space + 5);
}
for(FSTEntry child : getFileChildren(true)){
child.printRecursive(space + 5);
}
}
@Override
public String toString() {
return "FSTEntry [filename=" + filename + ", path=" + path + ", flags=" + flags + ", filesize=" + fileSize
+ ", fileoffset=" + fileOffset + ", content=" + content + ", isDir=" + isDir + ", isRoot=" + isRoot
+ ", notInPackage=" + notInPackage + "]";
}
}

View File

@ -0,0 +1,137 @@
package de.mas.jnus.lib.entities.fst;
import java.io.File;
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.extern.java.Log;
@Log
public class FSTService {
protected static void parseFST(FSTEntry rootEntry, byte[] fstSection, byte[] namesSection,Map<Integer,Content> contentsByIndex,Map<Integer,ContentFSTInfo> contentsFSTByIndex) {
int totalEntries = ByteUtils.getIntFromBytes(fstSection, 0x08);
int level = 0;
int[] LEntry = new int[16];
int[] Entry = new int[16];
HashMap<Integer,FSTEntry> fstEntryToOffsetMap = new HashMap<>();
rootEntry.setDir(true);
Entry[level] = 0;
LEntry[level++] = 0;
fstEntryToOffsetMap.put(0,rootEntry);
for(int i = 1;i< totalEntries;i++){
int entryOffset = i;
if( level > 0){
while( LEntry[level-1] == i ){level--;}
}
byte[] curEntry = Arrays.copyOfRange(fstSection,i*0x10,(i+1)*0x10);
FSTEntry entry = new FSTEntry();
String path = getFullPath(level, fstSection, namesSection, Entry);
String filename = getName(curEntry,namesSection);
long fileOffset = ByteUtils.getIntFromBytes(curEntry, 0x04);
long fileSize = ByteUtils.getUnsingedIntFromBytes(curEntry, 0x08);
short flags = ByteUtils.getShortFromBytes(curEntry, 0x0C);
short contentIndex = ByteUtils.getShortFromBytes(curEntry, 0x0E);
if((curEntry[0] & FSTEntry.FSTEntry_notInNUS) == FSTEntry.FSTEntry_notInNUS){
entry.setNotInPackage(true);
}
if((curEntry[0] & FSTEntry.FSTEntry_DIR) == FSTEntry.FSTEntry_DIR){
entry.setDir(true);
int parentOffset = (int) fileOffset;
int nextOffset = (int) fileSize;
FSTEntry parent = fstEntryToOffsetMap.get(parentOffset);
if(parent != null){
log.info("no parent found for a FSTEntry");
parent.addChildren(entry);
}
Entry[level] = i;
LEntry[level++] = nextOffset ;
if( level > 15 ){
log.warning("level > 15");
break;
}
}else{
entry.setFileOffset(fileOffset<<5);
entry.setFileSize(fileSize);
FSTEntry parent = fstEntryToOffsetMap.get(Entry[level-1]);
if(parent != null){
parent.addChildren(entry);
}else{
log.warning(entryOffset +"couldn't find parent @ " + Entry[level-1]);
}
}
entry.setFlags(flags);
entry.setFilename(filename);
entry.setPath(path);
if(contentsByIndex != null){
Content content = contentsByIndex.get((int)contentIndex);
if(content == null){
log.warning("Content for FST Entry not found");
}else{
entry.setContent(content);
ContentFSTInfo contentFSTInfo = contentsFSTByIndex.get((int)contentIndex);
if(contentFSTInfo == null){
log.warning("ContentFSTInfo for FST Entry not found");
}else{
content.setContentFSTInfo(contentFSTInfo);
}
}
}
entry.setContentFSTID(contentIndex);
fstEntryToOffsetMap.put(entryOffset, entry);
}
}
private static int getNameOffset(byte[] curEntry) {
//Its a 24bit number. We overwrite the first byte, then we can read it as an Integer.
//But at first we make a copy.
byte[] entryData = Arrays.copyOf(curEntry, curEntry.length);
entryData[0] = 0;
return ByteUtils.getIntFromBytes(entryData, 0);
}
public static String getName(byte[] data, byte[] namesSection){
int nameOffset = getNameOffset(data);
int j = 0;
while(namesSection[nameOffset + j] != 0){j++;}
return(new String(Arrays.copyOfRange(namesSection,nameOffset, nameOffset + j)));
}
public static String getFullPath(int level,byte[] fstSection,byte[] namesSection, int[] Entry){
StringBuilder sb = new StringBuilder();
for(int i=0; i < level; i++){
int entryOffset = Entry[i]*0x10;
byte[] entryData = Arrays.copyOfRange(fstSection, entryOffset,entryOffset + 10);
String entryName = getName(entryData,namesSection);
sb.append(entryName).append(File.separator);
}
return sb.toString();
}
}

View File

@ -0,0 +1,115 @@
package de.mas.jnus.lib.implementations;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import com.sun.istack.internal.NotNull;
import de.mas.jnus.lib.NUSTitle;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.utils.FileUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
/**
* Service Methods for loading NUS/Content data from
* different sources
* @author Maschell
*
*/
public abstract class NUSDataProvider {
@Getter @Setter private NUSTitle NUSTitle = null;
public NUSDataProvider () {
}
/**
* Saves the given content encrypted with his .h3 file in the given directory.
* The Target directory will be created if it's missing.
* If the content is not hashed, no .h3 will be saved
* @param content Content that should be saved
* @param outputFolder Target directory where the files will be stored in.
* @throws IOException
*/
public void saveEncryptedContentWithH3Hash(@NotNull Content content,@NotNull String outputFolder) throws IOException {
saveContentH3Hash(content, outputFolder);
saveEncryptedContent(content, outputFolder);
}
/**
* Saves the .h3 file of the given content into the given directory.
* The Target directory will be created if it's missing.
* If the content is not hashed, no .h3 will be saved
* @param content The content of which the h3 hashes should be saved
* @param outputFolder
* @throws IOException
*/
public void saveContentH3Hash(@NotNull Content content,@NotNull String outputFolder) throws IOException {
if(!content.isHashed()){
return;
}
byte[] hash = getContentH3Hash(content);
if(hash == null){
return;
}
String h3Filename = String.format("%08X%s", content.getID(),Settings.H3_EXTENTION);
File output = new File(outputFolder + File.separator + h3Filename);
if(output.exists() && output.length() == hash.length){
return;
}
log.info("Saving " + h3Filename +" ");
FileUtils.saveByteArrayToFile(output, hash);
}
/**
* Saves the given content encrypted in the given directory.
* The Target directory will be created if it's missing.
* If the content is not encrypted at all, it will be just saved anyway.
* @param content Content that should be saved
* @param outputFolder Target directory where the files will be stored in.
* @throws IOException
*/
public void saveEncryptedContent(@NotNull Content content,@NotNull String outputFolder) throws IOException {
Utils.createDir(outputFolder);
InputStream inputStream = getInputStreamFromContent(content, 0);
if(inputStream == null){
log.info("Couldn't save encrypted content. Input stream was null");
return;
}
File output = new File(outputFolder + File.separator + content.getFilename());
if(output.exists()){
if(output.length() == content.getEncryptedFileSize()){
log.info("Encrypted content alreadys exists, skipped");
return;
}else{
log.info("Encrypted content alreadys exists, but the length is not as expected. Saving it again");
}
}
FileUtils.saveInputStreamToFile(output,inputStream,content.getEncryptedFileSize());
}
/**
*
* @param content
* @param offset
* @return
* @throws IOException
*/
public abstract InputStream getInputStreamFromContent(Content content,long offset) throws IOException ;
public abstract byte[] getContentH3Hash(Content content) throws IOException;
public abstract byte[] getRawTMD() throws IOException;
public abstract byte[] getRawTicket() throws IOException;
public abstract byte[] getRawCert() throws IOException;
public abstract void cleanup() throws IOException;
}

View File

@ -0,0 +1,73 @@
package de.mas.jnus.lib.implementations;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.Content;
import lombok.Getter;
import lombok.Setter;
public class NUSDataProviderLocal extends NUSDataProvider {
@Getter @Setter private String localPath = "";
public NUSDataProviderLocal() {
}
public String getFilePathOnDisk(Content c) {
return getLocalPath() + File.separator + c.getFilename();
}
@Override
public InputStream getInputStreamFromContent(Content content, long offset) throws IOException {
File filepath = new File(getFilePathOnDisk(content));
if(!filepath.exists()){
return null;
}
InputStream in = new FileInputStream(filepath);
in.skip(offset);
return in;
}
@Override
public byte[] getContentH3Hash(Content content) throws IOException {
String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID());
File h3File = new File(h3Path);
if(!h3File.exists()){
return null;
}
return Files.readAllBytes(h3File.toPath());
}
@Override
public byte[] getRawTMD() throws IOException {
String inputPath = getLocalPath();
String tmdPath = inputPath + File.separator + Settings.TMD_FILENAME;
File tmdFile = new File(tmdPath);
return Files.readAllBytes(tmdFile.toPath());
}
@Override
public byte[] getRawTicket() throws IOException {
String inputPath = getLocalPath();
String ticketPath = inputPath + File.separator + Settings.TICKET_FILENAME;
File ticketFile = new File(ticketPath);
return Files.readAllBytes(ticketFile.toPath());
}
@Override
public void cleanup() throws IOException {
}
@Override
public byte[] getRawCert() throws IOException {
String inputPath = getLocalPath();
String certPath = inputPath + File.separator + Settings.CERT_FILENAME;
File certFile = new File(certPath);
return Files.readAllBytes(certFile.toPath());
}
}

View File

@ -0,0 +1,62 @@
package de.mas.jnus.lib.implementations;
import java.io.IOException;
import java.io.InputStream;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.utils.download.NUSDownloadService;
import lombok.Getter;
import lombok.Setter;
public class NUSDataProviderRemote extends NUSDataProvider {
@Getter @Setter private int version = Settings.LATEST_TMD_VERSION;
@Getter @Setter private long titleID = 0L;
@Override
public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
InputStream in = downloadService.getInputStreamForURL(getRemoteURL(content),fileOffsetBlock);
return in;
}
private String getRemoteURL(Content content) {
return String.format("%016x/%08X", getNUSTitle().getTMD().getTitleID(),content.getID());
}
@Override
public byte[] getContentH3Hash(Content content) throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
String url = getRemoteURL(content) + ".h3";
return downloadService.downloadToByteArray(url);
}
@Override
public byte[] getRawTMD() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
long titleID = getTitleID();
int version = getVersion();
return downloadService.downloadTMDToByteArray(titleID, version);
}
@Override
public byte[] getRawTicket() throws IOException {
NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance();
long titleID = getNUSTitle().getTMD().getTitleID();
return downloadService.downloadTicketToByteArray(titleID);
}
@Override
public void cleanup() throws IOException {
// TODO Auto-generated method stub
}
@Override
public byte[] getRawCert() throws IOException {
return nulL;
}
}

View File

@ -0,0 +1,102 @@
package de.mas.jnus.lib.implementations;
import java.io.IOException;
import java.io.InputStream;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.TMD;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.implementations.wud.parser.WUDInfo;
import de.mas.jnus.lib.implementations.wud.parser.WUDPartition;
import de.mas.jnus.lib.implementations.wud.parser.WUDPartitionHeader;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class NUSDataProviderWUD extends NUSDataProvider {
@Getter @Setter private WUDInfo WUDInfo = null;
@Setter(AccessLevel.PRIVATE) private TMD TMD = null;
public NUSDataProviderWUD() {
super();
}
public long getOffsetInWUD(Content content) {
if(content.getContentFSTInfo() == null){
return getAbsoluteReadOffset();
}else{
return getAbsoluteReadOffset() + content.getContentFSTInfo().getOffset();
}
}
public long getAbsoluteReadOffset(){
return (long)Settings.WIIU_DECRYPTED_AREA_OFFSET + getGamePartition().getPartitionOffset();
}
@Override
public InputStream getInputStreamFromContent(Content content, long fileOffsetBlock) throws IOException {
WUDDiscReader discReader = getDiscReader();
long offset = getOffsetInWUD(content) + fileOffsetBlock;
return discReader.readEncryptedToInputStream(offset, content.getEncryptedFileSize());
}
@Override
public byte[] getContentH3Hash(Content content) throws IOException {
if(getGamePartitionHeader() == null){
log.warning("GamePartitionHeader is null");
return null;
}
if(!getGamePartitionHeader().isCalculatedHashes()){
log.info("Calculating h3 hashes");
getGamePartitionHeader().calculateHashes(getTMD().getAllContents());
}
return getGamePartitionHeader().getH3Hash(content);
}
private TMD getTMD() {
if(TMD == null){
setTMD(de.mas.jnus.lib.entities.TMD.parseTMD(getRawTMD()));
}
return TMD;
}
@Override
public byte[] getRawTMD() {
return getWUDInfo().getGamePartition().getRawTMD();
}
@Override
public byte[] getRawTicket() {
return getWUDInfo().getGamePartition().getRawTicket();
}
@Override
public byte[] getRawCert() throws IOException {
return getWUDInfo().getGamePartition().getRawCert();
}
public WUDPartition getGamePartition(){
return getWUDInfo().getGamePartition();
}
public WUDPartitionHeader getGamePartitionHeader(){
return getGamePartition().getPartitionHeader();
}
public WUDDiscReader getDiscReader(){
return getWUDInfo().getWUDDiscReader();
}
@Override
public void cleanup() {
}
}

View File

@ -0,0 +1,95 @@
package de.mas.jnus.lib.implementations;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.implementations.woomy.WoomyInfo;
import de.mas.jnus.lib.implementations.woomy.WoomyZipFile;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class NUSDataProviderWoomy extends NUSDataProvider{
@Getter @Setter private WoomyInfo woomyInfo;
@Setter private WoomyZipFile woomyZipFile;
@Override
public InputStream getInputStreamFromContent(@NonNull Content content, long fileOffsetBlock) throws IOException {
WoomyZipFile zipFile = getSharedWoomyZipFile();
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
if(entry == null){
log.warning("Inputstream for " + content.getFilename() + " not found");
System.exit(1);
}
return zipFile.getInputStream(entry);
}
@Override
public byte[] getContentH3Hash(Content content) throws IOException {
ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase());
if(entry != null){
WoomyZipFile zipFile = getNewWoomyZipFile();
byte[] result = zipFile.getEntryAsByte(entry);
zipFile.close();
return result;
}
return null;
}
@Override
public byte[] getRawTMD() throws IOException {
ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TMD_FILENAME);
if(entry == null){
log.warning(Settings.TMD_FILENAME + " not found in woomy file");
System.exit(1);
}
WoomyZipFile zipFile = getNewWoomyZipFile();
byte[] result = zipFile.getEntryAsByte(entry);
zipFile.close();
return result;
}
@Override
public byte[] getRawTicket() throws IOException {
ZipEntry entry = getWoomyInfo().getContentFiles().get(Settings.TICKET_FILENAME);
if(entry == null){
log.warning(Settings.TICKET_FILENAME + " not found in woomy file");
System.exit(1);
}
WoomyZipFile zipFile = getNewWoomyZipFile();
byte[] result = zipFile.getEntryAsByte(entry);
zipFile.close();
return result;
}
public WoomyZipFile getSharedWoomyZipFile() throws ZipException, IOException {
if(woomyZipFile == null || woomyZipFile.isClosed()){
woomyZipFile = getNewWoomyZipFile();
}
return woomyZipFile;
}
private WoomyZipFile getNewWoomyZipFile() throws ZipException, IOException {
return new WoomyZipFile(getWoomyInfo().getWoomyFile());
}
@Override
public void cleanup() throws IOException {
if(woomyZipFile != null && woomyZipFile.isClosed()){
woomyZipFile.close();
}
}
@Override
public byte[] getRawCert() throws IOException {
// TODO Auto-generated method stub
return null;
}
}

View File

@ -0,0 +1,14 @@
package de.mas.jnus.lib.implementations.woomy;
import java.io.File;
import java.util.Map;
import java.util.zip.ZipEntry;
import lombok.Data;
@Data
public class WoomyInfo {
private String name;
private File woomyFile;
private Map<String,ZipEntry> contentFiles;
}

View File

@ -0,0 +1,39 @@
package de.mas.jnus.lib.implementations.woomy;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@Data
public class WoomyMeta {
private String name;
private int icon;
private List<WoomyEntry> entries;
public void addEntry(String name,String folder, int entryCount){
WoomyEntry entry = new WoomyEntry(name, folder, entryCount);
getEntries().add(entry);
}
public List<WoomyEntry> 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;
}
}

View File

@ -0,0 +1,62 @@
package de.mas.jnus.lib.implementations.woomy;
import java.io.InputStream;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import de.mas.jnus.lib.utils.XMLParser;
import lombok.extern.java.Log;
@Log
public class WoomyMetaParser extends XMLParser{
private static final String WOOMY_METADATA_NAME = "name";
private static final String WOOMY_METADATA_ICON = "icon";
private static final String WOOMY_METADATA_ENTRIES = "entries";
private static final String WOOMY_METADATA_ENTRY_NAME = "name";
private static final String WOOMY_METADATA_ENTRY_FOLDER = "folder";
private static final String WOOMY_METADATA_ENTRY_ENTRIES = "entries";
/**
* Overwrite the default constructor to force the user to use the factory.
*/
private WoomyMetaParser(){
}
public static WoomyMeta parseMeta(InputStream data){
XMLParser parser = new WoomyMetaParser();
WoomyMeta result = new WoomyMeta();
try {
parser.loadDocument(data);
} catch(Exception e){
log.info("Error while loading the data into the WoomyMetaParser");
return null;
}
String name = parser.getValueOfElement(WOOMY_METADATA_NAME);
if(name != null && !name.isEmpty()){
result.setName(name);
}
String icon = parser.getValueOfElement(WOOMY_METADATA_ICON);
if(icon != null && !icon.isEmpty()){
int icon_val = Integer.parseInt(icon);
result.setIcon(icon_val);
}
Node entries_node = parser.getNodeByValue(WOOMY_METADATA_ENTRIES);
NodeList entry_list = entries_node.getChildNodes();
for(int i = 0;i<entry_list.getLength();i++){
Node node = entry_list.item(i);
String folder = getAttributeValueFromNode(node, WOOMY_METADATA_ENTRY_FOLDER);
String entry_name = getAttributeValueFromNode(node, WOOMY_METADATA_ENTRY_NAME);
String entry_count = getAttributeValueFromNode(node, WOOMY_METADATA_ENTRY_ENTRIES);
int entry_count_val = Integer.parseInt(entry_count);
result.addEntry(entry_name, folder, entry_count_val);
}
return result;
}
}

View File

@ -0,0 +1,81 @@
package de.mas.jnus.lib.implementations.woomy;
import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import com.sun.istack.internal.NotNull;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.implementations.woomy.WoomyMeta.WoomyEntry;
import lombok.extern.java.Log;
/**
*
* @author Maschell
*
*/
@Log
public class WoomyParser {
public static WoomyInfo createWoomyInfo(File woomyFile) throws IOException, ParserConfigurationException, SAXException{
WoomyInfo result = new WoomyInfo();
if(!woomyFile.exists()){
log.info("File does not exist." + woomyFile.getAbsolutePath());
return null;
}
try (ZipFile zipFile = new ZipFile(woomyFile)) {
result.setWoomyFile(woomyFile);
ZipEntry metaFile = zipFile.getEntry(Settings.WOOMY_METADATA_FILENAME);
if(metaFile == null){
log.info("No meta ");
return null;
}
WoomyMeta meta = WoomyMetaParser.parseMeta(zipFile.getInputStream(metaFile));
/**
* Currently we will only use the first entry in the metadata.xml
*/
if(meta.getEntries().isEmpty()){
return null;
}
WoomyEntry entry = meta.getEntries().get(0);
String regEx = entry.getFolder() + ".*"; //We want all files in the entry fodler
Map<String,ZipEntry> contentFiles = loadFileList(zipFile,regEx);
result.setContentFiles(contentFiles);
}catch(ZipException e){
}
return result;
}
private static Map<String, ZipEntry> loadFileList(@NotNull ZipFile zipFile, @NotNull String regEx) {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
Map<String,ZipEntry> result = new HashMap<>();
Pattern pattern = Pattern.compile(regEx);
while (zipEntries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) zipEntries.nextElement();
if(!entry.isDirectory()){
String entryName = entry.getName();
Matcher matcher = pattern.matcher(entryName);
if(matcher.matches()){
String[] tokens = entryName.split("[\\\\|/]"); //We only want the filename!
String filename = tokens[tokens.length - 1];
result.put(filename.toLowerCase(), entry);
}
}
}
return result;
}
}

View File

@ -0,0 +1,35 @@
package de.mas.jnus.lib.implementations.woomy;
import java.io.File;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import de.mas.jnus.lib.utils.StreamUtils;
import lombok.Getter;
import lombok.Setter;
/**
* Woomy files are just zip files. This class is just the
* normal ZipFile class extended by an "isClosed" attribute.
* @author Maschell
*
*/
public class WoomyZipFile extends ZipFile {
@Getter @Setter boolean isClosed;
public WoomyZipFile(File file) throws ZipException, IOException {
super(file);
}
@Override
public void close() throws IOException {
super.close();
setClosed(true);
}
public byte[] getEntryAsByte(ZipEntry entry) throws IOException {
return StreamUtils.getBytesFromStream(getInputStream(entry),(int) entry.getSize());
}
}

View File

@ -0,0 +1,108 @@
package de.mas.jnus.lib.implementations.wud;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderCompressed;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderSplitted;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReaderUncompressed;
import de.mas.jnus.lib.utils.ByteUtils;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class WUDImage {
public static long WUD_FILESIZE = 0x5D3A00000L;
@Getter @Setter private File fileHandle = null;
@Getter @Setter private WUDImageCompressedInfo compressedInfo = null;
@Getter @Setter private boolean isCompressed = false;
@Getter @Setter private boolean isSplitted = false;
private long inputFileSize = 0L;
@Setter private WUDDiscReader WUDDiscReader = null;
public WUDImage(File file) throws IOException{
if(file == null || !file.exists()){
System.out.println("WUD file is null or does not exist");
System.exit(1);
}
RandomAccessFile fileStream = new RandomAccessFile(file,"r");
fileStream.seek(0);
byte[] wuxheader = new byte[WUDImageCompressedInfo.WUX_HEADER_SIZE];
fileStream.read(wuxheader);
WUDImageCompressedInfo compressedInfo = new WUDImageCompressedInfo(wuxheader);
if(compressedInfo.isWUX()){
log.info("Image is compressed");
setCompressed(true);
Map<Integer,Long> 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 if(file.getName().equals(String.format(WUDDiscReaderSplitted.WUD_SPLITTED_DEFAULT_FILEPATTERN, 1)) &&
(file.length() == WUDDiscReaderSplitted.WUD_SPLITTED_FILE_SIZE)){
setSplitted(true);
log.info("Image is splitted");
}
fileStream.close();
setFileHandle(file);
}
public WUDDiscReader getWUDDiscReader() {
if(WUDDiscReader == null){
if(isCompressed()){
setWUDDiscReader(new WUDDiscReaderCompressed(this));
}else if(isSplitted()){
setWUDDiscReader(new WUDDiscReaderSplitted(this));
}else{
setWUDDiscReader(new WUDDiscReaderUncompressed(this));
}
}
return WUDDiscReader;
}
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;
}
}

View File

@ -0,0 +1,100 @@
package de.mas.jnus.lib.implementations.wud;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.utils.ByteUtils;
import lombok.Getter;
import lombok.Setter;
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 @Setter private int magic0;
@Getter @Setter private int magic1;
@Getter @Setter private int sectorSize;
@Getter @Setter private long uncompressedSize;
@Getter @Setter private int flags;
@Getter @Setter private long indexTableEntryCount = 0;
@Getter @Setter private long offsetIndexTable = 0;
@Getter @Setter private long offsetSectorArray = 0;
@Getter @Setter private long indexTableSize = 0;
@Getter private Map<Integer,Long> 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<Integer,Long> 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();
}
}

View File

@ -0,0 +1,12 @@
package de.mas.jnus.lib.implementations.wud.parser;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper=true)
public class WUDGamePartition extends WUDPartition {
private byte[] rawTMD;
private byte[] rawCert;
private byte[] rawTicket;
}

View File

@ -0,0 +1,41 @@
package de.mas.jnus.lib.implementations.wud.parser;
import java.util.Map;
import java.util.Map.Entry;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Data
public class WUDInfo {
private byte[] titleKey = null;
private WUDDiscReader WUDDiscReader = null;
private Map<String,WUDPartition> 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<String,WUDPartition> e: getPartitions().entrySet()){
if(e.getKey().equals(getGamePartitionName())){
return (WUDGamePartition) e.getValue();
}
}
return null;
}
}

View File

@ -0,0 +1,155 @@
package de.mas.jnus.lib.implementations.wud.parser;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import de.mas.jnus.lib.Settings;
import de.mas.jnus.lib.entities.content.ContentFSTInfo;
import de.mas.jnus.lib.entities.fst.FST;
import de.mas.jnus.lib.entities.fst.FSTEntry;
import de.mas.jnus.lib.implementations.wud.reader.WUDDiscReader;
import de.mas.jnus.lib.utils.ByteUtils;
import de.mas.jnus.lib.utils.FileUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.extern.java.Log;
@Log
public class WUDInfoParser {
public static byte[] DECRYPTED_AREA_SIGNATURE = new byte[] { (byte) 0xCC, (byte) 0xA6, (byte) 0xE6, 0x7B };
public static byte[] PARTITION_FILE_TABLE_SIGNATURE = new byte[] { 0x46, 0x53, 0x54, 0x00 }; // "FST"
public final static int PARTITION_TOC_OFFSET = 0x800;
public final static int PARTITION_TOC_ENTRY_SIZE = 0x80;
public static final String WUD_TMD_FILENAME = "title.tmd";
public static final String WUD_TICKET_FILENAME = "title.tik";
public static final String WUD_CERT_FILENAME = "title.cert";
public static WUDInfo createAndLoad(WUDDiscReader discReader,byte[] titleKey) throws IOException {
WUDInfo result = new WUDInfo();
result.setTitleKey(titleKey);
result.setWUDDiscReader(discReader);
byte[] PartitionTocBlock = discReader.readDecryptedToByteArray(Settings.WIIU_DECRYPTED_AREA_OFFSET, 0, 0x8000, titleKey, null);
// verify DiscKey before proceeding
if(!Arrays.equals(Arrays.copyOfRange(PartitionTocBlock, 0, 4),DECRYPTED_AREA_SIGNATURE)){
System.out.println("Decryption of PartitionTocBlock failed");
return null;
}
Map<String,WUDPartition> partitions = readPartitions(result,PartitionTocBlock);
result.setPartitions(partitions);
//parsePartitions(wudInfo,partitions);
return result;
}
private static Map<String, WUDPartition> 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<String,WUDPartition> 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<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);
partition.setPartitionName(partitionName);
partition.setPartitionOffset(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());
FileUtils.saveByteArrayToFile("test.tmd", rawTMD);
gamePartition.setRawTMD(rawTMD);
gamePartition.setRawTicket(rawTIK);
gamePartition.setRawCert(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)){
gamePartition.setPartitionOffset(partitionOffset);
gamePartition.setPartitionName(partitionName);
System.out.println(String.format("partitionName: %s", partitionName));
System.out.println(String.format("Gameoffset: %016X", partitionOffset));
wudInfo.setGamePartitionName(partitionName);
partition = gamePartition;
}
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;
}
}

View File

@ -0,0 +1,10 @@
package de.mas.jnus.lib.implementations.wud.parser;
import lombok.Data;
@Data
public class WUDPartition {
private String partitionName = "";
private long partitionOffset = 0;
private WUDPartitionHeader partitionHeader;
}

View File

@ -0,0 +1,96 @@
package de.mas.jnus.lib.implementations.wud.parser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.mas.jnus.lib.entities.content.Content;
import de.mas.jnus.lib.utils.ByteUtils;
import de.mas.jnus.lib.utils.HashUtil;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public class WUDPartitionHeader {
@Getter @Setter
private boolean calculatedHashes = false;
private HashMap<Short,byte[]> 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<Short,byte[]> 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<Integer, Content> 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<Content> contents = new ArrayList<>(allContents.values());
Collections.sort( contents, new Comparator<Content>() {
@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);
}
}

View File

@ -0,0 +1,127 @@
package de.mas.jnus.lib.implementations.wud.reader;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.utils.cryptography.AESDecryption;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.java.Log;
@Log
public abstract class WUDDiscReader {
@Getter @Setter private WUDImage image = null;
public WUDDiscReader(WUDImage image){
setImage(image);
}
public InputStream readEncryptedToInputStream(long offset,long size) throws IOException {
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream(in);
new Thread(() -> {try {
readEncryptedToOutputStream(out,offset,size);
} catch (IOException e) {e.printStackTrace();}}).start();
return in;
}
public byte[] readEncryptedToByteArray(long offset,long fileoffset, long size) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
readEncryptedToOutputStream(out,offset,size);
return out.toByteArray();
}
public InputStream readDecryptedToInputStream(long offset,long fileoffset, long size, byte[] key,byte[] iv) throws IOException {
PipedInputStream in = new PipedInputStream();
PipedOutputStream out = new PipedOutputStream(in);
new Thread(() -> {try {
readDecryptedToOutputStream(out,offset,fileoffset,size,key,iv);
} catch (IOException e) {e.printStackTrace();}}).start();
return in;
}
public byte[] readDecryptedToByteArray(long offset,long fileoffset, long size, byte[] key,byte[] iv) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
readDecryptedToOutputStream(out,offset,fileoffset,size,key,iv);
return out.toByteArray();
}
protected abstract void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException;
/**
*
* @param readOffset Needs to be aligned to 0x8000
* @param key
* @param IV
* @return
* @throws IOException
*/
public byte[] readDecryptedChunk(long readOffset,byte[] key, byte[]IV) throws IOException{
int chunkSize = 0x8000;
byte[] encryptedChunk = readEncryptedToByteArray(readOffset, 0, chunkSize);
byte[] decryptedChunk = new byte[chunkSize];
AESDecryption aesDecryption = new AESDecryption(key, IV);
decryptedChunk = aesDecryption.decrypt(encryptedChunk);
return decryptedChunk;
}
public void readDecryptedToOutputStream(OutputStream outputStream,long clusterOffset, long fileOffset, long size,byte[] key,byte[] IV) throws IOException {
if(IV == null){
IV = new byte[0x10];
}
byte[] buffer;
long maxCopySize;
long copySize;
long readOffset;
int blockSize = 0x8000;
long totalread = 0;
do{
long blockNumber = (fileOffset / blockSize);
long blockOffset = (fileOffset % blockSize);
readOffset = clusterOffset + (blockNumber * blockSize);
// (long)WiiUDisc.WIIU_DECRYPTED_AREA_OFFSET + volumeOffset + clusterOffset + (blockStructure.getBlockNumber() * 0x8000);
buffer = readDecryptedChunk(readOffset,key, IV);
maxCopySize = 0x8000 - blockOffset;
copySize = (size > maxCopySize) ? maxCopySize : size;
outputStream.write(Arrays.copyOfRange(buffer, (int) blockOffset, (int) copySize));
totalread += copySize;
// update counters
size -= copySize;
fileOffset += copySize;
}while(totalread < size);
outputStream.close();
}
public RandomAccessFile getRandomAccessFileStream() throws FileNotFoundException{
if(getImage() == null || getImage().getFileHandle() == null){
log.warning("No image or image filehandle set.");
System.exit(1);
}
return new RandomAccessFile(getImage().getFileHandle(), "r");
}
}

View File

@ -0,0 +1,69 @@
package de.mas.jnus.lib.implementations.wud.reader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
import de.mas.jnus.lib.implementations.wud.WUDImageCompressedInfo;
import lombok.extern.java.Log;
@Log
public class WUDDiscReaderCompressed extends WUDDiscReader{
public WUDDiscReaderCompressed(WUDImage image) {
super(image);
}
/**
* Expects the .wux format by Exzap. You can more infos about it here.
* https://gbatemp.net/threads/wii-u-image-wud-compression-tool.397901/
*/
@Override
protected void readEncryptedToOutputStream(OutputStream out, long offset, long size) throws IOException {
// make sure there is no out-of-bounds read
WUDImageCompressedInfo info = getImage().getCompressedInfo();
long fileBytesLeft = info.getUncompressedSize() - offset;
if( fileBytesLeft <= 0 ){
log.warning("offset too big");
System.exit(1);
}
if( fileBytesLeft < size ){
size = fileBytesLeft;
}
// compressed read must be handled on a per-sector level
int bufferSize = 0x8000;
byte[] buffer = new byte[bufferSize];
RandomAccessFile input = getRandomAccessFileStream();
while( size > 0 ){
long sectorOffset = (offset % info.getSectorSize());
long remainingSectorBytes = info.getSectorSize() - sectorOffset;
long sectorIndex = (offset / info.getSectorSize());
int bytesToRead = (int) ((remainingSectorBytes<size)?remainingSectorBytes:size); // read only up to the end of the current sector
// look up real sector index
long realSectorIndex = info.getSectorIndex((int) sectorIndex);
long offset2 = info.getOffsetSectorArray() + realSectorIndex*info.getSectorSize()+sectorOffset;
input.seek(offset2);
int read = input.read(buffer);
if(read < 0) return;
try{
out.write(Arrays.copyOfRange(buffer, 0, bytesToRead));
}catch(IOException e){
if(e.getMessage().equals("Pipe closed")){
break;
}else{
throw e;
}
}
size -= bytesToRead;
offset += bytesToRead;
}
input.close();
}
}

View File

@ -0,0 +1,96 @@
package de.mas.jnus.lib.implementations.wud.reader;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
public class WUDDiscReaderSplitted extends WUDDiscReader{
public WUDDiscReaderSplitted(WUDImage image) {
super(image);
}
public static long WUD_SPLITTED_FILE_SIZE = 0x100000L * 0x800L;
public static long NUMBER_OF_FILES = 12;
public static String WUD_SPLITTED_DEFAULT_FILEPATTERN = "game_part%d.wud";
@Override
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset, long size) throws IOException {
RandomAccessFile input = getFileByOffset(offset);
int bufferSize = 0x8000;
byte[] buffer = new byte[bufferSize];
long totalread = 0;
long curOffset = offset;
int part = getFilePartByOffset(offset);
long offsetInFile = getOffsetInFilePart(part, curOffset);
do{
offsetInFile = getOffsetInFilePart(part, curOffset);
int curReadSize = bufferSize;
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
}
}
int read = input.read(buffer,0,curReadSize);
if(read < 0) break;
if(totalread + read > size){
read = (int) (size - totalread);
}
try{
outputStream.write(Arrays.copyOfRange(buffer, 0, read));
}catch(IOException e){
if(e.getMessage().equals("Pipe closed")){
break;
}else{
input.close();
throw e;
}
}
totalread += read;
curOffset += read;
}while(totalread < size);
input.close();
outputStream.close();
}
private int getFilePartByOffset(long offset){
return (int) (offset/WUD_SPLITTED_FILE_SIZE)+1;
}
private long getOffsetInFilePart(int part,long offset){
return offset - ((long)(part-1) * WUD_SPLITTED_FILE_SIZE);
}
private RandomAccessFile getFileByOffset(long offset) throws IOException{
File filehandlePart1 = getImage().getFileHandle();
String pathToFiles = filehandlePart1.getParentFile().getAbsolutePath();
int filePart = getFilePartByOffset(offset);
String filePartPath = pathToFiles + File.separator + String.format(WUD_SPLITTED_DEFAULT_FILEPATTERN, filePart);
File part = new File(filePartPath);
if(!part.exists()){
System.out.println("File does not exist");
return null;
}
RandomAccessFile result = new RandomAccessFile(part, "r");
result.seek(getOffsetInFilePart(filePart, offset));
return result;
}
}

View File

@ -0,0 +1,45 @@
package de.mas.jnus.lib.implementations.wud.reader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import de.mas.jnus.lib.implementations.wud.WUDImage;
public class WUDDiscReaderUncompressed extends WUDDiscReader {
public WUDDiscReaderUncompressed(WUDImage image) {
super(image);
}
@Override
protected void readEncryptedToOutputStream(OutputStream outputStream, long offset,long size) throws IOException{
FileInputStream input = new FileInputStream(getImage().getFileHandle());
input.skip(offset);
int bufferSize = 0x8000;
byte[] buffer = new byte[bufferSize];
long totalread = 0;
do{
int read = input.read(buffer);
if(read < 0) break;
if(totalread + read > size){
read = (int) (size - totalread);
}
try{
outputStream.write(Arrays.copyOfRange(buffer, 0, read));
}catch(IOException e){
if(e.getMessage().equals("Pipe closed")){
break;
}else{
input.close();
throw e;
}
}
totalread += read;
}while(totalread < size);
input.close();
outputStream.close();
}
}

View File

@ -0,0 +1,25 @@
package de.mas.jnus.lib.utils;
import lombok.Getter;
import lombok.Setter;
public class ByteArrayBuffer{
@Getter public byte[] buffer;
@Getter @Setter int lengthOfDataInBuffer;
public ByteArrayBuffer(int length){
buffer = new byte[(int) length];
}
public int getSpaceLeft() {
return buffer.length - getLengthOfDataInBuffer();
}
public void addLengthOfDataInBuffer(int bytesRead) {
lengthOfDataInBuffer += bytesRead;
}
public void resetLengthOfDataInBuffer() {
setLengthOfDataInBuffer(0);
}
}

View File

@ -0,0 +1,33 @@
package de.mas.jnus.lib.utils;
import java.util.Arrays;
public final class ByteArrayWrapper
{
private final byte[] data;
public ByteArrayWrapper(byte[] data)
{
if (data == null)
{
throw new NullPointerException();
}
this.data = data;
}
@Override
public boolean equals(Object other)
{
if (!(other instanceof ByteArrayWrapper))
{
return false;
}
return Arrays.equals(data, ((ByteArrayWrapper)other).data);
}
@Override
public int hashCode()
{
return Arrays.hashCode(data);
}
}

View File

@ -0,0 +1,76 @@
package de.mas.jnus.lib.utils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
public class ByteUtils {
public static int getIntFromBytes(byte[] input,int offset){
return getIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
}
public static int getIntFromBytes(byte[] input,int offset,ByteOrder bo){
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.order(bo);
Arrays.copyOfRange(input,offset, offset+4);
buffer.put(Arrays.copyOfRange(input,offset, offset+4));
return buffer.getInt(0);
}
public static long getUnsingedIntFromBytes(byte[] input,int offset){
return getUnsingedIntFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
}
public static long getUnsingedIntFromBytes(byte[] input,int offset,ByteOrder bo){
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.order(bo);
if(bo.equals(ByteOrder.BIG_ENDIAN)){
buffer.position(4);
}else{
buffer.position(0);
}
buffer.put(Arrays.copyOfRange(input,offset, offset+4));
return buffer.getLong(0);
}
public static long getLongFromBytes(byte[] input,int offset){
return getLongFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
}
public static long getLongFromBytes(byte[] input,int offset,ByteOrder bo){
return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+8)).order(bo).getLong(0);
}
public static short getShortFromBytes(byte[] input,int offset){
return getShortFromBytes(input, offset, ByteOrder.BIG_ENDIAN);
}
public static short getShortFromBytes(byte[] input, int offset,ByteOrder bo) {
return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+2)).order(bo).getShort();
}
public static byte[] getBytesFromLong(long value) {
return getBytesFromLong(value,ByteOrder.BIG_ENDIAN);
}
public static byte[] getBytesFromLong(long value,ByteOrder bo) {
byte[] result = new byte[0x08];
ByteBuffer.allocate(8).order(bo).putLong(value).get(result);
return result;
}
public static byte[] getBytesFromInt(int value) {
return getBytesFromInt(value,ByteOrder.BIG_ENDIAN);
}
public static byte[] getBytesFromInt(int value,ByteOrder bo) {
byte[] result = new byte[0x04];
ByteBuffer.allocate(4).order(bo).putInt(value).get(result);
return result;
}
public static byte[] getBytesFromShort(short value) {
byte[] result = new byte[0x02];
ByteBuffer.allocate(2).putShort(value).get(result);
return result;
}
}

View File

@ -0,0 +1,52 @@
package de.mas.jnus.lib.utils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import lombok.NonNull;
public class FileUtils {
public static boolean saveByteArrayToFile(String filePath,byte[] data) throws IOException {
File target = new File(filePath);
if(target.isDirectory()){
return false;
}
File parent = target.getParentFile();
if(parent != null){
Utils.createDir(parent.getAbsolutePath());
}
return saveByteArrayToFile(target,data);
}
/**
* Saves a byte array to a file (and overwrite it if its already exists)
* DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
* @param output
* @param data
* @return
* @throws IOException
*/
public static boolean saveByteArrayToFile(@NonNull File output,byte[] data) throws IOException {
FileOutputStream out = new FileOutputStream(output);
out.write(data);
out.close();
return true;
}
/**
* Saves a byte array to a file (and overwrite it if its already exists)
* DOES NOT IF THE PATH/FILE EXIST OR IS IT EVEN A FILE
* @param output
* @param inputStream
* @throws IOException
*/
public static void saveInputStreamToFile(@NonNull File output,InputStream inputStream,long filesize) throws IOException {
FileOutputStream out = new FileOutputStream(output);
StreamUtils.saveInputStreamToOutputStream(inputStream,out,filesize);
}
}

View File

@ -0,0 +1,185 @@
package de.mas.jnus.lib.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import lombok.extern.java.Log;
@Log
public class HashUtil {
public static byte[] hashSHA256(byte[] data){
MessageDigest sha256;
try {
sha256 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return new byte[0x20];
}
return sha256.digest(data);
}
public static byte[] hashSHA256(File file) {
return hashSHA256(file, 0);
}
//TODO: testing
public static byte[] hashSHA256(File file,int aligmnent) {
byte[] hash = new byte[0x20];
MessageDigest sha1 = null;
try {
InputStream in = new FileInputStream(file);
sha1 = MessageDigest.getInstance("SHA-256");
hash = hash(sha1,in,file.length(),0x8000,aligmnent);
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return hash;
}
public static byte[] hashSHA1(byte[] data){
MessageDigest sha1;
try {
sha1 = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return new byte[0x14];
}
return sha1.digest(data);
}
public static byte[] hashSHA1(File file) {
return hashSHA1(file, 0);
}
public static byte[] hashSHA1(File file,int aligmnent) {
byte[] hash = new byte[0x14];
MessageDigest sha1 = null;
try {
InputStream in = new FileInputStream(file);
sha1 = MessageDigest.getInstance("SHA1");
hash = hash(sha1,in,file.length(),0x8000,aligmnent);
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return hash;
}
public static byte [] hash(MessageDigest digest, InputStream in,long inputSize1, int bufferSize,int alignment) throws IOException {
long target_size = alignment == 0 ? inputSize1: Utils.align(inputSize1, alignment);
long cur_position = 0;
int inBlockBufferRead = 0;
byte[] blockBuffer = new byte[bufferSize];
ByteArrayBuffer overflow = new ByteArrayBuffer(bufferSize);
do{
if(cur_position + bufferSize > target_size){
int expectedSize = (int) (target_size - cur_position);
ByteBuffer buffer = ByteBuffer.allocate(expectedSize);
buffer.position(0);
inBlockBufferRead = StreamUtils.getChunkFromStream(in,blockBuffer,overflow,expectedSize);
buffer.put(Arrays.copyOfRange(blockBuffer, 0, inBlockBufferRead));
blockBuffer = buffer.array();
inBlockBufferRead = blockBuffer.length;
}else{
int expectedSize = bufferSize;
inBlockBufferRead = StreamUtils.getChunkFromStream(in,blockBuffer,overflow,expectedSize);
}
if(inBlockBufferRead == 0){
inBlockBufferRead = (int) (target_size - cur_position);
blockBuffer = new byte[inBlockBufferRead];
}
if(inBlockBufferRead <= 0) break;
digest.update(blockBuffer, 0, inBlockBufferRead);
cur_position += inBlockBufferRead;
}while(cur_position < target_size);
in.close();
return digest.digest();
}
public static void checkFileChunkHashes(byte[] hashes,byte[] h3Hashes, byte[] output,int block) {
int H0_start = (block % 16) * 20;
int H1_start = (16 + (block / 16) % 16) * 20;
int H2_start = (32 + (block / 256) % 16) * 20;
int H3_start = ((block / 4096) % 16) * 20;
byte[] real_h0_hash = HashUtil.hashSHA1(output);
byte[] expected_h0_hash = Arrays.copyOfRange(hashes,H0_start,H0_start + 20);
if(!Arrays.equals(real_h0_hash,expected_h0_hash)){
System.out.println("h0 checksum failed");
System.out.println("real hash :" + Utils.ByteArrayToString(real_h0_hash));
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h0_hash));
System.exit(2);
//throw new IllegalArgumentException("h0 checksumfail");
}else{
log.finest("h1 checksum right!");
}
if ((block % 16) == 0){
byte[] expected_h1_hash = Arrays.copyOfRange(hashes,H1_start,H1_start + 20);
byte[] real_h1_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H0_start,H0_start + (16*20)));
if(!Arrays.equals(expected_h1_hash, real_h1_hash)){
System.out.println("h1 checksum failed");
System.out.println("real hash :" + Utils.ByteArrayToString(real_h1_hash));
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h1_hash));
System.exit(2);
//throw new IllegalArgumentException("h1 checksumfail");
}else{
log.finer("h1 checksum right!");
}
}
if ((block % 256) == 0){
byte[] expected_h2_hash = Arrays.copyOfRange(hashes,H2_start,H2_start + 20);
byte[] real_h2_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H1_start,H1_start + (16*20)));
if(!Arrays.equals(expected_h2_hash, real_h2_hash)){
System.out.println("h2 checksum failed");
System.out.println("real hash :" + Utils.ByteArrayToString(real_h2_hash));
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h2_hash));
System.exit(2);
//throw new IllegalArgumentException("h2 checksumfail");
}else{
log.fine("h2 checksum right!");
}
}
if(h3Hashes == null){
log.info("didn't check the h3, its missing.");
return;
}
if ((block % 4096) == 0){
byte[] expected_h3_hash = Arrays.copyOfRange(h3Hashes,H3_start,H3_start + 20);
byte[] real_h3_hash = HashUtil.hashSHA1(Arrays.copyOfRange(hashes,H2_start,H2_start + (16*20)));
if(!Arrays.equals(expected_h3_hash, real_h3_hash)){
System.out.println("h3 checksum failed");
System.out.println("real hash :" + Utils.ByteArrayToString(real_h3_hash));
System.out.println("expected hash:" + Utils.ByteArrayToString(expected_h3_hash));
System.exit(2);
//throw new IllegalArgumentException("h3 checksumfail");
}else{
log.fine("h3 checksum right!");
}
}
}
}

View File

@ -0,0 +1,127 @@
package de.mas.jnus.lib.utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import lombok.extern.java.Log;
@Log
public class StreamUtils {
public static byte[] getBytesFromStream(InputStream in,int size) throws IOException{
byte[] result = new byte[size];
byte[] buffer = new byte[0x8000];
int totalRead = 0;
do{
int read = in.read(buffer);
if(read < 0) break;
System.arraycopy(buffer, 0, result, totalRead, read);
totalRead += read;
}while(totalRead < size);
in.close();
return result;
}
public static int getChunkFromStream(InputStream inputStream,byte[] output, ByteArrayBuffer overflowbuffer,int BLOCKSIZE) throws IOException {
int bytesRead = -1;
int inBlockBuffer = 0;
byte[] overflowbuf = overflowbuffer.getBuffer();
do{
try{
bytesRead = inputStream.read(overflowbuf,overflowbuffer.getLengthOfDataInBuffer(),overflowbuffer.getSpaceLeft());
}catch(IOException e){
if(!e.getMessage().equals("Write end dead")){
throw e;
}
bytesRead = -1;
}
if(bytesRead <= 0){
if(overflowbuffer.getLengthOfDataInBuffer() > 0){
System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer());
inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer();
}
break;
}
overflowbuffer.addLengthOfDataInBuffer(bytesRead);
if(inBlockBuffer + overflowbuffer.getLengthOfDataInBuffer() > BLOCKSIZE){
int tooMuch = (inBlockBuffer + bytesRead) - BLOCKSIZE;
int toRead = BLOCKSIZE - inBlockBuffer;
System.arraycopy(overflowbuf, 0, output, inBlockBuffer, toRead);
inBlockBuffer += toRead;
System.arraycopy(overflowbuf, toRead, overflowbuf, 0, tooMuch);
overflowbuffer.setLengthOfDataInBuffer(tooMuch);
}else{
System.arraycopy(overflowbuf, 0, output, inBlockBuffer, overflowbuffer.getLengthOfDataInBuffer());
inBlockBuffer +=overflowbuffer.getLengthOfDataInBuffer();
overflowbuffer.resetLengthOfDataInBuffer();
}
}while(inBlockBuffer != BLOCKSIZE);
return inBlockBuffer;
}
public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException {
saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null,0L);
}
public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream,long filesize, byte[] hash,long expectedSizeForHash) throws IOException {
MessageDigest sha1 = null;
if(hash != null){
try {
sha1 = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
int BUFFER_SIZE = 0x8000;
byte[] buffer = new byte[BUFFER_SIZE];
int read = 0;
long totalRead = 0;
long written = 0;
do{
read = inputStream.read(buffer);
if(read < 0){
break;
}
totalRead +=read;
if(totalRead > filesize){
read = (int) (read - (totalRead - filesize));
}
outputStream.write(buffer, 0, read);
written += read;
if(sha1 != null){
sha1.update(buffer,0,read);
}
}while(written < filesize);
if(sha1 != null){
long missingInHash = expectedSizeForHash - written;
if(missingInHash > 0){
sha1.update(new byte[(int) missingInHash]);
}
byte[] calculated_hash = sha1.digest();
byte[] expected_hash = hash;
if(!Arrays.equals(calculated_hash, expected_hash)){
log.info(Utils.ByteArrayToString(calculated_hash));
log.info(Utils.ByteArrayToString(expected_hash));
log.info("Hash doesn't match saves output stream.");
}else{
//log.warning("Hash DOES match saves output stream.");
}
}
outputStream.close();
inputStream.close();
}
}

View File

@ -0,0 +1,66 @@
package de.mas.jnus.lib.utils;
import java.io.File;
public class Utils {
public static long align(long numToRound, int multiple){
if((multiple>0) && ((multiple & (multiple -1)) == 0)){
return alignPower2(numToRound, multiple);
}else{
return alignGeneriv(numToRound,multiple);
}
}
private static long alignGeneriv(long numToRound,int multiple){
int isPositive = 0;
if(numToRound >=0){
isPositive = 1;
}
return ((numToRound + isPositive * (multiple - 1)) / multiple) * multiple;
}
private static long alignPower2(long numToRound, int multiple)
{
if(!((multiple>0) && ((multiple & (multiple -1)) == 0))) return 0L;
return (numToRound + (multiple - 1)) & ~(multiple - 1);
}
public static String ByteArrayToString(byte[] ba)
{
if(ba == null) return null;
StringBuilder hex = new StringBuilder(ba.length * 2);
for(byte b : ba){
hex.append(String.format("%02X", b));
}
return hex.toString();
}
public static byte[] StringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
public static boolean createDir(String path){
if(path == null || path.isEmpty()){
return false;
}
File pathFile = new File(path);
if(!pathFile.exists()){
boolean succes = new File(path).mkdirs();
if(!succes){
System.out.println("Creating directory \"" +path + "\" failed.");
return false;
}
}else if(!pathFile.isDirectory()){
System.out.println("\"" +path + "\" already exists but is no directoy");
return false;
}
return true;
}
}

View File

@ -0,0 +1,77 @@
package de.mas.jnus.lib.utils;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.sun.istack.internal.NotNull;
public class XMLParser {
private Document document;
public void loadDocument(InputStream inputStream) throws ParserConfigurationException, SAXException, IOException{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(inputStream);
this.document = document;
}
public long getValueOfElementAsInt(String element,int index){
return Integer.parseInt(getValueOfElement(element,index));
}
public long getValueOfElementAsLong(String element,int index){
return Long.parseLong(getValueOfElement(element,index));
}
public String getValueOfElement(String element){
return getValueOfElement(element,0);
}
public Node getNodeByValue(String element){
return getNodeByValue(element, 0);
}
public Node getNodeByValue(String element,int index){
if(document == null){
System.out.println("Please load the document first.");
}
NodeList list = document.getElementsByTagName(element);
if(list == null){
return null;
}
return list.item(index);
}
public String getValueOfElementAttribute(String element,int index,String attribute){
Node node = getNodeByValue(element, index);
if(node == null){
//System.out.println("Node is null");
return "";
}
return getAttributeValueFromNode(node,attribute);
}
public static String getAttributeValueFromNode(@NotNull Node element,String attribute){
return element.getAttributes().getNamedItem(attribute).getTextContent().toString();
}
public String getValueOfElement(String element,int index){
Node node = getNodeByValue(element, index);
if(node == null){
//System.out.println("Node is null");
return "";
}
return node.getTextContent().toString();
}
}

View File

@ -0,0 +1,71 @@
package de.mas.jnus.lib.utils.cryptography;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import lombok.Getter;
import lombok.Setter;
public class AESDecryption {
private Cipher cipher;
@Getter @Setter private byte[] AESKey;
@Getter @Setter private byte[] IV;
public AESDecryption(byte[] AESKey, byte[] IV) {
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
e.printStackTrace();
}
setAESKey(AESKey);
setIV(IV);
init();
}
protected void init() {
init(getAESKey(),getIV());
}
protected void init(byte[] decryptedKey,byte[] iv){
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
try {
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
System.exit(2);
}
}
public byte[] decrypt(byte[] input){
try {
return cipher.doFinal(input);
} catch (IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
System.exit(2);
}
return input;
}
public byte[] decrypt(byte[] input,int len){
return decrypt(input,0,len);
}
public byte[] decrypt(byte[] input,int offset,int len){
try {
return cipher.doFinal(input, offset, len);
} catch (IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
System.exit(2);
}
return input;
}
}

View File

@ -0,0 +1,171 @@
package de.mas.jnus.lib.utils.cryptography;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import de.mas.jnus.lib.entities.Ticket;
import de.mas.jnus.lib.utils.ByteArrayBuffer;
import de.mas.jnus.lib.utils.HashUtil;
import de.mas.jnus.lib.utils.StreamUtils;
import de.mas.jnus.lib.utils.Utils;
import lombok.extern.java.Log;
@Log
public class NUSDecryption extends AESDecryption{
public NUSDecryption(byte[] AESKey, byte[] IV) {
super(AESKey, IV);
}
public NUSDecryption(Ticket ticket) {
this(ticket.getDecryptedKey(),ticket.getIV());
}
private byte[] decryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) {
return decryptFileChunk(blockBuffer,0,BLOCKSIZE, IV);
}
private byte[] decryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) {
if(IV != null){
setIV(IV);
init();
}
return decrypt(blockBuffer,offset,BLOCKSIZE);
}
public void decryptFileStream(InputStream inputStream,OutputStream outputStream,long filesize,short contentIndex,byte[] h3hash,long expectedSizeForHash) throws IOException {
MessageDigest sha1 = null;
if(h3hash != null){
try {
sha1 = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
int BLOCKSIZE = 0x8000;
long dlFileLength = filesize;
if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){
dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE;
}
byte[] IV = new byte[0x10];
IV[0] = (byte)((contentIndex >> 8) & 0xFF);
IV[1] = (byte)(contentIndex);
byte[] blockBuffer = new byte[BLOCKSIZE];
int inBlockBuffer;
long written = 0;
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
do{
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream,blockBuffer,overflow,BLOCKSIZE);
byte[] output = decryptFileChunk(blockBuffer,BLOCKSIZE,IV);
IV = Arrays.copyOfRange(blockBuffer,BLOCKSIZE-16, BLOCKSIZE);
if((written + inBlockBuffer) > filesize){
inBlockBuffer = (int) (filesize- written);
}
written += inBlockBuffer;
outputStream.write(output, 0, inBlockBuffer);
if(sha1 != null){
sha1.update(output,0,inBlockBuffer);
}
}while(inBlockBuffer == BLOCKSIZE);
if(sha1 != null){
long missingInHash = expectedSizeForHash - written;
if(missingInHash > 0){
sha1.update(new byte[(int) missingInHash]);
}
byte[] calculated_hash = sha1.digest();
byte[] expected_hash = h3hash;
if(!Arrays.equals(calculated_hash, expected_hash)){
log.info(Utils.ByteArrayToString(calculated_hash));
log.info(Utils.ByteArrayToString(expected_hash));
log.info("Hash doesn't match decrypted content.");
}else{
//log.warning("###################################################Hash DOES match saves output stream.");
}
}
outputStream.close();
inputStream.close();
}
public void decryptFileStreamHashed(InputStream inputStream,OutputStream outputStream,long filesize,long fileoffset,short contentIndex,byte[] h3Hash) throws IOException{
int BLOCKSIZE = 0x10000;
int HASHBLOCKSIZE = 0xFC00;
long writeSize = HASHBLOCKSIZE;
long block = (fileoffset / HASHBLOCKSIZE);
long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
if( soffset+filesize > writeSize )
writeSize = writeSize - soffset;
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE);
long wrote = 0;
int inBlockBuffer;
do{
inBlockBuffer = StreamUtils.getChunkFromStream(inputStream,encryptedBlockBuffer,overflow,BLOCKSIZE);
if( writeSize > filesize )
writeSize = filesize;
byte[] output = decryptFileChunkHash(encryptedBlockBuffer, (int) block,contentIndex,h3Hash);
if((wrote + writeSize) > filesize){
writeSize = (int) (filesize- wrote);
}
outputStream.write(output, (int)(0+soffset), (int)writeSize);
wrote +=writeSize;
block++;
if( soffset > 0){
writeSize = HASHBLOCKSIZE;
soffset = 0;
}
}while(wrote < filesize && (inBlockBuffer == BLOCKSIZE));
outputStream.close();
inputStream.close();
}
private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, int contentIndex,byte[] h3_hashes){
int hashSize = 0x400;
int blocksize = 0xFC00;
byte[] IV = ByteBuffer.allocate(16).putShort((short) contentIndex).array();
byte[] hashes = decryptFileChunk(blockBuffer,hashSize,IV);
hashes[0] ^= (byte)((contentIndex >> 8) & 0xFF);
hashes[1] ^= (byte)(contentIndex & 0xFF);
int H0_start = (block % 16) * 20;
IV = Arrays.copyOfRange(hashes,H0_start,H0_start + 16);
byte[] output = decryptFileChunk(blockBuffer,hashSize,blocksize,IV);
HashUtil.checkFileChunkHashes(hashes,h3_hashes,output,block);
return output;
}
}

View File

@ -0,0 +1,38 @@
package de.mas.jnus.lib.utils.download;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class Downloader {
public static byte[] downloadFileToByteArray(String fileURL) throws IOException {
int BUFFER_SIZE = 0x800;
URL url = new URL(fileURL);
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
int responseCode = httpConn.getResponseCode();
byte[] file = null;
if (responseCode == HttpURLConnection.HTTP_OK) {
int contentLength = httpConn.getContentLength();
file = new byte[contentLength];
InputStream inputStream = httpConn.getInputStream();
int bytesRead = -1;
byte[] buffer = new byte[BUFFER_SIZE];
int filePostion = 0;
while ((bytesRead = inputStream.read(buffer)) != -1) {
System.arraycopy(buffer, 0, file, filePostion,bytesRead);
filePostion+=bytesRead;
}
inputStream.close();
}else{
System.out.println("File not found: " + fileURL);
}
httpConn.disconnect();
return file;
}
}

View File

@ -0,0 +1,70 @@
package de.mas.jnus.lib.utils.download;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import de.mas.jnus.lib.Settings;
import lombok.Setter;
public class NUSDownloadService extends Downloader{
private static NUSDownloadService defaultInstance;
public static NUSDownloadService getDefaultInstance(){
if(defaultInstance == null){
defaultInstance = new NUSDownloadService();
defaultInstance.setURL_BASE(Settings.URL_BASE);
}
return defaultInstance;
}
public static NUSDownloadService getInstance(String URL){
NUSDownloadService instance = new NUSDownloadService();
instance.setURL_BASE(URL);
return instance;
}
private NUSDownloadService(){
}
@Setter private String URL_BASE = "";
public byte[] downloadTMDToByteArray(long titleID, int version) throws IOException {
String version_suf = "";
if(version > Settings.LATEST_TMD_VERSION) version_suf = "." + version;
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd" +version_suf;
return downloadFileToByteArray(URL);
}
public byte[] downloadTicketToByteArray(long titleID) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
return downloadFileToByteArray(URL);
}
public byte[] downloadToByteArray(String url) throws IOException {
String URL = URL_BASE + "/" + url;
return downloadFileToByteArray(URL);
}
public InputStream getInputStream(String URL,long offset) throws IOException{
URL url_obj = new URL(URL);
HttpURLConnection connection = (HttpURLConnection) url_obj.openConnection();
connection.setRequestProperty("Range", "bytes=" + offset +"-");
try{
connection.connect();
}catch(Exception e){
e.printStackTrace();
}
return connection.getInputStream();
}
public InputStream getInputStreamForURL(String url, long offset) throws IOException {
String URL = URL_BASE + "/" + url;
return getInputStream(URL,offset);
}
}