Merge pull request #928 from aaru-dps/fakeshemp/aacs-2

Add AACS decryption when converting BDs
This commit is contained in:
2026-04-12 19:26:02 +01:00
committed by GitHub
26 changed files with 3291 additions and 221 deletions

View File

@@ -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; }
}
/// <summary>
/// Resolves the stream file extents for a Blu-ray optical media image.
/// It searches for the stream file extents in the <c>/BDMV/STREAM</c> and
/// <c>/BDMV/STREAM/SSIF</c> directories.
/// </summary>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="ranges">Stream file extents.</param>
/// <param name="error">Error message.</param>
/// <returns>Error number.</returns>
internal static ErrorNumber ResolveStreamFileExtents(IOpticalMediaImage inputOptical,
PluginRegister plugins,
out List<LbaRange> 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();
}
}
/// <summary>Checks if a given LBA is allowed for decryption.</summary>
/// <param name="ranges">Stream file extents.</param>
/// <param name="lba">LBA to check.</param>
/// <returns>True if the LBA is allowed for decryption.</returns>
internal static bool IsLbaAllowed(List<LbaRange> 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;
}
/// <summary>Adds the extents for a given directory to the list of stream file extents.</summary>
/// <param name="roFs">Read-only filesystem.</param>
/// <param name="udf">UDF filesystem.</param>
/// <param name="dirPath">Directory path.</param>
/// <param name="extension">Extension of the files to add.</param>
/// <param name="ranges">List of stream file extents.</param>
/// <returns>Error number.</returns>
static ErrorNumber AddDirectoryExtents(IReadOnlyFilesystem roFs, UDF udf, string dirPath, string extension,
ref List<LbaRange> 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;
}
/// <summary>Merges the extents for a given directory to the list of stream file extents.</summary>
/// <param name="ranges">List of stream file extents.</param>
/// <returns>Merged list of stream file extents.</returns>
static List<LbaRange> MergeRanges(List<LbaRange> ranges)
{
if(ranges.Count <= 1) return ranges;
List<LbaRange> 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;
}
}

View File

