[convert] Add AACS pipeline

This commit is contained in:
Rebecca Wallander
2026-04-11 15:59:41 +02:00
parent a9438b6611
commit b4e3b04e1d
4 changed files with 1000 additions and 0 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)
});
}
}