-
Notifications
You must be signed in to change notification settings - Fork 9
KIF Archive
Format fully deciphered.
The extension (.int
) stands for "Integrated File" (as shown from assembly log message: "統合ファイルオープン : %s").
As such, the file signature (KIF
) likely stands for "kclib Integrated File".
This format has been fully known for a long time.
Data Type | Value | Description |
---|---|---|
char[4] |
"KIF" | File Signature |
uint32 |
EntryCount | Number of entries in archive |
N = EntryCount
Data Type | Value | Description |
---|---|---|
byte[64] |
FileNameRaw | Obfuscated Name of the entry's file, or "__key__.dat" |
uint32 |
Offset | The offset to the entry's data |
uint32 |
Length | The length of the entry's data |
Position = Entries[i].Offset
N = EntryCount
Data Type | Value | Description |
---|---|---|
byte[Entries[i].Length] | Data | The raw data of the file |
Entry.FileNameRaw is listed as a byte[64]
because the original read bytes are needed during UnobfuscateFileName (except for "__key__.dat"). Below is a property to access the FileName of an Entry. It shall be assumed that this get-only property is contained within the Entry structure definition during code samples.
string FileName {
get {
Encoding shiftJIS = Encoding.GetEncoding(932);
int index = Array.IndexOf(fileNameRaw, (byte) 0);
int length = (index != -1 ? index : fileNameRaw.Length);
return shiftJIS.GetString(fileNameRaw, 0, length);
}
}
See DecryptEntries for a code sample on how entries are decrypted.
See DecryptEntryData for a code sample on how entry data is decrypted.
KIFINT archives are usually not readable without decryption.
Key Name | Location |
---|---|
V_CODE2 | A resource in the CatSystem2 executable |
"__key__.dat" | An entry in Entry Table with this FileName |
TocSeed | The table of contents seed obtained from the V_CODE2 |
FileKey | The Blowfish key obtained from "__key__.dat" |
See V_CODEs for more information on how to acquire the V_CODE2. The V_CODE2 is used in GenerateTocSeed. The
The "__key__.dat" Entry is a single Entry in the Entry Table signifies that the archive is encrypted, if it is not present, then no decryption is necessary, and none of the requirements are needed.
The "__key__.dat" Entry has the same size and structure as an normal Entry, but the values have different uses:
Data Type | Value | Description |
---|---|---|
char[64] |
"__key__.dat" (FileNameRaw) | Signifies this is the key entry |
uint32 |
Unknown (Offset) | Value is non-zero |
uint32 |
Seed (Length) | The seed to pass to Mersenne Twister |
Unlike a normal encrypted Entry, "__key__.dat"'s FileName will always be visible without calling UnobfuscateFileName. As such, it's data type is listed as char[64]
instead of byte[64]
.
The table of contents seed (TocSeed in code) is half of a Mersenne Twister seed used during the unobfuscation of encrypted Entry FileNames.
See GenerateTocSeed on how to get the TocSeed from the V_CODE2.
The FileKey is generated by using Mersenne Twister with "__key__.dat"'s Seed (Length) as the input seed.
The FileKey is then used as the key to initialize the Blowfish cipher which is used for the actual decryption of the Entry Table and EntryData Table.
// Returns true and outputs a non-null blowfish if the entries are encrypted
bool DecryptEntries(Header header, Entry[] entries, string vcode2, out Blowfish blowfish) {
Encoding shiftJIS = Encoding.GetEncoding(932);
uint tocSeed = GenerateTocSeed(vcode2);
uint fileKey = 0;
int fileKeyIndex = -1;
blowfish = null;
// Not decrypted, our work here is done
if (fileKeyIndex == -1)
return false;
// Obtain the decryption file key if one exists
for (int i = 0; i < header.EntryCount; i++) {
if (entries[i].FileName == "__key__.dat") {
fileKey = MersenneTwister.GenRand(entries[i].Length); // Seed
blowfish = new Blowfish(fileKey);
fileKeyIndex = i;
break;
}
}
// Decrypt the entries using the file key
if (fileKeyIndex != -1) {
for (int i = 0; i < header.EntryCount; i++) {
if (i == fileKeyIndex)
continue;
// Correct the entry's file name
UnobfuscateFileName(entries[i].FileNameRaw, unchecked(tocSeed + (uint) i));
// Apply the extra offset before decryption
entries[i].Offset += i;
// The following part can be done a better way in C#,
// but is shown this way to make the explanation easier.
// Get the bytes for the entry's encrypted offset and length combined
byte[] entryBytes = new byte[8];
Array.Copy(BitConverter.GetBytes(entries[i].Offset), 0, entryBytes, 0, 4);
Array.Copy(BitConverter.GetBytes(entries[i].Length), 0, entryBytes, 4, 4);
// Decrypt the entry's offset and length
blowfish.Decrypt(entryBytes);
// Return the entry's decrypted offset and length back to the entry
entries[i].Offset = BitConverter.ToUInt32(entryBytes, 0);
entries[i].Length = BitConverter.ToUInt32(entryBytes, 4);
}
}
return true;
}
Here is example code for how you would decrypt the data for an Entry. Note how the remaining bytes that are not a multiple of 8 are ignored and thus, are always unencrypted.
void DecryptEntryData(byte[] entryData, Blowfish blowfish) {
// Decryption length of data is rounded down to the previous
// multiple of 8 bytes. The remaining 0-7 bytes are unencrypted.
// & ~7 is equivalent to `(int) / 8 * 8`.
blowfish.Decrypt(data, data.Length & ~7);
}
See Key: TocSeed.
uint GenerateTocSeed(string vcode2) {
const uint magic = 0x04C11DB7;
uint seed = 0xFFFFFFFF;
// For each character in V_CODE2
for (int i = 0; i < vcode2.Length; i++) {
seed ^= ((uint) vcode2[i]) << 24;
for (int j = 0; j < 8; j++) {
if ((seed & 0x80000000) != 0) {
seed *= 2;
seed ^= magic;
}
else {
seed *= 2;
}
}
seed = ~seed;
}
return seed;
}
See Key: TocSeed.
void UnobfuscateFileName(byte[] fileName, uint seed) {
const int Length = 52;
const string FWD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const string REV = "zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA";
uint key = MersenneTwister.GenRand(seed);
int shift = (byte) ((key >> 24) + (key >> 16) + (key >> 8) + key);
for (int i = 0; i < fileName.Length; i++, shift++) {
byte c = fileName[i];
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
int index = 0;
int index2 = shift;
while (REV[index2 % Length] != c) {
if (REV[(shift + index + 1) % Length] == c) {
index += 1;
break;
}
if (REV[(shift + index + 2) % Length] == c) {
index += 2;
break;
}
if (REV[(shift + index + 3) % Length] == c) {
index += 3;
break;
}
index += 4;
index2 += 4;
if (index >= Length) // We're outside the array, no need to continue
break;
}
if (index < Length) // Only assign if we're inside the array
fileName[i] = (byte) FWD[index];
}
}
}