From 68417b54bf7e6b107ba76b4a03442ff9af5ef6d6 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 15:04:49 +0200 Subject: [PATCH 01/12] [ZZZRawImage] Read AACS keys --- Aaru.Images/ZZZRawImage/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Aaru.Images/ZZZRawImage/Constants.cs b/Aaru.Images/ZZZRawImage/Constants.cs index aa0967691..3f811049e 100644 --- a/Aaru.Images/ZZZRawImage/Constants.cs +++ b/Aaru.Images/ZZZRawImage/Constants.cs @@ -48,6 +48,8 @@ public sealed partial class ZZZRawImage readonly byte[] _cdSync = [0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00]; readonly (MediaTagType tag, string name)[] _readWriteSidecars = [ + (MediaTagType.AacsMediaKey, ".mk.bin"), (MediaTagType.AacsVolumeUniqueKey, ".vuk.bin"), + (MediaTagType.AACS_VolumeIdentifier, ".vid.bin"), (MediaTagType.ATA_IDENTIFY, ".identify.bin"), (MediaTagType.BD_DI, ".di.bin"), (MediaTagType.CD_ATIP, ".atip.bin"), (MediaTagType.CD_FullTOC, ".toc.bin"), (MediaTagType.CD_FirstTrackPregap, ".leadin.bin"), (MediaTagType.CD_PMA, ".pma.bin"), From 1f074d2bb76911025be2a2c3143a497d4a7ae474 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 15:13:07 +0200 Subject: [PATCH 02/12] [UDF] Add helper for getting sector extents from file paths --- Aaru.Filesystems/UDF/Extents.cs | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 Aaru.Filesystems/UDF/Extents.cs diff --git a/Aaru.Filesystems/UDF/Extents.cs b/Aaru.Filesystems/UDF/Extents.cs new file mode 100644 index 000000000..b2ef0b043 --- /dev/null +++ b/Aaru.Filesystems/UDF/Extents.cs @@ -0,0 +1,157 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Extents.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// Gets physical sector extents for a file path. +// +// --[ License ] -------------------------------------------------------------- +// +// 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; either version 2.1 of the +// License, or (at your option) any later version. +// +// 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.Helpers; +using Marshal = Aaru.Helpers.Marshal; + +namespace Aaru.Filesystems; + +public sealed partial class UDF +{ + /// + /// Gets physical sector extents for a file path. + /// + /// Absolute filesystem path. + /// Physical extents as (startSector, sectorCount). + /// Error number. + public ErrorNumber GetFilePhysicalSectorExtents(string path, out List<(ulong startSector, uint sectorCount)> extents) + { + extents = []; + + if(!_mounted) return ErrorNumber.AccessDenied; + + ErrorNumber errno = GetFileEntryBuffer(path, out byte[] feBuffer, out ushort partitionReferenceNumber); + + if(errno != ErrorNumber.NoError) return errno; + + errno = ParseFileEntryInfo(feBuffer, out UdfFileEntryInfo info); + + if(errno != ErrorNumber.NoError) return errno; + + var adType = (byte)((ushort)info.IcbTag.flags & 0x07); + + int fixedSize = info.IsExtended ? 216 : 176; + int adOffset = fixedSize + (int)info.LengthOfExtendedAttributes; + int adLength = (int)info.LengthOfAllocationDescriptors; + + return adType switch + { + 0 => CollectShortAdExtents(feBuffer, + adOffset, + adLength, + partitionReferenceNumber, + extents), + 1 => CollectLongAdExtents(feBuffer, adOffset, adLength, extents), + 2 => ErrorNumber.NotSupported, + 3 => ErrorNumber.NoError, + _ => ErrorNumber.InvalidArgument + }; + } + + /// + /// Collects short allocation descriptor extents. + /// + /// File entry buffer. + /// Allocation descriptor offset. + /// Allocation descriptor length. + /// Partition reference number. + /// Physical extents as (startSector, sectorCount). + /// Error number. + ErrorNumber CollectShortAdExtents(byte[] feBuffer, int adOffset, int adLength, ushort partitionReferenceNumber, + List<(ulong startSector, uint sectorCount)> extents) + { + int sadSize = System.Runtime.InteropServices.Marshal.SizeOf(); + int adPos = adOffset; + + while(adPos + sadSize <= adOffset + adLength) + { + ShortAllocationDescriptor sad = + Marshal.ByteArrayToStructureLittleEndian(feBuffer, adPos, sadSize); + + uint extentLength = sad.extentLength & 0x3FFFFFFF; + + if(extentLength == 0) break; + + uint sectorCount = (extentLength + _sectorSize - 1) / _sectorSize; + + if(sectorCount > 0) + { + ulong start = TranslateLogicalBlock(sad.extentLocation, partitionReferenceNumber, _partitionStartingLocation); + extents.Add((start, sectorCount)); + } + + adPos += sadSize; + } + + return ErrorNumber.NoError; + } + + /// + /// Collects long allocation descriptor extents. + /// + /// File entry buffer. + /// Allocation descriptor offset. + /// Allocation descriptor length. + /// Physical extents as (startSector, sectorCount). + /// Error number. + ErrorNumber CollectLongAdExtents(byte[] feBuffer, int adOffset, int adLength, + List<(ulong startSector, uint sectorCount)> extents) + { + int ladSize = System.Runtime.InteropServices.Marshal.SizeOf(); + int adPos = adOffset; + + while(adPos + ladSize <= adOffset + adLength) + { + LongAllocationDescriptor lad = + Marshal.ByteArrayToStructureLittleEndian(feBuffer, adPos, ladSize); + + uint extentLength = lad.extentLength & 0x3FFFFFFF; + + if(extentLength == 0) break; + + uint sectorCount = (extentLength + _sectorSize - 1) / _sectorSize; + + if(sectorCount > 0) + { + ulong start = TranslateLogicalBlock(lad.extentLocation.logicalBlockNumber, + lad.extentLocation.partitionReferenceNumber, + _partitionStartingLocation); + extents.Add((start, sectorCount)); + } + + adPos += ladSize; + } + + return ErrorNumber.NoError; + } +} From 06e424a7a3ec7ed02641eeba103e8f11622257ef Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 16:07:28 +0200 Subject: [PATCH 03/12] update translation --- Aaru.Localization/Core.Designer.cs | 78 ++++++++++++++++++++++++++++++ Aaru.Localization/Core.resx | 39 +++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/Aaru.Localization/Core.Designer.cs b/Aaru.Localization/Core.Designer.cs index 043a80aee..f82327f6f 100644 --- a/Aaru.Localization/Core.Designer.cs +++ b/Aaru.Localization/Core.Designer.cs @@ -6958,5 +6958,83 @@ namespace Aaru.Localization { return ResourceManager.GetString("Output_format_not_initialized", resourceCulture); } } + + public static string Aacs_missing_volume_unique_key { + get { + return ResourceManager.GetString("Aacs_missing_volume_unique_key", resourceCulture); + } + } + + public static string Aacs_missing_unit_keys { + get { + return ResourceManager.GetString("Aacs_missing_unit_keys", resourceCulture); + } + } + + public static string Aacs_unit_key_inf_invalid { + get { + return ResourceManager.GetString("Aacs_unit_key_inf_invalid", resourceCulture); + } + } + + public static string Aacs_encrypted_unit_key_invalid_length { + get { + return ResourceManager.GetString("Aacs_encrypted_unit_key_invalid_length", resourceCulture); + } + } + + public static string Aacs2_unit_keys_not_supported { + get { + return ResourceManager.GetString("Aacs2_unit_keys_not_supported", resourceCulture); + } + } + + public static string Aacs_hddvd_not_supported { + get { + return ResourceManager.GetString("Aacs_hddvd_not_supported", resourceCulture); + } + } + + public static string Aacs_incomplete_cps_unit_at_track_end { + get { + return ResourceManager.GetString("Aacs_incomplete_cps_unit_at_track_end", resourceCulture); + } + } + + public static string Aacs_incomplete_cps_unit_at_track_end_continuing { + get { + return ResourceManager.GetString("Aacs_incomplete_cps_unit_at_track_end_continuing", resourceCulture); + } + } + + public static string Aacs_incomplete_cps_unit_pending_before_read_error { + get { + return ResourceManager.GetString("Aacs_incomplete_cps_unit_pending_before_read_error", resourceCulture); + } + } + + public static string Aacs_incomplete_cps_unit_after_read_error_continuing { + get { + return ResourceManager.GetString("Aacs_incomplete_cps_unit_after_read_error_continuing", resourceCulture); + } + } + + public static string Aacs_could_not_decrypt_cps_unit_starting_at_0 { + get { + return ResourceManager.GetString("Aacs_could_not_decrypt_cps_unit_starting_at_0", resourceCulture); + } + } + + public static string Aacs_could_not_decrypt_cps_unit_starting_at_0_continuing { + get { + return ResourceManager.GetString("Aacs_could_not_decrypt_cps_unit_starting_at_0_continuing", resourceCulture); + } + } + + public static string Aacs_bd_decrypt_requires_0_byte_sectors { + get { + return ResourceManager.GetString("Aacs_bd_decrypt_requires_0_byte_sectors", resourceCulture); + } + } } } diff --git a/Aaru.Localization/Core.resx b/Aaru.Localization/Core.resx index 0061f6e68..633647b5e 100644 --- a/Aaru.Localization/Core.resx +++ b/Aaru.Localization/Core.resx @@ -3562,4 +3562,43 @@ It has no sense to do it, and it will put too much strain on the tape. [red]Output format not initialized.[/] + + Blu-ray AACS decryption requires AACS_VolumeUniqueKey media tag, or both AACS_MediaKey and AACS_VolumeIdentifier. + + + Blu-ray AACS decryption requires CPS unit keys: AACS/Unit_Key_RO.inf on a mounted UDF/ISO partition, or AACS_DataKeys media tag. + + + AACS/Unit_Key_RO.inf could not be parsed. + + + Invalid encrypted CPS unit key length (expected 16 bytes). + + + AACS2 (Ultra HD Blu-ray) unit keys are not supported in this version. + + + HD DVD AACS decryption is not implemented. + + + Incomplete AACS CPS unit at end of track ({0} sector(s) pending, starting at LBA {1}). + + + Incomplete AACS CPS unit ({0} sector(s) pending, starting at LBA {1}); writing pending sectors as-is and continuing. + + + Cannot read sectors while an incomplete AACS CPS unit is pending (started at LBA {0}). + + + Read failed with an incomplete AACS CPS unit pending ({0} sector(s) from LBA {1}); writing pending sectors as-is and continuing. + + + Could not decrypt AACS CPS unit starting at LBA {0}. + + + Could not decrypt AACS CPS unit starting at LBA {0}; writing ciphertext and continuing. + + + Blu-ray AACS decryption requires {0}-byte user sectors; this image reports a different sector size. + \ No newline at end of file From 8b066da6be027ded816bb5c9540880e3cc22fc35 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 15:38:16 +0200 Subject: [PATCH 04/12] [AACS] Add AACS decryption --- Aaru.Decryption/Aacs/AacsConvertBuffer.cs | 125 ++++++++++++++++ Aaru.Decryption/Aacs/AacsCrypto.cs | 165 ++++++++++++++++++++++ Aaru.Decryption/Aacs/AacsStreamDecrypt.cs | 135 ++++++++++++++++++ Aaru.Decryption/Aacs/UnitKeyRoParser.cs | 162 +++++++++++++++++++++ 4 files changed, 587 insertions(+) create mode 100644 Aaru.Decryption/Aacs/AacsConvertBuffer.cs create mode 100644 Aaru.Decryption/Aacs/AacsCrypto.cs create mode 100644 Aaru.Decryption/Aacs/AacsStreamDecrypt.cs create mode 100644 Aaru.Decryption/Aacs/UnitKeyRoParser.cs diff --git a/Aaru.Decryption/Aacs/AacsConvertBuffer.cs b/Aaru.Decryption/Aacs/AacsConvertBuffer.cs new file mode 100644 index 000000000..da5451b4b --- /dev/null +++ b/Aaru.Decryption/Aacs/AacsConvertBuffer.cs @@ -0,0 +1,125 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : AacsConvertBuffer.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// Buffers 2048-byte sectors into 6144-byte AACS CPS units across reads. +// +// --[ License ] -------------------------------------------------------------- +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using System; + +namespace Aaru.Decryption.Aacs; + +/// Aligns contiguous 2048-byte sectors to CPS units for decrypt during image conversion. +public sealed class AacsConvertBuffer +{ + byte[] _pending = Array.Empty(); + /// LBA of the first 2048-byte sector in . + ulong _pendingStartLba; + + /// Clears any trailing sectors (e.g. at end of conversion). + public void Reset() + { + _pending = Array.Empty(); + _pendingStartLba = 0; + } + + /// + /// Prepends pending data to (first sectors), + /// decrypts all complete 6144-byte units, writes decrypted bytes back into , + /// and stores an incomplete trailing 0–2 sectors for the next call. + /// + /// Sector buffer to decrypt. + /// Number of sectors to decrypt. + /// LBA of the first sector. + /// Decrypted CPS unit keys. + public void DecryptChunk(ref byte[] sectorBuffer, uint sectorCount, ulong firstSectorLba, + byte[][] decryptedCpsUnitKeys) + { + if(sectorCount == 0) + return; + + int newBytes = (int)(sectorCount * AacsStreamDecrypt.SectorLen); + + if(sectorBuffer.Length < newBytes) + throw new ArgumentException("sectorBuffer shorter than sectorCount implies.", nameof(sectorBuffer)); + + int pl = _pending.Length; + + if(pl > 0) + { + ulong expected = _pendingStartLba + (ulong)(pl / AacsStreamDecrypt.SectorLen); + + if(expected != firstSectorLba) + { + _pending = Array.Empty(); + _pendingStartLba = 0; + pl = 0; + } + } + + if(pl == 0) + _pendingStartLba = firstSectorLba; + + int totalLen = pl + newBytes; + byte[] merged = new byte[totalLen]; + + if(pl > 0) + Buffer.BlockCopy(_pending, 0, merged, 0, pl); + + Buffer.BlockCopy(sectorBuffer, 0, merged, pl, newBytes); + + int fullUnitsBytes = totalLen / AacsStreamDecrypt.AlignedUnitLen * AacsStreamDecrypt.AlignedUnitLen; + + Span mergedSpan = merged; + + for(int o = 0; o < fullUnitsBytes; o += AacsStreamDecrypt.AlignedUnitLen) + { + Span unit = mergedSpan.Slice(o, AacsStreamDecrypt.AlignedUnitLen); + AacsStreamDecrypt.TryDecryptAlignedUnit(unit, decryptedCpsUnitKeys); + } + + int rem = totalLen - fullUnitsBytes; + + if(rem > 0) + { + _pending = new byte[rem]; + Buffer.BlockCopy(merged, fullUnitsBytes, _pending, 0, rem); + } + else + { + _pending = Array.Empty(); + _pendingStartLba = 0; + } + + Buffer.BlockCopy(merged, pl, sectorBuffer, 0, newBytes); + } +} diff --git a/Aaru.Decryption/Aacs/AacsCrypto.cs b/Aaru.Decryption/Aacs/AacsCrypto.cs new file mode 100644 index 000000000..55961bf8c --- /dev/null +++ b/Aaru.Decryption/Aacs/AacsCrypto.cs @@ -0,0 +1,165 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : AacsCrypto.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// AES primitives for Blu-ray AACS +// +// --[ License ] -------------------------------------------------------------- +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Security.Cryptography; + +namespace Aaru.Decryption.Aacs; + +/// Low-level AES operations used by Blu-ray AACS stream decryption. +public static class AacsCrypto +{ + /// AES-128 CBC IV. + static readonly byte[] AacsCbcIv = + [ + 0x0b, 0xa0, 0xf8, 0xdd, 0xfe, 0xa6, 0x1f, 0xb3, 0xd8, 0xdf, 0x9f, 0x56, 0x6a, 0x05, 0x0f, 0x78 + ]; + + /// AES-128 ECB encrypt one block. + /// AES-128 key (16 bytes). + /// Plaintext to encrypt (16 bytes). + /// Encrypted output (16 bytes). + public static void Aes128EcbEncrypt(ReadOnlySpan key, ReadOnlySpan plaintext, Span ciphertext) + { + if(key.Length != 16 || plaintext.Length != 16 || ciphertext.Length != 16) + throw new ArgumentException("AES-128 block requires 16-byte key, plaintext, and output."); + + byte[] keyArr = key.ToArray(); + byte[] ptArr = plaintext.ToArray(); + + using(Aes aes = Aes.Create()) + { + aes.KeySize = 128; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.Key = keyArr; + + using ICryptoTransform enc = aes.CreateEncryptor(); + byte[] ctArr = enc.TransformFinalBlock(ptArr, 0, 16); + ctArr.AsSpan(0, 16).CopyTo(ciphertext); + } + } + + /// AES-128 ECB decrypt one block. + /// AES-128 key (16 bytes). + /// Ciphertext to decrypt (16 bytes). + /// Decrypted output (16 bytes). + public static void Aes128EcbDecrypt(ReadOnlySpan key, ReadOnlySpan ciphertext, Span plaintext) + { + if(key.Length != 16 || ciphertext.Length != 16 || plaintext.Length != 16) + throw new ArgumentException("AES-128 block requires 16-byte key, ciphertext, and output."); + + byte[] keyArr = key.ToArray(); + byte[] ctArr = ciphertext.ToArray(); + + using var aes = Aes.Create(); + aes.KeySize = 128; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.Key = keyArr; + + using ICryptoTransform dec = aes.CreateDecryptor(); + byte[] ptArr = dec.TransformFinalBlock(ctArr, 0, 16); + ptArr.AsSpan(0, 16).CopyTo(plaintext); + } + + /// AES-128 CBC decrypt. + /// AES-128 key (16 bytes). + /// Ciphertext to decrypt (16 bytes). + /// Decrypted output (16 bytes). + public static void AacsCbcDecrypt(ReadOnlySpan key, ReadOnlySpan ciphertext, Span plaintext) + { + if(key.Length != 16) + throw new ArgumentException("AES-128 key must be 16 bytes."); + + if(ciphertext.Length != plaintext.Length || ciphertext.Length == 0) + throw new ArgumentException("Ciphertext and plaintext spans must have the same non-zero length."); + + if((ciphertext.Length & 15) != 0) + throw new ArgumentException("Ciphertext length must be a multiple of 16."); + + byte[] keyArr = key.ToArray(); + byte[] ctArr = ciphertext.ToArray(); + byte[] ivArr = (byte[])AacsCbcIv.Clone(); + + using var aes = Aes.Create(); + aes.KeySize = 128; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.Key = keyArr; + aes.IV = ivArr; + + using ICryptoTransform dec = aes.CreateDecryptor(); + byte[] ptArr = dec.TransformFinalBlock(ctArr, 0, ctArr.Length); + + if(ptArr.Length != plaintext.Length) + throw new InvalidOperationException("AACS CBC decrypt length mismatch."); + + ptArr.AsSpan().CopyTo(plaintext); + } + + /// + /// Volume unique key from media key and volume ID. + /// + /// Media key (16 bytes). + /// Volume ID (16 bytes). + /// Volume unique key (16 bytes). + public static void DeriveVolumeUniqueKey(ReadOnlySpan mediaKey, ReadOnlySpan volumeId, + Span volumeUniqueKey) + { + if(mediaKey.Length != 16 || volumeId.Length != 16 || volumeUniqueKey.Length != 16) + throw new ArgumentException("MK, VID, and VUK must be 16 bytes."); + + Span tmp = stackalloc byte[16]; + Aes128EcbDecrypt(mediaKey, volumeId, tmp); + + for(int a = 0; a < 16; a++) + volumeUniqueKey[a] = (byte)(tmp[a] ^ volumeId[a]); + } + + /// Decrypt one CPS unit key from encrypted form. + /// Volume unique key (16 bytes). + /// Encrypted CPS unit key (16 bytes). + /// Decrypted CPS unit key (16 bytes). + public static void DecryptCpsUnitKey(ReadOnlySpan volumeUniqueKey, ReadOnlySpan encryptedUnitKey, + Span decryptedUnitKey) + { + if(volumeUniqueKey.Length != 16 || encryptedUnitKey.Length != 16 || decryptedUnitKey.Length != 16) + throw new ArgumentException("VUK and CPS unit keys must be 16 bytes."); + + Aes128EcbDecrypt(volumeUniqueKey, encryptedUnitKey, decryptedUnitKey); + } +} diff --git a/Aaru.Decryption/Aacs/AacsStreamDecrypt.cs b/Aaru.Decryption/Aacs/AacsStreamDecrypt.cs new file mode 100644 index 000000000..d2bf3ba63 --- /dev/null +++ b/Aaru.Decryption/Aacs/AacsStreamDecrypt.cs @@ -0,0 +1,135 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : AacsStreamDecrypt.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// Blu-ray AACS aligned-unit (6144-byte) decrypt. +// +// --[ License ] -------------------------------------------------------------- +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using System; + +namespace Aaru.Decryption.Aacs; + +/// Decrypts one 6144-byte aligned unit using decrypted CPS unit keys. +public static class AacsStreamDecrypt +{ + /// User-data sector size and bus-encryption block size. + public const int SectorLen = 2048; + + /// Three user sectors: CPS aligned unit length. + public const int AlignedUnitLen = 6144; + + /// + /// If the unit is marked unencrypted (MPEG-TS copy permission bits), returns + /// without changing the buffer. Otherwise tries each CPS unit key until MPEG-TS sync verifies. + /// + /// Unit to decrypt. + /// Decrypted CPS unit keys. + /// True if the unit was decrypted successfully. + public static bool TryDecryptAlignedUnit(Span unit, ReadOnlySpan decryptedCpsUnitKeys) + { + if(unit.Length != AlignedUnitLen) + throw new ArgumentException($"Unit must be {AlignedUnitLen} bytes.", nameof(unit)); + + if(decryptedCpsUnitKeys.Length == 0) + return false; + + // If the unit is not encrypted, return true. + if((unit[0] & 0xc0) == 0) + return true; + + byte[] work = new byte[AlignedUnitLen]; + + for(int i = 0; i < decryptedCpsUnitKeys.Length; i++) + { + unit.CopyTo(work); + + if(!TryDecryptUnitWithKey(work, decryptedCpsUnitKeys[i])) + continue; + + work.CopyTo(unit); + + return true; + } + + return false; + } + + /// Decrypts one unit with a given CPS unit key. + /// Unit to decrypt. + /// Decrypted CPS unit key. + /// True if the unit was decrypted successfully. + public static bool TryDecryptUnitWithKey(Span unit, ReadOnlySpan decryptedCpsUnitKey) + { + if(unit.Length != AlignedUnitLen) + throw new ArgumentException($"Unit must be {AlignedUnitLen} bytes.", nameof(unit)); + + if(decryptedCpsUnitKey.Length != 16) + throw new ArgumentException("CPS unit key must be 16 bytes.", nameof(decryptedCpsUnitKey)); + + Span key = stackalloc byte[16]; + AacsCrypto.Aes128EcbEncrypt(decryptedCpsUnitKey, unit.Slice(0, 16), key); + + for(int a = 0; a < 16; a++) + key[a] ^= unit[a]; + + ReadOnlySpan cipher = unit.Slice(16, AlignedUnitLen - 16); + Span plain = unit.Slice(16, AlignedUnitLen - 16); + byte[] tmp = new byte[AlignedUnitLen - 16]; + AacsCrypto.AacsCbcDecrypt(key, cipher, tmp); + tmp.CopyTo(plain); + + return VerifyAndNormalizeTransportStream(unit); + } + + /// Verifies and normalizes the transport stream. Removes copy-permission indicator bits. + /// Unit to verify and normalize. + /// True if the unit was verified and normalized successfully. + public static bool VerifyAndNormalizeTransportStream(Span unit) + { + if(unit.Length != AlignedUnitLen) + throw new ArgumentException($"Unit must be {AlignedUnitLen} bytes.", nameof(unit)); + + // In AACS aligned units, payload is expected to be 32 packets of 192 bytes. + // Each 192-byte packet has a 4-byte TP extra header followed by a TS packet. + for(int i = 0; i < AlignedUnitLen; i += 192) + { + // The TS sync byte (0x47) must be at offset +4 because of that extra 4-byte header. + if(unit[i + 4] != 0x47) + return false; + + // Clear copy-permission indicator bits in the TP extra header. + unit[i] &= 0x3f; + } + + return true; + } +} diff --git a/Aaru.Decryption/Aacs/UnitKeyRoParser.cs b/Aaru.Decryption/Aacs/UnitKeyRoParser.cs new file mode 100644 index 000000000..66452b4d7 --- /dev/null +++ b/Aaru.Decryption/Aacs/UnitKeyRoParser.cs @@ -0,0 +1,162 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : UnitKeyRoParser.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// Parses AACS/Unit_Key_RO.inf +// +// --[ License ] -------------------------------------------------------------- +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Buffers.Binary; + +namespace Aaru.Decryption.Aacs; + +/// Parsed CPS encrypted unit keys from Unit_Key_RO.inf. +public sealed class UnitKeyRoParseResult +{ + UnitKeyRoParseResult(byte[][] encryptedCpsUnitKeys, bool isAacs2Layout) + { + EncryptedCpsUnitKeys = encryptedCpsUnitKeys; + IsAacs2Layout = isAacs2Layout; + } + + /// One 16-byte encrypted key per CPS unit, index order. + public byte[][] EncryptedCpsUnitKeys { get; } + + /// True when the AACS2 key stride (64 bytes per key entry) was used. + public bool IsAacs2Layout { get; } + + /// Parses Blu-ray Unit_Key_RO.inf (AACS1 layout by default). + /// Data to parse. + /// True to try AACS2 layout. + /// Parsed CPS encrypted unit keys. + public static UnitKeyRoParseResult? TryParse(ReadOnlySpan data, bool tryAacs2 = false) + { + if(tryAacs2) + return TryParseInternal(data, true); + + UnitKeyRoParseResult? r = TryParseInternal(data, false); + + return r ?? TryParseInternal(data, true); + } + + /// Parses Blu-ray Unit_Key_RO.inf (AACS1 layout by default). + /// Data to parse. + /// True to try AACS2 layout. + /// Parsed CPS encrypted unit keys. + static UnitKeyRoParseResult? TryParseInternal(ReadOnlySpan data, bool aacs2) + { + if(data.Length < 20) + return null; + + byte numBdmvDir = data[17]; + + if(numBdmvDir < 1) + return null; + + if(data.Length < 4) + return null; + + uint ukPos = BinaryPrimitives.ReadUInt32BigEndian(data); + + if(data.Length - 2 < ukPos) + return null; + + if((int)ukPos >= data.Length) + return null; + + ushort numUk = BinaryPrimitives.ReadUInt16BigEndian(data.Slice((int)ukPos, 2)); + + if(numUk < 1) + return null; + + if(data.Length - (int)ukPos < 16) + return null; + + if((data.Length - (int)ukPos - 16) / 48 < numUk) + return null; + + ReadOnlySpan emptyKey = stackalloc byte[16]; + + if(aacs2 && numUk > 1 && data.Length >= 48 + 48 + 16) + { + if(data.Slice(48 + 48 + 16, 16).SequenceEqual(emptyKey)) + aacs2 = false; + else if(data.Length < (int)ukPos + 64 * numUk + 16) + return null; + } + + byte[][] encKeys = new byte[numUk][]; + + uint pos = ukPos; + + for(uint i = 0; i < numUk; i++) + { + pos += 48; + + if(pos + 16 > (uint)data.Length) + return null; + + byte[] key = new byte[16]; + data.Slice((int)pos, 16).CopyTo(key); + encKeys[i] = key; + + if(aacs2) + pos += 16; + } + + return new UnitKeyRoParseResult(encKeys, aacs2); + } + + /// + /// Interprets a tag or buffer as raw concatenated 16-byte encrypted CPS keys (no .inf wrapper). + /// + /// Data to parse. + /// Parsed CPS encrypted unit keys. + public static UnitKeyRoParseResult? TryParseRawEncryptedKeys(ReadOnlySpan data) + { + if(data.Length < 16 || data.Length % 16 != 0) + return null; + + int n = data.Length / 16; + byte[][] keys = new byte[n][]; + ReadOnlySpan p = data; + + for(int i = 0; i < n; i++) + { + byte[] key = new byte[16]; + p.Slice(i * 16, 16).CopyTo(key); + keys[i] = key; + } + + return new UnitKeyRoParseResult(keys, false); + } +} From a9438b66110864485f862df4b357f127e4219732 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 15:52:07 +0200 Subject: [PATCH 05/12] [convert] Move CSS to its own pipeline --- Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs | 222 ++++++++++ .../Image/Convert/CSS/CssDvdSectorDecrypt.cs | 415 ++++++++++++++++++ 2 files changed, 637 insertions(+) create mode 100644 Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs create mode 100644 Aaru.Core/Image/Convert/CSS/CssDvdSectorDecrypt.cs diff --git a/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs b/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs new file mode 100644 index 000000000..2fe09b8d6 --- /dev/null +++ b/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs @@ -0,0 +1,222 @@ +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +public partial class Convert +{ + /// Converts CSS-encrypted DVD optical sectors. + /// Input optical media image. + /// Output optical media image. + /// True to use long sector reads. + /// Error number. + ErrorNumber ConvertCssDvdOpticalSectors(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, + bool useLong) + { + if(_aborted) return ErrorNumber.NoError; + InitProgress?.Invoke(); + InitProgress2?.Invoke(); + byte[] generatedTitleKeys = null; + int currentTrack = 0; + + foreach(Track track in inputOptical.Tracks) + { + if(_aborted) break; + + UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_in_track_0_of_1, + currentTrack + 1, + inputOptical.Tracks.Count), + currentTrack, + inputOptical.Tracks.Count); + + ulong doneSectors = 0; + ulong trackSectors = track.EndSector - track.StartSector + 1; + + while(doneSectors < trackSectors) + { + if(_aborted) break; + byte[] sector; + + uint sectorsToDo; + + if(trackSectors - doneSectors >= _count) + sectorsToDo = _count; + else + sectorsToDo = (uint)(trackSectors - doneSectors); + + UpdateProgress2?.Invoke(string.Format(UI.Converting_sectors_0_to_1_in_track_2, + doneSectors + track.StartSector, + doneSectors + sectorsToDo + track.StartSector, + track.Sequence), + (long)doneSectors, + (long)trackSectors); + + bool useNotLong = false; + bool result = false; + SectorStatus sectorStatus = SectorStatus.NotDumped; + SectorStatus[] sectorStatusArray = new SectorStatus[1]; + ErrorNumber errno; + + if(useLong) + { + errno = sectorsToDo == 1 + ? inputOptical.ReadSectorLong(doneSectors + track.StartSector, + false, + out sector, + out sectorStatus) + : inputOptical.ReadSectorsLong(doneSectors + track.StartSector, + false, + sectorsToDo, + out sector, + out sectorStatusArray); + + if(errno == ErrorNumber.NoError) + { + CssDvdSectorDecrypt.ApplyCssAfterReadLong(ref sector, + ref sectorStatus, + sectorStatusArray, + sectorsToDo, + sectorsToDo == 1, + inputOptical, + doneSectors + track.StartSector, + _plugins, + ref generatedTitleKeys, + () => _aborted, + MODULE_NAME); + + result = sectorsToDo == 1 + ? outputOptical.WriteSectorLong(sector, + doneSectors + track.StartSector, + false, + sectorStatus) + : outputOptical.WriteSectorsLong(sector, + doneSectors + track.StartSector, + false, + sectorsToDo, + sectorStatusArray); + } + else + { + result = true; + + if(_force) + { + ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing, + errno, + doneSectors + track.StartSector)); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + doneSectors + track.StartSector)); + + return ErrorNumber.WriteError; + } + } + + if(!result && sector.Length % 2352 != 0) + { + if(!_force) + { + StoppingErrorMessage + ?.Invoke(UI.Input_image_is_not_returning_raw_sectors_use_force_if_you_want_to_continue); + + return ErrorNumber.InOutError; + } + + useNotLong = true; + } + } + + if(!useLong || useNotLong) + { + errno = sectorsToDo == 1 + ? inputOptical.ReadSector(doneSectors + track.StartSector, + false, + out sector, + out sectorStatus) + : inputOptical.ReadSectors(doneSectors + track.StartSector, + false, + sectorsToDo, + out sector, + out sectorStatusArray); + + if(errno == ErrorNumber.NoError) + { + CssDvdSectorDecrypt.ApplyCssAfterRead(ref sector, + ref sectorStatus, + sectorStatusArray, + sectorsToDo, + sectorsToDo == 1, + inputOptical, + doneSectors + track.StartSector, + _plugins, + ref generatedTitleKeys, + () => _aborted, + MODULE_NAME); + + result = sectorsToDo == 1 + ? outputOptical.WriteSector(sector, + doneSectors + track.StartSector, + false, + sectorStatus) + : outputOptical.WriteSectors(sector, + doneSectors + track.StartSector, + false, + sectorsToDo, + sectorStatusArray); + } + else + { + result = true; + + if(_force) + { + ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing, + errno, + doneSectors + track.StartSector)); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + doneSectors + track.StartSector)); + + return ErrorNumber.WriteError; + } + } + } + + if(!result) + { + if(_force) + { + ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing, + outputOptical.ErrorMessage, + doneSectors + track.StartSector)); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + doneSectors + track.StartSector)); + + return ErrorNumber.WriteError; + } + } + + doneSectors += sectorsToDo; + } + + currentTrack++; + } + + EndProgress2?.Invoke(); + EndProgress?.Invoke(); + + return ErrorNumber.NoError; + } +} diff --git a/Aaru.Core/Image/Convert/CSS/CssDvdSectorDecrypt.cs b/Aaru.Core/Image/Convert/CSS/CssDvdSectorDecrypt.cs new file mode 100644 index 000000000..738175729 --- /dev/null +++ b/Aaru.Core/Image/Convert/CSS/CssDvdSectorDecrypt.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Core.Media; +using Aaru.Decryption.DVD; +using Aaru.Devices; +using Aaru.Localization; +using Aaru.Logging; +using MediaType = Aaru.CommonTypes.MediaType; + +namespace Aaru.Core.Image; + +/// CSS decryption and tagging for DVD optical pipelines. +static class CssDvdSectorDecrypt +{ + internal static bool IsDvdMedia(MediaType mediaType) => + mediaType is MediaType.DVDROM + or MediaType.DVDR + or MediaType.DVDRW + or MediaType.DVDPR + or MediaType.DVDPRW + or MediaType.DVDPRWDL + or MediaType.DVDRDL + or MediaType.DVDPRDL + or MediaType.DVDRAM + or MediaType.DVDRWDL + or MediaType.DVDDownload + or MediaType.PS2DVD + or MediaType.PS3DVD + or MediaType.XGD + or MediaType.XGD2 + or MediaType.XGD3 + or MediaType.Nuon; + + /// Generates DVD title keys. + /// Input optical media image. + /// Plugin register. + /// Generated title keys. + /// Log module name. + /// Is aborted function. + internal static void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins, + ref byte[] generatedTitleKeys, string logModuleName, + Func isAborted) + { + if(isAborted()) return; + + List allPartitions = Partitions.GetAll(inputOptical); + + byte[] lastKeysFromVideoTsFilesystem = null; + + foreach(IReadOnlyFilesystem rofs in plugins.ReadOnlyFilesystems.Values) + { + if(isAborted()) return; + + List supportedPartitions = []; + + foreach(Partition partition in allPartitions) + { + if(!rofs.Identify(inputOptical, partition)) + continue; + + supportedPartitions.Add(partition); + } + + if(supportedPartitions.Count == 0) + continue; + + bool hasVideoTs = false; + + foreach(Partition partition in supportedPartitions) + { + if(CSS.HasVideoTsFolder(inputOptical, rofs, partition)) + { + hasVideoTs = true; + + break; + } + } + + if(!hasVideoTs) + continue; + + AaruLogging.Debug(logModuleName, UI.Generating_decryption_keys); + + byte[] keys = CSS.GenerateTitleKeys(inputOptical, + supportedPartitions, + inputOptical.Info.Sectors, + rofs); + + lastKeysFromVideoTsFilesystem = keys; + + if(!TitleKeysBufferIsAllZero(keys)) + { + generatedTitleKeys = keys; + + return; + } + } + + if(lastKeysFromVideoTsFilesystem != null) + generatedTitleKeys = lastKeysFromVideoTsFilesystem; + } + + /// Checks if the title keys buffer is all zero. + /// Title keys buffer. + /// True if the title keys buffer is all zero. + static bool TitleKeysBufferIsAllZero(byte[] keys) + { + if(keys is null || keys.Length == 0) + return true; + + for(int i = 0; i < keys.Length; i++) + { + if(keys[i] != 0) + return false; + } + + return true; + } + + /// Applies CSS after reading a sector. + /// Sector data. + /// Sector status. + /// Sector status array. + /// Number of sectors to do. + /// True if reading one sector. + /// Input optical media image. + /// Sector address. + /// Plugin register. + /// Generated title keys. + /// Is aborted function. + /// Log module name. + internal static void ApplyCssAfterRead(ref byte[] sector, ref SectorStatus sectorStatus, + SectorStatus[] sectorStatusArray, uint sectorsToDo, bool readOneSector, + IOpticalMediaImage inputOptical, ulong sectorAddress, + PluginRegister plugins, ref byte[] generatedTitleKeys, + Func isAborted, string logModuleName) + { + if(isAborted()) return; + + int blockSize = sector.Length / (int)sectorsToDo; + + if(sector.Length % sectorsToDo != 0 || blockSize <= 0) + return; + + if(!Mpeg.ContainsMpegPackets(sector, sectorsToDo, (uint)blockSize)) + { + SetStatusesDumped(ref sectorStatus, sectorStatusArray, sectorsToDo, readOneSector); + + return; + } + + bool[] blockAppliedCssDecrypt = new bool[sectorsToDo]; + + DecryptDvdSector(ref sector, + inputOptical, + sectorAddress, + sectorsToDo, + plugins, + ref generatedTitleKeys, + isAborted, + logModuleName, + blockAppliedCssDecrypt); + + ApplyDecryptFlagsToStatuses(blockAppliedCssDecrypt, + sectorsToDo, + ref sectorStatus, + sectorStatusArray, + readOneSector); + } + + /// Applies CSS after reading a long sector. + /// Sector data. + /// Sector status. + /// Sector status array. + /// Number of sectors to do. + /// True if reading one sector. + /// Input optical media image. + /// Sector address. + /// Plugin register. + /// Generated title keys. + /// Is aborted function. + /// Log module name. + internal static void ApplyCssAfterReadLong(ref byte[] sector, ref SectorStatus sectorStatus, + SectorStatus[] sectorStatusArray, uint sectorsToDo, bool readOneSector, + IOpticalMediaImage inputOptical, ulong sectorAddress, + PluginRegister plugins, ref byte[] generatedTitleKeys, + Func isAborted, string logModuleName) + { + if(isAborted()) return; + + if((long)sector.Length != (long)sectorsToDo * Mpeg.DvdLongSectorStride) + return; + + if(!Mpeg.ContainsMpegPacketsLong(sector, sectorsToDo)) + { + SetStatusesDumped(ref sectorStatus, sectorStatusArray, sectorsToDo, readOneSector); + + return; + } + + bool[] blockAppliedCssDecrypt = new bool[sectorsToDo]; + + DecryptDvdSectorLong(ref sector, + inputOptical, + sectorAddress, + sectorsToDo, + plugins, + ref generatedTitleKeys, + isAborted, + logModuleName, + blockAppliedCssDecrypt); + + ApplyDecryptFlagsToStatuses(blockAppliedCssDecrypt, + sectorsToDo, + ref sectorStatus, + sectorStatusArray, + readOneSector); + } + + /// Sets all the statuses to dumped. + /// Sector status. + /// Sector status array. + /// Number of sectors to do. + /// True if reading one sector. + static void SetStatusesDumped(ref SectorStatus sectorStatus, SectorStatus[] sectorStatusArray, uint sectorsToDo, + bool readOneSector) + { + if(readOneSector) + sectorStatus = SectorStatus.Dumped; + else + { + for(uint i = 0; i < sectorsToDo; i++) + sectorStatusArray[i] = SectorStatus.Dumped; + } + } + + /// Applies the decrypt flags to the statuses. + /// Block applied CSS decrypt. + /// Number of sectors to do. + /// Sector status. + /// Sector status array. + /// True if reading one sector. + static void ApplyDecryptFlagsToStatuses(bool[] blockAppliedCssDecrypt, uint sectorsToDo, + ref SectorStatus sectorStatus, SectorStatus[] sectorStatusArray, + bool readOneSector) + { + if(readOneSector) + sectorStatus = blockAppliedCssDecrypt[0] ? SectorStatus.Unencrypted : SectorStatus.Dumped; + else + { + for(uint i = 0; i < sectorsToDo; i++) + sectorStatusArray[i] = blockAppliedCssDecrypt[i] ? SectorStatus.Unencrypted : SectorStatus.Dumped; + } + } + + /// Decrypts a DVD sector. + /// Sector data. + /// Input optical media image. + /// Sector address. + /// Number of sectors to do. + /// Plugin register. + /// Generated title keys. + /// Is aborted function. + /// Log module name. + /// Block applied CSS decrypt. + static void DecryptDvdSector(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress, + uint sectorsToDo, PluginRegister plugins, ref byte[] generatedTitleKeys, + Func isAborted, string logModuleName, bool[] blockAppliedCssDecrypt) + { + if(isAborted()) return; + + int blockSize = sector.Length / (int)sectorsToDo; + + if(sector.Length % sectorsToDo != 0 || blockSize <= 0) + return; + + if(!Mpeg.ContainsMpegPackets(sector, sectorsToDo, (uint)blockSize)) return; + + byte[] cmi, titleKey; + + if(sectorsToDo == 1) + { + if(inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdSectorCmi, out cmi) == + ErrorNumber.NoError && + inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdTitleKeyDecrypted, out titleKey) == + ErrorNumber.NoError) + sector = CSS.DecryptSector(sector, titleKey, cmi, 1, (uint)blockSize, blockAppliedCssDecrypt); + else + { + if(generatedTitleKeys == null) + GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys, logModuleName, isAborted); + + if(generatedTitleKeys != null) + { + sector = CSS.DecryptSector(sector, + generatedTitleKeys.Skip((int)(5 * sectorAddress)).Take(5).ToArray(), + null, + 1, + (uint)blockSize, + blockAppliedCssDecrypt); + } + } + } + else + { + if(inputOptical.ReadSectorsTag(sectorAddress, false, sectorsToDo, SectorTagType.DvdSectorCmi, out cmi) == + ErrorNumber.NoError && + inputOptical.ReadSectorsTag(sectorAddress, + false, + sectorsToDo, + SectorTagType.DvdTitleKeyDecrypted, + out titleKey) == + ErrorNumber.NoError) + sector = CSS.DecryptSector(sector, titleKey, cmi, sectorsToDo, (uint)blockSize, blockAppliedCssDecrypt); + else + { + if(generatedTitleKeys == null) + GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys, logModuleName, isAborted); + + if(generatedTitleKeys != null) + { + sector = CSS.DecryptSector(sector, + generatedTitleKeys.Skip((int)(5 * sectorAddress)) + .Take((int)(5 * sectorsToDo)) + .ToArray(), + null, + sectorsToDo, + (uint)blockSize, + blockAppliedCssDecrypt); + } + } + } + } + + /// Decrypts a DVD long sector. + /// Sector data. + /// Input optical media image. + /// Sector address. + /// Number of sectors to do. + /// Plugin register. + /// Generated title keys. + /// Is aborted function. + /// Log module name. + /// Block applied CSS decrypt. + static void DecryptDvdSectorLong(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress, + uint sectorsToDo, PluginRegister plugins, ref byte[] generatedTitleKeys, + Func isAborted, string logModuleName, bool[] blockAppliedCssDecrypt) + { + if(isAborted()) return; + + if((long)sector.Length != (long)sectorsToDo * Mpeg.DvdLongSectorStride) + return; + + if(!Mpeg.ContainsMpegPacketsLong(sector, sectorsToDo)) return; + + byte[] cmi, titleKey; + + if(sectorsToDo == 1) + { + if(inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdSectorCmi, out cmi) == + ErrorNumber.NoError && + inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdTitleKeyDecrypted, out titleKey) == + ErrorNumber.NoError) + sector = CSS.DecryptSectorLong(sector, titleKey, cmi, 1, 2048, blockAppliedCssDecrypt); + else + { + if(generatedTitleKeys == null) + GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys, logModuleName, isAborted); + + if(generatedTitleKeys != null) + { + sector = CSS.DecryptSectorLong(sector, + generatedTitleKeys.Skip((int)(5 * sectorAddress)).Take(5).ToArray(), + null, + 1, + 2048, + blockAppliedCssDecrypt); + } + } + } + else + { + if(inputOptical.ReadSectorsTag(sectorAddress, false, sectorsToDo, SectorTagType.DvdSectorCmi, out cmi) == + ErrorNumber.NoError && + inputOptical.ReadSectorsTag(sectorAddress, + false, + sectorsToDo, + SectorTagType.DvdTitleKeyDecrypted, + out titleKey) == + ErrorNumber.NoError) + sector = CSS.DecryptSectorLong(sector, titleKey, cmi, sectorsToDo, 2048, blockAppliedCssDecrypt); + else + { + if(generatedTitleKeys == null) + GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys, logModuleName, isAborted); + + if(generatedTitleKeys != null) + { + sector = CSS.DecryptSectorLong(sector, + generatedTitleKeys.Skip((int)(5 * sectorAddress)) + .Take((int)(5 * sectorsToDo)) + .ToArray(), + null, + sectorsToDo, + 2048, + blockAppliedCssDecrypt); + } + } + } + } +} From b4e3b04e1d7b934ccbdfb2e371ff5f0472a9d52e Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 15:59:41 +0200 Subject: [PATCH 06/12] [convert] Add AACS pipeline --- .../Convert/AACS/AacsBdExtentResolver.cs | 222 ++++++++ .../Convert/AACS/AacsBdOpticalPipeline.cs | 472 ++++++++++++++++++ .../Image/Convert/AACS/AacsKeyResolver.cs | 249 +++++++++ Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs | 57 +++ 4 files changed, 1000 insertions(+) create mode 100644 Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs create mode 100644 Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs create mode 100644 Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs create mode 100644 Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs diff --git a/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs b/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs new file mode 100644 index 000000000..033c0d1f8 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Filesystems; + +namespace Aaru.Core.Image; + +internal static class AacsBdExtentResolver +{ + internal readonly struct LbaRange + { + internal ulong Start { get; init; } + internal ulong End { get; init; } + } + + /// + /// Resolves the stream file extents for a Blu-ray optical media image. + /// It searches for the stream file extents in the /BDMV/STREAM and + /// /BDMV/STREAM/SSIF directories. + /// + /// Input optical media image. + /// Plugin register. + /// Stream file extents. + /// Error message. + /// Error number. + internal static ErrorNumber ResolveStreamFileExtents(IOpticalMediaImage inputOptical, + PluginRegister plugins, + out List ranges, + out string error) + { + ranges = []; + error = null; + + IReadOnlyFilesystem udfRo = null; + UDF udf = null; + + foreach(IFilesystem fs in plugins.Filesystems.Values) + { + if(fs is not UDF udfPlugin) continue; + + Partition wholeImage = new() + { + Start = 0, + Length = inputOptical.Info.Sectors, + Size = inputOptical.Info.Sectors * inputOptical.Info.SectorSize, + Sequence = 0, + Type = "UDF" + }; + + if(!udfPlugin.Identify(inputOptical, wholeImage)) continue; + + ErrorNumber mountErr = udfPlugin.Mount(inputOptical, wholeImage, Encoding.UTF8, null, null); + + if(mountErr != ErrorNumber.NoError) + { + error = "UDF mount failed while resolving Blu-ray stream extents."; + + return mountErr; + } + + udfRo = udfPlugin; + udf = udfPlugin; + + break; + } + + if(udfRo is null || udf is null) + { + error = "Could not mount UDF filesystem for Blu-ray stream extent discovery."; + + return ErrorNumber.NotSupported; + } + + try + { + ErrorNumber errno = AddDirectoryExtents(udfRo, udf, "/BDMV/STREAM", ".M2TS", ref ranges); + + if(errno != ErrorNumber.NoError) + { + error = "Could not resolve extents for /BDMV/STREAM."; + + return errno; + } + + errno = AddDirectoryExtents(udfRo, udf, "/BDMV/STREAM/SSIF", ".SSIF", ref ranges); + + if(errno != ErrorNumber.NoError && errno != ErrorNumber.NoSuchFile) + { + error = "Could not resolve extents for /BDMV/STREAM/SSIF."; + + return errno; + } + + if(ranges.Count == 0) + { + error = "No stream file extents found under /BDMV/STREAM."; + + return ErrorNumber.NoData; + } + + ranges.Sort(static (a, b) => a.Start.CompareTo(b.Start)); + ranges = MergeRanges(ranges); + + return ErrorNumber.NoError; + } + finally + { + udfRo.Unmount(); + } + } + + /// Checks if a given LBA is allowed for decryption. + /// Stream file extents. + /// LBA to check. + /// True if the LBA is allowed for decryption. + internal static bool IsLbaAllowed(List ranges, ulong lba) + { + int lo = 0; + int hi = ranges.Count - 1; + + while(lo <= hi) + { + int mid = lo + (hi - lo) / 2; + LbaRange range = ranges[mid]; + + if(lba < range.Start) + hi = mid - 1; + else if(lba > range.End) + lo = mid + 1; + else + return true; + } + + return false; + } + + + /// Adds the extents for a given directory to the list of stream file extents. + /// Read-only filesystem. + /// UDF filesystem. + /// Directory path. + /// Extension of the files to add. + /// List of stream file extents. + /// Error number. + static ErrorNumber AddDirectoryExtents(IReadOnlyFilesystem roFs, UDF udf, string dirPath, string extension, + ref List ranges) + { + ErrorNumber errno = roFs.OpenDir(dirPath, out IDirNode dirNode); + + if(errno != ErrorNumber.NoError) return errno; + + try + { + while(true) + { + errno = roFs.ReadDir(dirNode, out string filename); + + if(errno != ErrorNumber.NoError) return errno; + + if(filename is null) break; + + if(!filename.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) continue; + + string fullPath = dirPath + "/" + filename; + + errno = udf.GetFilePhysicalSectorExtents(fullPath, out List<(ulong startSector, uint sectorCount)> extents); + + if(errno != ErrorNumber.NoError) return errno; + + foreach((ulong startSector, uint sectorCount) extent in extents) + { + if(extent.sectorCount == 0) continue; + + ranges.Add(new LbaRange + { + Start = extent.startSector, + End = extent.startSector + extent.sectorCount - 1 + }); + } + } + } + finally + { + roFs.CloseDir(dirNode); + } + + return ErrorNumber.NoError; + } + + /// Merges the extents for a given directory to the list of stream file extents. + /// List of stream file extents. + /// Merged list of stream file extents. + static List MergeRanges(List ranges) + { + if(ranges.Count <= 1) return ranges; + + List merged = []; + LbaRange current = ranges[0]; + + for(int i = 1; i < ranges.Count; i++) + { + LbaRange next = ranges[i]; + + if(next.Start <= current.End + 1) + { + current = new LbaRange { Start = current.Start, End = Math.Max(current.End, next.End) }; + continue; + } + + merged.Add(current); + current = next; + } + + merged.Add(current); + + return merged; + } +} diff --git a/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs b/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs new file mode 100644 index 000000000..eea731224 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decryption.Aacs; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +/// Blu-ray AACS sector pipeline for convert/merge: LBA-aligned 6144-byte CPS units with output staging. +internal static class AacsBdOpticalPipeline +{ + internal readonly struct Ui + { + public Action OnInitProgress { get; init; } + public Action OnInitProgress2 { get; init; } + public Action OnEndProgress { get; init; } + public Action OnEndProgress2 { get; init; } + public Action OnTrackProgress { get; init; } + public Action OnSectorRangeProgress { get; init; } + public Action ErrorMessage { get; init; } + public Action StoppingErrorMessage { get; init; } + } + + /// Runs the Blu-ray AACS sector pipeline. + /// Input optical media image. + /// Output optical media image. + /// Batch count. + /// Force flag. + /// Aborted flag. + /// Decrypted CPS unit keys. + /// Allow decrypt LBA function. + /// UI. + /// Error number. + internal static ErrorNumber Run(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, uint batchCount, + bool force, ref bool aborted, byte[][] decryptedCpsUnitKeys, + Func allowDecryptLba, Ui ui) + { + ui.OnInitProgress?.Invoke(); + ui.OnInitProgress2?.Invoke(); + + int currentTrack = 0; + + foreach(Track track in inputOptical.Tracks) + { + if(aborted) break; + + ui.OnTrackProgress?.Invoke(currentTrack, inputOptical.Tracks.Count); + + ulong doneSectors = 0; + ulong trackSectors = track.EndSector - track.StartSector + 1; + + List pending = []; + List pendingLba = []; + List pendingSt = []; + + while(doneSectors < trackSectors) + { + if(aborted) break; + + uint sectorsToDo = trackSectors - doneSectors >= batchCount + ? batchCount + : (uint)(trackSectors - doneSectors); + + ui.OnSectorRangeProgress?.Invoke(doneSectors + track.StartSector, + doneSectors + sectorsToDo + track.StartSector, + track.Sequence, + (long)doneSectors, + (long)trackSectors); + + ErrorNumber errno = inputOptical.ReadSectors(doneSectors + track.StartSector, + false, + sectorsToDo, + out byte[] sectorData, + out SectorStatus[] sectorStatusArray); + + if(errno != ErrorNumber.NoError) + { + if(force) + { + ErrorNumber pend = FlushPendingOnReadSkip(outputOptical, ref pending, ref pendingLba, + ref pendingSt, force, ui); + + if(pend != ErrorNumber.NoError) return pend; + + ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing, + errno, + doneSectors + track.StartSector)); + } + else + { + if(pending.Count > 0) + { + ui.StoppingErrorMessage?.Invoke(string.Format( + Aaru.Localization.Core.Aacs_incomplete_cps_unit_pending_before_read_error, + pendingLba[0])); + + return errno; + } + + ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + doneSectors + track.StartSector)); + + return errno; + } + + doneSectors += sectorsToDo; + + continue; + } + + int bps = sectorData.Length / (int)sectorsToDo; + + if(bps != AacsStreamDecrypt.SectorLen || sectorData.Length % sectorsToDo != 0) + { + ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_bd_decrypt_requires_0_byte_sectors, + bps)); + + return ErrorNumber.InOutError; + } + + for(uint i = 0; i < sectorsToDo; i++) + { + byte[] one = new byte[AacsStreamDecrypt.SectorLen]; + Buffer.BlockCopy(sectorData, (int)(i * AacsStreamDecrypt.SectorLen), one, 0, AacsStreamDecrypt.SectorLen); + ulong lba = doneSectors + track.StartSector + i; + SectorStatus st = sectorStatusArray[i]; + + bool allowDecrypt = allowDecryptLba is null || allowDecryptLba(lba); + + ErrorNumber step = ProcessOneUserSector(outputOptical, + decryptedCpsUnitKeys, + one, + lba, + allowDecrypt, + st, + ref pending, + ref pendingLba, + ref pendingSt, + force, + ui); + + if(step != ErrorNumber.NoError) return step; + } + + doneSectors += sectorsToDo; + } + + ErrorNumber flushErr = FlushPendingAtTrackEnd(outputOptical, + ref pending, + ref pendingLba, + ref pendingSt, + force, + ui); + + if(flushErr != ErrorNumber.NoError) return flushErr; + + currentTrack++; + } + + ui.OnEndProgress2?.Invoke(); + ui.OnEndProgress?.Invoke(); + + return ErrorNumber.NoError; + } + + /// Flushes the pending sectors on read skip. + /// Output optical media image. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushPendingOnReadSkip(IWritableOpticalImage outputOptical, ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + if(pending.Count == 0) return ErrorNumber.NoError; + + ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_after_read_error_continuing, + pending.Count, + pendingLba[0])); + + for(int i = 0; i < pending.Count; i++) + { + ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui); + + if(step != ErrorNumber.NoError) return step; + } + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return ErrorNumber.NoError; + } + + /// Processes one user sector. + /// Output optical media image. + /// Decrypted CPS unit keys. + /// Sector data. + /// LBA of the sector. + /// Allow decrypt flag. + /// Read status of the sector. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber ProcessOneUserSector(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys, + byte[] sector, ulong lba, bool allowDecrypt, SectorStatus readStatus, + ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + if(!allowDecrypt) + { + ErrorNumber flush = FlushPendingAsDumped(outputOptical, ref pending, ref pendingLba, ref pendingSt, force, ui); + + if(flush != ErrorNumber.NoError) return flush; + + return WriteOne(outputOptical, lba, sector, SectorStatus.Dumped, force, ui); + } + + if(pending.Count == 0) + { + if((sector[0] & 0xc0) == 0) + return WriteOne(outputOptical, lba, sector, SectorStatus.Dumped, force, ui); + + pending.Add(sector); + pendingLba.Add(lba); + pendingSt.Add(readStatus); + + return ErrorNumber.NoError; + } + + pending.Add(sector); + pendingLba.Add(lba); + pendingSt.Add(readStatus); + + if(pending.Count < 3) return ErrorNumber.NoError; + + return FlushCompleteUnit(outputOptical, + decryptedCpsUnitKeys, + ref pending, + ref pendingLba, + ref pendingSt, + force, + ui); + } + + /// Flushes a complete unit. + /// Output optical media image. + /// Decrypted CPS unit keys. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushCompleteUnit(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys, + ref List pending, ref List pendingLba, + ref List pendingSt, bool force, Ui ui) + { + byte[] unit = new byte[AacsStreamDecrypt.AlignedUnitLen]; + Buffer.BlockCopy(pending[0], 0, unit, 0, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(pending[1], 0, unit, AacsStreamDecrypt.SectorLen, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(pending[2], 0, unit, AacsStreamDecrypt.SectorLen * 2, AacsStreamDecrypt.SectorLen); + + if(!AacsStreamDecrypt.TryDecryptAlignedUnit(unit, decryptedCpsUnitKeys)) + { + if(!force) + { + ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_could_not_decrypt_cps_unit_starting_at_0, + pendingLba[0])); + + return ErrorNumber.WriteError; + } + + ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_could_not_decrypt_cps_unit_starting_at_0_continuing, + pendingLba[0])); + + ErrorNumber w = WriteThree(outputOptical, + pending[0], + pending[1], + pending[2], + pendingLba[0], + pendingSt[0], + pendingSt[1], + pendingSt[2], + force, + ui); + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return w; + } + + SectorStatus[] ok = [SectorStatus.Unencrypted, SectorStatus.Unencrypted, SectorStatus.Unencrypted]; + ErrorNumber e = WriteSectorsFromBuffer(outputOptical, unit, pendingLba[0], ok, force, ui); + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return e; + } + + /// Writes three sectors from a buffer. + /// Output optical media image. + /// Unit data. + /// First LBA of the sectors. + /// Statuses of the sectors. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber WriteSectorsFromBuffer(IWritableOpticalImage outputOptical, byte[] unit, ulong firstLba, + SectorStatus[] threeStatuses, bool force, Ui ui) + { + bool result = outputOptical.WriteSectors(unit, firstLba, false, 3, threeStatuses); + + if(!result) + { + if(force) + { + ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing, + outputOptical.ErrorMessage, + firstLba)); + } + else + { + ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + firstLba)); + + return ErrorNumber.WriteError; + } + } + + return ErrorNumber.NoError; + } + + /// Flushes the pending sectors as dumped. + /// Output optical media image. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushPendingAsDumped(IWritableOpticalImage outputOptical, ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + for(int i = 0; i < pending.Count; i++) + { + ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui); + + if(step != ErrorNumber.NoError) return step; + } + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return ErrorNumber.NoError; + } + + /// Writes three sectors from a buffer. + /// Output optical media image. + /// First sector data. + /// Second sector data. + /// Third sector data. + /// First LBA of the sectors. + /// Statuses of the sectors. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber WriteThree(IWritableOpticalImage outputOptical, byte[] a, byte[] b, byte[] c, ulong firstLba, + SectorStatus sa, SectorStatus sb, SectorStatus sc, bool force, Ui ui) + { + byte[] buf = new byte[AacsStreamDecrypt.AlignedUnitLen]; + Buffer.BlockCopy(a, 0, buf, 0, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(b, 0, buf, AacsStreamDecrypt.SectorLen, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(c, 0, buf, AacsStreamDecrypt.SectorLen * 2, AacsStreamDecrypt.SectorLen); + + SectorStatus[] sts = [sa, sb, sc]; + + return WriteSectorsFromBuffer(outputOptical, buf, firstLba, sts, force, ui); + } + + /// Writes one sector. + /// Output optical media image. + /// LBA of the sector. + /// Sector data. + /// Status of the sector. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber WriteOne(IWritableOpticalImage outputOptical, ulong lba, byte[] sector, SectorStatus status, + bool force, Ui ui) + { + bool result = outputOptical.WriteSector(sector, lba, false, status); + + if(!result) + { + if(force) + { + ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing, + outputOptical.ErrorMessage, + lba)); + } + else + { + ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + lba)); + + return ErrorNumber.WriteError; + } + } + + return ErrorNumber.NoError; + } + + /// Flushes the pending sectors at track end. + /// Output optical media image. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushPendingAtTrackEnd(IWritableOpticalImage outputOptical, ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + if(pending.Count == 0) return ErrorNumber.NoError; + + if(!force) + { + ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_at_track_end, + pending.Count, + pendingLba[0])); + + return ErrorNumber.WriteError; + } + + ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_at_track_end_continuing, + pending.Count, + pendingLba[0])); + + for(int i = 0; i < pending.Count; i++) + { + ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui); + + if(step != ErrorNumber.NoError) return step; + } + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return ErrorNumber.NoError; + } +} diff --git a/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs b/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs new file mode 100644 index 000000000..6035c36b8 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs @@ -0,0 +1,249 @@ +using System; +using System.Text; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decryption.Aacs; + +namespace Aaru.Core.Image; + +/// Loads VUK and decrypts CPS unit keys for Blu-ray AACS conversion. +public static class AacsKeyResolver +{ + static readonly string[] UnitKeyPaths = + [ + "AACS/Unit_Key_RO.inf", + "AACS\\Unit_Key_RO.inf" + ]; + + /// Checks if every byte in the buffer is zero. + /// Buffer to check. + /// True if every byte in the buffer is zero. + public static bool IsAllZero(ReadOnlySpan buffer) + { + for(int i = 0; i < buffer.Length; i++) + { + if(buffer[i] != 0) + return false; + } + + return true; + } + + /// + /// Obtains decrypted 16-byte CPS unit keys in disc order, or if keys cannot be loaded. + /// + /// Input optical media image. + /// Plugin register. + /// Encoding. + /// Error message. + /// Decrypted CPS unit keys. + public static byte[][]? TryGetDecryptedCpsUnitKeys(IOpticalMediaImage image, PluginRegister plugins, + Encoding? encoding, out string? errorMessage) + { + errorMessage = null; + encoding ??= Encoding.UTF8; + + if(!TryResolveVolumeUniqueKey(image, out byte[] vuk, out errorMessage)) + return null; + + if(!TryLoadEncryptedCpsUnitKeys(image, plugins, encoding, out byte[][]? encKeys, out errorMessage) || + encKeys is null) + return null; + + byte[][] decrypted = new byte[encKeys.Length][]; + + for(int i = 0; i < encKeys.Length; i++) + { + if(encKeys[i].Length != 16) + { + errorMessage = Aaru.Localization.Core.Aacs_encrypted_unit_key_invalid_length; + + return null; + } + + decrypted[i] = new byte[16]; + AacsCrypto.DecryptCpsUnitKey(vuk, encKeys[i], decrypted[i]); + } + + return decrypted; + } + + /// Tries to resolve the volume unique key from the media tags. + /// Input optical media image. + /// Volume unique key. + /// Error message. + /// True if the volume unique key was resolved successfully. + static bool TryResolveVolumeUniqueKey(IOpticalMediaImage image, out byte[] vuk, out string? errorMessage) + { + vuk = new byte[16]; + errorMessage = null; + + if(image.ReadMediaTag(MediaTagType.AacsVolumeUniqueKey, out byte[]? tagVuk) == ErrorNumber.NoError && + tagVuk is { Length: 16 } && + !IsAllZero(tagVuk)) + { + Buffer.BlockCopy(tagVuk, 0, vuk, 0, 16); + + return true; + } + + if(image.ReadMediaTag(MediaTagType.AacsMediaKey, out byte[]? mk) != ErrorNumber.NoError || + mk is not { Length: 16 } || + IsAllZero(mk)) + { + errorMessage = Aaru.Localization.Core.Aacs_missing_volume_unique_key; + + return false; + } + + if(image.ReadMediaTag(MediaTagType.AACS_VolumeIdentifier, out byte[]? vid) != ErrorNumber.NoError || + vid is not { Length: 16 } || + IsAllZero(vid)) + { + errorMessage = Aaru.Localization.Core.Aacs_missing_volume_unique_key; + + return false; + } + + AacsCrypto.DeriveVolumeUniqueKey(mk, vid, vuk); + + return true; + } + + /// Tries to load the encrypted CPS unit keys from the media tags or filesystem. + /// Input optical media image. + /// Plugin register. + /// Encoding. + /// Encrypted CPS unit keys. + /// Error message. + /// True if the encrypted CPS unit keys were loaded successfully. + static bool TryLoadEncryptedCpsUnitKeys(IOpticalMediaImage image, PluginRegister plugins, Encoding encoding, + out byte[][]? encKeys, out string? errorMessage) + { + encKeys = null; + errorMessage = null; + + if(image.ReadMediaTag(MediaTagType.AACS_DataKeys, out byte[]? dataKeys) == ErrorNumber.NoError && + dataKeys is { Length: > 0 }) + { + UnitKeyRoParseResult? parsed = UnitKeyRoParseResult.TryParse(dataKeys); + + if(parsed != null) + { + if(parsed.IsAacs2Layout) + { + errorMessage = Aaru.Localization.Core.Aacs2_unit_keys_not_supported; + + return false; + } + + encKeys = parsed.EncryptedCpsUnitKeys; + + if(encKeys.Length > 0) + return true; + } + + UnitKeyRoParseResult? raw = UnitKeyRoParseResult.TryParseRawEncryptedKeys(dataKeys); + + if(raw != null) + { + encKeys = raw.EncryptedCpsUnitKeys; + + if(encKeys.Length > 0) + return true; + } + } + + if(TryReadUnitKeyInfFromFilesystem(image, plugins, encoding, out byte[]? fileBytes) && + fileBytes is { Length: > 0 }) + { + UnitKeyRoParseResult? parsed = UnitKeyRoParseResult.TryParse(fileBytes); + + if(parsed == null) + { + errorMessage = Aaru.Localization.Core.Aacs_unit_key_inf_invalid; + + return false; + } + + if(parsed.IsAacs2Layout) + { + errorMessage = Aaru.Localization.Core.Aacs2_unit_keys_not_supported; + + return false; + } + + encKeys = parsed.EncryptedCpsUnitKeys; + + return encKeys.Length > 0; + } + + errorMessage = Aaru.Localization.Core.Aacs_missing_unit_keys; + + return false; + } + + /// Tries to read the Unit_Key_RO.inf file from the filesystem. + /// Input optical media image. + /// Plugin register. + /// Encoding. + /// File bytes. + /// True if the Unit_Key_RO.inf file was read successfully. + static bool TryReadUnitKeyInfFromFilesystem(IOpticalMediaImage image, PluginRegister plugins, Encoding encoding, + out byte[]? fileBytes) + { + fileBytes = null; + + foreach(Partition partition in Partitions.GetAll(image)) + { + foreach(IFilesystem fs in plugins.Filesystems.Values) + { + if(fs is not IReadOnlyFilesystem rofs) + continue; + + if(!fs.Identify(image, partition)) + continue; + + if(rofs.Mount(image, partition, encoding, null, null) != ErrorNumber.NoError) + continue; + + try + { + foreach(string path in UnitKeyPaths) + { + if(rofs.OpenFile(path, out IFileNode? node) != ErrorNumber.NoError || node is null) + continue; + + try + { + if(node.Length <= 0 || node.Length > 10 * 1024 * 1024) + continue; + + byte[] buf = new byte[node.Length]; + + if(rofs.ReadFile(node, node.Length, buf, out long read) != ErrorNumber.NoError || + read != node.Length) + continue; + + fileBytes = buf; + + return true; + } + finally + { + rofs.CloseFile(node); + } + } + } + finally + { + rofs.Unmount(); + } + } + } + + return false; + } +} diff --git a/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs b/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs new file mode 100644 index 000000000..71b4bd692 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +public partial class Convert +{ + /// Blu-ray AACS: sector-aligned CPS units, staging, and per-sector . + ErrorNumber ConvertAacsBdOpticalSectors(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical) + { + ErrorNumber errno = AacsBdExtentResolver.ResolveStreamFileExtents(inputOptical, + _plugins, + out List ranges, + out string err); + + Func allowDecryptLba = null; + + if(errno == ErrorNumber.NoError) + allowDecryptLba = lba => AacsBdExtentResolver.IsLbaAllowed(ranges, lba); + else if(_force) + ErrorMessage?.Invoke(err + " Continuing with broad Blu-ray AACS decrypt scope."); + else + { + StoppingErrorMessage?.Invoke(err); + + return errno; + } + + return AacsBdOpticalPipeline.Run(inputOptical, + outputOptical, + _count, + _force, + ref _aborted, + _aacsDecryptedCpsUnitKeys, + allowDecryptLba, + new AacsBdOpticalPipeline.Ui + { + OnInitProgress = () => InitProgress?.Invoke(), + OnInitProgress2 = () => InitProgress2?.Invoke(), + OnEndProgress = () => EndProgress?.Invoke(), + OnEndProgress2 = () => EndProgress2?.Invoke(), + OnTrackProgress = (i, n) => UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_in_track_0_of_1, i + 1, n), i, n), + OnSectorRangeProgress = (start, end, seq, done, tot) => + UpdateProgress2?.Invoke(string.Format(UI.Converting_sectors_0_to_1_in_track_2, + start, + end, + seq), + done, + tot), + ErrorMessage = msg => ErrorMessage?.Invoke(msg), + StoppingErrorMessage = msg => StoppingErrorMessage?.Invoke(msg) + }); + } +} From f40fb08b8fa9ef9c0b1eae129a752da62b0f100f Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 16:03:41 +0200 Subject: [PATCH 07/12] [convert] Use the new CSS/AACS pipelines --- Aaru.Core/Image/Convert/Optical.cs | 161 +++++++++++------------------ Aaru.Decryption/DVD/CSS.cs | 48 ++++++++- Aaru.Decryption/DVD/MPEG.cs | 21 ++++ 3 files changed, 123 insertions(+), 107 deletions(-) diff --git a/Aaru.Core/Image/Convert/Optical.cs b/Aaru.Core/Image/Convert/Optical.cs index f2cecb964..7d9672194 100644 --- a/Aaru.Core/Image/Convert/Optical.cs +++ b/Aaru.Core/Image/Convert/Optical.cs @@ -6,15 +6,15 @@ using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Core.Media; -using Aaru.Decryption.DVD; using Aaru.Devices; using Aaru.Localization; -using Aaru.Logging; +using MediaType = Aaru.CommonTypes.MediaType; namespace Aaru.Core.Image; public partial class Convert { + byte[][]? _aacsDecryptedCpsUnitKeys; ErrorNumber ConvertOptical(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, bool useLong) { if(!outputOptical.SetTracks(inputOptical.Tracks)) @@ -150,8 +150,39 @@ public partial class Convert if(_aborted) return ErrorNumber.NoError; InitProgress?.Invoke(); InitProgress2?.Invoke(); - byte[] generatedTitleKeys = null; - var currentTrack = 0; + var currentTrack = 0; + + _aacsDecryptedCpsUnitKeys = null; + + if(_decrypt) + { + if(IsHdDvdAacsMedia(inputOptical.Info.MediaType)) + { + StoppingErrorMessage?.Invoke(Aaru.Localization.Core.Aacs_hddvd_not_supported); + + return ErrorNumber.UnsupportedMedia; + } + + if(IsBluRayAacsMedia(inputOptical.Info.MediaType)) + { + _aacsDecryptedCpsUnitKeys = AacsKeyResolver.TryGetDecryptedCpsUnitKeys(inputOptical, + _plugins, + Encoding.UTF8, + out string aacsErr); + + if(_aacsDecryptedCpsUnitKeys == null) + { + StoppingErrorMessage?.Invoke(aacsErr ?? Aaru.Localization.Core.Aacs_missing_unit_keys); + + return ErrorNumber.NoData; + } + + return ConvertAacsBdOpticalSectors(inputOptical, outputOptical); + } + + if(CssDvdSectorDecrypt.IsDvdMedia(inputOptical.Info.MediaType)) + return ConvertCssDvdOpticalSectors(inputOptical, outputOptical, useLong); + } foreach(Track track in inputOptical.Tracks) { @@ -264,17 +295,6 @@ public partial class Convert out sector, out sectorStatusArray); - // TODO: Move to generic place when anything but CSS DVDs can be decrypted - if(IsDvdMedia(inputOptical.Info.MediaType) && _decrypt) - { - DecryptDvdSector(ref sector, - inputOptical, - doneSectors + track.StartSector, - sectorsToDo, - _plugins, - ref generatedTitleKeys); - } - if(errno == ErrorNumber.NoError) { result = sectorsToDo == 1 @@ -625,97 +645,34 @@ public partial class Convert } /// - /// Decrypts DVD sectors using CSS (Content Scramble System) decryption - /// Retrieves decryption keys from sector tags or generates them from ISO9660 filesystem - /// Only MPEG packets within sectors can be encrypted + /// Checks if media type is a Blu-ray. + /// Includes media that may be encrypted with AACS (as well as some that cannot, + /// in case of misidentifications (e.g. BDR)). /// - void DecryptDvdSector(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress, uint sectorsToDo, - PluginRegister plugins, ref byte[] generatedTitleKeys) - { - if(_aborted) return; - - // Only sectors which are MPEG packets can be encrypted. - if(!Mpeg.ContainsMpegPackets(sector, sectorsToDo)) return; - - byte[] cmi, titleKey; - - if(sectorsToDo == 1) - { - if(inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdSectorCmi, out cmi) == - ErrorNumber.NoError && - inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdTitleKeyDecrypted, out titleKey) == - ErrorNumber.NoError) - sector = CSS.DecryptSector(sector, titleKey, cmi); - else - { - if(generatedTitleKeys == null) GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys); - - if(generatedTitleKeys != null) - { - sector = CSS.DecryptSector(sector, - generatedTitleKeys.Skip((int)(5 * sectorAddress)).Take(5).ToArray(), - null); - } - } - } - else - { - if(inputOptical.ReadSectorsTag(sectorAddress, false, sectorsToDo, SectorTagType.DvdSectorCmi, out cmi) == - ErrorNumber.NoError && - inputOptical.ReadSectorsTag(sectorAddress, - false, - sectorsToDo, - SectorTagType.DvdTitleKeyDecrypted, - out titleKey) == - ErrorNumber.NoError) - sector = CSS.DecryptSector(sector, titleKey, cmi, sectorsToDo); - else - { - if(generatedTitleKeys == null) GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys); - - if(generatedTitleKeys != null) - { - sector = CSS.DecryptSector(sector, - generatedTitleKeys.Skip((int)(5 * sectorAddress)) - .Take((int)(5 * sectorsToDo)) - .ToArray(), - null, - sectorsToDo); - } - } - } - } + /// Media type to check. + /// True if media type is a Blu-ray, false otherwise. + static bool IsBluRayAacsMedia(MediaType mediaType) => + mediaType is MediaType.BDROM + or MediaType.BDR + or MediaType.BDRE + or MediaType.BDRXL + or MediaType.BDREXL + or MediaType.UHDBD; /// - /// Generates DVD CSS title keys from ISO9660 filesystem - /// Used when explicit title keys are not available in sector tags - /// Searches for ISO9660 partitions to derive decryption keys + /// Checks if media type is any variant of HD DVD (ROM, RAM, R, RW, RDL, PR, PRDL). + /// Includes media that may be encrypted with AACS (as well as some that cannot, + /// in case of misidentifications (e.g. HDDVDR)). /// - void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins, ref byte[] generatedTitleKeys) - { - if(_aborted) return; - - List partitions = Partitions.GetAll(inputOptical); - - partitions = partitions.FindAll(p => - { - Filesystems.Identify(inputOptical, out List idPlugins, p); - - return idPlugins.Contains("iso9660 filesystem"); - }); - - if(!plugins.ReadOnlyFilesystems.TryGetValue("iso9660 filesystem", out IReadOnlyFilesystem rofs)) return; - - AaruLogging.Debug(MODULE_NAME, UI.Generating_decryption_keys); - - generatedTitleKeys = CSS.GenerateTitleKeys(inputOptical, partitions, inputOptical.Info.Sectors, rofs); - } - - bool IsDvdMedia(MediaType mediaType) => - - // Checks if media type is any variant of DVD (ROM, R, RDL, PR, PRDL) - // Consolidates media type checking logic used throughout conversion process - mediaType is MediaType.DVDROM or MediaType.DVDR or MediaType.DVDRDL or MediaType.DVDPR or MediaType.DVDPRDL; + /// Media type to check. + /// True if media type is any variant of HD DVD, false otherwise. + static bool IsHdDvdAacsMedia(MediaType mediaType) => + mediaType is MediaType.HDDVDROM + or MediaType.HDDVDRAM + or MediaType.HDDVDR + or MediaType.HDDVDRW + or MediaType.HDDVDRDL + or MediaType.HDDVDRWDL; private bool IsCompactDiscMedia(MediaType mediaType) => diff --git a/Aaru.Decryption/DVD/CSS.cs b/Aaru.Decryption/DVD/CSS.cs index 375c76376..885a93c9a 100644 --- a/Aaru.Decryption/DVD/CSS.cs +++ b/Aaru.Decryption/DVD/CSS.cs @@ -624,13 +624,26 @@ public class CSS /// The encryption keys. /// Number of sectors in sectorData. /// Size of one sector. + /// + /// If non-null and Length >= blocks, each element is set to true when CSS unscrambling was + /// applied to that logical block, and false when the sector was left unchanged (including the global + /// "no encryption" early return and per-block false). + /// /// The decrypted sector. public static byte[] DecryptSector(byte[] sectorData, byte[] keyData, byte[]? cmiData, uint blocks = 1, - uint blockSize = 2048) + uint blockSize = 2048, bool[]? blockAppliedCssDecrypt = null) { // None of the sectors are encrypted - if(cmiData != null && cmiData.All(static cmi => (cmi & 0x80) >> 7 == 0) || keyData.All(static k => k == 0)) + if((cmiData != null && cmiData.All(static cmi => (cmi & 0x80) >> 7 == 0)) || keyData.All(static k => k == 0)) + { + if(blockAppliedCssDecrypt != null) + { + for(uint i = 0; i < blocks && i < (uint)blockAppliedCssDecrypt.Length; i++) + blockAppliedCssDecrypt[i] = false; + } + return sectorData; + } var decryptedBuffer = new byte[sectorData.Length]; @@ -641,11 +654,17 @@ public class CSS if(!IsEncrypted(cmiData?[i], currentKey, currentSector)) { + if(blockAppliedCssDecrypt != null && i < (uint)blockAppliedCssDecrypt.Length) + blockAppliedCssDecrypt[i] = false; + Array.Copy(currentSector, 0, decryptedBuffer, (int)(i * blockSize), blockSize); continue; } + if(blockAppliedCssDecrypt != null && i < (uint)blockAppliedCssDecrypt.Length) + blockAppliedCssDecrypt[i] = true; + Array.Copy(UnscrambleSector(currentKey, currentSector), 0, decryptedBuffer, @@ -662,13 +681,26 @@ public class CSS /// The encryption keys. /// Number of sectors in sectorData. /// Size of one sector. + /// + /// If non-null and Length >= blocks, each element is set to true when CSS unscrambling was + /// applied to that logical block, and false when the sector was left unchanged (including the global + /// "no encryption" early return and per-block false). + /// /// The decrypted sector. public static byte[] DecryptSectorLong(byte[] sectorData, byte[] keyData, byte[]? cmiData, uint blocks = 1, - uint blockSize = 2048) + uint blockSize = 2048, bool[]? blockAppliedCssDecrypt = null) { // None of the sectors are encrypted - if(cmiData != null && cmiData.All(static cmi => (cmi & 0x80) >> 7 == 0) || keyData.All(static k => k == 0)) + if((cmiData != null && cmiData.All(static cmi => (cmi & 0x80) >> 7 == 0)) || keyData.All(static k => k == 0)) + { + if(blockAppliedCssDecrypt != null) + { + for(uint i = 0; i < blocks && i < (uint)blockAppliedCssDecrypt.Length; i++) + blockAppliedCssDecrypt[i] = false; + } + return sectorData; + } var decryptedBuffer = new byte[sectorData.Length]; @@ -684,11 +716,17 @@ public class CSS if(!IsEncrypted(cmiData?[i], currentKey, currentSector)) { + if(blockAppliedCssDecrypt != null && i < (uint)blockAppliedCssDecrypt.Length) + blockAppliedCssDecrypt[i] = false; + Array.Copy(currentSector, 0, decryptedBuffer, (int)(12 + i * blockSize + 16 * i), blockSize); continue; } + if(blockAppliedCssDecrypt != null && i < (uint)blockAppliedCssDecrypt.Length) + blockAppliedCssDecrypt[i] = true; + Array.Copy(UnscrambleSector(currentKey, currentSector), 0, decryptedBuffer, @@ -1024,7 +1062,7 @@ public class CSS /// IReadOnlyFilesystem to check in. /// Partition to check in. /// true if VIDEO_TS folder was found. - static bool HasVideoTsFolder(IOpticalMediaImage input, IReadOnlyFilesystem fs, Partition partition) + public static bool HasVideoTsFolder(IOpticalMediaImage input, IReadOnlyFilesystem fs, Partition partition) { ErrorNumber error = fs.Mount(input, partition, null, null, null); diff --git a/Aaru.Decryption/DVD/MPEG.cs b/Aaru.Decryption/DVD/MPEG.cs index ac46fc17d..60dc78bc9 100644 --- a/Aaru.Decryption/DVD/MPEG.cs +++ b/Aaru.Decryption/DVD/MPEG.cs @@ -143,6 +143,27 @@ public class Mpeg return false; } + /// DVD raw long sector size (sync/header + 2048 user + 4 suffix) per CSS long decrypt. + public const uint DvdLongSectorStride = 2064; + + /// User data starts at this offset inside each -byte block. + public const uint DvdLongSectorUserOffset = 12; + + /// True if any logical DVD long sector in the buffer contains an MPEG pack header at the user payload offset. + public static bool ContainsMpegPacketsLong(byte[] sectorData, uint blocks) + { + if(sectorData.Length < (long)blocks * (long)DvdLongSectorStride) return false; + + for(uint i = 0; i < blocks; i++) + { + int off = (int)(DvdLongSectorUserOffset + i * DvdLongSectorStride); + + if(IsMpegPacket(sectorData.Skip(off))) return true; + } + + return false; + } + public static bool IsMpegPacket(IEnumerable sector) => sector.Take(3).ToArray().SequenceEqual(_mpeg2PackHeaderStartCode); From 7f3502e80f1cde4efb03b523a0c3fd0067c397bd Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 16:09:16 +0200 Subject: [PATCH 08/12] [merge] Move decryption to own pipelines --- Aaru.Core/Image/Merge/MergeAacsBd.cs | 57 +++++ Aaru.Core/Image/Merge/MergeCssDvd.cs | 313 +++++++++++++++++++++++++++ Aaru.Core/Image/Merge/Merger.cs | 1 + Aaru.Core/Image/Merge/Optical.cs | 164 +++++--------- 4 files changed, 423 insertions(+), 112 deletions(-) create mode 100644 Aaru.Core/Image/Merge/MergeAacsBd.cs create mode 100644 Aaru.Core/Image/Merge/MergeCssDvd.cs diff --git a/Aaru.Core/Image/Merge/MergeAacsBd.cs b/Aaru.Core/Image/Merge/MergeAacsBd.cs new file mode 100644 index 000000000..9c6036bce --- /dev/null +++ b/Aaru.Core/Image/Merge/MergeAacsBd.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +public sealed partial class Merger +{ + /// Merges Blu-ray AACS optical sectors. + /// Input optical media image. + /// Output optical media image. + /// Error number. + ErrorNumber CopyAacsBdOpticalSectorsPrimary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical) + { + ErrorNumber errno = AacsBdExtentResolver.ResolveStreamFileExtents(inputOptical, + _plugins, + out List ranges, + out string err); + + if(errno != ErrorNumber.NoError) + { + StoppingErrorMessage?.Invoke(err); + + return errno; + } + + Func allowDecryptLba = lba => AacsBdExtentResolver.IsLbaAllowed(ranges, lba); + + return AacsBdOpticalPipeline.Run(inputOptical, + outputOptical, + (uint)count, + false, + ref _aborted, + _aacsDecryptedCpsUnitKeys, + allowDecryptLba, + new AacsBdOpticalPipeline.Ui + { + OnInitProgress = () => InitProgress?.Invoke(), + OnInitProgress2 = () => InitProgress2?.Invoke(), + OnEndProgress = () => EndProgress?.Invoke(), + OnEndProgress2 = () => EndProgress2?.Invoke(), + OnTrackProgress = (i, n) => UpdateProgress?.Invoke(string.Format(UI.Copying_sectors_in_track_0_of_1, i + 1, n), i, n), + OnSectorRangeProgress = (start, end, seq, done, tot) => + UpdateProgress2?.Invoke(string.Format(UI.Copying_sectors_0_to_1_in_track_2, + start, + end, + seq), + done, + tot), + ErrorMessage = s => ErrorMessage?.Invoke(s), + StoppingErrorMessage = s => StoppingErrorMessage?.Invoke(s) + }); + } +} diff --git a/Aaru.Core/Image/Merge/MergeCssDvd.cs b/Aaru.Core/Image/Merge/MergeCssDvd.cs new file mode 100644 index 000000000..09fd8860f --- /dev/null +++ b/Aaru.Core/Image/Merge/MergeCssDvd.cs @@ -0,0 +1,313 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// --[ Description ] ---------------------------------------------------------- +// +// DVD CSS optical sector merge pipeline. +// +// ---------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +public sealed partial class Merger +{ + /// Merges CSS-encrypted DVD optical sectors. + /// Input optical media image. + /// Output optical media image. + /// True to use long sector reads. + /// Error number. + ErrorNumber CopyCssDvdOpticalSectorsPrimary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, + bool useLong) + { + if(_aborted) return ErrorNumber.NoError; + + InitProgress?.Invoke(); + InitProgress2?.Invoke(); + byte[] generatedTitleKeys = null; + int currentTrack = 0; + + foreach(Track track in inputOptical.Tracks) + { + if(_aborted) break; + + UpdateProgress?.Invoke(string.Format(UI.Copying_sectors_in_track_0_of_1, + currentTrack + 1, + inputOptical.Tracks.Count), + currentTrack, + inputOptical.Tracks.Count); + + ulong doneSectors = 0; + ulong trackSectors = track.EndSector - track.StartSector + 1; + + while(doneSectors < trackSectors) + { + if(_aborted) break; + + byte[] sector; + + uint sectorsToDo; + + if(trackSectors - doneSectors >= (ulong)count) + sectorsToDo = (uint)count; + else + sectorsToDo = (uint)(trackSectors - doneSectors); + + UpdateProgress2?.Invoke(string.Format(UI.Copying_sectors_0_to_1_in_track_2, + doneSectors + track.StartSector, + doneSectors + sectorsToDo + track.StartSector, + track.Sequence), + (long)doneSectors, + (long)trackSectors); + + bool result; + SectorStatus sectorStatus = SectorStatus.NotDumped; + SectorStatus[] sectorStatusArray = new SectorStatus[1]; + ErrorNumber errno; + + if(useLong) + { + errno = sectorsToDo == 1 + ? inputOptical.ReadSectorLong(doneSectors + track.StartSector, + false, + out sector, + out sectorStatus) + : inputOptical.ReadSectorsLong(doneSectors + track.StartSector, + false, + sectorsToDo, + out sector, + out sectorStatusArray); + + if(errno == ErrorNumber.NoError) + { + CssDvdSectorDecrypt.ApplyCssAfterReadLong(ref sector, + ref sectorStatus, + sectorStatusArray, + sectorsToDo, + sectorsToDo == 1, + inputOptical, + doneSectors + track.StartSector, + _plugins, + ref generatedTitleKeys, + () => _aborted, + MODULE_NAME); + + result = sectorsToDo == 1 + ? outputOptical.WriteSectorLong(sector, + doneSectors + track.StartSector, + false, + sectorStatus) + : outputOptical.WriteSectorsLong(sector, + doneSectors + track.StartSector, + false, + sectorsToDo, + sectorStatusArray); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + doneSectors + track.StartSector)); + + return ErrorNumber.WriteError; + } + + if(!result && sector.Length % 2352 != 0) + { + StoppingErrorMessage?.Invoke(UI.Input_image_is_not_returning_long_sectors_not_continuing); + + return ErrorNumber.InOutError; + } + } + else + { + errno = sectorsToDo == 1 + ? inputOptical.ReadSector(doneSectors + track.StartSector, + false, + out sector, + out sectorStatus) + : inputOptical.ReadSectors(doneSectors + track.StartSector, + false, + sectorsToDo, + out sector, + out sectorStatusArray); + + if(errno == ErrorNumber.NoError) + { + CssDvdSectorDecrypt.ApplyCssAfterRead(ref sector, + ref sectorStatus, + sectorStatusArray, + sectorsToDo, + sectorsToDo == 1, + inputOptical, + doneSectors + track.StartSector, + _plugins, + ref generatedTitleKeys, + () => _aborted, + MODULE_NAME); + + result = sectorsToDo == 1 + ? outputOptical.WriteSector(sector, + doneSectors + track.StartSector, + false, + sectorStatus) + : outputOptical.WriteSectors(sector, + doneSectors + track.StartSector, + false, + sectorsToDo, + sectorStatusArray); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + doneSectors + track.StartSector)); + + return ErrorNumber.WriteError; + } + } + + if(!result) + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + doneSectors + track.StartSector)); + + return ErrorNumber.WriteError; + } + + doneSectors += sectorsToDo; + } + + currentTrack++; + } + + EndProgress2?.Invoke(); + EndProgress?.Invoke(); + + return ErrorNumber.NoError; + } + + /// Merges CSS-encrypted DVD optical sectors from a secondary image. + /// Input optical media image. + /// Output optical media image. + /// True to use long sector reads. + /// Sectors to copy. + /// Error number. + ErrorNumber CopyCssDvdOpticalSectorsSecondary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, + bool useLong, List sectorsToCopy) + { + if(_aborted) return ErrorNumber.NoError; + + InitProgress?.Invoke(); + byte[] generatedTitleKeys = null; + int howManySectorsToCopy = sectorsToCopy.Count(t => t < inputOptical.Info.Sectors); + int howManySectorsCopied = 0; + + foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < inputOptical.Info.Sectors) + .TakeWhile(_ => !_aborted)) + { + UpdateProgress?.Invoke(string.Format(UI.Copying_sector_0, sectorAddress), + howManySectorsCopied, + howManySectorsToCopy); + + if(_aborted) break; + + byte[] sector; + bool result; + SectorStatus sectorStatus; + ErrorNumber errno; + + if(useLong) + { + errno = inputOptical.ReadSectorLong(sectorAddress, false, out sector, out sectorStatus); + + if(errno == ErrorNumber.NoError) + { + SectorStatus[] sectorStatusArray = new SectorStatus[1]; + CssDvdSectorDecrypt.ApplyCssAfterReadLong(ref sector, + ref sectorStatus, + sectorStatusArray, + 1, + true, + inputOptical, + sectorAddress, + _plugins, + ref generatedTitleKeys, + () => _aborted, + MODULE_NAME); + + result = outputOptical.WriteSectorLong(sector, sectorAddress, false, sectorStatus); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + sectorAddress)); + + return ErrorNumber.WriteError; + } + + if(!result && sector.Length % 2352 != 0) + { + StoppingErrorMessage?.Invoke(UI.Input_image_is_not_returning_long_sectors_not_continuing); + + return ErrorNumber.InOutError; + } + } + else + { + errno = inputOptical.ReadSector(sectorAddress, false, out sector, out sectorStatus); + + if(errno == ErrorNumber.NoError) + { + SectorStatus[] sectorStatusArray = new SectorStatus[1]; + CssDvdSectorDecrypt.ApplyCssAfterRead(ref sector, + ref sectorStatus, + sectorStatusArray, + 1, + true, + inputOptical, + sectorAddress, + _plugins, + ref generatedTitleKeys, + () => _aborted, + MODULE_NAME); + + result = outputOptical.WriteSector(sector, sectorAddress, false, sectorStatus); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + sectorAddress)); + + return ErrorNumber.WriteError; + } + } + + if(!result) + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + sectorAddress)); + + return ErrorNumber.WriteError; + } + + howManySectorsCopied++; + } + + EndProgress?.Invoke(); + + return ErrorNumber.NoError; + } +} diff --git a/Aaru.Core/Image/Merge/Merger.cs b/Aaru.Core/Image/Merge/Merger.cs index de7693a30..81c121564 100644 --- a/Aaru.Core/Image/Merge/Merger.cs +++ b/Aaru.Core/Image/Merge/Merger.cs @@ -55,6 +55,7 @@ public sealed partial class Merger const string MODULE_NAME = "Image merger"; bool _aborted; readonly PluginRegister _plugins = PluginRegister.Singleton; + byte[][]? _aacsDecryptedCpsUnitKeys; public ErrorNumber Start() { diff --git a/Aaru.Core/Image/Merge/Optical.cs b/Aaru.Core/Image/Merge/Optical.cs index 4895bcf9e..269ce4cf8 100644 --- a/Aaru.Core/Image/Merge/Optical.cs +++ b/Aaru.Core/Image/Merge/Optical.cs @@ -6,10 +6,9 @@ using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.CommonTypes.Structs; using Aaru.Core.Media; -using Aaru.Decryption.DVD; using Aaru.Devices; using Aaru.Localization; -using Aaru.Logging; +using MediaType = Aaru.CommonTypes.MediaType; namespace Aaru.Core.Image; @@ -173,8 +172,39 @@ public sealed partial class Merger InitProgress?.Invoke(); InitProgress2?.Invoke(); - byte[] generatedTitleKeys = null; - var currentTrack = 0; + var currentTrack = 0; + + _aacsDecryptedCpsUnitKeys = null; + + if(decrypt) + { + if(IsHdDvdAacsMedia(inputOptical.Info.MediaType)) + { + StoppingErrorMessage?.Invoke(Aaru.Localization.Core.Aacs_hddvd_not_supported); + + return ErrorNumber.UnsupportedMedia; + } + + if(IsBluRayAacsMedia(inputOptical.Info.MediaType)) + { + _aacsDecryptedCpsUnitKeys = AacsKeyResolver.TryGetDecryptedCpsUnitKeys(inputOptical, + _plugins, + Encoding.UTF8, + out string aacsErr); + + if(_aacsDecryptedCpsUnitKeys == null) + { + StoppingErrorMessage?.Invoke(aacsErr ?? Aaru.Localization.Core.Aacs_missing_unit_keys); + + return ErrorNumber.NoData; + } + + return CopyAacsBdOpticalSectorsPrimary(inputOptical, outputOptical); + } + + if(CssDvdSectorDecrypt.IsDvdMedia(inputOptical.Info.MediaType)) + return CopyCssDvdOpticalSectorsPrimary(inputOptical, outputOptical, useLong); + } foreach(Track track in inputOptical.Tracks) { @@ -269,17 +299,6 @@ public sealed partial class Merger out sector, out sectorStatusArray); - // TODO: Move to generic place when anything but CSS DVDs can be decrypted - if(IsDvdMedia(inputOptical.Info.MediaType) && decrypt) - { - DecryptDvdSector(ref sector, - inputOptical, - doneSectors + track.StartSector, - sectorsToDo, - _plugins, - ref generatedTitleKeys); - } - if(errno == ErrorNumber.NoError) { result = sectorsToDo == 1 @@ -329,9 +348,11 @@ public sealed partial class Merger { if(_aborted) return ErrorNumber.NoError; + if(decrypt && CssDvdSectorDecrypt.IsDvdMedia(inputOptical.Info.MediaType)) + return CopyCssDvdOpticalSectorsSecondary(inputOptical, outputOptical, useLong, sectorsToCopy); + InitProgress?.Invoke(); - byte[] generatedTitleKeys = null; - int howManySectorsToCopy = sectorsToCopy.Count(t => t < inputOptical.Info.Sectors); + int howManySectorsToCopy = sectorsToCopy.Count(t => t < inputOptical.Info.Sectors); var howManySectorsCopied = 0; foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < inputOptical.Info.Sectors) @@ -375,10 +396,6 @@ public sealed partial class Merger { errno = inputOptical.ReadSector(sectorAddress, false, out sector, out sectorStatus); - // TODO: Move to generic place when anything but CSS DVDs can be decrypted - if(IsDvdMedia(inputOptical.Info.MediaType) && decrypt) - DecryptDvdSector(ref sector, inputOptical, sectorAddress, 1, _plugins, ref generatedTitleKeys); - if(errno == ErrorNumber.NoError) result = outputOptical.WriteSector(sector, sectorAddress, false, sectorStatus); else @@ -753,98 +770,21 @@ public sealed partial class Merger return ErrorNumber.NoError; } - /// - /// Decrypts DVD sectors using CSS (Content Scramble System) decryption - /// Retrieves decryption keys from sector tags or generates them from ISO9660 filesystem - /// Only MPEG packets within sectors can be encrypted - /// - void DecryptDvdSector(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress, uint sectorsToDo, - PluginRegister plugins, ref byte[] generatedTitleKeys) - { - if(_aborted) return; + static bool IsBluRayAacsMedia(MediaType mediaType) => + mediaType is MediaType.BDROM + or MediaType.BDR + or MediaType.BDRE + or MediaType.BDRXL + or MediaType.BDREXL + or MediaType.UHDBD; - // Only sectors which are MPEG packets can be encrypted. - if(!Mpeg.ContainsMpegPackets(sector, sectorsToDo)) return; - - byte[] cmi, titleKey; - - if(sectorsToDo == 1) - { - if(inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdSectorCmi, out cmi) == - ErrorNumber.NoError && - inputOptical.ReadSectorTag(sectorAddress, false, SectorTagType.DvdTitleKeyDecrypted, out titleKey) == - ErrorNumber.NoError) - sector = CSS.DecryptSector(sector, titleKey, cmi); - else - { - if(generatedTitleKeys == null) GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys); - - if(generatedTitleKeys != null) - { - sector = CSS.DecryptSector(sector, - generatedTitleKeys.Skip((int)(5 * sectorAddress)).Take(5).ToArray(), - null); - } - } - } - else - { - if(inputOptical.ReadSectorsTag(sectorAddress, false, sectorsToDo, SectorTagType.DvdSectorCmi, out cmi) == - ErrorNumber.NoError && - inputOptical.ReadSectorsTag(sectorAddress, - false, - sectorsToDo, - SectorTagType.DvdTitleKeyDecrypted, - out titleKey) == - ErrorNumber.NoError) - sector = CSS.DecryptSector(sector, titleKey, cmi, sectorsToDo); - else - { - if(generatedTitleKeys == null) GenerateDvdTitleKeys(inputOptical, plugins, ref generatedTitleKeys); - - if(generatedTitleKeys != null) - { - sector = CSS.DecryptSector(sector, - generatedTitleKeys.Skip((int)(5 * sectorAddress)) - .Take((int)(5 * sectorsToDo)) - .ToArray(), - null, - sectorsToDo); - } - } - } - } - - /// - /// Generates DVD CSS title keys from ISO9660 filesystem - /// Used when explicit title keys are not available in sector tags - /// Searches for ISO9660 partitions to derive decryption keys - /// - void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins, ref byte[] generatedTitleKeys) - { - if(_aborted) return; - - List partitions = Partitions.GetAll(inputOptical); - - partitions = partitions.FindAll(p => - { - Filesystems.Identify(inputOptical, out List idPlugins, p); - - return idPlugins.Contains("iso9660 filesystem"); - }); - - if(!plugins.ReadOnlyFilesystems.TryGetValue("iso9660 filesystem", out IReadOnlyFilesystem rofs)) return; - - AaruLogging.Debug(MODULE_NAME, UI.Generating_decryption_keys); - - generatedTitleKeys = CSS.GenerateTitleKeys(inputOptical, partitions, inputOptical.Info.Sectors, rofs); - } - - static bool IsDvdMedia(MediaType mediaType) => - - // Checks if media type is any variant of DVD (ROM, R, RDL, PR, PRDL) - // Consolidates media type checking logic used throughout conversion process - mediaType is MediaType.DVDROM or MediaType.DVDR or MediaType.DVDRDL or MediaType.DVDPR or MediaType.DVDPRDL; + static bool IsHdDvdAacsMedia(MediaType mediaType) => + mediaType is MediaType.HDDVDROM + or MediaType.HDDVDRAM + or MediaType.HDDVDR + or MediaType.HDDVDRW + or MediaType.HDDVDRDL + or MediaType.HDDVDRWDL; private static bool IsCompactDiscMedia(MediaType mediaType) => From 70f0d8c0ffa1adc7fc33b9c33a4dcf187d4e2c1d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 12:49:01 +0200 Subject: [PATCH 09/12] Identify HD DVD video ISOs --- Aaru.Decryption/Aacs/HDDVD.cs | 65 ++++++++ Aaru.Images/ZZZRawImage/HdDvdIsoProbe.cs | 189 +++++++++++++++++++++++ Aaru.Images/ZZZRawImage/Read.cs | 10 ++ 3 files changed, 264 insertions(+) create mode 100644 Aaru.Decryption/Aacs/HDDVD.cs create mode 100644 Aaru.Images/ZZZRawImage/HdDvdIsoProbe.cs diff --git a/Aaru.Decryption/Aacs/HDDVD.cs b/Aaru.Decryption/Aacs/HDDVD.cs new file mode 100644 index 000000000..8a7f0914e --- /dev/null +++ b/Aaru.Decryption/Aacs/HDDVD.cs @@ -0,0 +1,65 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : HDDVD.cs +// Author(s) : Rebecca Wallander +// +// --[ Description ] ---------------------------------------------------------- +// +// HD DVD video disc helpers. +// +// --[ License ] -------------------------------------------------------------- +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Decryption.Aacs; + +/// Aligns contiguous 2048-byte sectors to CPS units for decrypt during image conversion. +public sealed class HDDVD +{ + /// + /// HD DVD video discs always have a HVDVD_TS folder. If it doesn't have one, it's not a HD DVD video. + /// + /// IOpticalMediaImage to check for HVDVD_TS folder in. + /// IReadOnlyFilesystem to check in. + /// Partition to check in. + /// true if HVDVD_TS folder was found. + public static bool HasHdDvdVideoTsFolder(IOpticalMediaImage input, IReadOnlyFilesystem fs, Partition partition) + { + ErrorNumber error = fs.Mount(input, partition, null, null, null); + + if(error != ErrorNumber.NoError) return false; + + error = fs.Stat("HVDVD_TS", out FileEntryInfo stat); + fs.Unmount(); + + return error == ErrorNumber.NoError; + } +} \ No newline at end of file diff --git a/Aaru.Images/ZZZRawImage/HdDvdIsoProbe.cs b/Aaru.Images/ZZZRawImage/HdDvdIsoProbe.cs new file mode 100644 index 000000000..3abb8f093 --- /dev/null +++ b/Aaru.Images/ZZZRawImage/HdDvdIsoProbe.cs @@ -0,0 +1,189 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : HdDvdIsoProbe.cs +// Author(s) : Rebecca Wallander +// +// Component : Disk image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Detects HD DVD-Video layout on bare .iso images (HVDVD_TS folder). +// Aaru.Core.Partitions.GetAll cannot be called from this assembly because +// Aaru.Core references Aaru.Images; the partition walk below mirrors the +// non-tape, in Partitions.GetAll. If we find a simpler way to detect a +// HD DVD-Video layout, we should use that instead. +// +// --[ License ] -------------------------------------------------------------- +// +// 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; either version 2.1 of the +// License, or (at your option) any later version. +// +// 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 +// . +// +// ---------------------------------------------------------------------------- +// Copyright © 2026 Rebecca Wallander +// ****************************************************************************/ + +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decryption.Aacs; + +namespace Aaru.Images; + +public sealed partial class ZZZRawImage +{ + /// True if a filesystem under some partition exposes the HD DVD-Video HVDVD_TS folder. + bool TryDetectHdDvdVideoIso() + { + IMediaImage image = this; + IOpticalMediaImage optical = this; + PluginRegister plugins = PluginRegister.Singleton; + List parts = EnumeratePartitionsForHdDvdProbe(image); + + foreach(Partition partition in parts) + { + foreach(IFilesystem fs in plugins.Filesystems.Values) + { + if(fs is not IReadOnlyFilesystem rofs) + continue; + + if(!fs.Identify(image, partition)) + continue; + + if(HDDVD.HasHdDvdVideoTsFolder(optical, rofs, partition)) + return true; + } + } + + return false; + } + + /// + /// Lists partitions the same way as for images that are not + /// tape. + /// + static List EnumeratePartitionsForHdDvdProbe(IMediaImage image) + { + PluginRegister plugins = PluginRegister.Singleton; + List foundPartitions = []; + List childPartitions = []; + List checkedLocations = []; + + var partitionableImage = image as IPartitionableMediaImage; + + // Getting all partitions from device (e.g. tracks) + if(partitionableImage?.Partitions != null) + { + foreach(Partition imagePartition in partitionableImage.Partitions) + { + foreach(IPartition plugin in plugins.Partitions.Values) + { + if(plugin is null) continue; + + if(!plugin.GetInformation(image, out List partitions, imagePartition.Start)) continue; + + foundPartitions.AddRange(partitions); + } + + checkedLocations.Add(imagePartition.Start); + } + } + + if(!checkedLocations.Contains(0) && image.Info.Sectors > 0) + { + foreach(IPartition plugin in plugins.Partitions.Values) + { + if(plugin is null) + continue; + + if(!plugin.GetInformation(image, out List partitions, 0)) + continue; + + foundPartitions.AddRange(partitions); + } + + checkedLocations.Add(0); + } + + while(foundPartitions.Count > 0) + { + if(checkedLocations.Contains(foundPartitions[0].Start)) + { + childPartitions.Add(foundPartitions[0]); + foundPartitions.RemoveAt(0); + + continue; + } + + List children = []; + + foreach(IPartition plugin in plugins.Partitions.Values) + { + if(plugin is null) + continue; + + if(!plugin.GetInformation(image, out List partitions, foundPartitions[0].Start)) + continue; + + children.AddRange(partitions); + } + + checkedLocations.Add(foundPartitions[0].Start); + + if(children.Count > 0) + { + foundPartitions.RemoveAt(0); + + foreach(Partition child in children) + { + if(checkedLocations.Contains(child.Start)) + childPartitions.Add(child); + else + foundPartitions.Add(child); + } + } + else + { + childPartitions.Add(foundPartitions[0]); + foundPartitions.RemoveAt(0); + } + } + + if(partitionableImage is not null) + { + List startLocations = + childPartitions.ConvertAll(static detectedPartition => detectedPartition.Start); + + if(partitionableImage.Partitions != null) + { + childPartitions.AddRange(partitionableImage.Partitions.Where(imagePartition => + !startLocations.Contains(imagePartition + .Start))); + } + } + + Partition[] childArray = childPartitions.OrderBy(static part => part.Start) + .ThenBy(static part => part.Length) + .ThenBy(static part => part.Scheme) + .ToArray(); + + for(long i = 0; i < childArray.LongLength; i++) + childArray[i].Sequence = (ulong)i; + + return childArray.ToList(); + } +} \ No newline at end of file diff --git a/Aaru.Images/ZZZRawImage/Read.cs b/Aaru.Images/ZZZRawImage/Read.cs index d6257899f..378637ae6 100644 --- a/Aaru.Images/ZZZRawImage/Read.cs +++ b/Aaru.Images/ZZZRawImage/Read.cs @@ -619,6 +619,16 @@ public sealed partial class ZZZRawImage else if(gcMagic == NGC_GC_MAGIC) _imageInfo.MediaType = MediaType.GOD; } + // Check for HD DVD video discs: .iso extension, size divisible by 2048, contains HVDVD_TS folder + // Needed for identifying the correct AACS path, so we don't misidentify it as a BD video disc + if(_extension == ".iso" && + _imageInfo.SectorSize == 2048 && + _imageInfo.ImageSize % 2048 == 0 && + _imageInfo.Sectors > 0 && + (_imageInfo.MediaType == MediaType.BDR || _imageInfo.MediaType == MediaType.BDRXL) && + TryDetectHdDvdVideoIso()) + _imageInfo.MediaType = MediaType.HDDVDROM; + // Check for bare .bca sidecar (64 bytes) for GameCube/Wii discs if(_imageInfo.MediaType is MediaType.GOD or MediaType.WOD && !_mediaTags.ContainsKey(MediaTagType.DVD_BCA)) { From 65988263cf0fb78e97f46f3ca3a591698be14c71 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 11 Apr 2026 13:37:24 +0200 Subject: [PATCH 10/12] Handle raw dvd sectors for iso9660 --- Aaru.Decoders/DVD/Sector.cs | 10 ++++++++++ Aaru.Filesystems/ISO9660/Mode2.cs | 20 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Aaru.Decoders/DVD/Sector.cs b/Aaru.Decoders/DVD/Sector.cs index 63033a30f..9373ba458 100644 --- a/Aaru.Decoders/DVD/Sector.cs +++ b/Aaru.Decoders/DVD/Sector.cs @@ -434,4 +434,14 @@ public sealed class Sector return ErrorNumber.NoError; } + + public static byte[] GetUserData(byte[] data) + { + if(data.Length != 2064) return data; + + byte[] sector = new byte[2048]; + Array.Copy(data, 12, sector, 0, 2048); + + return sector; + } } \ No newline at end of file diff --git a/Aaru.Filesystems/ISO9660/Mode2.cs b/Aaru.Filesystems/ISO9660/Mode2.cs index 30edf4be3..e93e458c3 100644 --- a/Aaru.Filesystems/ISO9660/Mode2.cs +++ b/Aaru.Filesystems/ISO9660/Mode2.cs @@ -135,7 +135,14 @@ public sealed partial class ISO9660 if(_blockSize == 2048) { - buffer = Sector.GetUserData(data, interleaved, fileNumber); + if(data.Length == 2064) + { + buffer = Decoders.DVD.Sector.GetUserData(data); + } + else + { + buffer = Sector.GetUserData(data, interleaved, fileNumber); + } return ErrorNumber.NoError; } @@ -232,7 +239,16 @@ public sealed partial class ISO9660 } } - byte[] sectorData = Sector.GetUserData(data, interleaved, fileNumber); + byte[] sectorData; + + if(data.Length == 2064) + { + sectorData = Decoders.DVD.Sector.GetUserData(data); + } + else + { + sectorData = Sector.GetUserData(data, interleaved, fileNumber); + } ms.Write(sectorData, 0, sectorData.Length); } From 5a04dc2491b061edc9302216a3533b9cd46bfe6b Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sun, 12 Apr 2026 17:50:24 +0200 Subject: [PATCH 11/12] Address code quality issues --- Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs | 7 +------ Aaru.Core/Image/Merge/MergeCssDvd.cs | 12 ++++-------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs b/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs index 2fe09b8d6..0839d95a0 100644 --- a/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs +++ b/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs @@ -39,12 +39,7 @@ public partial class Convert if(_aborted) break; byte[] sector; - uint sectorsToDo; - - if(trackSectors - doneSectors >= _count) - sectorsToDo = _count; - else - sectorsToDo = (uint)(trackSectors - doneSectors); + uint sectorsToDo = trackSectors - doneSectors >= _count ? _count : (uint)(trackSectors - doneSectors); UpdateProgress2?.Invoke(string.Format(UI.Converting_sectors_0_to_1_in_track_2, doneSectors + track.StartSector, diff --git a/Aaru.Core/Image/Merge/MergeCssDvd.cs b/Aaru.Core/Image/Merge/MergeCssDvd.cs index 09fd8860f..6125afd9c 100644 --- a/Aaru.Core/Image/Merge/MergeCssDvd.cs +++ b/Aaru.Core/Image/Merge/MergeCssDvd.cs @@ -54,12 +54,9 @@ public sealed partial class Merger byte[] sector; - uint sectorsToDo; - - if(trackSectors - doneSectors >= (ulong)count) - sectorsToDo = (uint)count; - else - sectorsToDo = (uint)(trackSectors - doneSectors); + uint sectorsToDo = trackSectors - doneSectors >= (ulong)count + ? (uint)count + : (uint)(trackSectors - doneSectors); UpdateProgress2?.Invoke(string.Format(UI.Copying_sectors_0_to_1_in_track_2, doneSectors + track.StartSector, @@ -212,8 +209,7 @@ public sealed partial class Merger int howManySectorsToCopy = sectorsToCopy.Count(t => t < inputOptical.Info.Sectors); int howManySectorsCopied = 0; - foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < inputOptical.Info.Sectors) - .TakeWhile(_ => !_aborted)) + foreach(ulong sectorAddress in sectorsToCopy.Where(t => t < inputOptical.Info.Sectors)) { UpdateProgress?.Invoke(string.Format(UI.Copying_sector_0, sectorAddress), howManySectorsCopied, From b9255b8b3e03b08cd04f48390ba55f7fea93c93d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sun, 12 Apr 2026 20:22:33 +0200 Subject: [PATCH 12/12] Address code quality issues --- Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs | 16 +------------ Aaru.Core/Image/Merge/MergeCssDvd.cs | 12 ---------- Aaru.Filesystems/ISO9660/Mode2.cs | 24 +++++--------------- 3 files changed, 7 insertions(+), 45 deletions(-) diff --git a/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs b/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs index 0839d95a0..df942d5f3 100644 --- a/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs +++ b/Aaru.Core/Image/Convert/CSS/ConvertCssDvd.cs @@ -48,7 +48,6 @@ public partial class Convert (long)doneSectors, (long)trackSectors); - bool useNotLong = false; bool result = false; SectorStatus sectorStatus = SectorStatus.NotDumped; SectorStatus[] sectorStatusArray = new SectorStatus[1]; @@ -112,21 +111,8 @@ public partial class Convert } } - if(!result && sector.Length % 2352 != 0) - { - if(!_force) - { - StoppingErrorMessage - ?.Invoke(UI.Input_image_is_not_returning_raw_sectors_use_force_if_you_want_to_continue); - - return ErrorNumber.InOutError; - } - - useNotLong = true; - } } - - if(!useLong || useNotLong) + else { errno = sectorsToDo == 1 ? inputOptical.ReadSector(doneSectors + track.StartSector, diff --git a/Aaru.Core/Image/Merge/MergeCssDvd.cs b/Aaru.Core/Image/Merge/MergeCssDvd.cs index 6125afd9c..ef8f844da 100644 --- a/Aaru.Core/Image/Merge/MergeCssDvd.cs +++ b/Aaru.Core/Image/Merge/MergeCssDvd.cs @@ -117,12 +117,6 @@ public sealed partial class Merger return ErrorNumber.WriteError; } - if(!result && sector.Length % 2352 != 0) - { - StoppingErrorMessage?.Invoke(UI.Input_image_is_not_returning_long_sectors_not_continuing); - - return ErrorNumber.InOutError; - } } else { @@ -252,12 +246,6 @@ public sealed partial class Merger return ErrorNumber.WriteError; } - if(!result && sector.Length % 2352 != 0) - { - StoppingErrorMessage?.Invoke(UI.Input_image_is_not_returning_long_sectors_not_continuing); - - return ErrorNumber.InOutError; - } } else { diff --git a/Aaru.Filesystems/ISO9660/Mode2.cs b/Aaru.Filesystems/ISO9660/Mode2.cs index e93e458c3..d33325ce1 100644 --- a/Aaru.Filesystems/ISO9660/Mode2.cs +++ b/Aaru.Filesystems/ISO9660/Mode2.cs @@ -135,14 +135,9 @@ public sealed partial class ISO9660 if(_blockSize == 2048) { - if(data.Length == 2064) - { - buffer = Decoders.DVD.Sector.GetUserData(data); - } - else - { - buffer = Sector.GetUserData(data, interleaved, fileNumber); - } + buffer = data.Length == 2064 + ? Decoders.DVD.Sector.GetUserData(data) + : Sector.GetUserData(data, interleaved, fileNumber); return ErrorNumber.NoError; } @@ -239,16 +234,9 @@ public sealed partial class ISO9660 } } - byte[] sectorData; - - if(data.Length == 2064) - { - sectorData = Decoders.DVD.Sector.GetUserData(data); - } - else - { - sectorData = Sector.GetUserData(data, interleaved, fileNumber); - } + byte[] sectorData = data.Length == 2064 + ? Decoders.DVD.Sector.GetUserData(data) + : Sector.GetUserData(data, interleaved, fileNumber); ms.Write(sectorData, 0, sectorData.Length); }