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);
+}