Misc fixes, start rewriting RomFS parts to make it in line with AesCtrBufferedInputStream
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dmitry Isaenko 2022-09-19 23:04:59 +03:00
parent c878992cb6
commit f058905e2f
20 changed files with 619 additions and 758 deletions

View file

@ -1,12 +1,12 @@
# libKonogonka # libKonogonka
[![Build Status](https://ci.redrise.ru/api/badges/desu/libKonogonka/status.svg)](https://ci.redrise.ru/desu/libKonogonka) ![License](https://img.shields.io/badge/License-GPLv3-blue.svg) [![Build Status](https://ci.redrise.ru/api/badges/desu/libKonogonka/status.svg)](https://ci.redrise.ru/desu/libKonogonka)
Library to work with NS-specific files / filesystem images. Dedicated back end part of [konogonka](https://github.com/developersu/konogonka) ([independent src location](https://git.redrise.ru/desu/konogonka)) Library to work with NS-specific files / filesystem images. Ex-backend of [konogonka](https://github.com/developersu/konogonka) ([independent src location](https://git.redrise.ru/desu/konogonka))
### License ### License
[GNU General Public License v3+](https://git.redrise.ru/desu/libKonogonka/LICENSE) [GNU General Public License v3 or higher](https://git.redrise.ru/desu/libKonogonka/LICENSE)
### Used libraries & resources ### Used libraries & resources
* [Bouncy Castle](https://www.bouncycastle.org/) for Java. * [Bouncy Castle](https://www.bouncycastle.org/) for Java.

View file

@ -36,14 +36,62 @@ public class RainbowDump {
public static void hexDumpUTF8(byte[] byteArray){ public static void hexDumpUTF8(byte[] byteArray){
if (byteArray == null || byteArray.length == 0)
return;
int k = 0;
System.out.printf("%s%08x %s", ANSI_BLUE, 0, ANSI_RESET);
for (int i = 0; i < byteArray.length; i++) {
if (k == 8)
System.out.print(" ");
if (k == 16){
System.out.print(ANSI_GREEN+"| "+ANSI_RESET);
printChars(byteArray, i);
System.out.println();
System.out.printf("%s%08x %s", ANSI_BLUE, i, ANSI_RESET);
k = 0;
}
System.out.printf("%02x ", byteArray[i]);
k++;
}
int paddingSize = 16 - (byteArray.length % 16);
if (paddingSize != 16) {
for (int i = 0; i < paddingSize; i++) {
System.out.print(" ");
}
if (paddingSize > 7) {
System.out.print(" ");
}
}
System.out.print(ANSI_GREEN+"| "+ANSI_RESET);
printChars(byteArray, byteArray.length);
System.out.println();
System.out.print(ANSI_RESET+new String(byteArray, StandardCharsets.UTF_8)+"\n");
}
private static void printChars(byte[] byteArray, int pointer){
for (int j = pointer-16; j < pointer; j++){
if ((byteArray[j] > 21) && (byteArray[j] < 126)) // man ascii
System.out.print((char) byteArray[j]);
else if (byteArray[j] == 0x0a)
System.out.print(""); //""
else if (byteArray[j] == 0x0d)
System.out.print(""); // ""
else
System.out.print(".");
}
}
public static void hexDumpUTF8Legacy(byte[] byteArray){
if (byteArray == null || byteArray.length == 0) if (byteArray == null || byteArray.length == 0)
return; return;
System.out.print(ANSI_BLUE); System.out.print(ANSI_BLUE);
for (int i=0; i < byteArray.length; i++) for (int i=0; i < byteArray.length; i++)
System.out.print(String.format("%02d-", i%100)); System.out.printf("%02d-", i%100);
System.out.println(">"+ANSI_RED+byteArray.length+ANSI_RESET); System.out.println(">"+ANSI_RED+byteArray.length+ANSI_RESET);
for (byte b: byteArray) for (byte b: byteArray)
System.out.print(String.format("%02x ", b)); System.out.printf("%02x ", b);
System.out.println(); System.out.println();
System.out.print(new String(byteArray, StandardCharsets.UTF_8)+"\n"); System.out.print(new String(byteArray, StandardCharsets.UTF_8)+"\n");
} }

View file

@ -22,7 +22,7 @@ import libKonogonka.Tools.NCA.NCASectionTableBlock.NcaFsHeader;
import libKonogonka.Tools.PFS0.IPFS0Provider; import libKonogonka.Tools.PFS0.IPFS0Provider;
import libKonogonka.Tools.PFS0.PFS0Provider; import libKonogonka.Tools.PFS0.PFS0Provider;
import libKonogonka.Tools.RomFs.IRomFsProvider; import libKonogonka.Tools.RomFs.IRomFsProvider;
import libKonogonka.Tools.RomFs.RomFsEncryptedProvider; import libKonogonka.Tools.RomFs.RomFsProvider;
import libKonogonka.ctraes.AesCtrBufferedInputStream; import libKonogonka.ctraes.AesCtrBufferedInputStream;
import libKonogonka.ctraes.AesCtrDecryptSimple; import libKonogonka.ctraes.AesCtrDecryptSimple;
import libKonogonka.exceptions.EmptySectionException; import libKonogonka.exceptions.EmptySectionException;
@ -32,7 +32,6 @@ import org.apache.logging.log4j.Logger;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.LinkedList;
public class NCAContent { public class NCAContent {
private final static Logger log = LogManager.getLogger(NCAContent.class); private final static Logger log = LogManager.getLogger(NCAContent.class);
@ -43,7 +42,6 @@ public class NCAContent {
private final NCAHeaderTableEntry ncaHeaderTableEntry; private final NCAHeaderTableEntry ncaHeaderTableEntry;
private final byte[] decryptedKey; private final byte[] decryptedKey;
private LinkedList<byte[]> Pfs0SHA256hashes;
private IPFS0Provider pfs0; private IPFS0Provider pfs0;
private IRomFsProvider romfs; private IRomFsProvider romfs;
@ -59,8 +57,6 @@ public class NCAContent {
this.ncaFsHeader = ncaFsHeader; this.ncaFsHeader = ncaFsHeader;
this.ncaHeaderTableEntry = ncaHeaderTableEntry; this.ncaHeaderTableEntry = ncaHeaderTableEntry;
this.decryptedKey = decryptedKey; this.decryptedKey = decryptedKey;
System.out.println("NCAContent pfs0offsetPosition: "+ncaOffsetPosition);
Pfs0SHA256hashes = new LinkedList<>();
// If nothing to do // If nothing to do
if (ncaHeaderTableEntry.getMediaEndOffset() == 0) if (ncaHeaderTableEntry.getMediaEndOffset() == 0)
throw new EmptySectionException("Empty section"); throw new EmptySectionException("Empty section");
@ -91,7 +87,6 @@ public class NCAContent {
ncaFsHeader.getSuperBlockPFS0(), ncaFsHeader.getSuperBlockPFS0(),
ncaHeaderTableEntry.getMediaStartOffset(), ncaHeaderTableEntry.getMediaStartOffset(),
ncaHeaderTableEntry.getMediaEndOffset()); ncaHeaderTableEntry.getMediaEndOffset());
Pfs0SHA256hashes = pfs0.getPfs0SHA256hashes();
} }
private void proceedPFS0Encrypted() throws Exception{ private void proceedPFS0Encrypted() throws Exception{
@ -103,16 +98,15 @@ public class NCAContent {
decryptor, decryptor,
ncaHeaderTableEntry.getMediaStartOffset(), ncaHeaderTableEntry.getMediaStartOffset(),
ncaHeaderTableEntry.getMediaEndOffset()); ncaHeaderTableEntry.getMediaEndOffset());
Pfs0SHA256hashes = pfs0.getPfs0SHA256hashes();
} }
private void proceedRomFs() throws Exception{ private void proceedRomFs() throws Exception{
switch (ncaFsHeader.getCryptoType()){ switch (ncaFsHeader.getCryptoType()){
case 0x01: case 0x01:
proceedRomFsNotEncrypted(); // IF NO ENCRYPTION proceedRomFsNotEncrypted();
break; break;
case 0x03: case 0x03:
proceedRomFsEncrypted(); // If encrypted regular [ 0x03 ] proceedRomFsEncrypted();
break; break;
default: default:
throw new Exception("Non-supported 'Crypto type'"); throw new Exception("Non-supported 'Crypto type'");
@ -125,7 +119,7 @@ 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 RomFsEncryptedProvider( this.romfs = new RomFsProvider(
ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(), ncaFsHeader.getSuperBlockIVFC().getLvl6Offset(),
file, file,
ncaOffsetPosition, ncaOffsetPosition,

View file

@ -29,8 +29,6 @@ import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
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.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;

View file

@ -33,8 +33,7 @@ import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedList; import java.util.LinkedList;
import static libKonogonka.Converter.getLEint; import static libKonogonka.Converter.*;
import static libKonogonka.Converter.getLElong;
public class PFS0Provider implements IPFS0Provider{ public class PFS0Provider implements IPFS0Provider{
private final static Logger log = LogManager.getLogger(PFS0Provider.class); private final static Logger log = LogManager.getLogger(PFS0Provider.class);
@ -295,11 +294,68 @@ public class PFS0Provider implements IPFS0Provider{
return true; return true;
} }
//TODO: REMOVE /**
* @deprecated
* */
@Override @Override
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception {return null;} 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 @Override
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception {return null;} 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() {

View file

@ -27,7 +27,7 @@ import org.apache.logging.log4j.Logger;
* This class stores information contained in Level 6 Header of the RomFS image * This class stores information contained in Level 6 Header of the RomFS image
* ------------------------------------ * ------------------------------------
* | Header Length (usually 0x50) | * | Header Length (usually 0x50) |
* | Directory Hash Table Offset | Not used by this library | '<< 32' to get real offset: see implementation * | Directory Hash Table Offset | Not used by this library
* | Directory Hash Table Length | Not used by this library * | Directory Hash Table Length | Not used by this library
* | Directory Metadata Table Offset | * | Directory Metadata Table Offset |
* | Directory Metadata Table Length | * | Directory Metadata Table Length |
@ -42,7 +42,7 @@ public class Level6Header {
private final static Logger log = LogManager.getLogger(Level6Header.class); private final static Logger log = LogManager.getLogger(Level6Header.class);
private final long headerLength; private final long headerLength;
private long directoryHashTableOffset; private final long directoryHashTableOffset;
private final long directoryHashTableLength; private final long directoryHashTableLength;
private final long directoryMetadataTableOffset; private final long directoryMetadataTableOffset;
private final long directoryMetadataTableLength; private final long directoryMetadataTableLength;
@ -61,7 +61,7 @@ public class Level6Header {
throw new Exception("Level 6 Header section is too small"); throw new Exception("Level 6 Header section is too small");
headerLength = getNext(); headerLength = getNext();
directoryHashTableOffset = getNext(); directoryHashTableOffset = getNext();
directoryHashTableOffset <<= 32; //directoryHashTableOffset <<= 32;
directoryHashTableLength = getNext(); directoryHashTableLength = getNext();
directoryMetadataTableOffset = getNext(); directoryMetadataTableOffset = getNext();
directoryMetadataTableLength = getNext(); directoryMetadataTableLength = getNext();
@ -73,7 +73,7 @@ public class Level6Header {
} }
private long getNext(){ private long getNext(){
final long result = Converter.getLEint(headerBytes, _cursor); final long result = Converter.getLElongOfInt(headerBytes, _cursor);
_cursor += 0x8; _cursor += 0x8;
return result; return result;
} }

View file

@ -0,0 +1,160 @@
/*
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.Converter;
import libKonogonka.ctraes.AesCtrBufferedInputStream;
import libKonogonka.ctraes.AesCtrDecryptSimple;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.BufferedInputStream;
import java.io.File;
import java.nio.file.Files;
public class RomFsConstruct {
private final static Logger log = LogManager.getLogger(RomFsConstruct.class);
private Level6Header header;
private FileSystemEntry rootEntry;
private final BufferedInputStream stream;
private int headerSize;
private byte[] directoryMetadataTable;
private byte[] fileMetadataTable;
private final File file;
private long offsetPositionInFile;
private final long level6Offset;
RomFsConstruct(File file,
long level6Offset) throws Exception{
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,
long ncaOffset, // NCA offset position
long level6Offset,
AesCtrDecryptSimple decryptor,
long mediaStartOffset,
long mediaEndOffset
) throws Exception {
if (level6Offset < 0)
throw new Exception("Incorrect Level 6 Offset");
this.file = file;
this.level6Offset = level6Offset;
this.offsetPositionInFile = ncaOffset + (mediaStartOffset * 0x200);
// In 512-blocks
// In 512-blocks
this.stream = new AesCtrBufferedInputStream(
decryptor,
ncaOffset,
mediaStartOffset,
mediaEndOffset,
Files.newInputStream(file.toPath()));
constructEverything();
}
private void constructEverything() throws Exception{
goToStartingPosition();
constructHeader();
directoryMetadataTableLengthCheck();
directoryMetadataTableConstruct();
fileMetadataTableLengthCheck();
fileMetadataTableConstruct();
constructRootFilesystemEntry();
stream.close();
}
private void goToStartingPosition() throws Exception{
skipBytes(offsetPositionInFile + level6Offset);
}
private void constructHeader() throws Exception{
byte[] headerSizeBytes = detectHeaderSize();
byte[] rawDataChunk = new byte[headerSize-0x8];
if (stream.read(rawDataChunk) != headerSize-0x8)
throw new Exception(String.format("Failed to read header (0x%x)", (headerSize-0x8)));
byte[] lv6headerBytes = new byte[headerSize];
System.arraycopy(headerSizeBytes, 0, lv6headerBytes, 0, 0x8);
System.arraycopy(rawDataChunk, 0, lv6headerBytes, 0x8, headerSize-0x8);
this.header = new Level6Header(lv6headerBytes);
}
private byte[] detectHeaderSize() throws Exception{
byte[] lv6HeaderSizeRaw = new byte[0x8];
if (stream.read(lv6HeaderSizeRaw) != 0x8)
throw new Exception("Failed to read header size");
headerSize = Converter.getLEint(lv6HeaderSizeRaw, 0);
return lv6HeaderSizeRaw;
}
private void directoryMetadataTableLengthCheck() throws Exception{
if (header.getDirectoryMetadataTableLength() < 0)
throw new Exception("Not supported: DirectoryMetadataTableLength < 0");
}
private void directoryMetadataTableConstruct() throws Exception{
skipBytes(header.getDirectoryMetadataTableOffset() - headerSize);
directoryMetadataTable = new byte[(int) header.getDirectoryMetadataTableLength()];
if (stream.read(directoryMetadataTable) != (int) header.getDirectoryMetadataTableLength())
throw new Exception("Failed to read "+header.getDirectoryMetadataTableLength());
}
private void fileMetadataTableLengthCheck() throws Exception{
if (header.getFileMetadataTableLength() < 0)
throw new Exception("Not supported: FileMetadataTableLength < 0");
}
private void fileMetadataTableConstruct() throws Exception{
skipBytes(header.getFileMetadataTableOffset() - header.getFileHashTableOffset());
fileMetadataTable = new byte[(int) header.getFileMetadataTableLength()];
if (stream.read(fileMetadataTable) != (int) header.getFileMetadataTableLength())
throw new Exception("Failed to read "+header.getFileMetadataTableLength());
}
private void constructRootFilesystemEntry() throws Exception{
rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable);
}
private void skipBytes(long size) throws Exception{
long mustSkip = size;
long skipped = 0;
while (mustSkip > 0){
skipped += stream.skip(mustSkip);
mustSkip = size - skipped;
}
}
Level6Header getHeader() { return header; }
FileSystemEntry getRootEntry(){ return rootEntry; }
byte[] getDirectoryMetadataTable() { return directoryMetadataTable; }
byte[] getFileMetadataTable() { return fileMetadataTable;}
}

View 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.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'");
}
}
}

View file

@ -1,121 +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.Converter;
import java.io.BufferedInputStream;
import java.io.File;
import java.nio.file.Files;
/**
* Construct header for RomFs and obtain root fileSystemEntry (meta information)
* */
class RomFsDecryptedConstruct {
private Level6Header header;
private FileSystemEntry rootEntry;
private final BufferedInputStream fileBufferedInputStream;
private int headerSize;
private byte[] directoryMetadataTable;
private byte[] fileMetadataTable;
RomFsDecryptedConstruct(File decryptedFsImageFile, long level6offset) throws Exception{
if (level6offset < 0)
throw new Exception("Incorrect Level 6 Offset");
fileBufferedInputStream = new BufferedInputStream(Files.newInputStream(decryptedFsImageFile.toPath()));
fastForwardBySkippingBytes(level6offset);
detectHeaderSize();
constructHeader();
fastForwardBySkippingBytes(header.getDirectoryMetadataTableOffset() - headerSize);
directoryMetadataTableLengthCheck();
directoryMetadataTableConstruct();
fastForwardBySkippingBytes(header.getFileMetadataTableOffset() - header.getFileHashTableOffset());
fileMetadataTableLengthCheck();
fileMetadataTableConstruct();
constructRootFilesystemEntry();
fileBufferedInputStream.close();
}
private void detectHeaderSize() throws Exception{
fileBufferedInputStream.mark(0x10);
byte[] lv6HeaderSizeRaw = new byte[0x8];
if (fileBufferedInputStream.read(lv6HeaderSizeRaw) != 0x8)
throw new Exception("Failed to read header size");
headerSize = Converter.getLEint(lv6HeaderSizeRaw, 0);
fileBufferedInputStream.reset();
}
private void constructHeader() throws Exception{
byte[] rawDataChunk = new byte[headerSize];
if (fileBufferedInputStream.read(rawDataChunk) != headerSize)
throw new Exception(String.format("Failed to read header (0x%x)", headerSize));
this.header = new Level6Header(rawDataChunk);
}
private void directoryMetadataTableLengthCheck() throws Exception{
if (header.getDirectoryMetadataTableLength() < 0)
throw new Exception("Not supported operation.");
}
private void directoryMetadataTableConstruct() throws Exception{
directoryMetadataTable = new byte[(int) header.getDirectoryMetadataTableLength()];
if (fileBufferedInputStream.read(directoryMetadataTable) != (int) header.getDirectoryMetadataTableLength())
throw new Exception("Failed to read "+header.getDirectoryMetadataTableLength());
}
private void fileMetadataTableLengthCheck() throws Exception{
if (header.getFileMetadataTableLength() < 0)
throw new Exception("Not supported operation.");
}
private void fileMetadataTableConstruct() throws Exception{
fileMetadataTable = new byte[(int) header.getFileMetadataTableLength()];
if (fileBufferedInputStream.read(fileMetadataTable) != (int) header.getFileMetadataTableLength())
throw new Exception("Failed to read "+header.getFileMetadataTableLength());
}
private void constructRootFilesystemEntry() throws Exception{
rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable);
//rootEntry.printTreeForDebug();
}
private void fastForwardBySkippingBytes(long size) throws Exception{
long mustSkip = size;
long skipped = 0;
while (mustSkip > 0){
skipped += fileBufferedInputStream.skip(mustSkip);
mustSkip = size - skipped;
}
}
Level6Header getHeader() { return header; }
FileSystemEntry getRootEntry(){ return rootEntry; }
byte[] getDirectoryMetadataTable() { return directoryMetadataTable; }
byte[] getFileMetadataTable() { return fileMetadataTable;}
}

View file

@ -1,93 +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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.PipedOutputStream;
import java.nio.file.Files;
public class RomFsDecryptedContentRetrieve implements Runnable {
private final static Logger log = LogManager.getLogger(RomFsDecryptedContentRetrieve.class);
private final File parentFile;
private final PipedOutputStream streamOut;
private final long internalFileRealPosition;
private final long internalFileSize;
RomFsDecryptedContentRetrieve(File parentFile,
PipedOutputStream streamOut,
long internalFileRealPosition,
long internalFileSize){
this.parentFile = parentFile;
this.streamOut = streamOut;
this.internalFileRealPosition = internalFileRealPosition;
this.internalFileSize = internalFileSize;
}
@Override
public void run() {
log.trace("Executing thread");
try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(parentFile.toPath()))){
fastForwardBySkippingBytes(bis, internalFileRealPosition);
int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576
long readFrom = 0;
byte[] readBuffer;
while (readFrom < internalFileSize) {
if (internalFileSize - readFrom < readPice)
readPice = Math.toIntExact(internalFileSize - readFrom); // it's safe, I guarantee
readBuffer = new byte[readPice];
if (bis.read(readBuffer) != readPice) {
log.error("getContent(): Unable to read requested size from file.");
return;
}
streamOut.write(readBuffer);
readFrom += readPice;
}
} catch (Exception exception) {
log.error("RomFsDecryptedProvider -> getContent(): Unable to provide stream", exception);
}
finally {
closeStreamOut();
}
log.trace("Thread died");
}
private void fastForwardBySkippingBytes(BufferedInputStream bis, long size) throws Exception{
long mustSkip = size;
long skipped = 0;
while (mustSkip > 0){
skipped += bis.skip(mustSkip);
mustSkip = size - skipped;
}
}
private void closeStreamOut(){
try {
streamOut.close();
}
catch (IOException e){
log.error("RomFsDecryptedProvider -> getContent(): Unable to close 'StreamOut'");
}
}
}

View file

@ -1,76 +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 libKonogonka.Tools.RomFs.view.DirectoryMetaTablePlainView;
import libKonogonka.Tools.RomFs.view.FileMetaTablePlainView;
import java.io.*;
public class RomFsDecryptedProvider implements IRomFsProvider{
private final File file;
private final long level6Offset;
private final Level6Header level6Header;
private final FileSystemEntry rootEntry;
// Used only for debug
private final byte[] directoryMetadataTable;
private final byte[] fileMetadataTable;
public RomFsDecryptedProvider(File decryptedFsImageFile, long level6offset) throws Exception{
RomFsDecryptedConstruct construct = new RomFsDecryptedConstruct(decryptedFsImageFile, level6offset);
this.file = decryptedFsImageFile;
this.level6Offset = level6offset;
this.level6Header = construct.getHeader();
this.rootEntry = construct.getRootEntry();
this.directoryMetadataTable = construct.getDirectoryMetadataTable();
this.fileMetadataTable = construct.getFileMetadataTable();
}
@Override
public File getFile() { return file; }
@Override
public long getLevel6Offset() { return level6Offset; }
@Override
public Level6Header getHeader() { return level6Header; }
@Override
public FileSystemEntry getRootEntry() { return rootEntry; }
@Override
public PipedInputStream getContent(FileSystemEntry entry) throws Exception{
if (entry.isDirectory())
throw new Exception("Request of the binary stream for the folder entry is not supported (and doesn't make sense).");
PipedOutputStream streamOut = new PipedOutputStream();
PipedInputStream streamIn = new PipedInputStream(streamOut);
long internalFileRealPosition = level6Offset + level6Header.getFileDataOffset() + entry.getOffset();
long internalFileSize = entry.getSize();
Thread contentRetrievingThread = new Thread(
new RomFsDecryptedContentRetrieve(file, streamOut, internalFileRealPosition, internalFileSize));
contentRetrievingThread.start();
return streamIn;
}
@Override
public void printDebug(){
level6Header.printDebugInfo();
new DirectoryMetaTablePlainView(level6Header.getDirectoryMetadataTableLength(), directoryMetadataTable);
new FileMetaTablePlainView(level6Header.getFileMetadataTableLength(), fileMetadataTable);
rootEntry.printTreeForDebug();
}
}

View file

@ -1,180 +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.AesCtrDecryptSimple;
import java.io.File;
import java.io.RandomAccessFile;
import java.util.Arrays;
public class RomFsEncryptedConstruct {
private final long absoluteOffsetPosition;
private final long level6Offset;
private final RandomAccessFile raf;
private final AesCtrDecryptSimple decryptor;
private Level6Header header;
private byte[] directoryMetadataTable;
private byte[] fileMetadataTable;
private FileSystemEntry rootEntry;
RomFsEncryptedConstruct(File encryptedFsImageFile,
long romFsOffset,
long level6Offset,
AesCtrDecryptSimple decryptor,
long mediaStartOffset
) throws Exception {
if (level6Offset < 0)
throw new Exception("Incorrect Level 6 Offset");
this.raf = new RandomAccessFile(encryptedFsImageFile, "r");
this.level6Offset = level6Offset;
this.absoluteOffsetPosition = romFsOffset + (mediaStartOffset * 0x200);
this.decryptor = decryptor;
raf.seek(absoluteOffsetPosition + level6Offset);
//Go to Level 6 header position
decryptor.skipNext(level6Offset / 0x200);
constructHeader();
directoryMetadataTableLengthCheck();
directoryMetadataTableConstruct();
fileMetadataTableLengthCheck();
fileMetadataTableConstruct();
constructRootFilesystemEntry();
raf.close();
}
private void constructHeader() throws Exception{
// Decrypt data
byte[] encryptedBlock = new byte[0x200];
byte[] decryptedBlock;
if (raf.read(encryptedBlock) == 0x200)
decryptedBlock = decryptor.decryptNext(encryptedBlock);
else
throw new Exception("Failed to read header header (0x200 - block)");
this.header = new Level6Header(decryptedBlock);
}
private void directoryMetadataTableLengthCheck() throws Exception{
if (header.getDirectoryMetadataTableLength() < 0)
throw new Exception("Not supported: DirectoryMetadataTableLength < 0");
}
private void directoryMetadataTableConstruct() throws Exception{
directoryMetadataTable = readMetaTable(header.getDirectoryMetadataTableOffset(),
header.getDirectoryMetadataTableLength());
}
private void fileMetadataTableLengthCheck() throws Exception{
if (header.getFileMetadataTableLength() < 0)
throw new Exception("Not supported: FileMetadataTableLength < 0");
}
private void fileMetadataTableConstruct() throws Exception{
fileMetadataTable = readMetaTable(header.getFileMetadataTableOffset(),
header.getFileMetadataTableLength());
}
private void constructRootFilesystemEntry() throws Exception{
rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable);
}
private byte[] readMetaTable(long metaOffset,
long metaSize) throws Exception{
byte[] encryptedBlock;
byte[] decryptedBlock;
byte[] metadataTable = new byte[(int) metaSize];
//0
decryptor.reset();
long startBlock = metaOffset / 0x200;
decryptor.skipNext(level6Offset / 0x200 + startBlock);
raf.seek(absoluteOffsetPosition + level6Offset + startBlock * 0x200);
//1
long ignoreBytes = metaOffset - startBlock * 0x200;
long currentPosition = 0;
if (ignoreBytes > 0) {
encryptedBlock = new byte[0x200];
if (raf.read(encryptedBlock) == 0x200) {
decryptedBlock = decryptor.decryptNext(encryptedBlock);
// If we have extra-small file that is less than a block and even more
if ((0x200 - ignoreBytes) > metaSize){
metadataTable = Arrays.copyOfRange(decryptedBlock, (int)ignoreBytes, 0x200);
return metadataTable;
}
else {
System.arraycopy(decryptedBlock, (int) ignoreBytes, metadataTable, 0, 0x200 - (int) ignoreBytes);
currentPosition = 0x200 - ignoreBytes;
}
}
else {
throw new Exception("Unable to get 512 bytes from 1st bock for Directory/File Metadata Table");
}
startBlock++;
}
long endBlock = (metaSize + ignoreBytes) / 0x200 + startBlock; // <- pointing to place where any data related to this media-block ends
//2
int extraData = (int) ((endBlock - startBlock)*0x200 - (metaSize + ignoreBytes));
if (extraData < 0)
endBlock--;
//3
while ( startBlock < endBlock ) {
encryptedBlock = new byte[0x200];
if (raf.read(encryptedBlock) == 0x200) {
decryptedBlock = decryptor.decryptNext(encryptedBlock);
System.arraycopy(decryptedBlock, 0, metadataTable, (int) currentPosition, 0x200);
}
else
throw new Exception("Unable to get 512 bytes from block for Directory/File Metadata Table");
startBlock++;
currentPosition += 0x200;
}
//4
if (extraData != 0){ // In case we didn't get what we want
encryptedBlock = new byte[0x200];
if (raf.read(encryptedBlock) == 0x200) {
decryptedBlock = decryptor.decryptNext(encryptedBlock);
System.arraycopy(decryptedBlock, 0, metadataTable, (int) currentPosition, Math.abs(extraData));
}
else
throw new Exception("Unable to get 512 bytes from block for Directory/File Metadata Table");
}
return metadataTable;
}
Level6Header getHeader() { return header; }
FileSystemEntry getRootEntry(){ return rootEntry; }
byte[] getDirectoryMetadataTable() { return directoryMetadataTable; }
byte[] getFileMetadataTable() { return fileMetadataTable;}
}

View file

@ -1,139 +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.AesCtrDecryptSimple;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.PipedOutputStream;
import java.io.RandomAccessFile;
public class RomFsEncryptedContentRetrieve implements Runnable{
private final static Logger log = LogManager.getLogger(RomFsEncryptedContentRetrieve.class);
private final File parentFile;
private final PipedOutputStream streamOut;
private final long absoluteOffsetPosition;
private final AesCtrDecryptSimple decryptor;
private final long internalFileOffset;
private final long internalFileSize;
private final long level6Offset;
private final long headersFileDataOffset;
RomFsEncryptedContentRetrieve(File parentFile,
PipedOutputStream streamOut,
long absoluteOffsetPosition,
AesCtrDecryptSimple decryptor,
long internalFileOffset,
long internalFileSize,
long level6Offset,
long headersFileDataOffset
){
log.fatal("Current implementation works incorrectly");
this.parentFile = parentFile;
this.absoluteOffsetPosition = absoluteOffsetPosition;
this.streamOut = streamOut;
this.decryptor = decryptor;
this.internalFileOffset = internalFileOffset;
this.internalFileSize = internalFileSize;
this.level6Offset = level6Offset;
this.headersFileDataOffset = headersFileDataOffset;
}
@Override
public void run() {
log.trace("Executing thread");
try {
byte[] encryptedBlock;
byte[] decryptedBlock;
RandomAccessFile raf = new RandomAccessFile(parentFile, "r");
//0
long startBlock = (internalFileOffset + headersFileDataOffset) / 0x200;
decryptor.skipNext(level6Offset / 0x200 + startBlock);
// long absoluteOffsetPosition = romFsOffsetPosition + (mediaStartOffset * 0x200); // calculated in constructor
raf.seek(absoluteOffsetPosition + level6Offset + startBlock * 0x200);
//1
long ignoreBytes = (internalFileOffset + headersFileDataOffset) - startBlock * 0x200;
if (ignoreBytes > 0) {
encryptedBlock = new byte[0x200];
if (raf.read(encryptedBlock) == 0x200) {
decryptedBlock = decryptor.decryptNext(encryptedBlock);
// If we have extra-small file that is less than a block and even more
if ((0x200 - ignoreBytes) > internalFileSize){
streamOut.write(decryptedBlock, (int)ignoreBytes, (int) internalFileSize); // safe cast
raf.close();
streamOut.close();
return;
}
else {
streamOut.write(decryptedBlock, (int) ignoreBytes, 0x200 - (int) ignoreBytes);
}
}
else {
throw new Exception("Unable to get 512 bytes from 1st bock");
}
startBlock++;
}
long endBlock = (internalFileSize + ignoreBytes) / 0x200 + startBlock; // <- pointing to place where any data related to this media-block ends
//2
int extraData = (int) ((endBlock - startBlock)*0x200 - (internalFileSize + ignoreBytes));
if (extraData < 0)
endBlock--;
//3
while ( startBlock < endBlock ) {
encryptedBlock = new byte[0x200];
if (raf.read(encryptedBlock) == 0x200) {
decryptedBlock = decryptor.decryptNext(encryptedBlock);
streamOut.write(decryptedBlock);
}
else
throw new Exception("Unable to get 512 bytes from block");
startBlock++;
}
//4
if (extraData != 0){ // In case we didn't get what we want
encryptedBlock = new byte[0x200];
if (raf.read(encryptedBlock) == 0x200) {
decryptedBlock = decryptor.decryptNext(encryptedBlock);
streamOut.write(decryptedBlock, 0, Math.abs(extraData));
}
else
throw new Exception("Unable to get 512 bytes from block");
}
raf.close();
streamOut.close();
} catch (Exception exception) {
log.error("Unable to provide stream", exception);
}
log.trace("Thread died");
}
}

View file

@ -16,7 +16,6 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with libKonogonka. If not, see <https://www.gnu.org/licenses/>. * along with libKonogonka. If not, see <https://www.gnu.org/licenses/>.
*/ */
package libKonogonka.Tools.RomFs; package libKonogonka.Tools.RomFs;
import libKonogonka.Tools.RomFs.view.DirectoryMetaTablePlainView; import libKonogonka.Tools.RomFs.view.DirectoryMetaTablePlainView;
@ -27,62 +26,70 @@ import java.io.File;
import java.io.PipedInputStream; import java.io.PipedInputStream;
import java.io.PipedOutputStream; import java.io.PipedOutputStream;
public class RomFsEncryptedProvider implements IRomFsProvider{ public class RomFsProvider implements IRomFsProvider{
private final File file; private final File file;
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 final byte[] key; // Used @ createDecryptor only private long ncaOffsetPosition;
private final byte[] sectionCTR; // Used @ createDecryptor only private byte[] key; // Used @ createDecryptor only
private final long mediaStartOffset; // Used @ createDecryptor only private byte[] sectionCTR; // Used @ createDecryptor only
private final long absoluteOffsetPosition; private long mediaStartOffset; // Used @ createDecryptor only
private long mediaEndOffset;
//private long mediaEndOffset; // We know this, but actually never use
// Used only for debug // Used only for debug
private final byte[] directoryMetadataTable; private final byte[] directoryMetadataTable;
private final byte[] fileMetadataTable; private final byte[] fileMetadataTable;
public RomFsEncryptedProvider(long level6Offset, private final boolean encryptedAesCtr;
File encryptedFsImageFile,
long romFsOffsetPosition, public RomFsProvider(File decryptedFsImageFile, long level6offset) throws Exception{
byte[] key, RomFsConstruct construct = new RomFsConstruct(decryptedFsImageFile, level6offset);
byte[] sectionCTR, this.file = decryptedFsImageFile;
long mediaStartOffset this.level6Offset = level6offset;
) throws Exception{ this.level6Header = construct.getHeader();
this(level6Offset, encryptedFsImageFile, romFsOffsetPosition, key, sectionCTR, mediaStartOffset, -1); this.rootEntry = construct.getRootEntry();
this.directoryMetadataTable = construct.getDirectoryMetadataTable();
this.fileMetadataTable = construct.getFileMetadataTable();
this.encryptedAesCtr = false;
} }
public RomFsEncryptedProvider(long level6Offset, public RomFsProvider(long level6Offset,
File encryptedFsImageFile, File encryptedFsImageFile,
long romFsOffsetPosition, long ncaOffsetPosition,
byte[] key, byte[] key,
byte[] sectionCTR, byte[] sectionCTR,
long mediaStartOffset, long mediaStartOffset,
long mediaEndOffset long mediaEndOffset
) throws Exception{ ) throws Exception{
this.key = key; this.key = key;
this.sectionCTR = sectionCTR; this.sectionCTR = sectionCTR;
this.mediaStartOffset = mediaStartOffset; this.mediaStartOffset = mediaStartOffset;
this.mediaEndOffset = mediaEndOffset;
this.ncaOffsetPosition = ncaOffsetPosition;
RomFsEncryptedConstruct construct = new RomFsEncryptedConstruct(encryptedFsImageFile, RomFsConstruct construct = new RomFsConstruct(encryptedFsImageFile,
romFsOffsetPosition, ncaOffsetPosition,
level6Offset, level6Offset,
createDecryptor(), createDecryptor(),
mediaStartOffset); mediaStartOffset,
mediaEndOffset);
this.file = encryptedFsImageFile; 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.absoluteOffsetPosition = romFsOffsetPosition + (mediaStartOffset * 0x200);
this.directoryMetadataTable = construct.getDirectoryMetadataTable(); this.directoryMetadataTable = construct.getDirectoryMetadataTable();
this.fileMetadataTable = construct.getFileMetadataTable(); this.fileMetadataTable = construct.getFileMetadataTable();
this.encryptedAesCtr = true;
} }
private AesCtrDecryptSimple createDecryptor() throws Exception{ private AesCtrDecryptSimple createDecryptor() throws Exception{
return new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200); return new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200);
} }
@Override @Override
public File getFile() { return file; } public File getFile() { return file; }
@Override @Override
@ -92,7 +99,12 @@ public class RomFsEncryptedProvider implements IRomFsProvider{
@Override @Override
public FileSystemEntry getRootEntry() { return rootEntry; } public FileSystemEntry getRootEntry() { return rootEntry; }
@Override @Override
public PipedInputStream getContent(FileSystemEntry entry) throws Exception{ public PipedInputStream getContent(FileSystemEntry entry) throws Exception {
if (encryptedAesCtr)
return getContentAesCtrEncrypted(entry);
return getContentNonEncrypted(entry);
}
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)."); throw new Exception("Request of the binary stream for the folder entry is not supported (and doesn't make sense).");
@ -101,19 +113,38 @@ public class RomFsEncryptedProvider implements IRomFsProvider{
long internalFileOffset = entry.getOffset(); long internalFileOffset = entry.getOffset();
long internalFileSize = entry.getSize(); long internalFileSize = entry.getSize();
Thread contentRetrievingThread = new Thread(new RomFsEncryptedContentRetrieve( Thread contentRetrievingThread = new Thread(new RomFsContentRetrieve(
file, file,
streamOut, streamOut,
absoluteOffsetPosition,
createDecryptor(), createDecryptor(),
internalFileOffset, internalFileOffset,
internalFileSize, internalFileSize,
level6Header.getFileDataOffset(),
level6Offset, level6Offset,
level6Header.getFileDataOffset() ncaOffsetPosition,
)); mediaStartOffset,
mediaEndOffset));
contentRetrievingThread.start(); contentRetrievingThread.start();
return streamIn; return streamIn;
} }
public PipedInputStream getContentNonEncrypted(FileSystemEntry entry) throws Exception{
if (entry.isDirectory())
throw new Exception("Request of the binary stream for the folder entry is not supported (and doesn't make sense).");
PipedOutputStream streamOut = new PipedOutputStream();
PipedInputStream streamIn = new PipedInputStream(streamOut);
long internalFileRealPosition = level6Offset + level6Header.getFileDataOffset() + entry.getOffset();
long internalFileSize = entry.getSize();
Thread contentRetrievingThread = new Thread(new RomFsContentRetrieve(
file,
streamOut,
internalFileRealPosition,
internalFileSize));
contentRetrievingThread.start();
return streamIn;
}
@Override @Override
public void printDebug(){ public void printDebug(){
level6Header.printDebugInfo(); level6Header.printDebugInfo();

View file

@ -22,6 +22,7 @@ import libKonogonka.Tools.ISuperProvider;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
import static libKonogonka.Converter.*; import static libKonogonka.Converter.*;
@ -30,59 +31,54 @@ import static libKonogonka.Converter.*;
* HFS0 * HFS0
* */ * */
public class HFS0Provider implements ISuperProvider { public class HFS0Provider implements ISuperProvider {
private final String magic;
private final int filesCount;
private final byte[] padding;
private final int stringTableSize;
private final long rawFileDataStart;
private boolean magicHFS0; private final HFS0File[] hfs0Files;
private int filesCnt;
private boolean paddingHfs0;
private int stringTableSize;
private long rawFileDataStart;
private HFS0File[] hfs0Files; private final File file;
private File file;
HFS0Provider(long hfsOffsetPosition, RandomAccessFile raf, File file) throws Exception{ HFS0Provider(long hfsOffsetPosition, RandomAccessFile raf, File file) throws Exception{
this.file = file; // Will be used @ getHfs0FilePipedInpStream. It's a bad implementation. this.file = file; // Will be used @ getHfs0FilePipedInpStream. It's a bad implementation.
byte[] hfs0bytes = new byte[16]; byte[] hfs0bytes = new byte[16];
try{
raf.seek(hfsOffsetPosition); raf.seek(hfsOffsetPosition);
if (raf.read(hfs0bytes) != 16){ if (raf.read(hfs0bytes) != 16){
throw new Exception("Read HFS0 structure failure. Can't read first 16 bytes on requested offset."); throw new Exception("Read HFS0 structure failure. Can't read first 16 bytes on requested offset.");
}
} }
catch (IOException ioe){
throw new Exception("Read HFS0 structure failure. Can't read first 16 bytes on requested offset: "+ioe.getMessage());
}
magicHFS0 = Arrays.equals(Arrays.copyOfRange(hfs0bytes, 0, 4),new byte[]{0x48, 0x46, 0x53, 0x30});
filesCnt = getLEint(hfs0bytes, 0x4);
stringTableSize = getLEint(hfs0bytes, 8);
paddingHfs0 = Arrays.equals(Arrays.copyOfRange(hfs0bytes, 12, 16),new byte[4]);
hfs0Files = new HFS0File[filesCnt]; this.magic = new String(hfs0bytes, 0x0, 0x4, StandardCharsets.US_ASCII);
this.filesCount = getLEint(hfs0bytes, 0x4);
this.stringTableSize = getLEint(hfs0bytes, 8);
this.padding = Arrays.copyOfRange(hfs0bytes, 12, 16);
// TODO: IF NOT EMPTY TABLE: hfs0Files = new HFS0File[filesCount];
long[] offsetHfs0files = new long[filesCnt]; // TODO: IF NOT EMPTY TABLE: add validation
long[] sizeHfs0files = new long[filesCnt];
int[] hashedRegionSizeHfs0Files = new int[filesCnt]; long[] offsetSubFile = new long[filesCount];
boolean[] paddingHfs0Files = new boolean[filesCnt]; long[] sizeSubFile = new long[filesCount];
byte[][] SHA256HashHfs0Files = new byte[filesCnt][]; int[] hashedRegionSubFile = new int[filesCount];
int[] strTableOffsets = new int[filesCnt]; boolean[] paddingSubFile = new boolean[filesCount];
byte[][] SHA256HashSubFile = new byte[filesCount][];
int[] stringTableOffsetSubFile = new int[filesCount];
try { try {
// Populate meta information regarding each file inside (?) HFS0 // Populate meta information regarding each file inside (?) HFS0
byte[] metaInfoBytes = new byte[64]; byte[] metaInfoBytes = new byte[64];
for (int i=0; i < filesCnt; i++){ for (int i = 0; i < filesCount; i++){
if (raf.read(metaInfoBytes) != 64) { if (raf.read(metaInfoBytes) != 64)
throw new Exception("Read HFS0 File Entry Table failure for file # "+i); throw new Exception("Read HFS0 File Entry Table failure for file # "+i);
} offsetSubFile[i] = getLElong(metaInfoBytes, 0);
offsetHfs0files[i] = getLElong(metaInfoBytes, 0); sizeSubFile[i] = getLElong(metaInfoBytes, 8);
sizeHfs0files[i] = getLElong(metaInfoBytes, 8); hashedRegionSubFile[i] = getLEint(metaInfoBytes, 20);
hashedRegionSizeHfs0Files[i] = getLEint(metaInfoBytes, 20); paddingSubFile[i] = Arrays.equals(Arrays.copyOfRange(metaInfoBytes, 24, 32), new byte[8]);
paddingHfs0Files[i] = Arrays.equals(Arrays.copyOfRange(metaInfoBytes, 24, 32), new byte[8]); SHA256HashSubFile[i] = Arrays.copyOfRange(metaInfoBytes, 32, 64);
SHA256HashHfs0Files[i] = Arrays.copyOfRange(metaInfoBytes, 32, 64);
strTableOffsets[i] = getLEint(metaInfoBytes, 16); stringTableOffsetSubFile[i] = getLEint(metaInfoBytes, 16);
} }
// Define location of actual data for this HFS0 // Define location of actual data for this HFS0
rawFileDataStart = raf.getFilePointer()+stringTableSize; rawFileDataStart = raf.getFilePointer()+stringTableSize;
@ -92,24 +88,24 @@ public class HFS0Provider implements ISuperProvider {
if (raf.read(stringTbl) != stringTableSize){ if (raf.read(stringTbl) != stringTableSize){
throw new Exception("Read HFS0 String table failure. Can't read requested string table size ("+stringTableSize+")"); throw new Exception("Read HFS0 String table failure. Can't read requested string table size ("+stringTableSize+")");
} }
String[] namesHfs0files = new String[filesCnt]; String[] namesSubFile = new String[filesCount];
// Parse string table // Parse string table
for (int i=0; i < filesCnt; i++){ for (int i = 0; i < filesCount; i++){
int j = 0; int j = 0;
while (stringTbl[strTableOffsets[i]+j] != (byte)0x00) while (stringTbl[stringTableOffsetSubFile[i]+j] != (byte)0x00)
j++; j++;
namesHfs0files[i] = new String(stringTbl, strTableOffsets[i], j, StandardCharsets.UTF_8); namesSubFile[i] = new String(stringTbl, stringTableOffsetSubFile[i], j, StandardCharsets.UTF_8);
} }
//---------------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------------
// Set files // Set files
for (int i=0; i < filesCnt; i++){ for (int i = 0; i < filesCount; i++){
hfs0Files[i] = new HFS0File( hfs0Files[i] = new HFS0File(
namesHfs0files[i], namesSubFile[i],
offsetHfs0files[i], offsetSubFile[i],
sizeHfs0files[i], sizeSubFile[i],
hashedRegionSizeHfs0Files[i], hashedRegionSubFile[i],
paddingHfs0Files[i], paddingSubFile[i],
SHA256HashHfs0Files[i] SHA256HashSubFile[i]
); );
} }
} }
@ -118,15 +114,18 @@ public class HFS0Provider implements ISuperProvider {
} }
} }
public boolean isMagicHFS0() { return magicHFS0; } public String getMagic() { return magic; }
public int getFilesCnt() { return filesCnt; } public int getFilesCount() { return filesCount; }
public boolean isPaddingHfs0() { return paddingHfs0; } public byte[] getPadding() { return padding; }
public int getStringTableSize() { return stringTableSize; } public int getStringTableSize() { return stringTableSize; }
@Override @Override
public long getRawFileDataStart() { return rawFileDataStart; } public long getRawFileDataStart() { return rawFileDataStart; }
public HFS0File[] getHfs0Files() { return hfs0Files; } public HFS0File[] getHfs0Files() { return hfs0Files; }
@Override @Override
public File getFile(){ return file; } public File getFile(){ return file; }
/**
* @deprecated
* */
@Override @Override
public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception{ public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception{
PipedOutputStream streamOut = new PipedOutputStream(); PipedOutputStream streamOut = new PipedOutputStream();
@ -140,13 +139,11 @@ public class HFS0Provider implements ISuperProvider {
System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Executing thread"); System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Executing thread");
try{ try{
long subFileRealPosition = rawFileDataStart + hfs0Files[subFileNumber].getOffset(); long subFileRealPosition = rawFileDataStart + hfs0Files[subFileNumber].getOffset();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(file.toPath()));
if (bis.skip(subFileRealPosition) != subFileRealPosition) { if (bis.skip(subFileRealPosition) != subFileRealPosition)
System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to skip requested offset"); throw new Exception("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to skip requested offset");
return;
}
int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576 int readPice = 0x800000; // 8mb NOTE: consider switching to 1mb 1048576
long readFrom = 0; long readFrom = 0;
long realFileSize = hfs0Files[subFileNumber].getSize(); long realFileSize = hfs0Files[subFileNumber].getSize();
@ -157,17 +154,16 @@ public class HFS0Provider implements ISuperProvider {
if (realFileSize - readFrom < readPice) if (realFileSize - readFrom < readPice)
readPice = Math.toIntExact(realFileSize - readFrom); // it's safe, I guarantee readPice = Math.toIntExact(realFileSize - readFrom); // it's safe, I guarantee
readBuf = new byte[readPice]; readBuf = new byte[readPice];
if (bis.read(readBuf) != readPice) { if (bis.read(readBuf) != readPice)
System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to read requested size from file."); throw new Exception("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to read requested size from file.");
return;
}
streamOut.write(readBuf, 0, readPice); streamOut.write(readBuf, 0, readPice);
readFrom += readPice; readFrom += readPice;
} }
bis.close(); bis.close();
streamOut.close(); streamOut.close();
} }
catch (IOException ioe){ catch (Exception ioe){
System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to provide stream"); System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to provide stream");
ioe.printStackTrace(); ioe.printStackTrace();
} }
@ -176,20 +172,17 @@ public class HFS0Provider implements ISuperProvider {
workerThread.start(); workerThread.start();
return streamIn; return streamIn;
} }
//TODO
@Override @Override
public boolean exportContent(String saveToLocation, String subFileName) throws Exception { public boolean exportContent(String saveToLocation, String subFileName) throws Exception {
return false; throw new Exception("Not implemented yet");
} }
//TODO
@Override @Override
public boolean exportContent(String saveToLocation, int subFileNumber) throws Exception { public boolean exportContent(String saveToLocation, int subFileNumber) throws Exception {
return false; throw new Exception("Not implemented yet");
} }
/**
* Sugar
* */
@Override @Override
public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception { public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception {
for (int i = 0; i < hfs0Files.length; i++){ for (int i = 0; i < hfs0Files.length; i++){

View file

@ -76,7 +76,7 @@ public class XCIProvider{
xciGamecardCert = new XCIGamecardCert(gamecardCertBytes); xciGamecardCert = new XCIGamecardCert(gamecardCertBytes);
hfs0ProviderMain = new HFS0Provider(0xf000, raf, file); hfs0ProviderMain = new HFS0Provider(0xf000, raf, file);
if (hfs0ProviderMain.getFilesCnt() < 3){ if (hfs0ProviderMain.getFilesCount() < 3){
raf.close(); raf.close();
throw new Exception("XCI Can't read Gamecard certificate bytes."); throw new Exception("XCI Can't read Gamecard certificate bytes.");
} }
@ -106,7 +106,7 @@ public class XCIProvider{
throw new Exception("XCI Failed file analyze for ["+file.getName()+"]\n "+ioe.getMessage()); throw new Exception("XCI Failed file analyze for ["+file.getName()+"]\n "+ioe.getMessage());
} }
} }
/* Getters */ /* API */
public XCIGamecardHeader getGCHeader(){ return this.xciGamecardHeader; } public XCIGamecardHeader getGCHeader(){ return this.xciGamecardHeader; }
public XCIGamecardInfo getGCInfo(){ return this.xciGamecardInfo; } public XCIGamecardInfo getGCInfo(){ return this.xciGamecardInfo; }
public XCIGamecardCert getGCCert(){ return this.xciGamecardCert; } public XCIGamecardCert getGCCert(){ return this.xciGamecardCert; }

View file

@ -172,23 +172,14 @@ public class AesCtrBufferedInputStream extends BufferedInputStream {
long leftovers = realCountOfBytesToSkip % 0x200; // most likely will be 0; TODO: a lot of tests long leftovers = realCountOfBytesToSkip % 0x200; // most likely will be 0; TODO: a lot of tests
long bytesToSkipTillRequiredBlock = realCountOfBytesToSkip - leftovers; long bytesToSkipTillRequiredBlock = realCountOfBytesToSkip - leftovers;
long skipped = super.skip(bytesToSkipTillRequiredBlock); skipLoop(bytesToSkipTillRequiredBlock);
if (bytesToSkipTillRequiredBlock != skipped)
throw new IOException("Can't skip bytes. To skip: " +
bytesToSkipTillRequiredBlock +
".\nActually skipped: " + skipped +
".\nLeftovers inside encrypted section: " + leftovers);
fillDecryptedCache(); fillDecryptedCache();
pseudoPos += n; pseudoPos += n;
pointerInsideDecryptedSection = (int) leftovers; pointerInsideDecryptedSection = (int) leftovers;
return n; return n;
} }
log.trace("4. Pointer Inside + End Position Outside Encrypted Section ("+pseudoPos+"-"+(pseudoPos+n)+")"); log.trace("4. Pointer Inside + End Position Outside Encrypted Section ("+pseudoPos+"-"+(pseudoPos+n)+")");
long skipped = super.skip(realCountOfBytesToSkip); skipLoop(realCountOfBytesToSkip);
if (realCountOfBytesToSkip != skipped)
throw new IOException("Can't skip bytes. To skip: " +
realCountOfBytesToSkip +
".\nActually skipped: " + skipped);
pseudoPos += n; pseudoPos += n;
pointerInsideDecryptedSection = 0; pointerInsideDecryptedSection = 0;
return n; return n;
@ -217,12 +208,20 @@ public class AesCtrBufferedInputStream extends BufferedInputStream {
return n; return n;
} }
log.trace("6. Not encrypted ("+pseudoPos+"-"+(pseudoPos+n)+")"); log.trace("6. Not encrypted ("+pseudoPos+"-"+(pseudoPos+n)+")");
long skipped = super.skip(n); skipLoop(n);
pseudoPos += n; pseudoPos += n;
pointerInsideDecryptedSection = 0; pointerInsideDecryptedSection = 0;
return skipped; return n;
}
private void skipLoop(long size) throws IOException{
long mustSkip = size;
long skipped = 0;
while (mustSkip > 0){
skipped += super.skip(mustSkip);
mustSkip = size - skipped;
log.trace("Skip loop: skipped: "+skipped+"\tmustSkip "+mustSkip);
}
} }
@Override @Override
public synchronized int read() throws IOException { public synchronized int read() throws IOException {
byte[] b = new byte[1]; byte[] b = new byte[1];

View file

@ -21,7 +21,7 @@ package libKonogonka.RomFsDecrypted;
import java.io.File; import java.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import libKonogonka.Tools.RomFs.RomFsDecryptedProvider; 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;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -36,7 +36,7 @@ public class RomFsDecryptedTest {
private static final String decryptedFileAbsolutePath = "./FilesForTests/NCAContent_0 [lv6 147456].bin"; private static final String decryptedFileAbsolutePath = "./FilesForTests/NCAContent_0 [lv6 147456].bin";
private File decryptedFile; private File decryptedFile;
long lv6offset; long lv6offset;
RomFsDecryptedProvider provider; RomFsProvider provider;
@Disabled @Disabled
@DisplayName("RomFsDecryptedProvider: tests") @DisplayName("RomFsDecryptedProvider: tests")
@ -55,7 +55,7 @@ public class RomFsDecryptedTest {
lv6offset = Long.parseLong(decryptedFile.getName().replaceAll("(^.*lv6\\s)|(]\\.bin)", "")); lv6offset = Long.parseLong(decryptedFile.getName().replaceAll("(^.*lv6\\s)|(]\\.bin)", ""));
} }
void makeProvider() throws Exception{ void makeProvider() throws Exception{
provider = new RomFsDecryptedProvider(decryptedFile, lv6offset); provider = new RomFsProvider(decryptedFile, lv6offset);
} }
/* /*

View file

@ -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{
@ -195,7 +195,7 @@ public class RomFsEncryptedTest {
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());
BufferedOutputStream extractedFileBOS = new BufferedOutputStream(new FileOutputStream(contentFile)); BufferedOutputStream extractedFileBOS = new BufferedOutputStream(Files.newOutputStream(contentFile.toPath()));
PipedInputStream pis = ncaProvider.getNCAContentProvider(1).getRomfs().getContent(entry); PipedInputStream pis = ncaProvider.getNCAContentProvider(1).getRomfs().getContent(entry);
byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576 byte[] readBuf = new byte[0x200]; // 8mb NOTE: consider switching to 1mb 1048576

View file

@ -0,0 +1,66 @@
/*
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.Tools.XCI.XCIProvider;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
// log.fatal("Configuration File Defined To Be :: "+System.getProperty("log4j.configurationFile"));
public class XciTest {
private static final String xci_header_keyFileLocation = "./FilesForTests/xci_header_key.txt";
private static final String decryptedFileAbsolutePath = "./FilesForTests/sample.xci";
private File xciFile;
XCIProvider provider;
String xci_header_key;
@Disabled
@DisplayName("RomFsDecryptedProvider: tests")
@Test
void romFsValidation() throws Exception{
makeFile();
getXciHeaderKey();
makeProvider();
}
void getXciHeaderKey() throws Exception{
BufferedReader br = new BufferedReader(new FileReader(xci_header_keyFileLocation));
xci_header_key = br.readLine();
br.close();
if (xci_header_key == null)
throw new Exception("Unable to retrieve xci_header_key");
xci_header_key = xci_header_key.trim();
}
void makeFile(){
xciFile = new File(decryptedFileAbsolutePath);
}
void makeProvider() throws Exception{
provider = new XCIProvider(xciFile, xci_header_key);
}
}