mirror of
https://github.com/Maschell/JNUSTool.git
synced 2024-11-30 19:54:16 +01:00
Code cleanup!
This commit is contained in:
parent
67a3082976
commit
eb5657ebf6
@ -1,158 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,352 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
long downloadTotalsize = 0;
|
|
||||||
long 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 = 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;
|
|
||||||
long 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();
|
|
||||||
|
|
||||||
f = new File(String.format("%016X", titleID) +"/" +toDownload.getFullPath().substring(1, toDownload.getFullPath().length()));
|
|
||||||
if(f.exists()){
|
|
||||||
if(f.length() == toDownload.getFileLength()){
|
|
||||||
System.out.println("Skipping: " + String.format("%8.2f MB ", toDownload.getFileLength()/1024.0/1024.0) + toDownload.getFullPath());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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) +"/";
|
|
||||||
File folder_ = null;
|
|
||||||
for(int i = 0;i<path.length-1;i++){
|
|
||||||
if(!path[i].equals("")){
|
|
||||||
folder += path[i] + "/";
|
|
||||||
folder_ = new File(folder);
|
|
||||||
if(!folder_.exists()){
|
|
||||||
folder_.mkdir();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
//if(toDownload.isExtractWithHash()){
|
|
||||||
if(!path[1].equals("code") && toDownload.isExtractWithHash()){
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,3 +1,6 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class Content {
|
public class Content {
|
||||||
|
|
@ -1,3 +1,6 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class ContentInfo {
|
public class ContentInfo {
|
||||||
public short indexOffset; // 0 0x204
|
public short indexOffset; // 0 0x204
|
@ -1,3 +1,5 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
@ -1,6 +1,13 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Downloader;
|
||||||
|
|
||||||
public class FEntry {
|
public class FEntry {
|
||||||
|
private FST fst;
|
||||||
|
|
||||||
public static int DIR_FLAG = 1;
|
public static int DIR_FLAG = 1;
|
||||||
public static int NOT_IN_NUSTITLE_FLAG = 0x80;
|
public static int NOT_IN_NUSTITLE_FLAG = 0x80;
|
||||||
@ -21,7 +28,7 @@ public class FEntry {
|
|||||||
|
|
||||||
|
|
||||||
public FEntry(String path, String filename, int contentID,int NUScontentID, long fileOffset, long fileLength, boolean dir,
|
public FEntry(String path, String filename, int contentID,int NUScontentID, long fileOffset, long fileLength, boolean dir,
|
||||||
boolean in_nus_title, boolean extract_withHash, List<String> pathList) {
|
boolean in_nus_title, boolean extract_withHash, List<String> pathList,FST fst) {
|
||||||
setPath(path);
|
setPath(path);
|
||||||
setFileName(filename);
|
setFileName(filename);
|
||||||
setContentID(contentID);
|
setContentID(contentID);
|
||||||
@ -32,6 +39,7 @@ public class FEntry {
|
|||||||
setExtractWithHash(extract_withHash);
|
setExtractWithHash(extract_withHash);
|
||||||
setNUScontentID(NUScontentID);
|
setNUScontentID(NUScontentID);
|
||||||
setPathList(pathList);
|
setPathList(pathList);
|
||||||
|
this.fst = fst;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDir() {
|
public boolean isDir() {
|
||||||
@ -115,8 +123,40 @@ public class FEntry {
|
|||||||
NUScontentID = nUScontentID;
|
NUScontentID = nUScontentID;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void download() {
|
private void createFolder() {
|
||||||
Downloader.getInstance().download(this);
|
long titleID = fst.getTmd().titleID;
|
||||||
|
String [] path = getFullPath().split("/");
|
||||||
|
File f = new File (String.format("%016X", titleID));
|
||||||
|
if(!f.exists())f.mkdir();
|
||||||
|
|
||||||
|
String folder = String.format("%016X", titleID) +"/";
|
||||||
|
File folder_ = null;
|
||||||
|
for(int i = 0;i<path.length-1;i++){
|
||||||
|
if(!path[i].equals("")){
|
||||||
|
folder += path[i] + "/";
|
||||||
|
folder_ = new File(folder);
|
||||||
|
if(!folder_.exists()){
|
||||||
|
folder_.mkdir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
Downloader.getInstance().downloadAndDecrypt(this);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,14 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class FST {
|
public class FST {
|
||||||
|
private TitleMetaData tmd;
|
||||||
long totalContentSize = 0L;
|
long totalContentSize = 0L;
|
||||||
long totalContentSizeInNUS = 0L;
|
long totalContentSizeInNUS = 0L;
|
||||||
|
|
||||||
@ -14,20 +19,50 @@ public class FST {
|
|||||||
int totalEntries = 0;
|
int totalEntries = 0;
|
||||||
int dirEntries = 0;
|
int dirEntries = 0;
|
||||||
|
|
||||||
private Directory directory = new Directory("root");
|
private Directory FSTDirectory = new Directory("root");
|
||||||
|
|
||||||
|
private Directory contentDirectory = new Directory("root");
|
||||||
|
|
||||||
public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
|
public FST(byte[] decrypteddata, TitleMetaData tmd) throws IOException {
|
||||||
parse(decrypteddata,tmd);
|
parse(decrypteddata,tmd);
|
||||||
buildDirectory();
|
setTmd(tmd);
|
||||||
|
buildDirectories();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildDirectory() {
|
private void buildDirectories() {
|
||||||
|
|
||||||
|
String contentfolder = "";
|
||||||
|
Directory curContent = contentDirectory;
|
||||||
for(FEntry f : getFileEntries()){
|
for(FEntry f : getFileEntries()){
|
||||||
if(!f.isDir() && f.isInNUSTitle()){
|
if(!f.isDir() && f.isInNUSTitle()){
|
||||||
Directory current = directory;
|
contentfolder = String.format("%08X",tmd.contents[f.getContentID()].ID);
|
||||||
int i = 0;
|
|
||||||
for(String s :f.getPathList()){
|
|
||||||
|
|
||||||
|
if(!contentDirectory.containsFolder(contentfolder)){
|
||||||
|
Directory newDir = new Directory(contentfolder);
|
||||||
|
contentDirectory.addFolder(newDir);
|
||||||
|
}
|
||||||
|
curContent = contentDirectory.getFolder(contentfolder);
|
||||||
|
|
||||||
|
Directory current = FSTDirectory;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for(String s :f.getPathList()){
|
||||||
|
i++;
|
||||||
|
|
||||||
|
//Content
|
||||||
|
if(curContent.containsFolder(s)){
|
||||||
|
curContent = curContent.get(s);
|
||||||
|
}else{
|
||||||
|
Directory newDir = new Directory(s);
|
||||||
|
curContent.addFolder(newDir);
|
||||||
|
curContent = newDir;
|
||||||
|
}
|
||||||
|
if(i==f.getPathList().size()){
|
||||||
|
curContent.addFile(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//FST
|
||||||
if(current.containsFolder(s)){
|
if(current.containsFolder(s)){
|
||||||
current = current.get(s);
|
current = current.get(s);
|
||||||
}else{
|
}else{
|
||||||
@ -35,7 +70,6 @@ public class FST {
|
|||||||
current.addFolder(newDir);
|
current.addFolder(newDir);
|
||||||
current = newDir;
|
current = newDir;
|
||||||
}
|
}
|
||||||
i++;
|
|
||||||
if(i==f.getPathList().size()){
|
if(i==f.getPathList().size()){
|
||||||
current.addFile(f);
|
current.addFile(f);
|
||||||
}
|
}
|
||||||
@ -43,6 +77,8 @@ public class FST {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -163,7 +199,7 @@ public class FST {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//add this to the List!
|
//add this to the List!
|
||||||
fileEntries.add(new FEntry(path,filename,contentID,tmd.contents[contentID].ID,fileOffset,fileLength,dir,in_nus_title,extract_withHash,pathList));
|
fileEntries.add(new FEntry(path,filename,contentID,tmd.contents[contentID].ID,fileOffset,fileLength,dir,in_nus_title,extract_withHash,pathList,this));
|
||||||
//System.out.println(fileEntries.get(i));
|
//System.out.println(fileEntries.get(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,9 +291,24 @@ public class FST {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Directory getDirectory() {
|
public Directory getFSTDirectory() {
|
||||||
return directory;
|
return FSTDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Directory getContentDirectory() {
|
||||||
|
return contentDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public TitleMetaData getTmd() {
|
||||||
|
return tmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTmd(TitleMetaData tmd) {
|
||||||
|
this.tmd = tmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,5 +1,11 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Decryption;
|
||||||
|
import de.mas.jnustool.util.Downloader;
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class NUSTitle {
|
public class NUSTitle {
|
||||||
private TitleMetaData tmd;
|
private TitleMetaData tmd;
|
||||||
private long titleID;
|
private long titleID;
|
||||||
@ -28,6 +34,7 @@ public class NUSTitle {
|
|||||||
byte[] decryptedFST = decryption.decrypt(encryptedFST);
|
byte[] decryptedFST = decryption.decrypt(encryptedFST);
|
||||||
|
|
||||||
fst = new FST(decryptedFST,tmd);
|
fst = new FST(decryptedFST,tmd);
|
||||||
|
tmd.setFst(fst);
|
||||||
|
|
||||||
System.out.println("Total Size of Content Files: " + ((int)((getTotalContentSize()/1024.0/1024.0)*100))/100.0 +" MB");
|
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("Total Size of Decrypted Files: " + ((int)((fst.getTotalContentSizeInNUS()/1024.0/1024.0)*100))/100.0 +" MB");
|
6
src/de/mas/jnustool/Settings.java
Normal file
6
src/de/mas/jnustool/Settings.java
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
|
|
||||||
|
public class Settings {
|
||||||
|
|
||||||
|
}
|
@ -1,8 +1,14 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import de.mas.jnustool.gui.NUSGUI;
|
||||||
|
import de.mas.jnustool.util.Downloader;
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class Starter {
|
public class Starter {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
@ -22,7 +28,7 @@ public class Starter {
|
|||||||
if( args.length > 1 && args[1].length() == 32){
|
if( args.length > 1 && args[1].length() == 32){
|
||||||
key = args[1].substring(0, 32);
|
key = args[1].substring(0, 32);
|
||||||
}
|
}
|
||||||
NUSGUI m = new NUSGUI(new NUSTitle(titleID, key));
|
NUSGUI m = new NUSGUI(new NUSTitle(titleID, key), null);
|
||||||
m.setVisible(true);
|
m.setVisible(true);
|
||||||
}else{
|
}else{
|
||||||
System.out.println("Need parameters: TITLEID [KEY]");
|
System.out.println("Need parameters: TITLEID [KEY]");
|
@ -1,7 +1,12 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Decryption;
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class TIK {
|
public class TIK {
|
||||||
public static int KEY_LENGTH = 16;
|
public static int KEY_LENGTH = 16;
|
||||||
private byte[] encryptedKey = new byte[16];
|
private byte[] encryptedKey = new byte[16];
|
@ -1,3 +1,5 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
public class TitleDownloader implements Callable<Integer>
|
public class TitleDownloader implements Callable<Integer>
|
||||||
@ -14,7 +16,7 @@ public class TitleDownloader implements Callable<Integer>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer call() throws Exception {
|
public Integer call() throws Exception {
|
||||||
f.download();
|
f.downloadAndDecrypt();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
|||||||
|
package de.mas.jnustool;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
|
|
||||||
|
import de.mas.jnustool.util.Util;
|
||||||
|
|
||||||
public class TitleMetaData {
|
public class TitleMetaData {
|
||||||
int signatureType; // 0x000
|
int signatureType; // 0x000
|
||||||
byte[] signature = new byte[0x100]; // 0x004
|
byte[] signature = new byte[0x100]; // 0x004
|
||||||
@ -23,6 +27,7 @@ public class TitleMetaData {
|
|||||||
ContentInfo[] contentInfos = new ContentInfo[64]; // 0x1E4
|
ContentInfo[] contentInfos = new ContentInfo[64]; // 0x1E4
|
||||||
Content[] contents; // 0x1E4
|
Content[] contents; // 0x1E4
|
||||||
|
|
||||||
|
private FST fst;
|
||||||
|
|
||||||
private long totalContentSize;
|
private long totalContentSize;
|
||||||
|
|
||||||
@ -148,5 +153,12 @@ public class TitleMetaData {
|
|||||||
return totalContentSize;
|
return totalContentSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FST getFst() {
|
||||||
|
return fst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFst(FST fst) {
|
||||||
|
this.fst = fst;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
package de.mas.jnustool.gui;
|
||||||
/**
|
/**
|
||||||
* Based on
|
* Based on
|
||||||
* http://stackoverflow.com/questions/21847411/java-swing-need-a-good-quality-developed-jtree-with-checkboxes/21851201#21851201
|
* http://stackoverflow.com/questions/21847411/java-swing-need-a-good-quality-developed-jtree-with-checkboxes/21851201#21851201
|
||||||
@ -24,6 +25,9 @@ import javax.swing.tree.TreeModel;
|
|||||||
import javax.swing.tree.TreeNode;
|
import javax.swing.tree.TreeNode;
|
||||||
import javax.swing.tree.TreePath;
|
import javax.swing.tree.TreePath;
|
||||||
|
|
||||||
|
import de.mas.jnustool.FEntry;
|
||||||
|
import de.mas.jnustool.NUSTitle;
|
||||||
|
|
||||||
public class JCheckBoxTree extends JTree {
|
public class JCheckBoxTree extends JTree {
|
||||||
|
|
||||||
private static final long serialVersionUID = -4194122328392241790L;
|
private static final long serialVersionUID = -4194122328392241790L;
|
||||||
@ -159,18 +163,7 @@ public class JCheckBoxTree extends JTree {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setModel(new DefaultTreeModel(nus.getFst().getFSTDirectory().getNodes()));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
parent.add(new DefaultMutableTreeNode("hot dogs"));
|
|
||||||
parent.add(new DefaultMutableTreeNode("pizza"));
|
|
||||||
parent.add(new DefaultMutableTreeNode("ravioli"));
|
|
||||||
parent.add(new DefaultMutableTreeNode("bananas"));*/
|
|
||||||
//return ;
|
|
||||||
|
|
||||||
setModel(new DefaultTreeModel(nus.getFst().getDirectory().getNodes()));
|
|
||||||
|
|
||||||
// Disabling toggling by double-click
|
// Disabling toggling by double-click
|
||||||
this.setToggleClickCount(0);
|
this.setToggleClickCount(0);
|
@ -1,3 +1,5 @@
|
|||||||
|
package de.mas.jnustool.gui;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
@ -11,11 +13,16 @@ import javax.swing.JScrollPane;
|
|||||||
import javax.swing.tree.DefaultMutableTreeNode;
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
import javax.swing.tree.TreePath;
|
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;
|
||||||
|
|
||||||
public class NUSGUI extends JFrame {
|
public class NUSGUI extends JFrame {
|
||||||
|
|
||||||
private static final long serialVersionUID = 4648172894076113183L;
|
private static final long serialVersionUID = 4648172894076113183L;
|
||||||
|
|
||||||
public NUSGUI(NUSTitle nus) {
|
public NUSGUI(NUSTitle nus,Settings mode) {
|
||||||
super();
|
super();
|
||||||
setSize(800, 600);
|
setSize(800, 600);
|
||||||
getContentPane().setLayout(new BorderLayout(0, 0));
|
getContentPane().setLayout(new BorderLayout(0, 0));
|
341
src/de/mas/jnustool/util/Decryption.java
Normal file
341
src/de/mas/jnustool/util/Decryption.java
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
package de.mas.jnustool.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import de.mas.jnustool.FEntry;
|
||||||
|
import de.mas.jnustool.TIK;
|
||||||
|
|
||||||
|
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));
|
||||||
|
throw new IllegalArgumentException("checksumfail");
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void decryptFile(InputStream inputSteam, OutputStream outputStream,FEntry toDownload) throws IOException{
|
||||||
|
int BLOCKSIZE = 0x8000;
|
||||||
|
long dlFileLength = toDownload.getFileLength();
|
||||||
|
if(dlFileLength > (dlFileLength/BLOCKSIZE)*BLOCKSIZE){
|
||||||
|
dlFileLength = ((dlFileLength/BLOCKSIZE)*BLOCKSIZE) +BLOCKSIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
long downloadTotalsize = 0;
|
||||||
|
long wrote = 0;
|
||||||
|
|
||||||
|
boolean first = true;
|
||||||
|
do{
|
||||||
|
downloadBuffer = new byte[BLOCKSIZE-overflowsize];
|
||||||
|
|
||||||
|
bytesRead = inputSteam.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 = decryptFileChunk(blockBuffer,BLOCKSIZE,IV);
|
||||||
|
|
||||||
|
if((wrote + inBlockBuffer) > toDownload.getFileLength()){
|
||||||
|
inBlockBuffer = (int) (toDownload.getFileLength()- wrote);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrote += inBlockBuffer;
|
||||||
|
outputStream.write(output, 0, inBlockBuffer);
|
||||||
|
|
||||||
|
inBlockBuffer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}while(downloadTotalsize < dlFileLength && !endd);
|
||||||
|
|
||||||
|
outputStream.close();
|
||||||
|
inputSteam.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decryptFileHash(InputStream inputSteam, OutputStream outputStream,FEntry toDownload) throws IOException{
|
||||||
|
int BLOCKSIZE = 0x10000;
|
||||||
|
int HASHBLOCKSIZE = 0xFC00;
|
||||||
|
long writeSize = HASHBLOCKSIZE; // Hash block size
|
||||||
|
|
||||||
|
long block = (toDownload.getFileOffset() / HASHBLOCKSIZE) & 0xF;
|
||||||
|
|
||||||
|
long soffset = toDownload.getFileOffset() - (toDownload.getFileOffset() / HASHBLOCKSIZE * HASHBLOCKSIZE);
|
||||||
|
|
||||||
|
long size = toDownload.getFileLength();
|
||||||
|
|
||||||
|
if( soffset+size > writeSize )
|
||||||
|
writeSize = writeSize - soffset;
|
||||||
|
|
||||||
|
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;
|
||||||
|
long wrote = 0;
|
||||||
|
|
||||||
|
do{
|
||||||
|
downloadBuffer = new byte[BLOCKSIZE-bufferPostion];
|
||||||
|
bytesRead = inputSteam.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 = decryptFileChunkHash(encryptedBlockBuffer, BLOCKSIZE, (int) block,toDownload.getContentID());
|
||||||
|
|
||||||
|
if((wrote + writeSize) > toDownload.getFileLength()){
|
||||||
|
writeSize = (int) (toDownload.getFileLength()- 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 < toDownload.getFileLength() || lastPart);
|
||||||
|
|
||||||
|
outputStream.close();
|
||||||
|
inputSteam.close();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
149
src/de/mas/jnustool/util/Downloader.java
Normal file
149
src/de/mas/jnustool/util/Downloader.java
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package de.mas.jnustool.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public static Downloader getInstance(){
|
||||||
|
if(instance == null){
|
||||||
|
instance = new Downloader();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
private 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());
|
||||||
|
URL url = new URL(URL);
|
||||||
|
String [] path = toDownload.getFullPath().split("/");
|
||||||
|
boolean decryptWithHash = false;
|
||||||
|
if(!path[1].equals("code") && toDownload.isExtractWithHash()){
|
||||||
|
decryptWithHash = true;
|
||||||
|
}
|
||||||
|
HttpURLConnection connection =(HttpURLConnection) url.openConnection();
|
||||||
|
long fileOffset = toDownload.getFileOffset();
|
||||||
|
|
||||||
|
if(decryptWithHash){
|
||||||
|
int BLOCKSIZE = 0x10000;
|
||||||
|
int HASHBLOCKSIZE = 0xFC00;
|
||||||
|
fileOffset = ((toDownload.getFileOffset() / HASHBLOCKSIZE) * BLOCKSIZE);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setRequestProperty("Range", "bytes=" + fileOffset +"-");
|
||||||
|
|
||||||
|
connection.connect();
|
||||||
|
|
||||||
|
Decryption decryption = new Decryption(ticket);
|
||||||
|
|
||||||
|
InputStream input = connection.getInputStream();
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(String.format("%016X", titleID) +"/" + toDownload.getFullPath().substring(1, toDownload.getFullPath().length()));
|
||||||
|
if(!decryptWithHash){
|
||||||
|
decryption.decryptFile(input, outputStream, toDownload);
|
||||||
|
}else{
|
||||||
|
decryption.decryptFileHash(input, outputStream, toDownload);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
|
package de.mas.jnustool.util;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
public class Util {
|
public class Util {
|
||||||
static byte[] commonKey;
|
|
||||||
|
public static byte[] commonKey;
|
||||||
|
|
||||||
public static byte[] hexStringToByteArray(String s) {
|
public static byte[] hexStringToByteArray(String s) {
|
||||||
int len = s.length();
|
int len = s.length();
|
Loading…
Reference in New Issue
Block a user