diff --git a/src/main/java/libKonogonka/Tools/other/System2/System2Header.java b/src/main/java/libKonogonka/Tools/other/System2/System2Header.java index 76c8e64..bedec72 100644 --- a/src/main/java/libKonogonka/Tools/other/System2/System2Header.java +++ b/src/main/java/libKonogonka/Tools/other/System2/System2Header.java @@ -20,7 +20,7 @@ package libKonogonka.Tools.other.System2; import libKonogonka.Converter; import libKonogonka.RainbowDump; -import libKonogonka.ctraes.AesCtrDecrypt; +import libKonogonka.ctraes.AesCtrClassic; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -56,6 +56,8 @@ public class System2Header { private byte[] sha256overEncryptedSection2; private byte[] sha256overEncryptedSection3; + private String key; + private HashMap package2Keys; private byte[] decodedHeaderBytes; @@ -74,13 +76,15 @@ public class System2Header { } } private void decodeEncrypted(byte[] headerBytes) throws Exception{ + int i=0; for (Map.Entry entry: package2Keys.entrySet()){ - AesCtrDecrypt decrypt = new AesCtrDecrypt(entry.getValue(), headerCtr, 0x100); + AesCtrClassic ctrClassic = new AesCtrClassic(entry.getValue(), headerCtr); - decodedHeaderBytes = decrypt.decryptNext(headerBytes); + decodedHeaderBytes = ctrClassic.decryptNext(headerBytes); byte[] magicBytes = Arrays.copyOfRange(decodedHeaderBytes, 0x50, 0x54); magic = new String(magicBytes, StandardCharsets.US_ASCII); if (magic.equals("PK21")) { + key = entry.getValue(); log.debug("Header key used "+entry.getKey() + " = " + entry.getValue()); return; } @@ -112,6 +116,31 @@ public class System2Header { sha256overEncryptedSection3 = Arrays.copyOfRange(decodedHeaderBytes, 0xe0, 0x100); } + public byte[] getHeaderCtr() { return headerCtr; } + public byte[] getSection0Ctr() { return section0Ctr; } + public byte[] getSection1Ctr() { return section1Ctr; } + public byte[] getSection2Ctr() { return section2Ctr; } + public byte[] getSection3Ctr() { return section3Ctr; } + public String getMagic() { return magic; } + public int getBaseOffset() { return baseOffset; } + public byte[] getZeroOrReserved() { return zeroOrReserved; } + public byte getPackage2Version() { return package2Version; } + public byte getBootloaderVersion() { return bootloaderVersion; } + public byte[] getPadding() { return padding; } + public int getSection0size() { return section0size; } + public int getSection1size() { return section1size; } + public int getSection2size() { return section2size; } + public int getSection3size() { return section3size; } + public int getSection0offset() { return section0offset; } + public int getSection1offset() { return section1offset; } + public int getSection2offset() { return section2offset; } + public int getSection3offset() { return section3offset; } + public byte[] getSha256overEncryptedSection0() { return sha256overEncryptedSection0; } + public byte[] getSha256overEncryptedSection1() { return sha256overEncryptedSection1; } + public byte[] getSha256overEncryptedSection2() { return sha256overEncryptedSection2; } + public byte[] getSha256overEncryptedSection3() { return sha256overEncryptedSection3; } + public String getKey() { return key; } + public void printDebug(){ log.debug("== System2 Header ==\n" + "Header CTR : " + Converter.byteArrToHexStringAsLE(headerCtr) + "\n" + diff --git a/src/main/java/libKonogonka/Tools/other/System2/System2Provider.java b/src/main/java/libKonogonka/Tools/other/System2/System2Provider.java index 571f3c2..8542b66 100644 --- a/src/main/java/libKonogonka/Tools/other/System2/System2Provider.java +++ b/src/main/java/libKonogonka/Tools/other/System2/System2Provider.java @@ -19,12 +19,20 @@ package libKonogonka.Tools.other.System2; import libKonogonka.KeyChainHolder; +import libKonogonka.RainbowDump; +import libKonogonka.ctraes.AesCtrClassic; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; public class System2Provider { + private final static Logger log = LogManager.getLogger(System2Provider.class); + private byte[] Rsa2048signature; private System2Header header; // ... @@ -37,6 +45,8 @@ public class System2Provider { this.keyChainHolder = keyChainHolder; readHeaderCtr(); + + } private void readHeaderCtr() throws Exception{ @@ -50,6 +60,56 @@ public class System2Provider { } } + public boolean exportKernel(String saveTo) throws Exception{ + AesCtrClassic aesCtrClassic = new AesCtrClassic(header.getKey(), header.getSection0Ctr()); // TODO: DELETE + + File location = new File(saveTo); + location.mkdirs(); + + try (BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(Paths.get(pathToFile))); + BufferedOutputStream extractedFileBOS = new BufferedOutputStream( + Files.newOutputStream(Paths.get(saveTo+File.separator+"Kernel.bin")))){ + + long kernelSize = header.getSection0size(); + + long toSkip = 0x200; + if (toSkip != stream.skip(toSkip)) + throw new Exception("Unable to skip offset: "+toSkip); + int blockSize = 0x200; + if (kernelSize < 0x200) + blockSize = (int) kernelSize; + + 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); + byte[] decrypted = aesCtrClassic.decryptNext(block); + if (i > 0 && i <= 0x201){ + RainbowDump.hexDumpUTF8(decrypted); + } + if (i == 0){ + RainbowDump.hexDumpUTF8(decrypted); + } + extractedFileBOS.write(decrypted); + i += blockSize; + if ((i + blockSize) > kernelSize) { + blockSize = (int) (kernelSize - i); + if (blockSize == 0) + break; + block = new byte[blockSize]; + } + } + } + catch (Exception e){ + log.error("File export failure", e); + return false; + } + return true; + } + public System2Header getHeader() { return header; } diff --git a/src/main/java/libKonogonka/ctraes/AesCtrDecrypt.java b/src/main/java/libKonogonka/ctraes/AesCtrClassic.java similarity index 50% rename from src/main/java/libKonogonka/ctraes/AesCtrDecrypt.java rename to src/main/java/libKonogonka/ctraes/AesCtrClassic.java index 771cde1..039aaa2 100644 --- a/src/main/java/libKonogonka/ctraes/AesCtrDecrypt.java +++ b/src/main/java/libKonogonka/ctraes/AesCtrClassic.java @@ -18,45 +18,40 @@ */ package libKonogonka.ctraes; -/** - * Simplify decryption of the CTR - */ -public class AesCtrDecrypt { +import org.bouncycastle.jce.provider.BouncyCastleProvider; - private long realMediaOffset; - private byte[] IVarray; - private AesCtr aesCtr; +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.Security; - private final byte[] initialKey; - private final byte[] initialSectionCTR; - private final long initialRealMediaOffset; +public class AesCtrClassic { - public AesCtrDecrypt(String key, byte[] sectionCTR, long realMediaOffset) throws Exception{ - this.initialKey = hexStrToByteArray(key); - this.initialSectionCTR = sectionCTR; - this.initialRealMediaOffset = realMediaOffset; - reset(); + private static boolean BCinitialized = false; + + private void initBCProvider(){ + Security.addProvider(new BouncyCastleProvider()); + BCinitialized = true; } - public void skipNext(){ - realMediaOffset += 0x200; + private final Cipher cipher; + + public AesCtrClassic(String keyString, byte[] IVarray) throws Exception{ + if ( ! BCinitialized) + initBCProvider(); + byte[] keyArray = hexStrToByteArray(keyString); + SecretKeySpec key = new SecretKeySpec(keyArray, "AES"); + cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); + IvParameterSpec iv = new IvParameterSpec(IVarray.clone()); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + //TODO: CipherOutputStream } - public void skipNext(long blocksNum){ - realMediaOffset += blocksNum * 0x200; + public byte[] decryptNext(byte[] encryptedData) { + return cipher.update(encryptedData); } - public byte[] decryptNext(byte[] encryptedBlock) throws Exception{ - byte[] decryptedBlock = aesCtr.decrypt(encryptedBlock, IVarray); - realMediaOffset += 0x200; - return decryptedBlock; - } - - public void reset() throws Exception{ - realMediaOffset = initialRealMediaOffset; - aesCtr = new AesCtr(initialKey); - IVarray = initialSectionCTR;//Converter.flip(); - } private byte[] hexStrToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; diff --git a/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java b/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java index 1547b41..db8f05d 100644 --- a/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java +++ b/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java @@ -27,7 +27,7 @@ public class AesCtrDecryptSimple { private long realMediaOffset; private byte[] IVarray; - private AesCtr aesCtr; + private AesCtrForMediaBlocks aesCtr; private final byte[] initialKey; private final byte[] initialSectionCTR; @@ -65,7 +65,7 @@ public class AesCtrDecryptSimple { public void reset() throws Exception{ realMediaOffset = initialRealMediaOffset; - aesCtr = new AesCtr(initialKey); + aesCtr = new AesCtrForMediaBlocks(initialKey); // IV for CTR == 16 bytes IVarray = new byte[0x10]; // Populate first 4 bytes taken from Header's section Block CTR (aka SecureValue) diff --git a/src/main/java/libKonogonka/ctraes/AesCtr.java b/src/main/java/libKonogonka/ctraes/AesCtrForMediaBlocks.java similarity index 90% rename from src/main/java/libKonogonka/ctraes/AesCtr.java rename to src/main/java/libKonogonka/ctraes/AesCtrForMediaBlocks.java index b020ec9..28b8700 100644 --- a/src/main/java/libKonogonka/ctraes/AesCtr.java +++ b/src/main/java/libKonogonka/ctraes/AesCtrForMediaBlocks.java @@ -25,7 +25,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.Security; -public class AesCtr { +public class AesCtrForMediaBlocks { private static boolean BCinitialized = false; @@ -37,7 +37,7 @@ public class AesCtr { private final Cipher cipher; private final SecretKeySpec key; - public AesCtr(byte[] keyArray) throws Exception{ + AesCtrForMediaBlocks(byte[] keyArray) throws Exception{ if ( ! BCinitialized) initBCProvider(); @@ -45,7 +45,7 @@ public class AesCtr { cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); } - public byte[] decrypt(byte[] encryptedData, byte[] IVarray) throws Exception{ + byte[] decrypt(byte[] encryptedData, byte[] IVarray) throws Exception{ IvParameterSpec iv = new IvParameterSpec(IVarray); cipher.init(Cipher.DECRYPT_MODE, key, iv); return cipher.doFinal(encryptedData); diff --git a/src/test/java/libKonogonka/RomFsDecrypted/Package2Test.java b/src/test/java/libKonogonka/RomFsDecrypted/Package2Test.java new file mode 100644 index 0000000..805e932 --- /dev/null +++ b/src/test/java/libKonogonka/RomFsDecrypted/Package2Test.java @@ -0,0 +1,110 @@ +/* + 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 . + */ +package libKonogonka.RomFsDecrypted; + +import libKonogonka.Converter; +import libKonogonka.KeyChainHolder; +import libKonogonka.RainbowDump; +import libKonogonka.Tools.NCA.NCAContent; +import libKonogonka.Tools.NCA.NCAProvider; +import libKonogonka.Tools.RomFs.RomFsProvider; +import libKonogonka.ctraes.InFileStreamProducer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; + +public class Package2Test { + + 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 = "/home/loper/Projects/tempPatchesPlayground/fw1100"; + private static KeyChainHolder keyChainHolder; + private static NCAProvider ncaProvider; + + //@Disabled + @Order(1) + @DisplayName("Package2 Test") + @Test + void test() 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); + + File parent = new File(ncaFileLocation); + String[] dirWithFiles = parent.list((file, s) -> s.endsWith(".nca")); //String[] dirWithFiles = parent.list((file, s) -> s.endsWith(".cnmt.nca")); + + Assertions.assertNotNull(dirWithFiles); + + for (String fileName : dirWithFiles){ + read(new File(ncaFileLocation + File.separator + fileName)); + } + } + + void read(File file) throws Exception{ + ncaProvider = new NCAProvider(file, keyChainHolder.getRawKeySet()); + + String titleId = Converter.byteArrToHexStringAsLE(ncaProvider.getTitleId()); + if (titleId.equals("0100000000000819")) + System.out.println(file.getName()+" "+titleId + "\tFAT"); + else if (titleId.equals("010000000000081b")) + System.out.println(file.getName()+" "+titleId + "\tEXFAT"); + else + return; + + for (int i = 0; i < 4; i++){ + NCAContent content = ncaProvider.getNCAContentProvider(i); + System.out.println("NCAContent "+i+" exists = "+!(content == null)); + } + + //ncaProvider.getSectionBlock0().printDebug(); + if (ncaProvider.getSectionBlock0().getSuperBlockIVFC() == null) + return; + + RomFsProvider romFsProvider = ncaProvider.getNCAContentProvider(0).getRomfs(); + romFsProvider.printDebug(); + romFsProvider.exportContent("./FilesForTests/"+file.getName()+"_extracted", romFsProvider.getRootEntry()); + + //int contentSize = (int) pfs0Provider.getHeader().getPfs0subFiles()[0].getSize(); + int contentSize = 0x200; + InFileStreamProducer producer = romFsProvider.getStreamProducer( + romFsProvider.getRootEntry() + .getContent().get(0) + .getContent().get(2) + ); + try (BufferedInputStream stream = producer.produce()){ + byte[] everythingCNMT = new byte[contentSize]; + Assertions.assertEquals(contentSize, stream.read(everythingCNMT)); + + RainbowDump.hexDumpUTF8(everythingCNMT); + } + + } +} diff --git a/src/test/java/libKonogonka/RomFsDecrypted/Package2UnpackedTest.java b/src/test/java/libKonogonka/RomFsDecrypted/Package2UnpackedTest.java new file mode 100644 index 0000000..b1f13c4 --- /dev/null +++ b/src/test/java/libKonogonka/RomFsDecrypted/Package2UnpackedTest.java @@ -0,0 +1,103 @@ +/* + 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 . + */ +package libKonogonka.RomFsDecrypted; + +import libKonogonka.KeyChainHolder; +import libKonogonka.Tools.other.System2.System2Provider; +import libKonogonka.ctraes.AesCtrClassic; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class Package2UnpackedTest { + + private static final String keysFileLocation = "./FilesForTests/prod.keys"; + private static final String xci_header_keyFileLocation = "./FilesForTests/xci_header_key.txt"; + private static KeyChainHolder keyChainHolder; + + private static final String fileLocation = "/home/loper/Projects/libKonogonka/FilesForTests/6*cc.nca_extracted/ROOT/nx/package2"; + + @DisplayName("Package2 unpacked test") + @Test + void discover() throws Exception{ + BufferedReader br = new BufferedReader(new FileReader(xci_header_keyFileLocation)); + String keyValue = br.readLine(); + br.close(); + + Assertions.assertNotNull(keyValue); + + keyValue = keyValue.trim(); + keyChainHolder = new KeyChainHolder(keysFileLocation, keyValue); + + HashMap rawKeys = keyChainHolder.getRawKeySet(); + + HashMap package2_keys = new HashMap<>(); + + for (String key: rawKeys.keySet()){ + if (key.matches("package2_key_[0-f][0-f]")) + package2_keys.put(key, rawKeys.get(key)); + } + + Assertions.assertNotNull(package2_keys); + + Path package2Path = Paths.get(fileLocation); + byte[] header; + + try (BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(package2Path))){ + Assertions.assertEquals(0x100, stream.skip(0x100)); + header = new byte[0x100]; + Assertions.assertEquals(0x100, stream.read(header)); + } + + Assertions.assertNotNull(header); + + byte[] headerCTR = Arrays.copyOfRange(header, 0, 0x10); + + for (Map.Entry entry: package2_keys.entrySet()){ + AesCtrClassic aesCtrClassic = new AesCtrClassic(entry.getValue(), headerCTR); + + byte[] decrypted = aesCtrClassic.decryptNext(header); + //RainbowDump.hexDumpUTF8(decrypted); + byte[] magic = Arrays.copyOfRange(decrypted, 0x50, 0x54); + String magicString = new String(magic, StandardCharsets.US_ASCII); + if (magicString.equals("PK21")) + System.out.println(entry.getKey()+" "+entry.getValue()+" "+magicString); + } + } + @DisplayName("Package2 written test") + @Test + void implement() throws Exception{ + System.out.printf("SIZE: %d 0x%x\n", Files.size(Paths.get(fileLocation)), Files.size(Paths.get(fileLocation))); + keyChainHolder = new KeyChainHolder(keysFileLocation, null); + System2Provider provider = new System2Provider(fileLocation, keyChainHolder); + provider.getHeader().printDebug(); + + boolean exported = provider.exportKernel("/tmp"); + System.out.println("Exported = "+exported); + } +}