From d83f486fa30efb81ca3ce40f10d0b390a41fae16 Mon Sep 17 00:00:00 2001 From: Maschell Date: Mon, 1 Feb 2016 20:54:01 +0100 Subject: [PATCH] First commit =) --- .classpath | 6 + .gitignore | 3 + .project | 17 + .settings/org.eclipse.core.resources.prefs | 2 + .settings/org.eclipse.jdt.core.prefs | 11 + src/Content.java | 22 ++ src/ContentInfo.java | 29 ++ src/Decryption.java | 158 ++++++++++ src/Downloader.java | 347 +++++++++++++++++++++ src/FEntry.java | 122 ++++++++ src/FST.java | 221 +++++++++++++ src/NUSTitle.java | 80 +++++ src/Starter.java | 38 +++ src/TIK.java | 66 ++++ src/TitleDownloader.java | 21 ++ src/TitleMetaData.java | 152 +++++++++ src/Util.java | 59 ++++ 17 files changed, 1354 insertions(+) create mode 100644 .classpath create mode 100644 .project create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 src/Content.java create mode 100644 src/ContentInfo.java create mode 100644 src/Decryption.java create mode 100644 src/Downloader.java create mode 100644 src/FEntry.java create mode 100644 src/FST.java create mode 100644 src/NUSTitle.java create mode 100644 src/Starter.java create mode 100644 src/TIK.java create mode 100644 src/TitleDownloader.java create mode 100644 src/TitleMetaData.java create mode 100644 src/Util.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..fceb480 --- /dev/null +++ b/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/.gitignore b/.gitignore index cd2946a..da5b111 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ $RECYCLE.BIN/ Network Trash Folder Temporary Items .apdisk +0* +bin +config diff --git a/.project b/.project new file mode 100644 index 0000000..51d1814 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + JNusTool + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..82bae0f --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding//src/Util.java=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..3a21537 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -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 diff --git a/src/Content.java b/src/Content.java new file mode 100644 index 0000000..39539f4 --- /dev/null +++ b/src/Content.java @@ -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); + } +} diff --git a/src/ContentInfo.java b/src/ContentInfo.java new file mode 100644 index 0000000..3935152 --- /dev/null +++ b/src/ContentInfo.java @@ -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); + } +} diff --git a/src/Decryption.java b/src/Decryption.java new file mode 100644 index 0000000..f5da766 --- /dev/null +++ b/src/Decryption.java @@ -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; + } + + + + + + +} diff --git a/src/Downloader.java b/src/Downloader.java new file mode 100644 index 0000000..3a3d6c2 --- /dev/null +++ b/src/Downloader.java @@ -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 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 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 getFileEntries() { + return fileEntries; + } + + + public void setFileEntries(List 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; + } +} diff --git a/src/NUSTitle.java b/src/NUSTitle.java new file mode 100644 index 0000000..ccb4a84 --- /dev/null +++ b/src/NUSTitle.java @@ -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 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(); + } + +} diff --git a/src/Starter.java b/src/Starter.java new file mode 100644 index 0000000..af83eb6 --- /dev/null +++ b/src/Starter.java @@ -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(); + + } + +} diff --git a/src/TIK.java b/src/TIK.java new file mode 100644 index 0000000..1f47ba9 --- /dev/null +++ b/src/TIK.java @@ -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); + } +} diff --git a/src/TitleDownloader.java b/src/TitleDownloader.java new file mode 100644 index 0000000..1ba1b19 --- /dev/null +++ b/src/TitleDownloader.java @@ -0,0 +1,21 @@ +import java.util.concurrent.Callable; + +public class TitleDownloader implements Callable +{ + + 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; +} + +} diff --git a/src/TitleMetaData.java b/src/TitleMetaData.java new file mode 100644 index 0000000..9c15f49 --- /dev/null +++ b/src/TitleMetaData.java @@ -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 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; + } + } +}