First commit =)

This commit is contained in:
Maschell 2016-02-01 20:54:01 +01:00
parent 910eaf340c
commit d83f486fa3
17 changed files with 1354 additions and 0 deletions

6
.classpath Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="output" path="bin"/>
</classpath>

3
.gitignore vendored
View File

@ -45,3 +45,6 @@ $RECYCLE.BIN/
Network Trash Folder
Temporary Items
.apdisk
0*
bin
config

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>JNusTool</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>

View File

@ -0,0 +1,2 @@
eclipse.preferences.version=1
encoding//src/Util.java=UTF-8

View File

@ -0,0 +1,11 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.8

22
src/Content.java Normal file
View File

@ -0,0 +1,22 @@
public class Content {
int ID; // 0 0xB04
short index; // 4 0xB08
short type; // 6 0xB0A
long size; // 8 0xB0C
byte[] SHA2 = new byte[32]; // 16 0xB14
public Content(int ID, short index, short type, long size, byte[] SHA2) {
this.ID = ID;
this.index = index;
this.type = type;
this.size = size;
this.SHA2 = SHA2;
}
@Override
public String toString(){
return "ID: " + ID +" index: " + index + " type: " + type + " size: " + size + " SHA2: " + Util.ByteArrayToString(SHA2);
}
}

29
src/ContentInfo.java Normal file
View File

@ -0,0 +1,29 @@
public class ContentInfo {
public short indexOffset; // 0 0x204
public short commandCount; // 2 0x206
public byte[] SHA2 = new byte[32]; // 12 0x208
//TODO: Test, size checking
/*
* untested
*/
public ContentInfo(byte[] info){
this.indexOffset=(short)( ((info[0]&0xFF)<<8) | (info[1]&0xFF) );
this.commandCount=(short)( ((info[2]&0xFF)<<8) | (info[3]&0xFF) );
for(int i = 0;i<32;i++){
this.SHA2[i] = info[4+i];
}
}
public ContentInfo(short indexOffset, short commandCount, byte[] SHA2) {
this.indexOffset = indexOffset;
this.commandCount = commandCount;
this.SHA2 = SHA2;
}
@Override
public String toString(){
return "indexOffset: " + indexOffset +" commandCount: " + commandCount + " SHA2: " + Util.ByteArrayToString(SHA2);
}
}

158
src/Decryption.java Normal file
View File

