New unified PFS0 Class with new method of data export that will be implemented everywhere
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
9b5eacdef9
commit
0238cb4d2c
12 changed files with 313 additions and 705 deletions
58
src/main/java/libKonogonka/TitleKeyChainHolder.java
Normal file
58
src/main/java/libKonogonka/TitleKeyChainHolder.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2019-2022 Dmitry Isaenko
|
||||
|
||||
This file is part of libKonogonka.
|
||||
|
||||
libKonogonka is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libKonogonka is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libKonogonka. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package libKonogonka;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class TitleKeyChainHolder {
|
||||
private final File keysFile;
|
||||
private HashMap<String, String> rawKeySet;
|
||||
|
||||
public TitleKeyChainHolder(String pathToKeysFile) throws Exception{
|
||||
this(new File(pathToKeysFile));
|
||||
}
|
||||
|
||||
public TitleKeyChainHolder(File keysFile) throws Exception{
|
||||
this.keysFile = keysFile;
|
||||
collectEverything();
|
||||
}
|
||||
|
||||
private void collectEverything() throws Exception{
|
||||
rawKeySet = new HashMap<>();
|
||||
|
||||
BufferedReader br = new BufferedReader(new FileReader(keysFile));
|
||||
|
||||
String fileLine;
|
||||
String[] keyValue;
|
||||
|
||||
while ((fileLine = br.readLine()) != null){
|
||||
keyValue = fileLine.trim().split("\\s*=\\s*", 2);
|
||||
if (keyValue.length == 2 && keyValue[0].length() > 16 && ! (keyValue[0].length() > 32) && keyValue[1].length() == 32){
|
||||
rawKeySet.put(keyValue[0], keyValue[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HashMap<String, String> getKeySet() {
|
||||
return rawKeySet;
|
||||
}
|
||||
}
|
|
@ -26,7 +26,8 @@ import java.io.PipedInputStream;
|
|||
public interface ISuperProvider {
|
||||
PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception;
|
||||
PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception;
|
||||
|
||||
boolean exportContent(String saveToLocation, String subFileName) throws Exception;
|
||||
boolean exportContent(String saveToLocation, int subFileNumber) throws Exception;
|
||||
File getFile();
|
||||
long getRawFileDataStart();
|
||||
}
|
||||
|
|
|
@ -19,10 +19,8 @@
|
|||
package libKonogonka.Tools.NCA;
|
||||
|
||||
import libKonogonka.Converter;
|
||||
import libKonogonka.RainbowDump;
|
||||
import libKonogonka.Tools.NCA.NCASectionTableBlock.NcaFsHeader;
|
||||
import libKonogonka.Tools.PFS0.IPFS0Provider;
|
||||
import libKonogonka.Tools.PFS0.PFS0EncryptedProvider;
|
||||
import libKonogonka.Tools.PFS0.PFS0Provider;
|
||||
import libKonogonka.Tools.RomFs.IRomFsProvider;
|
||||
import libKonogonka.Tools.RomFs.RomFsEncryptedProvider;
|
||||
|
@ -41,28 +39,28 @@ public class NCAContent {
|
|||
private final static Logger log = LogManager.getLogger(NCAContent.class);
|
||||
|
||||
private final File file;
|
||||
private final long offsetPosition;
|
||||
private final long ncaOffsetPosition;
|
||||
private final NcaFsHeader ncaFsHeader;
|
||||
private final NCAHeaderTableEntry ncaHeaderTableEntry;
|
||||
private final byte[] decryptedKey;
|
||||
|
||||
private final LinkedList<byte[]> Pfs0SHA256hashes;
|
||||
private LinkedList<byte[]> Pfs0SHA256hashes;
|
||||
private IPFS0Provider pfs0;
|
||||
private IRomFsProvider romfs;
|
||||
|
||||
// TODO: if decryptedKey is empty, throw exception ??
|
||||
public NCAContent(File file,
|
||||
long offsetPosition,
|
||||
long ncaOffsetPosition,
|
||||
NcaFsHeader ncaFsHeader,
|
||||
NCAHeaderTableEntry ncaHeaderTableEntry,
|
||||
byte[] decryptedKey) throws Exception
|
||||
{
|
||||
this.file = file;
|
||||
this.offsetPosition = offsetPosition;
|
||||
this.ncaOffsetPosition = ncaOffsetPosition;
|
||||
this.ncaFsHeader = ncaFsHeader;
|
||||
this.ncaHeaderTableEntry = ncaHeaderTableEntry;
|
||||
this.decryptedKey = decryptedKey;
|
||||
|
||||
System.out.println("NCAContent pfs0offsetPosition: "+ncaOffsetPosition);
|
||||
Pfs0SHA256hashes = new LinkedList<>();
|
||||
// If nothing to do
|
||||
if (ncaHeaderTableEntry.getMediaEndOffset() == 0)
|
||||
|
@ -89,60 +87,24 @@ public class NCAContent {
|
|||
}
|
||||
}
|
||||
private void proceedPFS0NotEncrypted() throws Exception{
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r");
|
||||
long thisMediaLocation = offsetPosition + (ncaHeaderTableEntry.getMediaStartOffset() * 0x200);// TODO: NOTE already defined inside PFS0
|
||||
long hashTableLocation = thisMediaLocation + ncaFsHeader.getSuperBlockPFS0().getHashTableOffset();
|
||||
long pfs0Location = thisMediaLocation + ncaFsHeader.getSuperBlockPFS0().getPfs0offset();
|
||||
|
||||
raf.seek(hashTableLocation);
|
||||
|
||||
byte[] rawData;
|
||||
long sha256recordsNumber = ncaFsHeader.getSuperBlockPFS0().getHashTableSize() / 0x20;
|
||||
// Collect hashes
|
||||
for (int i = 0; i < sha256recordsNumber; i++){
|
||||
rawData = new byte[0x20]; // 32 bytes - size of SHA256 hash
|
||||
if (raf.read(rawData) != -1)
|
||||
Pfs0SHA256hashes.add(rawData);
|
||||
else {
|
||||
raf.close();
|
||||
return; // TODO: fix
|
||||
}
|
||||
}
|
||||
raf.close();
|
||||
// Get pfs0
|
||||
pfs0 = new PFS0Provider(file, pfs0Location);
|
||||
}
|
||||
private void proceedPFS0Encrypted() throws Exception{/*
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r");
|
||||
long thisMediaLocation = offsetPosition + (ncaHeaderTableEntry.getMediaStartOffset() * 0x200);
|
||||
long hashTableLocation = thisMediaLocation + ncaFsHeader.getSuperBlockPFS0().getHashTableOffset();
|
||||
long pfs0Location = thisMediaLocation + ncaFsHeader.getSuperBlockPFS0().getPfs0offset();
|
||||
|
||||
raf.seek(hashTableLocation);
|
||||
|
||||
byte[] rawData;
|
||||
long sha256recordsNumber = ncaFsHeader.getSuperBlockPFS0().getHashTableSize() / 0x20;
|
||||
// Collect hashes
|
||||
for (int i = 0; i < sha256recordsNumber; i++){
|
||||
rawData = new byte[0x20]; // 32 bytes - size of SHA256 hash
|
||||
if (raf.read(rawData) != -1)
|
||||
Pfs0SHA256hashes.add(rawData);
|
||||
else {
|
||||
raf.close();
|
||||
return; // TODO: fix
|
||||
}
|
||||
}
|
||||
raf.close();
|
||||
// Get pfs0
|
||||
pfs0 = new PFS0Provider(file, pfs0Location);
|
||||
/*/
|
||||
new CryptoSection03Pfs0(file,
|
||||
offsetPosition,
|
||||
decryptedKey,
|
||||
ncaFsHeader,
|
||||
pfs0 = new PFS0Provider(file,
|
||||
ncaOffsetPosition,
|
||||
ncaFsHeader.getSuperBlockPFS0(),
|
||||
ncaHeaderTableEntry.getMediaStartOffset(),
|
||||
ncaHeaderTableEntry.getMediaEndOffset());
|
||||
//*/
|
||||
Pfs0SHA256hashes = pfs0.getPfs0SHA256hashes();
|
||||
}
|
||||
|
||||
private void proceedPFS0Encrypted() throws Exception{
|
||||
AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, ncaFsHeader.getSectionCTR(),
|
||||
ncaHeaderTableEntry.getMediaStartOffset() * 0x200);
|
||||
pfs0 = new PFS0Provider(file,
|
||||
ncaOffsetPosition,
|
||||
ncaFsHeader.getSuperBlockPFS0(),
|
||||
decryptor,
|
||||
ncaHeaderTableEntry.getMediaStartOffset(),
|
||||
ncaHeaderTableEntry.getMediaEndOffset());
|
||||
Pfs0SHA256hashes = pfs0.getPfs0SHA256hashes();
|
||||
}
|
||||
|
||||
private void proceedRomFs() throws Exception{
|
||||
|
@ -167,7 +129,7 @@ public class NCAContent {
|
|||
this.romfs = new RomFsEncryptedProvider(
|
||||
ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(),
|
||||
file,
|
||||
offsetPosition,
|
||||
ncaOffsetPosition,
|
||||
decryptedKey,
|
||||
ncaFsHeader.getSectionCTR(),
|
||||
ncaHeaderTableEntry.getMediaStartOffset(),
|
||||
|
@ -178,174 +140,6 @@ public class NCAContent {
|
|||
public IPFS0Provider getPfs0() { return pfs0; }
|
||||
public IRomFsProvider getRomfs() { return romfs; }
|
||||
|
||||
private class CryptoSection03Pfs0 {
|
||||
CryptoSection03Pfs0(File file,
|
||||
long offsetPosition,
|
||||
byte[] decryptedKey,
|
||||
NcaFsHeader ncaFsHeader,
|
||||
long mediaStartBlocksOffset,
|
||||
long mediaEndBlocksOffset) throws Exception
|
||||
{
|
||||
log.debug( "-== Crypto Section 03 PFS0 ==-\n" +
|
||||
"Media start location: " + RainbowDump.formatDecHexString(mediaStartBlocksOffset) + "\n" +
|
||||
"Media end location: " + RainbowDump.formatDecHexString(mediaEndBlocksOffset) + "\n" +
|
||||
"Media size: " + RainbowDump.formatDecHexString((mediaEndBlocksOffset-mediaStartBlocksOffset)) + "\n" +
|
||||
"Media actual location: " + RainbowDump.formatDecHexString((offsetPosition + (mediaStartBlocksOffset * 0x200))) + "\n" +
|
||||
"SHA256 hash table size: " + RainbowDump.formatDecHexString(ncaFsHeader.getSuperBlockPFS0().getHashTableSize()) + "\n" +
|
||||
"SHA256 hash table offs: " + RainbowDump.formatDecHexString(ncaFsHeader.getSuperBlockPFS0().getHashTableOffset()) + "\n" +
|
||||
"PFS0 Offset: " + RainbowDump.formatDecHexString(ncaFsHeader.getSuperBlockPFS0().getPfs0offset()) + "\n" +
|
||||
"SHA256 records: " + RainbowDump.formatDecHexString(ncaFsHeader.getSuperBlockPFS0().getHashTableSize() / 0x20) + "\n" +
|
||||
"KEY (decrypted): " + Converter.byteArrToHexString(decryptedKey) + "\n" +
|
||||
"CTR: " + Converter.byteArrToHexString(ncaFsHeader.getSectionCTR()) + "\n" +
|
||||
"-----------------------------------------------------------\n");
|
||||
if (decryptedKey == null)
|
||||
throw new Exception("CryptoSection03: unable to proceed. No decrypted key provided.");
|
||||
|
||||
RandomAccessFile raf = new RandomAccessFile(file, "r");
|
||||
long absoluteOffsetPosition = offsetPosition + (mediaStartBlocksOffset * 0x200);
|
||||
raf.seek(absoluteOffsetPosition);
|
||||
|
||||
AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, ncaFsHeader.getSectionCTR(),
|
||||
mediaStartBlocksOffset * 0x200);
|
||||
|
||||
byte[] encryptedBlock;
|
||||
byte[] decryptedBlock;
|
||||
long mediaBlocksSize = mediaEndBlocksOffset - mediaStartBlocksOffset;
|
||||
// Prepare thread to parse encrypted data
|
||||
PipedOutputStream streamOut = new PipedOutputStream();
|
||||
PipedInputStream streamInp = new PipedInputStream(streamOut);
|
||||
|
||||
Thread pThread = new Thread(new ParseThread(
|
||||
streamInp,
|
||||
ncaFsHeader.getSuperBlockPFS0().getPfs0offset(),
|
||||
ncaFsHeader.getSuperBlockPFS0().getHashTableOffset(),
|
||||
ncaFsHeader.getSuperBlockPFS0().getHashTableSize(),
|
||||
offsetPosition,
|
||||
file,
|
||||
decryptedKey,
|
||||
ncaFsHeader.getSectionCTR(),
|
||||
mediaStartBlocksOffset,
|
||||
mediaEndBlocksOffset
|
||||
));
|
||||
pThread.start();
|
||||
// Decrypt data
|
||||
for (int i = 0; i < mediaBlocksSize; i++){
|
||||
encryptedBlock = new byte[0x200];
|
||||
if (raf.read(encryptedBlock) != -1){
|
||||
//decryptedBlock = aesCtr.decrypt(encryptedBlock);
|
||||
decryptedBlock = decryptor.decryptNext(encryptedBlock);
|
||||
// Writing decrypted data to pipe
|
||||
try {
|
||||
streamOut.write(decryptedBlock);
|
||||
}
|
||||
catch (IOException e){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
pThread.join();
|
||||
streamOut.close();
|
||||
raf.close();
|
||||
}
|
||||
/**
|
||||
* Since we're representing decrypted data as stream (it's easier to look on it this way),
|
||||
* this thread will be parsing it.
|
||||
* */
|
||||
private class ParseThread implements Runnable{
|
||||
|
||||
PipedInputStream pipedInputStream;
|
||||
|
||||
long hashTableOffset;
|
||||
long hashTableSize;
|
||||
long hashTableRecordsCount;
|
||||
long pfs0offset;
|
||||
|
||||
private final long MetaOffsetPositionInFile;
|
||||
private final File MetaFileWithEncPFS0;
|
||||
private final byte[] MetaKey;
|
||||
private final byte[] MetaSectionCTR;
|
||||
private final long MetaMediaStartOffset;
|
||||
private final long MetaMediaEndOffset;
|
||||
|
||||
|
||||
ParseThread(PipedInputStream pipedInputStream,
|
||||
long pfs0offset,
|
||||
long hashTableOffset,
|
||||
long hashTableSize,
|
||||
|
||||
long MetaOffsetPositionInFile,
|
||||
File MetaFileWithEncPFS0,
|
||||
byte[] MetaKey,
|
||||
byte[] MetaSectionCTR,
|
||||
long MetaMediaStartOffset,
|
||||
long MetaMediaEndOffset
|
||||
){
|
||||
this.pipedInputStream = pipedInputStream;
|
||||
this.hashTableOffset = hashTableOffset;
|
||||
this.hashTableSize = hashTableSize;
|
||||
this.hashTableRecordsCount = hashTableSize / 0x20;
|
||||
this.pfs0offset = pfs0offset;
|
||||
|
||||
this.MetaOffsetPositionInFile = MetaOffsetPositionInFile;
|
||||
this.MetaFileWithEncPFS0 = MetaFileWithEncPFS0;
|
||||
this.MetaKey = MetaKey;
|
||||
this.MetaSectionCTR = MetaSectionCTR;
|
||||
this.MetaMediaStartOffset = MetaMediaStartOffset;
|
||||
this.MetaMediaEndOffset = MetaMediaEndOffset;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long counter = 0; // How many bytes already read
|
||||
|
||||
try{
|
||||
if (hashTableOffset > 0){
|
||||
if (hashTableOffset != pipedInputStream.skip(hashTableOffset))
|
||||
return; // TODO: fix?
|
||||
counter = hashTableOffset;
|
||||
}
|
||||
// Loop for collecting all recrods from sha256 hash table
|
||||
while ((counter - hashTableOffset) < hashTableSize){
|
||||
int hashCounter = 0;
|
||||
byte[] sectionHash = new byte[0x20];
|
||||
// Loop for collecting bytes for every SINGLE records, where record size == 0x20
|
||||
while (hashCounter < 0x20){
|
||||
int currentByte = pipedInputStream.read();
|
||||
if (currentByte == -1)
|
||||
break;
|
||||
sectionHash[hashCounter] = (byte)currentByte;
|
||||
hashCounter++;
|
||||
counter++;
|
||||
}
|
||||
// Write after collecting
|
||||
Pfs0SHA256hashes.add(sectionHash); // From the NCAContentProvider obviously
|
||||
}
|
||||
// Skip padding and go to PFS0 location
|
||||
if (counter < pfs0offset){
|
||||
long toSkip = pfs0offset-counter;
|
||||
if (toSkip != pipedInputStream.skip(toSkip))
|
||||
return; // TODO: fix?
|
||||
counter += toSkip;
|
||||
}
|
||||
//---------------------------------------------------------
|
||||
pfs0 = new PFS0EncryptedProvider(pipedInputStream,
|
||||
counter,
|
||||
MetaOffsetPositionInFile,
|
||||
MetaFileWithEncPFS0,
|
||||
MetaKey,
|
||||
MetaSectionCTR,
|
||||
MetaMediaStartOffset,
|
||||
MetaMediaEndOffset);
|
||||
pipedInputStream.close();
|
||||
}
|
||||
catch (Exception e){
|
||||
log.debug("NCA Content parsing thread exception: ", e);
|
||||
}
|
||||
//finally { System.out.println("NCA Content thread dies");}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export NCA content AS IS.
|
||||
|
@ -362,7 +156,7 @@ public class NCAContent {
|
|||
"Media start location: " + mediaStartBlocksOffset + "\n" +
|
||||
"Media end location: " + mediaEndBlocksOffset + "\n" +
|
||||
"Media size : " + (mediaEndBlocksOffset-mediaStartBlocksOffset) + "\n" +
|
||||
"Media act. location: " + (offsetPosition + (mediaStartBlocksOffset * 0x200)) + "\n" +
|
||||
"Media act. location: " + (ncaOffsetPosition + (mediaStartBlocksOffset * 0x200)) + "\n" +
|
||||
"KEY: " + Converter.byteArrToHexString(decryptedKey) + "\n" +
|
||||
"CTR: " + Converter.byteArrToHexString(ncaFsHeader.getSectionCTR()) + "\n");
|
||||
//---------------------------------------------------------------------------------------------------/
|
||||
|
@ -414,7 +208,7 @@ public class NCAContent {
|
|||
workerThread = new Thread(() -> {
|
||||
try {
|
||||
//RandomAccessFile raf = new RandomAccessFile(file, "r");
|
||||
long abosluteOffsetPosition = offsetPosition + (mediaStartBlocksOffset * 0x200);
|
||||
long abosluteOffsetPosition = ncaOffsetPosition + (mediaStartBlocksOffset * 0x200);
|
||||
raf.seek(abosluteOffsetPosition);
|
||||
|
||||
AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey,
|
||||
|
|
|
@ -283,7 +283,8 @@ public class NCAProvider {
|
|||
key = cipher.doFinal(rightsIDkey);
|
||||
}
|
||||
catch (Exception e){
|
||||
throw new Exception("No title.keys loaded?", e);
|
||||
throw new Exception("No title.keys loaded for '"+
|
||||
String.format("titlekek_%02x", cryptoTypeReal)+"' or '"+byteArrToHexString(rightsId)+"'? ("+e+")", e);
|
||||
}
|
||||
}
|
||||
getNcaContentByNumber(0, key);
|
||||
|
|
|
@ -48,6 +48,7 @@ public class NcaFsHeader {
|
|||
private final BucketTreeHeader BktrSection2;
|
||||
|
||||
private final byte[] generation;
|
||||
private final byte[] secureValue;
|
||||
private final byte[] sectionCTR;
|
||||
private final SparseInfo sparseInfo;
|
||||
private final CompressionInfo compressionInfo;
|
||||
|
@ -79,7 +80,10 @@ public class NcaFsHeader {
|
|||
BktrSection2 = new BucketTreeHeader(Arrays.copyOfRange(tableBlockBytes, 0x130, 0x140));
|
||||
|
||||
generation = Arrays.copyOfRange(tableBlockBytes, 0x140, 0x144);
|
||||
sectionCTR = Arrays.copyOfRange(tableBlockBytes, 0x144, 0x148);
|
||||
secureValue = Arrays.copyOfRange(tableBlockBytes, 0x144, 0x148);
|
||||
|
||||
sectionCTR = Arrays.copyOfRange(tableBlockBytes, 0x140, 0x148);
|
||||
|
||||
sparseInfo = new SparseInfo(Arrays.copyOfRange(tableBlockBytes, 0x148, 0x178));
|
||||
compressionInfo = new CompressionInfo(Arrays.copyOfRange(tableBlockBytes, 0x178, 0x1a0));
|
||||
metaDataHashDataInfo = new MetaDataHashDataInfo(Arrays.copyOfRange(tableBlockBytes, 0x1a0, 0x1d0));
|
||||
|
@ -109,6 +113,10 @@ public class NcaFsHeader {
|
|||
public int getEntryCountSection2() { return BktrSection2.getEntryCount(); }
|
||||
public byte[] getPatchInfoUnknownSection2() { return BktrSection2.getUnknown(); }
|
||||
public byte[] getGeneration() {return generation;}
|
||||
public byte[] getSecureValue() {return secureValue;}
|
||||
/**
|
||||
* Used for Aes Ctr decryption in IV context.
|
||||
* */
|
||||
public byte[] getSectionCTR() { return sectionCTR; }
|
||||
public SparseInfo getSparseInfo() {return sparseInfo;}
|
||||
public CompressionInfo getCompressionInfo() {return compressionInfo;}
|
||||
|
|
|
@ -20,6 +20,8 @@ package libKonogonka.Tools.PFS0;
|
|||
|
||||
import libKonogonka.Tools.ISuperProvider;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
public interface IPFS0Provider extends ISuperProvider {
|
||||
boolean isEncrypted();
|
||||
String getMagic();
|
||||
|
@ -30,4 +32,5 @@ public interface IPFS0Provider extends ISuperProvider {
|
|||
PFS0subFile[] getPfs0subFiles();
|
||||
|
||||
void printDebug();
|
||||
LinkedList<byte[]> getPfs0SHA256hashes();
|
||||
}
|
||||
|
|
|
@ -1,343 +0,0 @@
|
|||
/*
|
||||
Copyright 2019-2022 Dmitry Isaenko
|
||||
|
||||
This file is part of libKonogonka.
|
||||
|
||||
libKonogonka is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libKonogonka is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libKonogonka. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package libKonogonka.Tools.PFS0;
|
||||
|
||||
import libKonogonka.Converter;
|
||||
import libKonogonka.RainbowDump;
|
||||
import libKonogonka.Tools.RomFs.Level6Header;
|
||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static libKonogonka.Converter.*;
|
||||
|
||||
public class PFS0EncryptedProvider implements IPFS0Provider{
|
||||
private final static Logger log = LogManager.getLogger(PFS0EncryptedProvider.class);
|
||||
|
||||
//private long rawFileDataStart; // Always -1 @ PFS0EncryptedProvider
|
||||
|
||||
private final String magic;
|
||||
private final int filesCount;
|
||||
private final int stringTableSize;
|
||||
private final byte[] padding;
|
||||
private final PFS0subFile[] pfs0subFiles;
|
||||
|
||||
//---------------------------------------
|
||||
|
||||
private long rawBlockDataStart;
|
||||
|
||||
private final long offsetPositionInFile;
|
||||
private final File file;
|
||||
private final byte[] key;
|
||||
private final byte[] sectionCTR;
|
||||
private final long mediaStartOffset; // In 512-blocks
|
||||
private final long mediaEndOffset; // In 512-blocks
|
||||
|
||||
public PFS0EncryptedProvider(PipedInputStream pipedInputStream,
|
||||
long pfs0offsetPosition,
|
||||
long offsetPositionInFile,
|
||||
File fileWithEncPFS0,
|
||||
byte[] key,
|
||||
byte[] sectionCTR,
|
||||
long mediaStartOffset,
|
||||
long mediaEndOffset
|
||||
) throws Exception{
|
||||
// Populate 'meta' data that is needed for getProviderSubFilePipedInpStream()
|
||||
this.offsetPositionInFile = offsetPositionInFile + mediaStartOffset*0x200;
|
||||
this.file = fileWithEncPFS0;
|
||||
this.key = key;
|
||||
this.sectionCTR = sectionCTR;
|
||||
this.mediaStartOffset = mediaStartOffset;
|
||||
this.mediaEndOffset = mediaEndOffset;
|
||||
// pfs0offsetPosition is a position relative to Media block. Let's add pfs0 'header's' bytes count and get raw data start position in media block
|
||||
//rawFileDataStart = -1; // Set -1 for PFS0EncryptedProvider
|
||||
// Detect raw data start position using next var
|
||||
rawBlockDataStart = pfs0offsetPosition;
|
||||
|
||||
byte[] fileStartingBytes = new byte[0x10];
|
||||
// Read PFS0Provider, files count, header, padding (4 zero bytes)
|
||||
|
||||
for (int i = 0; i < 0x10; i++){
|
||||
int currentByte = pipedInputStream.read();
|
||||
if (currentByte == -1) {
|
||||
throw new Exception("PFS0EncryptedProvider: Reading stream suddenly ended while trying to read starting 0x10 bytes");
|
||||
}
|
||||
fileStartingBytes[i] = (byte)currentByte;
|
||||
}
|
||||
// Update position
|
||||
rawBlockDataStart += 0x10;
|
||||
// Check PFS0Provider
|
||||
magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII);
|
||||
if (! magic.equals("PFS0")){
|
||||
throw new Exception("PFS0EncryptedProvider: Bad magic");
|
||||
}
|
||||
// Get files count
|
||||
filesCount = getLEint(fileStartingBytes, 0x4);
|
||||
if (filesCount <= 0 ) {
|
||||
throw new Exception("PFS0EncryptedProvider: Files count is too small");
|
||||
}
|
||||
// Get string table
|
||||
stringTableSize = getLEint(fileStartingBytes, 0x8);
|
||||
if (stringTableSize <= 0 ){
|
||||
throw new Exception("PFS0EncryptedProvider: String table is too small");
|
||||
}
|
||||
padding = Arrays.copyOfRange(fileStartingBytes, 0xc, 0x10);
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
pfs0subFiles = new PFS0subFile[filesCount];
|
||||
|
||||
long[] offsetsSubFiles = new long[filesCount];
|
||||
long[] sizesSubFiles = new long[filesCount];
|
||||
int[] strTableOffsets = new int[filesCount];
|
||||
byte[][] zeroBytes = new byte[filesCount][];
|
||||
|
||||
byte[] fileEntryTable = new byte[0x18];
|
||||
for (int i=0; i < filesCount; i++){
|
||||
for (int j = 0; j < 0x18; j++){
|
||||
int currentByte = pipedInputStream.read();
|
||||
if (currentByte == -1) {
|
||||
throw new Exception("PFS0EncryptedProvider: Reading stream suddenly ended while trying to read File Entry Table #"+i);
|
||||
}
|
||||
fileEntryTable[j] = (byte)currentByte;
|
||||
}
|
||||
offsetsSubFiles[i] = getLElong(fileEntryTable, 0);
|
||||
sizesSubFiles[i] = getLElong(fileEntryTable, 0x8);
|
||||
strTableOffsets[i] = getLEint(fileEntryTable, 0x10);
|
||||
zeroBytes[i] = Arrays.copyOfRange(fileEntryTable, 0x14, 0x18);
|
||||
// Update position
|
||||
rawBlockDataStart += 0x18;
|
||||
}
|
||||
//**********************************************************************************************************
|
||||
// In here pointer in front of String table
|
||||
String[] subFileNames = new String[filesCount];
|
||||
byte[] stringTbl = new byte[stringTableSize];
|
||||
|
||||
for (int i = 0; i < stringTableSize; i++){
|
||||
int currentByte = pipedInputStream.read();
|
||||
if (currentByte == -1) {
|
||||
throw new Exception("PFS0EncryptedProvider: Reading stream suddenly ended while trying to read string table");
|
||||
}
|
||||
stringTbl[i] = (byte)currentByte;
|
||||
}
|
||||
// Update position
|
||||
rawBlockDataStart += stringTableSize;
|
||||
|
||||
for (int i=0; i < filesCount; i++){
|
||||
int j = 0;
|
||||
while (stringTbl[strTableOffsets[i]+j] != (byte)0x00)
|
||||
j++;
|
||||
subFileNames[i] = new String(stringTbl, strTableOffsets[i], j, StandardCharsets.UTF_8);
|
||||
}
|
||||
for (int i = 0; i < filesCount; i++){
|
||||
pfs0subFiles[i] = new PFS0subFile(
|
||||
subFileNames[i],
|
||||
offsetsSubFiles[i],
|
||||
sizesSubFiles[i],
|
||||
zeroBytes[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() { return true; }
|
||||
@Override
|
||||
public String getMagic() { return magic; }
|
||||
@Override
|
||||
public int getFilesCount() { return filesCount; }
|
||||
@Override
|
||||
public int getStringTableSize() { return stringTableSize; }
|
||||
@Override
|
||||
public byte[] getPadding() { return padding; }
|
||||
@Override
|
||||
public long getRawFileDataStart() { return rawBlockDataStart; }
|
||||
@Override
|
||||
public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; }
|
||||
@Override
|
||||
public File getFile(){ return file; }
|
||||
@Override
|
||||
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception { // TODO: rewrite
|
||||
if (subFileNumber >= pfs0subFiles.length)
|
||||
throw new Exception("PFS0Provider -> getPfs0subFilePipedInpStream(): Requested sub file doesn't exists");
|
||||
|
||||
Thread workerThread;
|
||||
PipedOutputStream streamOut = new PipedOutputStream();
|
||||
|
||||
PipedInputStream streamIn = new PipedInputStream(streamOut);
|
||||
workerThread = new Thread(() -> {
|
||||
log.debug("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Executing thread:\nSub file: " +
|
||||
pfs0subFiles[subFileNumber].getName() +
|
||||
"\nFor block # "+((rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) / 0x200) +
|
||||
"\nAnd initial skipped bytes are: "+offsetPositionInFile +
|
||||
"\nWhere Raw Block Data Start: "+rawBlockDataStart +
|
||||
"\nAnd sub file offset: "+pfs0subFiles[subFileNumber].getOffset()+
|
||||
"\nSkip bytes "+((rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) - ((rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) / 0x200) * 0x200)+
|
||||
"\nKEY "+Converter.byteArrToHexString(key)+
|
||||
"\nSection CTR "+Converter.byteArrToHexString(sectionCTR)+
|
||||
"\n______________________________________________________________");
|
||||
try {
|
||||
BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
||||
// Check if skip was successful
|
||||
if (bis.skip(offsetPositionInFile) != offsetPositionInFile) {
|
||||
System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Failed to skip range "+offsetPositionInFile);
|
||||
return;
|
||||
}
|
||||
|
||||
AesCtrDecryptSimple aesCtrDecryptSimple = new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200);
|
||||
|
||||
byte[] encryptedBlock;
|
||||
byte[] dectyptedBlock;
|
||||
|
||||
//----------------------------- Pre-set: skip non-necessary data --------------------------------
|
||||
|
||||
long startBlock = (rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) / 0x200; // <- pointing to place where actual data starts
|
||||
int skipBytes;
|
||||
|
||||
if (startBlock > 0) {
|
||||
aesCtrDecryptSimple.skipNext(startBlock);
|
||||
skipBytes = (int)(startBlock * 0x200);
|
||||
if (bis.skip(skipBytes) != skipBytes) {
|
||||
System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Failed to skip range "+skipBytes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------- Step 1: get starting bytes from the end of the junk block --------------------------------
|
||||
|
||||
// Since our data could be located in position with some offset from the decrypted block, let's skip bytes left. Considering the case when data is not aligned to block
|
||||
skipBytes = (int) ((rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) - startBlock * 0x200); // <- How much bytes shall we skip to reach requested data start of sub-file
|
||||
|
||||
if (skipBytes > 0) {
|
||||
encryptedBlock = new byte[0x200];
|
||||
if (bis.read(encryptedBlock) == 0x200) {
|
||||
dectyptedBlock = aesCtrDecryptSimple.decryptNext(encryptedBlock);
|
||||
// If we have extra-small file that is less then a block and even more
|
||||
if ((0x200 - skipBytes) > pfs0subFiles[subFileNumber].getSize()){
|
||||
streamOut.write(dectyptedBlock, skipBytes, (int) pfs0subFiles[subFileNumber].getSize()); // safe cast
|
||||
bis.close();
|
||||
streamOut.close();
|
||||
return;
|
||||
}
|
||||
else
|
||||
streamOut.write(dectyptedBlock, skipBytes, 0x200 - skipBytes);
|
||||
}
|
||||
else {
|
||||
System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from 1st bock");
|
||||
return;
|
||||
}
|
||||
startBlock++;
|
||||
}
|
||||
long endBlock = pfs0subFiles[subFileNumber].getSize() / 0x200 + startBlock; // <- pointing to place where any data related to this media-block ends
|
||||
|
||||
//----------------------------- Step 2: Detect if we have junk data on the end of the final block --------------------------------
|
||||
int extraData = (int)(rawBlockDataStart+pfs0subFiles[subFileNumber].getOffset()+pfs0subFiles[subFileNumber].getSize() - (endBlock*0x200)); // safe cast
|
||||
if (extraData < 0){
|
||||
endBlock--;
|
||||
}
|
||||
//----------------------------- Step 3: Read main part of data --------------------------------
|
||||
// Here we're reading main amount of bytes. We can read only less bytes.
|
||||
while ( startBlock < endBlock) {
|
||||
encryptedBlock = new byte[0x200];
|
||||
if (bis.read(encryptedBlock) == 0x200) {
|
||||
//dectyptedBlock = aesCtr.decrypt(encryptedBlock);
|
||||
dectyptedBlock = aesCtrDecryptSimple.decryptNext(encryptedBlock);
|
||||
// Writing decrypted data to pipe
|
||||
streamOut.write(dectyptedBlock);
|
||||
}
|
||||
else {
|
||||
System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from bock");
|
||||
return;
|
||||
}
|
||||
startBlock++;
|
||||
}
|
||||
//----------------------------- Step 4: Read what's left --------------------------------
|
||||
// Now we have to find out if data overlaps to one more extra block
|
||||
if (extraData > 0){ // In case we didn't get what we want
|
||||
encryptedBlock = new byte[0x200];
|
||||
if (bis.read(encryptedBlock) == 0x200) {
|
||||
dectyptedBlock = aesCtrDecryptSimple.decryptNext(encryptedBlock);
|
||||
streamOut.write(dectyptedBlock, 0, extraData);
|
||||
}
|
||||
else {
|
||||
System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from bock");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (extraData < 0){ // In case we can get more than we need
|
||||
encryptedBlock = new byte[0x200];
|
||||
if (bis.read(encryptedBlock) == 0x200) {
|
||||
dectyptedBlock = aesCtrDecryptSimple.decryptNext(encryptedBlock);
|
||||
streamOut.write(dectyptedBlock, 0, 0x200 + extraData); // WTF ??? THIS LOOKS INCORRECT
|
||||
}
|
||||
else {
|
||||
System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from last bock");
|
||||
return;
|
||||
}
|
||||
}
|
||||
bis.close();
|
||||
streamOut.close();
|
||||
}
|
||||
catch (Exception e){
|
||||
System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): "+e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Thread died");
|
||||
|
||||
|
||||
});
|
||||
workerThread.start();
|
||||
return streamIn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception{
|
||||
for (int i = 0; i < pfs0subFiles.length; i++){
|
||||
if (pfs0subFiles[i].getName().equals(subFileName))
|
||||
return getProviderSubFilePipedInpStream(i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void printDebug(){
|
||||
log.debug(".:: PFS0EncryptedProvider ::.\n" +
|
||||
"File name: " + file.getName() + "\n" +
|
||||
"Raw block data start " + RainbowDump.formatDecHexString(rawBlockDataStart) + "\n" +
|
||||
"Magic " + magic + "\n" +
|
||||
"Files count " + RainbowDump.formatDecHexString(filesCount) + "\n" +
|
||||
"String Table Size " + RainbowDump.formatDecHexString(stringTableSize) + "\n" +
|
||||
"Padding " + Converter.byteArrToHexString(padding) + "\n\n" +
|
||||
|
||||
"Offset position in file " + RainbowDump.formatDecHexString(offsetPositionInFile) + "\n" +
|
||||
"Media Start Offset " + RainbowDump.formatDecHexString(mediaStartOffset) + "\n" +
|
||||
"Media End Offset " + RainbowDump.formatDecHexString(mediaEndOffset) + "\n"
|
||||
);
|
||||
for (PFS0subFile subFile : pfs0subFiles){
|
||||
log.debug(
|
||||
"\nName: " + subFile.getName() + "\n" +
|
||||
"Offset " + RainbowDump.formatDecHexString(subFile.getOffset()) + "\n" +
|
||||
"Size " + RainbowDump.formatDecHexString(subFile.getSize()) + "\n" +
|
||||
"Zeroes " + Converter.byteArrToHexString(subFile.getZeroes()) + "\n" +
|
||||
"----------------------------------------------------------------"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ package libKonogonka.Tools.PFS0;
|
|||
|
||||
import libKonogonka.Converter;
|
||||
import libKonogonka.RainbowDump;
|
||||
import libKonogonka.Tools.NCA.NCASectionTableBlock.SuperBlockPFS0;
|
||||
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
|
@ -28,93 +29,124 @@ import org.apache.logging.log4j.Logger;
|
|||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import static libKonogonka.Converter.*;
|
||||
import static libKonogonka.Converter.getLEint;
|
||||
import static libKonogonka.Converter.getLElong;
|
||||
|
||||
public class PFS0Provider implements IPFS0Provider{
|
||||
private final static Logger log = LogManager.getLogger(PFS0Provider.class);
|
||||
|
||||
private long rawFileDataStartOffset;
|
||||
|
||||
private String magic;
|
||||
private int filesCount;
|
||||
private int stringTableSize;
|
||||
private byte[] padding;
|
||||
private PFS0subFile[] pfs0subFiles;
|
||||
//---------------------------------------
|
||||
private long rawBlockDataStart;
|
||||
|
||||
private final File file;
|
||||
private final long offsetPosition; // Where data starts, excluding header, string table etc.
|
||||
private long mediaStartOffset;
|
||||
private long mediaEndOffset;
|
||||
private long offsetPositionInFile;
|
||||
private long mediaStartOffset; // In 512-blocks
|
||||
private long mediaEndOffset; // In 512-blocks
|
||||
|
||||
private long ncaOffset;
|
||||
private BufferedInputStream stream;
|
||||
private SuperBlockPFS0 superBlockPFS0;
|
||||
private AesCtrDecryptSimple decryptor;
|
||||
|
||||
private final boolean encrypted;
|
||||
private LinkedList<byte[]> pfs0SHA256hashes;
|
||||
|
||||
public PFS0Provider(File fileWithPfs0,
|
||||
long offsetPosition,
|
||||
private boolean encrypted;
|
||||
|
||||
public PFS0Provider(File nspFile) throws Exception{
|
||||
this.file = nspFile;
|
||||
createBufferedInputStream();
|
||||
readPfs0Header();
|
||||
}
|
||||
|
||||
public PFS0Provider(File file,
|
||||
long ncaOffset,
|
||||
SuperBlockPFS0 superBlockPFS0,
|
||||
long mediaStartOffset,
|
||||
long mediaEndOffset,
|
||||
AesCtrDecryptSimple decryptor) throws Exception{
|
||||
this.file = fileWithPfs0;
|
||||
this.offsetPosition = offsetPosition + mediaStartOffset*0x200;
|
||||
this.encrypted = true;
|
||||
|
||||
long mediaEndOffset) throws Exception{
|
||||
this.file = file;
|
||||
this.ncaOffset = ncaOffset;
|
||||
this.superBlockPFS0 = superBlockPFS0;
|
||||
this.offsetPositionInFile = ncaOffset + mediaStartOffset * 0x200;
|
||||
this.mediaStartOffset = mediaStartOffset;
|
||||
this.mediaEndOffset = mediaEndOffset;
|
||||
this.decryptor = decryptor;
|
||||
proceedPfs0();
|
||||
}
|
||||
|
||||
public PFS0Provider(File fileWithPfs0) throws Exception{ this(fileWithPfs0, 0); }
|
||||
|
||||
public PFS0Provider(File fileWithPfs0, long offsetPosition) throws Exception{
|
||||
this.file = fileWithPfs0;
|
||||
this.offsetPosition = offsetPosition;
|
||||
this.encrypted = false;
|
||||
this.rawBlockDataStart = superBlockPFS0.getPfs0offset();
|
||||
//bufferedInputStream = new BufferedInputStream(Files.newInputStream(fileWithPfs0.toPath()));
|
||||
proceedPfs0();
|
||||
createBufferedInputStream();
|
||||
long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
|
||||
if (toSkip != stream.skip(toSkip))
|
||||
throw new Exception("Can't skip bytes prior Hash Table offset");
|
||||
collectHashes();
|
||||
|
||||
createBufferedInputStream();
|
||||
toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
|
||||
if (toSkip != stream.skip(toSkip))
|
||||
throw new Exception("Can't skip bytes prior PFS0 offset");
|
||||
readPfs0Header();
|
||||
}
|
||||
private void proceedPfs0() throws Exception{
|
||||
BufferedInputStream bufferedInputStream;
|
||||
|
||||
if (encrypted) {
|
||||
bufferedInputStream = new AesCtrBufferedInputStream(decryptor,
|
||||
offsetPosition,
|
||||
mediaStartOffset,
|
||||
mediaEndOffset,
|
||||
Files.newInputStream(file.toPath()));
|
||||
}
|
||||
else{
|
||||
bufferedInputStream = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
||||
}
|
||||
public PFS0Provider(File file,
|
||||
long ncaOffset,
|
||||
SuperBlockPFS0 superBlockPFS0,
|
||||
AesCtrDecryptSimple decryptor,
|
||||
long mediaStartOffset,
|
||||
long mediaEndOffset
|
||||
) throws Exception {
|
||||
this.file = file;
|
||||
this.ncaOffset = ncaOffset;
|
||||
this.superBlockPFS0 = superBlockPFS0;
|
||||
this.decryptor = decryptor;
|
||||
this.offsetPositionInFile = ncaOffset + mediaStartOffset * 0x200;
|
||||
this.mediaStartOffset = mediaStartOffset;
|
||||
this.mediaEndOffset = mediaEndOffset;
|
||||
this.rawBlockDataStart = superBlockPFS0.getPfs0offset();
|
||||
this.encrypted = true;
|
||||
|
||||
if (offsetPosition != bufferedInputStream.skip(offsetPosition))
|
||||
throw new Exception("PFS0Provider: Unable to skip initial offset: "+offsetPosition);
|
||||
createAesCtrEncryptedBufferedInputStream();
|
||||
long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
|
||||
if (toSkip != stream.skip(toSkip))
|
||||
throw new Exception("Can't skip bytes prior Hash Table offset");
|
||||
collectHashes();
|
||||
|
||||
createAesCtrEncryptedBufferedInputStream();
|
||||
toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
|
||||
if (toSkip != stream.skip(toSkip))
|
||||
throw new Exception("Can't skip bytes prior PFS0 offset");
|
||||
readPfs0Header();
|
||||
}
|
||||
|
||||
private void readPfs0Header()throws Exception{
|
||||
byte[] fileStartingBytes = new byte[0x10];
|
||||
// Read PFS0Provider, files count, header, padding (4 zero bytes)
|
||||
if (bufferedInputStream.read(fileStartingBytes) != 0x10){
|
||||
throw new Exception("PFS0Provider: Unable to read starting bytes");
|
||||
}
|
||||
rawFileDataStartOffset += 0x10;
|
||||
if (0x10 != stream.read(fileStartingBytes))
|
||||
throw new Exception("Reading stream suddenly ended while trying to read starting 0x10 bytes");
|
||||
|
||||
// Update position
|
||||
rawBlockDataStart += 0x10;
|
||||
// Check PFS0Provider
|
||||
magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII);
|
||||
if (! magic.equals("PFS0")){
|
||||
throw new Exception("PFS0Provider: Bad magic");
|
||||
throw new Exception("Bad magic");
|
||||
}
|
||||
// Get files count
|
||||
filesCount = getLEint(fileStartingBytes, 0x4);
|
||||
if (filesCount <= 0 ) {
|
||||
throw new Exception("PFS0Provider: Files count is too small");
|
||||
throw new Exception("Files count is too small");
|
||||
}
|
||||
// Get string table
|
||||
stringTableSize = getLEint(fileStartingBytes, 0x8);
|
||||
if (stringTableSize <= 0 ){
|
||||
throw new Exception("PFS0Provider: String table is too small");
|
||||
throw new Exception("String table is too small");
|
||||
}
|
||||
padding = Arrays.copyOfRange(fileStartingBytes, 0xc, 0x10);
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
//-------------------------------------------------------------------
|
||||
pfs0subFiles = new PFS0subFile[filesCount];
|
||||
|
||||
long[] offsetsSubFiles = new long[filesCount];
|
||||
|
@ -123,23 +155,27 @@ public class PFS0Provider implements IPFS0Provider{
|
|||
byte[][] zeroBytes = new byte[filesCount][];
|
||||
|
||||
byte[] fileEntryTable = new byte[0x18];
|
||||
for (int i=0; i<filesCount; i++){
|
||||
if (bufferedInputStream.read(fileEntryTable) != 0x18)
|
||||
throw new Exception("PFS0Provider: String table is too small");
|
||||
for (int i=0; i < filesCount; i++){
|
||||
if (0x18 != stream.read(fileEntryTable))
|
||||
throw new Exception("Reading stream suddenly ended while trying to read File Entry Table #"+i);
|
||||
|
||||
offsetsSubFiles[i] = getLElong(fileEntryTable, 0);
|
||||
sizesSubFiles[i] = getLElong(fileEntryTable, 0x8);
|
||||
strTableOffsets[i] = getLEint(fileEntryTable, 0x10);
|
||||
zeroBytes[i] = Arrays.copyOfRange(fileEntryTable, 0x14, 0x18);
|
||||
rawFileDataStartOffset += 0x18;
|
||||
rawBlockDataStart += 0x18;
|
||||
}
|
||||
//**********************************************************************************************************
|
||||
//*******************************************************************
|
||||
// In here pointer in front of String table
|
||||
String[] subFileNames = new String[filesCount];
|
||||
byte[] stringTbl = new byte[stringTableSize];
|
||||
if (bufferedInputStream.read(stringTbl) != stringTableSize){
|
||||
if (stream.read(stringTbl) != stringTableSize){
|
||||
throw new Exception("Read PFS0Provider String table failure. Can't read requested string table size ("+stringTableSize+")");
|
||||
}
|
||||
rawFileDataStartOffset += stringTableSize;
|
||||
|
||||
// Update position
|
||||
rawBlockDataStart += stringTableSize;
|
||||
|
||||
for (int i=0; i < filesCount; i++){
|
||||
int j = 0;
|
||||
while (stringTbl[strTableOffsets[i]+j] != (byte)0x00)
|
||||
|
@ -151,14 +187,44 @@ public class PFS0Provider implements IPFS0Provider{
|
|||
subFileNames[i],
|
||||
offsetsSubFiles[i],
|
||||
sizesSubFiles[i],
|
||||
zeroBytes[i]
|
||||
);
|
||||
zeroBytes[i]);
|
||||
}
|
||||
stream.close();
|
||||
}
|
||||
|
||||
private void createAesCtrEncryptedBufferedInputStream() throws Exception{
|
||||
decryptor.reset();
|
||||
this.stream = new AesCtrBufferedInputStream(
|
||||
decryptor,
|
||||
ncaOffset,
|
||||
mediaStartOffset,
|
||||
mediaEndOffset,
|
||||
Files.newInputStream(file.toPath()));
|
||||
}
|
||||
|
||||
private void createBufferedInputStream() throws Exception{
|
||||
this.stream = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
||||
}
|
||||
|
||||
private void collectHashes() throws Exception{
|
||||
pfs0SHA256hashes = new LinkedList<>();
|
||||
long hashTableOffset = superBlockPFS0.getHashTableOffset();
|
||||
long hashTableSize = superBlockPFS0.getHashTableSize();
|
||||
|
||||
if (hashTableOffset > 0){
|
||||
if (hashTableOffset != stream.skip(hashTableOffset))
|
||||
throw new Exception("Unable to skip bytes till Hash Table Offset: "+hashTableOffset);
|
||||
}
|
||||
for (int i = 0; i < hashTableSize / 0x20; i++){
|
||||
byte[] sectionHash = new byte[0x20];
|
||||
if (0x20 != stream.read(sectionHash))
|
||||
throw new Exception("Unable to read hash");
|
||||
pfs0SHA256hashes.add(sectionHash);
|
||||
}
|
||||
bufferedInputStream.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() { return encrypted; }
|
||||
public boolean isEncrypted() { return true; }
|
||||
@Override
|
||||
public String getMagic() { return magic; }
|
||||
@Override
|
||||
|
@ -168,80 +234,90 @@ public class PFS0Provider implements IPFS0Provider{
|
|||
@Override
|
||||
public byte[] getPadding() { return padding; }
|
||||
@Override
|
||||
public long getRawFileDataStart() { return rawFileDataStartOffset; }
|
||||
public long getRawFileDataStart() { return rawBlockDataStart;}
|
||||
@Override
|
||||
public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; }
|
||||
@Override
|
||||
public File getFile(){ return file; }
|
||||
|
||||
@Override
|
||||
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception{ // TODO: Throw exceptions?
|
||||
if (subFileNumber >= pfs0subFiles.length) {
|
||||
throw new Exception("PFS0Provider -> getPfs0subFilePipedInpStream(): Requested sub file doesn't exists");
|
||||
}
|
||||
PipedOutputStream streamOut = new PipedOutputStream();
|
||||
Thread workerThread;
|
||||
|
||||
PipedInputStream streamIn = new PipedInputStream(streamOut);
|
||||
|
||||
workerThread = new Thread(() -> {
|
||||
System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Executing thread");
|
||||
try {
|
||||
long subFileRealPosition = rawFileDataStartOffset + pfs0subFiles[subFileNumber].getOffset();
|
||||
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
|
||||
if (bis.skip(subFileRealPosition) != subFileRealPosition) {
|
||||
System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Unable to skip requested offset");
|
||||
return;
|
||||
}
|
||||
|
||||
int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576
|
||||
|
||||
long readFrom = 0;
|
||||
long realFileSize = pfs0subFiles[subFileNumber].getSize();
|
||||
|
||||
byte[] readBuf;
|
||||
|
||||
while (readFrom < realFileSize) {
|
||||
if (realFileSize - readFrom < readPice)
|
||||
readPice = Math.toIntExact(realFileSize - readFrom); // it's safe, I guarantee
|
||||
readBuf = new byte[readPice];
|
||||
if (bis.read(readBuf) != readPice) {
|
||||
System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Unable to read requested size from file.");
|
||||
return;
|
||||
}
|
||||
streamOut.write(readBuf);
|
||||
readFrom += readPice;
|
||||
}
|
||||
bis.close();
|
||||
streamOut.close();
|
||||
} catch (IOException ioe) {
|
||||
System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Unable to provide stream");
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Thread died");
|
||||
});
|
||||
workerThread.start();
|
||||
return streamIn;
|
||||
}
|
||||
/**
|
||||
* Some sugar
|
||||
* */
|
||||
@Override
|
||||
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception {
|
||||
public boolean exportContent(String saveToLocation, String subFileName){
|
||||
for (int i = 0; i < pfs0subFiles.length; i++){
|
||||
if (pfs0subFiles[i].getName().equals(subFileName))
|
||||
return getProviderSubFilePipedInpStream(i);
|
||||
return exportContent(saveToLocation, i);
|
||||
}
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public boolean exportContent(String saveToLocation, int subFileNumber){
|
||||
PFS0subFile subFile = pfs0subFiles[subFileNumber];
|
||||
File location = new File(saveToLocation);
|
||||
location.mkdirs();
|
||||
|
||||
try (BufferedOutputStream extractedFileBOS = new BufferedOutputStream(
|
||||
Files.newOutputStream(Paths.get(saveToLocation+File.separator+subFile.getName())))){
|
||||
if (encrypted)
|
||||
createAesCtrEncryptedBufferedInputStream();
|
||||
else
|
||||
createBufferedInputStream();
|
||||
|
||||
long subFileSize = subFile.getSize();
|
||||
|
||||
long toSkip = subFile.getOffset() + mediaStartOffset * 0x200 + rawBlockDataStart;
|
||||
if (toSkip != stream.skip(toSkip))
|
||||
throw new Exception("Unable to skip offset: "+toSkip);
|
||||
|
||||
int blockSize = 0x200;
|
||||
if (subFileSize < 0x200)
|
||||
blockSize = (int) subFileSize;
|
||||
|
||||
long i = 0;
|
||||
byte[] block = new byte[blockSize];
|
||||
|
||||
int actuallyRead;
|
||||
while (true) {
|
||||
if ((actuallyRead = stream.read(block)) != blockSize)
|
||||
throw new Exception("Read failure. Block Size: "+blockSize+", actuallyRead: "+actuallyRead);
|
||||
extractedFileBOS.write(block);
|
||||
i += blockSize;
|
||||
if ((i + blockSize) > subFileSize) {
|
||||
blockSize = (int) (subFileSize - i);
|
||||
if (blockSize == 0)
|
||||
break;
|
||||
block = new byte[blockSize];
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e){
|
||||
log.error("File export failure", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//TODO: REMOVE
|
||||
@Override
|
||||
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception {return null;}
|
||||
@Override
|
||||
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception {return null;}
|
||||
|
||||
|
||||
public LinkedList<byte[]> getPfs0SHA256hashes() {
|
||||
return pfs0SHA256hashes;
|
||||
}
|
||||
|
||||
public void printDebug(){
|
||||
log.debug(".:: PFS0Provider ::.\n" +
|
||||
"File name: " + file.getName() + "\n\n" +
|
||||
"Raw file data start: " + RainbowDump.formatDecHexString(rawFileDataStartOffset) + "\n" +
|
||||
"File name: " + file.getName() + "\n" +
|
||||
"Raw block data start " + RainbowDump.formatDecHexString(rawBlockDataStart) + "\n" +
|
||||
"Magic " + magic + "\n" +
|
||||
"Files count " + RainbowDump.formatDecHexString(filesCount) + "\n" +
|
||||
"String Table Size " + RainbowDump.formatDecHexString(stringTableSize) + "\n" +
|
||||
"Padding " + Converter.byteArrToHexString(padding) + "\n"
|
||||
"Padding " + Converter.byteArrToHexString(padding) + "\n\n" +
|
||||
|
||||
"Offset position in file " + RainbowDump.formatDecHexString(offsetPositionInFile) + "\n" +
|
||||
"Media Start Offset " + RainbowDump.formatDecHexString(mediaStartOffset) + "\n" +
|
||||
"Media End Offset " + RainbowDump.formatDecHexString(mediaEndOffset) + "\n"
|
||||
);
|
||||
for (PFS0subFile subFile : pfs0subFiles){
|
||||
log.debug(
|
||||
|
|
|
@ -176,6 +176,17 @@ public class HFS0Provider implements ISuperProvider {
|
|||
workerThread.start();
|
||||
return streamIn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exportContent(String saveToLocation, String subFileName) throws Exception {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exportContent(String saveToLocation, int subFileNumber) throws Exception {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugar
|
||||
* */
|
||||
|
|
|
@ -32,18 +32,18 @@ public class AesCtrBufferedInputStream extends BufferedInputStream {
|
|||
private final long mediaOffsetPositionEnd;
|
||||
|
||||
public AesCtrBufferedInputStream(AesCtrDecryptSimple decryptor,
|
||||
long offsetPosition,
|
||||
long ncaOffsetPosition,
|
||||
long mediaStartOffset,
|
||||
long mediaEndOffset,
|
||||
InputStream inputStream){
|
||||
super(inputStream);
|
||||
this.decryptor = decryptor;
|
||||
this.mediaOffsetPositionStart = offsetPosition + (mediaStartOffset * 0x200);
|
||||
this.mediaOffsetPositionEnd = offsetPosition + (mediaEndOffset * 0x200);
|
||||
this.mediaOffsetPositionStart = ncaOffsetPosition + (mediaStartOffset * 0x200);
|
||||
this.mediaOffsetPositionEnd = ncaOffsetPosition + (mediaEndOffset * 0x200);
|
||||
|
||||
log.debug("\nOffset Position "+offsetPosition+
|
||||
"\nMediaOffsetPositionStart "+RainbowDump.formatDecHexString(mediaOffsetPositionStart)+
|
||||
"\nMediaOffsetPositionEnd "+RainbowDump.formatDecHexString(mediaOffsetPositionEnd));
|
||||
log.trace("\n Offset Position "+ncaOffsetPosition+
|
||||
"\n MediaOffsetPositionStart "+RainbowDump.formatDecHexString(mediaOffsetPositionStart)+
|
||||
"\n MediaOffsetPositionEnd "+RainbowDump.formatDecHexString(mediaOffsetPositionEnd));
|
||||
}
|
||||
|
||||
private byte[] decryptedBytes;
|
||||
|
@ -214,7 +214,6 @@ public class AesCtrBufferedInputStream extends BufferedInputStream {
|
|||
fillDecryptedCache();
|
||||
pseudoPos += n;
|
||||
pointerInsideDecryptedSection = (int) leftovers;
|
||||
log.debug(" "+pseudoPos+" "+pointerInsideDecryptedSection);
|
||||
return n;
|
||||
}
|
||||
log.trace("6. Not encrypted ("+pseudoPos+"-"+(pseudoPos+n)+")");
|
||||
|
|
|
@ -70,6 +70,6 @@ public class AesCtrDecryptSimple {
|
|||
// IV for CTR == 16 bytes
|
||||
IVarray = new byte[0x10];
|
||||
// Populate first 4 bytes taken from Header's section Block CTR (aka SecureValue)
|
||||
System.arraycopy(Converter.flip(initialSectionCTR), 0x0, IVarray, 0x0, 0x4);
|
||||
System.arraycopy(Converter.flip(initialSectionCTR), 0x0, IVarray, 0x0, 0x8);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ status = error
|
|||
name = DebugConfigDevelopmentOnlyVerbose
|
||||
|
||||
# Configure root logger level
|
||||
rootLogger.level = TRACE
|
||||
rootLogger.level = DEBUG
|
||||
# Root logger referring to console appender
|
||||
rootLogger.appenderRef.stdout.ref = consoleLogger
|
||||
|
||||
|
|
Loading…
Reference in a new issue