Remedy Archive System
RavuAlHemio (Talk | contribs) m (MP1 crypt: ensure last value fits in a byte) |
RavuAlHemio (Talk | contribs) m (→RAS1+2 (Max Payne 1+2): clarify reduction-to-s8) |
||
| (19 intermediate revisions by one user not shown) | |||
| Line 11: | Line 11: | ||
== Format details == | == Format details == | ||
| + | |||
| + | Numbers are in little-endian byte order. | ||
| + | |||
| + | === Metadata === | ||
The file's header has the following structure: | The file's header has the following structure: | ||
| Line 16: | Line 20: | ||
struct RASHeader { | struct RASHeader { | ||
uint8_t magic[4]; // "RAS\0" | uint8_t magic[4]; // "RAS\0" | ||
| − | + | int32_t encryptionKey; | |
}; | }; | ||
</pre> | </pre> | ||
| Line 24: | Line 28: | ||
// decrypted structure | // decrypted structure | ||
struct RASMetadata { | struct RASMetadata { | ||
| − | + | uint32_t fileCount; | |
| + | uint32_t dirCount; | ||
| + | uint32_t fileListLength; | ||
| + | uint32_t dirListLength; | ||
| + | float32_t version; // binary32 according to IEEE 754 | ||
| + | uint32_t unknown5; | ||
| + | uint32_t unknown6; | ||
| + | uint32_t unknown7; | ||
| + | uint32_t compatibility; | ||
}; | }; | ||
</pre> | </pre> | ||
| + | |||
| + | The following version and compatibility values are known: | ||
| + | |||
| + | {| class="wikitable" | ||
| + | |- | ||
| + | ! Version | ||
| + | ! Compatibility | ||
| + | ! Game | ||
| + | |- | ||
| + | || 1.2 || 3 || Max Payne | ||
| + | |- | ||
| + | || 1.2 || 4 || Max Payne 2 | ||
| + | |} | ||
| + | |||
| + | What follows are <code>fileListLength</code> encrypted bytes of file metadata that can be decrypted using <code>encryptionKey</code>. (It is not necessary to remember the last value of <code>key</code> from decrypting <code>RASMetadata</code>.) Each entry has the following structure: | ||
| + | |||
| + | <pre> | ||
| + | // decrypted structure | ||
| + | struct RASFileListEntry { | ||
| + | char name[]; // NUL-terminated | ||
| + | uint32_t uncompressedSize; | ||
| + | uint32_t compressedSize; | ||
| + | uint32_t unknown2; | ||
| + | uint32_t dirIndex; | ||
| + | uint32_t unknown4; | ||
| + | uint32_t compressionMode; // 1 = some form of Lempel-Ziv, 3 = uncompressed | ||
| + | RASTimestamp timestamp; | ||
| + | }; | ||
| + | </pre> | ||
| + | |||
| + | Next is a directory list of <code>dirCount</code> elements which must be decrypted similarly. | ||
| + | |||
| + | <pre> | ||
| + | // decrypted structure | ||
| + | struct RASDirListEntry { | ||
| + | char name[]; // NUL-terminated | ||
| + | RASTimestamp timestamp; | ||
| + | }; | ||
| + | </pre> | ||
| + | |||
| + | Each directory is listed with its full name which starts and ends with a backslash (<code>\</code>). The top-most directory is named <code>\</code>. A file's <code>dirIndex</code> is a 0-based index into the directory list. | ||
| + | |||
| + | Timestamps are encoded using the same structure as SYSTEMTIME on Windows: | ||
| + | |||
| + | <pre> | ||
| + | struct RASTimestamp { | ||
| + | uint16_t year; | ||
| + | uint16_t month; | ||
| + | uint16_t dayOfWeek; // 0 = Sunday, 1 = Monday, ..., 6 = Saturday | ||
| + | uint16_t day; | ||
| + | uint16_t hour; | ||
| + | uint16_t minute; | ||
| + | uint16_t second; | ||
| + | uint16_t millisecond; | ||
| + | }; | ||
| + | </pre> | ||
| + | |||
| + | === Data === | ||
| + | |||
| + | The file data follows directly after the file and directory lists. Each file is stored in turn with no padding; skipping to a specific file requires adding up the <code>compressedSize</code> members of all files preceding it. | ||
== Encryption == | == Encryption == | ||
| Line 32: | Line 104: | ||
Depending on the generation of the RAS file format, different encryption schemes are used. | Depending on the generation of the RAS file format, different encryption schemes are used. | ||
| − | === RAS1 (Max Payne) === | + | === RAS1+2 (Max Payne 1+2) === |
<pre> | <pre> | ||
| − | + | void decrypt(uint8_t *buf, size_t count, int32_t key) { | |
| − | if key == 0 | + | size_t i; |
| − | key | + | |
| − | + | if (key == 0) { | |
| − | for i | + | key = 1; |
| − | a | + | } |
| − | b | + | for (i = 0; i < count; i++) { |
| − | + | uint8_t a = buf[i]; | |
| − | + | uint8_t b = ((uint8_t)(i % 5)) & 7; | |
| − | + | buf[i] = rotateLeftByte(a, b); | |
| − | + | key = key * 171 + (key / 177) * -30269; | |
| − | + | buf[i] = (uint8_t)((((((int8_t)i) + 3) * 6) ^ buf[i]) + ((int8_t)key)); | |
| − | + | } | |
| − | + | } | |
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
| − | + | ||
</pre> | </pre> | ||
| − | If your programming language doesn't support the <code> | + | If your programming language doesn't support the <code>rotateLeftByte</code> operation, it can be emulated using: |
<pre> | <pre> | ||
| − | function | + | uint8_t rotateLeftByte(uint8_t a, uint8_t b) { |
| − | return (i | + | return (uint8_t)((a << b) | (a >> (8 - b))); |
| + | } | ||
| + | </pre> | ||
| + | |||
| + | Note that the algorithm relies heavily on signed-integer wraparound and C's integer promotion rules; an operation like <code>((int8_t)i) + 3</code> will sign-extend the 8-bit value of <code>i</code> to 32 bits before adding 3. | ||
| + | |||
| + | A more complete description of the algorithm is as follows: | ||
| + | |||
| + | <pre> | ||
| + | function BitCastU8ToS8(u: u8): s8 | ||
| + | // reinterpret the bits of `u`, an 8-bit unsigned integer, | ||
| + | // as an 8-bit signed two's-complement integer | ||
| + | return ... | ||
| + | |||
| + | function BitCastS8ToU8(i: s8): i8 | ||
| + | // reinterpret the bits of `i`, an 8-bit signed two's-complement integer, | ||
| + | // as an 8-bit unsigned integer | ||
| + | return ... | ||
| + | |||
| + | function SignExtendS8ToS32(i: s8): s32 | ||
| + | // expand `i` from 8 bits to 32 bits | ||
| + | // by copying its topmost bit into the top 24 bits | ||
| + | // and the bottom 8 bits being the same as the bits of `i` | ||
| + | return ... | ||
| + | |||
| + | function RotateLeftS8(i: s8, by: u8): s8 | ||
| + | // rotate the 8-bit value `i` left by the number of bits in `by` | ||
| + | // (shift the value `i` left by `by` bits, | ||
| + | // shifting in the bits on the right that "fall out" on the left) | ||
| + | return ... | ||
| + | |||
| + | function MultiplyS32(factor1: s32, factor2: s32): s32 | ||
| + | // sign-extend `factor1` and `factor2` each to s64, | ||
| + | // multiply them, then take the bottom 32 bits of the product | ||
| + | // and return it as s32 | ||
| + | return ... | ||
| + | |||
| + | function ModuloU32U8(dividend: u32, divisor: u8): u8 | ||
| + | // divide `dividend` by `divisor` and return the remainder; | ||
| + | // this must be a value 0 ≤ value < divisor | ||
| + | return ... | ||
| + | |||
| + | function DivideS32U8(dividend: s32, divisor: u8): s32 | ||
| + | // divide `dividend` by `divisor` and return the quotient; | ||
| + | // always round toward 0 | ||
| + | return ... | ||
| + | |||
| + | function AddS32(summand1: s32, summand2: s32): s32 | ||
| + | // add `summand1` and `summand2`; | ||
| + | // wrap around (drop any carry bits) if addition overflows | ||
| + | return ... | ||
| + | |||
| + | function BitwiseXorS32(operand1: s32, operand2: s32): s32 | ||
| + | // perform a bitwise exclusive-or operation | ||
| + | // on the bits of operand1 with the corresponding bits of operand2 | ||
| + | // with the sign bits treated equally to all the other bits | ||
| + | return ... | ||
| + | |||
| + | function ReduceS32ToS8(i: s32): s8 | ||
| + | // remove the topmost 24 bits of `i`, | ||
| + | // returning the remaining bits as an 8-bit signed two's-complement integer; | ||
| + | // no modification is made to the topmost remaining bit | ||
| + | // (which becomes the result's sign bit) | ||
| + | return ... | ||
| + | |||
| + | function ReduceU32ToS8(u: u32): s8 | ||
| + | // remove the topmost 24 bits of `u`, | ||
| + | // returning the remaining bits as an 8-bit signed two's-complement integer; | ||
| + | // no modification is made to the topmost remaining bit | ||
| + | // (which becomes the result's sign bit) | ||
| + | return ... | ||
| + | |||
| + | function Decrypt(buf: u8[], key: s32): void | ||
| + | if key == 0 | ||
| + | key := 1 | ||
| + | |||
| + | for i from 0 to buf.length-1 | ||
| + | a := BitCastU8ToS8(buf[i]) | ||
| + | b := ModuloU32U8(i, 5) | ||
| + | c := RotateLeftS8(a, b) | ||
| + | buf[i] := BitCastS8ToU8(c) | ||
| + | |||
| + | d := MultiplyS32(key, 171) | ||
| + | e := DivideS32U8(key, 177) | ||
| + | f := MultiplyS32(e, -30269) | ||
| + | key := AddS32(d, f) | ||
| + | |||
| + | g := ReduceU32ToS8(i) | ||
| + | h := SignExtendS8ToS32(g) | ||
| + | j := AddS32(h, 3) | ||
| + | k := MultiplyS32(j, 6) | ||
| + | l := BitCastU8ToS8(buf[i]) | ||
| + | m := SignExtendS8ToS32(l) | ||
| + | n := BitwiseXorS32(k, m) | ||
| + | o := ReduceS32ToS8(key) | ||
| + | p := SignExtendS8ToS32(o) | ||
| + | q := AddS32(n, p) | ||
| + | r := ReduceS32ToS8(q) | ||
| + | buf[i] := BitCastS8ToU8(r) | ||
</pre> | </pre> | ||
[[Category:Remedy Entertainment]] | [[Category:Remedy Entertainment]] | ||
Latest revision as of 23:08, 23 November 2025
Remedy Archive System is used to store game data for Remedy Entertainment games such as Max Payne and Max Payne 2. The metadata (central directory) following the header is encrypted.
Contents |
[edit] Identification
Files begin with signature bytes 52 41 53 00.
[edit] Format details
Numbers are in little-endian byte order.
[edit] Metadata
The file's header has the following structure:
struct RASHeader {
uint8_t magic[4]; // "RAS\0"
int32_t encryptionKey;
};
The next section of the header must be decrypted first:
// decrypted structure
struct RASMetadata {
uint32_t fileCount;
uint32_t dirCount;
uint32_t fileListLength;
uint32_t dirListLength;
float32_t version; // binary32 according to IEEE 754
uint32_t unknown5;
uint32_t unknown6;
uint32_t unknown7;
uint32_t compatibility;
};
The following version and compatibility values are known:
| Version | Compatibility | Game |
|---|---|---|
| 1.2 | 3 | Max Payne |
| 1.2 | 4 | Max Payne 2 |
What follows are fileListLength encrypted bytes of file metadata that can be decrypted using encryptionKey. (It is not necessary to remember the last value of key from decrypting RASMetadata.) Each entry has the following structure:
// decrypted structure
struct RASFileListEntry {
char name[]; // NUL-terminated
uint32_t uncompressedSize;
uint32_t compressedSize;
uint32_t unknown2;
uint32_t dirIndex;
uint32_t unknown4;
uint32_t compressionMode; // 1 = some form of Lempel-Ziv, 3 = uncompressed
RASTimestamp timestamp;
};
Next is a directory list of dirCount elements which must be decrypted similarly.
// decrypted structure
struct RASDirListEntry {
char name[]; // NUL-terminated
RASTimestamp timestamp;
};
Each directory is listed with its full name which starts and ends with a backslash (\). The top-most directory is named \. A file's dirIndex is a 0-based index into the directory list.
Timestamps are encoded using the same structure as SYSTEMTIME on Windows:
struct RASTimestamp {
uint16_t year;
uint16_t month;
uint16_t dayOfWeek; // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
uint16_t day;
uint16_t hour;
uint16_t minute;
uint16_t second;
uint16_t millisecond;
};
[edit] Data
The file data follows directly after the file and directory lists. Each file is stored in turn with no padding; skipping to a specific file requires adding up the compressedSize members of all files preceding it.
[edit] Encryption
Depending on the generation of the RAS file format, different encryption schemes are used.
[edit] RAS1+2 (Max Payne 1+2)
void decrypt(uint8_t *buf, size_t count, int32_t key) {
size_t i;
if (key == 0) {
key = 1;
}
for (i = 0; i < count; i++) {
uint8_t a = buf[i];
uint8_t b = ((uint8_t)(i % 5)) & 7;
buf[i] = rotateLeftByte(a, b);
key = key * 171 + (key / 177) * -30269;
buf[i] = (uint8_t)((((((int8_t)i) + 3) * 6) ^ buf[i]) + ((int8_t)key));
}
}
If your programming language doesn't support the rotateLeftByte operation, it can be emulated using:
uint8_t rotateLeftByte(uint8_t a, uint8_t b) {
return (uint8_t)((a << b) | (a >> (8 - b)));
}
Note that the algorithm relies heavily on signed-integer wraparound and C's integer promotion rules; an operation like ((int8_t)i) + 3 will sign-extend the 8-bit value of i to 32 bits before adding 3.
A more complete description of the algorithm is as follows:
function BitCastU8ToS8(u: u8): s8
// reinterpret the bits of `u`, an 8-bit unsigned integer,
// as an 8-bit signed two's-complement integer
return ...
function BitCastS8ToU8(i: s8): i8
// reinterpret the bits of `i`, an 8-bit signed two's-complement integer,
// as an 8-bit unsigned integer
return ...
function SignExtendS8ToS32(i: s8): s32
// expand `i` from 8 bits to 32 bits
// by copying its topmost bit into the top 24 bits
// and the bottom 8 bits being the same as the bits of `i`
return ...
function RotateLeftS8(i: s8, by: u8): s8
// rotate the 8-bit value `i` left by the number of bits in `by`
// (shift the value `i` left by `by` bits,
// shifting in the bits on the right that "fall out" on the left)
return ...
function MultiplyS32(factor1: s32, factor2: s32): s32
// sign-extend `factor1` and `factor2` each to s64,
// multiply them, then take the bottom 32 bits of the product
// and return it as s32
return ...
function ModuloU32U8(dividend: u32, divisor: u8): u8
// divide `dividend` by `divisor` and return the remainder;
// this must be a value 0 ≤ value < divisor
return ...
function DivideS32U8(dividend: s32, divisor: u8): s32
// divide `dividend` by `divisor` and return the quotient;
// always round toward 0
return ...
function AddS32(summand1: s32, summand2: s32): s32
// add `summand1` and `summand2`;
// wrap around (drop any carry bits) if addition overflows
return ...
function BitwiseXorS32(operand1: s32, operand2: s32): s32
// perform a bitwise exclusive-or operation
// on the bits of operand1 with the corresponding bits of operand2
// with the sign bits treated equally to all the other bits
return ...
function ReduceS32ToS8(i: s32): s8
// remove the topmost 24 bits of `i`,
// returning the remaining bits as an 8-bit signed two's-complement integer;
// no modification is made to the topmost remaining bit
// (which becomes the result's sign bit)
return ...
function ReduceU32ToS8(u: u32): s8
// remove the topmost 24 bits of `u`,
// returning the remaining bits as an 8-bit signed two's-complement integer;
// no modification is made to the topmost remaining bit
// (which becomes the result's sign bit)
return ...
function Decrypt(buf: u8[], key: s32): void
if key == 0
key := 1
for i from 0 to buf.length-1
a := BitCastU8ToS8(buf[i])
b := ModuloU32U8(i, 5)
c := RotateLeftS8(a, b)
buf[i] := BitCastS8ToU8(c)
d := MultiplyS32(key, 171)
e := DivideS32U8(key, 177)
f := MultiplyS32(e, -30269)
key := AddS32(d, f)
g := ReduceU32ToS8(i)
h := SignExtendS8ToS32(g)
j := AddS32(h, 3)
k := MultiplyS32(j, 6)
l := BitCastU8ToS8(buf[i])
m := SignExtendS8ToS32(l)
n := BitwiseXorS32(k, m)
o := ReduceS32ToS8(key)
p := SignExtendS8ToS32(o)
q := AddS32(n, p)
r := ReduceS32ToS8(q)
buf[i] := BitCastS8ToU8(r)