@ -0,0 +1,158 @@
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
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;
public class Decryption {
Cipher cipher2;
public Decryption(TIK ticket){
this(ticket.getDecryptedKey());
}
public Decryption(byte[] decryptedKey){
this(decryptedKey,0);
}
public Decryption(byte[] decryptedKey, long titleId) {
try {
cipher2 = Cipher.getInstance("AES/CBC/NoPadding");
this.decryptedKey = decryptedKey;
init(titleId);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
byte[] decryptedKey;
private void init(byte[] IV) {
init(decryptedKey,IV);
}
private void init(long titleid) {
init(ByteBuffer.allocate(16).putLong(titleid).array());
}
public void init(byte[] decryptedKey,long titleid){
init(decryptedKey,ByteBuffer.allocate(16).putLong(titleid).array());
}
public void init(byte[] decryptedKey,byte[] iv){
try {
this.decryptedKey = decryptedKey;
SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES");
cipher2.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public byte[] decrypt(byte[] input){
try {
return cipher2.doFinal(input);
} catch (IllegalBlockSizeException | BadPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
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 cipher2.doFinal(input, offset, len);
} catch (IllegalBlockSizeException | BadPaddingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return input;
}
byte[] IV;
public byte[] decryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) {
return decryptFileChunk(blockBuffer,0,BLOCKSIZE, IV);
}
public byte[] decryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) {
if(IV != null) this.IV = IV;
init(this.IV);
byte[] output = decrypt(blockBuffer,offset,BLOCKSIZE);
this.IV = Arrays.copyOfRange(blockBuffer,BLOCKSIZE-16, BLOCKSIZE);
return output;
}
byte[] hash = new byte[20];
byte[] h0 = new byte[20];
public byte[] decryptFileChunkHash(byte[] blockBuffer, int BLOCKSIZE, int block, int contentID){
if(BLOCKSIZE != 0x10000) throw new IllegalArgumentException("Blocksize not supported");
IV = new byte[16];
IV[1] = (byte)contentID;
byte[] hashes = decryptFileChunk(blockBuffer,0x0400,IV);
System.arraycopy(hashes, (int) (0x14*block), IV, 0, 16);
System.arraycopy(hashes, (int) (0x14*block), h0, 0, 20);
if( block == 0 )
IV[1] ^= (byte)contentID;
byte[] output = decryptFileChunk(blockBuffer,0x400,0xFC00,IV);
hash = hash(output);
if(block == 0){
hash[1] ^= contentID;
}
if(Arrays.equals(hash, h0)){
//System.out.println("checksum right");
}
else{
System.out.println("checksum failed");
System.out.println(Util.ByteArrayToString(hash));
System.out.println(Util.ByteArrayToString(h0));
}
return output;
}
public static byte[] hash(byte[] hashThis) {
try {
byte[] hash = new byte[20];
MessageDigest md = MessageDigest.getInstance("SHA-1");
hash = md.digest(hashThis);
return hash;
} catch (NoSuchAlgorithmException nsae) {
System.err.println("SHA-1 algorithm is not available...");
System.exit(2);
}
return null;
}
}

347
src/Downloader.java Normal file
View File

@ -0,0 +1,347 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class Downloader {
private static Downloader instance;
public static Downloader getInstance(){
if(instance == null){
instance = new Downloader();
}
return instance;
}
private Downloader(){
}
public void downloadAndDecrypt(String URL,long fileOffset,long fileLength,FEntry toDownload,TIK ticket) throws IOException{
URL url = new URL(URL);
HttpURLConnection connection =
(HttpURLConnection) url.openConnection();
int BLOCKSIZE = 0x8000;
long dlFileLength = fileLength;
if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){
dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE;
}
fileOffset = ((fileOffset / BLOCKSIZE) * BLOCKSIZE);
connection.setRequestProperty("Range", "bytes=" + fileOffset+"-");
connection.connect();
FileOutputStream outputStream = new FileOutputStream(String.format("%016X", titleID) +"/" + toDownload.getFullPath().substring(1, toDownload.getFullPath().length()));
InputStream input = connection.getInputStream();
int bytesRead = -1;
byte[] IV = new byte[16];
IV[1] = (byte)toDownload.getContentID();
byte[] downloadBuffer;
byte[] blockBuffer = new byte[BLOCKSIZE];
byte[] overflowBuffer = new byte[BLOCKSIZE];
int overflowsize = 0;
int inBlockBuffer = 0;
byte[] tmp = new byte[BLOCKSIZE];
boolean endd = false;
int downloadTotalsize = 0;
int wrote = 0;
Decryption decryption = new Decryption(ticket);
boolean first = true;
do{
downloadBuffer = new byte[BLOCKSIZE-overflowsize];
bytesRead = input.read(downloadBuffer);
downloadTotalsize += bytesRead;
if(bytesRead ==-1){
endd = true;
}
if(!endd)System.arraycopy(downloadBuffer, 0, overflowBuffer, overflowsize,bytesRead);
bytesRead += overflowsize;
overflowsize = 0;
int oldInThisBlock = inBlockBuffer;
if(oldInThisBlock + bytesRead > BLOCKSIZE){
int tooMuch = (oldInThisBlock + bytesRead) - BLOCKSIZE;
int toRead = BLOCKSIZE - oldInThisBlock;
System.arraycopy(overflowBuffer, 0, blockBuffer, oldInThisBlock, toRead);
inBlockBuffer += toRead;
overflowsize = tooMuch;
System.arraycopy(overflowBuffer, toRead, tmp, 0, tooMuch);
System.arraycopy(tmp, 0, overflowBuffer, 0, tooMuch);
}else{
if(!endd)System.arraycopy(overflowBuffer, 0, blockBuffer, inBlockBuffer, bytesRead);
inBlockBuffer +=bytesRead;
}
if(inBlockBuffer == BLOCKSIZE || endd){
if(first){
first = false;
}else{
IV = null;
}
byte[] output =decryption.decryptFileChunk(blockBuffer,BLOCKSIZE,IV);
if((wrote + inBlockBuffer) > fileLength){
inBlockBuffer = (int) (fileLength- wrote);
}
wrote += inBlockBuffer;
outputStream.write(output, 0, inBlockBuffer);
inBlockBuffer = 0;
}
}while(downloadTotalsize < dlFileLength && !endd);
outputStream.close();
input.close();
connection.disconnect();
}
public void downloadAndDecryptHash(String URL,long fileOffset,long fileLength,FEntry toDownload,TIK ticket) throws IOException{
int BLOCKSIZE = 0x10000;
int HASHBLOCKSIZE = 0xFC00;
long writeSize = HASHBLOCKSIZE; // Hash block size
long block = (fileOffset / HASHBLOCKSIZE) & 0xF;
long soffset = fileOffset - (fileOffset / HASHBLOCKSIZE * HASHBLOCKSIZE);
fileOffset = ((fileOffset / HASHBLOCKSIZE) * BLOCKSIZE);
long size = (int) fileLength;
if( soffset+size > writeSize )
writeSize = writeSize - soffset;
URL url = new URL(URL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Range", "bytes=" + fileOffset+"-");
connection.connect();
FileOutputStream outputStream = new FileOutputStream(String.format("%016X", titleID) +"/" +toDownload.getFullPath().substring(1, toDownload.getFullPath().length()));
InputStream input = connection.getInputStream();
int bytesRead = -1;
byte[] downloadBuffer;
byte[] encryptedBlockBuffer = new byte[BLOCKSIZE];
byte[] buffer = new byte[BLOCKSIZE];
int encryptedBytesInBuffer = 0;
int bufferPostion = 0;
byte[] tmp = new byte[BLOCKSIZE];
boolean lastPart = false;
int wrote = 0;
Decryption decryption = new Decryption(ticket);
do{
downloadBuffer = new byte[BLOCKSIZE-bufferPostion];
bytesRead = input.read(downloadBuffer);
int bytesInBuffer = bytesRead + bufferPostion;
if(bytesRead ==-1){
lastPart = true;
}else{
System.arraycopy(downloadBuffer, 0, buffer, bufferPostion,bytesRead); //copy downloaded stuff in buffer
bufferPostion = 0;
}
if(encryptedBytesInBuffer + bytesInBuffer > BLOCKSIZE){
int tooMuch = (encryptedBytesInBuffer + bytesInBuffer) - BLOCKSIZE;
int toRead = BLOCKSIZE - encryptedBytesInBuffer;
System.arraycopy(buffer, 0, encryptedBlockBuffer, encryptedBytesInBuffer, toRead); // make buffer with encrypteddata full
encryptedBytesInBuffer += toRead;
bufferPostion = tooMuch; //set buffer position;
System.arraycopy(buffer, toRead, tmp, 0, tooMuch);
System.arraycopy(tmp, 0, buffer, 0, tooMuch);
}else{
if(!lastPart) System.arraycopy(buffer, 0, encryptedBlockBuffer, encryptedBytesInBuffer, bytesInBuffer); //When File if at the end, no more need to copy
encryptedBytesInBuffer +=bytesInBuffer;
}
//If downloaded BLOCKSIZE, or file at the end: Decrypt!
if(encryptedBytesInBuffer == BLOCKSIZE || lastPart){
if( writeSize > size )
writeSize = size;
byte[] output = decryption.decryptFileChunkHash(encryptedBlockBuffer, BLOCKSIZE, (int) block,toDownload.getContentID());
if((wrote + writeSize) > fileLength){
writeSize = (int) (fileLength- wrote);
}
outputStream.write(output, (int)(0+soffset), (int)writeSize);
wrote +=writeSize;
encryptedBytesInBuffer = 0;
block++;
if( block >= 16 )
block = 0;
if( soffset > 0)
{
writeSize = HASHBLOCKSIZE;
soffset = 0;
}
}
}while(wrote < fileLength || lastPart);
outputStream.close();
input.close();
connection.disconnect();
}
public long titleID =0;
public TIK ticket = null;
public void download( FEntry toDownload) {
File f = new File (String.format("%016X", titleID));
if(!f.exists())f.mkdir();
System.out.println("Downloading: " + String.format("%8.2f MB ", toDownload.getFileLength()/1024.0/1024.0) + toDownload.getFullPath());
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", toDownload.getNUScontentID());
String [] path = toDownload.getFullPath().split("/");
String folder = String.format("%016X", titleID) +"/";
for(int i = 0;i<path.length-1;i++){
if(!path[i].equals("")){
folder += path[i] + "/";
}
}
File folder_ = new File(folder);
if(!folder_.exists()) folder_.mkdir();
try {
//if(toDownload.isExtractWithHash()){
if(path[1].equals("content")){
downloadAndDecryptHash(URL,toDownload.getFileOffset(),toDownload.getFileLength(),toDownload,ticket);
}else{
downloadAndDecrypt(URL,toDownload.getFileOffset(),toDownload.getFileLength(),toDownload,ticket);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//System.out.println("Finished: " + toDownload.getFullPath());
}
public static String URL_BASE = "";
public void downloadTMD(int version) throws IOException {
downloadTMD();
}
public void downloadTMD() throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd";
downloadFile(URL, "tmd");
}
public void downloadFile(String fileURL,String filename) throws IOException{
int BUFFER_SIZE = 0x800;
URL url = new URL(fileURL);
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
InputStream inputStream = httpConn.getInputStream();
FileOutputStream outputStream = new FileOutputStream(filename);
int bytesRead = -1;
byte[] buffer = new byte[BUFFER_SIZE];
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
inputStream.close();
httpConn.disconnect();
}
public void downloadTicket() throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
downloadFile(URL, "cetk");
}
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 byte[] downloadContentToByteArray(int contentID) throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/" + String.format("%08X", contentID);
return downloadFileToByteArray(URL);
}
public byte[] downloadTMDToByteArray() throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/tmd";
return downloadFileToByteArray(URL);
}
private byte[] downloadFileToByteArray(String fileURL) throws IOException {
int BUFFER_SIZE = 0x800;
URL url = new URL(fileURL);
HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
int responseCode = httpConn.getResponseCode();
// always check HTTP response code first
byte[] file = null;
if (responseCode == HttpURLConnection.HTTP_OK) {
int contentLength = httpConn.getContentLength();
file = new byte[contentLength];
// always check HTTP response code first
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.err.println("File not found: " + fileURL);
}
httpConn.disconnect();
return file;
}
public byte[] downloadTicketToByteArray() throws IOException {
String URL = URL_BASE + "/" + String.format("%016X", titleID) + "/cetk";
return downloadFileToByteArray(URL);
}
}

122
src/FEntry.java Normal file
View File

@ -0,0 +1,122 @@
public class FEntry {
public static int DIR_FLAG = 1;
public static int NOT_IN_NUSTITLE_FLAG = 0x80;
public static int EXTRACT_WITH_HASH_FLAG = 0x440;
public static int CHANGE_OFFSET_FLAG = 0x04;
private boolean dir = false;
private boolean in_nus_title = false;
private boolean extract_withHash = false;
private String fileName = "";
private String path = "";
private long fileOffset = 0L;
private long fileLength = 0;
private int contentID = 0;
private int NUScontentID = 0;
public FEntry(String path, String filename, int contentID,int NUScontentID, long fileOffset, long fileLength, boolean dir,
boolean in_nus_title, boolean extract_withHash) {
setPath(path);
setFileName(filename);
setContentID(contentID);
setFileOffset(fileOffset);
setFileLength(fileLength);
setDir(dir);
setInNusTitle(in_nus_title);
setExtractWithHash(extract_withHash);
setNUScontentID(NUScontentID);
}
public boolean isDir() {
return dir;
}
private void setDir(boolean dir) {
this.dir = dir;
}
public boolean isInNUSTitle() {
return in_nus_title;
}
private void setInNusTitle(boolean in_nus_title) {
this.in_nus_title = in_nus_title;
}
public boolean isExtractWithHash() {
return extract_withHash;
}
private void setExtractWithHash(boolean extract_withHash) {
this.extract_withHash = extract_withHash;
}
public String getFileName() {
return fileName;
}
private void setFileName(String filename) {
this.fileName = filename;
}
public String getPath() {
return path;
}
public String getFullPath() {
return path + fileName;
}
private void setPath(String path) {
this.path = path;
}
public long getFileOffset() {
return fileOffset;
}
private void setFileOffset(long fileOffset) {
this.fileOffset = fileOffset;
}
public int getContentID() {
return contentID;
}
private void setContentID(int contentID) {
this.contentID = contentID;
}
public long getFileLength() {
return fileLength;
}
private void setFileLength(long fileLength) {
this.fileLength = fileLength;
}
@Override
public String toString(){
return getFullPath() + " Content ID:" + contentID + " Size: " + fileLength +"MB Offset: " + fileOffset;
}
public int getNUScontentID() {
return NUScontentID;
}
private void setNUScontentID(int nUScontentID) {
NUScontentID = nUScontentID;
}
public void download() {
Downloader.getInstance().download(this);
}
}

221
src/FST.java Normal file
View File

@ -0,0 +1,221 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FST {
long totalContentSize = 0L;
long totalContentSizeInNUS = 0L;
List<FEntry> fileEntries = new ArrayList<>();
int totalContentCount = 0;
int totalEntries = 0;
int dirEntries = 0;
public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
parse(decrypteddata,tmd);
}
private void parse(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
if(!Arrays.equals(Arrays.copyOfRange(decrypteddata, 0, 3), new byte[]{0x46,0x53,0x54})){
System.err.println("Not a FST. Maybe a wrong key?");
throw new IllegalArgumentException("File not a FST");
}
this.totalContentCount = Util.getIntFromBytes(decrypteddata, 8);
int base_offset = 0x20+totalContentCount*0x20;
this.totalEntries = Util.getIntFromBytes(decrypteddata, base_offset+8);
int nameOff = base_offset + totalEntries * 0x10;
int level=0;
int[] LEntry = new int[16];
int[] Entry = new int[16];
for(int i = 0;i<this.totalEntries;i++){
boolean dir = false;
boolean in_nus_title = true;
boolean extract_withHash = false;
long fileOffset;
long fileLength;
int type;
int contentID;
String filename = "";
String path = "";
if( level > 0)
{
while( LEntry[level-1] == i )
{
level--;
}
}
int offset = base_offset + i*0x10;
//getting the type
type = (int) decrypteddata[offset]+128;
if((type & FEntry.DIR_FLAG) == 1) dir = true;
if((type & FEntry.NOT_IN_NUSTITLE_FLAG) == 0 ) in_nus_title = false;
//getting Name
decrypteddata[offset] = 0;
int nameoff_entry_offset = Util.getIntFromBytes(decrypteddata, offset);
int j = 0;
int nameoff_entry = nameOff + nameoff_entry_offset;
while(decrypteddata[nameoff_entry + j] != 0){j++;}
filename = new String(Arrays.copyOfRange(decrypteddata,nameoff_entry, nameoff_entry + j));
//getting offsets. save in two ways
offset+=4;
fileOffset = (long) Util.getIntFromBytes(decrypteddata, offset);
offset+=4;
fileLength = Util.getIntAsLongFromBytes(decrypteddata, offset);
@SuppressWarnings("unused")
int parentOffset = (int) fileOffset;
int nextOffset = (int) fileLength;
//grabbing flags
offset+=4;
int flags = Util.getShortFromBytes(decrypteddata, offset);
if((flags & FEntry.EXTRACT_WITH_HASH_FLAG) > 0) extract_withHash = true;
if((flags & FEntry.CHANGE_OFFSET_FLAG) == 0) fileOffset <<=5;
//grabbing contentid
offset+=2;
contentID = Util.getShortFromBytes(decrypteddata, offset) ;
//remember total size
this.totalContentSize += fileLength;
if(in_nus_title)this.totalContentSizeInNUS += fileLength;
//getting the full path of entry
if(dir)
{
dirEntries++;
Entry[level] = i;
LEntry[level++] = nextOffset ;
if( level > 15 ) // something is wrong!
{
break;
}
}else{
StringBuilder sb = new StringBuilder();
int k = 0;
int nameoffoff,nameoff_entrypath;
for( j=0; j<level; ++j )
{
nameoffoff = Util.getIntFromBytes(decrypteddata,base_offset+Entry[j]*0x10);
k=0;
nameoff_entrypath = nameOff + nameoffoff;
while(decrypteddata[nameoff_entrypath + k] != 0){k++;}
sb.append(new String(Arrays.copyOfRange(decrypteddata,nameoff_entrypath, nameoff_entrypath + k)));
sb.append("/");
}
path = sb.toString();
}
//add this to the List!
fileEntries.add(new FEntry(path,filename,contentID,tmd.contents[contentID].ID,fileOffset,fileLength,dir,in_nus_title,extract_withHash));
//System.out.println(fileEntries.get(i));
}
}
public long getTotalContentSize() {
return totalContentSize;
}
public void setTotalContentSize(long totalContentSize) {
this.totalContentSize = totalContentSize;
}
public long getTotalContentSizeInNUS() {
return totalContentSizeInNUS;
}
public void setTotalContentSizeInNUS(long totalContentSizeInNUS) {
this.totalContentSizeInNUS = totalContentSizeInNUS;
}
public List<FEntry> getFileEntries() {
return fileEntries;
}
public void setFileEntries(List<FEntry> fileEntries) {
this.fileEntries = fileEntries;
}
public int getTotalContentCount() {
return totalContentCount;
}
public void setTotalContentCount(int totalContentCount) {
this.totalContentCount = totalContentCount;
}
public int getTotalEntries() {
return totalEntries;
}
public void setTotalEntries(int totalEntries) {
this.totalEntries = totalEntries;
}
public int getDirEntries() {
return dirEntries;
}
public void setDirEntries(int dirEntries) {
this.dirEntries = dirEntries;
}
@Override
public String toString(){
return "entryCount: " + totalContentCount+ " entries: " + totalEntries;
}
public int getFileCount() {
int i = 0;
for(FEntry f: getFileEntries()){
if(!f.isDir())
i++;
}
return i;
}
public int getFileCountInNUS() {
int i = 0;
for(FEntry f: getFileEntries()){
if(!f.isDir() && f.isInNUSTitle())
i++;
}
return i;
}
}

