diff --git a/.classpath b/.classpath index 7026f46..4f8e155 100644 --- a/.classpath +++ b/.classpath @@ -1,7 +1,18 @@ - - + + + + + + + + + + + + + @@ -12,5 +23,11 @@ + + + + + + diff --git a/pom.xml b/pom.xml index 826c20d..eeade7b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ - src + src/main/java maven-compiler-plugin diff --git a/src/main/java/de/mas/wiiu/jnus/ExtractionService.java b/src/main/java/de/mas/wiiu/jnus/ExtractionService.java index 0fb5d3f..785f826 100644 --- a/src/main/java/de/mas/wiiu/jnus/ExtractionService.java +++ b/src/main/java/de/mas/wiiu/jnus/ExtractionService.java @@ -19,6 +19,7 @@ package de.mas.wiiu.jnus; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -52,14 +53,14 @@ public final class ExtractionService { } private ExtractionService(NUSTitle nustitle) { - if (nustitle.getDataProvider() instanceof Parallelizable) { + if (nustitle.getDataProcessor().getDataProvider() instanceof Parallelizable) { parallelizable = true; } this.NUSTitle = nustitle; } private NUSDataProvider getDataProvider() { - return getNUSTitle().getDataProvider(); + return getNUSTitle().getDataProcessor().getDataProvider(); } public void extractAllEncrpytedContentFileHashes(String outputFolder) throws IOException { @@ -95,7 +96,7 @@ public final class ExtractionService { } } - public void extractEncryptedContentFilesTo(List list, String outputFolder, boolean withHashes) throws IOException { + public void extractEncryptedContentFilesTo(Collection list, String outputFolder, boolean withHashes) throws IOException { Utils.createDir(outputFolder); if (parallelizable && Settings.ALLOW_PARALLELISATION) { try { diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitle.java b/src/main/java/de/mas/wiiu/jnus/NUSTitle.java index 26b9c2b..2b23566 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitle.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitle.java @@ -17,7 +17,6 @@ package de.mas.wiiu.jnus; import java.io.IOException; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -27,10 +26,9 @@ import de.mas.wiiu.jnus.entities.TMD; import de.mas.wiiu.jnus.entities.Ticket; import de.mas.wiiu.jnus.entities.fst.FST; import de.mas.wiiu.jnus.entities.fst.FSTEntry; -import de.mas.wiiu.jnus.interfaces.NUSDataProvider; +import de.mas.wiiu.jnus.interfaces.NUSDataProcessor; import de.mas.wiiu.jnus.utils.FSTUtils; import lombok.Getter; -import lombok.NonNull; import lombok.Setter; public class NUSTitle { @@ -39,12 +37,18 @@ public class NUSTitle { @Getter private final TMD TMD; - @Getter private final NUSDataProvider dataProvider; + @Getter private final NUSDataProcessor dataProcessor; - public NUSTitle(@NonNull NUSDataProvider dataProvider) throws ParseException, IOException { - byte[] tmdData = dataProvider.getRawTMD().orElseThrow(() -> new ParseException("No TMD data found", 0)); - this.TMD = de.mas.wiiu.jnus.entities.TMD.parseTMD(tmdData); - this.dataProvider = dataProvider; + private NUSTitle(TMD tmd, NUSDataProcessor dataProcessor) { + this.TMD = tmd; + this.dataProcessor = dataProcessor; + } + + public static NUSTitle create(TMD tmd, NUSDataProcessor dataProcessor, Optional ticket, Optional fst) { + NUSTitle result = new NUSTitle(tmd, dataProcessor); + result.setTicket(ticket); + result.setFST(fst); + return result; } public Stream getAllFSTEntriesAsStream() { @@ -66,13 +70,13 @@ public class NUSTitle { } public void cleanup() throws IOException { - if (getDataProvider() != null) { - getDataProvider().cleanup(); + if (getDataProcessor() != null && getDataProcessor().getDataProvider() != null) { + getDataProcessor().getDataProvider().cleanup(); } } @Override public String toString() { - return "NUSTitle [dataProvider=" + dataProvider + "]"; + return "NUSTitle [dataProcessor=" + dataProcessor + "]"; } } diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoader.java b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoader.java index 9bbacf6..defa7de 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoader.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoader.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright (C) 2016-2019 Maschell + * Copyright (C) 2016-2020 Maschell * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,54 +16,78 @@ ****************************************************************************/ package de.mas.wiiu.jnus; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; +import java.security.NoSuchProviderException; import java.text.ParseException; import java.util.Optional; import java.util.function.Supplier; +import de.mas.wiiu.jnus.entities.TMD; import de.mas.wiiu.jnus.entities.Ticket; import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.fst.FST; import de.mas.wiiu.jnus.entities.fst.FSTEntry; import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle; +import de.mas.wiiu.jnus.interfaces.ContentDecryptor; +import de.mas.wiiu.jnus.interfaces.ContentEncryptor; import de.mas.wiiu.jnus.interfaces.FSTDataProvider; +import de.mas.wiiu.jnus.interfaces.NUSDataProcessor; import de.mas.wiiu.jnus.interfaces.NUSDataProvider; -import de.mas.wiiu.jnus.utils.StreamUtils; -import de.mas.wiiu.jnus.utils.cryptography.AESDecryption; +import de.mas.wiiu.jnus.interfaces.TriFunction; +import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption; +import de.mas.wiiu.jnus.utils.cryptography.NUSEncryption; public class NUSTitleLoader { private NUSTitleLoader() { // should be empty } - public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier dataProviderFunction) throws IOException, ParseException { + public static NUSTitle loadNusTitle(NUSTitleConfig config, Supplier dataProviderFunction, + TriFunction, Optional, NUSDataProcessor> dataProcessorFunction) + throws IOException, ParseException { NUSDataProvider dataProvider = dataProviderFunction.get(); - NUSTitle result = new NUSTitle(dataProvider); + TMD tmd = TMD.parseTMD(dataProvider.getRawTMD().orElseThrow(() -> new FileNotFoundException("No TMD data found"))); if (config.isNoDecryption()) { + NUSTitle result = NUSTitle.create(tmd, dataProcessorFunction.apply(dataProvider, Optional.empty(), Optional.empty()), Optional.empty(), + Optional.empty()); return result; } - Ticket ticket = null; + Optional ticket = Optional.empty(); + Optional decryption = Optional.empty(); + Optional encryption = Optional.empty(); if (config.isTicketNeeded()) { - ticket = config.getTicket(); - if (ticket == null) { + Ticket ticketT = config.getTicket(); + if (ticketT == null) { Optional ticketOpt = dataProvider.getRawTicket(); if (ticketOpt.isPresent()) { - ticket = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey()); + ticketT = Ticket.parseTicket(ticketOpt.get(), config.getCommonKey()); } } - if (ticket == null) { - new ParseException("Failed to get ticket data", 0); + if (ticketT == null) { + throw new ParseException("Failed to get ticket data", 0); + } + + ticket = Optional.of(ticketT); + + decryption = Optional.of(new NUSDecryption(ticketT)); + try { + encryption = Optional.of(new NUSEncryption(ticketT)); + } catch (NoSuchProviderException e) { + throw new IOException(e); } - result.setTicket(Optional.of(ticket)); } + NUSDataProcessor dpp = dataProcessorFunction.apply(dataProvider, decryption, encryption); + // If we have just content, we don't have a FST. - if (result.getTMD().getAllContents().size() == 1) { + if (tmd.getAllContents().size() == 1) { // The only way to check if the key is right, is by trying to decrypt the whole thing. + NUSTitle result = NUSTitle.create(tmd, dpp, ticket, Optional.empty()); + FSTDataProvider dp = new FSTDataProviderNUSTitle(result); for (FSTEntry children : dp.getRoot().getChildren()) { dp.readFile(children); @@ -72,27 +96,16 @@ public class NUSTitleLoader { return result; } // If we have more than one content, the index 0 is the FST. - Content fstContent = result.getTMD().getContentByIndex(0); - - InputStream fstContentEncryptedStream = dataProvider.readContentAsStream(fstContent); - - byte[] fstBytes = StreamUtils.getBytesFromStream(fstContentEncryptedStream, (int) fstContent.getEncryptedFileSize()); - - if (fstContent.isEncrypted()) { - AESDecryption aesDecryption = new AESDecryption(ticket.getDecryptedKey(), new byte[0x10]); - if (fstBytes.length % 0x10 != 0) { - throw new IOException("FST length is not align to 16"); - } - fstBytes = aesDecryption.decrypt(fstBytes); - } + Content fstContent = tmd.getContentByIndex(0); + byte[] fstBytes = dpp.readPlainDecryptedContent(fstContent, true); FST fst = FST.parseFST(fstBytes); - result.setFST(Optional.of(fst)); // The dataprovider may need the FST to calculate the offset of a content // on the partition. dataProvider.setFST(fst); - return result; + return NUSTitle.create(tmd, dpp, ticket, Optional.of(fst)); } + } diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderFST.java b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderFST.java index 80c6d91..4da0616 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderFST.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderFST.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.text.ParseException; import de.mas.wiiu.jnus.entities.fst.FSTEntry; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.NUSDataProviderFST; import de.mas.wiiu.jnus.interfaces.FSTDataProvider; @@ -36,7 +37,7 @@ public final class NUSTitleLoaderFST { NUSTitleConfig config = new NUSTitleConfig(); config.setCommonKey(commonKey); - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderFST(dataProvider, base)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderFST(dataProvider, base), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd)); } } diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java index 0653c58..1acd502 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocal.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.text.ParseException; import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.NUSDataProviderLocal; public final class NUSTitleLoaderLocal { @@ -43,7 +44,7 @@ public final class NUSTitleLoaderLocal { throw new IOException("Ticket was null and no commonKey was given"); } - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocal(inputPath), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd)); } } diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocalBackup.java b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocalBackup.java index 95a30cf..c57906a 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocalBackup.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderLocalBackup.java @@ -18,6 +18,7 @@ package de.mas.wiiu.jnus; import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.NUSDataProviderLocalBackup; public final class NUSTitleLoaderLocalBackup { @@ -35,7 +36,8 @@ public final class NUSTitleLoaderLocalBackup { config.setNoDecryption(true); } - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderLocalBackup(inputPath, titleVersion), + (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd)); } } diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java index ba7516e..0c3e03f 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderRemote.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.text.ParseException; import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.NUSDataProviderRemote; public final class NUSTitleLoaderRemote { @@ -53,7 +54,7 @@ public final class NUSTitleLoaderRemote { throw new IOException("Ticket was null and no commonKey was given"); } - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderRemote(version, titleID), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd)); } } diff --git a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java index d66b6b9..f8b2565 100644 --- a/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java +++ b/src/main/java/de/mas/wiiu/jnus/NUSTitleLoaderWoomy.java @@ -24,6 +24,7 @@ import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.NUSDataProviderWoomy; import de.mas.wiiu.jnus.implementations.woomy.WoomyInfo; import de.mas.wiiu.jnus.implementations.woomy.WoomyParser; @@ -38,10 +39,10 @@ public final class NUSTitleLoaderWoomy { NUSTitleConfig config = new NUSTitleConfig(); config.setTicketNeeded(false); - + WoomyInfo woomyInfo = WoomyParser.createWoomyInfo(new File(inputFile)); - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWoomy(woomyInfo)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWoomy(woomyInfo), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd)); } } diff --git a/src/main/java/de/mas/wiiu/jnus/WUDLoader.java b/src/main/java/de/mas/wiiu/jnus/WUDLoader.java index b51c75c..036d66a 100644 --- a/src/main/java/de/mas/wiiu/jnus/WUDLoader.java +++ b/src/main/java/de/mas/wiiu/jnus/WUDLoader.java @@ -24,6 +24,7 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle; import de.mas.wiiu.jnus.implementations.FSTDataProviderWUDDataPartition; import de.mas.wiiu.jnus.implementations.NUSDataProviderWUD; @@ -95,7 +96,7 @@ public final class WUDLoader { final NUSTitleConfig config = new NUSTitleConfig(); config.setCommonKey(commonKey); gamePartition.getTmd(); - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWUD(gamePartition, discReader), (dp, cd, en) -> new DefaultNUSDataProcessor(dp, cd)); } public static List getPartitonsAsFSTDataProvider(@NonNull WUDInfo wudInfo, byte[] commonKey) throws IOException, ParseException { diff --git a/src/main/java/de/mas/wiiu/jnus/WumadLoader.java b/src/main/java/de/mas/wiiu/jnus/WumadLoader.java index fc43e4c..d65bc27 100644 --- a/src/main/java/de/mas/wiiu/jnus/WumadLoader.java +++ b/src/main/java/de/mas/wiiu/jnus/WumadLoader.java @@ -11,6 +11,7 @@ import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; +import de.mas.wiiu.jnus.implementations.DefaultNUSDataProcessor; import de.mas.wiiu.jnus.implementations.FSTDataProviderNUSTitle; import de.mas.wiiu.jnus.implementations.FSTDataProviderWumadDataPartition; import de.mas.wiiu.jnus.implementations.NUSDataProviderWumad; @@ -40,7 +41,7 @@ public class WumadLoader { final NUSTitleConfig config = new NUSTitleConfig(); config.setCommonKey(commonKey); gamePartition.getTmd(); - return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile)); + return NUSTitleLoader.loadNusTitle(config, () -> new NUSDataProviderWumad(gamePartition, wudmadFile), (dp, cd, ce) -> new DefaultNUSDataProcessor(dp, cd)); } public static List getPartitonsAsFSTDataProvider(@NonNull WumadInfo wumadInfo, byte[] commonKey) throws IOException, ParseException { @@ -57,7 +58,6 @@ public class WumadLoader { } return result; - } } diff --git a/src/main/java/de/mas/wiiu/jnus/entities/content/Content.java b/src/main/java/de/mas/wiiu/jnus/entities/content/Content.java index ac2a645..9fdc5fb 100644 --- a/src/main/java/de/mas/wiiu/jnus/entities/content/Content.java +++ b/src/main/java/de/mas/wiiu/jnus/entities/content/Content.java @@ -37,6 +37,7 @@ public class Content implements Comparable { public static final short CONTENT_FLAG_UNKWN1 = 0x4000; public static final short CONTENT_HASHED = 0x0002; public static final short CONTENT_ENCRYPTED = 0x0001; + public static final int CONTENT_SIZE = 0x30; @Getter private final int ID; @@ -127,7 +128,7 @@ public class Content implements Comparable { */ public long getDecryptedFileSize() { if (isHashed()) { - return getEncryptedFileSize() / 0x10000 * 0xFC00; + return (getEncryptedFileSize() / 0x10000) * 0xFC00; } else { return getEncryptedFileSize(); } diff --git a/src/main/java/de/mas/wiiu/jnus/entities/fst/FST.java b/src/main/java/de/mas/wiiu/jnus/entities/fst/FST.java index 32293ab..2f1d8c0 100644 --- a/src/main/java/de/mas/wiiu/jnus/entities/fst/FST.java +++ b/src/main/java/de/mas/wiiu/jnus/entities/fst/FST.java @@ -61,7 +61,7 @@ public final class FST { int contentCount = ByteUtils.getIntFromBytes(fstData, 0x08); FST result = new FST(sectorSize, contentCount); - + int contentfst_offset = 0x20; int contentfst_size = 0x20 * contentCount; @@ -71,8 +71,8 @@ public final class FST { int fst_size = fileCount * 0x10; int nameOff = fst_offset + fst_size; - int nameSize = nameOff + 1; - + int nameSize = fstData.length - nameOff; + // Get list with null-terminated Strings. Ends with \0\0. for (int i = nameOff; i < fstData.length - 1; i++) { if (fstData[i] == 0 && fstData[i + 1] == 0) { @@ -88,7 +88,7 @@ public final class FST { byte fstSection[] = Arrays.copyOfRange(fstData, fst_offset, fst_offset + fst_size); byte nameSection[] = Arrays.copyOfRange(fstData, nameOff, nameOff + nameSize); - + FSTEntry root = result.getRoot(); FSTService.parseFST(root, fstSection, nameSection, sectorSize); diff --git a/src/main/java/de/mas/wiiu/jnus/entities/fst/FSTService.java b/src/main/java/de/mas/wiiu/jnus/entities/fst/FSTService.java index daa6cc3..52de2f1 100644 --- a/src/main/java/de/mas/wiiu/jnus/entities/fst/FSTService.java +++ b/src/main/java/de/mas/wiiu/jnus/entities/fst/FSTService.java @@ -104,7 +104,6 @@ public final class FSTService { while ((nameOffset + j) < namesSection.length && namesSection[nameOffset + j] != 0) { j++; } - return (new String(Arrays.copyOfRange(namesSection, nameOffset, nameOffset + j))); } diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/DefaultNUSDataProcessor.java b/src/main/java/de/mas/wiiu/jnus/implementations/DefaultNUSDataProcessor.java new file mode 100644 index 0000000..436119f --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/implementations/DefaultNUSDataProcessor.java @@ -0,0 +1,352 @@ +package de.mas.wiiu.jnus.implementations; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Optional; + +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.interfaces.ContentDecryptor; +import de.mas.wiiu.jnus.interfaces.NUSDataProcessor; +import de.mas.wiiu.jnus.interfaces.NUSDataProvider; +import de.mas.wiiu.jnus.utils.ByteArrayBuffer; +import de.mas.wiiu.jnus.utils.CheckSumWrongException; +import de.mas.wiiu.jnus.utils.HashUtil; +import de.mas.wiiu.jnus.utils.PipedInputStreamWithException; +import de.mas.wiiu.jnus.utils.StreamUtils; +import de.mas.wiiu.jnus.utils.Utils; +import lombok.extern.java.Log; + +@Log +public class DefaultNUSDataProcessor implements NUSDataProcessor { + protected final NUSDataProvider dataProvider; + private final Optional decryptor; + + public DefaultNUSDataProcessor(NUSDataProvider dataProvider, Optional decryptor) { + this.dataProvider = dataProvider; + this.decryptor = decryptor; + } + + @Override + public InputStream readContentAsStream(Content c, long offset, long size) throws IOException { + return dataProvider.readRawContentAsStream(c, offset, size); + } + + @Override + public InputStream readDecryptedContentAsStream(Content c, long offset, long size) throws IOException { + if (!c.isEncrypted()) { + return dataProvider.readRawContentAsStream(c, offset, size); + } + + PipedOutputStream out = new PipedOutputStream(); + PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000); + + new Thread(() -> { + try { + readDecryptedContentToStream(out, c, offset, size); + in.throwException(null); + } catch (Exception e) { + in.throwException(e); + try { + out.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + }).start(); + + return in; + } + + @Override + public long readDecryptedContentToStream(OutputStream out, Content c, long offset, long size) throws IOException { + if (!c.isEncrypted()) { + InputStream in = dataProvider.readRawContentAsStream(c, offset, size); + return StreamUtils.saveInputStreamToOutputStream(in, out, size); + } + + if (!decryptor.isPresent()) { + throw new IOException("Decryptor was null. Maybe the ticket is missing?"); + } + + if (c.isHashed()) { + long stream_offset = (offset / 0x10000) * 0x10000; + + InputStream in = dataProvider.readRawContentAsStream(c, stream_offset, size + offset - stream_offset); + + return decryptor.get().readDecryptedContentToStreamHashed(in, out, offset, size, offset - stream_offset, dataProvider.getContentH3Hash(c).get()); + } else { + byte[] IV = new byte[0x10]; + IV[0] = (byte) ((c.getIndex() >> 8) & 0xFF); + IV[1] = (byte) (c.getIndex() & 0xFF); + + long streamOffset = (offset / 16) * 16; + long streamFilesize = size; + + // if we have an offset we can't calculate the hash anymore + // we need a new IV + if (streamOffset > 15) { + streamFilesize = size; + + streamOffset -= 16; + streamFilesize += 16; + + // We need to get the current IV as soon as we get the InputStream. + IV = null; + } else if ((offset > 0 && offset < 16) && size < 16) { + streamFilesize = 16; + } + + long curStreamOffset = streamOffset; + + InputStream in = dataProvider.readRawContentAsStream(c, streamOffset, streamFilesize); + if (IV == null) { + // If we read with an offset > 16 we need the previous 16 bytes because they are the IV. + // The input stream has been prepared to start 16 bytes earlier on this case. + int toRead = 16; + byte[] data = new byte[toRead]; + int readTotal = 0; + while (readTotal < toRead) { + int res = in.read(data, readTotal, toRead - readTotal); + if (res < 0) { + StreamUtils.closeAll(in, out); + return -1; + } + readTotal += res; + } + IV = Arrays.copyOfRange(data, 0, toRead); + curStreamOffset = streamOffset + 16; + } + long res = decryptor.get().readDecryptedContentToStreamNonHashed(in, out, curStreamOffset, size, offset - curStreamOffset, IV); + + return res; + } + } + + @Override + public InputStream readPlainDecryptedContentAsStream(Content c, long offset, long size, boolean forceCheckHash) throws IOException { + PipedOutputStream out = new PipedOutputStream(); + PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x10000); + + new Thread(() -> { + try { + readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash); + in.throwException(null); + } catch (Exception e) { + in.throwException(e); + try { + in.close(); + } catch (IOException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + } + }).start(); + + return in; + } + + @Override + public long readPlainDecryptedContentToStream(OutputStream out, Content c, long offset, long size, boolean forceCheckHash) throws IOException { + if (c.isHashed()) { + long payloadOffset = offset; + long streamOffset = payloadOffset; + long streamFilesize = 0; + + streamOffset = (payloadOffset / 0xFC00) * 0x10000; + long offsetInBlock = payloadOffset - ((streamOffset / 0x10000) * 0xFC00); + if (offsetInBlock + size < 0xFC00) { + streamFilesize = 0x10000L; + } else { + long curVal = 0x10000; + long missing = (size - (0xFC00 - offsetInBlock)); + + curVal += (missing / 0xFC00) * 0x10000; + + if (missing % 0xFC00 > 0) { + curVal += 0x10000; + } + + streamFilesize = curVal; + } + + InputStream in = readDecryptedContentAsStream(c, streamOffset, streamFilesize); + + try { + return processHashedStream(in, out, (int) (offset / 0xFC00), size, offsetInBlock, dataProvider.getContentH3Hash(c).get()); + } catch (NoSuchAlgorithmException | CheckSumWrongException e) { + throw new IOException(e); + } + } else { + InputStream in = readDecryptedContentAsStream(c, offset, size); + + byte[] hash = null; + if (forceCheckHash) { + hash = c.getSHA2Hash(); + } + + try { + return processNonHashedStream(in, out, offset, size, hash, c.getEncryptedFileSize()); + } catch (CheckSumWrongException e) { + throw new IOException(e); + } + } + } + + private long processNonHashedStream(InputStream inputStream, OutputStream outputStream, long payloadOffset, long filesize, byte[] hash, + long expectedSizeForHash) throws IOException, CheckSumWrongException { + MessageDigest sha1 = null; + MessageDigest sha1fallback = null; + + if (hash != null) { + try { + sha1 = MessageDigest.getInstance("SHA1"); + sha1fallback = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + int BLOCKSIZE = 0x8000; + + byte[] blockBuffer = new byte[BLOCKSIZE]; + + int inBlockBuffer; + long written = 0; + long writtenFallback = 0; + + try { + ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE); + + // We can only decrypt multiples of 16. So we need to align it. + long toRead = Utils.align(filesize, 16); + + do { + int curReadSize = BLOCKSIZE; + if (toRead < BLOCKSIZE) { + curReadSize = (int) toRead; + } + inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, curReadSize); + if (inBlockBuffer <= 0) { + break; + } + + byte[] output = blockBuffer; + + int toWrite = inBlockBuffer; + + if ((written + inBlockBuffer) > filesize) { + toWrite = (int) (filesize - written); + } + + written += toWrite; + toRead -= toWrite; + + outputStream.write(output, 0, toWrite); + + if (sha1 != null && sha1fallback != null) { + sha1.update(output, 0, toWrite); + + // In some cases it's using the hash of the whole .app file instead of the part + // that's been actually used. + long toFallback = inBlockBuffer; + if (writtenFallback + toFallback > expectedSizeForHash) { + toFallback = expectedSizeForHash - writtenFallback; + } + sha1fallback.update(output, 0, (int) toFallback); + writtenFallback += toFallback; + } + + if (written >= filesize && hash == null) { + break; + } + } while (inBlockBuffer == BLOCKSIZE); + + if (sha1 != null && sha1fallback != null) { + long missingInHash = expectedSizeForHash - writtenFallback; + if (missingInHash > 0) { + sha1fallback.update(new byte[(int) missingInHash]); + } + + byte[] calculated_hash1 = sha1.digest(); + byte[] calculated_hash2 = sha1fallback.digest(); + byte[] expected_hash = hash; + if (!Arrays.equals(calculated_hash1, expected_hash) && !Arrays.equals(calculated_hash2, expected_hash)) { + throw new CheckSumWrongException("hash checksum failed ", calculated_hash1, expected_hash); + } else { + log.fine("Hash DOES match saves output stream."); + } + + } + } finally { + StreamUtils.closeAll(inputStream, outputStream); + } + return written; + } + + private long processHashedStream(InputStream inputStream, OutputStream outputStream, int block, long filesize, long payloadOffset, byte[] h3_hashes) + throws IOException, NoSuchAlgorithmException, CheckSumWrongException { + int BLOCKSIZE = 0x10000; + int HASHBLOCKSIZE = 0xFC00; + int HASHSIZE = BLOCKSIZE - HASHBLOCKSIZE; + + long curBlock = block; + + byte[] blockBuffer = new byte[BLOCKSIZE]; + ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE); + long written = 0; + int inBlockBuffer = 0; + + long writeOffset = payloadOffset; + + try { + do { + inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, BLOCKSIZE); + if (inBlockBuffer < 0) { + break; + } + + if (inBlockBuffer != BLOCKSIZE) { + throw new IOException("buffer was not " + BLOCKSIZE + " bytes"); + } + + byte[] hashes = null; + byte[] output = null; + + hashes = Arrays.copyOfRange(blockBuffer, 0, HASHSIZE); + output = Arrays.copyOfRange(blockBuffer, HASHSIZE, BLOCKSIZE); + + HashUtil.checkFileChunkHashes(hashes, h3_hashes, output, (int) curBlock); + + try { + long writeLength = Math.min((output.length - writeOffset), (filesize - written)); + outputStream.write(output, (int) writeOffset, (int) writeLength); + written += writeLength; + } catch (IOException e) { + if (e.getMessage().equals("Pipe closed")) { + break; + } + e.printStackTrace(); + throw e; + } + writeOffset = 0; + + curBlock++; + } while (written < filesize); + log.finest("Decryption okay"); + } finally { + StreamUtils.closeAll(inputStream, outputStream); + } + return written > 0 ? written : -1; + } + + @Override + public NUSDataProvider getDataProvider() { + return dataProvider; + } + +} diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderNUSTitle.java b/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderNUSTitle.java index cca3f21..b7eecf2 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderNUSTitle.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderNUSTitle.java @@ -17,10 +17,7 @@ package de.mas.wiiu.jnus.implementations; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.stream.Collectors; import de.mas.wiiu.jnus.NUSTitle; @@ -28,22 +25,20 @@ import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.fst.FSTEntry; import de.mas.wiiu.jnus.interfaces.FSTDataProvider; import de.mas.wiiu.jnus.interfaces.HasNUSTitle; -import de.mas.wiiu.jnus.interfaces.NUSDataProvider; -import de.mas.wiiu.jnus.utils.CheckSumWrongException; -import de.mas.wiiu.jnus.utils.StreamUtils; -import de.mas.wiiu.jnus.utils.Utils; -import de.mas.wiiu.jnus.utils.cryptography.NUSDecryption; +import de.mas.wiiu.jnus.interfaces.NUSDataProcessor; import lombok.Getter; import lombok.Setter; import lombok.extern.java.Log; @Log public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle { + private final NUSDataProcessor dataProcessor; private final NUSTitle title; private final FSTEntry rootEntry; @Getter @Setter private String name; public FSTDataProviderNUSTitle(NUSTitle title) throws IOException { + this.dataProcessor = title.getDataProcessor(); this.title = title; this.name = String.format("%016X", title.getTMD().getTitleID()); @@ -59,157 +54,25 @@ public class FSTDataProviderNUSTitle implements FSTDataProvider, HasNUSTitle { throw new IOException("No FST root entry was found"); } } - + @Override public FSTEntry getRoot() { return rootEntry; } @Override - public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException { - try { - return decryptFSTEntryToStream(entry, out, offset, size); - } catch (CheckSumWrongException | NoSuchAlgorithmException e) { - throw new IOException(e); - } - } - - private boolean decryptFSTEntryToStreamHashed(FSTEntry entry, OutputStream outputStream, long offset, long size) - throws IOException, CheckSumWrongException, NoSuchAlgorithmException { - Content c = title.getTMD().getContentByIndex(entry.getContentIndex()); - - long payloadOffset = entry.getFileOffset() + offset; - long streamOffset = payloadOffset; - long streamFilesize = 0; - - streamOffset = (payloadOffset / 0xFC00) * 0x10000; - long offsetInBlock = payloadOffset - ((streamOffset / 0x10000) * 0xFC00); - if (offsetInBlock + size < 0xFC00) { - streamFilesize = 0x10000L; - } else { - long curVal = 0x10000; - long missing = (size - (0xFC00 - offsetInBlock)); - - curVal += (missing / 0xFC00) * 0x10000; - - if (missing % 0xFC00 > 0) { - curVal += 0x10000; - } - - streamFilesize = curVal; - } - - NUSDataProvider dataProvider = title.getDataProvider(); - InputStream in = dataProvider.readContentAsStream(c, streamOffset, streamFilesize); - - NUSDecryption nusdecryption = new NUSDecryption(title.getTicket().get()); - - return nusdecryption.decryptStreamsHashed(in, outputStream, payloadOffset, size, dataProvider.getContentH3Hash(c)); - } - - private boolean decryptFSTEntryToStreamNonHashed(FSTEntry entry, OutputStream outputStream, long offset, long size) - throws IOException, CheckSumWrongException, NoSuchAlgorithmException { - - Content c = title.getTMD().getContentByIndex(entry.getContentIndex()); - - byte[] IV = new byte[0x10]; - IV[0] = (byte) ((c.getIndex() >> 8) & 0xFF); - IV[1] = (byte) (c.getIndex() & 0xFF); - - long payloadOffset = entry.getFileOffset() + offset; - long streamOffset = payloadOffset; - long streamFilesize = c.getEncryptedFileSize(); - - // if we have an offset we can't calculate the hash anymore - // we need a new IV - if (streamOffset > 0) { - streamFilesize = size; - - streamOffset -= 16; - streamFilesize += 16; - - // We need to get the current IV as soon as we get the InputStream. - IV = null; - } - - NUSDataProvider dataProvider = title.getDataProvider(); - InputStream in = dataProvider.readContentAsStream(c, streamOffset, streamFilesize); - - if (IV == null) { - // If we read with an offset > 16 we need the previous 16 bytes because they are the IV. - // The input stream has been prepared to start 16 bytes earlier on this case. - int toRead = 16; - byte[] data = new byte[toRead]; - int readTotal = 0; - while (readTotal < toRead) { - int res = in.read(data, readTotal, toRead - readTotal); - if (res < 0) { - // This should NEVER happen. - throw new IOException(); - } - readTotal += res; - } - IV = Arrays.copyOfRange(data, 0, toRead); - } - NUSDecryption nusdecryption = new NUSDecryption(title.getTicket().get()); - - return nusdecryption.decryptStreamsNonHashed(in, outputStream, payloadOffset, size, c, IV, size != entry.getFileSize()); - } - - private boolean decryptFSTEntryToStream(FSTEntry entry, OutputStream outputStream, long offset, long size) - throws IOException, CheckSumWrongException, NoSuchAlgorithmException { + public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException { if (entry.isNotInPackage()) { if (entry.isNotInPackage()) { log.info("Decryption not possible because the FSTEntry is not in this package"); } - outputStream.close(); - return false; - } - if (offset % 16 != 0) { - throw new IOException("The offset for decryption need to be aligned to 16"); + out.close(); + return -1; } Content c = title.getTMD().getContentByIndex(entry.getContentIndex()); - try { - if (c.isEncrypted()) { - if (!title.getTicket().isPresent()) { - log.info("Decryption not possible because no ticket was set."); - outputStream.close(); - return false; - } - if (c.isHashed()) { - return decryptFSTEntryToStreamHashed(entry, outputStream, offset, size); - } else { - return decryptFSTEntryToStreamNonHashed(entry, outputStream, offset, size); - } - } else { - InputStream in = title.getDataProvider().readContentAsStream(c, offset, size); - - try { - StreamUtils.saveInputStreamToOutputStreamWithHash(in, outputStream, size, c.getSHA2Hash(), c.getEncryptedFileSize(), - size != entry.getFileSize()); - return true; - } finally { - StreamUtils.closeAll(in, outputStream); - } - } - } catch (CheckSumWrongException e) { - if (c.isUNKNWNFlag1Set()) { - log.info("Hash doesn't match. But file is optional. Don't worry."); - } else { - StringBuilder sb = new StringBuilder(); - sb.append("Hash doesn't match").append(System.lineSeparator()); - sb.append("Detailed info:").append(System.lineSeparator()); - sb.append(entry).append(System.lineSeparator()); - sb.append(String.format("%016x", title.getTMD().getTitleID())); - sb.append(e.getMessage() + " Calculated Hash: " + Utils.ByteArrayToString(e.getGivenHash()) + ", expected hash: " - + Utils.ByteArrayToString(e.getExpectedHash())); - log.info(sb.toString()); - throw e; - } - } - return false; + return dataProcessor.readPlainDecryptedContentToStream(out, c, offset + entry.getFileOffset(), size, size == entry.getFileSize()); } @Override diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWUDDataPartition.java b/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWUDDataPartition.java index 708bec3..c2fc82f 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWUDDataPartition.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWUDDataPartition.java @@ -49,7 +49,7 @@ public class FSTDataProviderWUDDataPartition implements FSTDataProvider { } @Override - public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException { + public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException { ContentFSTInfo info = FSTUtils.getFSTInfoForContent(partition.getFST(), entry.getContentIndex()) .orElseThrow(() -> new IOException("Failed to find FSTInfo")); if (titleKey == null) { diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWumadDataPartition.java b/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWumadDataPartition.java index df084b7..23dc7ad 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWumadDataPartition.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/FSTDataProviderWumadDataPartition.java @@ -32,15 +32,15 @@ public class FSTDataProviderWumadDataPartition implements FSTDataProvider { } @Override - public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException { - StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size); - return true; + public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException { + return StreamUtils.saveInputStreamToOutputStream(readFileAsStream(entry, offset, size), out, size); } @Override public InputStream readFileAsStream(FSTEntry entry, long offset, long size) throws IOException { ZipEntry zipEntry = zipFile.stream() - .filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex()))).findFirst() + .filter(e -> e.getName().equals(String.format("p%s.s%04d.00000000.app", dataPartition.getPartitionName(), entry.getContentIndex()))) + .findFirst() .orElseThrow(() -> new FileNotFoundException()); InputStream in = zipFile.getInputStream(zipEntry); diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderFST.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderFST.java index ee7835e..b700db8 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderFST.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderFST.java @@ -26,6 +26,7 @@ import java.util.Optional; import de.mas.wiiu.jnus.Settings; import de.mas.wiiu.jnus.entities.content.Content; import de.mas.wiiu.jnus.entities.fst.FSTEntry; +import de.mas.wiiu.jnus.interfaces.ContentDecryptor; import de.mas.wiiu.jnus.interfaces.FSTDataProvider; import de.mas.wiiu.jnus.interfaces.NUSDataProvider; import de.mas.wiiu.jnus.utils.FSTUtils; @@ -39,12 +40,12 @@ public class NUSDataProviderFST implements NUSDataProvider { this.fstDataProvider = fstDataProvider; } - public NUSDataProviderFST(FSTDataProvider fstDataProvider) { + public NUSDataProviderFST(FSTDataProvider fstDataProvider, ContentDecryptor decryptor) { this(fstDataProvider, fstDataProvider.getRoot()); } @Override - public InputStream readContentAsStream(Content content, long offset, long size) throws IOException { + public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException { String filename = content.getFilename(); Optional contentFileOpt = FSTUtils.getChildOfDirectory(base, filename); FSTEntry contentFile = contentFileOpt.orElseThrow(() -> new FileNotFoundException(filename + " was not found.")); diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocal.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocal.java index 1c3ca14..03034c1 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocal.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocal.java @@ -45,7 +45,7 @@ public final class NUSDataProviderLocal implements NUSDataProvider { } @Override - public InputStream readContentAsStream(Content content, long offset, long size) throws IOException { + public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException { File filepath = FileUtils.getFileIgnoringFilenameCases(getLocalPath(), content.getFilename()); if (filepath == null || !filepath.exists()) { String errormsg = "Couldn't open \"" + getLocalPath() + File.separator + content.getFilename() + "\", file does not exist"; diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocalBackup.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocalBackup.java index 9b16f84..0417bb2 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocalBackup.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderLocalBackup.java @@ -40,7 +40,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider { public NUSDataProviderLocalBackup(String localPath, short version) { this.localPath = localPath; - this.titleVersion = version; + this.titleVersion = version; } private String getFilePathOnDisk(Content c) { @@ -48,7 +48,7 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider { } @Override - public InputStream readContentAsStream(Content content, long offset, long size) throws IOException { + public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException { File filepath = new File(getFilePathOnDisk(content)); if (!filepath.exists()) { throw new FileNotFoundException(filepath.getAbsolutePath() + " was not found."); @@ -62,7 +62,9 @@ public class NUSDataProviderLocalBackup implements NUSDataProvider { public Optional getContentH3Hash(Content content) throws IOException { String h3Path = getLocalPath() + File.separator + String.format("%08X.h3", content.getID()); File h3File = new File(h3Path); - + if (!h3File.exists()) { + throw new FileNotFoundException(h3File.getAbsolutePath() + " was not found."); + } return Optional.of(Files.readAllBytes(h3File.toPath())); } diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java index a34eed8..8872a45 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderRemote.java @@ -39,7 +39,7 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable { } @Override - public InputStream readContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException { + public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException { NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); return downloadService.getInputStreamForURL(getRemoteURL(content), fileOffsetBlock, size); } @@ -56,7 +56,6 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable { if (resOpt == null) { NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); String url = getRemoteURL(content) + Settings.H3_EXTENTION; - System.out.println(url); byte[] res = downloadService.downloadToByteArray(url); if (res == null || res.length == 0) { @@ -69,29 +68,39 @@ public class NUSDataProviderRemote implements NUSDataProvider, Parallelizable { return resOpt; } + Optional tmdCache = null; + @Override public Optional getRawTMD() throws IOException { - NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); + if (tmdCache == null) { + NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); - long titleID = getTitleID(); - int version = getVersion(); + long titleID = getTitleID(); + int version = getVersion(); - byte[] res = downloadService.downloadTMDToByteArray(titleID, version); + byte[] res = downloadService.downloadTMDToByteArray(titleID, version); - if (res == null || res.length == 0) { - return Optional.empty(); + if (res == null || res.length == 0) { + return Optional.empty(); + } + tmdCache = Optional.of(res); } - return Optional.of(res); + return tmdCache; } + Optional ticketCache = null; + @Override public Optional getRawTicket() throws IOException { - NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); - byte[] res = downloadService.downloadTicketToByteArray(titleID); - if (res == null || res.length == 0) { - return Optional.empty(); + if (ticketCache == null) { + NUSDownloadService downloadService = NUSDownloadService.getDefaultInstance(); + byte[] res = downloadService.downloadTicketToByteArray(titleID); + if (res == null || res.length == 0) { + return Optional.empty(); + } + ticketCache = Optional.of(res); } - return Optional.of(res); + return ticketCache; } @Override diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java index 445994f..1f5f43b 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWUD.java @@ -56,7 +56,7 @@ public class NUSDataProviderWUD implements NUSDataProvider { } @Override - public InputStream readContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException { + public InputStream readRawContentAsStream(Content content, long fileOffsetBlock, long size) throws IOException { WUDDiscReader discReader = getDiscReader(); long offset = getOffsetInWUD(content) + fileOffsetBlock; diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java index 424ccc2..ba2117c 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWoomy.java @@ -16,7 +16,6 @@ ****************************************************************************/ package de.mas.wiiu.jnus.implementations; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -46,7 +45,7 @@ public class NUSDataProviderWoomy implements NUSDataProvider { } @Override - public InputStream readContentAsStream(@NonNull Content content, long offset, long size) throws IOException { + public InputStream readRawContentAsStream(@NonNull Content content, long offset, long size) throws IOException { WoomyZipFile zipFile = getSharedWoomyZipFile(); ZipEntry entry = getWoomyInfo().getContentFiles().get(content.getFilename().toLowerCase()); if (entry == null) { diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWumad.java b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWumad.java index 29970a0..01caa73 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWumad.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/NUSDataProviderWumad.java @@ -61,7 +61,7 @@ public class NUSDataProviderWumad implements NUSDataProvider { } @Override - public InputStream readContentAsStream(Content content, long offset, long size) throws IOException { + public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException { ZipEntry entry = files.values().stream().filter(e -> e.getName().startsWith("p" + partition.getPartitionName() + ".")) .filter(e -> e.getName().endsWith(content.getFilename().toLowerCase())).findFirst().orElseThrow(() -> new FileNotFoundException()); InputStream in = wumad.getInputStream(entry); diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java index a01bc63..3439d13 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReader.java @@ -54,7 +54,7 @@ public abstract class WUDDiscReader { return out.toByteArray(); } - public abstract boolean readEncryptedToStream(OutputStream out, long offset, long size) throws IOException; + public abstract long readEncryptedToStream(OutputStream out, long offset, long size) throws IOException; public InputStream readEncryptedToStream(long offset, long size) throws IOException { PipedOutputStream out = new PipedOutputStream(); @@ -72,6 +72,23 @@ public abstract class WUDDiscReader { return in; } + public InputStream readDecryptedToStream(long offset, long fileOffset, long size, byte[] key, byte[] IV, + boolean useFixedIV) throws IOException { + PipedOutputStream out = new PipedOutputStream(); + PipedInputStreamWithException in = new PipedInputStreamWithException(out, 0x8000); + + new Thread(() -> { + try { + readDecryptedToOutputStream(out, offset, fileOffset, size, key, IV, useFixedIV); + in.throwException(null); + } catch (Exception e) { + in.throwException(e); + } + }).start(); + + return in; + } + /** * * @param readOffset @@ -93,7 +110,7 @@ public abstract class WUDDiscReader { return decryptedChunk; } - public boolean readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV, + public long readDecryptedToOutputStream(OutputStream outputStream, long clusterOffset, long fileOffset, long size, byte[] key, byte[] IV, boolean useFixedIV) throws IOException { byte[] usedIV = null; if (useFixedIV) { @@ -153,7 +170,7 @@ public abstract class WUDDiscReader { StreamUtils.closeAll(outputStream); } - return totalread >= size; + return totalread; } /** diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java index e64be60..381b8ac 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderCompressed.java @@ -35,7 +35,7 @@ public class WUDDiscReaderCompressed extends WUDDiscReader { * Expects the .wux format by Exzap. You can more infos about it here. https://gbatemp.net/threads/wii-u-image-wud-compression-tool.397901/ */ @Override - public boolean readEncryptedToStream(OutputStream out, long offset, long size) throws IOException { + public long readEncryptedToStream(OutputStream out, long offset, long size) throws IOException { // make sure there is no out-of-bounds read WUDImageCompressedInfo info = getImage().getCompressedInfo(); @@ -91,6 +91,6 @@ public class WUDDiscReaderCompressed extends WUDDiscReader { } finally { StreamUtils.closeAll(input, out); } - return usedSize == 0; + return size - usedSize; } } diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderSplitted.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderSplitted.java index 7a34c6f..0973ba9 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderSplitted.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderSplitted.java @@ -37,7 +37,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader { } @Override - public boolean readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException { + public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException { RandomAccessFile input = getFileByOffset(offset); int bufferSize = 0x8000; @@ -86,7 +86,7 @@ public class WUDDiscReaderSplitted extends WUDDiscReader { input.close(); outputStream.close(); - return totalread >= size; + return totalread; } private int getFilePartByOffset(long offset) { diff --git a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java index 5e55ecf..ab497b5 100644 --- a/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java +++ b/src/main/java/de/mas/wiiu/jnus/implementations/wud/reader/WUDDiscReaderUncompressed.java @@ -31,7 +31,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader { } @Override - public boolean readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException { + public long readEncryptedToStream(OutputStream outputStream, long offset, long size) throws IOException { FileInputStream input = new FileInputStream(getImage().getFileHandle()); @@ -62,7 +62,7 @@ public class WUDDiscReaderUncompressed extends WUDDiscReader { } while (totalread < size); input.close(); outputStream.close(); - return totalread >= size; + return totalread; } @Override diff --git a/src/main/java/de/mas/wiiu/jnus/interfaces/ContentDecryptor.java b/src/main/java/de/mas/wiiu/jnus/interfaces/ContentDecryptor.java new file mode 100644 index 0000000..f6c808d --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/interfaces/ContentDecryptor.java @@ -0,0 +1,24 @@ +package de.mas.wiiu.jnus.interfaces; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface ContentDecryptor { + + /** + * + * @param in InputStream of the Encrypted Data with hashed + * @param out OutputStream of the decrypted data with hashes + * @param offset absolute offset in this Content stream + * @param size size of the payload that will be written to the outputstream + * @param payloadOffset relative offset to the start of the inputstream + * @param h3_hashes level 3 hashes of the content file + * @return + * @throws IOException + */ + long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes) throws IOException; + + long readDecryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV) throws IOException; + +} diff --git a/src/main/java/de/mas/wiiu/jnus/interfaces/ContentEncryptor.java b/src/main/java/de/mas/wiiu/jnus/interfaces/ContentEncryptor.java new file mode 100644 index 0000000..d7f451a --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/interfaces/ContentEncryptor.java @@ -0,0 +1,15 @@ +package de.mas.wiiu.jnus.interfaces; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import de.mas.wiiu.jnus.utils.IVCache; + +public interface ContentEncryptor { + + long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException; + + long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, IVCache ivcache) throws IOException; + +} diff --git a/src/main/java/de/mas/wiiu/jnus/interfaces/FSTDataProvider.java b/src/main/java/de/mas/wiiu/jnus/interfaces/FSTDataProvider.java index 5aee03f..f5fdae8 100644 --- a/src/main/java/de/mas/wiiu/jnus/interfaces/FSTDataProvider.java +++ b/src/main/java/de/mas/wiiu/jnus/interfaces/FSTDataProvider.java @@ -56,20 +56,25 @@ public interface FSTDataProvider { in.throwException(null); } catch (Exception e) { in.throwException(e); + try { + out.close(); + } catch (IOException e1) { + e1.printStackTrace(); + } } }).start(); return in; } - default public boolean readFileToStream(OutputStream out, FSTEntry entry) throws IOException { + default public long readFileToStream(OutputStream out, FSTEntry entry) throws IOException { return readFileToStream(out, entry, 0, entry.getFileSize()); } - default public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException { + default public long readFileToStream(OutputStream out, FSTEntry entry, long offset) throws IOException { return readFileToStream(out, entry, offset, entry.getFileSize()); } - public boolean readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException; + public long readFileToStream(OutputStream out, FSTEntry entry, long offset, long size) throws IOException; } diff --git a/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProcessor.java b/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProcessor.java new file mode 100644 index 0000000..3e8922c --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProcessor.java @@ -0,0 +1,111 @@ +package de.mas.wiiu.jnus.interfaces; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.utils.StreamUtils; + +public interface NUSDataProcessor { + + public NUSDataProvider getDataProvider(); + + default public byte[] readContent(Content c) throws IOException { + return readContent(c, 0, c.getEncryptedFileSizeAligned()); + } + + default public byte[] readContent(Content c, long offset, long size) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + readContentToStream(out, c, offset, size); + + return out.toByteArray(); + } + + default public InputStream readContentAsStream(Content c) throws IOException { + return readContentAsStream(c, 0, c.getEncryptedFileSizeAligned()); + } + + public InputStream readContentAsStream(Content c, long offset, long size) throws IOException; + + default public long readContentToStream(OutputStream out, Content entry) throws IOException { + return readContentToStream(out, entry, 0, entry.getEncryptedFileSizeAligned()); + } + + default public long readContentToStream(OutputStream out, Content entry, long offset) throws IOException { + return readContentToStream(out, entry, offset, entry.getEncryptedFileSizeAligned()); + } + + default public long readContentToStream(OutputStream out, Content c, long offset, long size) throws IOException { + InputStream in = readContentAsStream(c, offset, size); + return StreamUtils.saveInputStreamToOutputStream(in, out, size); + } + + default public byte[] readDecryptedContent(Content c) throws IOException { + return readDecryptedContent(c, 0, c.getEncryptedFileSizeAligned()); + } + + default public byte[] readDecryptedContent(Content c, long offset, long size) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + long len = readDecryptedContentToStream(out, c, offset, size); + if(len < 0) { + return new byte[0]; + } + return out.toByteArray(); + } + + default public InputStream readDecryptedContentAsStream(Content c) throws IOException { + return readDecryptedContentAsStream(c, 0, c.getEncryptedFileSizeAligned()); + } + + public InputStream readDecryptedContentAsStream(Content c, long offset, long size) throws IOException; + + default public long readDecryptedContentToStream(OutputStream out, Content c) throws IOException { + return readDecryptedContentToStream(out, c, 0, c.getEncryptedFileSizeAligned()); + } + + default public long readDecryptedContentToStream(OutputStream out, Content c, long offset) throws IOException { + return readDecryptedContentToStream(out, c, offset, c.getEncryptedFileSizeAligned()); + } + + default public long readDecryptedContentToStream(OutputStream out, Content c, long offset, long size) throws IOException { + InputStream in = readDecryptedContentAsStream(c, offset, size); + return StreamUtils.saveInputStreamToOutputStream(in, out, size); + } + + default public byte[] readPlainDecryptedContent(Content c, boolean forceCheckHash) throws IOException { + return readPlainDecryptedContent(c, 0, c.getEncryptedFileSizeAligned(), forceCheckHash); + } + + default public byte[] readPlainDecryptedContent(Content c, long offset, long size, boolean forceCheckHash) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + long len = readPlainDecryptedContentToStream(out, c, offset, size, forceCheckHash); + if(len < 0) { + return new byte[0]; + } + + return out.toByteArray(); + } + + default public InputStream readPlainDecryptedContentAsStream(Content c, boolean forceCheckHash) throws IOException { + return readPlainDecryptedContentAsStream(c, 0, c.getEncryptedFileSizeAligned(), forceCheckHash); + } + + public InputStream readPlainDecryptedContentAsStream(Content c, long offset, long size, boolean forceCheckHash) throws IOException; + + default public long readPlainDecryptedContentToStream(OutputStream out, Content entry, boolean forceCheckHash) throws IOException { + return readPlainDecryptedContentToStream(out, entry, 0, entry.getEncryptedFileSizeAligned(), forceCheckHash); + } + + default public long readPlainDecryptedContentToStream(OutputStream out, Content entry, long offset, boolean forceCheckHash) throws IOException { + return readPlainDecryptedContentToStream(out, entry, offset, entry.getEncryptedFileSizeAligned(), forceCheckHash); + } + + default public long readPlainDecryptedContentToStream(OutputStream out, Content c, long offset, long size, boolean forceCheckHash) throws IOException { + InputStream in = readPlainDecryptedContentAsStream(c, offset, size, forceCheckHash); + return StreamUtils.saveInputStreamToOutputStream(in, out, size); + } +} diff --git a/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProvider.java b/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProvider.java index 866a556..f4573be 100644 --- a/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProvider.java +++ b/src/main/java/de/mas/wiiu/jnus/interfaces/NUSDataProvider.java @@ -26,19 +26,19 @@ import de.mas.wiiu.jnus.entities.fst.FST; import de.mas.wiiu.jnus.utils.StreamUtils; public interface NUSDataProvider { - default public byte[] readContent(Content content, long offset, int size) throws IOException { - return StreamUtils.getBytesFromStream(readContentAsStream(content, offset, size), size); + default byte[] readRawContent(Content content, long offset, int size) throws IOException { + return StreamUtils.getBytesFromStream(readRawContentAsStream(content, offset, size), size); } - default public InputStream readContentAsStream(Content content) throws IOException { - return readContentAsStream(content, 0); + default InputStream readRawContentAsStream(Content content) throws IOException { + return readRawContentAsStream(content, 0); } - default public InputStream readContentAsStream(Content content, long offset) throws IOException { - return readContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset); + default InputStream readRawContentAsStream(Content content, long offset) throws IOException { + return readRawContentAsStream(content, offset, content.getEncryptedFileSizeAligned() - offset); } - public InputStream readContentAsStream(Content content, long offset, long size) throws IOException; + public InputStream readRawContentAsStream(Content content, long offset, long size) throws IOException; public Optional getContentH3Hash(Content content) throws IOException; @@ -53,5 +53,4 @@ public interface NUSDataProvider { default public void setFST(FST fst) { } - } \ No newline at end of file diff --git a/src/main/java/de/mas/wiiu/jnus/interfaces/TriFunction.java b/src/main/java/de/mas/wiiu/jnus/interfaces/TriFunction.java new file mode 100644 index 0000000..b1f7f4b --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/interfaces/TriFunction.java @@ -0,0 +1,17 @@ +package de.mas.wiiu.jnus.interfaces; + +import java.util.Objects; +import java.util.function.Function; + + +@FunctionalInterface +public interface TriFunction { + + R apply(A a, B b, C c); + + default TriFunction andThen( + Function after) { + Objects.requireNonNull(after); + return (A a, B b, C c) -> after.apply(apply(a, b, c)); + } +} \ No newline at end of file diff --git a/src/main/java/de/mas/wiiu/jnus/utils/ByteUtils.java b/src/main/java/de/mas/wiiu/jnus/utils/ByteUtils.java index f6c7574..202444a 100644 --- a/src/main/java/de/mas/wiiu/jnus/utils/ByteUtils.java +++ b/src/main/java/de/mas/wiiu/jnus/utils/ByteUtils.java @@ -88,7 +88,9 @@ public final class ByteUtils { public static byte[] getBytesFromInt(int value, ByteOrder bo) { byte[] result = new byte[0x04]; - ByteBuffer.allocate(4).order(bo).putInt(value).get(result); + ByteBuffer buffer = ByteBuffer.allocate(4).order(bo).putInt(value); + buffer.position(0); + buffer.get(result); return result; } @@ -97,5 +99,11 @@ public final class ByteUtils { ByteBuffer.allocate(2).putShort(value).get(result); return result; } + + public static short getByteFromBytes(byte[] input, int offset) { + ByteBuffer buffer = ByteBuffer.allocate(2).put(Arrays.copyOfRange(input, offset, offset + 1)).order(ByteOrder.BIG_ENDIAN); + buffer.position(0); + return (short) ((buffer.getShort() & 0xFF00) >> 8); + } } diff --git a/src/main/java/de/mas/wiiu/jnus/utils/DataProviderUtils.java b/src/main/java/de/mas/wiiu/jnus/utils/DataProviderUtils.java index 3a457ec..5aa43aa 100644 --- a/src/main/java/de/mas/wiiu/jnus/utils/DataProviderUtils.java +++ b/src/main/java/de/mas/wiiu/jnus/utils/DataProviderUtils.java @@ -133,7 +133,7 @@ public class DataProviderUtils { } Utils.createDir(outputFolder); - InputStream inputStream = dataProvider.readContentAsStream(content); + InputStream inputStream = dataProvider.readRawContentAsStream(content); if (inputStream == null) { log.warning(content.getFilename() + " Couldn't save encrypted content. Input stream was null"); return; diff --git a/src/main/java/de/mas/wiiu/jnus/utils/IVCache.java b/src/main/java/de/mas/wiiu/jnus/utils/IVCache.java new file mode 100644 index 0000000..d91bbb9 --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/utils/IVCache.java @@ -0,0 +1,38 @@ +package de.mas.wiiu.jnus.utils; + +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import lombok.val; + +public class IVCache { + private final Map cache = new TreeMap<>(); + + public IVCache(long first, byte[] IV) { + if (!addForOffset(first, IV)) { + throw new IllegalArgumentException("IV was null or not 16 bytes big"); + } + } + + public boolean addForOffset(long offset, byte[] IV) { + if (IV == null || IV.length != 16) { + return false; + } + + cache.put(offset, IV); + return true; + } + + public Optional> getNearestForOffset(long offset) { + Optional> result = Optional.empty(); + for (val e : cache.entrySet()) { + if (e.getKey().longValue() <= offset) { + result = Optional.of(new Pair<>(e.getKey(), e.getValue())); + } else { + break; + } + } + return result; + } +} diff --git a/src/main/java/de/mas/wiiu/jnus/utils/Pair.java b/src/main/java/de/mas/wiiu/jnus/utils/Pair.java new file mode 100644 index 0000000..7da7369 --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/utils/Pair.java @@ -0,0 +1,9 @@ +package de.mas.wiiu.jnus.utils; + +import lombok.Data; + +@Data +public class Pair { + public final T1 k; + public final T2 v; +} diff --git a/src/main/java/de/mas/wiiu/jnus/utils/StreamUtils.java b/src/main/java/de/mas/wiiu/jnus/utils/StreamUtils.java index 1cf4286..96be838 100644 --- a/src/main/java/de/mas/wiiu/jnus/utils/StreamUtils.java +++ b/src/main/java/de/mas/wiiu/jnus/utils/StreamUtils.java @@ -89,8 +89,11 @@ public final class StreamUtils { if (overflowbuffer.getLengthOfDataInBuffer() > 0) { System.arraycopy(overflowbuf, 0, output, 0, overflowbuffer.getLengthOfDataInBuffer()); inBlockBuffer = overflowbuffer.getLengthOfDataInBuffer(); + } else { + if (inBlockBuffer == 0) { + return bytesRead; + } } - break; } @@ -124,19 +127,21 @@ public final class StreamUtils { } } - public static void saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException { + public static long saveInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream, long filesize) throws IOException { try { - saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true); + return saveInputStreamToOutputStreamWithHash(inputStream, outputStream, filesize, null, 0L, true); } catch (CheckSumWrongException e) { // Should never happen because the hash is not set. Lets print it anyway. e.printStackTrace(); } + return -1; } - public static void saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash, + public static long saveInputStreamToOutputStreamWithHash(InputStream inputStream, OutputStream outputStream, long filesize, byte[] hash, long expectedSizeForHash, boolean partial) throws IOException, CheckSumWrongException { - synchronized (inputStream) { + long written = 0; + synchronized (inputStream) { MessageDigest sha1 = null; if (hash != null && !partial) { try { @@ -150,7 +155,6 @@ public final class StreamUtils { byte[] buffer = new byte[BUFFER_SIZE]; int read = 0; long totalRead = 0; - long written = 0; try { do { @@ -189,6 +193,7 @@ public final class StreamUtils { StreamUtils.closeAll(inputStream, outputStream); } } + return written > 0 ? written : -1; } public static void skipExactly(InputStream in, long offset) throws IOException { diff --git a/src/main/java/de/mas/wiiu/jnus/utils/Utils.java b/src/main/java/de/mas/wiiu/jnus/utils/Utils.java index 6e7e4be..5cf3879 100644 --- a/src/main/java/de/mas/wiiu/jnus/utils/Utils.java +++ b/src/main/java/de/mas/wiiu/jnus/utils/Utils.java @@ -23,12 +23,14 @@ import java.io.InputStream; import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLConnection; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.net.ssl.HttpsURLConnection; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -238,4 +240,31 @@ public final class Utils { return null; } + public static Long getLastModifiedURL(HttpURLConnection connectionForURL, int timeout) throws IOException { + HttpURLConnection connection = connectionForURL; + connection.setRequestProperty("User-Agent", Settings.USER_AGENT); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + + int responseCode = connection.getResponseCode(); + + if (responseCode == HttpsURLConnection.HTTP_OK) { + InputStream inputStream = connection.getInputStream(); + byte[] buffer = new byte[0x10]; + inputStream.read(buffer); + inputStream.close(); + } else { + return null; + } + + Long dateTime = connection.getLastModified(); + + if (200 <= responseCode && responseCode <= 399) { + return dateTime; + } + + return null; + } + + } diff --git a/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java index 027ef0a..6339006 100644 --- a/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java +++ b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESDecryption.java @@ -29,6 +29,7 @@ import javax.crypto.spec.SecretKeySpec; import lombok.Getter; import lombok.Setter; +import lombok.Synchronized; public class AESDecryption { private Cipher cipher; @@ -51,6 +52,7 @@ public class AESDecryption { init(getAESKey(), getIV()); } + @Synchronized("cipher") protected void init(byte[] decryptedKey, byte[] iv) { SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES"); try { @@ -61,6 +63,7 @@ public class AESDecryption { } } + @Synchronized("cipher") public byte[] decrypt(byte[] input) { try { return cipher.doFinal(input); @@ -75,6 +78,7 @@ public class AESDecryption { return decrypt(input, 0, len); } + @Synchronized("cipher") public byte[] decrypt(byte[] input, int offset, int len) { try { return cipher.doFinal(input, offset, len); diff --git a/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESEncryption.java b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESEncryption.java new file mode 100644 index 0000000..9cccf70 --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/AESEncryption.java @@ -0,0 +1,92 @@ +/**************************************************************************** + * Copyright (C) 2016-2019 Maschell + * + * This program 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. + * + * This program 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 this program. If not, see . + ****************************************************************************/ +package de.mas.wiiu.jnus.utils.cryptography; + +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import lombok.Getter; +import lombok.Setter; +import lombok.Synchronized; + +public class AESEncryption { + private Cipher cipher; + + @Getter @Setter private byte[] AESKey; + @Getter @Setter private byte[] IV; + + public AESEncryption(byte[] AESKey, byte[] IV) throws NoSuchProviderException { + try { + cipher = Cipher.getInstance("AES/CBC/NoPadding", "SunJCE"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + e.printStackTrace(); + } + setAESKey(AESKey); + setIV(IV); + init(); + } + + protected final void init() { + init(getAESKey(), getIV()); + } + + @Synchronized("cipher") + protected void init(byte[] decryptedKey, byte[] iv) { + SecretKeySpec secretKeySpec = new SecretKeySpec(decryptedKey, "AES"); + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + e.printStackTrace(); + System.exit(2); + } + } + + @Synchronized("cipher") + public byte[] encrypt(byte[] input) { + try { + return cipher.doFinal(input); + } catch (IllegalBlockSizeException | BadPaddingException e) { + e.printStackTrace(); + System.exit(2); + } + return input; + } + + public byte[] encrypt(byte[] input, int len) { + return encrypt(input, 0, len); + } + + @Synchronized("cipher") + public byte[] encrypt(byte[] input, int offset, int len) { + try { + return cipher.doFinal(input, offset, len); + } catch (IllegalBlockSizeException | BadPaddingException e) { + e.printStackTrace(); + System.exit(2); + } + return input; + } +} diff --git a/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java index f46ffa0..eb91b21 100644 --- a/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java +++ b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSDecryption.java @@ -16,26 +16,18 @@ ****************************************************************************/ package de.mas.wiiu.jnus.utils.cryptography; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import java.util.Optional; import de.mas.wiiu.jnus.entities.Ticket; -import de.mas.wiiu.jnus.entities.content.Content; +import de.mas.wiiu.jnus.interfaces.ContentDecryptor; import de.mas.wiiu.jnus.utils.ByteArrayBuffer; -import de.mas.wiiu.jnus.utils.CheckSumWrongException; -import de.mas.wiiu.jnus.utils.HashUtil; import de.mas.wiiu.jnus.utils.StreamUtils; import de.mas.wiiu.jnus.utils.Utils; -import lombok.extern.java.Log; -@Log -public class NUSDecryption extends AESDecryption { +public class NUSDecryption extends AESDecryption implements ContentDecryptor { public NUSDecryption(byte[] AESKey, byte[] IV) { super(AESKey, IV); } @@ -56,144 +48,82 @@ public class NUSDecryption extends AESDecryption { return decrypt(blockBuffer, offset, BLOCKSIZE); } - public void decryptFileStream(InputStream inputStream, OutputStream outputStream, long fileOffset, long filesize, byte[] IV, byte[] h3hash, - long expectedSizeForHash) throws IOException, CheckSumWrongException { - MessageDigest sha1 = null; - MessageDigest sha1fallback = null; - - if (h3hash != null) { - try { - sha1 = MessageDigest.getInstance("SHA1"); - sha1fallback = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } - } - - int BLOCKSIZE = 0x8000; - - byte[] blockBuffer = new byte[BLOCKSIZE]; - - int inBlockBuffer; - long written = 0; - long writtenFallback = 0; - - try { - ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE); - - // We can only decrypt multiples of 16. So we need to align it. - long toRead = Utils.align(filesize, 16); - - do { - - int curReadSize = BLOCKSIZE; - if (toRead < BLOCKSIZE) { - curReadSize = (int) toRead; - } - - inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, curReadSize); - - byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV); - - if (inBlockBuffer == BLOCKSIZE) { - IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE); - } - - int toWrite = inBlockBuffer; - - if ((written + inBlockBuffer) > filesize) { - toWrite = (int) (filesize - written); - } - - written += toWrite; - toRead -= toWrite; - - outputStream.write(output, 0, toWrite); - - if (sha1 != null && sha1fallback != null) { - sha1.update(output, 0, toWrite); - - // In some cases it's using the hash of the whole .app file instead of the part - // that's been actually used. - long toFallback = inBlockBuffer; - if (writtenFallback + toFallback > expectedSizeForHash) { - toFallback = expectedSizeForHash - writtenFallback; - } - sha1fallback.update(output, 0, (int) toFallback); - writtenFallback += toFallback; - } - - if (written >= filesize && h3hash == null) { - break; - } - } while (inBlockBuffer == BLOCKSIZE); - - if (sha1 != null && sha1fallback != null) { - long missingInHash = expectedSizeForHash - writtenFallback; - if (missingInHash > 0) { - sha1fallback.update(new byte[(int) missingInHash]); - } - - byte[] calculated_hash1 = sha1.digest(); - byte[] calculated_hash2 = sha1fallback.digest(); - byte[] expected_hash = h3hash; - if (!Arrays.equals(calculated_hash1, expected_hash) && !Arrays.equals(calculated_hash2, expected_hash)) { - throw new CheckSumWrongException("hash checksum failed ", calculated_hash1, expected_hash); - } else { - log.finest("Hash DOES match saves output stream."); - } - - } - } finally { - StreamUtils.closeAll(inputStream, outputStream); - } - if (written < filesize) { - throw new IOException("Failed to read. Missing " + (filesize - written)); - } - } - - public void decryptFileStreamHashed(InputStream inputStream, OutputStream outputStream, long fileoffset, long filesize, byte[] h3Hash) - throws IOException, CheckSumWrongException, NoSuchAlgorithmException { + @Override + public long readDecryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] h3_hashes) + throws IOException { int BLOCKSIZE = 0x10000; - int HASHBLOCKSIZE = 0xFC00; + int HASHEDBLOCKSIZE = 0xFC00; + int HASHSIZE = BLOCKSIZE - HASHEDBLOCKSIZE; - long writeSize = HASHBLOCKSIZE; + long block = (offset / BLOCKSIZE); + long writeSize = BLOCKSIZE; - long block = (fileoffset / HASHBLOCKSIZE); - long soffset = fileoffset - (fileoffset / HASHBLOCKSIZE * HASHBLOCKSIZE); - - if (soffset + filesize > writeSize) { - writeSize = writeSize - soffset; - - } + long soffset = payloadOffset; byte[] encryptedBlockBuffer = new byte[BLOCKSIZE]; ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE); + long wrote = 0; int inBlockBuffer = 0; try { do { - inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, encryptedBlockBuffer, overflow, BLOCKSIZE); - if (writeSize > filesize) writeSize = filesize; - + inBlockBuffer = StreamUtils.getChunkFromStream(in, encryptedBlockBuffer, overflow, BLOCKSIZE); + if (inBlockBuffer < 0) { + return wrote; + } if (inBlockBuffer != BLOCKSIZE) { + throw new IOException("wasn't able to read " + BLOCKSIZE); } - byte[] output; - try { - output = decryptFileChunkHash(encryptedBlockBuffer, (int) block, h3Hash); - } catch (CheckSumWrongException | NoSuchAlgorithmException e) { - throw e; - } + byte[] hashes = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, new byte[16]); - if ((wrote + writeSize) > filesize) { - writeSize = (int) (filesize - wrote); - } + int H0_start = (int) (((int) block % 16) * 20); + + byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16); + byte[] output = decryptFileChunk(encryptedBlockBuffer, HASHSIZE, HASHEDBLOCKSIZE, IV); try { - outputStream.write(output, (int) (0 + soffset), (int) writeSize); + if (writeSize > size) { + writeSize = size; + } + if (writeSize + wrote > size) { + writeSize = size - wrote; + } + + long toBeWritten = writeSize; + + if (soffset <= HASHSIZE) { + long writeHashSize = HASHSIZE; + if (writeSize < HASHSIZE) { + writeHashSize = writeSize; + } + if (writeHashSize + soffset > HASHSIZE) { + writeHashSize = HASHSIZE - soffset; + } + out.write(hashes, (int) (0 + soffset), (int) writeHashSize); + wrote += writeHashSize; + toBeWritten -= writeHashSize; + + if (toBeWritten > 0) { + if (toBeWritten > HASHEDBLOCKSIZE) { + toBeWritten = HASHEDBLOCKSIZE; + writeSize = toBeWritten - HASHEDBLOCKSIZE; + } + out.write(output, 0, (int) toBeWritten); + wrote += toBeWritten; + } + } else { + soffset -= 0x400; + long writeThisTime = writeSize; + if (writeSize + soffset > HASHEDBLOCKSIZE) { + writeThisTime = HASHEDBLOCKSIZE - soffset; + } + out.write(output, (int) (0 + soffset), (int) writeThisTime); + wrote += writeThisTime; + } + writeSize = BLOCKSIZE; } catch (IOException e) { if (e.getMessage().equals("Pipe closed")) { break; @@ -201,62 +131,76 @@ public class NUSDecryption extends AESDecryption { e.printStackTrace(); throw e; } - wrote += writeSize; block++; if (soffset > 0) { - writeSize = HASHBLOCKSIZE; soffset = 0; } - } while (wrote < filesize && (inBlockBuffer == BLOCKSIZE)); - log.finest("Decryption okay"); + } while (wrote < size && (inBlockBuffer == BLOCKSIZE)); } finally { - StreamUtils.closeAll(inputStream, outputStream); + StreamUtils.closeAll(in, out); } + return wrote > 0 ? wrote : -1; } - private byte[] decryptFileChunkHash(byte[] blockBuffer, int block, byte[] h3_hashes) throws CheckSumWrongException, NoSuchAlgorithmException { - int hashSize = 0x400; - int blocksize = 0xFC00; + @Override + public long readDecryptedContentToStreamNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, long payloadOffset, byte[] IV) + throws IOException { + int BLOCKSIZE = 0x80000; - byte[] hashes = decryptFileChunk(blockBuffer, hashSize, new byte[16]); + byte[] blockBuffer = new byte[BLOCKSIZE]; - int H0_start = (block % 16) * 20; + int inBlockBuffer; + long written = 0; + long read = 0; - byte[] IV = Arrays.copyOfRange(hashes, H0_start, H0_start + 16); - byte[] output = decryptFileChunk(blockBuffer, hashSize, blocksize, IV); - - HashUtil.checkFileChunkHashes(hashes, h3_hashes, output, block); - - return output; - } - - public boolean decryptStreamsHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Optional h3HashHashed) - throws IOException, CheckSumWrongException, NoSuchAlgorithmException { try { - byte[] h3 = h3HashHashed.orElseThrow(() -> new FileNotFoundException("h3 hash not found.")); - decryptFileStreamHashed(inputStream, outputStream, offset, size, h3); + ByteArrayBuffer overflow = new ByteArrayBuffer(BLOCKSIZE); + + // We can only decrypt multiples of 16. So we need to align it. + long toRead = Utils.align(size, 16); + + do { + long writeOffset = Math.max(0, payloadOffset - read); + int curReadSize = BLOCKSIZE; + if (toRead < BLOCKSIZE) { + curReadSize = (int) (toRead + writeOffset); + } + inBlockBuffer = StreamUtils.getChunkFromStream(inputStream, blockBuffer, overflow, (int) Utils.align(curReadSize, 16)); + + if (inBlockBuffer < 0) { + break; + } + + byte[] output = decryptFileChunk(blockBuffer, (int) Utils.align(inBlockBuffer, 16), IV); + + if (inBlockBuffer > 16) { + IV = Arrays.copyOfRange(blockBuffer, BLOCKSIZE - 16, BLOCKSIZE); + } + + long writeLength = Math.min((output.length - writeOffset), (size - written)); + + try { + read += inBlockBuffer; + outputStream.write(output, (int) writeOffset, (int) writeLength); + written += writeLength; + toRead -= writeLength; + } catch (IOException e) { + if (e.getMessage().equals("Pipe closed")) { + break; + } else { + throw e; + } + } + if (written >= size) { + break; + } + } while (true); + } finally { StreamUtils.closeAll(inputStream, outputStream); } - - return true; - } - - public boolean decryptStreamsNonHashed(InputStream inputStream, OutputStream outputStream, long offset, long size, Content content, byte[] IV, - boolean partial) throws IOException, CheckSumWrongException { - try { - byte[] h3Hash = content.getSHA2Hash(); - // Ignore the h3hash if we don't read the whole file. - if (partial) { - h3Hash = null; - } - decryptFileStream(inputStream, outputStream, offset, size, IV, h3Hash, content.getEncryptedFileSize()); - } finally { - StreamUtils.closeAll(inputStream, outputStream); - } - - return true; + return written > 0 ? written : -1; } } diff --git a/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSEncryption.java b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSEncryption.java new file mode 100644 index 0000000..de2a46d --- /dev/null +++ b/src/main/java/de/mas/wiiu/jnus/utils/cryptography/NUSEncryption.java @@ -0,0 +1,152 @@ +package de.mas.wiiu.jnus.utils.cryptography; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchProviderException; +import java.util.Arrays; + +import de.mas.wiiu.jnus.entities.Ticket; +import de.mas.wiiu.jnus.interfaces.ContentEncryptor; +import de.mas.wiiu.jnus.utils.ByteArrayBuffer; +import de.mas.wiiu.jnus.utils.IVCache; +import de.mas.wiiu.jnus.utils.StreamUtils; +import lombok.Synchronized; + +public class NUSEncryption extends AESEncryption implements ContentEncryptor { + public NUSEncryption(byte[] AESKey, byte[] IV) throws NoSuchProviderException { + super(AESKey, IV); + } + + public NUSEncryption(Ticket ticket) throws NoSuchProviderException { + this(ticket.getDecryptedKey(), ticket.getIV()); + } + + @Synchronized + private byte[] encryptFileChunk(byte[] blockBuffer, int BLOCKSIZE, byte[] IV) { + return encryptFileChunk(blockBuffer, 0, BLOCKSIZE, IV); + } + + @Synchronized + private byte[] encryptFileChunk(byte[] blockBuffer, int offset, int BLOCKSIZE, byte[] IV) { + if (IV != null) { + setIV(IV); + init(); + } + return encrypt(blockBuffer, offset, BLOCKSIZE); + } + + @Override + public long readEncryptedContentToStreamHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset) throws IOException { + int BLOCKSIZE = 0x10000; + + int HASHBLOCKSIZE = 0x400; + int HASHEDBLOCKSIZE = 0xFC00; + + int buffer_size = BLOCKSIZE; + byte[] decryptedBlockBuffer = new byte[buffer_size]; + ByteArrayBuffer overflowbuffer = new ByteArrayBuffer(buffer_size); + int block = (int) (offset / 0x10000); + int inBlockBuffer = 0; + long read = 0; + long written = 0; + + try { + do { + inBlockBuffer = StreamUtils.getChunkFromStream(in, decryptedBlockBuffer, overflowbuffer, BLOCKSIZE); + read += inBlockBuffer; + if (read - offset < payloadOffset) { + continue; + } + if (inBlockBuffer != buffer_size) { + break; + } + + long curOffset = Math.max(0, payloadOffset - offset - read); + + byte[] IV = new byte[16]; + if (curOffset < HASHBLOCKSIZE) { + byte[] encryptedhashes = encryptFileChunk(Arrays.copyOfRange(decryptedBlockBuffer, 0, HASHBLOCKSIZE), HASHBLOCKSIZE, IV); + + long writeLength = Math.min((encryptedhashes.length - curOffset), (size - written)); + + out.write(encryptedhashes, (int) curOffset, (int) writeLength); + written += writeLength; + } else { + curOffset = curOffset > HASHBLOCKSIZE ? curOffset - HASHBLOCKSIZE : 0; + } + if(curOffset < HASHEDBLOCKSIZE) { + int iv_start = (block % 16) * 20; + IV = Arrays.copyOfRange(decryptedBlockBuffer, iv_start, iv_start + 16); + + byte[] encryptedContent = encryptFileChunk(Arrays.copyOfRange(decryptedBlockBuffer, HASHBLOCKSIZE, HASHEDBLOCKSIZE + HASHBLOCKSIZE), + HASHEDBLOCKSIZE, IV); + + long writeLength = Math.min((encryptedContent.length - curOffset), (size - written)); + out.write(encryptedContent, (int) curOffset, (int) writeLength); + written += writeLength; + } + + block++; + } while (inBlockBuffer == buffer_size); + } finally { + StreamUtils.closeAll(in, out); + } + return written > 0 ? written : -1; + } + + @Override + public long readEncryptedContentToStreamNonHashed(InputStream in, OutputStream out, long offset, long size, long payloadOffset, byte[] IV, + IVCache ivcache) throws IOException { + int BLOCKSIZE = 0x08000; + + int buffer_size = BLOCKSIZE; + byte[] decryptedBlockBuffer = new byte[buffer_size]; + ByteArrayBuffer overflowbuffer = new ByteArrayBuffer(buffer_size); + int inBlockBuffer = 0; + + setIV(IV); + init(); + + long read = 0; + long written = 0; + + long curPos = offset; + + try { + do { + int curReadLength = (int) (curPos % buffer_size); + if (curReadLength == 0) { + curReadLength = buffer_size; + } + inBlockBuffer = StreamUtils.getChunkFromStream(in, decryptedBlockBuffer, overflowbuffer, curReadLength); + if (inBlockBuffer < 0) { + break; + } + curPos += inBlockBuffer; + read += inBlockBuffer; + + byte[] output = encrypt(decryptedBlockBuffer, 0, inBlockBuffer); + + byte[] curIV = Arrays.copyOfRange(output, output.length - 16, output.length); + ivcache.addForOffset(curPos, curIV); + + setIV(Arrays.copyOfRange(output, BLOCKSIZE - 16, BLOCKSIZE)); + init(); + + if (read < payloadOffset) { + continue; + } + + long writeOffset = Math.max(0, payloadOffset - offset - read); + long writeLength = Math.min((output.length - writeOffset), (size - written)); + + out.write(output, (int) writeOffset, (int) writeLength); + written += writeLength; + } while (written < size); + } finally { + StreamUtils.closeAll(in, out); + } + return written > 0 ? written : -1; + } +}