mirror of
https://github.com/Maschell/JNUSLib.git
synced 2024-11-26 01:44:17 +01:00
first commit, have fun
This commit is contained in:
parent
26d41a1611
commit
a75fe2e6bd
7
.classpath
Normal file
7
.classpath
Normal 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
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/bin/
|
17
.project
Normal file
17
.project
Normal 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
BIN
libs/lombok.jar
Normal file
Binary file not shown.
336
src/de/mas/jnus/lib/DecryptionService.java
Normal file
336
src/de/mas/jnus/lib/DecryptionService.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
125
src/de/mas/jnus/lib/ExtractionService.java
Normal file
125
src/de/mas/jnus/lib/ExtractionService.java
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
103
src/de/mas/jnus/lib/NUSTitle.java
Normal file
103
src/de/mas/jnus/lib/NUSTitle.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
18
src/de/mas/jnus/lib/NUSTitleConfig.java
Normal file
18
src/de/mas/jnus/lib/NUSTitleConfig.java
Normal 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;
|
||||||
|
}
|
67
src/de/mas/jnus/lib/NUSTitleLoader.java
Normal file
67
src/de/mas/jnus/lib/NUSTitleLoader.java
Normal 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);
|
||||||
|
}
|
35
src/de/mas/jnus/lib/NUSTitleLoaderLocal.java
Normal file
35
src/de/mas/jnus/lib/NUSTitleLoaderLocal.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
src/de/mas/jnus/lib/NUSTitleLoaderRemote.java
Normal file
43
src/de/mas/jnus/lib/NUSTitleLoaderRemote.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
49
src/de/mas/jnus/lib/NUSTitleLoaderWUD.java
Normal file
49
src/de/mas/jnus/lib/NUSTitleLoaderWUD.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
33
src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java
Normal file
33
src/de/mas/jnus/lib/NUSTitleLoaderWoomy.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
src/de/mas/jnus/lib/Settings.java
Normal file
18
src/de/mas/jnus/lib/Settings.java
Normal 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;
|
||||||
|
}
|
176
src/de/mas/jnus/lib/WUDService.java
Normal file
176
src/de/mas/jnus/lib/WUDService.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
167
src/de/mas/jnus/lib/entities/TMD.java
Normal file
167
src/de/mas/jnus/lib/entities/TMD.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
src/de/mas/jnus/lib/entities/Ticket.java
Normal file
122
src/de/mas/jnus/lib/entities/Ticket.java
Normal 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) + "]";
|
||||||
|
}
|
||||||
|
}
|
149
src/de/mas/jnus/lib/entities/content/Content.java
Normal file
149
src/de/mas/jnus/lib/entities/content/Content.java
Normal 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) + "]";
|
||||||
|
}
|
||||||
|
}
|
85
src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java
Normal file
85
src/de/mas/jnus/lib/entities/content/ContentFSTInfo.java
Normal 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 + "]";
|
||||||
|
}
|
||||||
|
}
|
65
src/de/mas/jnus/lib/entities/content/ContentInfo.java
Normal file
65
src/de/mas/jnus/lib/entities/content/ContentInfo.java
Normal 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) + "]";
|
||||||
|
}
|
||||||
|
}
|
81
src/de/mas/jnus/lib/entities/fst/FST.java
Normal file
81
src/de/mas/jnus/lib/entities/fst/FST.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
165
src/de/mas/jnus/lib/entities/fst/FSTEntry.java
Normal file
165
src/de/mas/jnus/lib/entities/fst/FSTEntry.java
Normal 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 + "]";
|
||||||
|
}
|
||||||
|
}
|
137
src/de/mas/jnus/lib/entities/fst/FSTService.java
Normal file
137
src/de/mas/jnus/lib/entities/fst/FSTService.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
115
src/de/mas/jnus/lib/implementations/NUSDataProvider.java
Normal file
115
src/de/mas/jnus/lib/implementations/NUSDataProvider.java
Normal 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;
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
102
src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java
Normal file
102
src/de/mas/jnus/lib/implementations/NUSDataProviderWUD.java
Normal 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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
14
src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java
Normal file
14
src/de/mas/jnus/lib/implementations/woomy/WoomyInfo.java
Normal 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;
|
||||||
|
}
|
39
src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java
Normal file
39
src/de/mas/jnus/lib/implementations/woomy/WoomyMeta.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
81
src/de/mas/jnus/lib/implementations/woomy/WoomyParser.java
Normal file
81
src/de/mas/jnus/lib/implementations/woomy/WoomyParser.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
35
src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java
Normal file
35
src/de/mas/jnus/lib/implementations/woomy/WoomyZipFile.java
Normal 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
src/de/mas/jnus/lib/implementations/wud/WUDImage.java
Normal file
108
src/de/mas/jnus/lib/implementations/wud/WUDImage.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
41
src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java
Normal file
41
src/de/mas/jnus/lib/implementations/wud/parser/WUDInfo.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
src/de/mas/jnus/lib/utils/ByteArrayBuffer.java
Normal file
25
src/de/mas/jnus/lib/utils/ByteArrayBuffer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
33
src/de/mas/jnus/lib/utils/ByteArrayWrapper.java
Normal file
33
src/de/mas/jnus/lib/utils/ByteArrayWrapper.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
76
src/de/mas/jnus/lib/utils/ByteUtils.java
Normal file
76
src/de/mas/jnus/lib/utils/ByteUtils.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
52
src/de/mas/jnus/lib/utils/FileUtils.java
Normal file
52
src/de/mas/jnus/lib/utils/FileUtils.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
185
src/de/mas/jnus/lib/utils/HashUtil.java
Normal file
185
src/de/mas/jnus/lib/utils/HashUtil.java
Normal 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
src/de/mas/jnus/lib/utils/StreamUtils.java
Normal file
127
src/de/mas/jnus/lib/utils/StreamUtils.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
66
src/de/mas/jnus/lib/utils/Utils.java
Normal file
66
src/de/mas/jnus/lib/utils/Utils.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
77
src/de/mas/jnus/lib/utils/XMLParser.java
Normal file
77
src/de/mas/jnus/lib/utils/XMLParser.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
71
src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java
Normal file
71
src/de/mas/jnus/lib/utils/cryptography/AESDecryption.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
171
src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java
Normal file
171
src/de/mas/jnus/lib/utils/cryptography/NUSDecryption.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
38
src/de/mas/jnus/lib/utils/download/Downloader.java
Normal file
38
src/de/mas/jnus/lib/utils/download/Downloader.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
70
src/de/mas/jnus/lib/utils/download/NUSDownloadService.java
Normal file
70
src/de/mas/jnus/lib/utils/download/NUSDownloadService.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user