diff --git a/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs b/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs new file mode 100644 index 000000000..033c0d1f8 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/AacsBdExtentResolver.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Filesystems; + +namespace Aaru.Core.Image; + +internal static class AacsBdExtentResolver +{ + internal readonly struct LbaRange + { + internal ulong Start { get; init; } + internal ulong End { get; init; } + } + + /// + /// Resolves the stream file extents for a Blu-ray optical media image. + /// It searches for the stream file extents in the /BDMV/STREAM and + /// /BDMV/STREAM/SSIF directories. + /// + /// Input optical media image. + /// Plugin register. + /// Stream file extents. + /// Error message. + /// Error number. + internal static ErrorNumber ResolveStreamFileExtents(IOpticalMediaImage inputOptical, + PluginRegister plugins, + out List ranges, + out string error) + { + ranges = []; + error = null; + + IReadOnlyFilesystem udfRo = null; + UDF udf = null; + + foreach(IFilesystem fs in plugins.Filesystems.Values) + { + if(fs is not UDF udfPlugin) continue; + + Partition wholeImage = new() + { + Start = 0, + Length = inputOptical.Info.Sectors, + Size = inputOptical.Info.Sectors * inputOptical.Info.SectorSize, + Sequence = 0, + Type = "UDF" + }; + + if(!udfPlugin.Identify(inputOptical, wholeImage)) continue; + + ErrorNumber mountErr = udfPlugin.Mount(inputOptical, wholeImage, Encoding.UTF8, null, null); + + if(mountErr != ErrorNumber.NoError) + { + error = "UDF mount failed while resolving Blu-ray stream extents."; + + return mountErr; + } + + udfRo = udfPlugin; + udf = udfPlugin; + + break; + } + + if(udfRo is null || udf is null) + { + error = "Could not mount UDF filesystem for Blu-ray stream extent discovery."; + + return ErrorNumber.NotSupported; + } + + try + { + ErrorNumber errno = AddDirectoryExtents(udfRo, udf, "/BDMV/STREAM", ".M2TS", ref ranges); + + if(errno != ErrorNumber.NoError) + { + error = "Could not resolve extents for /BDMV/STREAM."; + + return errno; + } + + errno = AddDirectoryExtents(udfRo, udf, "/BDMV/STREAM/SSIF", ".SSIF", ref ranges); + + if(errno != ErrorNumber.NoError && errno != ErrorNumber.NoSuchFile) + { + error = "Could not resolve extents for /BDMV/STREAM/SSIF."; + + return errno; + } + + if(ranges.Count == 0) + { + error = "No stream file extents found under /BDMV/STREAM."; + + return ErrorNumber.NoData; + } + + ranges.Sort(static (a, b) => a.Start.CompareTo(b.Start)); + ranges = MergeRanges(ranges); + + return ErrorNumber.NoError; + } + finally + { + udfRo.Unmount(); + } + } + + /// Checks if a given LBA is allowed for decryption. + /// Stream file extents. + /// LBA to check. + /// True if the LBA is allowed for decryption. + internal static bool IsLbaAllowed(List ranges, ulong lba) + { + int lo = 0; + int hi = ranges.Count - 1; + + while(lo <= hi) + { + int mid = lo + (hi - lo) / 2; + LbaRange range = ranges[mid]; + + if(lba < range.Start) + hi = mid - 1; + else if(lba > range.End) + lo = mid + 1; + else + return true; + } + + return false; + } + + + /// Adds the extents for a given directory to the list of stream file extents. + /// Read-only filesystem. + /// UDF filesystem. + /// Directory path. + /// Extension of the files to add. + /// List of stream file extents. + /// Error number. + static ErrorNumber AddDirectoryExtents(IReadOnlyFilesystem roFs, UDF udf, string dirPath, string extension, + ref List ranges) + { + ErrorNumber errno = roFs.OpenDir(dirPath, out IDirNode dirNode); + + if(errno != ErrorNumber.NoError) return errno; + + try + { + while(true) + { + errno = roFs.ReadDir(dirNode, out string filename); + + if(errno != ErrorNumber.NoError) return errno; + + if(filename is null) break; + + if(!filename.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) continue; + + string fullPath = dirPath + "/" + filename; + + errno = udf.GetFilePhysicalSectorExtents(fullPath, out List<(ulong startSector, uint sectorCount)> extents); + + if(errno != ErrorNumber.NoError) return errno; + + foreach((ulong startSector, uint sectorCount) extent in extents) + { + if(extent.sectorCount == 0) continue; + + ranges.Add(new LbaRange + { + Start = extent.startSector, + End = extent.startSector + extent.sectorCount - 1 + }); + } + } + } + finally + { + roFs.CloseDir(dirNode); + } + + return ErrorNumber.NoError; + } + + /// Merges the extents for a given directory to the list of stream file extents. + /// List of stream file extents. + /// Merged list of stream file extents. + static List MergeRanges(List ranges) + { + if(ranges.Count <= 1) return ranges; + + List merged = []; + LbaRange current = ranges[0]; + + for(int i = 1; i < ranges.Count; i++) + { + LbaRange next = ranges[i]; + + if(next.Start <= current.End + 1) + { + current = new LbaRange { Start = current.Start, End = Math.Max(current.End, next.End) }; + continue; + } + + merged.Add(current); + current = next; + } + + merged.Add(current); + + return merged; + } +} diff --git a/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs b/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs new file mode 100644 index 000000000..eea731224 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/AacsBdOpticalPipeline.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decryption.Aacs; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +/// Blu-ray AACS sector pipeline for convert/merge: LBA-aligned 6144-byte CPS units with output staging. +internal static class AacsBdOpticalPipeline +{ + internal readonly struct Ui + { + public Action OnInitProgress { get; init; } + public Action OnInitProgress2 { get; init; } + public Action OnEndProgress { get; init; } + public Action OnEndProgress2 { get; init; } + public Action OnTrackProgress { get; init; } + public Action OnSectorRangeProgress { get; init; } + public Action ErrorMessage { get; init; } + public Action StoppingErrorMessage { get; init; } + } + + /// Runs the Blu-ray AACS sector pipeline. + /// Input optical media image. + /// Output optical media image. + /// Batch count. + /// Force flag. + /// Aborted flag. + /// Decrypted CPS unit keys. + /// Allow decrypt LBA function. + /// UI. + /// Error number. + internal static ErrorNumber Run(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical, uint batchCount, + bool force, ref bool aborted, byte[][] decryptedCpsUnitKeys, + Func allowDecryptLba, Ui ui) + { + ui.OnInitProgress?.Invoke(); + ui.OnInitProgress2?.Invoke(); + + int currentTrack = 0; + + foreach(Track track in inputOptical.Tracks) + { + if(aborted) break; + + ui.OnTrackProgress?.Invoke(currentTrack, inputOptical.Tracks.Count); + + ulong doneSectors = 0; + ulong trackSectors = track.EndSector - track.StartSector + 1; + + List pending = []; + List pendingLba = []; + List pendingSt = []; + + while(doneSectors < trackSectors) + { + if(aborted) break; + + uint sectorsToDo = trackSectors - doneSectors >= batchCount + ? batchCount + : (uint)(trackSectors - doneSectors); + + ui.OnSectorRangeProgress?.Invoke(doneSectors + track.StartSector, + doneSectors + sectorsToDo + track.StartSector, + track.Sequence, + (long)doneSectors, + (long)trackSectors); + + ErrorNumber errno = inputOptical.ReadSectors(doneSectors + track.StartSector, + false, + sectorsToDo, + out byte[] sectorData, + out SectorStatus[] sectorStatusArray); + + if(errno != ErrorNumber.NoError) + { + if(force) + { + ErrorNumber pend = FlushPendingOnReadSkip(outputOptical, ref pending, ref pendingLba, + ref pendingSt, force, ui); + + if(pend != ErrorNumber.NoError) return pend; + + ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_continuing, + errno, + doneSectors + track.StartSector)); + } + else + { + if(pending.Count > 0) + { + ui.StoppingErrorMessage?.Invoke(string.Format( + Aaru.Localization.Core.Aacs_incomplete_cps_unit_pending_before_read_error, + pendingLba[0])); + + return errno; + } + + ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_reading_sector_1_not_continuing, + errno, + doneSectors + track.StartSector)); + + return errno; + } + + doneSectors += sectorsToDo; + + continue; + } + + int bps = sectorData.Length / (int)sectorsToDo; + + if(bps != AacsStreamDecrypt.SectorLen || sectorData.Length % sectorsToDo != 0) + { + ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_bd_decrypt_requires_0_byte_sectors, + bps)); + + return ErrorNumber.InOutError; + } + + for(uint i = 0; i < sectorsToDo; i++) + { + byte[] one = new byte[AacsStreamDecrypt.SectorLen]; + Buffer.BlockCopy(sectorData, (int)(i * AacsStreamDecrypt.SectorLen), one, 0, AacsStreamDecrypt.SectorLen); + ulong lba = doneSectors + track.StartSector + i; + SectorStatus st = sectorStatusArray[i]; + + bool allowDecrypt = allowDecryptLba is null || allowDecryptLba(lba); + + ErrorNumber step = ProcessOneUserSector(outputOptical, + decryptedCpsUnitKeys, + one, + lba, + allowDecrypt, + st, + ref pending, + ref pendingLba, + ref pendingSt, + force, + ui); + + if(step != ErrorNumber.NoError) return step; + } + + doneSectors += sectorsToDo; + } + + ErrorNumber flushErr = FlushPendingAtTrackEnd(outputOptical, + ref pending, + ref pendingLba, + ref pendingSt, + force, + ui); + + if(flushErr != ErrorNumber.NoError) return flushErr; + + currentTrack++; + } + + ui.OnEndProgress2?.Invoke(); + ui.OnEndProgress?.Invoke(); + + return ErrorNumber.NoError; + } + + /// Flushes the pending sectors on read skip. + /// Output optical media image. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushPendingOnReadSkip(IWritableOpticalImage outputOptical, ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + if(pending.Count == 0) return ErrorNumber.NoError; + + ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_after_read_error_continuing, + pending.Count, + pendingLba[0])); + + for(int i = 0; i < pending.Count; i++) + { + ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui); + + if(step != ErrorNumber.NoError) return step; + } + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return ErrorNumber.NoError; + } + + /// Processes one user sector. + /// Output optical media image. + /// Decrypted CPS unit keys. + /// Sector data. + /// LBA of the sector. + /// Allow decrypt flag. + /// Read status of the sector. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber ProcessOneUserSector(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys, + byte[] sector, ulong lba, bool allowDecrypt, SectorStatus readStatus, + ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + if(!allowDecrypt) + { + ErrorNumber flush = FlushPendingAsDumped(outputOptical, ref pending, ref pendingLba, ref pendingSt, force, ui); + + if(flush != ErrorNumber.NoError) return flush; + + return WriteOne(outputOptical, lba, sector, SectorStatus.Dumped, force, ui); + } + + if(pending.Count == 0) + { + if((sector[0] & 0xc0) == 0) + return WriteOne(outputOptical, lba, sector, SectorStatus.Dumped, force, ui); + + pending.Add(sector); + pendingLba.Add(lba); + pendingSt.Add(readStatus); + + return ErrorNumber.NoError; + } + + pending.Add(sector); + pendingLba.Add(lba); + pendingSt.Add(readStatus); + + if(pending.Count < 3) return ErrorNumber.NoError; + + return FlushCompleteUnit(outputOptical, + decryptedCpsUnitKeys, + ref pending, + ref pendingLba, + ref pendingSt, + force, + ui); + } + + /// Flushes a complete unit. + /// Output optical media image. + /// Decrypted CPS unit keys. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushCompleteUnit(IWritableOpticalImage outputOptical, byte[][] decryptedCpsUnitKeys, + ref List pending, ref List pendingLba, + ref List pendingSt, bool force, Ui ui) + { + byte[] unit = new byte[AacsStreamDecrypt.AlignedUnitLen]; + Buffer.BlockCopy(pending[0], 0, unit, 0, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(pending[1], 0, unit, AacsStreamDecrypt.SectorLen, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(pending[2], 0, unit, AacsStreamDecrypt.SectorLen * 2, AacsStreamDecrypt.SectorLen); + + if(!AacsStreamDecrypt.TryDecryptAlignedUnit(unit, decryptedCpsUnitKeys)) + { + if(!force) + { + ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_could_not_decrypt_cps_unit_starting_at_0, + pendingLba[0])); + + return ErrorNumber.WriteError; + } + + ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_could_not_decrypt_cps_unit_starting_at_0_continuing, + pendingLba[0])); + + ErrorNumber w = WriteThree(outputOptical, + pending[0], + pending[1], + pending[2], + pendingLba[0], + pendingSt[0], + pendingSt[1], + pendingSt[2], + force, + ui); + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return w; + } + + SectorStatus[] ok = [SectorStatus.Unencrypted, SectorStatus.Unencrypted, SectorStatus.Unencrypted]; + ErrorNumber e = WriteSectorsFromBuffer(outputOptical, unit, pendingLba[0], ok, force, ui); + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return e; + } + + /// Writes three sectors from a buffer. + /// Output optical media image. + /// Unit data. + /// First LBA of the sectors. + /// Statuses of the sectors. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber WriteSectorsFromBuffer(IWritableOpticalImage outputOptical, byte[] unit, ulong firstLba, + SectorStatus[] threeStatuses, bool force, Ui ui) + { + bool result = outputOptical.WriteSectors(unit, firstLba, false, 3, threeStatuses); + + if(!result) + { + if(force) + { + ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing, + outputOptical.ErrorMessage, + firstLba)); + } + else + { + ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + firstLba)); + + return ErrorNumber.WriteError; + } + } + + return ErrorNumber.NoError; + } + + /// Flushes the pending sectors as dumped. + /// Output optical media image. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushPendingAsDumped(IWritableOpticalImage outputOptical, ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + for(int i = 0; i < pending.Count; i++) + { + ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui); + + if(step != ErrorNumber.NoError) return step; + } + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return ErrorNumber.NoError; + } + + /// Writes three sectors from a buffer. + /// Output optical media image. + /// First sector data. + /// Second sector data. + /// Third sector data. + /// First LBA of the sectors. + /// Statuses of the sectors. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber WriteThree(IWritableOpticalImage outputOptical, byte[] a, byte[] b, byte[] c, ulong firstLba, + SectorStatus sa, SectorStatus sb, SectorStatus sc, bool force, Ui ui) + { + byte[] buf = new byte[AacsStreamDecrypt.AlignedUnitLen]; + Buffer.BlockCopy(a, 0, buf, 0, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(b, 0, buf, AacsStreamDecrypt.SectorLen, AacsStreamDecrypt.SectorLen); + Buffer.BlockCopy(c, 0, buf, AacsStreamDecrypt.SectorLen * 2, AacsStreamDecrypt.SectorLen); + + SectorStatus[] sts = [sa, sb, sc]; + + return WriteSectorsFromBuffer(outputOptical, buf, firstLba, sts, force, ui); + } + + /// Writes one sector. + /// Output optical media image. + /// LBA of the sector. + /// Sector data. + /// Status of the sector. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber WriteOne(IWritableOpticalImage outputOptical, ulong lba, byte[] sector, SectorStatus status, + bool force, Ui ui) + { + bool result = outputOptical.WriteSector(sector, lba, false, status); + + if(!result) + { + if(force) + { + ui.ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing, + outputOptical.ErrorMessage, + lba)); + } + else + { + ui.StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + outputOptical.ErrorMessage, + lba)); + + return ErrorNumber.WriteError; + } + } + + return ErrorNumber.NoError; + } + + /// Flushes the pending sectors at track end. + /// Output optical media image. + /// List of pending sectors. + /// List of pending LBA. + /// List of pending sector statuses. + /// Force flag. + /// UI. + /// Error number. + static ErrorNumber FlushPendingAtTrackEnd(IWritableOpticalImage outputOptical, ref List pending, + ref List pendingLba, ref List pendingSt, bool force, + Ui ui) + { + if(pending.Count == 0) return ErrorNumber.NoError; + + if(!force) + { + ui.StoppingErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_at_track_end, + pending.Count, + pendingLba[0])); + + return ErrorNumber.WriteError; + } + + ui.ErrorMessage?.Invoke(string.Format(Aaru.Localization.Core.Aacs_incomplete_cps_unit_at_track_end_continuing, + pending.Count, + pendingLba[0])); + + for(int i = 0; i < pending.Count; i++) + { + ErrorNumber step = WriteOne(outputOptical, pendingLba[i], pending[i], SectorStatus.Dumped, force, ui); + + if(step != ErrorNumber.NoError) return step; + } + + pending.Clear(); + pendingLba.Clear(); + pendingSt.Clear(); + + return ErrorNumber.NoError; + } +} diff --git a/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs b/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs new file mode 100644 index 000000000..6035c36b8 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/AacsKeyResolver.cs @@ -0,0 +1,249 @@ +using System; +using System.Text; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decryption.Aacs; + +namespace Aaru.Core.Image; + +/// Loads VUK and decrypts CPS unit keys for Blu-ray AACS conversion. +public static class AacsKeyResolver +{ + static readonly string[] UnitKeyPaths = + [ + "AACS/Unit_Key_RO.inf", + "AACS\\Unit_Key_RO.inf" + ]; + + /// Checks if every byte in the buffer is zero. + /// Buffer to check. + /// True if every byte in the buffer is zero. + public static bool IsAllZero(ReadOnlySpan buffer) + { + for(int i = 0; i < buffer.Length; i++) + { + if(buffer[i] != 0) + return false; + } + + return true; + } + + /// + /// Obtains decrypted 16-byte CPS unit keys in disc order, or if keys cannot be loaded. + /// + /// Input optical media image. + /// Plugin register. + /// Encoding. + /// Error message. + /// Decrypted CPS unit keys. + public static byte[][]? TryGetDecryptedCpsUnitKeys(IOpticalMediaImage image, PluginRegister plugins, + Encoding? encoding, out string? errorMessage) + { + errorMessage = null; + encoding ??= Encoding.UTF8; + + if(!TryResolveVolumeUniqueKey(image, out byte[] vuk, out errorMessage)) + return null; + + if(!TryLoadEncryptedCpsUnitKeys(image, plugins, encoding, out byte[][]? encKeys, out errorMessage) || + encKeys is null) + return null; + + byte[][] decrypted = new byte[encKeys.Length][]; + + for(int i = 0; i < encKeys.Length; i++) + { + if(encKeys[i].Length != 16) + { + errorMessage = Aaru.Localization.Core.Aacs_encrypted_unit_key_invalid_length; + + return null; + } + + decrypted[i] = new byte[16]; + AacsCrypto.DecryptCpsUnitKey(vuk, encKeys[i], decrypted[i]); + } + + return decrypted; + } + + /// Tries to resolve the volume unique key from the media tags. + /// Input optical media image. + /// Volume unique key. + /// Error message. + /// True if the volume unique key was resolved successfully. + static bool TryResolveVolumeUniqueKey(IOpticalMediaImage image, out byte[] vuk, out string? errorMessage) + { + vuk = new byte[16]; + errorMessage = null; + + if(image.ReadMediaTag(MediaTagType.AacsVolumeUniqueKey, out byte[]? tagVuk) == ErrorNumber.NoError && + tagVuk is { Length: 16 } && + !IsAllZero(tagVuk)) + { + Buffer.BlockCopy(tagVuk, 0, vuk, 0, 16); + + return true; + } + + if(image.ReadMediaTag(MediaTagType.AacsMediaKey, out byte[]? mk) != ErrorNumber.NoError || + mk is not { Length: 16 } || + IsAllZero(mk)) + { + errorMessage = Aaru.Localization.Core.Aacs_missing_volume_unique_key; + + return false; + } + + if(image.ReadMediaTag(MediaTagType.AACS_VolumeIdentifier, out byte[]? vid) != ErrorNumber.NoError || + vid is not { Length: 16 } || + IsAllZero(vid)) + { + errorMessage = Aaru.Localization.Core.Aacs_missing_volume_unique_key; + + return false; + } + + AacsCrypto.DeriveVolumeUniqueKey(mk, vid, vuk); + + return true; + } + + /// Tries to load the encrypted CPS unit keys from the media tags or filesystem. + /// Input optical media image. + /// Plugin register. + /// Encoding. + /// Encrypted CPS unit keys. + /// Error message. + /// True if the encrypted CPS unit keys were loaded successfully. + static bool TryLoadEncryptedCpsUnitKeys(IOpticalMediaImage image, PluginRegister plugins, Encoding encoding, + out byte[][]? encKeys, out string? errorMessage) + { + encKeys = null; + errorMessage = null; + + if(image.ReadMediaTag(MediaTagType.AACS_DataKeys, out byte[]? dataKeys) == ErrorNumber.NoError && + dataKeys is { Length: > 0 }) + { + UnitKeyRoParseResult? parsed = UnitKeyRoParseResult.TryParse(dataKeys); + + if(parsed != null) + { + if(parsed.IsAacs2Layout) + { + errorMessage = Aaru.Localization.Core.Aacs2_unit_keys_not_supported; + + return false; + } + + encKeys = parsed.EncryptedCpsUnitKeys; + + if(encKeys.Length > 0) + return true; + } + + UnitKeyRoParseResult? raw = UnitKeyRoParseResult.TryParseRawEncryptedKeys(dataKeys); + + if(raw != null) + { + encKeys = raw.EncryptedCpsUnitKeys; + + if(encKeys.Length > 0) + return true; + } + } + + if(TryReadUnitKeyInfFromFilesystem(image, plugins, encoding, out byte[]? fileBytes) && + fileBytes is { Length: > 0 }) + { + UnitKeyRoParseResult? parsed = UnitKeyRoParseResult.TryParse(fileBytes); + + if(parsed == null) + { + errorMessage = Aaru.Localization.Core.Aacs_unit_key_inf_invalid; + + return false; + } + + if(parsed.IsAacs2Layout) + { + errorMessage = Aaru.Localization.Core.Aacs2_unit_keys_not_supported; + + return false; + } + + encKeys = parsed.EncryptedCpsUnitKeys; + + return encKeys.Length > 0; + } + + errorMessage = Aaru.Localization.Core.Aacs_missing_unit_keys; + + return false; + } + + /// Tries to read the Unit_Key_RO.inf file from the filesystem. + /// Input optical media image. + /// Plugin register. + /// Encoding. + /// File bytes. + /// True if the Unit_Key_RO.inf file was read successfully. + static bool TryReadUnitKeyInfFromFilesystem(IOpticalMediaImage image, PluginRegister plugins, Encoding encoding, + out byte[]? fileBytes) + { + fileBytes = null; + + foreach(Partition partition in Partitions.GetAll(image)) + { + foreach(IFilesystem fs in plugins.Filesystems.Values) + { + if(fs is not IReadOnlyFilesystem rofs) + continue; + + if(!fs.Identify(image, partition)) + continue; + + if(rofs.Mount(image, partition, encoding, null, null) != ErrorNumber.NoError) + continue; + + try + { + foreach(string path in UnitKeyPaths) + { + if(rofs.OpenFile(path, out IFileNode? node) != ErrorNumber.NoError || node is null) + continue; + + try + { + if(node.Length <= 0 || node.Length > 10 * 1024 * 1024) + continue; + + byte[] buf = new byte[node.Length]; + + if(rofs.ReadFile(node, node.Length, buf, out long read) != ErrorNumber.NoError || + read != node.Length) + continue; + + fileBytes = buf; + + return true; + } + finally + { + rofs.CloseFile(node); + } + } + } + finally + { + rofs.Unmount(); + } + } + } + + return false; + } +} diff --git a/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs b/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs new file mode 100644 index 000000000..71b4bd692 --- /dev/null +++ b/Aaru.Core/Image/Convert/AACS/ConvertAacsBd.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Localization; + +namespace Aaru.Core.Image; + +public partial class Convert +{ + /// Blu-ray AACS: sector-aligned CPS units, staging, and per-sector . + ErrorNumber ConvertAacsBdOpticalSectors(IOpticalMediaImage inputOptical, IWritableOpticalImage outputOptical) + { + ErrorNumber errno = AacsBdExtentResolver.ResolveStreamFileExtents(inputOptical, + _plugins, + out List ranges, + out string err); + + Func allowDecryptLba = null; + + if(errno == ErrorNumber.NoError) + allowDecryptLba = lba => AacsBdExtentResolver.IsLbaAllowed(ranges, lba); + else if(_force) + ErrorMessage?.Invoke(err + " Continuing with broad Blu-ray AACS decrypt scope."); + else + { + StoppingErrorMessage?.Invoke(err); + + return errno; + } + + return AacsBdOpticalPipeline.Run(inputOptical, + outputOptical, + _count, + _force, + ref _aborted, + _aacsDecryptedCpsUnitKeys, + allowDecryptLba, + new AacsBdOpticalPipeline.Ui + { + OnInitProgress = () => InitProgress?.Invoke(), + OnInitProgress2 = () => InitProgress2?.Invoke(), + OnEndProgress = () => EndProgress?.Invoke(), + OnEndProgress2 = () => EndProgress2?.Invoke(), + OnTrackProgress = (i, n) => UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_in_track_0_of_1, i + 1, n), i, n), + OnSectorRangeProgress = (start, end, seq, done, tot) => + UpdateProgress2?.Invoke(string.Format(UI.Converting_sectors_0_to_1_in_track_2, + start, + end, + seq), + done, + tot), + ErrorMessage = msg => ErrorMessage?.Invoke(msg), + StoppingErrorMessage = msg => StoppingErrorMessage?.Invoke(msg) + }); + } +}