Simplify, fix KeyChainHolder, implement HFS0 export functions
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
53af386738
commit
9a854774d7
30 changed files with 719 additions and 1036 deletions
|
@ -63,10 +63,9 @@ public class KeyChainHolder {
|
||||||
private HashMap<String, String> collectKeysByType(String keyName){
|
private HashMap<String, String> collectKeysByType(String keyName){
|
||||||
HashMap<String, String> tempKeySet = new HashMap<>();
|
HashMap<String, String> tempKeySet = new HashMap<>();
|
||||||
String keyNamePattern = keyName+"_%02x";
|
String keyNamePattern = keyName+"_%02x";
|
||||||
HashMap<String, String> map = new HashMap<>();
|
|
||||||
String keyParsed;
|
String keyParsed;
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
while ((keyParsed = map.get(String.format(keyNamePattern, counter))) != null){
|
while ((keyParsed = rawKeySet.get(String.format(keyNamePattern, counter))) != null){
|
||||||
tempKeySet.put(String.format(keyNamePattern, counter), keyParsed);
|
tempKeySet.put(String.format(keyNamePattern, counter), keyParsed);
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +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;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PipedInputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Any class of this type must be able to accept data from stream (and file as any other).
|
|
||||||
* */
|
|
||||||
|
|
||||||
public abstract class ASuperInFileProvider {
|
|
||||||
protected byte[] readFromStream(PipedInputStream pis, int size) throws IOException {
|
|
||||||
byte[] buffer = new byte[size];
|
|
||||||
int startingPos = 0;
|
|
||||||
int readCnt;
|
|
||||||
while (size > 0){
|
|
||||||
readCnt = pis.read(buffer, startingPos, size);
|
|
||||||
if (readCnt == -1)
|
|
||||||
return null;
|
|
||||||
startingPos += readCnt;
|
|
||||||
size -= readCnt;
|
|
||||||
}
|
|
||||||
return buffer;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,14 +18,16 @@
|
||||||
*/
|
*/
|
||||||
package libKonogonka.Tools;
|
package libKonogonka.Tools;
|
||||||
|
|
||||||
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.PipedInputStream;
|
|
||||||
/**
|
/**
|
||||||
* Any class of this type must provide streams
|
* Any class of this type must provide streams
|
||||||
* */
|
* */
|
||||||
public interface ISuperProvider {
|
public interface ISuperProvider {
|
||||||
PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception;
|
InFileStreamProducer getStreamProducer(String subFileName) throws Exception;
|
||||||
PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception;
|
InFileStreamProducer getStreamProducer(int subFileNumber) throws Exception;
|
||||||
boolean exportContent(String saveToLocation, String subFileName) throws Exception;
|
boolean exportContent(String saveToLocation, String subFileName) throws Exception;
|
||||||
boolean exportContent(String saveToLocation, int subFileNumber) throws Exception;
|
boolean exportContent(String saveToLocation, int subFileNumber) throws Exception;
|
||||||
File getFile();
|
File getFile();
|
||||||
|
|
|
@ -19,12 +19,11 @@
|
||||||
package libKonogonka.Tools.NCA;
|
package libKonogonka.Tools.NCA;
|
||||||
|
|
||||||
import libKonogonka.Tools.NCA.NCASectionTableBlock.NcaFsHeader;
|
import libKonogonka.Tools.NCA.NCASectionTableBlock.NcaFsHeader;
|
||||||
import libKonogonka.Tools.PFS0.IPFS0Provider;
|
|
||||||
import libKonogonka.Tools.PFS0.PFS0Provider;
|
import libKonogonka.Tools.PFS0.PFS0Provider;
|
||||||
import libKonogonka.Tools.RomFs.IRomFsProvider;
|
|
||||||
import libKonogonka.Tools.RomFs.RomFsProvider;
|
import libKonogonka.Tools.RomFs.RomFsProvider;
|
||||||
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
||||||
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
import libKonogonka.exceptions.EmptySectionException;
|
import libKonogonka.exceptions.EmptySectionException;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
@ -42,8 +41,8 @@ public class NCAContent {
|
||||||
private final NCAHeaderTableEntry ncaHeaderTableEntry;
|
private final NCAHeaderTableEntry ncaHeaderTableEntry;
|
||||||
private final byte[] decryptedKey;
|
private final byte[] decryptedKey;
|
||||||
|
|
||||||
private IPFS0Provider pfs0;
|
private PFS0Provider pfs0;
|
||||||
private IRomFsProvider romfs;
|
private RomFsProvider romfs;
|
||||||
|
|
||||||
// TODO: if decryptedKey is empty, throw exception?
|
// TODO: if decryptedKey is empty, throw exception?
|
||||||
public NCAContent(File file,
|
public NCAContent(File file,
|
||||||
|
@ -82,22 +81,16 @@ public class NCAContent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void proceedPFS0NotEncrypted() throws Exception{
|
private void proceedPFS0NotEncrypted() throws Exception{
|
||||||
pfs0 = new PFS0Provider(file,
|
InFileStreamProducer producer = new InFileStreamProducer(file); // no need to bypass ncaOffsetPosition!
|
||||||
ncaOffsetPosition,
|
pfs0 = new PFS0Provider(producer,
|
||||||
|
makeOffsetPositionInFile(),
|
||||||
ncaFsHeader.getSuperBlockPFS0(),
|
ncaFsHeader.getSuperBlockPFS0(),
|
||||||
ncaHeaderTableEntry.getMediaStartOffset(),
|
ncaHeaderTableEntry.getMediaStartOffset());
|
||||||
ncaHeaderTableEntry.getMediaEndOffset());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void proceedPFS0Encrypted() throws Exception{
|
private void proceedPFS0Encrypted() throws Exception{
|
||||||
AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, ncaFsHeader.getSectionCTR(),
|
pfs0 = new PFS0Provider(makeEncryptedProducer(), makeOffsetPositionInFile(), ncaFsHeader.getSuperBlockPFS0(),
|
||||||
ncaHeaderTableEntry.getMediaStartOffset() * 0x200);
|
ncaHeaderTableEntry.getMediaStartOffset());
|
||||||
pfs0 = new PFS0Provider(file,
|
|
||||||
ncaOffsetPosition,
|
|
||||||
ncaFsHeader.getSuperBlockPFS0(),
|
|
||||||
decryptor,
|
|
||||||
ncaHeaderTableEntry.getMediaStartOffset(),
|
|
||||||
ncaHeaderTableEntry.getMediaEndOffset());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void proceedRomFs() throws Exception{
|
private void proceedRomFs() throws Exception{
|
||||||
|
@ -109,7 +102,7 @@ public class NCAContent {
|
||||||
proceedRomFsEncrypted();
|
proceedRomFsEncrypted();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Exception("Non-supported 'Crypto type'");
|
throw new Exception("Non-supported 'Crypto type' "+ncaFsHeader.getCryptoType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void proceedRomFsNotEncrypted(){ // TODO: Clarify, implement if needed
|
private void proceedRomFsNotEncrypted(){ // TODO: Clarify, implement if needed
|
||||||
|
@ -119,18 +112,21 @@ public class NCAContent {
|
||||||
if (decryptedKey == null)
|
if (decryptedKey == null)
|
||||||
throw new Exception("CryptoSection03: unable to proceed. No decrypted key provided.");
|
throw new Exception("CryptoSection03: unable to proceed. No decrypted key provided.");
|
||||||
|
|
||||||
this.romfs = new RomFsProvider(
|
this.romfs = new RomFsProvider(makeEncryptedProducer(), ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(),
|
||||||
ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(),
|
makeOffsetPositionInFile(), ncaHeaderTableEntry.getMediaStartOffset());
|
||||||
file,
|
|
||||||
ncaOffsetPosition,
|
|
||||||
decryptedKey,
|
|
||||||
ncaFsHeader.getSectionCTR(),
|
|
||||||
ncaHeaderTableEntry.getMediaStartOffset(),
|
|
||||||
ncaHeaderTableEntry.getMediaEndOffset());
|
|
||||||
}
|
}
|
||||||
public IPFS0Provider getPfs0() { return pfs0; }
|
public PFS0Provider getPfs0() { return pfs0; }
|
||||||
public IRomFsProvider getRomfs() { return romfs; }
|
public RomFsProvider getRomfs() { return romfs; }
|
||||||
|
|
||||||
|
private InFileStreamProducer makeEncryptedProducer() throws Exception{
|
||||||
|
AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, ncaFsHeader.getSectionCTR(),
|
||||||
|
ncaHeaderTableEntry.getMediaStartOffset() * 0x200);
|
||||||
|
return new InFileStreamProducer(file, ncaOffsetPosition, 0, decryptor,
|
||||||
|
ncaHeaderTableEntry.getMediaStartOffset(), ncaHeaderTableEntry.getMediaEndOffset());
|
||||||
|
}
|
||||||
|
private long makeOffsetPositionInFile(){
|
||||||
|
return ncaOffsetPosition + ncaHeaderTableEntry.getMediaStartOffset() * 0x200;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Export NCA content AS IS.
|
* Export NCA content AS IS.
|
||||||
* Not so good for PFS0 since there are SHAs list that discourages but good for 'romfs' and things like that
|
* Not so good for PFS0 since there are SHAs list that discourages but good for 'romfs' and things like that
|
||||||
|
|
|
@ -184,8 +184,8 @@ public class NcaFsHeader {
|
||||||
"Crypto Type : " + cryptoType + " (" + cryptoTypeDescription + ")\n" +
|
"Crypto Type : " + cryptoType + " (" + cryptoTypeDescription + ")\n" +
|
||||||
"Meta Data Hash Type : " + metaDataHashType + "\n" +
|
"Meta Data Hash Type : " + metaDataHashType + "\n" +
|
||||||
"Padding : " + byteArrToHexString(padding) + "\n" +
|
"Padding : " + byteArrToHexString(padding) + "\n" +
|
||||||
"Super Block IVFC : " + superBlockIVFC + "\n" +
|
"Super Block IVFC : " + (superBlockIVFC == null ? "-\n": "YES\n") +
|
||||||
"Super Block PFS0 : " + superBlockPFS0 + "\n" +
|
"Super Block PFS0 : " + (superBlockPFS0 == null ? "-\n": "YES\n") +
|
||||||
"================================================================================================\n" +
|
"================================================================================================\n" +
|
||||||
(((fsType == 0) && (hashType == 0x3))?
|
(((fsType == 0) && (hashType == 0x3))?
|
||||||
("| Hash Data - RomFS\n" +
|
("| Hash Data - RomFS\n" +
|
||||||
|
|
|
@ -27,21 +27,21 @@ import java.util.Arrays;
|
||||||
import static libKonogonka.Converter.getLEint;
|
import static libKonogonka.Converter.getLEint;
|
||||||
|
|
||||||
public class ACI0Provider {
|
public class ACI0Provider {
|
||||||
private String magicNum;
|
private final String magicNum;
|
||||||
private byte[] reserved1;
|
private final byte[] reserved1;
|
||||||
private byte[] titleID;
|
private final byte[] titleID;
|
||||||
private byte[] reserved2;
|
private final byte[] reserved2;
|
||||||
private int fsAccessHeaderOffset;
|
private final int fsAccessHeaderOffset;
|
||||||
private int fsAccessHeaderSize;
|
private final int fsAccessHeaderSize;
|
||||||
private int serviceAccessControlOffset;
|
private final int serviceAccessControlOffset;
|
||||||
private int serviceAccessControlSize;
|
private final int serviceAccessControlSize;
|
||||||
private int kernelAccessControlOffset;
|
private final int kernelAccessControlOffset;
|
||||||
private int kernelAccessControlSize;
|
private final int kernelAccessControlSize;
|
||||||
private byte[] reserved3;
|
private final byte[] reserved3;
|
||||||
|
|
||||||
private FSAccessHeaderProvider fsAccessHeaderProvider;
|
private final FSAccessHeaderProvider fsAccessHeaderProvider;
|
||||||
private ServiceAccessControlProvider serviceAccessControlProvider;
|
private final ServiceAccessControlProvider serviceAccessControlProvider;
|
||||||
private KernelAccessControlProvider kernelAccessControlProvider;
|
private final KernelAccessControlProvider kernelAccessControlProvider;
|
||||||
|
|
||||||
public ACI0Provider(byte[] aci0bytes) throws Exception {
|
public ACI0Provider(byte[] aci0bytes) throws Exception {
|
||||||
if (aci0bytes.length < 0x40)
|
if (aci0bytes.length < 0x40)
|
||||||
|
|
|
@ -28,28 +28,28 @@ import static libKonogonka.Converter.*;
|
||||||
|
|
||||||
public class ACIDProvider {
|
public class ACIDProvider {
|
||||||
|
|
||||||
private byte[] rsa2048signature;
|
private final byte[] rsa2048signature;
|
||||||
private byte[] rsa2048publicKey;
|
private final byte[] rsa2048publicKey;
|
||||||
private String magicNum;
|
private final String magicNum;
|
||||||
private int dataSize;
|
private final int dataSize;
|
||||||
private byte[] reserved1;
|
private final byte[] reserved1;
|
||||||
private byte flag1;
|
private final byte flag1;
|
||||||
private byte flag2;
|
private final byte flag2;
|
||||||
private byte flag3;
|
private final byte flag3;
|
||||||
private byte flag4;
|
private final byte flag4;
|
||||||
private long titleRangeMin;
|
private final long titleRangeMin;
|
||||||
private long titleRangeMax;
|
private final long titleRangeMax;
|
||||||
private int fsAccessControlOffset;
|
private final int fsAccessControlOffset;
|
||||||
private int fsAccessControlSize;
|
private final int fsAccessControlSize;
|
||||||
private int serviceAccessControlOffset;
|
private final int serviceAccessControlOffset;
|
||||||
private int serviceAccessControlSize;
|
private final int serviceAccessControlSize;
|
||||||
private int kernelAccessControlOffset;
|
private final int kernelAccessControlOffset;
|
||||||
private int kernelAccessControlSize;
|
private final int kernelAccessControlSize;
|
||||||
private byte[] reserved2;
|
private final byte[] reserved2;
|
||||||
|
|
||||||
private FSAccessControlProvider fsAccessControlProvider;
|
private final FSAccessControlProvider fsAccessControlProvider;
|
||||||
private ServiceAccessControlProvider serviceAccessControlProvider;
|
private final ServiceAccessControlProvider serviceAccessControlProvider;
|
||||||
private KernelAccessControlProvider kernelAccessControlProvider;
|
private final KernelAccessControlProvider kernelAccessControlProvider;
|
||||||
|
|
||||||
public ACIDProvider(byte[] acidBytes) throws Exception{
|
public ACIDProvider(byte[] acidBytes) throws Exception{
|
||||||
if (acidBytes.length < 0x240)
|
if (acidBytes.length < 0x240)
|
||||||
|
|
|
@ -27,10 +27,10 @@ import java.util.Arrays;
|
||||||
* */
|
* */
|
||||||
public class FSAccessControlProvider {
|
public class FSAccessControlProvider {
|
||||||
|
|
||||||
private byte version;
|
private final byte version;
|
||||||
private byte[] padding;
|
private final byte[] padding;
|
||||||
private long permissionsBitmask;
|
private final long permissionsBitmask;
|
||||||
private byte[] reserved;
|
private final byte[] reserved;
|
||||||
|
|
||||||
public FSAccessControlProvider(byte[] bytes) {
|
public FSAccessControlProvider(byte[] bytes) {
|
||||||
version = bytes[0];
|
version = bytes[0];
|
||||||
|
|
|
@ -18,19 +18,18 @@
|
||||||
*/
|
*/
|
||||||
package libKonogonka.Tools.NPDM;
|
package libKonogonka.Tools.NPDM;
|
||||||
|
|
||||||
import libKonogonka.Tools.ASuperInFileProvider;
|
|
||||||
import libKonogonka.Tools.NPDM.ACI0.ACI0Provider;
|
import libKonogonka.Tools.NPDM.ACI0.ACI0Provider;
|
||||||
import libKonogonka.Tools.NPDM.ACID.ACIDProvider;
|
import libKonogonka.Tools.NPDM.ACID.ACIDProvider;
|
||||||
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.PipedInputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import static libKonogonka.Converter.*;
|
import static libKonogonka.Converter.*;
|
||||||
|
|
||||||
public class NPDMProvider extends ASuperInFileProvider {
|
public class NPDMProvider{
|
||||||
|
|
||||||
private final String magicNum;
|
private final String magicNum;
|
||||||
private final byte[] reserved1;
|
private final byte[] reserved1;
|
||||||
|
@ -50,38 +49,31 @@ public class NPDMProvider extends ASuperInFileProvider {
|
||||||
private final int acidOffset; // originally 4-bytes (u-int)
|
private final int acidOffset; // originally 4-bytes (u-int)
|
||||||
private final int acidSize; // originally 4-bytes (u-int)
|
private final int acidSize; // originally 4-bytes (u-int)
|
||||||
|
|
||||||
private final ACI0Provider aci0;
|
private ACI0Provider aci0;
|
||||||
private final ACIDProvider acid;
|
private ACIDProvider acid;
|
||||||
|
|
||||||
public NPDMProvider(PipedInputStream pis) throws Exception{
|
public NPDMProvider(File file) throws Exception {
|
||||||
|
this(file, 0);
|
||||||
|
}
|
||||||
|
public NPDMProvider(File file, long offset) throws Exception {
|
||||||
|
this(new InFileStreamProducer(file, offset));
|
||||||
|
}
|
||||||
|
public NPDMProvider(InFileStreamProducer producer) throws Exception{
|
||||||
|
try (BufferedInputStream stream = producer.produce()) {
|
||||||
byte[] mainBuf = new byte[0x80];
|
byte[] mainBuf = new byte[0x80];
|
||||||
if(pis.read(mainBuf) != 0x80)
|
if (stream.read(mainBuf) != 0x80)
|
||||||
throw new Exception("NPDMProvider: Failed to read 'META'");
|
throw new Exception("NPDMProvider: Failed to read 'META'");
|
||||||
aci0offset = getLEint(mainBuf, 0x70);
|
aci0offset = getLEint(mainBuf, 0x70);
|
||||||
aci0size = getLEint(mainBuf, 0x74);
|
aci0size = getLEint(mainBuf, 0x74);
|
||||||
acidOffset = getLEint(mainBuf, 0x78);
|
acidOffset = getLEint(mainBuf, 0x78);
|
||||||
acidSize = getLEint(mainBuf, 0x7C);
|
acidSize = getLEint(mainBuf, 0x7C);
|
||||||
byte[] aci0Buf;
|
|
||||||
byte[] acidBuf;
|
if (aci0offset < acidOffset) {
|
||||||
if (aci0offset < acidOffset){
|
calculateACI0(stream, aci0offset - 0x80);
|
||||||
if (pis.skip(aci0offset - 0x80) != (aci0offset - 0x80))
|
calculateACID(stream, acidOffset - aci0offset - aci0size);
|
||||||
throw new Exception("NPDMProvider: Failed to skip bytes till 'ACI0'");
|
} else {
|
||||||
if ((aci0Buf = readFromStream(pis, aci0size)) == null)
|
calculateACID(stream, acidOffset - 0x80);
|
||||||
throw new Exception("NPDMProvider: Failed to read 'ACI0'");
|
calculateACI0(stream, aci0offset - acidOffset - acidSize);
|
||||||
if (pis.skip(acidOffset - aci0offset - aci0size) != (acidOffset - aci0offset - aci0size))
|
|
||||||
throw new Exception("NPDMProvider: Failed to skip bytes till 'ACID'");
|
|
||||||
if ((acidBuf = readFromStream(pis, acidSize)) == null)
|
|
||||||
throw new Exception("NPDMProvider: Failed to read 'ACID'");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (pis.skip(acidOffset - 0x80) != (acidOffset - 0x80))
|
|
||||||
throw new Exception("NPDMProvider: Failed to skip bytes till 'ACID'");
|
|
||||||
if ((acidBuf = readFromStream(pis, acidSize)) == null)
|
|
||||||
throw new Exception("NPDMProvider: Failed to read 'ACID'");
|
|
||||||
if (pis.skip(aci0offset - acidOffset - acidSize) != (aci0offset - acidOffset - acidSize))
|
|
||||||
throw new Exception("NPDMProvider: Failed to skip bytes till 'ACI0'");
|
|
||||||
if ((aci0Buf = readFromStream(pis, aci0size)) == null)
|
|
||||||
throw new Exception("NPDMProvider: Failed to read 'ACI0'");
|
|
||||||
}
|
}
|
||||||
magicNum = new String(mainBuf, 0, 4, StandardCharsets.UTF_8);
|
magicNum = new String(mainBuf, 0, 4, StandardCharsets.UTF_8);
|
||||||
reserved1 = Arrays.copyOfRange(mainBuf, 0x4, 0xC);
|
reserved1 = Arrays.copyOfRange(mainBuf, 0x4, 0xC);
|
||||||
|
@ -96,52 +88,24 @@ public class NPDMProvider extends ASuperInFileProvider {
|
||||||
titleName = new String(mainBuf, 0x20, 0x10, StandardCharsets.UTF_8);
|
titleName = new String(mainBuf, 0x20, 0x10, StandardCharsets.UTF_8);
|
||||||
productCode = Arrays.copyOfRange(mainBuf, 0x30, 0x40);
|
productCode = Arrays.copyOfRange(mainBuf, 0x30, 0x40);
|
||||||
reserved4 = Arrays.copyOfRange(mainBuf, 0x40, 0x70);
|
reserved4 = Arrays.copyOfRange(mainBuf, 0x40, 0x70);
|
||||||
|
}
|
||||||
aci0 = new ACI0Provider(aci0Buf);
|
|
||||||
acid = new ACIDProvider(acidBuf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public NPDMProvider(File file) throws Exception { this(file, 0); }
|
private void calculateACID(BufferedInputStream stream, int toSkip) throws Exception{
|
||||||
|
byte[] acidBuf = new byte[acidSize];
|
||||||
public NPDMProvider(File file, long offset) throws Exception {
|
if (stream.skip(toSkip) != toSkip)
|
||||||
if (file.length() - offset < 0x80) // Header's size
|
throw new Exception("NPDMProvider: Failed to skip bytes till 'ACID'");
|
||||||
throw new Exception("NPDMProvider: File is too small.");
|
if (acidSize != stream.read(acidBuf))
|
||||||
RandomAccessFile raf = new RandomAccessFile(file, "r");
|
|
||||||
raf.seek(offset);
|
|
||||||
// Get META
|
|
||||||
byte[] metaBuf = new byte[0x80];
|
|
||||||
if (raf.read(metaBuf) != 0x80)
|
|
||||||
throw new Exception("NPDMProvider: Failed to read 'META'");
|
|
||||||
magicNum = new String(metaBuf, 0, 4, StandardCharsets.UTF_8);
|
|
||||||
reserved1 = Arrays.copyOfRange(metaBuf, 0x4, 0xC);
|
|
||||||
MMUFlags = metaBuf[0xC];
|
|
||||||
reserved2 = metaBuf[0xD];
|
|
||||||
mainThreadPrio = metaBuf[0xE];
|
|
||||||
mainThreadCoreNum = metaBuf[0xF];
|
|
||||||
reserved3 = Arrays.copyOfRange(metaBuf, 0x10, 0x14);
|
|
||||||
personalMmHeapSize = getLEint(metaBuf, 0x14);
|
|
||||||
version = getLEint(metaBuf, 0x18);
|
|
||||||
mainThreadStackSize = getLElongOfInt(metaBuf, 0x1C);
|
|
||||||
titleName = new String(metaBuf, 0x20, 0x10, StandardCharsets.UTF_8);
|
|
||||||
productCode = Arrays.copyOfRange(metaBuf, 0x30, 0x40);
|
|
||||||
reserved4 = Arrays.copyOfRange(metaBuf, 0x40, 0x70);
|
|
||||||
aci0offset = getLEint(metaBuf, 0x70);
|
|
||||||
aci0size = getLEint(metaBuf, 0x74);
|
|
||||||
acidOffset = getLEint(metaBuf, 0x78);
|
|
||||||
acidSize = getLEint(metaBuf, 0x7C);
|
|
||||||
// Get ACI0
|
|
||||||
raf.seek(aci0offset);
|
|
||||||
metaBuf = new byte[aci0size]; // TODO: NOTE: we read all size but it's memory consuming
|
|
||||||
if (raf.read(metaBuf) != aci0size)
|
|
||||||
throw new Exception("NPDMProvider: Failed to read 'ACI0'");
|
|
||||||
aci0 = new ACI0Provider(metaBuf);
|
|
||||||
// Get ACID
|
|
||||||
raf.seek(acidOffset);
|
|
||||||
metaBuf = new byte[acidSize]; // TODO: NOTE: we read all size but it's memory consuming
|
|
||||||
if (raf.read(metaBuf) != acidSize)
|
|
||||||
throw new Exception("NPDMProvider: Failed to read 'ACID'");
|
throw new Exception("NPDMProvider: Failed to read 'ACID'");
|
||||||
acid = new ACIDProvider(metaBuf);
|
acid = new ACIDProvider(acidBuf);
|
||||||
raf.close();
|
}
|
||||||
|
private void calculateACI0(BufferedInputStream stream, int toSkip) throws Exception{
|
||||||
|
byte[] aci0Buf = new byte[aci0size];
|
||||||
|
if (stream.skip(toSkip) != toSkip)
|
||||||
|
throw new Exception("NPDMProvider: Failed to skip bytes till 'ACI0'");
|
||||||
|
if (aci0size != stream.read(aci0Buf))
|
||||||
|
throw new Exception("NPDMProvider: Failed to read 'ACI0'");
|
||||||
|
aci0 = new ACI0Provider(aci0Buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMagicNum() { return magicNum; }
|
public String getMagicNum() { return magicNum; }
|
||||||
|
|
|
@ -23,7 +23,7 @@ import java.util.LinkedHashMap;
|
||||||
|
|
||||||
public class ServiceAccessControlProvider {
|
public class ServiceAccessControlProvider {
|
||||||
|
|
||||||
private LinkedHashMap<String, Byte> collection;
|
private final LinkedHashMap<String, Byte> collection;
|
||||||
|
|
||||||
public ServiceAccessControlProvider(byte[] bytes){
|
public ServiceAccessControlProvider(byte[] bytes){
|
||||||
collection = new LinkedHashMap<>();
|
collection = new LinkedHashMap<>();
|
||||||
|
|
|
@ -1,43 +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.Tools.ISuperProvider;
|
|
||||||
import libKonogonka.ctraes.InFileStreamProducer;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
|
|
||||||
public interface IPFS0Provider extends ISuperProvider {
|
|
||||||
boolean isEncrypted();
|
|
||||||
String getMagic();
|
|
||||||
int getFilesCount();
|
|
||||||
int getStringTableSize();
|
|
||||||
byte[] getPadding();
|
|
||||||
|
|
||||||
PFS0subFile[] getPfs0subFiles();
|
|
||||||
|
|
||||||
void printDebug();
|
|
||||||
|
|
||||||
InFileStreamProducer getStreamProducer(String subFileName) throws FileNotFoundException;
|
|
||||||
|
|
||||||
InFileStreamProducer getStreamProducer(int subFileNumber);
|
|
||||||
|
|
||||||
LinkedList<byte[]> getPfs0SHA256hashes();
|
|
||||||
}
|
|
125
src/main/java/libKonogonka/Tools/PFS0/PFS0Header.java
Normal file
125
src/main/java/libKonogonka/Tools/PFS0/PFS0Header.java
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018-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 org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static libKonogonka.Converter.getLEint;
|
||||||
|
import static libKonogonka.Converter.getLElong;
|
||||||
|
|
||||||
|
public class PFS0Header {
|
||||||
|
private final static Logger log = LogManager.getLogger(PFS0Header.class);
|
||||||
|
|
||||||
|
private final String magic;
|
||||||
|
private final int filesCount;
|
||||||
|
private final int stringTableSize;
|
||||||
|
private final byte[] padding;
|
||||||
|
private final PFS0subFile[] pfs0subFiles;
|
||||||
|
|
||||||
|
public PFS0Header(BufferedInputStream stream) throws Exception{
|
||||||
|
byte[] fileStartingBytes = new byte[0x10];
|
||||||
|
if (0x10 != stream.read(fileStartingBytes))
|
||||||
|
throw new Exception("Reading stream suddenly ended while trying to read starting 0x10 bytes");
|
||||||
|
// Check PFS0Provider
|
||||||
|
magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII);
|
||||||
|
if (! magic.equals("PFS0")){
|
||||||
|
throw new Exception("Bad magic");
|
||||||
|
}
|
||||||
|
// Get files count
|
||||||
|
filesCount = getLEint(fileStartingBytes, 0x4);
|
||||||
|
if (filesCount <= 0 ) {
|
||||||
|
throw new Exception("Files count is too small");
|
||||||
|
}
|
||||||
|
// Get string table
|
||||||
|
stringTableSize = getLEint(fileStartingBytes, 0x8);
|
||||||
|
if (stringTableSize <= 0 ){
|
||||||
|
throw new Exception("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++){
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
//*******************************************************************
|
||||||
|
// Here pointer is in front of String table
|
||||||
|
String[] subFileNames = new String[filesCount];
|
||||||
|
byte[] stringTbl = new byte[stringTableSize];
|
||||||
|
if (stream.read(stringTbl) != stringTableSize){
|
||||||
|
throw new Exception("Read PFS0Provider String table failure. Can't read requested string table size ("+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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMagic() {return magic;}
|
||||||
|
public int getFilesCount() {return filesCount;}
|
||||||
|
public int getStringTableSize() {return stringTableSize;}
|
||||||
|
public byte[] getPadding() {return padding;}
|
||||||
|
public PFS0subFile[] getPfs0subFiles() {return pfs0subFiles;}
|
||||||
|
|
||||||
|
public void printDebug(){
|
||||||
|
log.debug(".:: PFS0Header ::.\n" +
|
||||||
|
"Magic " + magic + "\n" +
|
||||||
|
"Files count " + RainbowDump.formatDecHexString(filesCount) + "\n" +
|
||||||
|
"String Table Size " + RainbowDump.formatDecHexString(stringTableSize) + "\n" +
|
||||||
|
"Padding " + Converter.byteArrToHexString(padding) + "\n\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" +
|
||||||
|
"----------------------------------------------------------------"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,192 +18,61 @@
|
||||||
*/
|
*/
|
||||||
package libKonogonka.Tools.PFS0;
|
package libKonogonka.Tools.PFS0;
|
||||||
|
|
||||||
import libKonogonka.Converter;
|
|
||||||
import libKonogonka.RainbowDump;
|
import libKonogonka.RainbowDump;
|
||||||
|
import libKonogonka.Tools.ISuperProvider;
|
||||||
import libKonogonka.Tools.NCA.NCASectionTableBlock.SuperBlockPFS0;
|
import libKonogonka.Tools.NCA.NCASectionTableBlock.SuperBlockPFS0;
|
||||||
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
|
||||||
import libKonogonka.ctraes.InFileStreamProducer;
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import static libKonogonka.Converter.*;
|
public class PFS0Provider implements ISuperProvider {
|
||||||
|
|
||||||
public class PFS0Provider implements IPFS0Provider{
|
|
||||||
private final static Logger log = LogManager.getLogger(PFS0Provider.class);
|
private final static Logger log = LogManager.getLogger(PFS0Provider.class);
|
||||||
|
|
||||||
private String magic;
|
private final long rawBlockDataStart;
|
||||||
private int filesCount;
|
|
||||||
private int stringTableSize;
|
|
||||||
private byte[] padding;
|
|
||||||
private PFS0subFile[] pfs0subFiles;
|
|
||||||
//---------------------------------------
|
|
||||||
private long rawBlockDataStart;
|
|
||||||
|
|
||||||
private final File file;
|
|
||||||
private long offsetPositionInFile;
|
private long offsetPositionInFile;
|
||||||
private long mediaStartOffset; // In 512-blocks
|
private final InFileStreamProducer producer;
|
||||||
private long mediaEndOffset; // In 512-blocks
|
|
||||||
|
|
||||||
private long ncaOffset;
|
|
||||||
private BufferedInputStream stream;
|
private BufferedInputStream stream;
|
||||||
private SuperBlockPFS0 superBlockPFS0;
|
private SuperBlockPFS0 superBlockPFS0;
|
||||||
private AesCtrDecryptSimple decryptor;
|
|
||||||
|
|
||||||
|
private long mediaStartOffset;
|
||||||
|
|
||||||
|
private final PFS0Header header;
|
||||||
private LinkedList<byte[]> pfs0SHA256hashes;
|
private LinkedList<byte[]> pfs0SHA256hashes;
|
||||||
|
|
||||||
private boolean encrypted;
|
|
||||||
|
|
||||||
public PFS0Provider(File nspFile) throws Exception{
|
public PFS0Provider(File nspFile) throws Exception{
|
||||||
this.file = nspFile;
|
this.producer = new InFileStreamProducer(nspFile);
|
||||||
createBufferedInputStream();
|
this.stream = producer.produce();
|
||||||
readPfs0Header();
|
this.header = new PFS0Header(stream);
|
||||||
|
this.rawBlockDataStart = 0x10L + (header.getFilesCount() * 0x18L) + header.getStringTableSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PFS0Provider(File file,
|
public PFS0Provider(InFileStreamProducer producer,
|
||||||
long ncaOffset,
|
long offsetPositionInFile,
|
||||||
SuperBlockPFS0 superBlockPFS0,
|
SuperBlockPFS0 superBlockPFS0,
|
||||||
long mediaStartOffset,
|
long mediaStartOffset) throws Exception {
|
||||||
long mediaEndOffset) throws Exception{
|
this.producer = producer;
|
||||||
this.file = file;
|
this.offsetPositionInFile = offsetPositionInFile;
|
||||||
this.ncaOffset = ncaOffset;
|
|
||||||
this.superBlockPFS0 = superBlockPFS0;
|
this.superBlockPFS0 = superBlockPFS0;
|
||||||
this.offsetPositionInFile = ncaOffset + mediaStartOffset * 0x200;
|
|
||||||
this.mediaStartOffset = mediaStartOffset;
|
this.mediaStartOffset = mediaStartOffset;
|
||||||
this.mediaEndOffset = mediaEndOffset;
|
|
||||||
this.rawBlockDataStart = superBlockPFS0.getPfs0offset();
|
this.stream = producer.produce();
|
||||||
//bufferedInputStream = new BufferedInputStream(Files.newInputStream(fileWithPfs0.toPath()));
|
|
||||||
createBufferedInputStream();
|
|
||||||
long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
|
long toSkip = offsetPositionInFile + superBlockPFS0.getHashTableOffset();
|
||||||
if (toSkip != stream.skip(toSkip))
|
if (toSkip != stream.skip(toSkip))
|
||||||
throw new Exception("Can't skip bytes prior Hash Table offset");
|
throw new Exception("Can't skip bytes prior Hash Table offset");
|
||||||
collectHashes();
|
collectHashes();
|
||||||
|
|
||||||
createBufferedInputStream();
|
this.stream = producer.produce();
|
||||||
toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
|
toSkip = offsetPositionInFile + superBlockPFS0.getPfs0offset();
|
||||||
if (toSkip != stream.skip(toSkip))
|
if (toSkip != stream.skip(toSkip))
|
||||||
throw new Exception("Can't skip bytes prior PFS0 offset");
|
throw new Exception("Can't skip bytes prior PFS0 offset");
|
||||||
readPfs0Header();
|
this.header = new PFS0Header(stream);
|
||||||
}
|
this.rawBlockDataStart = superBlockPFS0.getPfs0offset() + 0x10L + (header.getFilesCount() * 0x18L) + header.getStringTableSize();
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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];
|
|
||||||
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("Bad magic");
|
|
||||||
}
|
|
||||||
// Get files count
|
|
||||||
filesCount = getLEint(fileStartingBytes, 0x4);
|
|
||||||
if (filesCount <= 0 ) {
|
|
||||||
throw new Exception("Files count is too small");
|
|
||||||
}
|
|
||||||
// Get string table
|
|
||||||
stringTableSize = getLEint(fileStartingBytes, 0x8);
|
|
||||||
if (stringTableSize <= 0 ){
|
|
||||||
throw new Exception("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++){
|
|
||||||
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);
|
|
||||||
rawBlockDataStart += 0x18;
|
|
||||||
}
|
|
||||||
//*******************************************************************
|
|
||||||
// In here pointer in front of String table
|
|
||||||
String[] subFileNames = new String[filesCount];
|
|
||||||
byte[] stringTbl = new byte[stringTableSize];
|
|
||||||
if (stream.read(stringTbl) != stringTableSize){
|
|
||||||
throw new Exception("Read PFS0Provider String table failure. Can't read requested string table size ("+stringTableSize+")");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]);
|
|
||||||
}
|
|
||||||
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{
|
private void collectHashes() throws Exception{
|
||||||
|
@ -223,25 +92,14 @@ public class PFS0Provider implements IPFS0Provider{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public boolean isEncrypted() { return producer.isEncrypted(); }
|
||||||
public boolean isEncrypted() { return true; }
|
public PFS0Header getHeader() {return header;}
|
||||||
@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
|
@Override
|
||||||
public long getRawFileDataStart() { return rawBlockDataStart;}
|
public long getRawFileDataStart() { return rawBlockDataStart;}
|
||||||
@Override
|
|
||||||
public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; }
|
|
||||||
@Override
|
|
||||||
public File getFile(){ return file; }
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exportContent(String saveToLocation, String subFileName){
|
public boolean exportContent(String saveToLocation, String subFileName){
|
||||||
|
PFS0subFile[] pfs0subFiles = header.getPfs0subFiles();
|
||||||
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 exportContent(saveToLocation, i);
|
return exportContent(saveToLocation, i);
|
||||||
|
@ -250,16 +108,14 @@ public class PFS0Provider implements IPFS0Provider{
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public boolean exportContent(String saveToLocation, int subFileNumber){
|
public boolean exportContent(String saveToLocation, int subFileNumber){
|
||||||
PFS0subFile subFile = pfs0subFiles[subFileNumber];
|
PFS0subFile subFile = header.getPfs0subFiles()[subFileNumber];
|
||||||
File location = new File(saveToLocation);
|
File location = new File(saveToLocation);
|
||||||
location.mkdirs();
|
location.mkdirs();
|
||||||
|
|
||||||
try (BufferedOutputStream extractedFileBOS = new BufferedOutputStream(
|
try (BufferedOutputStream extractedFileBOS = new BufferedOutputStream(
|
||||||
Files.newOutputStream(Paths.get(saveToLocation+File.separator+subFile.getName())))){
|
Files.newOutputStream(Paths.get(saveToLocation+File.separator+subFile.getName())))){
|
||||||
if (encrypted)
|
|
||||||
createAesCtrEncryptedBufferedInputStream();
|
this.stream = producer.produce();
|
||||||
else
|
|
||||||
createBufferedInputStream();
|
|
||||||
|
|
||||||
long subFileSize = subFile.getSize();
|
long subFileSize = subFile.getSize();
|
||||||
|
|
||||||
|
@ -297,6 +153,7 @@ public class PFS0Provider implements IPFS0Provider{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InFileStreamProducer getStreamProducer(String subFileName) throws FileNotFoundException {
|
public InFileStreamProducer getStreamProducer(String subFileName) throws FileNotFoundException {
|
||||||
|
PFS0subFile[] pfs0subFiles = header.getPfs0subFiles();
|
||||||
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 getStreamProducer(i);
|
return getStreamProducer(i);
|
||||||
|
@ -305,110 +162,28 @@ public class PFS0Provider implements IPFS0Provider{
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public InFileStreamProducer getStreamProducer(int subFileNumber) {
|
public InFileStreamProducer getStreamProducer(int subFileNumber) {
|
||||||
PFS0subFile subFile = pfs0subFiles[subFileNumber];
|
PFS0subFile subFile = header.getPfs0subFiles()[subFileNumber];
|
||||||
long subFileOffset = subFile.getOffset() + mediaStartOffset * 0x200 + rawBlockDataStart;
|
long subFileOffset = subFile.getOffset() + mediaStartOffset * 0x200 + rawBlockDataStart;
|
||||||
|
|
||||||
if (encrypted) {
|
return producer.getSuccessor(subFileOffset);
|
||||||
return new InFileStreamProducer(file,
|
|
||||||
pfs0subFiles[subFileNumber].getSize(),
|
|
||||||
ncaOffset,
|
|
||||||
subFileOffset,
|
|
||||||
decryptor,
|
|
||||||
mediaStartOffset,
|
|
||||||
mediaEndOffset);
|
|
||||||
}
|
}
|
||||||
return new InFileStreamProducer(file, offsetPositionInFile, subFileOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* */
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
throw new Exception("No file with such name exists: "+subFileName);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* */
|
|
||||||
@Override
|
|
||||||
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception {
|
|
||||||
PipedOutputStream streamOut = new PipedOutputStream();
|
|
||||||
PipedInputStream streamInp = new PipedInputStream(streamOut);
|
|
||||||
|
|
||||||
Thread workerThread = new Thread(() -> {
|
|
||||||
try {
|
|
||||||
PFS0subFile subFile = pfs0subFiles[subFileNumber];
|
|
||||||
|
|
||||||
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);
|
|
||||||
streamOut.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(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
workerThread.start();
|
|
||||||
return streamInp;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public LinkedList<byte[]> getPfs0SHA256hashes() {
|
public LinkedList<byte[]> getPfs0SHA256hashes() {
|
||||||
return pfs0SHA256hashes;
|
return pfs0SHA256hashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getFile() {
|
||||||
|
return producer.getFile();
|
||||||
|
}
|
||||||
|
|
||||||
public void printDebug(){
|
public void printDebug(){
|
||||||
log.debug(".:: PFS0Provider ::.\n" +
|
log.debug(".:: PFS0Provider ::.\n" +
|
||||||
"File name: " + file.getName() + "\n" +
|
"File name: " + getFile().getName() + "\n" +
|
||||||
"Raw block data start " + RainbowDump.formatDecHexString(rawBlockDataStart) + "\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" +
|
"Offset position in file " + RainbowDump.formatDecHexString(offsetPositionInFile) + "\n" +
|
||||||
"Media Start Offset " + RainbowDump.formatDecHexString(mediaStartOffset) + "\n" +
|
"Media Start Offset " + RainbowDump.formatDecHexString(mediaStartOffset) + "\n"
|
||||||
"Media End Offset " + RainbowDump.formatDecHexString(mediaEndOffset) + "\n"
|
|
||||||
);
|
);
|
||||||
for (PFS0subFile subFile : pfs0subFiles){
|
header.printDebug();
|
||||||
log.debug(
|
|
||||||
"\nName: " + subFile.getName() + "\n" +
|
|
||||||
"Offset " + RainbowDump.formatDecHexString(subFile.getOffset()) + "\n" +
|
|
||||||
"Size " + RainbowDump.formatDecHexString(subFile.getSize()) + "\n" +
|
|
||||||
"Zeroes " + Converter.byteArrToHexString(subFile.getZeroes()) + "\n" +
|
|
||||||
"----------------------------------------------------------------"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,32 +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.RomFs;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.PipedInputStream;
|
|
||||||
|
|
||||||
public interface IRomFsProvider {
|
|
||||||
File getFile();
|
|
||||||
long getLevel6Offset();
|
|
||||||
Level6Header getHeader();
|
|
||||||
FileSystemEntry getRootEntry();
|
|
||||||
PipedInputStream getContent(FileSystemEntry entry) throws Exception;
|
|
||||||
void printDebug();
|
|
||||||
}
|
|
|
@ -19,16 +19,11 @@
|
||||||
package libKonogonka.Tools.RomFs;
|
package libKonogonka.Tools.RomFs;
|
||||||
|
|
||||||
import libKonogonka.Converter;
|
import libKonogonka.Converter;
|
||||||
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
public class RomFsConstruct {
|
public class RomFsConstruct {
|
||||||
//private final static Logger log = LogManager.getLogger(RomFsConstruct.class);
|
|
||||||
|
|
||||||
private Level6Header header;
|
private Level6Header header;
|
||||||
|
|
||||||
private FileSystemEntry rootEntry;
|
private FileSystemEntry rootEntry;
|
||||||
|
@ -37,55 +32,29 @@ public class RomFsConstruct {
|
||||||
private byte[] directoryMetadataTable;
|
private byte[] directoryMetadataTable;
|
||||||
private byte[] fileMetadataTable;
|
private byte[] fileMetadataTable;
|
||||||
|
|
||||||
|
private final long offsetPositionInFile;
|
||||||
private final File file;
|
|
||||||
private long offsetPositionInFile;
|
|
||||||
private final long level6Offset;
|
private final long level6Offset;
|
||||||
|
|
||||||
RomFsConstruct(File file,
|
RomFsConstruct(InFileStreamProducer producer, long level6Offset) throws Exception{
|
||||||
long level6Offset) throws Exception{
|
this(producer, level6Offset, 0);
|
||||||
if (level6Offset < 0)
|
|
||||||
throw new Exception("Incorrect Level 6 Offset");
|
|
||||||
this.file = file;
|
|
||||||
this.level6Offset = level6Offset;
|
|
||||||
this.stream = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
|
||||||
constructEverything();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RomFsConstruct(File file,
|
RomFsConstruct(InFileStreamProducer producer,
|
||||||
long ncaOffset, // NCA offset position
|
|
||||||
long level6Offset,
|
long level6Offset,
|
||||||
AesCtrDecryptSimple decryptor,
|
long offsetPositionInFile) throws Exception{
|
||||||
long mediaStartOffset,
|
|
||||||
long mediaEndOffset
|
|
||||||
) throws Exception {
|
|
||||||
if (level6Offset < 0)
|
if (level6Offset < 0)
|
||||||
throw new Exception("Incorrect Level 6 Offset");
|
throw new Exception("Incorrect Level 6 Offset");
|
||||||
this.file = file;
|
|
||||||
this.level6Offset = level6Offset;
|
this.level6Offset = level6Offset;
|
||||||
this.offsetPositionInFile = ncaOffset + (mediaStartOffset * 0x200);
|
this.stream = producer.produce();
|
||||||
this.stream = new AesCtrBufferedInputStream(
|
this.offsetPositionInFile = offsetPositionInFile;
|
||||||
decryptor,
|
|
||||||
ncaOffset,
|
|
||||||
mediaStartOffset,
|
|
||||||
mediaEndOffset,
|
|
||||||
Files.newInputStream(file.toPath()));
|
|
||||||
constructEverything();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void constructEverything() throws Exception{
|
|
||||||
goToStartingPosition();
|
goToStartingPosition();
|
||||||
|
|
||||||
constructHeader();
|
constructHeader();
|
||||||
|
|
||||||
directoryMetadataTableLengthCheck();
|
directoryMetadataTableLengthCheck();
|
||||||
directoryMetadataTableConstruct();
|
directoryMetadataTableConstruct();
|
||||||
|
|
||||||
fileMetadataTableLengthCheck();
|
fileMetadataTableLengthCheck();
|
||||||
fileMetadataTableConstruct();
|
fileMetadataTableConstruct();
|
||||||
|
|
||||||
constructRootFilesystemEntry();
|
constructRootFilesystemEntry();
|
||||||
|
|
||||||
stream.close();
|
stream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,13 +106,8 @@ public class RomFsConstruct {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void constructRootFilesystemEntry() throws Exception{
|
private void constructRootFilesystemEntry() throws Exception{
|
||||||
try {
|
|
||||||
rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable);
|
rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable);
|
||||||
}
|
}
|
||||||
catch (Exception e){
|
|
||||||
throw new Exception("File: " + file.getName(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void skipBytes(long size) throws Exception{
|
private void skipBytes(long size) throws Exception{
|
||||||
long mustSkip = size;
|
long mustSkip = size;
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018-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.RomFs;
|
|
||||||
|
|
||||||
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
|
|
||||||
public class RomFsContentRetrieve implements Runnable{
|
|
||||||
private final static Logger log = LogManager.getLogger(RomFsContentRetrieve.class);
|
|
||||||
|
|
||||||
private final PipedOutputStream streamOut;
|
|
||||||
private final long internalFileSize;
|
|
||||||
private final long startPosition;
|
|
||||||
private final BufferedInputStream bis;
|
|
||||||
|
|
||||||
RomFsContentRetrieve(File parentFile,
|
|
||||||
PipedOutputStream streamOut,
|
|
||||||
long internalFileRealPosition,
|
|
||||||
long internalFileSize) throws Exception{
|
|
||||||
this.streamOut = streamOut;
|
|
||||||
this.internalFileSize = internalFileSize;
|
|
||||||
|
|
||||||
this.startPosition = internalFileRealPosition;
|
|
||||||
this.bis = new BufferedInputStream(Files.newInputStream(parentFile.toPath()));
|
|
||||||
}
|
|
||||||
|
|
||||||
RomFsContentRetrieve(File parentFile,
|
|
||||||
PipedOutputStream streamOut,
|
|
||||||
AesCtrDecryptSimple decryptor,
|
|
||||||
long entryOffset,
|
|
||||||
long internalFileSize,
|
|
||||||
long headersFileDataOffset, //level6Header.getFileDataOffset()
|
|
||||||
long level6Offset,
|
|
||||||
long ncaOffsetPosition,
|
|
||||||
long mediaStartOffset,
|
|
||||||
long mediaEndOffset
|
|
||||||
) throws Exception{
|
|
||||||
log.fatal("Current implementation works incorrectly");
|
|
||||||
this.streamOut = streamOut;
|
|
||||||
this.internalFileSize = internalFileSize;
|
|
||||||
|
|
||||||
this.startPosition = entryOffset + mediaStartOffset*0x200 + headersFileDataOffset + level6Offset;
|
|
||||||
|
|
||||||
this.bis = new AesCtrBufferedInputStream(
|
|
||||||
decryptor,
|
|
||||||
ncaOffsetPosition,
|
|
||||||
mediaStartOffset,
|
|
||||||
mediaEndOffset,
|
|
||||||
Files.newInputStream(parentFile.toPath())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
log.trace("Executing thread");
|
|
||||||
try {
|
|
||||||
skipBytesTillBegining();
|
|
||||||
|
|
||||||
int readPiece = 8388608;
|
|
||||||
long readFrom = 0;
|
|
||||||
byte[] readBuffer;
|
|
||||||
|
|
||||||
while (readFrom < internalFileSize) {
|
|
||||||
if (internalFileSize - readFrom < readPiece)
|
|
||||||
readPiece = Math.toIntExact(internalFileSize - readFrom);
|
|
||||||
readBuffer = new byte[readPiece];
|
|
||||||
if (bis.read(readBuffer) != readPiece) {
|
|
||||||
log.error("getContent(): Unable to read requested size from file.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
streamOut.write(readBuffer);
|
|
||||||
readFrom += readPiece;
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
|
||||||
log.error("RomFsProvider -> getContent(): Unable to provide stream", exception);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
closeStreams();
|
|
||||||
}
|
|
||||||
log.trace("Thread died");
|
|
||||||
}
|
|
||||||
private void skipBytesTillBegining() throws Exception{
|
|
||||||
long mustSkip = startPosition;
|
|
||||||
long skipped = 0;
|
|
||||||
while (mustSkip > 0){
|
|
||||||
skipped += bis.skip(mustSkip);
|
|
||||||
mustSkip = startPosition - skipped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private void closeStreams(){
|
|
||||||
try {
|
|
||||||
streamOut.close();
|
|
||||||
}
|
|
||||||
catch (IOException e){
|
|
||||||
log.error("RomFsProvider -> getContent(): Unable to close 'StreamOut'");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
bis.close();
|
|
||||||
}
|
|
||||||
catch (IOException e){
|
|
||||||
log.error("RomFsProvider -> getContent(): Unable to close 'StreamOut'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,134 +18,130 @@
|
||||||
*/
|
*/
|
||||||
package libKonogonka.Tools.RomFs;
|
package libKonogonka.Tools.RomFs;
|
||||||
|
|
||||||
|
import libKonogonka.Tools.PFS0.PFS0subFile;
|
||||||
import libKonogonka.Tools.RomFs.view.DirectoryMetaTablePlainView;
|
import libKonogonka.Tools.RomFs.view.DirectoryMetaTablePlainView;
|
||||||
import libKonogonka.Tools.RomFs.view.FileMetaTablePlainView;
|
import libKonogonka.Tools.RomFs.view.FileMetaTablePlainView;
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.PipedInputStream;
|
import java.nio.file.Files;
|
||||||
import java.io.PipedOutputStream;
|
|
||||||
|
|
||||||
public class RomFsProvider implements IRomFsProvider{
|
public class RomFsProvider{
|
||||||
private final File file;
|
private final static Logger log = LogManager.getLogger(RomFsProvider.class);
|
||||||
|
|
||||||
|
private final InFileStreamProducer producer;
|
||||||
private final long level6Offset;
|
private final long level6Offset;
|
||||||
private final Level6Header level6Header;
|
private final Level6Header level6Header;
|
||||||
private final FileSystemEntry rootEntry;
|
private final FileSystemEntry rootEntry;
|
||||||
|
private long mediaStartOffset;
|
||||||
private long ncaOffsetPosition;
|
|
||||||
private byte[] key; // Used @ createDecryptor only
|
|
||||||
private byte[] sectionCTR; // Used @ createDecryptor only
|
|
||||||
private long mediaStartOffset; // Used @ createDecryptor only
|
|
||||||
private long mediaEndOffset;
|
|
||||||
// Used only for debug
|
// Used only for debug
|
||||||
private final byte[] directoryMetadataTable;
|
private final byte[] directoryMetadataTable;
|
||||||
private final byte[] fileMetadataTable;
|
private final byte[] fileMetadataTable;
|
||||||
|
|
||||||
private final boolean encryptedAesCtr;
|
|
||||||
|
|
||||||
public RomFsProvider(File decryptedFsImageFile, long level6offset) throws Exception{
|
public RomFsProvider(File decryptedFsImageFile, long level6offset) throws Exception{
|
||||||
RomFsConstruct construct = new RomFsConstruct(decryptedFsImageFile, level6offset);
|
this.producer = new InFileStreamProducer(decryptedFsImageFile);
|
||||||
this.file = decryptedFsImageFile;
|
RomFsConstruct construct = new RomFsConstruct(producer, level6offset);
|
||||||
this.level6Offset = level6offset;
|
this.level6Offset = level6offset;
|
||||||
this.level6Header = construct.getHeader();
|
this.level6Header = construct.getHeader();
|
||||||
this.rootEntry = construct.getRootEntry();
|
this.rootEntry = construct.getRootEntry();
|
||||||
|
|
||||||
this.directoryMetadataTable = construct.getDirectoryMetadataTable();
|
this.directoryMetadataTable = construct.getDirectoryMetadataTable();
|
||||||
this.fileMetadataTable = construct.getFileMetadataTable();
|
this.fileMetadataTable = construct.getFileMetadataTable();
|
||||||
|
|
||||||
this.encryptedAesCtr = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public RomFsProvider(long level6Offset,
|
public RomFsProvider(InFileStreamProducer producer,
|
||||||
File encryptedFsImageFile,
|
long level6Offset,
|
||||||
long ncaOffsetPosition,
|
long offsetPositionInFile,
|
||||||
byte[] key,
|
long mediaStartOffset
|
||||||
byte[] sectionCTR,
|
|
||||||
long mediaStartOffset,
|
|
||||||
long mediaEndOffset
|
|
||||||
) throws Exception{
|
) throws Exception{
|
||||||
this.key = key;
|
this.producer = producer;
|
||||||
this.sectionCTR = sectionCTR;
|
|
||||||
this.mediaStartOffset = mediaStartOffset;
|
this.mediaStartOffset = mediaStartOffset;
|
||||||
this.mediaEndOffset = mediaEndOffset;
|
RomFsConstruct construct = new RomFsConstruct(producer, level6Offset, offsetPositionInFile);
|
||||||
this.ncaOffsetPosition = ncaOffsetPosition;
|
|
||||||
|
|
||||||
RomFsConstruct construct = new RomFsConstruct(encryptedFsImageFile,
|
|
||||||
ncaOffsetPosition,
|
|
||||||
level6Offset,
|
|
||||||
createDecryptor(),
|
|
||||||
mediaStartOffset,
|
|
||||||
mediaEndOffset);
|
|
||||||
this.file = encryptedFsImageFile;
|
|
||||||
this.level6Offset = level6Offset;
|
this.level6Offset = level6Offset;
|
||||||
this.level6Header = construct.getHeader();
|
this.level6Header = construct.getHeader();
|
||||||
this.rootEntry = construct.getRootEntry();
|
this.rootEntry = construct.getRootEntry();
|
||||||
|
|
||||||
this.directoryMetadataTable = construct.getDirectoryMetadataTable();
|
this.directoryMetadataTable = construct.getDirectoryMetadataTable();
|
||||||
this.fileMetadataTable = construct.getFileMetadataTable();
|
this.fileMetadataTable = construct.getFileMetadataTable();
|
||||||
|
|
||||||
this.encryptedAesCtr = true;
|
|
||||||
}
|
|
||||||
private AesCtrDecryptSimple createDecryptor() throws Exception{
|
|
||||||
return new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public File getFile() { return file; }
|
|
||||||
@Override
|
|
||||||
public long getLevel6Offset() { return level6Offset; }
|
public long getLevel6Offset() { return level6Offset; }
|
||||||
@Override
|
|
||||||
public Level6Header getHeader() {return level6Header;}
|
public Level6Header getHeader() {return level6Header;}
|
||||||
@Override
|
|
||||||
public FileSystemEntry getRootEntry() { return rootEntry; }
|
public FileSystemEntry getRootEntry() { return rootEntry; }
|
||||||
@Override
|
|
||||||
public PipedInputStream getContent(FileSystemEntry entry) throws Exception {
|
public boolean exportContent(String saveToLocation, FileSystemEntry entry){
|
||||||
if (encryptedAesCtr)
|
try{
|
||||||
return getContentAesCtrEncrypted(entry);
|
if (! saveToLocation.endsWith(File.separator))
|
||||||
return getContentNonEncrypted(entry);
|
saveToLocation += File.separator;
|
||||||
}
|
|
||||||
public PipedInputStream getContentAesCtrEncrypted(FileSystemEntry entry) throws Exception{
|
|
||||||
if (entry.isDirectory())
|
if (entry.isDirectory())
|
||||||
throw new Exception("Request of the binary stream for the folder entry is not supported (and doesn't make sense).");
|
exportFolderContent(entry, saveToLocation);
|
||||||
|
else
|
||||||
PipedOutputStream streamOut = new PipedOutputStream();
|
exportSingleFile(entry, saveToLocation);
|
||||||
PipedInputStream streamIn = new PipedInputStream(streamOut);
|
|
||||||
long internalFileOffset = entry.getOffset();
|
|
||||||
long internalFileSize = entry.getSize();
|
|
||||||
|
|
||||||
Thread contentRetrievingThread = new Thread(new RomFsContentRetrieve(
|
|
||||||
file,
|
|
||||||
streamOut,
|
|
||||||
createDecryptor(),
|
|
||||||
internalFileOffset,
|
|
||||||
internalFileSize,
|
|
||||||
level6Header.getFileDataOffset(),
|
|
||||||
level6Offset,
|
|
||||||
ncaOffsetPosition,
|
|
||||||
mediaStartOffset,
|
|
||||||
mediaEndOffset));
|
|
||||||
contentRetrievingThread.start();
|
|
||||||
return streamIn;
|
|
||||||
}
|
}
|
||||||
public PipedInputStream getContentNonEncrypted(FileSystemEntry entry) throws Exception{
|
catch (Exception e){
|
||||||
|
log.error("File export failure", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportFolderContent(FileSystemEntry entry, String saveToLocation) throws Exception{
|
||||||
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
contentFile.mkdirs();
|
||||||
|
String currentDirPath = saveToLocation + entry.getName() + File.separator;
|
||||||
|
for (FileSystemEntry fileEntry : entry.getContent()){
|
||||||
|
if (fileEntry.isDirectory())
|
||||||
|
exportFolderContent(fileEntry, currentDirPath);
|
||||||
|
else
|
||||||
|
exportSingleFile(fileEntry, currentDirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportSingleFile(FileSystemEntry entry, String saveToLocation) throws Exception {
|
||||||
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
try(BufferedOutputStream extractedFileBOS = new BufferedOutputStream(Files.newOutputStream(contentFile.toPath()));
|
||||||
|
BufferedInputStream stream = producer.produce()) {
|
||||||
|
long skipBytes = entry.getOffset() + mediaStartOffset * 0x200 + level6Header.getFileDataOffset() + level6Offset;
|
||||||
|
|
||||||
|
if (skipBytes != stream.skip(skipBytes))
|
||||||
|
throw new Exception("Can't skip");
|
||||||
|
|
||||||
|
int blockSize = 0x200;
|
||||||
|
if (entry.getSize() < 0x200)
|
||||||
|
blockSize = (int) entry.getSize();
|
||||||
|
|
||||||
|
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) >= entry.getSize()) {
|
||||||
|
blockSize = (int) (entry.getSize() - i);
|
||||||
|
if (blockSize == 0)
|
||||||
|
break;
|
||||||
|
block = new byte[blockSize];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public InFileStreamProducer getStreamProducer(FileSystemEntry entry) throws Exception{
|
||||||
if (entry.isDirectory())
|
if (entry.isDirectory())
|
||||||
throw new Exception("Request of the binary stream for the folder entry is not supported (and doesn't make sense).");
|
throw new Exception("Directory entries are not supported");
|
||||||
|
return producer.getSuccessor(
|
||||||
PipedOutputStream streamOut = new PipedOutputStream();
|
entry.getOffset() + mediaStartOffset * 0x200 + level6Header.getFileDataOffset() + level6Offset);
|
||||||
PipedInputStream streamIn = new PipedInputStream(streamOut);
|
}
|
||||||
long internalFileRealPosition = level6Offset + level6Header.getFileDataOffset() + entry.getOffset();
|
|
||||||
long internalFileSize = entry.getSize();
|
public File getFile(){
|
||||||
|
return producer.getFile();
|
||||||
Thread contentRetrievingThread = new Thread(new RomFsContentRetrieve(
|
|
||||||
file,
|
|
||||||
streamOut,
|
|
||||||
internalFileRealPosition,
|
|
||||||
internalFileSize));
|
|
||||||
contentRetrievingThread.start();
|
|
||||||
return streamIn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void printDebug(){
|
public void printDebug(){
|
||||||
level6Header.printDebugInfo();
|
level6Header.printDebugInfo();
|
||||||
new DirectoryMetaTablePlainView(level6Header.getDirectoryMetadataTableLength(), directoryMetadataTable);
|
new DirectoryMetaTablePlainView(level6Header.getDirectoryMetadataTableLength(), directoryMetadataTable);
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
package libKonogonka.Tools.XCI;
|
package libKonogonka.Tools.XCI;
|
||||||
|
|
||||||
public class HFS0File {
|
public class HFS0File {
|
||||||
private String name;
|
private final String name;
|
||||||
private long offset;
|
private final long offset;
|
||||||
private long size;
|
private final long size;
|
||||||
private long hashedRegionSize;
|
private final long hashedRegionSize;
|
||||||
private boolean padding;
|
private final boolean padding;
|
||||||
private byte[] SHA256Hash;
|
private final byte[] SHA256Hash;
|
||||||
|
|
||||||
public HFS0File(String name, long offset, long size, long hashedRegionSize, boolean padding, byte[] SHA256Hash){
|
public HFS0File(String name, long offset, long size, long hashedRegionSize, boolean padding, byte[] SHA256Hash){
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|
|
@ -19,12 +19,14 @@
|
||||||
package libKonogonka.Tools.XCI;
|
package libKonogonka.Tools.XCI;
|
||||||
|
|
||||||
import libKonogonka.Tools.ISuperProvider;
|
import libKonogonka.Tools.ISuperProvider;
|
||||||
|
import libKonogonka.ctraes.InFileStreamProducer;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
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 static libKonogonka.Converter.*;
|
import static libKonogonka.Converter.*;
|
||||||
|
@ -125,74 +127,71 @@ public class HFS0Provider implements ISuperProvider {
|
||||||
@Override
|
@Override
|
||||||
public long getRawFileDataStart() { return rawFileDataStart; }
|
public long getRawFileDataStart() { return rawFileDataStart; }
|
||||||
public HFS0File[] getHfs0Files() { return hfs0Files; }
|
public HFS0File[] getHfs0Files() { return hfs0Files; }
|
||||||
@Override
|
|
||||||
public File getFile(){ return file; }
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* */
|
|
||||||
@Override
|
|
||||||
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception{
|
|
||||||
PipedOutputStream streamOut = new PipedOutputStream();
|
|
||||||
Thread workerThread;
|
|
||||||
if (subFileNumber >= hfs0Files.length) {
|
|
||||||
throw new Exception("HFS0Provider -> getHfs0FilePipedInpStream(): Requested sub file doesn't exists");
|
|
||||||
}
|
|
||||||
PipedInputStream streamIn = new PipedInputStream(streamOut);
|
|
||||||
|
|
||||||
workerThread = new Thread(() -> {
|
|
||||||
log.trace("HFS0Provider -> getHfs0FilePipedInpStream(): Executing thread");
|
|
||||||
try{
|
|
||||||
long subFileRealPosition = rawFileDataStart + hfs0Files[subFileNumber].getOffset();
|
|
||||||
BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
|
||||||
if (bis.skip(subFileRealPosition) != subFileRealPosition)
|
|
||||||
throw new Exception("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to skip requested offset");
|
|
||||||
|
|
||||||
int readPice = 0x800000; // 8mb NOTE: consider switching to 1mb 1048576
|
|
||||||
|
|
||||||
long readFrom = 0;
|
|
||||||
long realFileSize = hfs0Files[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)
|
|
||||||
throw new Exception("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to read requested size from file.");
|
|
||||||
|
|
||||||
streamOut.write(readBuf, 0, readPice);
|
|
||||||
readFrom += readPice;
|
|
||||||
}
|
|
||||||
bis.close();
|
|
||||||
streamOut.close();
|
|
||||||
}
|
|
||||||
catch (Exception ioe){
|
|
||||||
log.error("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to provide stream");
|
|
||||||
ioe.printStackTrace();
|
|
||||||
}
|
|
||||||
log.trace("HFS0Provider -> getHfs0FilePipedInpStream(): Thread died");
|
|
||||||
});
|
|
||||||
workerThread.start();
|
|
||||||
return streamIn;
|
|
||||||
}
|
|
||||||
//TODO
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exportContent(String saveToLocation, String subFileName) throws Exception {
|
public boolean exportContent(String saveToLocation, String subFileName) throws Exception {
|
||||||
throw new Exception("Not implemented yet");
|
for (int i = 0; i < hfs0Files.length; i++) {
|
||||||
|
if (hfs0Files[i].getName().equals(subFileName))
|
||||||
|
return exportContent(saveToLocation, i);
|
||||||
}
|
}
|
||||||
//TODO
|
throw new FileNotFoundException("No file with such name exists: " + subFileName);
|
||||||
@Override
|
|
||||||
public boolean exportContent(String saveToLocation, int subFileNumber) throws Exception {
|
|
||||||
throw new Exception("Not implemented yet");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception {
|
public boolean exportContent(String saveToLocation, int subFileNumber) throws Exception {
|
||||||
|
HFS0File subFile = hfs0Files[subFileNumber];
|
||||||
|
File location = new File(saveToLocation);
|
||||||
|
location.mkdirs();
|
||||||
|
|
||||||
|
try (BufferedOutputStream extractedFileBOS = new BufferedOutputStream(
|
||||||
|
Files.newOutputStream(Paths.get(saveToLocation+File.separator+subFile.getName())));
|
||||||
|
BufferedInputStream stream = getStreamProducer(subFileNumber).produce()){
|
||||||
|
|
||||||
|
long subFileSize = subFile.getSize();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public InFileStreamProducer getStreamProducer(String subFileName) throws FileNotFoundException{
|
||||||
for (int i = 0; i < hfs0Files.length; i++){
|
for (int i = 0; i < hfs0Files.length; i++){
|
||||||
if (hfs0Files[i].getName().equals(subFileName))
|
if (hfs0Files[i].getName().equals(subFileName))
|
||||||
return getProviderSubFilePipedInpStream(i);
|
return getStreamProducer(i);
|
||||||
}
|
}
|
||||||
return null;
|
throw new FileNotFoundException("No file with such name exists: "+subFileName);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public InFileStreamProducer getStreamProducer(int subFileNumber) {
|
||||||
|
long offset = rawFileDataStart + hfs0Files[subFileNumber].getOffset();
|
||||||
|
return new InFileStreamProducer(file, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getFile() {
|
||||||
|
return file;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,9 +18,9 @@
|
||||||
*/
|
*/
|
||||||
package libKonogonka.Tools.XCI;
|
package libKonogonka.Tools.XCI;
|
||||||
|
|
||||||
|
import libKonogonka.Converter;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static libKonogonka.Converter.getLEint;
|
import static libKonogonka.Converter.getLEint;
|
||||||
import static libKonogonka.Converter.getLElong;
|
import static libKonogonka.Converter.getLElong;
|
||||||
|
@ -28,27 +28,27 @@ import static libKonogonka.Converter.getLElong;
|
||||||
* Header information
|
* Header information
|
||||||
* */
|
* */
|
||||||
public class XCIGamecardHeader{
|
public class XCIGamecardHeader{
|
||||||
private byte[] rsa2048PKCS1sig;
|
private final byte[] rsa2048PKCS1sig;
|
||||||
private boolean magicHead;
|
private final boolean magicHead;
|
||||||
private byte[] SecureAreaStartAddr;
|
private final byte[] SecureAreaStartAddr;
|
||||||
private boolean bkupAreaStartAddr;
|
private final boolean bkupAreaStartAddr;
|
||||||
private byte titleKEKIndexBoth;
|
private final byte titleKEKIndexBoth;
|
||||||
private byte titleKEKIndex;
|
private final byte titleKEKIndex;
|
||||||
private byte KEKIndex;
|
private final byte KEKIndex;
|
||||||
private byte gcSize;
|
private final byte gcSize;
|
||||||
private byte gcVersion;
|
private final byte gcVersion;
|
||||||
private byte gcFlags;
|
private final byte gcFlags;
|
||||||
private byte[] pkgID;
|
private final byte[] pkgID;
|
||||||
private long valDataEndAddr;
|
private final long valDataEndAddr;
|
||||||
private byte[] gcInfoIV;
|
private final byte[] gcInfoIV;
|
||||||
private long hfs0partOffset;
|
private final long hfs0partOffset;
|
||||||
private long hfs0headerSize;
|
private final long hfs0headerSize;
|
||||||
private byte[] hfs0headerSHA256;
|
private final byte[] hfs0headerSHA256;
|
||||||
private byte[] hfs0initDataSHA256 ;
|
private final byte[] hfs0initDataSHA256;
|
||||||
private int secureModeFlag;
|
private final int secureModeFlag;
|
||||||
private int titleKeyFlag;
|
private final int titleKeyFlag;
|
||||||
private int keyFlag;
|
private final int keyFlag;
|
||||||
private byte[] normAreaEndAddr;
|
private final byte[] normAreaEndAddress;
|
||||||
|
|
||||||
XCIGamecardHeader(byte[] headerBytes) throws Exception{
|
XCIGamecardHeader(byte[] headerBytes) throws Exception{
|
||||||
if (headerBytes.length != 400)
|
if (headerBytes.length != 400)
|
||||||
|
@ -65,7 +65,7 @@ public class XCIGamecardHeader{
|
||||||
gcFlags = headerBytes[271];
|
gcFlags = headerBytes[271];
|
||||||
pkgID = Arrays.copyOfRange(headerBytes, 272, 280);
|
pkgID = Arrays.copyOfRange(headerBytes, 272, 280);
|
||||||
valDataEndAddr = getLElong(headerBytes, 280); //TODO: FIX/simplify //
|
valDataEndAddr = getLElong(headerBytes, 280); //TODO: FIX/simplify //
|
||||||
gcInfoIV = reverseBytes(Arrays.copyOfRange(headerBytes, 288, 304));
|
gcInfoIV = Converter.flip(Arrays.copyOfRange(headerBytes, 288, 304));
|
||||||
hfs0partOffset = getLElong(headerBytes, 304);
|
hfs0partOffset = getLElong(headerBytes, 304);
|
||||||
hfs0headerSize = getLElong(headerBytes, 312);
|
hfs0headerSize = getLElong(headerBytes, 312);
|
||||||
hfs0headerSHA256 = Arrays.copyOfRange(headerBytes, 320, 352);
|
hfs0headerSHA256 = Arrays.copyOfRange(headerBytes, 320, 352);
|
||||||
|
@ -73,7 +73,7 @@ public class XCIGamecardHeader{
|
||||||
secureModeFlag = getLEint(headerBytes, 384);
|
secureModeFlag = getLEint(headerBytes, 384);
|
||||||
titleKeyFlag = getLEint(headerBytes, 388);
|
titleKeyFlag = getLEint(headerBytes, 388);
|
||||||
keyFlag = getLEint(headerBytes, 392);
|
keyFlag = getLEint(headerBytes, 392);
|
||||||
normAreaEndAddr = Arrays.copyOfRange(headerBytes, 396, 400);
|
normAreaEndAddress = Arrays.copyOfRange(headerBytes, 396, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getRsa2048PKCS1sig() { return rsa2048PKCS1sig; }
|
public byte[] getRsa2048PKCS1sig() { return rsa2048PKCS1sig; }
|
||||||
|
@ -83,67 +83,21 @@ public class XCIGamecardHeader{
|
||||||
public byte getTitleKEKIndexBoth() { return titleKEKIndexBoth; }
|
public byte getTitleKEKIndexBoth() { return titleKEKIndexBoth; }
|
||||||
public byte getTitleKEKIndex() { return titleKEKIndex; }
|
public byte getTitleKEKIndex() { return titleKEKIndex; }
|
||||||
public byte getKEKIndex() { return KEKIndex; }
|
public byte getKEKIndex() { return KEKIndex; }
|
||||||
|
public byte getGcSize() { return gcSize; }
|
||||||
public byte getGcSize() {
|
public byte getGcVersion() { return gcVersion; }
|
||||||
return gcSize;
|
public byte getGcFlags() { return gcFlags; }
|
||||||
}
|
public byte[] getPkgID() { return pkgID; }
|
||||||
public byte getGcVersion() {
|
public long getValDataEndAddr() { return valDataEndAddr; }
|
||||||
return gcVersion;
|
public byte[] getGcInfoIV() { return gcInfoIV; }
|
||||||
}
|
public long getHfs0partOffset() { return hfs0partOffset; }
|
||||||
public byte getGcFlags() {
|
public long getHfs0headerSize() { return hfs0headerSize; }
|
||||||
return gcFlags;
|
public byte[] getHfs0headerSHA256() { return hfs0headerSHA256; }
|
||||||
}
|
public byte[] getHfs0initDataSHA256() { return hfs0initDataSHA256; }
|
||||||
public byte[] getPkgID() {
|
public int getSecureModeFlag() { return secureModeFlag; }
|
||||||
return pkgID;
|
public boolean isSecureModeFlagOk(){ return secureModeFlag == 1; }
|
||||||
}
|
public int getTitleKeyFlag() { return titleKeyFlag; }
|
||||||
public long getValDataEndAddr() {
|
public boolean istitleKeyFlagOk(){ return titleKeyFlag == 2; }
|
||||||
return valDataEndAddr;
|
public int getKeyFlag() { return keyFlag; }
|
||||||
}
|
public boolean iskeyFlagOk(){ return keyFlag == 0; }
|
||||||
public byte[] getGcInfoIV() {
|
public byte[] getNormAreaEndAddr() { return normAreaEndAddress; }
|
||||||
return gcInfoIV;
|
|
||||||
}
|
|
||||||
public long getHfs0partOffset() {
|
|
||||||
return hfs0partOffset;
|
|
||||||
}
|
|
||||||
public long getHfs0headerSize() {
|
|
||||||
return hfs0headerSize;
|
|
||||||
}
|
|
||||||
public byte[] getHfs0headerSHA256() {
|
|
||||||
return hfs0headerSHA256;
|
|
||||||
}
|
|
||||||
public byte[] getHfs0initDataSHA256() {
|
|
||||||
return hfs0initDataSHA256;
|
|
||||||
}
|
|
||||||
public int getSecureModeFlag() {
|
|
||||||
return secureModeFlag;
|
|
||||||
}
|
|
||||||
public boolean isSecureModeFlagOk(){
|
|
||||||
return secureModeFlag == 1;
|
|
||||||
}
|
|
||||||
public int getTitleKeyFlag() {
|
|
||||||
return titleKeyFlag;
|
|
||||||
}
|
|
||||||
public boolean istitleKeyFlagOk(){
|
|
||||||
return titleKeyFlag == 2;
|
|
||||||
}
|
|
||||||
public int getKeyFlag() {
|
|
||||||
return keyFlag;
|
|
||||||
}
|
|
||||||
public boolean iskeyFlagOk(){
|
|
||||||
return keyFlag == 0;
|
|
||||||
}
|
|
||||||
public byte[] getNormAreaEndAddr() {
|
|
||||||
return normAreaEndAddr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] reverseBytes(byte[] bArr){
|
|
||||||
Byte[] objArr = new Byte[bArr.length];
|
|
||||||
for (int i=0;i < bArr.length; i++)
|
|
||||||
objArr[i] = bArr[i];
|
|
||||||
List<Byte> bytesList = Arrays.asList(objArr);
|
|
||||||
Collections.reverse(bytesList);
|
|
||||||
for (int i=0;i < bArr.length; i++)
|
|
||||||
bArr[i] = objArr[i];
|
|
||||||
return bArr;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -27,14 +27,14 @@ public class XCIProvider{
|
||||||
// TODO: Since LOGO partition added, we have to handle it properly. Is it works??
|
// TODO: Since LOGO partition added, we have to handle it properly. Is it works??
|
||||||
|
|
||||||
//private BufferedInputStream xciBIS;
|
//private BufferedInputStream xciBIS;
|
||||||
private XCIGamecardHeader xciGamecardHeader;
|
private final XCIGamecardHeader xciGamecardHeader;
|
||||||
private XCIGamecardInfo xciGamecardInfo;
|
private final XCIGamecardInfo xciGamecardInfo;
|
||||||
private XCIGamecardCert xciGamecardCert;
|
private final XCIGamecardCert xciGamecardCert;
|
||||||
private HFS0Provider hfs0ProviderMain,
|
private final HFS0Provider hfs0ProviderMain;
|
||||||
hfs0ProviderUpdate,
|
private HFS0Provider hfs0ProviderUpdate;
|
||||||
hfs0ProviderNormal,
|
private HFS0Provider hfs0ProviderNormal;
|
||||||
hfs0ProviderSecure,
|
private HFS0Provider hfs0ProviderSecure;
|
||||||
hfs0ProviderLogo;
|
private HFS0Provider hfs0ProviderLogo;
|
||||||
|
|
||||||
public XCIProvider(File file, String XCI_HEADER_KEY) throws Exception{ // TODO: ADD FILE SIZE CHECK !!! Check xciHdrKey
|
public XCIProvider(File file, String XCI_HEADER_KEY) throws Exception{ // TODO: ADD FILE SIZE CHECK !!! Check xciHdrKey
|
||||||
RandomAccessFile raf;
|
RandomAccessFile raf;
|
||||||
|
|
|
@ -22,10 +22,8 @@ import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
|
||||||
// TODO: rework, simplify
|
|
||||||
public class InFileStreamProducer {
|
public class InFileStreamProducer {
|
||||||
private final boolean encrypted;
|
private boolean encrypted;
|
||||||
private final long size;
|
|
||||||
|
|
||||||
private final File file;
|
private final File file;
|
||||||
private final long initialOffset;
|
private final long initialOffset;
|
||||||
|
@ -34,50 +32,35 @@ public class InFileStreamProducer {
|
||||||
private long mediaStartOffset;
|
private long mediaStartOffset;
|
||||||
private long mediaEndOffset;
|
private long mediaEndOffset;
|
||||||
|
|
||||||
|
public InFileStreamProducer(File file){
|
||||||
|
this.file = file;
|
||||||
|
this.initialOffset = 0;
|
||||||
|
this.subOffset = 0;
|
||||||
|
}
|
||||||
|
public InFileStreamProducer(File file, long subOffset){
|
||||||
|
this.file = file;
|
||||||
|
this.initialOffset = 0;
|
||||||
|
this.subOffset = subOffset;
|
||||||
|
}
|
||||||
public InFileStreamProducer(
|
public InFileStreamProducer(
|
||||||
File file,
|
File file,
|
||||||
long size,
|
|
||||||
long initialOffset,
|
long initialOffset,
|
||||||
long subOffset,
|
long subOffset,
|
||||||
AesCtrDecryptSimple decryptor,
|
AesCtrDecryptSimple decryptor,
|
||||||
long mediaStartOffset,
|
long mediaStartOffset,
|
||||||
long mediaEndOffset
|
long mediaEndOffset){
|
||||||
){
|
|
||||||
this.encrypted = (decryptor != null);
|
this.encrypted = (decryptor != null);
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.size = size;
|
|
||||||
this.initialOffset = initialOffset;
|
this.initialOffset = initialOffset;
|
||||||
this.subOffset = subOffset;
|
this.subOffset = subOffset;
|
||||||
this.decryptor = decryptor;
|
this.decryptor = decryptor;
|
||||||
this.mediaStartOffset = mediaStartOffset;
|
this.mediaStartOffset = mediaStartOffset;
|
||||||
this.mediaEndOffset = mediaEndOffset;
|
this.mediaEndOffset = mediaEndOffset;
|
||||||
}
|
}
|
||||||
public InFileStreamProducer(File file, long size, long initialOffset, long subOffset){
|
|
||||||
this.encrypted = false;
|
|
||||||
this.file = file;
|
|
||||||
this.size = size;
|
|
||||||
this.initialOffset = 0;
|
|
||||||
this.subOffset = initialOffset+subOffset;
|
|
||||||
}
|
|
||||||
public InFileStreamProducer(File file, long size){
|
|
||||||
this.encrypted = false;
|
|
||||||
this.file = file;
|
|
||||||
this.size = size;
|
|
||||||
this.initialOffset = 0;
|
|
||||||
this.subOffset = 0;
|
|
||||||
}
|
|
||||||
public InFileStreamProducer(File file, long size, long subOffset){
|
|
||||||
this.encrypted = false;
|
|
||||||
this.file = file;
|
|
||||||
this.size = size;
|
|
||||||
this.initialOffset = 0;
|
|
||||||
this.subOffset = subOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BufferedInputStream produce() throws Exception{
|
public BufferedInputStream produce() throws Exception{
|
||||||
if (encrypted) {
|
if (encrypted)
|
||||||
return produceAesCtr();
|
return produceAesCtr();
|
||||||
}
|
|
||||||
return produceNotEncrypted();
|
return produceNotEncrypted();
|
||||||
}
|
}
|
||||||
private AesCtrBufferedInputStream produceAesCtr() throws Exception{
|
private AesCtrBufferedInputStream produceAesCtr() throws Exception{
|
||||||
|
@ -88,30 +71,34 @@ public class InFileStreamProducer {
|
||||||
mediaStartOffset,
|
mediaStartOffset,
|
||||||
mediaEndOffset,
|
mediaEndOffset,
|
||||||
Files.newInputStream(file.toPath()));
|
Files.newInputStream(file.toPath()));
|
||||||
if (subOffset != stream.skip(subOffset))
|
skipBytesTillBeginning(stream, subOffset);
|
||||||
throw new Exception("Unable to skip offset: " + subOffset);
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
private BufferedInputStream produceNotEncrypted() throws Exception{
|
private BufferedInputStream produceNotEncrypted() throws Exception{
|
||||||
BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(file.toPath()));
|
||||||
if (subOffset != stream.skip(subOffset))
|
skipBytesTillBeginning(stream, subOffset);
|
||||||
throw new Exception("Unable to skip offset: " + subOffset);
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InFileStreamProducer getSuccessor(long subOffset, long size){
|
public InFileStreamProducer getSuccessor(long subOffset){
|
||||||
this.subOffset = subOffset;
|
this.subOffset = subOffset;
|
||||||
return new InFileStreamProducer(file, size, initialOffset, subOffset, decryptor, mediaStartOffset, mediaEndOffset);
|
return new InFileStreamProducer(file, initialOffset, subOffset, decryptor, mediaStartOffset, mediaEndOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEncrypted() {
|
public boolean isEncrypted() {
|
||||||
return encrypted;
|
return encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getSize() {
|
private void skipBytesTillBeginning(BufferedInputStream stream, long size) throws Exception{
|
||||||
return size;
|
long mustSkip = size;
|
||||||
|
long skipped = 0;
|
||||||
|
while (mustSkip > 0){
|
||||||
|
skipped += stream.skip(mustSkip);
|
||||||
|
mustSkip = size - skipped;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public File getFile(){ return file; }
|
||||||
@Override
|
@Override
|
||||||
public String toString(){
|
public String toString(){
|
||||||
return file.getName();
|
return file.getName();
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package libKonogonka.RomFsDecrypted;
|
package libKonogonka.RomFsDecrypted;
|
||||||
|
|
||||||
import libKonogonka.KeyChainHolder;
|
import libKonogonka.KeyChainHolder;
|
||||||
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ public class KeyChainHolderTest {
|
||||||
private static final String xci_header_keyFileLocation = "./FilesForTests/xci_header_key.txt";
|
private static final String xci_header_keyFileLocation = "./FilesForTests/xci_header_key.txt";
|
||||||
private KeyChainHolder keyChainHolder;
|
private KeyChainHolder keyChainHolder;
|
||||||
|
|
||||||
//@Disabled
|
@Disabled
|
||||||
@DisplayName("Key Chain Holder Test")
|
@DisplayName("Key Chain Holder Test")
|
||||||
@Test
|
@Test
|
||||||
void keysChain() throws Exception{
|
void keysChain() throws Exception{
|
||||||
|
|
|
@ -22,7 +22,7 @@ import libKonogonka.KeyChainHolder;
|
||||||
import libKonogonka.RainbowDump;
|
import libKonogonka.RainbowDump;
|
||||||
import libKonogonka.Tools.NCA.NCAProvider;
|
import libKonogonka.Tools.NCA.NCAProvider;
|
||||||
import libKonogonka.Tools.NSO.NSO0Provider;
|
import libKonogonka.Tools.NSO.NSO0Provider;
|
||||||
import libKonogonka.Tools.PFS0.IPFS0Provider;
|
import libKonogonka.Tools.PFS0.PFS0Provider;
|
||||||
import libKonogonka.Tools.PFS0.PFS0subFile;
|
import libKonogonka.Tools.PFS0.PFS0subFile;
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
|
@ -83,7 +83,7 @@ public class NSOTest {
|
||||||
|
|
||||||
void nso0Validation() throws Exception{
|
void nso0Validation() throws Exception{
|
||||||
File nca = new File(ncaFileLocation);
|
File nca = new File(ncaFileLocation);
|
||||||
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getPfs0subFiles();
|
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getHeader().getPfs0subFiles();
|
||||||
|
|
||||||
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
|
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
|
||||||
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
|
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
|
||||||
|
@ -111,7 +111,8 @@ public class NSOTest {
|
||||||
ncaProvider.getSectionBlock0().getSectionCTR(),
|
ncaProvider.getSectionBlock0().getSectionCTR(),
|
||||||
ncaProvider.getTableEntry0().getMediaStartOffset() * 0x200);
|
ncaProvider.getTableEntry0().getMediaStartOffset() * 0x200);
|
||||||
|
|
||||||
IPFS0Provider pfs0Provider = ncaProvider.getNCAContentProvider(0).getPfs0();
|
PFS0Provider pfs0Provider = ncaProvider.getNCAContentProvider(0).getPfs0();
|
||||||
|
pfs0Provider.printDebug();
|
||||||
|
|
||||||
NSO0Provider nso0Provider = new NSO0Provider(pfs0Provider.getStreamProducer(0));
|
NSO0Provider nso0Provider = new NSO0Provider(pfs0Provider.getStreamProducer(0));
|
||||||
nso0Provider.printDebug();
|
nso0Provider.printDebug();
|
||||||
|
|
|
@ -73,7 +73,7 @@ public class NSPpfs0EncryptedTest {
|
||||||
|
|
||||||
pfs0Provider = new PFS0Provider(nspFile);
|
pfs0Provider = new PFS0Provider(nspFile);
|
||||||
|
|
||||||
for (PFS0subFile subFile : pfs0Provider.getPfs0subFiles()) {
|
for (PFS0subFile subFile : pfs0Provider.getHeader().getPfs0subFiles()) {
|
||||||
if (subFile.getName().endsWith("890.nca")) {
|
if (subFile.getName().endsWith("890.nca")) {
|
||||||
System.out.println("File found: "+subFile.getName());
|
System.out.println("File found: "+subFile.getName());
|
||||||
ncaProvider = new NCAProvider(nspFile, finalKeysSet, pfs0Provider.getRawFileDataStart()+subFile.getOffset());
|
ncaProvider = new NCAProvider(nspFile, finalKeysSet, pfs0Provider.getRawFileDataStart()+subFile.getOffset());
|
||||||
|
@ -109,7 +109,7 @@ public class NSPpfs0EncryptedTest {
|
||||||
|
|
||||||
void AesCtrBufferedInputStreamTest() throws Exception {
|
void AesCtrBufferedInputStreamTest() throws Exception {
|
||||||
File nca = new File(nspFileLocation); // TODO:NOTICE
|
File nca = new File(nspFileLocation); // TODO:NOTICE
|
||||||
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getPfs0subFiles();
|
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getHeader().getPfs0subFiles();
|
||||||
|
|
||||||
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
|
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
|
||||||
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
|
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
|
||||||
|
@ -207,7 +207,10 @@ public class NSPpfs0EncryptedTest {
|
||||||
File contentFile = new File(saveToLocation + entry.getName());
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
|
||||||
BufferedOutputStream extractedFileBOS = new BufferedOutputStream(new FileOutputStream(contentFile));
|
BufferedOutputStream extractedFileBOS = new BufferedOutputStream(new FileOutputStream(contentFile));
|
||||||
PipedInputStream pis = ncaProvider.getNCAContentProvider(0).getPfs0().getProviderSubFilePipedInpStream(entry.getName());
|
BufferedInputStream pis = ncaProvider.getNCAContentProvider(0)
|
||||||
|
.getPfs0()
|
||||||
|
.getStreamProducer(entry.getName())
|
||||||
|
.produce();
|
||||||
|
|
||||||
byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576
|
byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576
|
||||||
int readSize;
|
int readSize;
|
||||||
|
|
72
src/test/java/libKonogonka/RomFsDecrypted/PFS0Test.java
Normal file
72
src/test/java/libKonogonka/RomFsDecrypted/PFS0Test.java
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018-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.RomFsDecrypted;
|
||||||
|
|
||||||
|
import libKonogonka.KeyChainHolder;
|
||||||
|
import libKonogonka.RainbowDump;
|
||||||
|
import libKonogonka.TitleKeyChainHolder;
|
||||||
|
import libKonogonka.Tools.NCA.NCAProvider;
|
||||||
|
import libKonogonka.Tools.PFS0.PFS0Provider;
|
||||||
|
import libKonogonka.Tools.PFS0.PFS0subFile;
|
||||||
|
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
||||||
|
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
||||||
|
import org.junit.jupiter.api.Disabled;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class PFS0Test {
|
||||||
|
private static final String keysFileLocation = "./FilesForTests/prod.keys";
|
||||||
|
private static final String titleFileLocation = "./FilesForTests/simple_nsp.title_key";
|
||||||
|
private static final String xci_header_keyFileLocation = "./FilesForTests/xci_header_key.txt";
|
||||||
|
private static final String nspFileLocation = "./FilesForTests/sample.nsp";
|
||||||
|
private static KeyChainHolder keyChainHolder;
|
||||||
|
private static PFS0Provider pfs0Provider;
|
||||||
|
|
||||||
|
@Disabled
|
||||||
|
@DisplayName("NSP validation")
|
||||||
|
@Test
|
||||||
|
void pfs0test() throws Exception {
|
||||||
|
BufferedReader br = new BufferedReader(new FileReader(xci_header_keyFileLocation));
|
||||||
|
String keyValue = br.readLine();
|
||||||
|
br.close();
|
||||||
|
|
||||||
|
if (keyValue == null)
|
||||||
|
throw new Exception("Unable to retrieve xci_header_key");
|
||||||
|
|
||||||
|
keyValue = keyValue.trim();
|
||||||
|
keyChainHolder = new KeyChainHolder(keysFileLocation, keyValue);
|
||||||
|
|
||||||
|
TitleKeyChainHolder titleKeyChainHolder = new TitleKeyChainHolder(titleFileLocation);
|
||||||
|
|
||||||
|
HashMap<String, String> finalKeysSet = keyChainHolder.getRawKeySet();
|
||||||
|
finalKeysSet.putAll(titleKeyChainHolder.getKeySet());
|
||||||
|
|
||||||
|
File nspFile = new File(nspFileLocation);
|
||||||
|
|
||||||
|
pfs0Provider = new PFS0Provider(nspFile);
|
||||||
|
pfs0Provider.printDebug();
|
||||||
|
for (PFS0subFile subFile : pfs0Provider.getHeader().getPfs0subFiles()) {
|
||||||
|
pfs0Provider.exportContent("/tmp/NSP_PFS0_NON-ENCRYPTED_TEST", subFile.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ package libKonogonka.RomFsDecrypted;
|
||||||
import libKonogonka.KeyChainHolder;
|
import libKonogonka.KeyChainHolder;
|
||||||
import libKonogonka.RainbowDump;
|
import libKonogonka.RainbowDump;
|
||||||
import libKonogonka.Tools.NCA.NCAProvider;
|
import libKonogonka.Tools.NCA.NCAProvider;
|
||||||
import libKonogonka.Tools.PFS0.IPFS0Provider;
|
import libKonogonka.Tools.PFS0.PFS0Provider;
|
||||||
import libKonogonka.Tools.PFS0.PFS0subFile;
|
import libKonogonka.Tools.PFS0.PFS0subFile;
|
||||||
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
import libKonogonka.ctraes.AesCtrBufferedInputStream;
|
||||||
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
import libKonogonka.ctraes.AesCtrDecryptSimple;
|
||||||
|
@ -80,7 +80,7 @@ public class Pfs0EncryptedTest {
|
||||||
|
|
||||||
void AesCtrBufferedInputStreamTest() throws Exception {
|
void AesCtrBufferedInputStreamTest() throws Exception {
|
||||||
File nca = new File(ncaFileLocation);
|
File nca = new File(ncaFileLocation);
|
||||||
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getPfs0subFiles();
|
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getHeader().getPfs0subFiles();
|
||||||
|
|
||||||
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
|
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
|
||||||
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
|
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
|
||||||
|
@ -113,7 +113,7 @@ public class Pfs0EncryptedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
IPFS0Provider pfs0Provider = ncaProvider.getNCAContentProvider(0).getPfs0();
|
PFS0Provider pfs0Provider = ncaProvider.getNCAContentProvider(0).getPfs0();
|
||||||
//----------------------------------------------------------------------
|
//----------------------------------------------------------------------
|
||||||
for (PFS0subFile subFile : subfiles) {
|
for (PFS0subFile subFile : subfiles) {
|
||||||
System.out.println("Exporting "+subFile.getName());
|
System.out.println("Exporting "+subFile.getName());
|
||||||
|
@ -181,7 +181,10 @@ public class Pfs0EncryptedTest {
|
||||||
File contentFile = new File(saveToLocation + entry.getName());
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
|
||||||
BufferedOutputStream extractedFileBOS = new BufferedOutputStream(new FileOutputStream(contentFile));
|
BufferedOutputStream extractedFileBOS = new BufferedOutputStream(new FileOutputStream(contentFile));
|
||||||
PipedInputStream pis = ncaProvider.getNCAContentProvider(0).getPfs0().getProviderSubFilePipedInpStream(entry.getName());
|
BufferedInputStream pis = ncaProvider.getNCAContentProvider(0)
|
||||||
|
.getPfs0()
|
||||||
|
.getStreamProducer(entry.getName())
|
||||||
|
.produce();
|
||||||
|
|
||||||
byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576
|
byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576
|
||||||
int readSize;
|
int readSize;
|
||||||
|
|
|
@ -18,9 +18,11 @@
|
||||||
*/
|
*/
|
||||||
package libKonogonka.RomFsDecrypted;
|
package libKonogonka.RomFsDecrypted;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import libKonogonka.Tools.RomFs.FileSystemEntry;
|
||||||
import libKonogonka.Tools.RomFs.RomFsProvider;
|
import libKonogonka.Tools.RomFs.RomFsProvider;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
@ -39,13 +41,14 @@ public class RomFsDecryptedTest {
|
||||||
RomFsProvider provider;
|
RomFsProvider provider;
|
||||||
|
|
||||||
@Disabled
|
@Disabled
|
||||||
@DisplayName("RomFsDecryptedProvider: tests")
|
@DisplayName("RomFsDecryptedProvider: Overall and export")
|
||||||
@Test
|
@Test
|
||||||
void romFsValidation() throws Exception{
|
void romFsValidation() throws Exception{
|
||||||
makeFile();
|
makeFile();
|
||||||
parseLv6offsetFromFileName();
|
parseLv6offsetFromFileName();
|
||||||
makeProvider();
|
makeProvider();
|
||||||
provider.printDebug();
|
provider.printDebug();
|
||||||
|
export();
|
||||||
}
|
}
|
||||||
|
|
||||||
void makeFile(){
|
void makeFile(){
|
||||||
|
@ -58,12 +61,93 @@ public class RomFsDecryptedTest {
|
||||||
provider = new RomFsProvider(decryptedFile, lv6offset);
|
provider = new RomFsProvider(decryptedFile, lv6offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
void export() throws Exception {
|
||||||
void checkFilesWorkers(){
|
System.out.println("lv6offset = "+lv6offset);
|
||||||
assertTrue(fw1 instanceof WorkerFiles);
|
|
||||||
assertTrue(fw2 instanceof WorkerFiles);
|
FileSystemEntry entry = provider.getRootEntry();
|
||||||
assertTrue(fw3 instanceof WorkerFiles);
|
System.out.print(" entry.getFileOffset(): " + entry.getOffset() +
|
||||||
|
"\n entry.getFileSize(): " + entry.getSize() +
|
||||||
|
"\nExport new.......");
|
||||||
|
exportFolderContent(entry, "/tmp/decrypted_brandnew");
|
||||||
|
System.out.println("done");
|
||||||
|
/*----------------------------------------------------------------------
|
||||||
|
System.out.print("Export legacy....");
|
||||||
|
exportFolderContentLegacy(entry, "/tmp/decrypted_legacy");
|
||||||
|
System.out.println("done"); */
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
private void exportFolderContent(FileSystemEntry entry, String saveToLocation) throws Exception{
|
||||||
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
contentFile.mkdirs();
|
||||||
|
String currentDirPath = saveToLocation + entry.getName() + File.separator;
|
||||||
|
for (FileSystemEntry fse : entry.getContent()){
|
||||||
|
if (fse.isDirectory())
|
||||||
|
exportFolderContent(fse, currentDirPath);
|
||||||
|
else
|
||||||
|
exportSingleFile(fse, currentDirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private void exportSingleFile(FileSystemEntry entry, String saveToLocation) throws Exception {
|
||||||
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
try(BufferedOutputStream extractedFileBOS = new BufferedOutputStream(Files.newOutputStream(contentFile.toPath()));
|
||||||
|
BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(decryptedFile.toPath()))) {
|
||||||
|
long skipBytes = entry.getOffset()+
|
||||||
|
//ncaProvider.getTableEntry1().getMediaStartOffset()*0x200+
|
||||||
|
provider.getHeader().getFileDataOffset()+
|
||||||
|
lv6offset;
|
||||||
|
if (skipBytes != stream.skip(skipBytes))
|
||||||
|
throw new Exception("Can't skip");
|
||||||
|
|
||||||
|
int blockSize = 0x200;
|
||||||
|
if (entry.getSize() < 0x200)
|
||||||
|
blockSize = (int) entry.getSize();
|
||||||
|
|
||||||
|
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) >= entry.getSize()) {
|
||||||
|
blockSize = (int) (entry.getSize() - i);
|
||||||
|
if (blockSize == 0)
|
||||||
|
break;
|
||||||
|
block = new byte[blockSize];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
private void exportFolderContentLegacy(FileSystemEntry entry, String saveToLocation) throws Exception{
|
||||||
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
contentFile.mkdirs();
|
||||||
|
String currentDirPath = saveToLocation + entry.getName() + File.separator;
|
||||||
|
for (FileSystemEntry fse : entry.getContent()){
|
||||||
|
if (fse.isDirectory())
|
||||||
|
exportFolderContentLegacy(fse, currentDirPath);
|
||||||
|
else
|
||||||
|
exportSingleFileLegacy(fse, currentDirPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportSingleFileLegacy(FileSystemEntry entry, String saveToLocation) throws Exception {
|
||||||
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
|
||||||
|
BufferedOutputStream extractedFileBOS = new BufferedOutputStream(Files.newOutputStream(contentFile.toPath()));
|
||||||
|
PipedInputStream pis = provider.getContent(entry);
|
||||||
|
|
||||||
|
byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576
|
||||||
|
int readSize;
|
||||||
|
|
||||||
|
while ((readSize = pis.read(readBuf)) > -1) {
|
||||||
|
extractedFileBOS.write(readBuf, 0, readSize);
|
||||||
|
readBuf = new byte[0x200];
|
||||||
|
}
|
||||||
|
|
||||||
|
extractedFileBOS.close();
|
||||||
|
}*/
|
||||||
}
|
}
|
|
@ -117,7 +117,7 @@ public class RomFsEncryptedTest {
|
||||||
|
|
||||||
exportFolderContent(entry, "/tmp/brandnew");
|
exportFolderContent(entry, "/tmp/brandnew");
|
||||||
//----------------------------------------------------------------------
|
//----------------------------------------------------------------------
|
||||||
// exportFolderContentLegacy(entry, "/tmp/legacy");
|
//exportFolderContentLegacy(entry, "/tmp/legacy");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportFolderContent(FileSystemEntry entry, String saveToLocation) throws Exception{
|
private void exportFolderContent(FileSystemEntry entry, String saveToLocation) throws Exception{
|
||||||
|
@ -170,7 +170,7 @@ public class RomFsEncryptedTest {
|
||||||
throw new Exception("Read failure. Block Size: "+blockSize+", actuallyRead: "+actuallyRead);
|
throw new Exception("Read failure. Block Size: "+blockSize+", actuallyRead: "+actuallyRead);
|
||||||
extractedFileBOS.write(block);
|
extractedFileBOS.write(block);
|
||||||
i += blockSize;
|
i += blockSize;
|
||||||
if ((i + blockSize) > entry.getSize()) {
|
if ((i + blockSize) >= entry.getSize()) {
|
||||||
blockSize = (int) (entry.getSize() - i);
|
blockSize = (int) (entry.getSize() - i);
|
||||||
if (blockSize == 0)
|
if (blockSize == 0)
|
||||||
break;
|
break;
|
||||||
|
@ -180,7 +180,7 @@ public class RomFsEncryptedTest {
|
||||||
//---
|
//---
|
||||||
extractedFileBOS.close();
|
extractedFileBOS.close();
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
private void exportFolderContentLegacy(FileSystemEntry entry, String saveToLocation) throws Exception{
|
private void exportFolderContentLegacy(FileSystemEntry entry, String saveToLocation) throws Exception{
|
||||||
File contentFile = new File(saveToLocation + entry.getName());
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
contentFile.mkdirs();
|
contentFile.mkdirs();
|
||||||
|
@ -192,6 +192,7 @@ public class RomFsEncryptedTest {
|
||||||
exportSingleFileLegacy(fse, currentDirPath);
|
exportSingleFileLegacy(fse, currentDirPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportSingleFileLegacy(FileSystemEntry entry, String saveToLocation) throws Exception {
|
private void exportSingleFileLegacy(FileSystemEntry entry, String saveToLocation) throws Exception {
|
||||||
File contentFile = new File(saveToLocation + entry.getName());
|
File contentFile = new File(saveToLocation + entry.getName());
|
||||||
|
|
||||||
|
@ -207,8 +208,7 @@ public class RomFsEncryptedTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
extractedFileBOS.close();
|
extractedFileBOS.close();
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@Disabled
|
@Disabled
|
||||||
@Order(6)
|
@Order(6)
|
||||||
@DisplayName("RomFsEncryptedProvider: PFS test")
|
@DisplayName("RomFsEncryptedProvider: PFS test")
|
||||||
|
|
|
@ -90,6 +90,6 @@ public class XciTest {
|
||||||
NCAProvider ncaProvider = new NCAProvider(xciFile, keyChainHolder.getRawKeySet(),
|
NCAProvider ncaProvider = new NCAProvider(xciFile, keyChainHolder.getRawKeySet(),
|
||||||
hfs0Provider.getRawFileDataStart() +
|
hfs0Provider.getRawFileDataStart() +
|
||||||
hfs0File.getOffset());
|
hfs0File.getOffset());
|
||||||
//ncaProvider.
|
ncaProvider.getSectionBlock0().printDebug();
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue