Skip to content

Commit

Permalink
Add basic CIA support
Browse files Browse the repository at this point in the history
  • Loading branch information
Martmists-GH committed Jan 5, 2024
1 parent d741630 commit 408d0e6
Show file tree
Hide file tree
Showing 13 changed files with 425 additions and 77 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ gradle-local.properties
.idea/
lib/
*.cxi
*.cia
12 changes: 9 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.Properties

plugins {
kotlin("jvm") version "1.7.10"
id("com.github.ben-manes.versions") version "0.42.0"
kotlin("jvm") version "1.9.21"
id("com.github.ben-manes.versions") version "0.50.0"
}

group = "com.martmists"
Expand Down Expand Up @@ -46,14 +46,20 @@ tasks {
"*.kts",
"gradle*.properties",
"*.cxi",
"*.cia",
"exefs/",
"romfs/",
)
}

withType<JavaCompile> {
sourceCompatibility = JavaVersion.VERSION_1_8.toString()
targetCompatibility = JavaVersion.VERSION_1_8.toString()
}

withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs = listOf(
"-opt-in=kotlin.contracts.ExperimentalContracts"
)
Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.martmists.ctr.loader.filesystem

import com.martmists.ctr.ext.*
import com.martmists.ctr.loader.format.*
import com.martmists.ctr.reader.Reader
import ghidra.app.util.bin.ByteProvider
import ghidra.formats.gfilesystem.*
import ghidra.formats.gfilesystem.annotations.FileSystemInfo
import ghidra.formats.gfilesystem.fileinfo.FileAttributes
import ghidra.util.exception.CancelledException
import ghidra.util.task.TaskMonitor
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.experimental.and
import kotlin.math.min


@FileSystemInfo(
type = "cia",
description = "CIA Container",
factory = CIAFileSystemFactory::class,
priority = FileSystemInfo.PRIORITY_HIGH,
)
class CIAFileSystem(fsFSRL: FSRLRoot, provider: ByteProvider, fsService: FileSystemService) : MountableGFileSystem by CXIFileSystem(fsFSRL, CIAByteProvider(provider), fsService) {
class CIAByteProvider(private val provider: ByteProvider) : ByteProvider by provider {
private var startOffset: Long

init {
val source = provider.getInputStream(0)
source.reader {
val header = read<CIAHeader>()
align(64)
var pos = tell()
val caCert = Certificate.parse(this)
val ticketCert = Certificate.parse(this)
val tmdCert = Certificate.parse(this)
require(tell() - pos == header.certificateChainSize.toLong()) { "Certificate chain size mismatch; expected ${header.certificateChainSize}, got ${tell() - pos}" }
align(64)
pos = tell()
val ticket = Ticket.parse(this)
require(tell() - pos == header.ticketSize.toLong()) { "Ticket size mismatch; expected ${header.ticketSize}, got ${tell() - pos}" }
align(64)
pos = tell()
val tmd = readBytes(header.tmdSize)
require(tell() - pos == header.tmdSize.toLong()) { "TMD size mismatch; expected ${header.tmdSize}, got ${tell() - pos}" }
align(64)

// TODO: Verify in TMD that there is no encryption
// TODO: Add support for multiple NCCH containers in CIA

startOffset = tell()
}
}

override fun getInputStream(index: Long): InputStream {
return OffsetInputStream(provider, startOffset + index)
}
}

class OffsetInputStream(private val provider: ByteProvider, private val offset: Long) : InputStream() {
private var mark = 0L
private var currentPos = 0L
private var stream = provider.getInputStream(offset)

override fun markSupported() = true

override fun mark(readlimit: Int) {
mark = currentPos + readlimit
}

override fun reset() {
stream.close()
stream = provider.getInputStream(offset + mark)
currentPos = mark
}

override fun read() = stream.read().also { currentPos++ }
override fun read(b: ByteArray) = read(b, 0, b.size).also { currentPos += it }
override fun read(b: ByteArray, off: Int, len: Int) = stream.read(b, off, len).also { currentPos += it }
override fun readNBytes(len: Int) = stream.readNBytes(len).also { currentPos += it.size }
override fun readNBytes(b: ByteArray?, off: Int, len: Int) = stream.readNBytes(b, off, len).also { currentPos += it }
override fun skip(n: Long) = stream.skip(n).also { currentPos += n }
override fun skipNBytes(n: Long) = stream.skipNBytes(n).also { currentPos += n }
override fun readAllBytes() = stream.readAllBytes().also { currentPos += it.size }
override fun available() = stream.available()

override fun close() {
stream.close()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.martmists.ctr.loader.filesystem

import ghidra.app.util.bin.ByteProvider
import ghidra.formats.gfilesystem.FSRLRoot
import ghidra.formats.gfilesystem.FSUtilities
import ghidra.formats.gfilesystem.FileSystemService
import ghidra.formats.gfilesystem.factory.GFileSystemFactoryByteProvider
import ghidra.formats.gfilesystem.factory.GFileSystemProbeByteProvider
import ghidra.util.task.TaskMonitor
import java.util.*


class CIAFileSystemFactory : GFileSystemFactoryByteProvider<CIAFileSystem>, GFileSystemProbeByteProvider {
override fun create(targetFSRL: FSRLRoot, byteProvider: ByteProvider, fsService: FileSystemService, monitor: TaskMonitor): CIAFileSystem {
val fs = CIAFileSystem(targetFSRL, byteProvider, fsService)
fs.mount(monitor)
return fs
}

override fun probe(provider: ByteProvider, fsService: FileSystemService, taskMonitor: TaskMonitor): Boolean {
val filename = provider.fsrl.name
var ext: String = FSUtilities.getExtension(filename, 1) ?: return false
ext = ext.lowercase(Locale.getDefault())
return ext == ".cia"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import kotlin.math.min
factory = CXIFileSystemFactory::class,
priority = FileSystemInfo.PRIORITY_HIGH,
)
class CXIFileSystem(private val fsFSRL: FSRLRoot, private var provider: ByteProvider, private val fsService: FileSystemService) : GFileSystem {
class CXIFileSystem(private val fsFSRL: FSRLRoot, private var provider: ByteProvider, private val fsService: FileSystemService) : MountableGFileSystem {
data class Metadata(
val ncch: NCCHHeader,
val ncchEx: NCCHExHeader,
Expand All @@ -34,12 +34,7 @@ class CXIFileSystem(private val fsFSRL: FSRLRoot, private var provider: ByteProv
private var fileCount = 0L
private var closed = false

/**
* Mounts (opens) the file system.
*
* @param monitor A cancellable task monitor.
*/
fun mount(monitor: TaskMonitor) {
override fun mount(monitor: TaskMonitor) {
val stream = provider.getInputStream(0)
stream.reader {
val ncch = read<NCCHHeader>()
Expand Down Expand Up @@ -134,7 +129,7 @@ class CXIFileSystem(private val fsFSRL: FSRLRoot, private var provider: ByteProv
codeSection.size
}

fsService.getDerivedByteProviderPush(provider.fsrl, file.fsrl, file.path, size.toLong(), { out ->
fsService.getDerivedByteProviderPush(fsFSRL.container, file.fsrl, file.path, size.toLong(), { out ->
provider.getInputStream(0).reader {
seek(exefsStart + codeSection.offset)
var code = readBytes(codeSection.size)
Expand All @@ -153,7 +148,7 @@ class CXIFileSystem(private val fsFSRL: FSRLRoot, private var provider: ByteProv
val level3HeaderStart = tell()
val level3Header = read<IVFCHeader.Level3Header>()

fsService.getDerivedByteProviderPush(provider.fsrl, file.fsrl, file.path, metadata.file!!.dataSize, { out ->
fsService.getDerivedByteProviderPush(fsFSRL.container, file.fsrl, file.path, metadata.file!!.dataSize, { out ->
val stream = provider.getInputStream(level3HeaderStart + level3Header.fileDataOffset + metadata.file.dataOffset)
var remaining = metadata.file.dataSize
while (remaining > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.martmists.ctr.loader.filesystem

import ghidra.formats.gfilesystem.GFileSystem
import ghidra.util.task.TaskMonitor

interface MountableGFileSystem : GFileSystem {
/**
* Mounts (opens) the file system.
*
* @param monitor A cancellable task monitor.
*/
fun mount(monitor: TaskMonitor)
}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/martmists/ctr/loader/format/CIAHeader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.martmists.ctr.loader.format

data class CIAHeader(
val archiveHeaderSize: Int,
val type: Short,
val version: Short,
val certificateChainSize: Int,
val ticketSize: Int,
val tmdSize: Int,
val metaSize: Int,
val contentSize: Long,
val contentIndex_8192: ByteArray,
)
9 changes: 9 additions & 0 deletions src/main/kotlin/com/martmists/ctr/loader/format/CIAMeta.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.martmists.ctr.loader.format

data class CIAMeta(
val dependencyModuleList_384: ByteArray,
val reserved1_384: ByteArray,
val coreVersion: Int,
val reserved2_252: ByteArray,
val iconData_14016: ByteArray,
)
68 changes: 68 additions & 0 deletions src/main/kotlin/com/martmists/ctr/loader/format/Certificate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.martmists.ctr.loader.format

import com.martmists.ctr.reader.Reader

interface PublicKey

data class RSAPublicKey(
val modulus: ByteArray,
val exponent: Int,
) : PublicKey

data class ECCPublicKey(
val key: ByteArray,
) : PublicKey

data class Certificate(
val signatureType: Int,
val signature: ByteArray,
val issuer: ByteArray,
val keyType: Int,
val name: ByteArray,
val expirationTime: Int,
val pubKey: PublicKey,
) {
companion object {
fun parse(reader: Reader): Certificate {
return reader.withEndian(false) {
val signatureType = read<Int>()
val signature = when (signatureType) {
0x010000, 0x010003 -> readBytes(0x200).also { skip(0x3c) }
0x010001, 0x010004 -> readBytes(0x100).also { skip(0x3c) }
0x010002, 0x010005 -> readBytes(0x3c).also { skip(0x40) }
else -> throw Exception("Unknown signature type: $signatureType")
}
val issuer = readBytes(0x40)
val keyType = read<Int>()
val name = readBytes(0x40)
val expirationTime = read<Int>()
val pubkey = when (keyType) {
0 -> RSAPublicKey(
modulus = readBytes(0x200),
exponent = read<Int>(),
).also { skip(0x34) }

1 -> RSAPublicKey(
modulus = readBytes(0x100),
exponent = read<Int>(),
).also { skip(0x34) }

2 -> ECCPublicKey(
key = readBytes(0x3c),
).also { skip(0x3c) }

else -> throw Exception("Unknown key type: $keyType")
}
Certificate(
signatureType,
signature,
issuer,
keyType,
name,
expirationTime,
pubkey,
)
}
}
}
}
Loading

0 comments on commit 408d0e6

Please sign in to comment.