@@ -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;
/// <summary>Blu-ray AACS sector pipeline for convert/merge: LBA-aligned 6144-byte CPS units with output staging.</summary>
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<int, int> OnTrackProgress { get; init; }
public Action<ulong, ulong, uint, long, long> OnSectorRangeProgress { get; init; }
public Action<string> ErrorMessage { get; init; }
public Action<string> StoppingErrorMessage { get; init; }
}
/// <summary>Runs the Blu-ray AACS sector pipeline.</summary>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="batchCount">Batch count.</param>
/// <param name="force">Force flag.</param>
/// <param name="aborted">Aborted flag.</param>
/// <param name="decryptedCpsUnitKeys">Decrypted CPS unit keys.</param>
/// <param name="allowDecryptLba">Allow decrypt LBA function.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
internal static ErrorNumber Run(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, uint batchCount,
bool force, ref bool aborted, byte[][] decryptedCpsUnitKeys,
Func<ulong, bool> 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<byte[]> pending = [];
List<ulong> pendingLba = [];
List<SectorStatus> 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;
}
/// <summary>Flushes the pending sectors on read skip.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="pending">List of pending sectors.</param>
/// <param name="pendingLba">List of pending LBA.</param>
/// <param name="pendingSt">List of pending sector statuses.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
static ErrorNumber FlushPendingOnReadSkip(IWritableOpticalImage outputOptical, ref List<byte[]> pending,
ref List<ulong> pendingLba, ref List<SectorStatus> 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;
}
/// <summary>Processes one user sector.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="decryptedCpsUnitKeys">Decrypted CPS unit keys.</param>
/// <param name="sector">Sector data.</param>
/// <param name="lba">LBA of the sector.</param>
/// <param name="allowDecrypt">Allow decrypt flag.</param>
/// <param name="readStatus">Read status of the sector.</param>
/// <param name="pending">List of pending sectors.</param>
/// <param name="pendingLba">List of pending LBA.</param>
/// <param name="pendingSt">List of pending sector statuses.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
static ErrorNumber ProcessOneUserSector(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys,
byte[] sector, ulong lba, bool allowDecrypt, SectorStatus readStatus,
ref List<byte[]> pending,
ref List<ulong> pendingLba, ref List<SectorStatus> 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);
}
/// <summary>Flushes a complete unit.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="decryptedCpsUnitKeys">Decrypted CPS unit keys.</param>
/// <param name="pending">List of pending sectors.</param>
/// <param name="pendingLba">List of pending LBA.</param>
/// <param name="pendingSt">List of pending sector statuses.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
static ErrorNumber FlushCompleteUnit(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys,
ref List<byte[]> pending, ref List<ulong> pendingLba,
ref List<SectorStatus> 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;
}
/// <summary>Writes three sectors from a buffer.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="unit">Unit data.</param>
/// <param name="firstLba">First LBA of the sectors.</param>
/// <param name="threeStatuses">Statuses of the sectors.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
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;
}
/// <summary>Flushes the pending sectors as dumped.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="pending">List of pending sectors.</param>
/// <param name="pendingLba">List of pending LBA.</param>
/// <param name="pendingSt">List of pending sector statuses.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
static ErrorNumber FlushPendingAsDumped(IWritableOpticalImage outputOptical, ref List<byte[]> pending,
ref List<ulong> pendingLba, ref List<SectorStatus> 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;
}
/// <summary>Writes three sectors from a buffer.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="a">First sector data.</param>
/// <param name="b">Second sector data.</param>
/// <param name="c">Third sector data.</param>
/// <param name="firstLba">First LBA of the sectors.</param>
/// <param name="threeStatuses">Statuses of the sectors.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
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);
}
/// <summary>Writes one sector.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="lba">LBA of the sector.</param>
/// <param name="sector">Sector data.</param>
/// <param name="status">Status of the sector.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
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;
}
/// <summary>Flushes the pending sectors at track end.</summary>
/// <param name="outputOptical">Output optical media image.</param>
/// <param name="pending">List of pending sectors.</param>
/// <param name="pendingLba">List of pending LBA.</param>
/// <param name="pendingSt">List of pending sector statuses.</param>
/// <param name="force">Force flag.</param>
/// <param name="ui">UI.</param>
/// <returns>Error number.</returns>
static ErrorNumber FlushPendingAtTrackEnd(IWritableOpticalImage outputOptical, ref List<byte[]> pending,
ref List<ulong> pendingLba, ref List<SectorStatus> 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;
}
}

View File

@@ -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;
/// <summary>Loads VUK and decrypts CPS unit keys for Blu-ray AACS conversion.</summary>
public static class AacsKeyResolver
{
static readonly string[] UnitKeyPaths =
[
"AACS/Unit_Key_RO.inf",
"AACS\\Unit_Key_RO.inf"
];
/// <summary>Checks if every byte in the buffer is zero.</summary>
/// <param name="buffer">Buffer to check.</param>
/// <returns>True if every byte in the buffer is zero.</returns>
public static bool IsAllZero(ReadOnlySpan<byte> buffer)
{
for(int i = 0; i < buffer.Length; i++)
{
if(buffer[i] != 0)
return false;
}
return true;
}
/// <summary>
/// Obtains decrypted 16-byte CPS unit keys in disc order, or <see langword="null"/> if keys cannot be loaded.
/// </summary>
/// <param name="image">Input optical media image.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="encoding">Encoding.</param>
/// <param name="errorMessage">Error message.</param>
/// <returns>Decrypted CPS unit keys.</returns>
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;
}
/// <summary>Tries to resolve the volume unique key from the media tags.</summary>
/// <param name="image">Input optical media image.</param>
/// <param name="vuk">Volume unique key.</param>
/// <param name="errorMessage">Error message.</param>
/// <returns>True if the volume unique key was resolved successfully.</returns>
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;
}
/// <summary>Tries to load the encrypted CPS unit keys from the media tags or filesystem.</summary>
/// <param name="image">Input optical media image.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="encoding">Encoding.</param>
/// <param name="encKeys">Encrypted CPS unit keys.</param>
/// <param name="errorMessage">Error message.</param>
/// <returns>True if the encrypted CPS unit keys were loaded successfully.</returns>
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;
}
/// <summary>Tries to read the <c>Unit_Key_RO.inf</c> file from the filesystem.</summary>
/// <param name="image">Input optical media image.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="encoding">Encoding.</param>
/// <param name="fileBytes">File bytes.</param>
/// <returns>True if the <c>Unit_Key_RO.inf</c> file was read successfully.</returns>
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;
}
}

