Skip to content

Commit

Permalink
Merge branch 'feature/v3'
Browse files Browse the repository at this point in the history
  • Loading branch information
mullermarian committed Dec 31, 2023
2 parents d78ae82 + 5b86dfd commit bf90ff9
Show file tree
Hide file tree
Showing 9 changed files with 514 additions and 187 deletions.
28 changes: 18 additions & 10 deletions core/src/main/java/studio/core/v1/reader/fs/FsStoryPackReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import studio.core.v1.Constants;
import studio.core.v1.model.*;
import studio.core.v1.model.metadata.StoryPackMetadata;
import studio.core.v1.utils.BytesUtils;
import studio.core.v1.utils.XXTEACipher;

import java.io.DataInputStream;
Expand All @@ -31,6 +32,7 @@ public class FsStoryPackReader {
private static final String SOUND_INDEX_FILENAME = "si";
private static final String SOUND_FOLDER = "sf" + File.separator;
private static final String NIGHT_MODE_FILENAME = "nm";
private static final String CLEARTEXT_FILENAME = ".cleartext";

public StoryPackMetadata readMetadata(Path inputFolder) throws IOException {
// Pack metadata model
Expand Down Expand Up @@ -68,10 +70,16 @@ public StoryPack read(Path inputFolder) throws IOException {
// Night mode is available if file 'nm' exists
boolean nightModeAvailable = new File(packFolder, NIGHT_MODE_FILENAME).exists();

// Pack files should be kept as cleartext in the library, with cipher operations happening during transfer to/from device
// We keep a backward-compatibility with packs in library ciphered for v2

// Assets are cleartext if file '.cleartext' exists
boolean isCleartext = new File(packFolder, CLEARTEXT_FILENAME).exists();

// Load ri, si and li files
byte[] riContent = readCipheredFile(new File(packFolder, IMAGE_INDEX_FILENAME).toPath());
byte[] siContent = readCipheredFile(new File(packFolder, SOUND_INDEX_FILENAME).toPath());
byte[] liContent = readCipheredFile(new File(packFolder, LIST_INDEX_FILENAME).toPath());
byte[] riContent = readCipheredFile(new File(packFolder, IMAGE_INDEX_FILENAME).toPath(), isCleartext);
byte[] siContent = readCipheredFile(new File(packFolder, SOUND_INDEX_FILENAME).toPath(), isCleartext);
byte[] liContent = readCipheredFile(new File(packFolder, LIST_INDEX_FILENAME).toPath(), isCleartext);

// Open 'ni' file
FileInputStream niFis = new FileInputStream(new File(packFolder, NODE_INDEX_FILENAME));
Expand Down Expand Up @@ -140,7 +148,7 @@ public StoryPack read(Path inputFolder) throws IOException {
byte[] imagePath = Arrays.copyOfRange(riContent, imageAssetIndexInRI*12, imageAssetIndexInRI*12+12); // Each entry takes 12 bytes
String path = new String(imagePath, StandardCharsets.UTF_8);
// Read image file
byte[] rfContent = readCipheredFile(new File(packFolder, IMAGE_FOLDER+path.replaceAll("\\\\", "/")).toPath());
byte[] rfContent = readCipheredFile(new File(packFolder, IMAGE_FOLDER+path.replaceAll("\\\\", "/")).toPath(), isCleartext);
image = new ImageAsset("image/bmp", rfContent);
}
AudioAsset audio = null;
Expand All @@ -149,7 +157,7 @@ public StoryPack read(Path inputFolder) throws IOException {
byte[] audioPath = Arrays.copyOfRange(siContent, soundAssetIndexInSI*12, soundAssetIndexInSI*12+12); // Each entry takes 12 bytes
String path = new String(audioPath, StandardCharsets.UTF_8);
// Read audio file
byte[] sfContent = readCipheredFile(new File(packFolder, SOUND_FOLDER+path.replaceAll("\\\\", "/")).toPath());
byte[] sfContent = readCipheredFile(new File(packFolder, SOUND_FOLDER+path.replaceAll("\\\\", "/")).toPath(), isCleartext);
audio = new AudioAsset("audio/mpeg", sfContent);
}

Expand Down Expand Up @@ -193,16 +201,16 @@ public StoryPack read(Path inputFolder) throws IOException {
return new StoryPack(uuid, factoryDisabled, version, List.copyOf(stageNodes.values()), null, nightModeAvailable);
}

private byte[] readCipheredFile(Path path) throws IOException {
private byte[] readCipheredFile(Path path, boolean isCleartext) throws IOException {
byte[] content = Files.readAllBytes(path);
return decipherFirstBlockCommonKey(content);
return isCleartext ? content : decipherFirstBlockCommonKey(content);
}

private byte[] decipherFirstBlockCommonKey(byte[] data) {
byte[] block = Arrays.copyOfRange(data, 0, Math.min(512, data.length));
int[] dataInt = XXTEACipher.toIntArray(block, ByteOrder.LITTLE_ENDIAN);
int[] decryptedInt = XXTEACipher.btea(dataInt, -(Math.min(128, data.length/4)), XXTEACipher.toIntArray(XXTEACipher.COMMON_KEY, ByteOrder.BIG_ENDIAN));
byte[] decryptedBlock = XXTEACipher.toByteArray(decryptedInt, ByteOrder.LITTLE_ENDIAN);
int[] dataInt = BytesUtils.toIntArray(block, ByteOrder.LITTLE_ENDIAN);
int[] decryptedInt = XXTEACipher.btea(dataInt, -(Math.min(128, data.length/4)), BytesUtils.toIntArray(XXTEACipher.COMMON_KEY, ByteOrder.BIG_ENDIAN));
byte[] decryptedBlock = BytesUtils.toByteArray(decryptedInt, ByteOrder.LITTLE_ENDIAN);
ByteBuffer bb = ByteBuffer.allocate(data.length);
bb.put(decryptedBlock);
if (data.length > 512) {
Expand Down
38 changes: 38 additions & 0 deletions core/src/main/java/studio/core/v1/utils/BytesUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package studio.core.v1.utils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

public class BytesUtils {

public static int[] toIntArray(byte[] data, ByteOrder endianness) {
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(endianness);
List<Integer> ints = new ArrayList<>();
for (int i=0; i<data.length/4; i++) {
ints.add(bb.getInt());
}
return ints.stream().mapToInt(i->i).toArray();
}

public static byte[] toByteArray(int[] data, ByteOrder endianness) {
ByteBuffer bb = ByteBuffer.allocate(data.length*4);
bb.order(endianness);
for (int i : data) {
bb.putInt(i);
}
return bb.array();
}

public static byte[] reverseEndianness(byte[] data) {
return toByteArray(toIntArray(data, ByteOrder.LITTLE_ENDIAN), ByteOrder.BIG_ENDIAN);
}
}
24 changes: 0 additions & 24 deletions core/src/main/java/studio/core/v1/utils/XXTEACipher.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,12 @@

package studio.core.v1.utils;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

public class XXTEACipher {

public static final byte[] COMMON_KEY = new byte[] { (byte)0x91, (byte)0xbd, (byte)0x7a, (byte)0x0a, (byte)0xa7, (byte)0x54, (byte)0x40, (byte)0xa9, (byte)0xbb, (byte)0xd4, (byte)0x9d, (byte)0x6c, (byte)0xe0, (byte)0xdc, (byte)0xc0, (byte)0xe3};

private static final int DELTA = 0x9e3779b9;

public static int[] toIntArray(byte[] data, ByteOrder endianness) {
ByteBuffer bb = ByteBuffer.wrap(data);
bb.order(endianness);
List<Integer> ints = new ArrayList<>();
for (int i=0; i<data.length/4; i++) {
ints.add(bb.getInt());
}
return ints.stream().mapToInt(i->i).toArray();
}

public static byte[] toByteArray(int[] data, ByteOrder endianness) {
ByteBuffer bb = ByteBuffer.allocate(data.length*4);
bb.order(endianness);
for (int i : data) {
bb.putInt(i);
}
return bb.array();
}

public static int[] btea(int[] v, int n, int[] k) {
int y, z, sum;
int p, rounds, e;
Expand Down
Loading

0 comments on commit bf90ff9

Please sign in to comment.