mirror of
https://github.com/Maschell/JNUSTool.git
synced 2025-01-08 06:10:45 +01:00
6dead58500
- printing more information about the title when loaded (encrypted/decrypted key,version,name,titleid) -version bump to 0.3
428 lines
14 KiB
Java
428 lines
14 KiB
Java
package de.mas.jnustool;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.File;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.concurrent.ForkJoinPool;
|
|
|
|
import javax.xml.parsers.DocumentBuilder;
|
|
import javax.xml.parsers.DocumentBuilderFactory;
|
|
import javax.xml.parsers.ParserConfigurationException;
|
|
|
|
import org.w3c.dom.Document;
|
|
import org.xml.sax.SAXException;
|
|
|
|
import de.mas.jnustool.util.Decryption;
|
|
import de.mas.jnustool.util.Downloader;
|
|
import de.mas.jnustool.util.NUSTitleInformation;
|
|
import de.mas.jnustool.util.Settings;
|
|
import de.mas.jnustool.util.Util;
|
|
|
|
public class NUSTitle {
|
|
private TitleMetaData tmd;
|
|
private TIK ticket;
|
|
private FST fst;
|
|
private long titleID;
|
|
private String targetPath = new String();
|
|
private String longNameFolder = new String();
|
|
private int version = -1;
|
|
|
|
|
|
private String getTMDName(){
|
|
String result = "title.tmd";
|
|
if(version > 0 && Settings.DL_ALL_VERSIONS){
|
|
result += "." + version;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public NUSTitle(long titleId,int version, String key) {
|
|
setVersion(version);
|
|
setTitleID(titleId);
|
|
if(version != -1){
|
|
Logger.log("Version " + version);
|
|
}
|
|
|
|
try {
|
|
/*
|
|
if(Settings.downloadContent){
|
|
File f = new File(getContentPath());
|
|
if(!f.exists())f.mkdir();
|
|
}
|
|
|
|
if(Settings.downloadContent){
|
|
|
|
File f = new File(getContentPath() + "/" + getTMDName());
|
|
if(!(f.exists() && Settings.skipExistingTMDTICKET)){
|
|
Logger.log("Downloading TMD");
|
|
Downloader.getInstance().downloadTMD(titleId,getContentPath());
|
|
}else{
|
|
Logger.log("Skipped download of TMD. Already existing");
|
|
}
|
|
|
|
f = new File(getContentPath() + "/" + "cetk");
|
|
if(!(f.exists() && Settings.skipExistingTMDTICKET)){
|
|
if(key == null){
|
|
Logger.log("Downloading Ticket");
|
|
Downloader.getInstance().downloadTicket(titleId,getContentPath());
|
|
}
|
|
}else{
|
|
Logger.log("Skipped download of ticket. Already existing");
|
|
}
|
|
}*/
|
|
|
|
if(Settings.useCachedFiles){
|
|
File f = new File(getContentPath() + "/" + getTMDName());
|
|
if(f.exists()){
|
|
Logger.log("Using cached TMD.");
|
|
tmd = new TitleMetaData(f);
|
|
}else{
|
|
Logger.log("No cached TMD found.");
|
|
}
|
|
}
|
|
if(tmd == null){
|
|
if(Settings.downloadWhenCachedFilesMissingOrBroken){
|
|
if(Settings.useCachedFiles) Logger.log("Getting missing tmd from Server!");
|
|
try{
|
|
tmd = new TitleMetaData(Downloader.getInstance().downloadTMDToByteArray(titleId,this.version));
|
|
}catch(IllegalArgumentException e){
|
|
Logger.log("TMD wrong. Title not found");
|
|
setTitleID(0);
|
|
return;
|
|
}
|
|
|
|
}else{
|
|
Logger.log("Downloading of missing files is not enabled. Exiting");
|
|
System.exit(2);
|
|
}
|
|
}
|
|
|
|
if(key != null){
|
|
Logger.log("Using ticket from parameter.");
|
|
ticket = new TIK(key,tmd.titleID);
|
|
}else{
|
|
if(Settings.useCachedFiles){
|
|
File f = new File(getContentPath() + "/" + "title.tik");
|
|
if(f.exists()){
|
|
Logger.log("Using cached cetk.");
|
|
ticket = new TIK(f,tmd.titleID);
|
|
}else{
|
|
Logger.log("No cached ticket found.");
|
|
}
|
|
}
|
|
if(ticket == null){
|
|
if(Settings.downloadWhenCachedFilesMissingOrBroken){
|
|
if(Settings.useCachedFiles) Logger.log("getting missing ticket");
|
|
ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(titleId),tmd.titleID);
|
|
}else{
|
|
Logger.log("Downloading of missing files is not enabled. Exiting");
|
|
System.exit(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*if(Settings.downloadContent){
|
|
File f = new File(getContentPath() + "/" + String.format("%08x", tmd.contents[0].ID) + ".app");
|
|
if(!(f.exists() && Settings.skipExistingFiles)){
|
|
Logger.log("Downloading FST (" + String.format("%08x", tmd.contents[0].ID) + ")");
|
|
Downloader.getInstance().downloadContent(titleId,tmd.contents[0].ID,getContentPath(),null);
|
|
}else{
|
|
if(f.length() != tmd.contents[0].size){
|
|
if(Settings.downloadWhenCachedFilesMissingOrBroken){
|
|
Logger.log("FST already existing, but broken. Downloading it again.");
|
|
Downloader.getInstance().downloadContent(titleId,tmd.contents[0].ID,getContentPath(),null);
|
|
}else{
|
|
Logger.log("FST already existing, but broken. No download allowed.");
|
|
System.exit(2);
|
|
}
|
|
}else{
|
|
Logger.log("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()){
|
|
Logger.log("Using cached FST");
|
|
Path file = Paths.get(path);
|
|
encryptedFST = Files.readAllBytes(file);
|
|
}else{
|
|
Logger.log("No cached FST (" + String.format("%08x", tmd.contents[0].ID) + ") found.");
|
|
}
|
|
}
|
|
if(encryptedFST == null){
|
|
if(Settings.downloadWhenCachedFilesMissingOrBroken){
|
|
if(Settings.useCachedFiles)Logger.log("Getting FST from server.");
|
|
encryptedFST = Downloader.getInstance().downloadContentToByteArray(titleId,tmd.contents[0].ID);
|
|
}else{
|
|
Logger.log("Downloading of missing files is not enabled. Exiting");
|
|
System.exit(2);
|
|
}
|
|
}
|
|
|
|
decryption.init(ticket.getDecryptedKey(),0);
|
|
byte[] decryptedFST = decryption.decrypt(encryptedFST);
|
|
|
|
|
|
/*FileOutputStream fw = new FileOutputStream ("fst123.dec");
|
|
fw.write(decryptedFST);
|
|
fw.close();*/
|
|
|
|
try{
|
|
fst = new FST(decryptedFST,tmd);
|
|
}catch(Exception e){
|
|
e.printStackTrace();
|
|
}
|
|
|
|
tmd.setNUSTitle(this);
|
|
|
|
setTargetPath(String.format("%016X", getTitleID()));
|
|
setLongNameFolder(String.format("%016X", getTitleID()));
|
|
|
|
|
|
|
|
if(fst != null && fst.metaFENtry != null){
|
|
byte[] metaxml = fst.metaFENtry.getAsByteArray();
|
|
if(metaxml != null){
|
|
try{
|
|
InputStream bis = new ByteArrayInputStream(metaxml);
|
|
NUSTitleInformation nusinfo = readMeta(bis);
|
|
if(nusinfo != null){
|
|
//String folder = nusinfo.getLongnameEN().replaceAll("[^\\x20-\\x7E]", "") + " [" + nusinfo.getID6() + "]";
|
|
String folder = nusinfo.getLongnameEN() + " [" + nusinfo.getID6() + "]";
|
|
String subfolder = "";
|
|
if(tmd.isUpdate()) subfolder = "/" + "updates" + "/" + "v" + tmd.titleVersion;
|
|
if(tmd.isDLC()) subfolder = "/" + String.format("aoc%016X", titleId);
|
|
setTargetPath(folder + subfolder);
|
|
setLongNameFolder(folder);
|
|
}
|
|
}catch(Exception e){
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if(Settings.downloadContent){
|
|
downloadEncryptedFiles(null);
|
|
}
|
|
Logger.log("");
|
|
Logger.log("Title \t\t: " + getLongNameFolder());
|
|
Logger.log("TitleID \t\t: " + String.format("%016X", tmd.titleID));
|
|
Logger.log("Version \t\t: " + String.format("%016X", tmd.version));
|
|
Logger.log("Encrypted Key \t\t: " + Util.ByteArrayToString(ticket.getEncryptedKey()));
|
|
Logger.log("Decrypted Key \t\t: " + Util.ByteArrayToString(ticket.getDecryptedKey()));
|
|
Logger.log("Total Size of Content Files\t: " + ((int)((getTotalContentSize()/1024.0/1024.0)*100))/100.0 +" MB");
|
|
if(fst != null)Logger.log("Total Size of Decrypted Files\t: " + ((int)((fst.getTotalContentSizeInNUS()/1024.0/1024.0)*100))/100.0 +" MB");
|
|
if(fst != null)Logger.log("Entries \t\t: " + fst.getTotalEntries());
|
|
if(fst != null)Logger.log("Files \t\t: " + fst.getFileCount());
|
|
if(fst != null)Logger.log("Files in NUS-Title \t: " + fst.getFileCountInNUS());
|
|
|
|
} catch (IOException e) {
|
|
// TODO Auto-generated catch block
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
public void downloadEncryptedFiles(Progress progress) throws IOException {
|
|
Util.createSubfolder(getContentPath());
|
|
|
|
Logger.log("---Downloading encrypted files---");
|
|
Downloader.getInstance().downloadTMD(titleID,version,getContentPath());
|
|
Logger.log("Downloaded title.tmd");
|
|
tmd.downloadContents(progress);
|
|
Logger.log("Downloaded content files");
|
|
|
|
File f = new File(getContentPath() + "/" + "title.tik");
|
|
if(!f.exists()){
|
|
try{
|
|
Downloader.getInstance().downloadTicket(titleID,getContentPath());
|
|
Logger.log("Downloaded title.tik");
|
|
}catch(Exception e){
|
|
|
|
}
|
|
}else{
|
|
Logger.log("Skipped title.tik");
|
|
}
|
|
|
|
f = new File(getContentPath() + "/" + "title.tik");
|
|
byte[] defaultcert = null;
|
|
|
|
Logger.log("Trying to create title.cert");
|
|
if(!f.exists() || Arrays.equals(ticket.cert1, new byte[0x300])){
|
|
try{
|
|
defaultcert = Util.getDefaultCert();
|
|
}catch(Exception e){
|
|
Logger.log("Failed to get missing cert from OSv10 cetk =(. Couldn't create title.cert");
|
|
e.printStackTrace();
|
|
return;
|
|
}
|
|
Logger.log("Got missing cert from OSv10 title");
|
|
}else{
|
|
|
|
defaultcert = ticket.cert1;
|
|
}
|
|
|
|
try{
|
|
FileOutputStream fos = new FileOutputStream(getContentPath() + "/title.cert");
|
|
fos.write(tmd.cert1);
|
|
fos.write(tmd.cert2);
|
|
fos.write(defaultcert);
|
|
fos.close();
|
|
if(version > 0 && Settings.DL_ALL_VERSIONS){
|
|
fos = new FileOutputStream(getContentPath() + "/title.cert." + version);
|
|
fos.write(tmd.cert1);
|
|
fos.write(tmd.cert2);
|
|
fos.write(defaultcert);
|
|
fos.close();
|
|
}
|
|
}catch(Exception e){
|
|
e.printStackTrace();
|
|
Logger.log("Error while creating ticket files.");
|
|
}
|
|
Logger.log("Created title.cert");
|
|
Logger.log("---Successfully downloaded encrypted files---");
|
|
}
|
|
|
|
public NUSTitleInformation readMeta(InputStream bis) {
|
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
|
DocumentBuilder builder;
|
|
|
|
String ID6 = null;
|
|
try {
|
|
builder = factory.newDocumentBuilder();
|
|
Document document = builder.parse(bis);
|
|
String proc = document.getElementsByTagName("product_code").item(0).getTextContent().toString();
|
|
String comp = document.getElementsByTagName("company_code").item(0).getTextContent().toString();
|
|
String title_id = document.getElementsByTagName("title_id").item(0).getTextContent().toString();
|
|
|
|
String longname = document.getElementsByTagName("longname_en").item(0).getTextContent().toString();
|
|
longname = longname.replace("\n", " ");
|
|
String id = proc.substring(proc.length()-4, proc.length());
|
|
comp = comp.substring(comp.length()-2, comp.length());
|
|
ID6 = id+comp;
|
|
String company_code = document.getElementsByTagName("company_code").item(0).getTextContent().toString();
|
|
String content_platform = document.getElementsByTagName("content_platform").item(0).getTextContent().toString();
|
|
String region = document.getElementsByTagName("region").item(0).getTextContent().toString();
|
|
NUSTitleInformation nusinfo = new NUSTitleInformation(Util.StringToLong(title_id),longname,ID6,proc,content_platform,company_code,(int) Util.StringToLong(region),new String[1]);
|
|
return nusinfo;
|
|
|
|
} catch (ParserConfigurationException | SAXException | IOException e) {
|
|
Logger.log("Error while parsing the meta files");
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
public FST getFst() {
|
|
return fst;
|
|
}
|
|
|
|
public void setFst(FST fst) {
|
|
this.fst = fst;
|
|
}
|
|
|
|
public TitleMetaData getTmd() {
|
|
return tmd;
|
|
}
|
|
|
|
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() {
|
|
String result = getContentPathPrefix() + String.format("%016X", getTitleID());
|
|
if(version > 0 && !Settings.DL_ALL_VERSIONS){ //Only add the prefix when we don't download all version of that title
|
|
result += "_v" + version;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public String getContentPathPrefix() {
|
|
return "tmp_";
|
|
}
|
|
|
|
public long getTitleID() {
|
|
return titleID;
|
|
}
|
|
|
|
private void setTitleID(long titleId) {
|
|
this.titleID = titleId;
|
|
}
|
|
|
|
public void decryptFEntries(List<FEntry> list,Progress progress) {
|
|
Util.createSubfolder(getTargetPath());
|
|
//progress = null;
|
|
ForkJoinPool pool = new ForkJoinPool(25);
|
|
List<FEntryDownloader> dlList = new ArrayList<>();
|
|
for(FEntry f : list){
|
|
if(f.isInNUSTitle()){
|
|
dlList.add(new FEntryDownloader(f,progress));
|
|
}
|
|
}
|
|
pool.invokeAll(dlList);
|
|
if(tmd.isUpdate()) Util.buildFileList(getTargetPath(),"content",Settings.FILELIST_NAME);
|
|
Logger.log("Done!");
|
|
}
|
|
|
|
public void setTargetPath(String path){
|
|
path = Util.replaceCharsInString(path);
|
|
this.targetPath = path;
|
|
}
|
|
|
|
public String getTargetPath() {
|
|
return this.targetPath;
|
|
}
|
|
|
|
public String getLongNameFolder() {
|
|
return longNameFolder;
|
|
}
|
|
|
|
public void setLongNameFolder(String path) {
|
|
path = Util.replaceCharsInString(path);
|
|
longNameFolder = path;
|
|
}
|
|
|
|
public int getVersion() {
|
|
return version;
|
|
}
|
|
|
|
public void setVersion(int version) {
|
|
this.version = version;
|
|
}
|
|
|
|
/* Just for testing ~
|
|
public void decryptContent(){
|
|
System.out.println(tmd.getContentPath() + "/" + "00000004.app");
|
|
FileInputStream in = new FileInputStream(new File(tmd.getContentPath() + "/" + "00000004.app"));
|
|
FileOutputStream out = new FileOutputStream(new File(tmd.getContentPath() + "/" + "00000004.dec"));
|
|
decryption.decryptContentHash(in,out,tmd.contents[0x4],Files.readAllBytes(new File(tmd.getContentPath() + "/" + "00000004.h3").toPath()));
|
|
}*/
|
|
}
|