[merge] Move decryption to own pipelines

This commit is contained in:
Rebecca Wallander
2026-04-11 16:09:16 +02:00
parent f40fb08b8f
commit 7f3502e80f
4 changed files with 423 additions and 112 deletions

View File

@@ -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
{
/// <summary>Merges Blu-ray AACS optical sectors.</summary>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="outputOptical">Output optical media image.</param>
/// <returns>Error number.</returns>
ErrorNumber CopyAacsBdOpticalSectorsPrimary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical)
{
ErrorNumber errno = AacsBdExtentResolver.ResolveStreamFileExtents(inputOptical,
_plugins,
out List<AacsBdExtentResolver.LbaRange> ranges,
out string err);
if(errno != ErrorNumber.NoError)
{
StoppingErrorMessage?.Invoke(err);
return errno;
}
Func<ulong, bool> 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)
});
}
}

View File

@@ -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
{
/// <summary>Merges CSS-encrypted DVD optical sectors.</summary>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="useLong">True to use long sector reads.</param>
/// <returns>Error number.</returns>
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;
}
/// <summary>Merges CSS-encrypted DVD optical sectors from a secondary image.</summary>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="useLong">True to use long sector reads.</param>
/// <param name="sectorsToCopy">Sectors to copy.</param>
/// <returns>Error number.</returns>
ErrorNumber CopyCssDvdOpticalSectorsSecondary(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical,
bool useLong, List<ulong> 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;
}
}

View File

@@ -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()
{

View File

@@ -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;
}
/// <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
/// </summary>
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);
}
}
}
}
/// <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
/// </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);
}
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) =>