[convert] Use the new CSS/AACS pipelines

This commit is contained in:
Rebecca Wallander
2026-04-11 16:03:41 +02:00
parent b4e3b04e1d
commit f40fb08b8f
3 changed files with 123 additions and 107 deletions

View File

@@ -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
}
/// <summary>
/// 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)).
/// </summary>
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);
}
}
}
}
/// <param name="mediaType">Media type to check.</param>
/// <returns>True if media type is a Blu-ray, false otherwise.</returns>
static bool IsBluRayAacsMedia(MediaType mediaType) =>
mediaType is MediaType.BDROM
or MediaType.BDR
or MediaType.BDRE
or MediaType.BDRXL
or MediaType.BDREXL
or MediaType.UHDBD;
/// <summary>
/// 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)).
/// </summary>
void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins, ref byte[] generatedTitleKeys)
{
if(_aborted) return;
List<Partition> partitions = Partitions.GetAll(inputOptical);
partitions = partitions.FindAll(p =>
{
Filesystems.Identify(inputOptical, out List<string> 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;
/// <param name="mediaType">Media type to check.</param>
/// <returns>True if media type is any variant of HD DVD, false otherwise.</returns>
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) =>

View File

@@ -624,13 +624,26 @@ public class CSS
/// <param name="keyData">The encryption keys.</param>
/// <param name="blocks">Number of sectors in <c>sectorData</c>.</param>
/// <param name="blockSize">Size of one sector.</param>
/// <param name="blockAppliedCssDecrypt">
/// If non-null and <c>Length >= blocks</c>, each element is set to <c>true</c> when CSS unscrambling was
/// applied to that logical block, and <c>false</c> when the sector was left unchanged (including the global
/// "no encryption" early return and per-block <see cref="IsEncrypted"/> false).
/// </param>
/// <returns>The decrypted sector.</returns>
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
/// <param name="keyData">The encryption keys.</param>
/// <param name="blocks">Number of sectors in <c>sectorData</c>.</param>
/// <param name="blockSize">Size of one sector.</param>
/// <param name="blockAppliedCssDecrypt">
/// If non-null and <c>Length >= blocks</c>, each element is set to <c>true</c> when CSS unscrambling was
/// applied to that logical block, and <c>false</c> when the sector was left unchanged (including the global
/// "no encryption" early return and per-block <see cref="IsEncrypted"/> false).
/// </param>
/// <returns>The decrypted sector.</returns>
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
/// <param name="fs"><c>IReadOnlyFilesystem</c> to check in.</param>
/// <param name="partition"><c>Partition</c> to check in.</param>
/// <returns><c>true</c> if <c>VIDEO_TS</c> folder was found.</returns>
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);

View File

@@ -143,6 +143,27 @@ public class Mpeg
return false;
}
/// <summary>DVD raw long sector size (sync/header + 2048 user + 4 suffix) per CSS long decrypt.</summary>
public const uint DvdLongSectorStride = 2064;
/// <summary>User data starts at this offset inside each <see cref="DvdLongSectorStride"/>-byte block.</summary>
public const uint DvdLongSectorUserOffset = 12;
/// <summary>True if any logical DVD long sector in the buffer contains an MPEG pack header at the user payload offset.</summary>
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<byte> sector) =>
sector.Take(3).ToArray().SequenceEqual(_mpeg2PackHeaderStartCode);