Added a lot of things!

Added a settings class! (no runtime configuration atm)
added decryption from files
added download to files
This commit is contained in:
Maschell 2016-02-02 22:55:33 +01:00
parent eb5657ebf6
commit 0e03adf10f
11 changed files with 394 additions and 77 deletions

View File

@ -4,7 +4,10 @@ import java.io.File;
import java.io.IOException;
import java.util.List;
import de.mas.jnustool.util.Decryption;
import de.mas.jnustool.util.Downloader;
import de.mas.jnustool.util.ExitException;
import de.mas.jnustool.util.Settings;
public class FEntry {
private FST fst;
@ -124,7 +127,7 @@ public class FEntry {
}
private void createFolder() {
long titleID = fst.getTmd().titleID;
long titleID = getTitleID();
String [] path = getFullPath().split("/");
File f = new File (String.format("%016X", titleID));
if(!f.exists())f.mkdir();
@ -140,20 +143,71 @@ public class FEntry {
}
}
}
f = new File(String.format("%016X", titleID) +"/" +getFullPath().substring(1, getFullPath().length()));
}
public String getDownloadPath(){
String [] path = getFullPath().split("/");
String folder = String.format("%016X", getTitleID()) +"/";
for(int i = 0;i<path.length-1;i++){
if(!path[i].equals("")){
folder += path[i] + "/";
}
}
return folder;
}
public void downloadAndDecrypt() throws ExitException {
createFolder();
long titleID = getTitleID();
File f = new File(String.format("%016X", titleID) +"/" +getFullPath().substring(1, getFullPath().length()));
if(f.exists()){
if(f.length() == getFileLength()){
System.out.println("Skipping: " + String.format("%8.2f MB ",getFileLength()/1024.0/1024.0) + getFullPath());
return;
}
}
}
public void downloadAndDecrypt() {
System.out.println("Downloading: " + String.format("%8.2f MB ", getFileLength()/1024.0/1024.0) + getFullPath());
try {
if(Settings.useCachedFiles){
f = new File(getContentPath());
if(f.exists()){
if(f.length() == fst.getTmd().contents[this.getContentID()].size){
System.out.println("Decrypting: " + String.format("%8.2f MB ", getFileLength()/1024.0/1024.0) + getFullPath());
Decryption decrypt = new Decryption(fst.getTmd().getNUSTitle().getTicket());
decrypt.decrypt(this,getDownloadPath());
return;
}else{
if(!Settings.downloadWhenCachedFilesMissingOrBroken){
System.out.println("Cached content has the wrong size! Please check your: "+ getContentPath() + " Downloading not allowed");
if(!Settings.skipBrokenFiles){
throw new ExitException("");
}else{
System.out.println("Ignoring the missing file: " + this.getFileName());
}
}else{
System.out.println("Content missing. Downloading the file from the server: " + this.getFileName());
}
}
}else{
if(!Settings.downloadWhenCachedFilesMissingOrBroken){
System.out.println("Content missing. Downloading not allowed");
if(!Settings.skipBrokenFiles){
throw new ExitException("");
}else{
System.out.println("Ignoring the missing file: " + this.getFileName());
}
}else{
System.out.println("Content missing. Downloading the file from the server: " + this.getFileName());
}
}
}
System.out.println("Downloading: " + String.format("%8.2f MB ", getFileLength()/1024.0/1024.0) + getFullPath());
Downloader.getInstance().downloadAndDecrypt(this);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
@ -168,6 +222,18 @@ public class FEntry {
this.pathList = pathList;
}
public String getContentPath() {
return fst.getTmd().getContentPath() + "/" + String.format("%08X", getNUScontentID()) + ".app";
}
public long getTitleID() {
return fst.getTmd().titleID;
}
public TIK getTicket() {
return fst.getTmd().getNUSTitle().getTicket();
}

View File

@ -2,14 +2,13 @@ package de.mas.jnustool;
import java.util.concurrent.Callable;
public class TitleDownloader implements Callable<Integer>
public class FEntryDownloader implements Callable<Integer>
{
FEntry f;
public void setTitle(FEntry f){
this.f = f;
}
public TitleDownloader(FEntry f){
public FEntryDownloader(FEntry f){
setTitle(f);
}

View File

@ -1,40 +1,144 @@
package de.mas.jnustool;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import de.mas.jnustool.util.Decryption;
import de.mas.jnustool.util.Downloader;
import de.mas.jnustool.util.Util;
import de.mas.jnustool.util.ExitException;
import de.mas.jnustool.util.Settings;
public class NUSTitle {
private TitleMetaData tmd;
private long titleID;
private TIK ticket;
private FST fst;
public NUSTitle(long titleId,String key){
private long titleID;
public NUSTitle(long titleId,String key) throws ExitException{
setTitleID(titleId);
try {
titleID = titleId;
Downloader.getInstance().titleID = titleId;
Decryption decryption = new Decryption(Util.commonKey,titleId);
tmd = new TitleMetaData(Downloader.getInstance().downloadTMDToByteArray());
if(Settings.downloadContent){
File f = new File(getContentPath());
if(!f.exists())f.mkdir();
}
if(Settings.downloadContent){
if(key == null){
ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(),tmd.titleID);
}else{
ticket = new TIK(key,titleId);
File f = new File(getContentPath() + "/" + "tmd");
if(!(f.exists() && Settings.skipExistingTMDTICKET)){
System.out.println("Downloading TMD");
Downloader.getInstance().downloadTMD(titleId,getContentPath());
}else{
System.out.println("Skipped download of TMD. Already existing");
}
f = new File(getContentPath() + "/" + "cetk");
if(!(f.exists() && Settings.skipExistingTMDTICKET)){
if(key == null){
System.out.print("Downloading Ticket");
Downloader.getInstance().downloadTicket(titleId,getContentPath());
}
}else{
System.out.println("Skipped download of ticket. Already existing");
}
}
if(Settings.useCachedFiles){
File f = new File(getContentPath() + "/" + "tmd");
if(f.exists()){
System.out.println("Using cached TMD.");
tmd = new TitleMetaData(f);
}else{
System.out.println("No cached TMD found.");
}
}
Downloader.getInstance().ticket = ticket;
decryption.init(ticket.getDecryptedKey(),0);
byte[] encryptedFST = Downloader.getInstance().downloadContentToByteArray(tmd.contents[0].ID);
if(tmd == null){
if(Settings.downloadWhenCachedFilesMissingOrBroken){
if(Settings.useCachedFiles) System.out.println("Getting missing tmd from Server!");
tmd = new TitleMetaData(Downloader.getInstance().downloadTMDToByteArray(titleId));
}else{
System.out.println("Downloading of missing files is not enabled. Exiting");
throw new ExitException("TMD missing.");
}
}
if(key != null){
System.out.println("Using ticket from parameter.");
ticket = new TIK(key,titleId);
}else{
if(Settings.useCachedFiles){
File f = new File(getContentPath() + "/" + "cetk");
if(f.exists()){
System.out.println("Using cached cetk.");
ticket = new TIK(f,titleId);
}else{
System.out.println("No cached ticket found.");
}
}
if(ticket == null){
if(Settings.downloadWhenCachedFilesMissingOrBroken){
if(Settings.useCachedFiles) System.out.println("getting missing ticket");
ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(titleId),tmd.titleID);
}else{
System.out.println("Downloading of missing files is not enabled. Exiting");
throw new ExitException("Ticket missing.");
}
}
}
if(Settings.downloadContent){
File f = new File(getContentPath() + "/" + String.format("%08x", tmd.contents[0].ID) + ".app");
if(!(f.exists() && Settings.skipExistingFiles)){
System.out.println("Downloading FST (" + String.format("%08x", tmd.contents[0].ID) + ")");
Downloader.getInstance().downloadContent(titleId,tmd.contents[0].ID,getContentPath());
}else{
if(f.length() != tmd.contents[0].size){
if(Settings.downloadWhenCachedFilesMissingOrBroken){
System.out.println("FST already existing, but broken. Downloading it again.");
Downloader.getInstance().downloadContent(titleId,tmd.contents[0].ID,getContentPath());
}else{
System.out.println("FST already existing, but broken. No download allowed.");
throw new ExitException("FST missing.");
}
}else{
System.out.println("Skipped download of FST. Already existing");
}
}
}
Decryption decryption = new Decryption(ticket.getDecryptedKey(),0);
byte[] encryptedFST = null;
if(Settings.useCachedFiles){
String path = getContentPath() + "/" + String.format("%08x", tmd.contents[0].ID) + ".app";
File f = new File(path);
if(f.exists()){
System.out.println("Using cached FST");
Path file = Paths.get(path);
encryptedFST = Files.readAllBytes(file);
}else{
System.out.println("No cached FST (" + String.format("%08x", tmd.contents[0].ID) + ") found.");
}
}
if(encryptedFST == null){
if(Settings.downloadWhenCachedFilesMissingOrBroken){
if(Settings.useCachedFiles)System.out.println("Getting FST from server.");
encryptedFST = Downloader.getInstance().downloadContentToByteArray(titleId,tmd.contents[0].ID);
}else{
System.out.println("Downloading of missing files is not enabled. Exiting");
throw new ExitException("");
}
}
byte[] decryptedFST = decryption.decrypt(encryptedFST);
fst = new FST(decryptedFST,tmd);
tmd.setFst(fst);
tmd.setNUSTitle(this);
if(Settings.downloadContent){
tmd.downloadContents();
}
System.out.println("Total Size of Content Files: " + ((int)((getTotalContentSize()/1024.0/1024.0)*100))/100.0 +" MB");
System.out.println("Total Size of Decrypted Files: " + ((int)((fst.getTotalContentSizeInNUS()/1024.0/1024.0)*100))/100.0 +" MB");
@ -49,26 +153,61 @@ public class NUSTitle {
}
public FST getFst() {
return fst;
}
public void setFst(FST fst) {
this.fst = fst;
}
public long getTitleID() {
return titleID;
public TitleMetaData getTmd() {
return tmd;
}
public void setTitleID(long titleID) {
this.titleID = titleID;
public void setTmd(TitleMetaData tmd) {
this.tmd = tmd;
}
public TIK getTicket() {
return ticket;
}
public void setTicket(TIK ticket) {
this.ticket = ticket;
}
public long getTotalContentSize() {
return tmd.getTotalContentSize();
}
public String getContentPath() {
return getContentPathPrefix() + String.format("%016X", getTitleID());
}
public String getContentPathPrefix() {
return "tmp_";
}
private long getTitleID() {
return titleID;
}
private void setTitleID(long titleId) {
this.titleID = titleId;
}
}

View File

@ -1,6 +0,0 @@
package de.mas.jnustool;
public class Settings {
}

View File

@ -7,6 +7,7 @@ import java.io.IOException;
import de.mas.jnustool.gui.NUSGUI;
import de.mas.jnustool.util.Downloader;
import de.mas.jnustool.util.ExitException;
import de.mas.jnustool.util.Util;
public class Starter {
@ -28,7 +29,13 @@ public class Starter {
if( args.length > 1 && args[1].length() == 32){
key = args[1].substring(0, 32);
}
NUSGUI m = new NUSGUI(new NUSTitle(titleID, key), null);
NUSGUI m;
try {
m = new NUSGUI(new NUSTitle(titleID, key), null);
} catch (ExitException e) {
System.out.println("Error: " + e.getMessage());
return;
}
m.setVisible(true);
}else{
System.out.println("Need parameters: TITLEID [KEY]");

View File

@ -3,8 +3,18 @@ package de.mas.jnustool;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import de.mas.jnustool.util.Downloader;
import de.mas.jnustool.util.ExitException;
import de.mas.jnustool.util.Settings;
import de.mas.jnustool.util.Util;
public class TitleMetaData {
@ -27,7 +37,8 @@ public class TitleMetaData {
ContentInfo[] contentInfos = new ContentInfo[64]; // 0x1E4
Content[] contents; // 0x1E4
private FST fst;
private NUSTitle nus;
private long totalContentSize;
@ -105,8 +116,7 @@ public class TitleMetaData {
type = f.readShort();
size = f.readLong();
byte[] buffer = new byte[0x20]; // 16 0xB14
f.read(buffer,0, 0x20);
f.read(buffer,0, 0x20);
this.contents[i] = new Content(ID,index,type,size,buffer);
}
@ -152,13 +162,50 @@ public class TitleMetaData {
public long getTotalContentSize() {
return totalContentSize;
}
public FST getFst() {
return fst;
public void downloadContents() throws IOException, ExitException{
String tmpPath = getContentPath();
File f = new File(tmpPath);
if(!f.exists())f.mkdir();
for(Content c : contents){
if(c != contents[0]){
f = new File(tmpPath + "/" + String.format("%08X", c.ID ) + ".app");
if(f.exists()){
if(f.length() == c.size){
System.out.println("Skipping Content: " + String.format("%08X", c.ID));
}else{
if(Settings.downloadWhenCachedFilesMissingOrBroken){
System.out.println("Content " +String.format("%08X", c.ID) + " is broken. Downloading it again.");
Downloader.getInstance().downloadContent(titleID,c.ID,tmpPath);
}else{
if(Settings.skipBrokenFiles){
System.out.println("Content " +String.format("%08X", c.ID) + " is broken. Ignoring it.");
}else{
System.out.println("Content " +String.format("%08X", c.ID) + " is broken. Downloading not allowed.");
throw new ExitException("Content missing.");
}
}
}
}else{
System.out.println("Download Content: " + String.format("%08X", c.ID));
Downloader.getInstance().downloadContent(titleID,c.ID,tmpPath);
}
}
}
}
public void setFst(FST fst) {
this.fst = fst;
}
public String getContentPath() {
return nus.getContentPath();
}
public NUSTitle getNUSTitle() {
return nus;
}
public void setNUSTitle(NUSTitle nus) {
this.nus = nus;
}
}

View File

@ -13,10 +13,10 @@ import javax.swing.JScrollPane;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import de.mas.jnustool.Settings;
import de.mas.jnustool.FEntry;
import de.mas.jnustool.NUSTitle;
import de.mas.jnustool.TitleDownloader;
import de.mas.jnustool.util.Settings;
import de.mas.jnustool.FEntryDownloader;
public class NUSGUI extends JFrame {
@ -39,16 +39,19 @@ public class NUSGUI extends JFrame {
public void actionPerformed(ActionEvent e) {
new Thread(new Runnable() { public void run() {
ForkJoinPool pool = ForkJoinPool.commonPool();
List<TitleDownloader> list = new ArrayList<>();
List<FEntryDownloader> list = new ArrayList<>();
TreePath[] paths = cbt.getCheckedPaths();
for (TreePath tp : paths) {
Object obj = tp.getPath()[tp.getPath().length-1];
if(((DefaultMutableTreeNode)obj).getUserObject() instanceof FEntry){
FEntry f = (FEntry) ((DefaultMutableTreeNode)obj).getUserObject();
if(!f.isDir() && f.isInNUSTitle())
list.add(new TitleDownloader(f));
if(!f.isDir() && f.isInNUSTitle()){
list.add(new FEntryDownloader(f));
}
}
}
pool.invokeAll(list);

View File

@ -2,6 +2,7 @@ package de.mas.jnustool.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -333,6 +334,34 @@ public class Decryption {
inputSteam.close();
}
public void decrypt(FEntry fileEntry,String outputPath) throws IOException {
String [] path = fileEntry.getFullPath().split("/");
boolean decryptWithHash = false;
if(!path[1].equals("code") && fileEntry.isExtractWithHash()){
decryptWithHash = true;
}
long fileOffset = fileEntry.getFileOffset();
if(decryptWithHash){
int BLOCKSIZE = 0x10000;
int HASHBLOCKSIZE = 0xFC00;
fileOffset = ((fileEntry.getFileOffset() / HASHBLOCKSIZE) * BLOCKSIZE);
}
InputStream input = new FileInputStream(fileEntry.getContentPath());
FileOutputStream outputStream = new FileOutputStream(outputPath + "/" + fileEntry.getFileName());
input.skip(fileOffset);
if(!decryptWithHash){
decryptFile(input, outputStream, fileEntry);
}else{
decryptFileHash(input, outputStream, fileEntry);
}
}

View File

@ -1,6 +1,5 @@
package de.mas.jnustool.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -8,7 +7,6 @@ import java.net.HttpURLConnection;
import java.net.URL;
import de.mas.jnustool.FEntry;
import de.mas.jnustool.TIK;
public class Downloader {
private static Downloader instance;
@ -23,11 +21,9 @@ public class Downloader {
}
public long titleID =0;
public TIK ticket = null;
public void downloadAndDecrypt(FEntry toDownload) throws IOException{
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", toDownload.getNUScontentID());
String URL = URL_BASE + "/" + String.format("%016X", toDownload.getTitleID()) + "/" + String.format("%08X", toDownload.getNUScontentID());
URL url = new URL(URL);
String [] path = toDownload.getFullPath().split("/");
boolean decryptWithHash = false;
@ -48,10 +44,10 @@ public class Downloader {
connection.connect();
Decryption decryption = new Decryption(ticket);
Decryption decryption = new Decryption(toDownload.getTicket());
InputStream input = connection.getInputStream();
FileOutputStream outputStream = new FileOutputStream(String.format("%016X", titleID) +"/" + toDownload.getFullPath().substring(1, toDownload.getFullPath().length()));
FileOutputStream outputStream = new FileOutputStream(String.format("%016X", toDownload.getTitleID()) +"/" + toDownload.getFullPath().substring(1, toDownload.getFullPath().length()));
if(!decryptWithHash){
decryption.decryptFile(input, outputStream, toDownload);
}else{
@ -63,20 +59,23 @@ public class Downloader {
public static String URL_BASE = "";
public void downloadTMD(int version) throws IOException {
downloadTMD();
public void downloadTMD(long titleID,int version,String path) throws IOException {
downloadTMD(titleID,path);
}
public void downloadTMD() throws IOException {
public void downloadTMD(long titleID,String path) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd";
downloadFile(URL, "tmd");
downloadFile(URL, "tmd",path);
}
public void downloadFile(String fileURL,String filename) throws IOException{
public void downloadFile(String fileURL,String filename,String tmpPath) throws IOException{
int BUFFER_SIZE = 0x800;
URL url = new URL(fileURL);
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
InputStream inputStream = httpConn.getInputStream();
if(tmpPath != null){
filename = tmpPath + "/" + filename;
}
FileOutputStream outputStream = new FileOutputStream(filename);
int bytesRead = -1;
@ -90,20 +89,22 @@ public class Downloader {
httpConn.disconnect();
}
public void downloadTicket() throws IOException {
public void downloadFile(String fileURL,String filename) throws IOException{
downloadFile(fileURL, filename,null);
}
public void downloadTicket(long titleID,String path) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
downloadFile(URL, "cetk");
downloadFile(URL, "cetk",path);
}
public void downloadContent(int contentID) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID);
downloadFile(URL, String.format("%08X", contentID));
public void downloadContent(long titleID,int contentID) throws IOException {
downloadContent(titleID,contentID, null);
}
public byte[] downloadContentToByteArray(int contentID) throws IOException {
public byte[] downloadContentToByteArray(long titleID,int contentID) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID);
return downloadFileToByteArray(URL);
}
public byte[] downloadTMDToByteArray() throws IOException {
public byte[] downloadTMDToByteArray(long titleID) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd";
return downloadFileToByteArray(URL);
}
@ -141,9 +142,15 @@ public class Downloader {
return file;
}
public byte[] downloadTicketToByteArray() throws IOException {
public byte[] downloadTicketToByteArray(long titleID) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
return downloadFileToByteArray(URL);
}
public void downloadContent(long titleID,int contentID, String tmpPath) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID);
downloadFile(URL, String.format("%08X", contentID) +".app",tmpPath);
}
}

View File

@ -0,0 +1,14 @@
package de.mas.jnustool.util;
public class ExitException extends Exception {
public ExitException(String string) {
super(string);
}
/**
*
*/
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,12 @@
package de.mas.jnustool.util;
public class Settings {
public static boolean downloadContent = false;
public static boolean useCachedFiles = false;
public static boolean downloadWhenCachedFilesMissingOrBroken = true;
public static boolean skipBrokenFiles = false;
public static boolean skipExistingFiles = true;
public static boolean skipExistingTMDTICKET = true;
}