1. Start moving to 'InFileStreamProducer' entity to simplify work with [inside-NCA]-[in-pfs0/romfs]-[sub-files] that could be located inside encrypted or not encrypted sections. This approach will allow us to map filesystem to/as 'enough-informed' objects that could perform actions on content without pre-extraction. For example decompress NSO0 'ELF' sections from the inside PFS0 that is located inside NCA3 that is located inside XCI/NSP.

2. Add NSO0 support. Add NSO0 decompressor/extractor
3. Add logo draft sketch, update dependencies, add LZ4 libs deps, add final identifier in some places
This commit is contained in:
Dmitry Isaenko 2022-12-09 05:37:31 +03:00
parent 51398a6ea9
commit 610aa5dd96
15 changed files with 894 additions and 41 deletions

View file

@ -1,4 +1,4 @@
# libKonogonka
<img src="misc/logo.svg" title="" alt="libKonogonka" data-align="center">
![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)
@ -13,10 +13,13 @@ You can get this application from independent source location: [https://git.redr
[GNU General Public License v3 or higher](https://git.redrise.ru/desu/libKonogonka/LICENSE)
### Used libraries & resources
* [Bouncy Castle](https://www.bouncycastle.org/) for Java.
* [Java-XTS-AES](https://github.com/horrorho/Java-XTS-AES) by horrorho with minimal changes.
* [lz4-java](https://github.com/lz4/lz4-java)
#### Thanks
* Switch brew wiki
* Original ScriesM software
* roothorick, [shchmue](https://github.com/shchmue/), He, other Team AtlasNX discord members for their advices, notes and examples!

17
pom.xml
View file

@ -15,7 +15,7 @@
<inceptionYear>2022</inceptionYear>
<organization>
<name>Dmitry Isaenko</name>
<url>https://developersu.blogspot.com/</url>
<url>https://redrise.ru/</url>
</organization>
<licenses>
@ -67,28 +67,33 @@
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.18.0</version>
<version>2.19.0</version>
<scope>compile</scope>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.2</version>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.5.2</version>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.lz4</groupId>
<artifactId>lz4-pure-java</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}-${maven.build.timestamp}</finalName>
@ -111,7 +116,7 @@
</execution>
</executions>
</plugin>
<!-- UNCOMMENT WHEN LEARN HOW TO MAKE NORMAL DOCS
<!-- UNCOMMENT ONCE LEARN HOW TO MAKE NORMAL DOCS
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>

View file

@ -56,6 +56,10 @@ public class Converter {
return sb.toString();
}
public static String intToBinaryString(int value){
return String.format("%32s", Integer.toBinaryString( value )).replace(' ', '0')+" | "+value;
}
public static String longToOctString(long value){
return String.format("%64s", Long.toBinaryString( value )).replace(' ', '0');
}

View file

@ -41,7 +41,7 @@ public class RainbowDump {
private static StringBuilder stringBuilder;
public static void hexDumpUTF8(byte[] byteArray){
stringBuilder = new StringBuilder();
stringBuilder = new StringBuilder(" -- RainbowDump --\n");
if (byteArray == null || byteArray.length == 0)
return;
@ -106,11 +106,11 @@ public class RainbowDump {
System.out.print(new String(byteArray, StandardCharsets.UTF_8)+"\n");
}
public static void octDumpInt(int value){
System.out.println(String.format("%32s", Integer.toBinaryString( value )).replace(' ', '0')+" | "+value);
public static void binDumpInt(int value){
log.debug(Converter.intToBinaryString(value));
}
public static void octDumpLong(long value){
public static void binDumpLong(long value){
System.out.println(String.format("%64s", Long.toBinaryString( value )).replace(' ', '0')+" | "+value);
}

View file

@ -27,14 +27,14 @@ import java.util.Arrays;
* */
public class FSAccessHeaderProvider {
private byte version;
private byte[] padding;
private long permissionsBitmask;
private int dataSize;
private int contentOwnIdSectionSize;
private int dataNownerSizes;
private int saveDataOwnSectionSize;
private byte[] unknownData;
private final byte version;
private final byte[] padding;
private final long permissionsBitmask;
private final int dataSize;
private final int contentOwnIdSectionSize;
private final int dataOwnerSizes;
private final int saveDataOwnSectionSize;
private final byte[] unknownData;
public FSAccessHeaderProvider(byte[] bytes) {
version = bytes[0];
@ -42,7 +42,7 @@ public class FSAccessHeaderProvider {
permissionsBitmask = Converter.getLElong(bytes, 0x4);
dataSize = Converter.getLEint(bytes, 0xC);
contentOwnIdSectionSize = Converter.getLEint(bytes, 0x10);
dataNownerSizes = Converter.getLEint(bytes, 0x14);
dataOwnerSizes = Converter.getLEint(bytes, 0x14);
saveDataOwnSectionSize = Converter.getLEint(bytes, 0x18);
unknownData = Arrays.copyOfRange(bytes, 0x1C, bytes.length);
}
@ -52,7 +52,7 @@ public class FSAccessHeaderProvider {
public long getPermissionsBitmask() { return permissionsBitmask; }
public int getDataSize() { return dataSize; }
public int getContentOwnIdSectionSize() { return contentOwnIdSectionSize; }
public int getDataNownerSizes() { return dataNownerSizes; }
public int getDataNownerSizes() { return dataOwnerSizes; }
public int getSaveDataOwnSectionSize() { return saveDataOwnSectionSize; }
public byte[] getUnknownData() { return unknownData; }
}

View file

@ -32,26 +32,26 @@ import static libKonogonka.Converter.*;
public class NPDMProvider extends ASuperInFileProvider {
private String magicNum;
private byte[] reserved1;
private byte MMUFlags;
private byte reserved2;
private byte mainThreadPrio;
private byte mainThreadCoreNum;
private byte[] reserved3;
private int personalMmHeapSize; // safe-to-store
private int version; // safe?
private long mainThreadStackSize; // TODO: check if safe
private String titleName;
private byte[] productCode;
private byte[] reserved4;
private int aci0offset; // originally 4-bytes (u-int)
private int aci0size; // originally 4-bytes (u-int)
private int acidOffset; // originally 4-bytes (u-int)
private int acidSize; // originally 4-bytes (u-int)
private final String magicNum;
private final byte[] reserved1;
private final byte MMUFlags;
private final byte reserved2;
private final byte mainThreadPrio;
private final byte mainThreadCoreNum;
private final byte[] reserved3;
private final int personalMmHeapSize; // safe-to-store
private final int version; // safe?
private final long mainThreadStackSize; // TODO: check if safe
private final String titleName;
private final byte[] productCode;
private final byte[] reserved4;
private final int aci0offset; // originally 4-bytes (u-int)
private final int aci0size; // originally 4-bytes (u-int)
private final int acidOffset; // originally 4-bytes (u-int)
private final int acidSize; // originally 4-bytes (u-int)
private ACI0Provider aci0;
private ACIDProvider acid;
private final ACI0Provider aci0;
private final ACIDProvider acid;
public NPDMProvider(PipedInputStream pis) throws Exception{
byte[] mainBuf = new byte[0x80];

View file

@ -0,0 +1,204 @@
/*
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.NSO;
import libKonogonka.Converter;
import libKonogonka.RainbowDump;
import libKonogonka.ctraes.InFileStreamProducer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.BufferedInputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static libKonogonka.Converter.getLEint;
public class NSO0Provider {
private final static Logger log = LogManager.getLogger(NSO0Provider.class);
private final InFileStreamProducer producer;
private String magic;
private int version;
private byte[] upperReserved;
private int flags;
private SegmentHeader textSegmentHeader;
private int moduleNameOffset; // note: could be unsigned int; consider 'long'
private SegmentHeader rodataSegmentHeader;
private int moduleNameSize;
private SegmentHeader dataSegmentHeader;
private int bssSize; // note: could be unsigned int; consider 'long'
private byte[] moduleId;
private int textCompressedSize;
private int rodataCompressedSize;
private int dataCompressedSize;
private byte[] bottomReserved;
private SegmentHeaderRelative _api_infoRelative;
private SegmentHeaderRelative _dynstrRelative;
private SegmentHeaderRelative _dynsymRelative;
private byte[] textHash;
private byte[] rodataHash;
private byte[] dataHash;
/* Encrypted */
public NSO0Provider(InFileStreamProducer producer) throws Exception {
this.producer = producer;
try (BufferedInputStream stream = producer.produce()){
byte[] knownStartingBytes = new byte[0x100];
if (0x100 != stream.read(knownStartingBytes))
throw new Exception("Reading stream suddenly ended while trying to read starting 0x100 bytes");
parse(knownStartingBytes);
}
}
/* Not Encrypted */
public NSO0Provider(File file) throws Exception{
this(file, 0);
}
public NSO0Provider(File file, long offsetPosition) throws Exception {
this.producer = new InFileStreamProducer(file, offsetPosition);
try (BufferedInputStream stream = producer.produce()){
if (offsetPosition != stream.skip(offsetPosition))
throw new Exception("Can't skip bytes prior NSO0 offset");
byte[] knownStartingBytes = new byte[0x100];
if (0x100 != stream.read(knownStartingBytes))
throw new Exception("Reading stream suddenly ended while trying to read starting 0x100 bytes");
parse(knownStartingBytes);
}
}
private void parse(byte[] knownStartingBytes) throws Exception{
this.magic = new String(knownStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII);
if (! magic.equals("NSO0")){
throw new Exception("Bad magic");
}
this.version = getLEint(knownStartingBytes, 0x4);
this.upperReserved = Arrays.copyOfRange(knownStartingBytes, 0x8, 0xC);
this.flags = getLEint(knownStartingBytes, 0xC);
this.textSegmentHeader = new SegmentHeader(knownStartingBytes, 0x10);
this.moduleNameOffset = Converter.getLEint(knownStartingBytes, 0x1C);
this.rodataSegmentHeader = new SegmentHeader(knownStartingBytes, 0x20);
this.moduleNameSize = Converter.getLEint(knownStartingBytes, 0x2C);
this.dataSegmentHeader = new SegmentHeader(knownStartingBytes, 0x30);
this.bssSize = Converter.getLEint(knownStartingBytes, 0x3C);
this.moduleId = Arrays.copyOfRange(knownStartingBytes, 0x40, 0x60);
this.textCompressedSize = Converter.getLEint(knownStartingBytes, 0x60);
this.rodataCompressedSize = Converter.getLEint(knownStartingBytes, 0x64);
this.dataCompressedSize = Converter.getLEint(knownStartingBytes, 0x68);
this.bottomReserved = Arrays.copyOfRange(knownStartingBytes, 0x6C, 0x88);
this._api_infoRelative = new SegmentHeaderRelative(knownStartingBytes, 0x88);
this._dynstrRelative = new SegmentHeaderRelative(knownStartingBytes, 0x90);
this._dynsymRelative = new SegmentHeaderRelative(knownStartingBytes, 0x98);
this.textHash = Arrays.copyOfRange(knownStartingBytes, 0xA0, 0xC0);
this.rodataHash = Arrays.copyOfRange(knownStartingBytes, 0xC0, 0xE0);
this.dataHash = Arrays.copyOfRange(knownStartingBytes, 0xE0, 0x100);
}
public void exportAsDecompressedNSO0(String saveToLocation) throws Exception{
NSO0Unpacker.unpack(this, producer, saveToLocation);
}
/* API */
public String getMagic() { return magic; }
public int getVersion() {return version; }
public byte[] getUpperReserved() { return upperReserved; }
public int getFlags() { return flags; }
public boolean isTextCompressFlag(){ return (flags & 0b000001) == 1; }
public boolean isRoCompressFlag(){ return (flags & 0b000010) >> 1 == 1; }
public boolean isDataCompressFlag(){ return (flags & 0b000100 ) >> 2 == 1; }
public boolean isTextHashFlag(){ return (flags & 0b001000 ) >> 3 == 1; }
public boolean isRoHashFlag(){ return (flags & 0b010000 ) >> 4 == 1; }
public boolean isDataHashFlag(){ return (flags & 0b100000 ) >> 5 == 1; }
public SegmentHeader getTextSegmentHeader() { return textSegmentHeader; }
public int getModuleNameOffset() { return moduleNameOffset; }
public SegmentHeader getRodataSegmentHeader() { return rodataSegmentHeader; }
public int getModuleNameSize() { return moduleNameSize; }
public SegmentHeader getDataSegmentHeader() { return dataSegmentHeader; }
public int getBssSize() { return bssSize; }
public byte[] getModuleId() { return moduleId; }
public int getTextCompressedSize() { return textCompressedSize; }
public int getRodataCompressedSize() { return rodataCompressedSize; }
public int getDataCompressedSize() { return dataCompressedSize; }
public byte[] getBottomReserved() { return bottomReserved; }
public SegmentHeaderRelative get_api_infoRelative() { return _api_infoRelative; }
public SegmentHeaderRelative get_dynstrRelative() { return _dynstrRelative; }
public SegmentHeaderRelative get_dynsymRelative() { return _dynsymRelative; }
public byte[] getTextHash() { return textHash; }
public byte[] getRodataHash() { return rodataHash; }
public byte[] getDataHash() { return dataHash; }
public void printDebug(){
log.debug(".:: NSO0 Provider ::.\n" +
" ============================================================= \n" +
"Magic \"NSO0\" " + magic + "\n" +
"Version (always 0) " + version + "\n" +
"Reserved " + Converter.byteArrToHexString(upperReserved) + "\n" +
"Flags " + Converter.intToBinaryString(flags) + "\n" +
" |- 0. .text Compress " + isTextCompressFlag() + "\n" +
" |- 1. .rodata Compress " + isRoCompressFlag() + "\n" +
" |- 2. .data Compress " + isDataCompressFlag() + "\n" +
" |- 3. .text Hash " + isTextHashFlag() + "\n" +
" |- 4. .rodata Hash " + isRoHashFlag() + "\n" +
" |- 5. .data Hash " + isDataHashFlag() + "\n" +
" +++\n"+
"SegmentHeader for .text\n" +
" |- File Offset - - - - - - - - - - - - - - "+ RainbowDump.formatDecHexString(textSegmentHeader.getSegmentOffset()) + "\n" +
" |- Memory Offset - - - - - - - - - - - - - "+ RainbowDump.formatDecHexString(textSegmentHeader.getMemoryOffset()) + "\n" +
" |- Size As Decompressed - - - - - - - - - "+ RainbowDump.formatDecHexString(textSegmentHeader.getSizeAsDecompressed()) + "\n" +
"ModuleNameOffset (calculated by sizeof(header)) " + RainbowDump.formatDecHexString(moduleNameOffset) + "\n" +
" +++\n"+
"SegmentHeader for .rodata\n" +
" |- File Offset - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(rodataSegmentHeader.getSegmentOffset()) + "\n" +
" |- Memory Offset - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(rodataSegmentHeader.getMemoryOffset()) + "\n" +
" |- Size As Decompressed - - - - - - - - - " + RainbowDump.formatDecHexString(rodataSegmentHeader.getSizeAsDecompressed()) + "\n" +
"ModuleNameSize " + RainbowDump.formatDecHexString(moduleNameSize) + "\n" +
" +++\n"+
"SegmentHeader for .data\n" +
" |- File Offset - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(dataSegmentHeader.getSegmentOffset()) + "\n" +
" |- Memory Offset - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(dataSegmentHeader.getMemoryOffset()) + "\n" +
" |- Size As Decompressed - - - - - - - - - " + RainbowDump.formatDecHexString(dataSegmentHeader.getSizeAsDecompressed()) + "\n" +
" .bss Size " + RainbowDump.formatDecHexString(bssSize) + "\n" + // Block Started by Symbol
"Module ID (aka Build ID) " + Converter.byteArrToHexString(moduleId) + "\n" +
" .text Size (compressed) " + RainbowDump.formatDecHexString(textCompressedSize) + "\n" +
" .rodata Size (compressed) " + RainbowDump.formatDecHexString(rodataCompressedSize) + "\n" +
" .data Size (compressed) " + RainbowDump.formatDecHexString(dataCompressedSize) + "\n" +
"Reserved " + Converter.byteArrToHexString(bottomReserved) + "\n" +
" xxx\n"+
"SegmentHeaderRelative for .api_info\n" +
" |- Offset - - - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(_api_infoRelative.getOffset()) + "\n" +
" |- Size - - - - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(_api_infoRelative.getSize()) + "\n" +
" xxx\n"+
"SegmentHeaderRelative for .dynstr\n" +
" |- Offset - - - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(_dynstrRelative.getOffset()) + "\n" +
" |- Size - - - - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(_dynstrRelative.getSize()) + "\n" +
" xxx\n"+
"SegmentHeaderRelative for .dynsym\n" +
" |- Offset - - - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(_dynsymRelative.getOffset()) + "\n" +
" |- Size - - - - - - - - - - - - - - - - - " + RainbowDump.formatDecHexString(_dynsymRelative.getSize()) + "\n" +
" xxx\n"+
".text decompressed' SHA-256 hash " + Converter.byteArrToHexString(textHash) + "\n" +
".rodata decompressed' SHA-256 hash " + Converter.byteArrToHexString(rodataHash) + "\n" +
".data decompressed' SHA-256 hash " + Converter.byteArrToHexString(dataHash) + "\n" +
" ============================================================= "
);
}
}

View file

@ -0,0 +1,221 @@
/*
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.NSO;
import libKonogonka.ctraes.InFileStreamProducer;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4SafeDecompressor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class NSO0Unpacker {
private static final String DECOMPRESSED_FILE_NAME = "main_decompressed";
private final MessageDigest digest = MessageDigest.getInstance("SHA-256");
private NSO0Provider provider;
private InFileStreamProducer producer;
private byte[] _textDecompressedSection;
private byte[] _rodataDecompressedSection;
private byte[] _dataDecompressedSection;
private byte[] header;
private int textFileOffsetNew;
private int rodataFileOffsetNew;
private int dataFileOffsetNew;
private NSO0Unpacker() throws NoSuchAlgorithmException { }
public static void unpack(NSO0Provider provider, InFileStreamProducer producer, String saveToLocation) throws Exception{
if (! provider.isTextCompressFlag() && ! provider.isRoCompressFlag() && ! provider.isDataCompressFlag())
throw new Exception("This file is not compressed");
NSO0Unpacker instance = new NSO0Unpacker();
instance.provider = provider;
instance.producer = producer;
instance.decompressSections();
instance.validateHashes();
instance.makeHeader();
instance.writeFile(saveToLocation);
}
private void decompressSections() throws Exception{
decompressTextSection();
decompressRodataSection();
decompressDataSection();
}
private void decompressTextSection() throws Exception{
if (provider.isTextCompressFlag())
_textDecompressedSection = decompressSection(provider.getTextSegmentHeader(), provider.getTextCompressedSize());
else
_textDecompressedSection = duplicateSection(provider.getTextSegmentHeader());
}
private void decompressRodataSection() throws Exception{
if (provider.isRoCompressFlag())
_rodataDecompressedSection = decompressSection(provider.getRodataSegmentHeader(), provider.getRodataCompressedSize());
else
_rodataDecompressedSection = duplicateSection(provider.getRodataSegmentHeader());
}
private void decompressDataSection() throws Exception{
if (provider.isDataCompressFlag())
_dataDecompressedSection = decompressSection(provider.getDataSegmentHeader(), provider.getDataCompressedSize());
else
_dataDecompressedSection = duplicateSection(provider.getDataSegmentHeader());
}
private byte[] decompressSection(SegmentHeader segmentHeader, int compressedSectionSize) throws Exception{
try (BufferedInputStream stream = producer.produce()) {
int sectionDecompressedSize = segmentHeader.getSizeAsDecompressed();
byte[] compressed = new byte[compressedSectionSize];
if (segmentHeader.getSegmentOffset() != stream.skip(segmentHeader.getSegmentOffset()))
throw new Exception("Failed to skip " + segmentHeader.getSegmentOffset() + " bytes till section");
if (compressedSectionSize != stream.read(compressed))
throw new Exception("Failed to read entire section");
LZ4Factory factory = LZ4Factory.fastestInstance();
LZ4SafeDecompressor decompressor = factory.safeDecompressor();
byte[] restored = new byte[sectionDecompressedSize];
int decompressedLength = decompressor.decompress(compressed, 0, compressedSectionSize, restored, 0);
if (decompressedLength != sectionDecompressedSize)
throw new Exception("Decompression failure. Expected vs. actual decompressed sizes mismatch: " +
decompressedLength + " / " + sectionDecompressedSize);
return restored;
}
}
private byte[] duplicateSection(SegmentHeader segmentHeader) throws Exception{
try (BufferedInputStream stream = producer.produce()) {
int size = segmentHeader.getSizeAsDecompressed();
byte[] sectionContent = new byte[size];
if (segmentHeader.getSegmentOffset() != stream.skip(segmentHeader.getSegmentOffset()))
throw new Exception("Failed to skip " + segmentHeader.getSegmentOffset() + " bytes till section");
if (size != stream.read(sectionContent))
throw new Exception("Failed to read entire section");
return sectionContent;
}
}
private void validateHashes() throws Exception{
if ( ! Arrays.equals(provider.getTextHash(), digest.digest(_textDecompressedSection)) ) {
throw new Exception(".text hash mismatch for .text section");
}
if ( ! Arrays.equals(provider.getRodataHash(), digest.digest(_rodataDecompressedSection)) ) {
throw new Exception(".rodata hash mismatch for .text section");
}
if ( ! Arrays.equals(provider.getDataHash(), digest.digest(_dataDecompressedSection)) ) {
throw new Exception(".data hash mismatch for .text section");
}
}
private void makeHeader() throws Exception{
try (BufferedInputStream stream = producer.produce()) {
byte[] headerBytes = new byte[0x100];
if (0x100 != stream.read(headerBytes))
throw new Exception("Unable to read initial 0x100 bytes needed for export.");
/* Simplified for computing however made hard for updating. Consider removing.
ByteBuffer resultingHeader = ByteBuffer.wrap(headerBytes).order(ByteOrder.LITTLE_ENDIAN);
((Buffer) resultingHeader).position(0xC);
resultingHeader.putInt(provider.getFlags() & 0b111000)
.putInt(provider.getTextSegmentHeader().getMemoryOffset()+0x100);
((Buffer) resultingHeader).position(0x1C);
resultingHeader.putInt(0x100)
.putInt(provider.getRodataSegmentHeader().getMemoryOffset()+0x100);
((Buffer) resultingHeader).position(0x2C);
resultingHeader.putInt(0)
.putInt(provider.getDataSegmentHeader().getMemoryOffset()+0x100);
((Buffer) resultingHeader).position(0x60);
resultingHeader.putInt(provider.getTextSegmentHeader().getSizeAsDecompressed())
.putInt(provider.getRodataSegmentHeader().getSizeAsDecompressed())
.putInt(provider.getDataSegmentHeader().getSizeAsDecompressed());
((Buffer) resultingHeader).flip();
header = resultingHeader.array();
*/
textFileOffsetNew = provider.getTextSegmentHeader().getMemoryOffset()+0x100;
rodataFileOffsetNew = provider.getRodataSegmentHeader().getMemoryOffset()+0x100;
dataFileOffsetNew = provider.getDataSegmentHeader().getMemoryOffset()+0x100;
ByteBuffer resultingHeader = ByteBuffer.allocate(0x100).order(ByteOrder.LITTLE_ENDIAN);
resultingHeader.put("NSO0".getBytes(StandardCharsets.US_ASCII))
.putInt(provider.getVersion())
.put(provider.getUpperReserved())
.putInt(provider.getFlags() & 0b111000)
.putInt(textFileOffsetNew)
.putInt(provider.getTextSegmentHeader().getMemoryOffset())
.putInt(provider.getTextSegmentHeader().getSizeAsDecompressed())
.putInt(0x100)
.putInt(rodataFileOffsetNew)
.putInt(provider.getRodataSegmentHeader().getMemoryOffset())
.putInt(provider.getRodataSegmentHeader().getSizeAsDecompressed())
.putInt(0)
.putInt(dataFileOffsetNew)
.putInt(provider.getDataSegmentHeader().getMemoryOffset())
.putInt(provider.getDataSegmentHeader().getSizeAsDecompressed())
.putInt(provider.getBssSize())
.put(provider.getModuleId())
.putInt(provider.getTextSegmentHeader().getSizeAsDecompressed())
.putInt(provider.getRodataSegmentHeader().getSizeAsDecompressed())
.putInt(provider.getDataSegmentHeader().getSizeAsDecompressed())
.put(provider.getBottomReserved())
.putInt(provider.get_api_infoRelative().getOffset())
.putInt(provider.get_api_infoRelative().getSize())
.putInt(provider.get_dynstrRelative().getOffset())
.putInt(provider.get_dynstrRelative().getSize())
.putInt(provider.get_dynsymRelative().getOffset())
.putInt(provider.get_dynsymRelative().getSize())
.put(provider.getTextHash())
.put(provider.getRodataHash())
.put(provider.getDataHash());
header = resultingHeader.array();
}
}
private void writeFile(String saveToLocation) throws Exception{
File location = new File(saveToLocation);
location.mkdirs();
try (RandomAccessFile raf = new RandomAccessFile(saveToLocation+File.separator+DECOMPRESSED_FILE_NAME,
"rw")){
raf.write(header);
raf.seek(textFileOffsetNew);
raf.write(_textDecompressedSection);
raf.seek(rodataFileOffsetNew);
raf.write(_rodataDecompressedSection);
raf.seek(dataFileOffsetNew);
raf.write(_dataDecompressedSection);
}
}
}

View file

@ -0,0 +1,49 @@
/*
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.NSO;
import libKonogonka.Converter;
public class SegmentHeader {
private final int segmentOffset;
private final int memoryOffset;
private final int sizeAsDecompressed;
SegmentHeader(byte[] data){
this(data, 0);
}
SegmentHeader(byte[] data, int fromOffset){
this.segmentOffset = Converter.getLEint(data, fromOffset);
this.memoryOffset = Converter.getLEint(data, fromOffset+4);
this.sizeAsDecompressed = Converter.getLEint(data, fromOffset+8);
}
public int getSegmentOffset() {
return segmentOffset;
}
public int getMemoryOffset() {
return memoryOffset;
}
public int getSizeAsDecompressed() {
return sizeAsDecompressed;
}
}

View file

@ -0,0 +1,43 @@
/*
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.NSO;
import libKonogonka.Converter;
public class SegmentHeaderRelative {
private final int offset;
private final int size;
SegmentHeaderRelative(byte[] data){
this(data, 0);
}
SegmentHeaderRelative(byte[] data, int fromOffset){
this.offset = Converter.getLEint(data, fromOffset);
this.size = Converter.getLEint(data, fromOffset+4);
}
public int getOffset() {
return offset;
}
public int getSize() {
return size;
}
}

View file

@ -19,7 +19,9 @@
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 {
@ -32,5 +34,10 @@ public interface IPFS0Provider extends ISuperProvider {
PFS0subFile[] getPfs0subFiles();
void printDebug();
InFileStreamProducer getStreamProducer(String subFileName) throws FileNotFoundException;
InFileStreamProducer getStreamProducer(int subFileNumber);
LinkedList<byte[]> getPfs0SHA256hashes();
}

View file

@ -23,6 +23,7 @@ import libKonogonka.RainbowDump;
import libKonogonka.Tools.NCA.NCASectionTableBlock.SuperBlockPFS0;
import libKonogonka.ctraes.AesCtrBufferedInputStream;
import libKonogonka.ctraes.AesCtrDecryptSimple;
import libKonogonka.ctraes.InFileStreamProducer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -294,6 +295,31 @@ public class PFS0Provider implements IPFS0Provider{
return true;
}
@Override
public InFileStreamProducer getStreamProducer(String subFileName) throws FileNotFoundException {
for (int i = 0; i < pfs0subFiles.length; i++) {
if (pfs0subFiles[i].getName().equals(subFileName))
return getStreamProducer(i);
}
throw new FileNotFoundException("No file with such name exists: "+subFileName);
}
@Override
public InFileStreamProducer getStreamProducer(int subFileNumber) {
PFS0subFile subFile = pfs0subFiles[subFileNumber];
long subFileOffset = subFile.getOffset() + mediaStartOffset * 0x200 + rawBlockDataStart;
if (encrypted) {
return new InFileStreamProducer(file,
pfs0subFiles[subFileNumber].getSize(),
ncaOffset,
subFileOffset,
decryptor,
mediaStartOffset,
mediaEndOffset);
}
return new InFileStreamProducer(file, offsetPositionInFile, subFileOffset);
}
/**
* @deprecated
* */

View file

@ -0,0 +1,119 @@
/*
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.ctraes;
import java.io.BufferedInputStream;
import java.io.File;
import java.nio.file.Files;
// TODO: rework, simplify
public class InFileStreamProducer {
private final boolean encrypted;
private final long size;
private final File file;
private final long initialOffset;
private long subOffset;
private AesCtrDecryptSimple decryptor;
private long mediaStartOffset;
private long mediaEndOffset;
public InFileStreamProducer(
File file,
long size,
long initialOffset,
long subOffset,
AesCtrDecryptSimple decryptor,
long mediaStartOffset,
long mediaEndOffset
){
this.encrypted = (decryptor != null);
this.file = file;
this.size = size;
this.initialOffset = initialOffset;
this.subOffset = subOffset;
this.decryptor = decryptor;
this.mediaStartOffset = mediaStartOffset;
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{
if (encrypted) {
return produceAesCtr();
}
return produceNotEncrypted();
}
private AesCtrBufferedInputStream produceAesCtr() throws Exception{
decryptor.reset();
AesCtrBufferedInputStream stream = new AesCtrBufferedInputStream(
decryptor,
initialOffset,
mediaStartOffset,
mediaEndOffset,
Files.newInputStream(file.toPath()));
if (subOffset != stream.skip(subOffset))
throw new Exception("Unable to skip offset: " + subOffset);
return stream;
}
private BufferedInputStream produceNotEncrypted() throws Exception{
BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(file.toPath()));
if (subOffset != stream.skip(subOffset))
throw new Exception("Unable to skip offset: " + subOffset);
return stream;
}
public InFileStreamProducer getSuccessor(long subOffset, long size){
this.subOffset = subOffset;
return new InFileStreamProducer(file, size, initialOffset, subOffset, decryptor, mediaStartOffset, mediaEndOffset);
}
public boolean isEncrypted() {
return encrypted;
}
public long getSize() {
return size;
}
@Override
public String toString(){
return file.getName();
}
}

View file

@ -0,0 +1,43 @@
/*
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.NSO.NSO0Provider;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
public class NSODecompressTest {
private static final String ncaExtractedFileLocation = "./FilesForTests/NSO0/main";
private static final String ncaExtractedFileLocationDec = "./FilesForTests/NSO0/main_d";
@Disabled
@DisplayName("NSO0 Decompression test")
@Test
void nso0DecompressionTest() throws Exception {
NSO0Provider nso0Provider = new NSO0Provider(new File(ncaExtractedFileLocation));
//nso0Provider.exportAsDecompressedNSO0("./FilesForTests/NSO0");
nso0Provider.printDebug();
NSO0Provider nso0Provider1 = new NSO0Provider(new File(ncaExtractedFileLocationDec));
nso0Provider1.printDebug();
}
}

View file

@ -0,0 +1,129 @@
/*
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.Tools.NCA.NCAProvider;
import libKonogonka.Tools.NSO.NSO0Provider;
import libKonogonka.Tools.PFS0.IPFS0Provider;
import libKonogonka.Tools.PFS0.PFS0subFile;
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;
public class NSOTest {
private static final String keysFileLocation = "./FilesForTests/prod.keys";
private static final String xci_header_keyFileLocation = "./FilesForTests/xci_header_key.txt";
private static final String ncaFileLocation = "./FilesForTests/nso_container.nca";
private static KeyChainHolder keyChainHolder;
private static NCAProvider ncaProvider;
@Disabled
@DisplayName("NSO0 test")
@Test
void nso0Test() 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);
ncaProvider = new NCAProvider(new File(ncaFileLocation), keyChainHolder.getRawKeySet());
pfs0Validation();
nso0Validation();
// AesCtrBufferedInputStreamTest();
}
void pfs0Validation() throws Exception{
for (byte i = 0; i < 4; i++){
System.out.println("..:: TEST SECTION #"+i+" ::..");
if (ncaProvider.getSectionBlock(i).getFsType() == 1 &&
ncaProvider.getSectionBlock(i).getHashType() == 2 &&
ncaProvider.getSectionBlock(i).getCryptoType() == 3){
ncaProvider.getNCAContentProvider(i).getPfs0().printDebug();
ncaProvider.getSectionBlock(i).printDebug();
return;
}
}
}
long ACBISoffsetPosition;
long ACBISmediaStartOffset;
long ACBISmediaEndOffset;
long offsetPosition;
void nso0Validation() throws Exception{
File nca = new File(ncaFileLocation);
PFS0subFile[] subfiles = ncaProvider.getNCAContentProvider(0).getPfs0().getPfs0subFiles();
offsetPosition = ncaProvider.getTableEntry0().getMediaStartOffset()*0x200 +
ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart();
System.out.println("\t=============================================================");
System.out.println("\tNCA SIZE: "+ RainbowDump.formatDecHexString(nca.length()));
System.out.println("\tPFS0 Offset(get) "+RainbowDump.formatDecHexString(ncaProvider.getSectionBlock0().getSuperBlockPFS0().getPfs0offset()));
System.out.println("\tPFS0 MediaStart (* 0x200) "+RainbowDump.formatDecHexString(ncaProvider.getTableEntry0().getMediaStartOffset()*0x200));
System.out.println("\tPFS0 MediaEnd (* 0x200) "+RainbowDump.formatDecHexString(ncaProvider.getTableEntry0().getMediaEndOffset()*0x200));
System.out.println("\tPFS0 Offset+MediaBlockStart: "+RainbowDump.formatDecHexString(offsetPosition));
System.out.println("\tRAW Offset: "+RainbowDump.formatDecHexString(ncaProvider.getNCAContentProvider(0).getPfs0().getRawFileDataStart()));
System.out.println("\tHashTableSize: "+RainbowDump.formatDecHexString(ncaProvider.getSectionBlock0().getSuperBlockPFS0().getHashTableSize()));
for (PFS0subFile subFile : subfiles){
System.out.println("\n\tEntry Name: "+subFile.getName());
System.out.println("\tEntry Offset: "+RainbowDump.formatDecHexString(subFile.getOffset()));
System.out.println("\tEntry Size: "+RainbowDump.formatDecHexString(subFile.getSize()));
}
System.out.println("\t=============================================================");
ACBISoffsetPosition = 0;
ACBISmediaStartOffset = ncaProvider.getTableEntry0().getMediaStartOffset();
ACBISmediaEndOffset = ncaProvider.getTableEntry0().getMediaEndOffset();
AesCtrDecryptSimple decryptSimple = new AesCtrDecryptSimple(
ncaProvider.getDecryptedKey2(),
ncaProvider.getSectionBlock0().getSectionCTR(),
ncaProvider.getTableEntry0().getMediaStartOffset() * 0x200);
IPFS0Provider pfs0Provider = ncaProvider.getNCAContentProvider(0).getPfs0();
NSO0Provider nso0Provider = new NSO0Provider(pfs0Provider.getStreamProducer(0));
nso0Provider.printDebug();
// NPDMProvider npdmProvider = new NPDMProvider(pfs0Provider.getProviderSubFilePipedInpStream(1));
System.out.println("__--++ SDK VERSION ++--__\n"
+ncaProvider.getSdkVersion()[3]
+"."+ncaProvider.getSdkVersion()[2]
+"."+ncaProvider.getSdkVersion()[1]
+"."+ncaProvider.getSdkVersion()[0]);
}
}