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

This commit is contained in:
Dmitry Isaenko 2022-09-11 23:47:05 +03:00
parent 9b5eacdef9
commit 0238cb4d2c
12 changed files with 313 additions and 705 deletions

View 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;
}
}

View file

@ -26,7 +26,8 @@ import java.io.PipedInputStream;
public interface ISuperProvider { public interface ISuperProvider {
PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception; PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception;
PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) 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(); File getFile();
long getRawFileDataStart(); long getRawFileDataStart();
} }

View file

@ -19,10 +19,8 @@
package libKonogonka.Tools.NCA; package libKonogonka.Tools.NCA;
import libKonogonka.Converter; import libKonogonka.Converter;
import libKonogonka.RainbowDump;
import libKonogonka.Tools.NCA.NCASectionTableBlock.NcaFsHeader; import libKonogonka.Tools.NCA.NCASectionTableBlock.NcaFsHeader;
import libKonogonka.Tools.PFS0.IPFS0Provider; import libKonogonka.Tools.PFS0.IPFS0Provider;
import libKonogonka.Tools.PFS0.PFS0EncryptedProvider;
import libKonogonka.Tools.PFS0.PFS0Provider; import libKonogonka.Tools.PFS0.PFS0Provider;
import libKonogonka.Tools.RomFs.IRomFsProvider; import libKonogonka.Tools.RomFs.IRomFsProvider;
import libKonogonka.Tools.RomFs.RomFsEncryptedProvider; import libKonogonka.Tools.RomFs.RomFsEncryptedProvider;
@ -41,28 +39,28 @@ public class NCAContent {
private final static Logger log = LogManager.getLogger(NCAContent.class); private final static Logger log = LogManager.getLogger(NCAContent.class);
private final File file; private final File file;
private final long offsetPosition; private final long ncaOffsetPosition;
private final NcaFsHeader ncaFsHeader; private final NcaFsHeader ncaFsHeader;
private final NCAHeaderTableEntry ncaHeaderTableEntry; private final NCAHeaderTableEntry ncaHeaderTableEntry;
private final byte[] decryptedKey; private final byte[] decryptedKey;
private final LinkedList<byte[]> Pfs0SHA256hashes; private LinkedList<byte[]> Pfs0SHA256hashes;
private IPFS0Provider pfs0; private IPFS0Provider pfs0;
private IRomFsProvider romfs; private IRomFsProvider romfs;
// TODO: if decryptedKey is empty, throw exception ?? // TODO: if decryptedKey is empty, throw exception ??
public NCAContent(File file, public NCAContent(File file,
long offsetPosition, long ncaOffsetPosition,
NcaFsHeader ncaFsHeader, NcaFsHeader ncaFsHeader,
NCAHeaderTableEntry ncaHeaderTableEntry, NCAHeaderTableEntry ncaHeaderTableEntry,
byte[] decryptedKey) throws Exception byte[] decryptedKey) throws Exception
{ {
this.file = file; this.file = file;
this.offsetPosition = offsetPosition; this.ncaOffsetPosition = ncaOffsetPosition;
this.ncaFsHeader = ncaFsHeader; this.ncaFsHeader = ncaFsHeader;
this.ncaHeaderTableEntry = ncaHeaderTableEntry; this.ncaHeaderTableEntry = ncaHeaderTableEntry;
this.decryptedKey = decryptedKey; this.decryptedKey = decryptedKey;
System.out.println("NCAContent pfs0offsetPosition: "+ncaOffsetPosition);
Pfs0SHA256hashes = new LinkedList<>(); Pfs0SHA256hashes = new LinkedList<>();
// If nothing to do // If nothing to do
if (ncaHeaderTableEntry.getMediaEndOffset() == 0) if (ncaHeaderTableEntry.getMediaEndOffset() == 0)
@ -89,60 +87,24 @@ public class NCAContent {
} }
} }
private void proceedPFS0NotEncrypted() throws Exception{ private void proceedPFS0NotEncrypted() throws Exception{
RandomAccessFile raf = new RandomAccessFile(file, "r"); pfs0 = new PFS0Provider(file,
long thisMediaLocation = offsetPosition + (ncaHeaderTableEntry.getMediaStartOffset() * 0x200);// TODO: NOTE already defined inside PFS0 ncaOffsetPosition,
long hashTableLocation = thisMediaLocation + ncaFsHeader.getSuperBlockPFS0().getHashTableOffset(); ncaFsHeader.getSuperBlockPFS0(),
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,
ncaHeaderTableEntry.getMediaStartOffset(), ncaHeaderTableEntry.getMediaStartOffset(),
ncaHeaderTableEntry.getMediaEndOffset()); 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{ private void proceedRomFs() throws Exception{
@ -167,7 +129,7 @@ public class NCAContent {
this.romfs = new RomFsEncryptedProvider( this.romfs = new RomFsEncryptedProvider(
ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(), ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(),
file, file,
offsetPosition, ncaOffsetPosition,
decryptedKey, decryptedKey,
ncaFsHeader.getSectionCTR(), ncaFsHeader.getSectionCTR(),
ncaHeaderTableEntry.getMediaStartOffset(), ncaHeaderTableEntry.getMediaStartOffset(),
@ -178,174 +140,6 @@ public class NCAContent {
public IPFS0Provider getPfs0() { return pfs0; } public IPFS0Provider getPfs0() { return pfs0; }
public IRomFsProvider getRomfs() { return romfs; } 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. * Export NCA content AS IS.
@ -362,7 +156,7 @@ public class NCAContent {
"Media start location: " + mediaStartBlocksOffset + "\n" + "Media start location: " + mediaStartBlocksOffset + "\n" +
"Media end location: " + mediaEndBlocksOffset + "\n" + "Media end location: " + mediaEndBlocksOffset + "\n" +
"Media size : " + (mediaEndBlocksOffset-mediaStartBlocksOffset) + "\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" + "KEY: " + Converter.byteArrToHexString(decryptedKey) + "\n" +
"CTR: " + Converter.byteArrToHexString(ncaFsHeader.getSectionCTR()) + "\n"); "CTR: " + Converter.byteArrToHexString(ncaFsHeader.getSectionCTR()) + "\n");
//---------------------------------------------------------------------------------------------------/ //---------------------------------------------------------------------------------------------------/
@ -414,7 +208,7 @@ public class NCAContent {
workerThread = new Thread(() -> { workerThread = new Thread(() -> {
try { try {
//RandomAccessFile raf = new RandomAccessFile(file, "r"); //RandomAccessFile raf = new RandomAccessFile(file, "r");
long abosluteOffsetPosition = offsetPosition + (mediaStartBlocksOffset * 0x200); long abosluteOffsetPosition = ncaOffsetPosition + (mediaStartBlocksOffset * 0x200);
raf.seek(abosluteOffsetPosition); raf.seek(abosluteOffsetPosition);
AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey,

View file

@ -283,7 +283,8 @@ public class NCAProvider {
key = cipher.doFinal(rightsIDkey); key = cipher.doFinal(rightsIDkey);
} }
catch (Exception e){ 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); getNcaContentByNumber(0, key);

View file

@ -48,6 +48,7 @@ public class NcaFsHeader {
private final BucketTreeHeader BktrSection2; private final BucketTreeHeader BktrSection2;
private final byte[] generation; private final byte[] generation;
private final byte[] secureValue;
private final byte[] sectionCTR; private final byte[] sectionCTR;
private final SparseInfo sparseInfo; private final SparseInfo sparseInfo;
private final CompressionInfo compressionInfo; private final CompressionInfo compressionInfo;
@ -79,7 +80,10 @@ public class NcaFsHeader {
BktrSection2 = new BucketTreeHeader(Arrays.copyOfRange(tableBlockBytes, 0x130, 0x140)); BktrSection2 = new BucketTreeHeader(Arrays.copyOfRange(tableBlockBytes, 0x130, 0x140));
generation = Arrays.copyOfRange(tableBlockBytes, 0x140, 0x144); 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)); sparseInfo = new SparseInfo(Arrays.copyOfRange(tableBlockBytes, 0x148, 0x178));
compressionInfo = new CompressionInfo(Arrays.copyOfRange(tableBlockBytes, 0x178, 0x1a0)); compressionInfo = new CompressionInfo(Arrays.copyOfRange(tableBlockBytes, 0x178, 0x1a0));
metaDataHashDataInfo = new MetaDataHashDataInfo(Arrays.copyOfRange(tableBlockBytes, 0x1a0, 0x1d0)); metaDataHashDataInfo = new MetaDataHashDataInfo(Arrays.copyOfRange(tableBlockBytes, 0x1a0, 0x1d0));
@ -109,6 +113,10 @@ public class NcaFsHeader {
public int getEntryCountSection2() { return BktrSection2.getEntryCount(); } public int getEntryCountSection2() { return BktrSection2.getEntryCount(); }
public byte[] getPatchInfoUnknownSection2() { return BktrSection2.getUnknown(); } public byte[] getPatchInfoUnknownSection2() { return BktrSection2.getUnknown(); }
public byte[] getGeneration() {return generation;} public byte[] getGeneration() {return generation;}
public byte[] getSecureValue() {return secureValue;}
/**
* Used for Aes Ctr decryption in IV context.
* */
public byte[] getSectionCTR() { return sectionCTR; } public byte[] getSectionCTR() { return sectionCTR; }
public SparseInfo getSparseInfo() {return sparseInfo;} public SparseInfo getSparseInfo() {return sparseInfo;}
public CompressionInfo getCompressionInfo() {return compressionInfo;} public CompressionInfo getCompressionInfo() {return compressionInfo;}

View file

@ -20,6 +20,8 @@ package libKonogonka.Tools.PFS0;
import libKonogonka.Tools.ISuperProvider; import libKonogonka.Tools.ISuperProvider;
import java.util.LinkedList;
public interface IPFS0Provider extends ISuperProvider { public interface IPFS0Provider extends ISuperProvider {
boolean isEncrypted(); boolean isEncrypted();
String getMagic(); String getMagic();
@ -30,4 +32,5 @@ public interface IPFS0Provider extends ISuperProvider {
PFS0subFile[] getPfs0subFiles(); PFS0subFile[] getPfs0subFiles();
void printDebug(); void printDebug();
LinkedList<byte[]> getPfs0SHA256hashes();
} }

View file

@ -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" +
"----------------------------------------------------------------"
);
}
}
}

View file

@ -20,6 +20,7 @@ package libKonogonka.Tools.PFS0;
import libKonogonka.Converter; import libKonogonka.Converter;
import libKonogonka.RainbowDump; import libKonogonka.RainbowDump;
import libKonogonka.Tools.NCA.NCASectionTableBlock.SuperBlockPFS0;
import libKonogonka.ctraes.AesCtrBufferedInputStream; import libKonogonka.ctraes.AesCtrBufferedInputStream;
import libKonogonka.ctraes.AesCtrDecryptSimple; import libKonogonka.ctraes.AesCtrDecryptSimple;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@ -28,93 +29,124 @@ import org.apache.logging.log4j.Logger;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays; 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{ public class PFS0Provider implements IPFS0Provider{
private final static Logger log = LogManager.getLogger(PFS0Provider.class); private final static Logger log = LogManager.getLogger(PFS0Provider.class);
private long rawFileDataStartOffset;
private String magic; private String magic;
private int filesCount; private int filesCount;
private int stringTableSize; private int stringTableSize;
private byte[] padding; private byte[] padding;
private PFS0subFile[] pfs0subFiles; private PFS0subFile[] pfs0subFiles;
//---------------------------------------
private long rawBlockDataStart;
private final File file; private final File file;
private final long offsetPosition; // Where data starts, excluding header, string table etc. private long offsetPositionInFile;
private long mediaStartOffset; private long mediaStartOffset; // In 512-blocks
private long mediaEndOffset; private long mediaEndOffset; // In 512-blocks
private long ncaOffset;
private BufferedInputStream stream;
private SuperBlockPFS0 superBlockPFS0;
private AesCtrDecryptSimple decryptor; private AesCtrDecryptSimple decryptor;
private final boolean encrypted; private LinkedList<byte[]> pfs0SHA256hashes;
public PFS0Provider(File fileWithPfs0, private boolean encrypted;
long offsetPosition,
public PFS0Provider(File nspFile) throws Exception{
this.file = nspFile;
createBufferedInputStream();
readPfs0Header();
}
public PFS0Provider(File file,
long ncaOffset,
SuperBlockPFS0 superBlockPFS0,
long mediaStartOffset, long mediaStartOffset,
long mediaEndOffset, long mediaEndOffset) throws Exception{
AesCtrDecryptSimple decryptor) throws Exception{ this.file = file;
this.file = fileWithPfs0; this.ncaOffset = ncaOffset;
this.offsetPosition = offsetPosition + mediaStartOffset*0x200; this.superBlockPFS0 = superBlockPFS0;
this.encrypted = true; this.offsetPositionInFile = ncaOffset + mediaStartOffset * 0x200;
this.mediaStartOffset = mediaStartOffset; this.mediaStartOffset = mediaStartOffset;
this.mediaEndOffset = mediaEndOffset; this.mediaEndOffset = mediaEndOffset;
this.decryptor = decryptor; this.rawBlockDataStart = superBlockPFS0.getPfs0offset();
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;
//bufferedInputStream = new BufferedInputStream(Files.newInputStream(fileWithPfs0.toPath())); //bufferedInputStream = new BufferedInputStream(Files.newInputStream(fileWithPfs0.toPath()));
proceedPfs0(); createBufferedInputStream();
} long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
private void proceedPfs0() throws Exception{ if (toSkip != stream.skip(toSkip))
BufferedInputStream bufferedInputStream; throw new Exception("Can't skip bytes prior Hash Table offset");
collectHashes();
if (encrypted) { createBufferedInputStream();
bufferedInputStream = new AesCtrBufferedInputStream(decryptor, toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
offsetPosition, if (toSkip != stream.skip(toSkip))
mediaStartOffset, throw new Exception("Can't skip bytes prior PFS0 offset");
mediaEndOffset, readPfs0Header();
Files.newInputStream(file.toPath()));
}
else{
bufferedInputStream = new BufferedInputStream(Files.newInputStream(file.toPath()));
} }
if (offsetPosition != bufferedInputStream.skip(offsetPosition)) public PFS0Provider(File file,
throw new Exception("PFS0Provider: Unable to skip initial offset: "+offsetPosition); 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;
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]; byte[] fileStartingBytes = new byte[0x10];
// Read PFS0Provider, files count, header, padding (4 zero bytes) if (0x10 != stream.read(fileStartingBytes))
if (bufferedInputStream.read(fileStartingBytes) != 0x10){ throw new Exception("Reading stream suddenly ended while trying to read starting 0x10 bytes");
throw new Exception("PFS0Provider: Unable to read starting bytes");
} // Update position
rawFileDataStartOffset += 0x10; rawBlockDataStart += 0x10;
// Check PFS0Provider // Check PFS0Provider
magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII); magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII);
if (! magic.equals("PFS0")){ if (! magic.equals("PFS0")){
throw new Exception("PFS0Provider: Bad magic"); throw new Exception("Bad magic");
} }
// Get files count // Get files count
filesCount = getLEint(fileStartingBytes, 0x4); filesCount = getLEint(fileStartingBytes, 0x4);
if (filesCount <= 0 ) { if (filesCount <= 0 ) {
throw new Exception("PFS0Provider: Files count is too small"); throw new Exception("Files count is too small");
} }
// Get string table // Get string table
stringTableSize = getLEint(fileStartingBytes, 0x8); stringTableSize = getLEint(fileStartingBytes, 0x8);
if (stringTableSize <= 0 ){ 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); padding = Arrays.copyOfRange(fileStartingBytes, 0xc, 0x10);
//--------------------------------------------------------------------------------------------------------- //-------------------------------------------------------------------
pfs0subFiles = new PFS0subFile[filesCount]; pfs0subFiles = new PFS0subFile[filesCount];
long[] offsetsSubFiles = new long[filesCount]; long[] offsetsSubFiles = new long[filesCount];
@ -123,23 +155,27 @@ public class PFS0Provider implements IPFS0Provider{
byte[][] zeroBytes = new byte[filesCount][]; byte[][] zeroBytes = new byte[filesCount][];
byte[] fileEntryTable = new byte[0x18]; byte[] fileEntryTable = new byte[0x18];
for (int i=0; i<filesCount; i++){ for (int i=0; i < filesCount; i++){
if (bufferedInputStream.read(fileEntryTable) != 0x18) if (0x18 != stream.read(fileEntryTable))
throw new Exception("PFS0Provider: String table is too small"); throw new Exception("Reading stream suddenly ended while trying to read File Entry Table #"+i);
offsetsSubFiles[i] = getLElong(fileEntryTable, 0); offsetsSubFiles[i] = getLElong(fileEntryTable, 0);
sizesSubFiles[i] = getLElong(fileEntryTable, 0x8); sizesSubFiles[i] = getLElong(fileEntryTable, 0x8);
strTableOffsets[i] = getLEint(fileEntryTable, 0x10); strTableOffsets[i] = getLEint(fileEntryTable, 0x10);
zeroBytes[i] = Arrays.copyOfRange(fileEntryTable, 0x14, 0x18); zeroBytes[i] = Arrays.copyOfRange(fileEntryTable, 0x14, 0x18);
rawFileDataStartOffset += 0x18; rawBlockDataStart += 0x18;
} }
//********************************************************************************************************** //*******************************************************************
// In here pointer in front of String table // In here pointer in front of String table
String[] subFileNames = new String[filesCount]; String[] subFileNames = new String[filesCount];
byte[] stringTbl = new byte[stringTableSize]; 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+")"); 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++){ for (int i=0; i < filesCount; i++){
int j = 0; int j = 0;
while (stringTbl[strTableOffsets[i]+j] != (byte)0x00) while (stringTbl[strTableOffsets[i]+j] != (byte)0x00)
@ -151,14 +187,44 @@ public class PFS0Provider implements IPFS0Provider{
subFileNames[i], subFileNames[i],
offsetsSubFiles[i], offsetsSubFiles[i],
sizesSubFiles[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 @Override
public boolean isEncrypted() { return encrypted; } public boolean isEncrypted() { return true; }
@Override @Override
public String getMagic() { return magic; } public String getMagic() { return magic; }
@Override @Override
@ -168,80 +234,90 @@ public class PFS0Provider implements IPFS0Provider{
@Override @Override
public byte[] getPadding() { return padding; } public byte[] getPadding() { return padding; }
@Override @Override
public long getRawFileDataStart() { return rawFileDataStartOffset; } public long getRawFileDataStart() { return rawBlockDataStart;}
@Override @Override
public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; } public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; }
@Override @Override
public File getFile(){ return file; } public File getFile(){ return file; }
@Override @Override
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception{ // TODO: Throw exceptions? public boolean exportContent(String saveToLocation, String subFileName){
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 {
for (int i = 0; i < pfs0subFiles.length; i++){ for (int i = 0; i < pfs0subFiles.length; i++){
if (pfs0subFiles[i].getName().equals(subFileName)) 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(){ public void printDebug(){
log.debug(".:: PFS0Provider ::.\n" + log.debug(".:: PFS0Provider ::.\n" +
"File name: " + file.getName() + "\n\n" + "File name: " + file.getName() + "\n" +
"Raw file data start: " + RainbowDump.formatDecHexString(rawFileDataStartOffset) + "\n" + "Raw block data start " + RainbowDump.formatDecHexString(rawBlockDataStart) + "\n" +
"Magic " + magic + "\n" + "Magic " + magic + "\n" +
"Files count " + RainbowDump.formatDecHexString(filesCount) + "\n" + "Files count " + RainbowDump.formatDecHexString(filesCount) + "\n" +
"String Table Size " + RainbowDump.formatDecHexString(stringTableSize) + "\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){ for (PFS0subFile subFile : pfs0subFiles){
log.debug( log.debug(

View file

@ -176,6 +176,17 @@ public class HFS0Provider implements ISuperProvider {
workerThread.start(); workerThread.start();
return streamIn; 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 * Sugar
* */ * */

View file

@ -32,18 +32,18 @@ public class AesCtrBufferedInputStream extends BufferedInputStream {
private final long mediaOffsetPositionEnd; private final long mediaOffsetPositionEnd;
public AesCtrBufferedInputStream(AesCtrDecryptSimple decryptor, public AesCtrBufferedInputStream(AesCtrDecryptSimple decryptor,
long offsetPosition, long ncaOffsetPosition,
long mediaStartOffset, long mediaStartOffset,
long mediaEndOffset, long mediaEndOffset,
InputStream inputStream){ InputStream inputStream){
super(inputStream); super(inputStream);
this.decryptor = decryptor; this.decryptor = decryptor;
this.mediaOffsetPositionStart = offsetPosition + (mediaStartOffset * 0x200); this.mediaOffsetPositionStart = ncaOffsetPosition + (mediaStartOffset * 0x200);
this.mediaOffsetPositionEnd = offsetPosition + (mediaEndOffset * 0x200); this.mediaOffsetPositionEnd = ncaOffsetPosition + (mediaEndOffset * 0x200);
log.debug("\nOffset Position "+offsetPosition+ log.trace("\n Offset Position "+ncaOffsetPosition+
"\nMediaOffsetPositionStart "+RainbowDump.formatDecHexString(mediaOffsetPositionStart)+ "\n MediaOffsetPositionStart "+RainbowDump.formatDecHexString(mediaOffsetPositionStart)+
"\nMediaOffsetPositionEnd "+RainbowDump.formatDecHexString(mediaOffsetPositionEnd)); "\n MediaOffsetPositionEnd "+RainbowDump.formatDecHexString(mediaOffsetPositionEnd));
} }
private byte[] decryptedBytes; private byte[] decryptedBytes;
@ -214,7 +214,6 @@ public class AesCtrBufferedInputStream extends BufferedInputStream {
fillDecryptedCache(); fillDecryptedCache();
pseudoPos += n; pseudoPos += n;
pointerInsideDecryptedSection = (int) leftovers; pointerInsideDecryptedSection = (int) leftovers;
log.debug(" "+pseudoPos+" "+pointerInsideDecryptedSection);
return n; return n;
} }
log.trace("6. Not encrypted ("+pseudoPos+"-"+(pseudoPos+n)+")"); log.trace("6. Not encrypted ("+pseudoPos+"-"+(pseudoPos+n)+")");

View file

@ -70,6 +70,6 @@ public class AesCtrDecryptSimple {
// IV for CTR == 16 bytes // IV for CTR == 16 bytes
IVarray = new byte[0x10]; IVarray = new byte[0x10];
// Populate first 4 bytes taken from Header's section Block CTR (aka SecureValue) // 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);
} }
} }

View file

@ -15,7 +15,7 @@ status = error
name = DebugConfigDevelopmentOnlyVerbose name = DebugConfigDevelopmentOnlyVerbose
# Configure root logger level # Configure root logger level
rootLogger.level = TRACE rootLogger.level = DEBUG
# Root logger referring to console appender # Root logger referring to console appender
rootLogger.appenderRef.stdout.ref = consoleLogger rootLogger.appenderRef.stdout.ref = consoleLogger