View File

@@ -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
{
/// <summary>Blu-ray AACS: sector-aligned CPS units, staging, and per-sector <see cref="SectorStatus" />.</summary>
ErrorNumber ConvertAacsBdOpticalSectors(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical)
{
ErrorNumber errno = AacsBdExtentResolver.ResolveStreamFileExtents(inputOptical,
_plugins,
out List<AacsBdExtentResolver.LbaRange> ranges,
out string err);
Func<ulong, bool> 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)
});
}
}

View File

@@ -0,0 +1,203 @@
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
using Aaru.Localization;
namespace Aaru.Core.Image;
public partial class Convert
{
/// <summary>Converts 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 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 = trackSectors - doneSectors >= _count ? _count : (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 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;
}
}
}
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
{
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;
}
}

View File

@@ -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;
/// <summary>CSS decryption and <see cref="SectorStatus"/> tagging for DVD optical pipelines.</summary>
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;
/// <summary>Generates DVD title keys.</summary>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="generatedTitleKeys">Generated title keys.</param>
/// <param name="logModuleName">Log module name.</param>
/// <param name="isAborted">Is aborted function.</param>
internal static void GenerateDvdTitleKeys(IOpticalMediaImage inputOptical, PluginRegister plugins,
ref byte[] generatedTitleKeys, string logModuleName,
Func<bool> isAborted)
{
if(isAborted()) return;
List<Partition> allPartitions = Partitions.GetAll(inputOptical);
byte[] lastKeysFromVideoTsFilesystem = null;
foreach(IReadOnlyFilesystem rofs in plugins.ReadOnlyFilesystems.Values)
{
if(isAborted()) return;
List<Partition> 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;
}
/// <summary>Checks if the title keys buffer is all zero.</summary>
/// <param name="keys">Title keys buffer.</param>
/// <returns>True if the title keys buffer is all zero.</returns>
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;
}
/// <summary>Applies CSS after reading a sector.</summary>
/// <param name="sector">Sector data.</param>
/// <param name="sectorStatus">Sector status.</param>
/// <param name="sectorStatusArray">Sector status array.</param>
/// <param name="sectorsToDo">Number of sectors to do.</param>
/// <param name="readOneSector">True if reading one sector.</param>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="sectorAddress">Sector address.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="generatedTitleKeys">Generated title keys.</param>
/// <param name="isAborted">Is aborted function.</param>
/// <param name="logModuleName">Log module name.</param>
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<bool> 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);
}
/// <summary>Applies CSS after reading a long sector.</summary>
/// <param name="sector">Sector data.</param>
/// <param name="sectorStatus">Sector status.</param>
/// <param name="sectorStatusArray">Sector status array.</param>
/// <param name="sectorsToDo">Number of sectors to do.</param>
/// <param name="readOneSector">True if reading one sector.</param>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="sectorAddress">Sector address.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="generatedTitleKeys">Generated title keys.</param>
/// <param name="isAborted">Is aborted function.</param>
/// <param name="logModuleName">Log module name.</param>
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<bool> 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);
}
/// <summary>Sets all the statuses to dumped.</summary>
/// <param name="sectorStatus">Sector status.</param>
/// <param name="sectorStatusArray">Sector status array.</param>
/// <param name="sectorsToDo">Number of sectors to do.</param>
/// <param name="readOneSector">True if reading one sector.</param>
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;
}
}
/// <summary>Applies the decrypt flags to the statuses.</summary>
/// <param name="blockAppliedCssDecrypt">Block applied CSS decrypt.</param>
/// <param name="sectorsToDo">Number of sectors to do.</param>
/// <param name="sectorStatus">Sector status.</param>
/// <param name="sectorStatusArray">Sector status array.</param>
/// <param name="readOneSector">True if reading one sector.</param>
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;
}
}
/// <summary>Decrypts a DVD sector.</summary>
/// <param name="sector">Sector data.</param>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="sectorAddress">Sector address.</param>
/// <param name="sectorsToDo">Number of sectors to do.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="generatedTitleKeys">Generated title keys.</param>
/// <param name="isAborted">Is aborted function.</param>
/// <param name="logModuleName">Log module name.</param>
/// <param name="blockAppliedCssDecrypt">Block applied CSS decrypt.</param>
static void DecryptDvdSector(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress,
uint sectorsToDo, PluginRegister plugins, ref byte[] generatedTitleKeys,
Func<bool> 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);
}
}
}
}
/// <summary>Decrypts a DVD long sector.</summary>
/// <param name="sector">Sector data.</param>
/// <param name="inputOptical">Input optical media image.</param>
/// <param name="sectorAddress">Sector address.</param>
/// <param name="sectorsToDo">Number of sectors to do.</param>
/// <param name="plugins">Plugin register.</param>
/// <param name="generatedTitleKeys">Generated title keys.</param>
/// <param name="isAborted">Is aborted function.</param>
/// <param name="logModuleName">Log module name.</param>
/// <param name="blockAppliedCssDecrypt">Block applied CSS decrypt.</param>
static void DecryptDvdSectorLong(ref byte[] sector, IOpticalMediaImage inputOptical, ulong sectorAddress,
uint sectorsToDo, PluginRegister plugins, ref byte[] generatedTitleKeys,
Func<bool> 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);
}
}
}
}
}

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

