diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d7086e..4ddfba4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,10 +235,13 @@ add_library(aaruformat SHARED src/dump.c include/aaruformat/structs/tape.h src/blocks/tape.c - src/blocks/flux.c) src/blocks/flux.c src/ps3/aes128.c src/ps3/aes128.h + src/ps3/ps3_crypto.c + src/ps3/ps3_crypto.h + src/ps3/ps3_encryption_map.c + src/ps3/ps3_encryption_map.h) # Set up include directories for the target target_include_directories(aaruformat diff --git a/src/ps3/ps3_crypto.c b/src/ps3/ps3_crypto.c new file mode 100644 index 0000000..2c2e966 --- /dev/null +++ b/src/ps3/ps3_crypto.c @@ -0,0 +1,64 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; version 2.1 of the License. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see + * . + * + * PS3 disc encryption: key derivation, sector encrypt/decrypt, IV derivation. + */ + +#include +#include + +#include "aes128.h" +#include "ps3_crypto.h" + +/* PS3 Encryption Round Key — publicly known constant */ +static const uint8_t PS3_ERK[16] = {0x38, 0x0B, 0xCF, 0x0B, 0x53, 0x45, 0x5B, 0x3C, + 0x78, 0x17, 0xAB, 0x4F, 0xA3, 0xBA, 0x90, 0xED}; + +/* PS3 ERK IV — publicly known constant */ +static const uint8_t PS3_ERK_IV[16] = {0x69, 0x47, 0x47, 0x72, 0xAF, 0x6F, 0xDA, 0xB3, + 0x42, 0x74, 0x3A, 0xEF, 0xAA, 0x18, 0x62, 0x87}; + +void ps3_derive_disc_key(const uint8_t data1[16], uint8_t disc_key[16]) +{ + memcpy(disc_key, data1, 16); + aes128_cbc_encrypt(PS3_ERK, PS3_ERK_IV, disc_key, 16); +} + +void ps3_derive_iv(uint64_t sector_num, uint8_t iv[16]) +{ + memset(iv, 0, 16); + + for(int i = 15; i >= 0 && sector_num > 0; i--) + { + iv[i] = (uint8_t)(sector_num & 0xFF); + sector_num >>= 8; + } +} + +void ps3_encrypt_sector(const uint8_t disc_key[16], uint64_t sector_num, uint8_t *data, uint32_t length) +{ + uint8_t iv[16]; + ps3_derive_iv(sector_num, iv); + aes128_cbc_encrypt(disc_key, iv, data, length); +} + +void ps3_decrypt_sector(const uint8_t disc_key[16], uint64_t sector_num, uint8_t *data, uint32_t length) +{ + uint8_t iv[16]; + ps3_derive_iv(sector_num, iv); + aes128_cbc_decrypt(disc_key, iv, data, length); +} diff --git a/src/ps3/ps3_crypto.h b/src/ps3/ps3_crypto.h new file mode 100644 index 0000000..8fd0317 --- /dev/null +++ b/src/ps3/ps3_crypto.h @@ -0,0 +1,76 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; version 2.1 of the License. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see + * . + * + * PS3 disc encryption: key derivation, sector encrypt/decrypt, IV derivation. + */ + +#ifndef LIBAARUFORMAT_PS3_CRYPTO_H +#define LIBAARUFORMAT_PS3_CRYPTO_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** + * @brief Derive a PS3 disc key from a data1 key. + * + * disc_key = AES-128-CBC-Encrypt(PS3_ERK, PS3_ERK_IV, data1) + * + * @param data1 16-byte data1 key (from disc or IRD). + * @param disc_key Output: 16-byte derived disc key. + */ +void ps3_derive_disc_key(const uint8_t data1[16], uint8_t disc_key[16]); + +/** + * @brief Derive the per-sector AES IV from a sector number. + * + * IV = sector number as 128-bit big-endian integer, zero-padded left. + * + * @param sector_num Sector number. + * @param iv Output: 16-byte IV buffer. + */ +void ps3_derive_iv(uint64_t sector_num, uint8_t iv[16]); + +/** + * @brief Encrypt a sector using PS3 disc encryption (AES-128-CBC). + * + * @param disc_key 16-byte disc key. + * @param sector_num Sector number (for IV derivation). + * @param data Buffer to encrypt in-place. + * @param length Number of bytes (must be multiple of 16). + */ +void ps3_encrypt_sector(const uint8_t disc_key[16], uint64_t sector_num, uint8_t *data, uint32_t length); + +/** + * @brief Decrypt a sector using PS3 disc encryption (AES-128-CBC). + * + * @param disc_key 16-byte disc key. + * @param sector_num Sector number (for IV derivation). + * @param data Buffer to decrypt in-place. + * @param length Number of bytes (must be multiple of 16). + */ +void ps3_decrypt_sector(const uint8_t disc_key[16], uint64_t sector_num, uint8_t *data, uint32_t length); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBAARUFORMAT_PS3_CRYPTO_H */ diff --git a/src/ps3/ps3_encryption_map.c b/src/ps3/ps3_encryption_map.c new file mode 100644 index 0000000..7ce3e25 --- /dev/null +++ b/src/ps3/ps3_encryption_map.c @@ -0,0 +1,161 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; version 2.1 of the License. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see + * . + * + * PS3 encryption map: plaintext region parsing, serialization, and lookup. + */ + +#include +#include +#include + +#include "ps3_encryption_map.h" + +/* Read a big-endian uint32 from a byte buffer. */ +static uint32_t read_be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +/* Read a little-endian uint32 from a byte buffer. */ +static uint32_t read_le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +/* Write a little-endian uint32 to a byte buffer. */ +static void write_le32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); + p[2] = (uint8_t)((v >> 16) & 0xFF); + p[3] = (uint8_t)((v >> 24) & 0xFF); +} + +int32_t ps3_parse_encryption_map(const uint8_t *sector0, uint32_t length, Ps3PlaintextRegion **regions, + uint32_t *count) +{ + if(sector0 == NULL || regions == NULL || count == NULL) return -1; + + /* Minimum required: 4 (region_count) + 4 (unknown) = 8 bytes */ + if(length < 8) return -2; + + uint32_t region_count = read_be32(sector0); + + if(region_count > PS3_MAX_PLAINTEXT_REGIONS) return -3; + + if(region_count == 0) + { + *regions = NULL; + *count = 0; + return 0; + } + + /* Need 8 + region_count * 8 bytes */ + uint32_t required = 8 + region_count * 8; + if(length < required) return -2; + + Ps3PlaintextRegion *r = (Ps3PlaintextRegion *)malloc(region_count * sizeof(Ps3PlaintextRegion)); + if(r == NULL) return -4; + + const uint8_t *ptr = sector0 + 8; /* skip region_count (4) + unknown (4) */ + + for(uint32_t i = 0; i < region_count; i++) + { + r[i].start_sector = read_be32(ptr); + r[i].end_sector = read_be32(ptr + 4); + ptr += 8; + + if(r[i].start_sector > r[i].end_sector) + { + free(r); + return -5; + } + } + + *regions = r; + *count = region_count; + return 0; +} + +int32_t ps3_serialize_encryption_map(const Ps3PlaintextRegion *regions, uint32_t count, uint8_t **out_data, + uint32_t *out_length) +{ + if(out_data == NULL || out_length == NULL) return -1; + if(count > PS3_MAX_PLAINTEXT_REGIONS) return -3; + + uint32_t size = 4 + count * 8; + uint8_t *buf = (uint8_t *)malloc(size); + if(buf == NULL) return -4; + + write_le32(buf, count); + + for(uint32_t i = 0; i < count; i++) + { + write_le32(buf + 4 + i * 8, regions[i].start_sector); + write_le32(buf + 4 + i * 8 + 4, regions[i].end_sector); + } + + *out_data = buf; + *out_length = size; + return 0; +} + +int32_t ps3_deserialize_encryption_map(const uint8_t *data, uint32_t length, Ps3PlaintextRegion **regions, + uint32_t *count) +{ + if(data == NULL || regions == NULL || count == NULL) return -1; + if(length < 4) return -2; + + uint32_t region_count = read_le32(data); + + if(region_count > PS3_MAX_PLAINTEXT_REGIONS) return -3; + + if(region_count == 0) + { + *regions = NULL; + *count = 0; + return 0; + } + + uint32_t required = 4 + region_count * 8; + if(length < required) return -2; + + Ps3PlaintextRegion *r = (Ps3PlaintextRegion *)malloc(region_count * sizeof(Ps3PlaintextRegion)); + if(r == NULL) return -4; + + for(uint32_t i = 0; i < region_count; i++) + { + r[i].start_sector = read_le32(data + 4 + i * 8); + r[i].end_sector = read_le32(data + 4 + i * 8 + 4); + } + + *regions = r; + *count = region_count; + return 0; +} + +bool ps3_is_sector_encrypted(const Ps3PlaintextRegion *plaintext_regions, uint32_t region_count, + uint64_t sector_address) +{ + if(plaintext_regions == NULL || region_count == 0) return true; + + for(uint32_t i = 0; i < region_count; i++) + if(sector_address >= plaintext_regions[i].start_sector && sector_address <= plaintext_regions[i].end_sector) + return false; + + return true; +} diff --git a/src/ps3/ps3_encryption_map.h b/src/ps3/ps3_encryption_map.h new file mode 100644 index 0000000..3e3299d --- /dev/null +++ b/src/ps3/ps3_encryption_map.h @@ -0,0 +1,94 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; version 2.1 of the License. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see + * . + * + * PS3 encryption map: plaintext region parsing, serialization, and lookup. + */ + +#ifndef LIBAARUFORMAT_PS3_ENCRYPTION_MAP_H +#define LIBAARUFORMAT_PS3_ENCRYPTION_MAP_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/** A plaintext (unencrypted) region on a PS3 disc. */ +typedef struct Ps3PlaintextRegion +{ + uint32_t start_sector; /**< First sector of plaintext region (inclusive). */ + uint32_t end_sector; /**< Last sector of plaintext region (inclusive). */ +} Ps3PlaintextRegion; + +#define PS3_MAX_PLAINTEXT_REGIONS 32 + +/** + * @brief Parse the encryption map from PS3 disc sector 0 (big-endian on disc). + * + * @param sector0 Pointer to the 2048-byte sector 0 data. + * @param length Length of sector0 data (must be >= 264). + * @param regions Output: allocated array of plaintext regions. Caller must free(). + * @param count Output: number of regions. + * @return 0 on success, negative error code on failure. + */ +int32_t ps3_parse_encryption_map(const uint8_t *sector0, uint32_t length, Ps3PlaintextRegion **regions, + uint32_t *count); + +/** + * @brief Serialize plaintext regions to little-endian binary for storage as a media tag. + * + * Format: [4B region_count LE] + region_count × [4B start LE, 4B end LE] + * + * @param regions Array of plaintext regions. + * @param count Number of regions. + * @param out_data Output: allocated buffer. Caller must free(). + * @param out_length Output: size of the buffer in bytes. + * @return 0 on success, negative error code on failure. + */ +int32_t ps3_serialize_encryption_map(const Ps3PlaintextRegion *regions, uint32_t count, uint8_t **out_data, + uint32_t *out_length); + +/** + * @brief Deserialize plaintext regions from little-endian binary (media tag format). + * + * @param data Serialized data buffer. + * @param length Length of data buffer. + * @param regions Output: allocated array of plaintext regions. Caller must free(). + * @param count Output: number of regions. + * @return 0 on success, negative error code on failure. + */ +int32_t ps3_deserialize_encryption_map(const uint8_t *data, uint32_t length, Ps3PlaintextRegion **regions, + uint32_t *count); + +/** + * @brief Check whether a sector is encrypted (i.e., not in any plaintext region). + * + * @param plaintext_regions Array of plaintext regions. + * @param region_count Number of regions. + * @param sector_address Sector number to check. + * @return true if the sector is encrypted (not in any plaintext region). + */ +bool ps3_is_sector_encrypted(const Ps3PlaintextRegion *plaintext_regions, uint32_t region_count, + uint64_t sector_address); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBAARUFORMAT_PS3_ENCRYPTION_MAP_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7c9a611..157efe1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -88,6 +88,8 @@ add_executable(tests_run open_image.cpp create_image.cpp ps3_aes.cpp + ps3_crypto.cpp + ps3_encryption_map.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/aes128.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/ps3_crypto.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/ps3/ps3_encryption_map.c diff --git a/tests/ps3_crypto.cpp b/tests/ps3_crypto.cpp new file mode 100644 index 0000000..5edbc4e --- /dev/null +++ b/tests/ps3_crypto.cpp @@ -0,0 +1,162 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * PS3 key derivation, IV derivation, and sector encrypt/decrypt tests. + */ + +#include +#include + +#include "gtest/gtest.h" + +extern "C" +{ +#include "../src/ps3/ps3_crypto.h" +} + +/* Test: IV derivation for sector 0 */ +TEST(PS3Crypto, IvDeriveSector0) +{ + uint8_t iv[16]; + uint8_t expected[16] = {0}; + + ps3_derive_iv(0, iv); + EXPECT_EQ(0, memcmp(iv, expected, 16)); +} + +/* Test: IV derivation for sector 1 */ +TEST(PS3Crypto, IvDeriveSector1) +{ + uint8_t iv[16]; + uint8_t expected[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + + ps3_derive_iv(1, iv); + EXPECT_EQ(0, memcmp(iv, expected, 16)); +} + +/* Test: IV derivation for sector 0xFF */ +TEST(PS3Crypto, IvDeriveSectorFF) +{ + uint8_t iv[16]; + uint8_t expected[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF}; + + ps3_derive_iv(0xFF, iv); + EXPECT_EQ(0, memcmp(iv, expected, 16)); +} + +/* Test: IV derivation for sector 0x12345678 */ +TEST(PS3Crypto, IvDeriveSectorMultiByte) +{ + uint8_t iv[16]; + uint8_t expected[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78}; + + ps3_derive_iv(0x12345678, iv); + EXPECT_EQ(0, memcmp(iv, expected, 16)); +} + +/* Test: IV derivation for large sector number spanning 8 bytes */ +TEST(PS3Crypto, IvDeriveSectorLarge) +{ + uint8_t iv[16]; + uint8_t expected[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89}; + + ps3_derive_iv(0xABCDEF0123456789ULL, iv); + EXPECT_EQ(0, memcmp(iv, expected, 16)); +} + +/* Test: disc key derivation produces consistent output */ +TEST(PS3Crypto, DiscKeyDerivation) +{ + uint8_t data1[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}; + uint8_t disc_key[16] = {0}; + + ps3_derive_disc_key(data1, disc_key); + + /* Disc key should not be all zeros (derivation produced output) */ + uint8_t zeros[16] = {0}; + EXPECT_NE(0, memcmp(disc_key, zeros, 16)); + + /* Disc key should not equal data1 (it was actually transformed) */ + EXPECT_NE(0, memcmp(disc_key, data1, 16)); + + /* Derivation should be deterministic */ + uint8_t disc_key2[16] = {0}; + ps3_derive_disc_key(data1, disc_key2); + EXPECT_EQ(0, memcmp(disc_key, disc_key2, 16)); +} + +/* Test: different data1 values produce different disc keys */ +TEST(PS3Crypto, DiscKeyDifferentData1) +{ + uint8_t data1_a[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}; + uint8_t data1_b[16] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, + 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2, 0xF1, 0xF0}; + uint8_t disc_key_a[16], disc_key_b[16]; + + ps3_derive_disc_key(data1_a, disc_key_a); + ps3_derive_disc_key(data1_b, disc_key_b); + + EXPECT_NE(0, memcmp(disc_key_a, disc_key_b, 16)); +} + +/* Test: sector encrypt/decrypt round-trip */ +TEST(PS3Crypto, SectorRoundTrip) +{ + uint8_t disc_key[16] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; + + uint8_t original[2048]; + uint8_t data[2048]; + + for(int i = 0; i < 2048; i++) original[i] = (uint8_t)(i & 0xFF); + memcpy(data, original, 2048); + + ps3_encrypt_sector(disc_key, 42, data, 2048); + EXPECT_NE(0, memcmp(data, original, 2048)); + + ps3_decrypt_sector(disc_key, 42, data, 2048); + EXPECT_EQ(0, memcmp(data, original, 2048)); +} + +/* Test: different sectors produce different ciphertext */ +TEST(PS3Crypto, DifferentSectorsDifferentCiphertext) +{ + uint8_t disc_key[16] = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; + + uint8_t plaintext[2048]; + for(int i = 0; i < 2048; i++) plaintext[i] = (uint8_t)(i & 0xFF); + + uint8_t data_a[2048], data_b[2048]; + memcpy(data_a, plaintext, 2048); + memcpy(data_b, plaintext, 2048); + + ps3_encrypt_sector(disc_key, 100, data_a, 2048); + ps3_encrypt_sector(disc_key, 200, data_b, 2048); + + /* Same plaintext encrypted with different sector IVs should differ */ + EXPECT_NE(0, memcmp(data_a, data_b, 2048)); +} + +/* Test: full key derivation + sector encrypt/decrypt */ +TEST(PS3Crypto, FullPipelineRoundTrip) +{ + uint8_t data1[16] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00}; + uint8_t disc_key[16]; + + ps3_derive_disc_key(data1, disc_key); + + uint8_t original[2048]; + uint8_t data[2048]; + for(int i = 0; i < 2048; i++) original[i] = (uint8_t)((i * 3 + 7) & 0xFF); + memcpy(data, original, 2048); + + ps3_encrypt_sector(disc_key, 1000, data, 2048); + ps3_decrypt_sector(disc_key, 1000, data, 2048); + + EXPECT_EQ(0, memcmp(data, original, 2048)); +} diff --git a/tests/ps3_encryption_map.cpp b/tests/ps3_encryption_map.cpp new file mode 100644 index 0000000..acf9113 --- /dev/null +++ b/tests/ps3_encryption_map.cpp @@ -0,0 +1,264 @@ +/* + * This file is part of the Aaru Data Preservation Suite. + * Copyright (c) 2019-2026 Natalia Portillo. + * + * PS3 encryption map parsing, serialization, and sector lookup tests. + */ + +#include +#include +#include + +#include "gtest/gtest.h" + +extern "C" +{ +#include "../src/ps3/ps3_encryption_map.h" +} + +/* + * Build a synthetic sector 0 in big-endian format: + * offset 0x00: region_count = 2 (BE) + * offset 0x04: unknown = 0 (BE) + * offset 0x08: region[0].start = 0 (BE) + * offset 0x0C: region[0].end = 31 (BE) + * offset 0x10: region[1].start = 256 (BE) + * offset 0x14: region[1].end = 511 (BE) + */ +static void build_synthetic_sector0(uint8_t *buf, uint32_t size) +{ + memset(buf, 0, size); + + /* region_count = 2, big-endian */ + buf[0] = 0; + buf[1] = 0; + buf[2] = 0; + buf[3] = 2; + + /* unknown field */ + buf[4] = 0; + buf[5] = 0; + buf[6] = 0; + buf[7] = 0; + + /* region[0]: start=0, end=31 */ + buf[8] = 0; + buf[9] = 0; + buf[10] = 0; + buf[11] = 0; /* start=0 */ + buf[12] = 0; + buf[13] = 0; + buf[14] = 0; + buf[15] = 31; /* end=31 */ + + /* region[1]: start=256, end=511 */ + buf[16] = 0; + buf[17] = 0; + buf[18] = 1; + buf[19] = 0; /* start=256 */ + buf[20] = 0; + buf[21] = 0; + buf[22] = 1; + buf[23] = 0xFF; /* end=511 */ +} + +/* Test: parse valid sector 0 */ +TEST(PS3EncMap, ParseValid) +{ + uint8_t sector0[2048]; + build_synthetic_sector0(sector0, 2048); + + Ps3PlaintextRegion *regions = NULL; + uint32_t count = 0; + + int32_t ret = ps3_parse_encryption_map(sector0, 2048, ®ions, &count); + ASSERT_EQ(0, ret); + ASSERT_EQ(2u, count); + ASSERT_NE(nullptr, regions); + + EXPECT_EQ(0u, regions[0].start_sector); + EXPECT_EQ(31u, regions[0].end_sector); + EXPECT_EQ(256u, regions[1].start_sector); + EXPECT_EQ(511u, regions[1].end_sector); + + free(regions); +} + +/* Test: parse zero regions */ +TEST(PS3EncMap, ParseZeroRegions) +{ + uint8_t sector0[2048]; + memset(sector0, 0, 2048); /* region_count = 0 */ + + Ps3PlaintextRegion *regions = NULL; + uint32_t count = 99; + + int32_t ret = ps3_parse_encryption_map(sector0, 2048, ®ions, &count); + ASSERT_EQ(0, ret); + EXPECT_EQ(0u, count); + EXPECT_EQ(nullptr, regions); +} + +/* Test: parse rejects too many regions */ +TEST(PS3EncMap, ParseTooManyRegions) +{ + uint8_t sector0[2048]; + memset(sector0, 0, 2048); + /* region_count = 33 (> max 32) */ + sector0[3] = 33; + + Ps3PlaintextRegion *regions = NULL; + uint32_t count = 0; + + int32_t ret = ps3_parse_encryption_map(sector0, 2048, ®ions, &count); + EXPECT_LT(ret, 0); +} + +/* Test: parse rejects invalid region (start > end) */ +TEST(PS3EncMap, ParseInvalidRegion) +{ + uint8_t sector0[2048]; + memset(sector0, 0, 2048); + sector0[3] = 1; /* 1 region */ + /* start=100, end=50 (invalid) */ + sector0[11] = 100; + sector0[15] = 50; + + Ps3PlaintextRegion *regions = NULL; + uint32_t count = 0; + + int32_t ret = ps3_parse_encryption_map(sector0, 2048, ®ions, &count); + EXPECT_LT(ret, 0); +} + +/* Test: parse rejects too-short buffer */ +TEST(PS3EncMap, ParseBufferTooShort) +{ + uint8_t sector0[8]; + memset(sector0, 0, 8); + sector0[3] = 1; /* 1 region, but buffer is only 8 bytes (need 16) */ + + Ps3PlaintextRegion *regions = NULL; + uint32_t count = 0; + + int32_t ret = ps3_parse_encryption_map(sector0, 8, ®ions, &count); + EXPECT_LT(ret, 0); +} + +/* Test: serialize and deserialize round-trip */ +TEST(PS3EncMap, SerializeDeserializeRoundTrip) +{ + Ps3PlaintextRegion input[3] = {{0, 31}, {256, 511}, {1024, 2047}}; + + uint8_t *data = NULL; + uint32_t length = 0; + + int32_t ret = ps3_serialize_encryption_map(input, 3, &data, &length); + ASSERT_EQ(0, ret); + ASSERT_NE(nullptr, data); + ASSERT_EQ(4u + 3u * 8u, length); /* 4 + 24 = 28 */ + + Ps3PlaintextRegion *output = NULL; + uint32_t count = 0; + + ret = ps3_deserialize_encryption_map(data, length, &output, &count); + ASSERT_EQ(0, ret); + ASSERT_EQ(3u, count); + ASSERT_NE(nullptr, output); + + for(uint32_t i = 0; i < 3; i++) + { + EXPECT_EQ(input[i].start_sector, output[i].start_sector); + EXPECT_EQ(input[i].end_sector, output[i].end_sector); + } + + free(data); + free(output); +} + +/* Test: serialize zero regions */ +TEST(PS3EncMap, SerializeZeroRegions) +{ + uint8_t *data = NULL; + uint32_t length = 0; + + int32_t ret = ps3_serialize_encryption_map(NULL, 0, &data, &length); + ASSERT_EQ(0, ret); + ASSERT_NE(nullptr, data); + ASSERT_EQ(4u, length); + + /* First 4 bytes should be 0 (LE) */ + EXPECT_EQ(0, data[0]); + EXPECT_EQ(0, data[1]); + EXPECT_EQ(0, data[2]); + EXPECT_EQ(0, data[3]); + + free(data); +} + +/* Test: sector encrypted — outside plaintext regions */ +TEST(PS3EncMap, SectorEncryptedOutside) +{ + Ps3PlaintextRegion regions[2] = {{0, 31}, {256, 511}}; + + /* Sector 32: between regions → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, 2, 32)); + /* Sector 100: between regions → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, 2, 100)); + /* Sector 255: just before second region → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, 2, 255)); + /* Sector 512: just after second region → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, 2, 512)); + /* Sector 10000: way past all regions → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, 2, 10000)); +} + +/* Test: sector not encrypted — inside plaintext regions */ +TEST(PS3EncMap, SectorPlaintextInside) +{ + Ps3PlaintextRegion regions[2] = {{0, 31}, {256, 511}}; + + /* Sector 0: first sector of first region → plaintext */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, 2, 0)); + /* Sector 15: middle of first region → plaintext */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, 2, 15)); + /* Sector 31: last sector of first region → plaintext */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, 2, 31)); + /* Sector 256: first sector of second region → plaintext */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, 2, 256)); + /* Sector 400: middle of second region → plaintext */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, 2, 400)); + /* Sector 511: last sector of second region → plaintext */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, 2, 511)); +} + +/* Test: sector encrypted when no regions defined */ +TEST(PS3EncMap, SectorEncryptedNoRegions) +{ + EXPECT_TRUE(ps3_is_sector_encrypted(NULL, 0, 0)); + EXPECT_TRUE(ps3_is_sector_encrypted(NULL, 0, 100)); +} + +/* Test: parse then use for sector lookup */ +TEST(PS3EncMap, ParseThenLookup) +{ + uint8_t sector0[2048]; + build_synthetic_sector0(sector0, 2048); + + Ps3PlaintextRegion *regions = NULL; + uint32_t count = 0; + + int32_t ret = ps3_parse_encryption_map(sector0, 2048, ®ions, &count); + ASSERT_EQ(0, ret); + + /* Sector 0: in first plaintext region */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, count, 0)); + /* Sector 50: between regions → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, count, 50)); + /* Sector 300: in second plaintext region */ + EXPECT_FALSE(ps3_is_sector_encrypted(regions, count, 300)); + /* Sector 600: past all regions → encrypted */ + EXPECT_TRUE(ps3_is_sector_encrypted(regions, count, 600)); + + free(regions); +}