80
src/NUSTitle.java Normal file
View File

@ -0,0 +1,80 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
public class NUSTitle {
private TitleMetaData tmd;
private long titleID;
private TIK ticket;
public NUSTitle(long titleId,String key){
try {
titleID = titleId;
Downloader.getInstance().titleID = titleId;
Decryption decryption = new Decryption(Util.commonKey,titleId);
tmd = new TitleMetaData(Downloader.getInstance().downloadTMDToByteArray());
if(key == null){
ticket = new TIK(Downloader.getInstance().downloadTicketToByteArray(),tmd.titleID);
}else{
ticket = new TIK(key,titleId);
}
Downloader.getInstance().ticket = ticket;
decryption.init(ticket.getDecryptedKey(),0);
byte[] encryptedFST = Downloader.getInstance().downloadContentToByteArray(tmd.contents[0].ID);
byte[] decryptedFST = decryption.decrypt(encryptedFST);
FST fst = new FST(decryptedFST,tmd);
long start = new Date().getTime();
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");
System.out.println("Entries: " + fst.getTotalEntries());
System.out.println("Entries: " + fst.getFileCount());
System.out.println("Files in NUSTitle: " + fst.getFileCountInNUS());
System.out.println("");
System.out.println("Downloading all files.");
System.out.println("");
ForkJoinPool pool = ForkJoinPool.commonPool();
List<TitleDownloader> list = new ArrayList<>();
for(FEntry f: fst.getFileEntries()){
if(!f.isDir() && f.isInNUSTitle())
list.add(new TitleDownloader(f));
}
pool.invokeAll(list);
long runningTime = new Date().getTime() - start;
System.out.println("");
System.out.println("Done in: " + runningTime/1000.0 + " seconds");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public long getTitleID() {
return titleID;
}
public void setTitleID(long titleID) {
this.titleID = titleID;
}
public long getTotalContentSize() {
return tmd.getTotalContentSize();
}
}

38
src/Starter.java Normal file
View File

@ -0,0 +1,38 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class Starter {
public static void main(String[] args) {
try {
readConfig();
} catch (IOException e) {
System.err.println("Error while reading config! Needs to be:");
System.err.println("DOWNLOAD URL BASE");
System.err.println("COMMONKEY");
return;
}
if(args.length != 0){
long titleID = Util.StringToLong(args[0]);
String key = null;
if( args.length > 1 && args[1].length() == 32){
key = args[1].substring(0, 32);
}
new NUSTitle(titleID, key);
}else{
System.out.println("Need parameters: TITLEID [KEY]");
}
}
private static void readConfig() throws IOException {
BufferedReader in = new BufferedReader(new FileReader(new File("config")));
Downloader.URL_BASE = in.readLine();
Util.commonKey = Util.hexStringToByteArray(in.readLine());
in.close();
}
}

66
src/TIK.java Normal file
View File

@ -0,0 +1,66 @@
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class TIK {
public static int KEY_LENGTH = 16;
private byte[] encryptedKey = new byte[16];
private byte[] decryptedKey = new byte[16];
public TIK(File cetk,long titleid) throws IOException{
parse(cetk);
calculateDecryptedKey(titleid);
}
public TIK(String ticketKey,long titleid) {
setEncryptedKey(ticketKey);
calculateDecryptedKey(titleid);
}
public TIK(byte[] file, long titleID) throws IOException {
parse(file);
calculateDecryptedKey(titleID);
}
private void calculateDecryptedKey(long titleid) {
Decryption decryption = new Decryption(Util.commonKey,titleid);
decryptedKey = decryption.decrypt(encryptedKey);
}
private void parse(byte[] cetk) throws IOException {
System.arraycopy(cetk, 0x1bf, this.encryptedKey, 0,16);
}
private void parse(File cetk) throws IOException {
RandomAccessFile f = new RandomAccessFile(cetk, "r");
f.seek(0x1bf);
f.read(this.encryptedKey, 0, 16);
f.close();
}
public void setEncryptedKey(String key) {
this.encryptedKey = Util.hexStringToByteArray(key);
}
public byte[] getEncryptedKey() {
return encryptedKey;
}
public void setEncryptedKey(byte[] encryptedKey) {
this.encryptedKey = encryptedKey;
}
public byte[] getDecryptedKey() {
return decryptedKey;
}
public void setDecryptedKey(byte[] decryptedKey) {
this.decryptedKey = decryptedKey;
}
@Override
public String toString(){
return "encrypted key: " + Util.ByteArrayToString(encryptedKey)+ " decrypted key: " + Util.ByteArrayToString(decryptedKey);
}
}

21
src/TitleDownloader.java Normal file
View File

@ -0,0 +1,21 @@
import java.util.concurrent.Callable;
public class TitleDownloader implements Callable<Integer>
{
FEntry f;
public void setTitle(FEntry f){
this.f = f;
}
public TitleDownloader(FEntry f){
setTitle(f);
}
@Override
public Integer call() throws Exception {
f.download();
return null;
}
}

152
src/TitleMetaData.java Normal file
View File

@ -0,0 +1,152 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
public class TitleMetaData {
int signatureType; // 0x000
byte[] signature = new byte[0x100]; // 0x004
byte[] issuer = new byte[0x40]; // 0x140
byte version; // 0x180
byte CACRLVersion; // 0x181
byte signerCRLVersion; // 0x182
long systemVersion; // 0x184
long titleID; // 0x18C
int titleType; // 0x194
short groupID; // 0x198
byte[] reserved = new byte[62]; // 0x19A
int accessRights; // 0x1D8
short titleVersion; // 0x1DC
short contentCount; // 0x1DE
short bootIndex; // 0x1E0
byte[] SHA2 = new byte[32]; // 0x1E4
ContentInfo[] contentInfos = new ContentInfo[64]; // 0x1E4
Content[] contents; // 0x1E4
private long totalContentSize;
public TitleMetaData(File tmd) throws IOException {
parse(tmd);
setTotalContentSize();
}
public TitleMetaData(byte[] downloadTMDToByteArray) throws IOException {
if(downloadTMDToByteArray != null){
File tempFile = File.createTempFile("bla","blubb");
FileOutputStream fos = new FileOutputStream(tempFile);
fos.write(downloadTMDToByteArray);
fos.close();
parse(tempFile);
setTotalContentSize();
}else{
System.err.println("Invalid TMD");
throw new IllegalArgumentException("Invalid TMD");
}
}
private void parse(File tmd) throws IOException {
RandomAccessFile f = new RandomAccessFile(tmd, "r");
f.seek(0);
this.signatureType = f.readInt();
f.read(signature, 0, 0x100);
f.seek(0x140);
f.read(issuer, 0, 0x40);
f.seek(0x180);
this.version = f.readByte();
this.CACRLVersion = f.readByte();
this.signerCRLVersion = f.readByte();
f.seek(0x184);
this.systemVersion = f.readLong();
this.titleID = f.readLong();
this.titleType = f.readInt();
this.groupID = f.readShort();
f.seek(0x19A);
f.read(reserved, 0, 62);
f.seek(0x1D8);
this.accessRights = f.readInt();
this.titleVersion = f.readShort();
this.contentCount = f.readShort();
this.bootIndex = f.readShort();
f.seek(0x1E4);
f.read(SHA2, 0, 32);
f.seek(0x204);
short indexOffset;
short commandCount;
for(int i =0;i<64;i++){
f.seek(0x204+(0x24*i));
indexOffset =f.readShort();
commandCount =f.readShort();
byte[] buffer = new byte[0x20]; // 16 0xB14
f.read(buffer, 0, 0x20);
this.contentInfos[i] = new ContentInfo(indexOffset,commandCount,buffer);
}
this.contents = new Content[contentCount];
int ID; // 0 0xB04
short index; // 4 0xB08
short type; // 6 0xB0A
long size; // 8 0xB0C
for(int i =0;i<contentCount;i++){
f.seek(0xB04+(0x30*i));
ID = f.readInt();
index = f.readShort();
type = f.readShort();
size = f.readLong();
byte[] buffer = new byte[0x20]; // 16 0xB14
f.read(buffer,0, 0x20);
this.contents[i] = new Content(ID,index,type,size,buffer);
}
f.close();
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("signatureType: " + signatureType +"\n");
sb.append("signature: " + Util.ByteArrayToString(signature) +"\n");
sb.append("issuer: " + Util.ByteArrayToString(issuer) +"\n");
sb.append("version: " + version +"\n");
sb.append("CACRLVersion: " + CACRLVersion +"\n");
sb.append("signerCRLVersion: " + signerCRLVersion +"\n");
sb.append("systemVersion: " + String.format("%8X",systemVersion) +"\n");
sb.append("titleID: " + String.format("%8X",titleID) +"\n");
sb.append("titleType: " + titleType +"\n");
sb.append("groupID: " + groupID +"\n");
sb.append("reserved: " + Util.ByteArrayToString(reserved) +"\n");
sb.append("accessRights: " + accessRights +"\n");
sb.append("titleVersion: " + titleVersion +"\n");
sb.append("contentCount: " + contentCount +"\n");
sb.append("bootIndex: " + bootIndex +"\n");
sb.append("SHA2: " + Util.ByteArrayToString(SHA2) +"\n");
sb.append("contentInfos: \n");
for(int i = 0; i<contents.length-1;i++){
sb.append(" " + contentInfos[i] +"\n");
}
sb.append("contents: \n");
for(int i = 0; i<contents.length-1;i++){
sb.append(" " + contents[i] +"\n");
}
return sb.toString();
}
public void setTotalContentSize(){
this.totalContentSize = 0;
for(int i = 0; i <contents.length-1;i++){
this.totalContentSize += contents[i].size;
}
}
public long getTotalContentSize() {
return totalContentSize;
}
}

59
src/Util.java Normal file
View File

@ -0,0 +1,59 @@
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class Util {
static byte[] commonKey;
public static byte[] hexStringToByteArray(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 String ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.length * 2);
for(byte b : ba){
hex.append(String.format("%X", b));
}
return hex.toString();
}
public static int getIntFromBytes(byte[] input,int offset){
return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+4)).getInt();
}
public static long getIntAsLongFromBytes(byte[] input,int offset){
long result = 0 ;
if((int)input[offset]+128 > 0 && (int)input[offset]+128 < 128){
input[offset] += 128;
result = (long)ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+4)).getInt();
result += 1024L*1024L*2048L;
return result;
}
return (long)ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+4)).getInt();
}
public static short getShortFromBytes(byte[] input, int offset) {
return ByteBuffer.wrap(Arrays.copyOfRange(input,offset, offset+2)).getShort();
}
public static long StringToLong(String s) {
try{
BigInteger bi = new BigInteger(s, 16);
return bi.longValue();
}catch(NumberFormatException e){
System.err.println("Invalid Title ID");
return 0L;
}
}
}