@@ -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,297 @@
// /***************************************************************************
// 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 = 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,
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;
}
}
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))
{
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;
}
}
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

@@ -57,6 +57,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) =>

View File

@@ -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;
}
}

View File

@@ -0,0 +1,125 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : AacsConvertBuffer.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ 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;
/// <summary>Aligns contiguous 2048-byte sectors to CPS units for decrypt during image conversion.</summary>
public sealed class AacsConvertBuffer
{
byte[] _pending = Array.Empty<byte>();
/// <summary>LBA of the first 2048-byte sector in <see cref="_pending"/>.</summary>
ulong _pendingStartLba;
/// <summary>Clears any trailing sectors (e.g. at end of conversion).</summary>
public void Reset()
{
_pending = Array.Empty<byte>();
_pendingStartLba = 0;
}
/// <summary>
/// Prepends pending data to <paramref name="sectorBuffer"/> (first <paramref name="sectorCount"/> sectors),
/// decrypts all complete 6144-byte units, writes decrypted bytes back into <paramref name="sectorBuffer"/>,
/// and stores an incomplete trailing 02 sectors for the next call.
/// </summary>
/// <param name="sectorBuffer">Sector buffer to decrypt.</param>
/// <param name="sectorCount">Number of sectors to decrypt.</param>
/// <param name="firstSectorLba">LBA of the first sector.</param>
/// <param name="decryptedCpsUnitKeys">Decrypted CPS unit keys.</param>
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<byte>();
_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<byte> mergedSpan = merged;
for(int o = 0; o < fullUnitsBytes; o += AacsStreamDecrypt.AlignedUnitLen)
{
Span<byte> 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<byte>();
_pendingStartLba = 0;
}
Buffer.BlockCopy(merged, pl, sectorBuffer, 0, newBytes);
}
}

View File

@@ -0,0 +1,165 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : AacsCrypto.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ 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;
/// <summary>Low-level AES operations used by Blu-ray AACS stream decryption.</summary>
public static class AacsCrypto
{
/// <summary>AES-128 CBC IV.</summary>
static readonly byte[] AacsCbcIv =
[
0x0b, 0xa0, 0xf8, 0xdd, 0xfe, 0xa6, 0x1f, 0xb3, 0xd8, 0xdf, 0x9f, 0x56, 0x6a, 0x05, 0x0f, 0x78
];
/// <summary>AES-128 ECB encrypt one block.</summary>
/// <param name="key">AES-128 key (16 bytes).</param>
/// <param name="plaintext">Plaintext to encrypt (16 bytes).</param>
/// <param name="ciphertext">Encrypted output (16 bytes).</param>
public static void Aes128EcbEncrypt(ReadOnlySpan<byte> key, ReadOnlySpan<byte> plaintext, Span<byte> 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);
}
}
/// <summary>AES-128 ECB decrypt one block.</summary>
/// <param name="key">AES-128 key (16 bytes).</param>
/// <param name="ciphertext">Ciphertext to decrypt (16 bytes).</param>
/// <param name="plaintext">Decrypted output (16 bytes).</param>
public static void Aes128EcbDecrypt(ReadOnlySpan<byte> key, ReadOnlySpan<byte> ciphertext, Span<byte> 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);
}
/// <summary>AES-128 CBC decrypt.</summary>
/// <param name="key">AES-128 key (16 bytes).</param>
/// <param name="ciphertext">Ciphertext to decrypt (16 bytes).</param>
/// <param name="plaintext">Decrypted output (16 bytes).</param>
public static void AacsCbcDecrypt(ReadOnlySpan<byte> key, ReadOnlySpan<byte> ciphertext, Span<byte> 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);
}
/// <summary>
/// Volume unique key from media key and volume ID.
/// </summary>
/// <param name="mediaKey">Media key (16 bytes).</param>
/// <param name="volumeId">Volume ID (16 bytes).</param>
/// <param name="volumeUniqueKey">Volume unique key (16 bytes).</param>
public static void DeriveVolumeUniqueKey(ReadOnlySpan<byte> mediaKey, ReadOnlySpan<byte> volumeId,
Span<byte> volumeUniqueKey)
{
if(mediaKey.Length != 16 || volumeId.Length != 16 || volumeUniqueKey.Length != 16)
throw new ArgumentException("MK, VID, and VUK must be 16 bytes.");
Span<byte> tmp = stackalloc byte[16];
Aes128EcbDecrypt(mediaKey, volumeId, tmp);
for(int a = 0; a < 16; a++)
volumeUniqueKey[a] = (byte)(tmp[a] ^ volumeId[a]);
}
/// <summary>Decrypt one CPS unit key from encrypted form.</summary>
/// <param name="volumeUniqueKey">Volume unique key (16 bytes).</param>
/// <param name="encryptedUnitKey">Encrypted CPS unit key (16 bytes).</param>
/// <param name="decryptedUnitKey">Decrypted CPS unit key (16 bytes).</param>
public static void DecryptCpsUnitKey(ReadOnlySpan<byte> volumeUniqueKey, ReadOnlySpan<byte> encryptedUnitKey,
Span<byte> 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);
}
}

View File

@@ -0,0 +1,135 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : AacsStreamDecrypt.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ 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;
/// <summary>Decrypts one 6144-byte aligned unit using decrypted CPS unit keys.</summary>
public static class AacsStreamDecrypt
{
/// <summary>User-data sector size and bus-encryption block size.</summary>
public const int SectorLen = 2048;
/// <summary>Three user sectors: CPS aligned unit length.</summary>
public const int AlignedUnitLen = 6144;
/// <summary>
/// If the unit is marked unencrypted (MPEG-TS copy permission bits), returns <see langword="true"/>
/// without changing the buffer. Otherwise tries each CPS unit key until MPEG-TS sync verifies.
/// </summary>
/// <param name="unit">Unit to decrypt.</param>
/// <param name="decryptedCpsUnitKeys">Decrypted CPS unit keys.</param>
/// <returns>True if the unit was decrypted successfully.</returns>
public static bool TryDecryptAlignedUnit(Span<byte> unit, ReadOnlySpan<byte[]> 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;
}
/// <summary>Decrypts one unit with a given CPS unit key.</summary>
/// <param name="unit">Unit to decrypt.</param>
/// <param name="decryptedCpsUnitKey">Decrypted CPS unit key.</param>
/// <returns>True if the unit was decrypted successfully.</returns>
public static bool TryDecryptUnitWithKey(Span<byte> unit, ReadOnlySpan<byte> 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<byte> key = stackalloc byte[16];
AacsCrypto.Aes128EcbEncrypt(decryptedCpsUnitKey, unit.Slice(0, 16), key);
for(int a = 0; a < 16; a++)
key[a] ^= unit[a];
ReadOnlySpan<byte> cipher = unit.Slice(16, AlignedUnitLen - 16);
Span<byte> plain = unit.Slice(16, AlignedUnitLen - 16);
byte[] tmp = new byte[AlignedUnitLen - 16];
AacsCrypto.AacsCbcDecrypt(key, cipher, tmp);
tmp.CopyTo(plain);
return VerifyAndNormalizeTransportStream(unit);
}
/// <summary>Verifies and normalizes the transport stream. Removes copy-permission indicator bits.</summary>
/// <param name="unit">Unit to verify and normalize.</param>
/// <returns>True if the unit was verified and normalized successfully.</returns>
public static bool VerifyAndNormalizeTransportStream(Span<byte> 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;
}
}

View File

@@ -0,0 +1,65 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : HDDVD.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ 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;
/// <summary>Aligns contiguous 2048-byte sectors to CPS units for decrypt during image conversion.</summary>
public sealed class HDDVD
{
/// <summary>
/// HD DVD video discs always have a <c>HVDVD_TS</c> folder. If it doesn't have one, it's not a HD DVD video.
/// </summary>
/// <param name="input"><c>IOpticalMediaImage</c> to check for <c>HVDVD_TS</c> folder in.</param>
/// <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>HVDVD_TS</c> folder was found.</returns>
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;
}
}

View File

@@ -0,0 +1,162 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : UnitKeyRoParser.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ 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;
/// <summary>Parsed CPS encrypted unit keys from <c>Unit_Key_RO.inf</c>.</summary>
public sealed class UnitKeyRoParseResult
{
UnitKeyRoParseResult(byte[][] encryptedCpsUnitKeys, bool isAacs2Layout)
{
EncryptedCpsUnitKeys = encryptedCpsUnitKeys;
IsAacs2Layout = isAacs2Layout;
}
/// <summary>One 16-byte encrypted key per CPS unit, index order.</summary>
public byte[][] EncryptedCpsUnitKeys { get; }
/// <summary>True when the AACS2 key stride (64 bytes per key entry) was used.</summary>
public bool IsAacs2Layout { get; }
/// <summary>Parses Blu-ray <c>Unit_Key_RO.inf</c> (AACS1 layout by default).</summary>
/// <param name="data">Data to parse.</param>
/// <param name="tryAacs2">True to try AACS2 layout.</param>
/// <returns>Parsed CPS encrypted unit keys.</returns>
public static UnitKeyRoParseResult? TryParse(ReadOnlySpan<byte> data, bool tryAacs2 = false)
{
if(tryAacs2)
return TryParseInternal(data, true);
UnitKeyRoParseResult? r = TryParseInternal(data, false);
return r ?? TryParseInternal(data, true);
}
/// <summary>Parses Blu-ray <c>Unit_Key_RO.inf</c> (AACS1 layout by default).</summary>
/// <param name="data">Data to parse.</param>
/// <param name="aacs2">True to try AACS2 layout.</param>
/// <returns>Parsed CPS encrypted unit keys.</returns>
static UnitKeyRoParseResult? TryParseInternal(ReadOnlySpan<byte> 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<byte> 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);
}
/// <summary>
/// Interprets a tag or buffer as raw concatenated 16-byte encrypted CPS keys (no .inf wrapper).
/// </summary>
/// <param name="data">Data to parse.</param>
/// <returns>Parsed CPS encrypted unit keys.</returns>
public static UnitKeyRoParseResult? TryParseRawEncryptedKeys(ReadOnlySpan<byte> data)
{
if(data.Length < 16 || data.Length % 16 != 0)
return null;
int n = data.Length / 16;
byte[][] keys = new byte[n][];
ReadOnlySpan<byte> 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);
}
}

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

View File

@@ -135,7 +135,9 @@ public sealed partial class ISO9660
if(_blockSize == 2048)
{
buffer = Sector.GetUserData(data, interleaved, fileNumber);
buffer = data.Length == 2064
? Decoders.DVD.Sector.GetUserData(data)
: Sector.GetUserData(data, interleaved, fileNumber);
return ErrorNumber.NoError;
}
@@ -232,7 +234,9 @@ public sealed partial class ISO9660
}
}
byte[] 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);
}

View File

@@ -0,0 +1,157 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Extents.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
{
/// <summary>
/// Gets physical sector extents for a file path.
/// </summary>
/// <param name="path">Absolute filesystem path.</param>
/// <param name="extents">Physical extents as (startSector, sectorCount).</param>
/// <returns>Error number.</returns>
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
};
}
/// <summary>
/// Collects short allocation descriptor extents.
/// </summary>
/// <param name="feBuffer">File entry buffer.</param>
/// <param name="adOffset">Allocation descriptor offset.</param>
/// <param name="adLength">Allocation descriptor length.</param>
/// <param name="partitionReferenceNumber">Partition reference number.</param>
/// <param name="extents">Physical extents as (startSector, sectorCount).</param>
/// <returns>Error number.</returns>
ErrorNumber CollectShortAdExtents(byte[] feBuffer, int adOffset, int adLength, ushort partitionReferenceNumber,
List<(ulong startSector, uint sectorCount)> extents)
{
int sadSize = System.Runtime.InteropServices.Marshal.SizeOf<ShortAllocationDescriptor>();
int adPos = adOffset;
while(adPos + sadSize <= adOffset + adLength)
{
ShortAllocationDescriptor sad =
Marshal.ByteArrayToStructureLittleEndian<ShortAllocationDescriptor>(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;
}
/// <summary>
/// Collects long allocation descriptor extents.
/// </summary>
/// <param name="feBuffer">File entry buffer.</param>
/// <param name="adOffset">Allocation descriptor offset.</param>
/// <param name="adLength">Allocation descriptor length.</param>
/// <param name="extents">Physical extents as (startSector, sectorCount).</param>
/// <returns>Error number.</returns>
ErrorNumber CollectLongAdExtents(byte[] feBuffer, int adOffset, int adLength,
List<(ulong startSector, uint sectorCount)> extents)
{
int ladSize = System.Runtime.InteropServices.Marshal.SizeOf<LongAllocationDescriptor>();
int adPos = adOffset;
while(adPos + ladSize <= adOffset + adLength)
{
LongAllocationDescriptor lad =
Marshal.ByteArrayToStructureLittleEndian<LongAllocationDescriptor>(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;
}
}

View File

@@ -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"),

View File

@@ -0,0 +1,189 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : HdDvdIsoProbe.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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
// <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
{
/// <summary>True if a filesystem under some partition exposes the HD DVD-Video <c>HVDVD_TS</c> folder.</summary>
bool TryDetectHdDvdVideoIso()
{
IMediaImage image = this;
IOpticalMediaImage optical = this;
PluginRegister plugins = PluginRegister.Singleton;
List<Partition> 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;
}
/// <summary>
/// Lists partitions the same way as <see cref="Aaru.Core.Partitions.GetAll"/> for images that are not
/// tape.
/// </summary>
static List<Partition> EnumeratePartitionsForHdDvdProbe(IMediaImage image)
{
PluginRegister plugins = PluginRegister.Singleton;
List<Partition> foundPartitions = [];
List<Partition> childPartitions = [];
List<ulong> 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<Partition> 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<Partition> 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<Partition> children = [];
foreach(IPartition plugin in plugins.Partitions.Values)
{
if(plugin is null)
continue;
if(!plugin.GetInformation(image, out List<Partition> 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<ulong> 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();
}
}

View File

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

View File

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

View File

@@ -3562,4 +3562,43 @@ It has no sense to do it, and it will put too much strain on the tape.</value>
<data name="Output_format_not_initialized" xml:space="preserve">
<value>[red]Output format not initialized.[/]</value>
</data>
<data name="Aacs_missing_volume_unique_key" xml:space="preserve">
<value>Blu-ray AACS decryption requires AACS_VolumeUniqueKey media tag, or both AACS_MediaKey and AACS_VolumeIdentifier.</value>
</data>
<data name="Aacs_missing_unit_keys" xml:space="preserve">
<value>Blu-ray AACS decryption requires CPS unit keys: AACS/Unit_Key_RO.inf on a mounted UDF/ISO partition, or AACS_DataKeys media tag.</value>
</data>
<data name="Aacs_unit_key_inf_invalid" xml:space="preserve">
<value>AACS/Unit_Key_RO.inf could not be parsed.</value>
</data>
<data name="Aacs_encrypted_unit_key_invalid_length" xml:space="preserve">
<value>Invalid encrypted CPS unit key length (expected 16 bytes).</value>
</data>
<data name="Aacs2_unit_keys_not_supported" xml:space="preserve">
<value>AACS2 (Ultra HD Blu-ray) unit keys are not supported in this version.</value>
</data>
<data name="Aacs_hddvd_not_supported" xml:space="preserve">
<value>HD DVD AACS decryption is not implemented.</value>
</data>
<data name="Aacs_incomplete_cps_unit_at_track_end" xml:space="preserve">
<value>Incomplete AACS CPS unit at end of track ({0} sector(s) pending, starting at LBA {1}).</value>
</data>
<data name="Aacs_incomplete_cps_unit_at_track_end_continuing" xml:space="preserve">
<value>Incomplete AACS CPS unit ({0} sector(s) pending, starting at LBA {1}); writing pending sectors as-is and continuing.</value>
</data>
<data name="Aacs_incomplete_cps_unit_pending_before_read_error" xml:space="preserve">
<value>Cannot read sectors while an incomplete AACS CPS unit is pending (started at LBA {0}).</value>
</data>
<data name="Aacs_incomplete_cps_unit_after_read_error_continuing" xml:space="preserve">
<value>Read failed with an incomplete AACS CPS unit pending ({0} sector(s) from LBA {1}); writing pending sectors as-is and continuing.</value>
</data>
<data name="Aacs_could_not_decrypt_cps_unit_starting_at_0" xml:space="preserve">
<value>Could not decrypt AACS CPS unit starting at LBA {0}.</value>
</data>
<data name="Aacs_could_not_decrypt_cps_unit_starting_at_0_continuing" xml:space="preserve">
<value>Could not decrypt AACS CPS unit starting at LBA {0}; writing ciphertext and continuing.</value>
</data>
<data name="Aacs_bd_decrypt_requires_0_byte_sectors" xml:space="preserve">
<value>Blu-ray AACS decryption requires {0}-byte user sectors; this image reports a different sector size.</value>
</data>
</root>