Dump NGCW

This commit is contained in:
Rebecca Wallander
2026-03-29 12:10:53 +02:00
parent 2414a8a780
commit 0dfef8ccb3
12 changed files with 555 additions and 260 deletions

View File

@@ -104,6 +104,7 @@ public partial class Dump
readonly Stopwatch _speedStopwatch;
readonly bool _stopOnError;
readonly bool _storeEncrypted;
readonly bool _bypassWiiDecryption;
readonly DumpSubchannel _subchannel;
readonly bool _titleKeys;
readonly bool _trim;
@@ -171,6 +172,7 @@ public partial class Dump
/// <param name="dimensions">Dimensions of graph in pixels for a square</param>
/// <param name="paranoia">Check sectors integrity before writing to image</param>
/// <param name="cureParanoia">Try to fix sectors integrity</param>
/// <param name="bypassWiiDecryption">When dumping Wii (WOD), skip partition AES decryption and store encrypted data</param>
public Dump(bool doResume, Device dev, string devicePath, IBaseWritableImage outputPlugin, ushort retryPasses,
bool force, bool dumpRaw, bool persistent, bool stopOnError, Resume resume, Encoding encoding,
string outputPrefix, string outputPath, Dictionary<string, string> formatOptions, Metadata preSidecar,
@@ -179,7 +181,7 @@ public partial class Dump
bool fixSubchannel, bool fixSubchannelCrc, bool skipCdireadyHole, ErrorLog errorLog,
bool generateSubchannels, uint maximumReadable, bool useBufferedReads, bool storeEncrypted,
bool titleKeys, uint ignoreCdrRunOuts, bool createGraph, uint dimensions, bool paranoia,
bool cureParanoia)
bool cureParanoia, bool bypassWiiDecryption)
{
_doResume = doResume;
_dev = dev;
@@ -223,6 +225,7 @@ public partial class Dump
_dimensions = dimensions;
_paranoia = paranoia;
_cureParanoia = cureParanoia;
_bypassWiiDecryption = bypassWiiDecryption;
_dumpStopwatch = new Stopwatch();
_sidecarStopwatch = new Stopwatch();
_speedStopwatch = new Stopwatch();

View File

@@ -58,21 +58,17 @@ partial class Dump
ulong[] _ngcwPartSysEnd;
DataRegion[] _ngcwGcDataMap;
ulong _ngcwGcSysEnd;
bool _omniDriveNintendoSoftwareDescramble;
byte? _nintendoDerivedDiscKey;
readonly Aaru.Decoders.Nintendo.Sector _nintendoSectorDecoder = new Aaru.Decoders.Nintendo.Sector();
bool InitializeNgcwContext(MediaType dskType, Reader scsiReader, IWritableImage outputFormat)
{
_ngcwEnabled = dskType is MediaType.GOD or MediaType.WOD;
_ngcwMediaType = dskType;
_ngcwJunkCollector = new JunkCollector();
_omniDriveNintendoSoftwareDescramble = scsiReader.OmniDriveNintendoMode;
if(!_ngcwEnabled) return true;
if(_omniDriveNintendoSoftwareDescramble)
if(scsiReader.OmniDriveNintendoMode)
{
UpdateStatus?.Invoke(UI.Ngcw_nintendo_software_descramble);
@@ -108,10 +104,6 @@ partial class Dump
return true;
}
if(_omniDriveNintendoSoftwareDescramble &&
!DescrambleNintendoLongBuffer(longBuffer, startSector, sectors))
return false;
if(_ngcwMediaType == MediaType.GOD)
return TransformGameCubeLongSectors(longBuffer, startSector, sectors, statuses);
@@ -155,6 +147,15 @@ partial class Dump
return false;
}
UpdateStatus?.Invoke(string.Format(UI.Ngcw_found_0_partitions, _ngcwPartitions.Count));
if(_bypassWiiDecryption)
{
UpdateStatus?.Invoke(UI.Ngcw_wii_dump_bypass_decryption);
return true;
}
WiiPartitionRegion[] regions = NgcwPartitions.BuildRegionMap(_ngcwPartitions);
byte[] keyMapData = NgcwPartitions.SerializeKeyMap(regions);
@@ -253,6 +254,13 @@ partial class Dump
ulong discOffset = startSector * NGCW_SECTOR_SIZE;
int partIndex = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, discOffset);
if(_bypassWiiDecryption && partIndex >= 0)
{
for(int i = 0; i < sectors; i++) statuses[i] = SectorStatus.Encrypted;
return true;
}
if(partIndex < 0)
{
byte[] payload = new byte[sectors * NGCW_SECTOR_SIZE];
@@ -443,7 +451,9 @@ partial class Dump
bool EnsureNintendoDerivedKeyFromLba0(Reader scsiReader)
{
if(!_omniDriveNintendoSoftwareDescramble || _nintendoDerivedDiscKey.HasValue) return true;
if(_nintendoDerivedDiscKey.HasValue) return true;
if(!scsiReader.OmniDriveNintendoMode) return true;
bool sense = scsiReader.ReadBlock(out byte[] raw, 0, out _, out _, out _);
@@ -454,63 +464,15 @@ partial class Dump
return false;
}
ErrorNumber errno = _nintendoSectorDecoder.Scramble(raw, 0, out byte[] descrambled);
if(errno != ErrorNumber.NoError || descrambled == null)
{
StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium);
return false;
}
byte[] cprMai8 = new byte[8];
Array.Copy(descrambled, 6, cprMai8, 0, 8);
_nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8);
byte[] keyMaterial = new byte[8];
Array.Copy(raw, Sector.NintendoMainDataOffset, keyMaterial, 0, 8);
_nintendoDerivedDiscKey = Sector.DeriveNintendoKey(keyMaterial);
scsiReader.NintendoDerivedDiscKey = _nintendoDerivedDiscKey;
UpdateStatus?.Invoke(string.Format(UI.Ngcw_nintendo_derived_key_0, _nintendoDerivedDiscKey.Value));
return true;
}
bool DescrambleNintendoLongBuffer(byte[] longBuffer, ulong startSector, uint sectors)
{
if(!_omniDriveNintendoSoftwareDescramble) return true;
for(uint i = 0; i < sectors; i++)
{
if(!DescrambleNintendo2064At(longBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i))
return false;
}
return true;
}
bool DescrambleNintendo2064At(byte[] buffer, int offset, ulong lba)
{
byte[] one = new byte[NGCW_LONG_SECTOR_SIZE];
Array.Copy(buffer, offset, one, 0, NGCW_LONG_SECTOR_SIZE);
byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedDiscKey ?? (byte)0);
ErrorNumber error = _nintendoSectorDecoder.Scramble(one, key, out byte[] decoded);
if(error != ErrorNumber.NoError)
{
Array.Clear(buffer, offset, NGCW_LONG_SECTOR_SIZE);
return false;
}
if(decoded != null) Array.Copy(decoded, 0, buffer, offset, NGCW_LONG_SECTOR_SIZE);
if(lba == 0 && decoded != null)
{
byte[] cprMai8 = new byte[8];
Array.Copy(decoded, 6, cprMai8, 0, 8);
_nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8);
}
return true;
}
List<WiiPartition> ParseWiiPartitionsFromDevice(Reader scsiReader)
{
byte[] partitionTable = ReadDiscBytesFromDevice(scsiReader, 0x40000, 32);
@@ -585,12 +547,6 @@ partial class Dump
if(sense || _dev.Error || rawSector == null || rawSector.Length < NGCW_PAYLOAD_OFFSET + NGCW_SECTOR_SIZE)
return null;
if(_omniDriveNintendoSoftwareDescramble && rawSector.Length >= NGCW_LONG_SECTOR_SIZE)
{
if(!DescrambleNintendo2064At(rawSector, 0, sector))
return null;
}
Array.Copy(rawSector, NGCW_PAYLOAD_OFFSET + sectorOff, result, read, chunk);
read += chunk;
}
@@ -604,15 +560,6 @@ partial class Dump
if(sense || _dev.Error || rawBuffer == null) return null;
if(_omniDriveNintendoSoftwareDescramble && rawBuffer.Length >= count * NGCW_LONG_SECTOR_SIZE)
{
for(uint i = 0; i < count; i++)
{
if(!DescrambleNintendo2064At(rawBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i))
return null;
}
}
byte[] payload = new byte[count * NGCW_SECTOR_SIZE];
for(uint i = 0; i < count; i++)

View File

@@ -79,6 +79,12 @@ sealed partial class Reader
internal bool CanReadRaw { get; private set; }
/// <summary>When true with OmniDrive raw reads, use descramble=0 and software Nintendo descrambling (GameCube/Wii).</summary>
internal bool OmniDriveNintendoMode { get; set; }
/// <summary>
/// Disc-wide Nintendo XOR key (015) from LBA 0 CPR_MAI, set after the dump pipeline reads LBA 0. Used for
/// OmniDrive Nintendo reads at LBAs ≥ 16; null until derived.
/// </summary>
internal byte? NintendoDerivedDiscKey { get; set; }
internal bool CanSeek => _ataSeek || _seek6 || _seek10;
internal bool CanSeekLba => _ataSeekLba || _seek6 || _seek10;

View File

@@ -591,8 +591,8 @@ sealed partial class Reader
// Try OmniDrive on drives with OmniDrive firmware (standard descramble=1 and Nintendo descramble=0)
if(_dev.IsOmniDriveFirmware())
{
bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _);
bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _);
bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _, true, true);
bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _, true, true);
OmniDriveReadRaw = omniStandardOk || omniNintendoOk;
}
@@ -856,7 +856,10 @@ sealed partial class Reader
lba,
count,
_timeout,
out duration);
out duration,
false,
true,
NintendoDerivedDiscKey);
else
sense = _dev.OmniDriveReadRawDvd(out buffer,
out senseBuf,

View File

@@ -271,6 +271,216 @@ public partial class Convert
return ErrorNumber.NoError;
}
/// <summary>Fill a 32 KiB Wii encrypted group from 2048-byte logical ReadSector slices.</summary>
void FillWiiEncryptedGroupFromShortSectors(ulong firstLogicalSector, byte[] encGrp)
{
const int sectorSize = Crypto.SECTOR_SIZE;
const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP;
for(int s = 0; s < sectorsPerBlock; s++)
{
ulong sec = firstLogicalSector + (ulong)s;
ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _);
if(errno != ErrorNumber.NoError || sd == null)
Array.Clear(encGrp, s * sectorSize, sectorSize);
else
Array.Copy(sd, 0, encGrp, s * sectorSize, sectorSize);
}
}
/// <summary>
/// Read one encrypted Wii group from long sectors: copy main_data into <paramref name="encGrp" /> and
/// keep full long buffers for output (one ReadSectorLong per logical sector).
/// </summary>
void ReadWiiEncryptedGroupFromLongSectors(ulong firstLogicalSector, byte[] encGrp, byte[][] longSectorBuffers,
int longSectorSize)
{
const int sectorSize = Crypto.SECTOR_SIZE;
const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP;
for(int s = 0; s < sectorsPerBlock; s++)
{
ulong sec = firstLogicalSector + (ulong)s;
ErrorNumber errno = _inputImage.ReadSectorLong(sec, false, out byte[] sd, out _);
byte[] stored = longSectorBuffers[s];
if(stored == null || stored.Length != longSectorSize)
{
stored = new byte[longSectorSize];
longSectorBuffers[s] = stored;
}
if(errno != ErrorNumber.NoError || sd == null)
{
Array.Clear(encGrp, s * sectorSize, sectorSize);
Array.Clear(stored, 0, longSectorSize);
continue;
}
int copyLong = sd.Length < longSectorSize ? sd.Length : longSectorSize;
Array.Copy(sd, 0, stored, 0, copyLong);
if(copyLong < longSectorSize)
Array.Clear(stored, copyLong, longSectorSize - copyLong);
if(sd.Length >= Sector.NintendoMainDataOffset + sectorSize)
Array.Copy(sd, Sector.NintendoMainDataOffset, encGrp, s * sectorSize, sectorSize);
else
Array.Clear(encGrp, s * sectorSize, sectorSize);
}
}
/// <summary>
/// Classify FST regions, run LFG junk detection on decrypted user data, and build the 32 KiB
/// plaintext group (hash + user, junk zeroed).
/// </summary>
byte[] ProcessWiiDecryptedGroup(int inPart, ulong groupDiscOff, byte[] hashBlock, byte[] groupData,
JunkCollector jc, ref ulong dataSectors, ref ulong junkSectors)
{
const int groupSize = Crypto.GROUP_SIZE;
const int hashSize = Crypto.GROUP_HASH_SIZE;
const int groupDataSize = Crypto.GROUP_DATA_SIZE;
const int sectorSize = Crypto.SECTOR_SIZE;
ulong groupNum = (groupDiscOff - _ngcwPartitions[inPart].DataOffset) / groupSize;
ulong logicalOffset = groupNum * groupDataSize;
bool[] sectorIsData = new bool[16];
int udCount = 0;
for(ulong off = 0; off < groupDataSize; off += sectorSize)
{
ulong chunk = groupDataSize - off;
if(chunk > sectorSize) chunk = sectorSize;
if(logicalOffset + off < _ngcwPartSysEnd[inPart])
sectorIsData[udCount] = true;
else if(_ngcwPartDataMaps[inPart] != null)
{
sectorIsData[udCount] = DataMap.IsDataRegion(_ngcwPartDataMaps[inPart],
logicalOffset + off,
chunk);
}
else
sectorIsData[udCount] = true;
udCount++;
}
ulong blockPhase = logicalOffset % groupSize;
ulong block2Start = blockPhase > 0 ? groupSize - blockPhase : groupDataSize;
if(block2Start > groupDataSize) block2Start = groupDataSize;
bool haveSeed1 = false;
uint[] seed1 = new uint[Lfg.SEED_SIZE];
bool haveSeed2 = false;
uint[] seed2 = new uint[Lfg.SEED_SIZE];
for(int s = 0; s < udCount; s++)
{
if(sectorIsData[s]) continue;
ulong soff = (ulong)s * sectorSize;
bool inBlock2 = soff >= block2Start;
if(inBlock2 && haveSeed2) continue;
if(!inBlock2 && haveSeed1) continue;
int avail = (int)(groupDataSize - soff);
int doff = (int)((logicalOffset + soff) % groupSize);
if(avail < Lfg.MIN_SEED_DATA_BYTES) continue;
uint[] dst = inBlock2 ? seed2 : seed1;
int m = Lfg.GetSeed(groupData.AsSpan((int)soff, avail), doff, dst);
if(m > 0)
{
if(inBlock2)
haveSeed2 = true;
else
haveSeed1 = true;
}
if(haveSeed1 && haveSeed2) break;
}
byte[] decryptedGroup = new byte[groupSize];
Array.Copy(hashBlock, 0, decryptedGroup, 0, hashSize);
for(int s = 0; s < udCount; s++)
{
ulong off = (ulong)s * sectorSize;
int chunk = groupDataSize - (int)off;
int outOff = hashSize + (int)off;
if(chunk > sectorSize) chunk = sectorSize;
if(sectorIsData[s])
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
dataSectors++;
continue;
}
bool inBlock2 = off >= block2Start;
bool haveSeed = inBlock2 ? haveSeed2 : haveSeed1;
uint[] theSeed = inBlock2 ? seed2 : seed1;
if(!haveSeed)
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
dataSectors++;
continue;
}
uint[] lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)];
uint[] seedCopy = new uint[Lfg.SEED_SIZE];
Array.Copy(theSeed, seedCopy, Lfg.SEED_SIZE);
Lfg.SetSeed(lfgBuffer, seedCopy);
int positionBytes = 0;
int adv = (int)((logicalOffset + off) % groupSize);
if(adv > 0)
{
byte[] discard = new byte[4096];
int rem = adv;
while(rem > 0)
{
int step = rem > discard.Length ? discard.Length : rem;
Lfg.GetBytes(lfgBuffer, ref positionBytes, discard, 0, step);
rem -= step;
}
}
byte[] expected = new byte[sectorSize];
Lfg.GetBytes(lfgBuffer, ref positionBytes, expected, 0, chunk);
if(groupData.AsSpan((int)off, chunk).SequenceEqual(expected.AsSpan(0, chunk)))
{
Array.Clear(decryptedGroup, outOff, chunk);
jc.Add(groupDiscOff + hashSize + off, (ulong)chunk, (ushort)inPart, theSeed);
junkSectors++;
}
else
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
dataSectors++;
}
}
return decryptedGroup;
}
/// <summary>Wii sector conversion pipeline.</summary>
ErrorNumber ConvertWiiSectors(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, ref ulong dataSectors,
ref ulong junkSectors)
@@ -307,162 +517,16 @@ public partial class Convert
ulong groupDiscOff = _ngcwPartitions[inPart].DataOffset +
(offset - _ngcwPartitions[inPart].DataOffset) / groupSize * groupSize;
// Read encrypted group
var encGrp = new byte[groupSize];
FillWiiEncryptedGroupFromShortSectors(groupDiscOff / sectorSize, encGrp);
for(var s = 0; s < sectorsPerBlock; s++)
{
ulong sec = groupDiscOff / sectorSize + (ulong)s;
ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _);
if(errno != ErrorNumber.NoError || sd == null)
Array.Clear(encGrp, s * sectorSize, sectorSize);
else
Array.Copy(sd, 0, encGrp, s * sectorSize, sectorSize);
}
// Decrypt
var hashBlock = new byte[hashSize];
var groupData = new byte[groupDataSize];
Crypto.DecryptGroup(_ngcwPartitions[inPart].TitleKey, encGrp, hashBlock, groupData);
// Classify user data sectors
ulong groupNum = (groupDiscOff - _ngcwPartitions[inPart].DataOffset) / groupSize;
ulong logicalOffset = groupNum * groupDataSize;
var sectorIsData = new bool[16];
var udCount = 0;
for(ulong off = 0; off < groupDataSize; off += sectorSize)
{
ulong chunk = groupDataSize - off;
if(chunk > sectorSize) chunk = sectorSize;
if(logicalOffset + off < _ngcwPartSysEnd[inPart])
sectorIsData[udCount] = true;
else if(_ngcwPartDataMaps[inPart] != null)
{
sectorIsData[udCount] = DataMap.IsDataRegion(_ngcwPartDataMaps[inPart],
logicalOffset + off,
chunk);
}
else
sectorIsData[udCount] = true;
udCount++;
}
// Extract LFG seeds (up to 2 per group for block boundaries)
ulong blockPhase = logicalOffset % groupSize;
ulong block2Start = blockPhase > 0 ? groupSize - blockPhase : groupDataSize;
if(block2Start > groupDataSize) block2Start = groupDataSize;
var haveSeed1 = false;
var seed1 = new uint[Lfg.SEED_SIZE];
var haveSeed2 = false;
var seed2 = new uint[Lfg.SEED_SIZE];
for(var s = 0; s < udCount; s++)
{
if(sectorIsData[s]) continue;
ulong soff = (ulong)s * sectorSize;
bool inBlock2 = soff >= block2Start;
if(inBlock2 && haveSeed2) continue;
if(!inBlock2 && haveSeed1) continue;
var avail = (int)(groupDataSize - soff);
var doff = (int)((logicalOffset + soff) % groupSize);
if(avail < Lfg.MIN_SEED_DATA_BYTES) continue;
uint[] dst = inBlock2 ? seed2 : seed1;
int m = Lfg.GetSeed(groupData.AsSpan((int)soff, avail), doff, dst);
if(m > 0)
{
if(inBlock2)
haveSeed2 = true;
else
haveSeed1 = true;
}
if(haveSeed1 && haveSeed2) break;
}
// Build decrypted group: hash_block + processed user_data
var decryptedGroup = new byte[groupSize];
Array.Copy(hashBlock, 0, decryptedGroup, 0, hashSize);
for(var s = 0; s < udCount; s++)
{
ulong off = (ulong)s * sectorSize;
int chunk = groupDataSize - (int)off;
int outOff = hashSize + (int)off;
if(chunk > sectorSize) chunk = sectorSize;
if(sectorIsData[s])
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
dataSectors++;
continue;
}
bool inBlock2 = off >= block2Start;
bool haveSeed = inBlock2 ? haveSeed2 : haveSeed1;
uint[] theSeed = inBlock2 ? seed2 : seed1;
if(!haveSeed)
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
dataSectors++;
continue;
}
// Verify sector against LFG
var lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)];
var seedCopy = new uint[Lfg.SEED_SIZE];
Array.Copy(theSeed, seedCopy, Lfg.SEED_SIZE);
Lfg.SetSeed(lfgBuffer, seedCopy);
var positionBytes = 0;
var adv = (int)((logicalOffset + off) % groupSize);
if(adv > 0)
{
var discard = new byte[4096];
int rem = adv;
while(rem > 0)
{
int step = rem > discard.Length ? discard.Length : rem;
Lfg.GetBytes(lfgBuffer, ref positionBytes, discard, 0, step);
rem -= step;
}
}
var expected = new byte[sectorSize];
Lfg.GetBytes(lfgBuffer, ref positionBytes, expected, 0, chunk);
if(groupData.AsSpan((int)off, chunk).SequenceEqual(expected.AsSpan(0, chunk)))
{
// Junk — zero it out, record in junk map
Array.Clear(decryptedGroup, outOff, chunk);
jc.Add(groupDiscOff + hashSize + off, (ulong)chunk, (ushort)inPart, theSeed);
junkSectors++;
}
else
{
Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk);
dataSectors++;
}
}
byte[] decryptedGroup =
ProcessWiiDecryptedGroup(inPart, groupDiscOff, hashBlock, groupData, jc, ref dataSectors,
ref junkSectors);
// Write all 16 sectors as SectorStatusUnencrypted
for(var s = 0; s < sectorsPerBlock; s++)
@@ -879,9 +943,194 @@ public partial class Convert
return ErrorNumber.NoError;
}
/// Wii sector conversion pipeline for long sectors.
ErrorNumber ConvertWiiSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, ref ulong dataSectors, ref ulong junkSectors){
// TODO: Implement
/// <summary>Wii sector conversion pipeline for long sectors (main_data + framing).</summary>
ErrorNumber ConvertWiiSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc,
ref ulong dataSectors, ref ulong junkSectors)
{
const int groupSize = Crypto.GROUP_SIZE;
const int hashSize = Crypto.GROUP_HASH_SIZE;
const int groupDataSize = Crypto.GROUP_DATA_SIZE;
const int sectorSize = Crypto.SECTOR_SIZE;
const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP;
ErrorNumber probeErr = _inputImage.ReadSectorLong(0, false, out byte[] longProbe, out _);
if(probeErr != ErrorNumber.NoError || longProbe == null ||
longProbe.Length < Sector.NintendoMainDataOffset + sectorSize)
return ErrorNumber.InOutError;
int longSectorSize = longProbe.Length;
BuildWiiPartitionFstMaps();
var longSectorBufs = new byte[sectorsPerBlock][];
ulong offset = 0;
while(offset < discSize)
{
if(_aborted) break;
ulong baseSector = offset / sectorSize;
UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_0_to_1,
baseSector,
baseSector + sectorsPerBlock),
(long)baseSector,
(long)totalLogicalSectors);
int inPart = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, offset);
if(inPart >= 0)
{
ulong groupDiscOff = _ngcwPartitions[inPart].DataOffset +
(offset - _ngcwPartitions[inPart].DataOffset) / groupSize * groupSize;
var encGrp = new byte[groupSize];
ReadWiiEncryptedGroupFromLongSectors(groupDiscOff / sectorSize, encGrp, longSectorBufs, longSectorSize);
var hashBlock = new byte[hashSize];
var groupData = new byte[groupDataSize];
Crypto.DecryptGroup(_ngcwPartitions[inPart].TitleKey, encGrp, hashBlock, groupData);
byte[] decryptedGroup =
ProcessWiiDecryptedGroup(inPart, groupDiscOff, hashBlock, groupData, jc, ref dataSectors,
ref junkSectors);
for(int s = 0; s < sectorsPerBlock; s++)
{
int dataOff = s * sectorSize;
int outOff = hashSize + dataOff;
if(dataOff >= groupDataSize)
{
Array.Clear(longSectorBufs[s], Sector.NintendoMainDataOffset, sectorSize);
continue;
}
int copyLen = groupDataSize - dataOff;
if(copyLen > sectorSize)
copyLen = sectorSize;
Array.Copy(decryptedGroup, outOff, longSectorBufs[s], Sector.NintendoMainDataOffset, copyLen);
if(copyLen < sectorSize)
Array.Clear(longSectorBufs[s], Sector.NintendoMainDataOffset + copyLen, sectorSize - copyLen);
}
for(int s = 0; s < sectorsPerBlock; s++)
{
ulong sector = groupDiscOff / sectorSize + (ulong)s;
bool ok = _outputImage.WriteSectorLong(longSectorBufs[s],
sector,
false,
SectorStatus.Unencrypted);
if(!ok)
{
if(_force)
{
ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing,
_outputImage.ErrorMessage,
sector));
}
else
{
StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
_outputImage.ErrorMessage,
sector));
return ErrorNumber.WriteError;
}
}
}
offset = groupDiscOff + groupSize;
}
else
{
const int blockSize = groupSize;
ulong alignedOff = offset & ~(ulong)(blockSize - 1);
int blockBytes = blockSize;
if(alignedOff + (ulong)blockBytes > discSize) blockBytes = (int)(discSize - alignedOff);
var blockBuf = new byte[blockSize];
for(int s = 0; s < sectorsPerBlock && s * sectorSize < blockBytes; s++)
{
ulong sec = alignedOff / sectorSize + (ulong)s;
ErrorNumber errno = _inputImage.ReadSectorLong(sec, false, out byte[] sd, out _);
byte[] stored = new byte[longSectorSize];
longSectorBufs[s] = stored;
if(errno != ErrorNumber.NoError || sd == null)
{
Array.Clear(blockBuf, s * sectorSize, sectorSize);
Array.Clear(stored, 0, longSectorSize);
continue;
}
int copyLong = sd.Length < longSectorSize ? sd.Length : longSectorSize;
Array.Copy(sd, 0, stored, 0, copyLong);
if(copyLong < longSectorSize)
Array.Clear(stored, copyLong, longSectorSize - copyLong);
if(sd.Length >= Sector.NintendoMainDataOffset + sectorSize)
Array.Copy(sd, Sector.NintendoMainDataOffset, blockBuf, s * sectorSize, sectorSize);
else
Array.Clear(blockBuf, s * sectorSize, sectorSize);
}
var sectorStatuses = new SectorStatus[sectorsPerBlock];
Junk.DetectJunkInBlock(blockBuf,
blockBytes,
alignedOff,
null,
0x50000,
0xFFFF,
jc,
ref dataSectors,
ref junkSectors,
sectorStatuses);
int numSectors = blockBytes / sectorSize;
for(int si = 0; si < numSectors; si++)
{
ulong sector = alignedOff / sectorSize + (ulong)si;
bool ok = _outputImage.WriteSectorLong(longSectorBufs[si], sector, false, sectorStatuses[si]);
if(!ok)
{
if(_force)
{
ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing,
_outputImage.ErrorMessage,
sector));
}
else
{
StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
_outputImage.ErrorMessage,
sector));
return ErrorNumber.WriteError;
}
}
}
offset = alignedOff + (ulong)blockBytes;
}
}
return ErrorNumber.NoError;
}

View File

@@ -301,6 +301,21 @@ public sealed class Sector
return computed == stored;
}
/// <summary>
/// Check if the IED of sectors is correct
/// </summary>
/// <param name="sectorLong">Buffer of the sector</param>
/// <param name="transferLength">The number of sectors to check</param>
/// <returns><c>True</c> if IED is correct, <c>False</c> if not</returns>
public static bool CheckIed(byte[] sectorLong, uint transferLength)
{
for(uint i = 0; i < transferLength; i++)
{
if(!CheckIed(sectorLong.Skip((int)(i * 2064)).Take(2064).ToArray())) return false;
}
return true;
}
/// <summary>
/// Tests if a seed unscrambles a sector correctly
/// </summary>

View File

@@ -33,14 +33,17 @@
// ****************************************************************************/
using System;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Structs.Devices.SCSI;
using Aaru.Logging;
using Aaru.Decoders.DVD;
using NintendoSector = Aaru.Decoders.Nintendo.Sector;
namespace Aaru.Devices;
public partial class Device
{
readonly NintendoSector _nintendoSectorDecoder = new NintendoSector();
enum OmniDriveDiscType
{
CD = 0,
@@ -89,20 +92,20 @@ public partial class Device
{
bool sense = ScsiInquiry(out byte[] buffer, out _, Timeout, out _);
if(sense || buffer == null) return false;
if (sense || buffer == null) return false;
Inquiry? inquiry = Inquiry.Decode(buffer);
if(!inquiry.HasValue || inquiry.Value.Reserved5 == null || inquiry.Value.Reserved5.Length < 11)
if (!inquiry.HasValue || inquiry.Value.Reserved5 == null || inquiry.Value.Reserved5.Length < 11)
return false;
byte[] reserved5 = inquiry.Value.Reserved5;
byte[] omnidrive = [0x4F, 0x6D, 0x6E, 0x69, 0x44, 0x72, 0x69, 0x76, 0x65]; // "OmniDrive"
if(reserved5.Length < omnidrive.Length) return false;
if (reserved5.Length < omnidrive.Length) return false;
for(int i = 0; i < omnidrive.Length; i++)
if(reserved5[i] != omnidrive[i])
for (int i = 0; i < omnidrive.Length; i++)
if (reserved5[i] != omnidrive[i])
return false;
return true;
@@ -132,7 +135,10 @@ public partial class Device
LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense);
if(!Sector.CheckEdc(buffer, transferLength)) return true;
if (!Sector.CheckIed(buffer, transferLength)) return true;
if(descramble)
if (!Sector.CheckEdc(buffer, transferLength)) return true;
Error = LastError != 0;
@@ -142,13 +148,16 @@ public partial class Device
}
/// <summary>
/// Reads raw Nintendo GameCube/Wii DVD sectors (2064 bytes) on OmniDrive. Default matches redumper raw DVD
/// (<c>descramble</c> off); use software descramble via Aaru.Decoders.Nintendo.Sector when needed.
/// Reads Nintendo GameCube/Wii DVD sectors (2064 bytes) on OmniDrive. The drive returns DVD-layer data with
/// drive-side DVD descramble off; this method applies per-sector Nintendo XOR descramble and returns the result.
/// LBAs 015 use key 0; LBAs ≥ 16 use <paramref name="derivedDiscKey" /> (from LBA 0 CPR_MAI), or 0 if not yet
/// derived.
/// </summary>
/// <param name="descramble">Drive-side DVD descramble (redumper raw DVD uses <c>false</c>).</param>
/// <param name="derivedDiscKey">Disc key from LBA 0 (015); <c>null</c> until the host has read and derived it.</param>
/// <returns><c>true</c> if the command failed and <paramref name="senseBuffer" /> contains the sense buffer.</returns>
public bool OmniDriveReadNintendoDvd(out byte[] buffer, out ReadOnlySpan<byte> senseBuffer, uint lba, uint transferLength,
uint timeout, out double duration, bool descramble = false)
uint timeout, out double duration, bool fua = false, bool descramble = true,
byte? derivedDiscKey = null)
{
senseBuffer = SenseBuffer;
Span<byte> cdb = CdbBuffer[..12];
@@ -157,11 +166,55 @@ public partial class Device
FillOmniDriveReadDvdCdb(cdb,
lba,
transferLength,
EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, false, descramble));
EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, fua, false));
LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense);
// Scrambled Nintendo sectors do not pass standard DVD EDC until software-descrambled.
if(!Sector.CheckIed(buffer, transferLength)) return true;
if(descramble) {
const int sectorBytes = 2064;
byte[] outBuf = new byte[sectorBytes * transferLength];
if(lba < 16 && lba + transferLength > 16) {
for(uint i = 0; i < transferLength; i++)
{
var slice = new byte[sectorBytes];
Array.Copy(buffer, i * sectorBytes, slice, 0, sectorBytes);
uint absLba = lba + i;
byte key = absLba < 16 ? (byte)0 : (derivedDiscKey ?? (byte)0);
ErrorNumber errno = _nintendoSectorDecoder.Scramble(slice, key, out byte[] descrambled);
if(errno != ErrorNumber.NoError || descrambled == null)
{
LastError = (int)errno;
Error = true;
return true;
}
Array.Copy(descrambled, 0, outBuf, i * sectorBytes, sectorBytes);
}
} else {
ErrorNumber errno = _nintendoSectorDecoder.Scramble(
buffer,
transferLength,
lba < 16 || lba > 0xffffff ? (byte)0 : (derivedDiscKey ?? (byte)0),
out outBuf);
if(errno != ErrorNumber.NoError || outBuf == null)
{
LastError = (int)errno;
Error = true;
return true;
}
}
buffer = outBuf;
}
Error = LastError != 0;
AaruLogging.Debug(SCSI_MODULE_NAME, "OmniDrive READ NINTENDO DVD took {0} ms", duration);

View File

@@ -753,7 +753,8 @@ public sealed partial class MediaDumpViewModel : ViewModelBase
true,
1080,
Paranoia,
CureParanoia);
CureParanoia,
false);
new Thread(DoWork).Start();
}

View File

@@ -1995,6 +1995,12 @@ namespace Aaru.Localization {
}
}
public static string Ngcw_wii_dump_bypass_decryption {
get {
return ResourceManager.GetString("Ngcw_wii_dump_bypass_decryption", resourceCulture);
}
}
public static string PS3_disc_key_resolved_from_0 {
get {
return ResourceManager.GetString("PS3_disc_key_resolved_from_0", resourceCulture);

View File

@@ -669,6 +669,9 @@
</data>
<data name="Ngcw_nintendo_derived_key_0" xml:space="preserve">
<value>[slateblue1]Clave de disco Nintendo derivada: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_wii_dump_bypass_decryption" xml:space="preserve">
<value>[slateblue1]Omisión del descifrado de particiones Wii: guardando datos de partición cifrados tal como se leen.[/]</value>
</data>
<data name="PS3_disc_key_resolved_from_0" xml:space="preserve">
<value>[slateblue1]Clave de disco PS3 resuelta desde [aqua]{0}[/].[/]</value>

View File

@@ -971,7 +971,7 @@ In you are unsure, please press N to not continue.</value>
<value>Skip Wii U disc encryption processing during conversion.</value>
</data>
<data name="Bypass_Wii_decryption_help" xml:space="preserve">
<value>Skip Wii disc encryption processing during conversion.</value>
<value>Skip Wii disc encryption processing during conversion or dump.</value>
</data>
<data name="Ngcw_parsing_partition_table" xml:space="preserve">
<value>[slateblue1]Parsing Wii partition table...[/]</value>
@@ -1008,6 +1008,9 @@ In you are unsure, please press N to not continue.</value>
</data>
<data name="Ngcw_nintendo_derived_key_0" xml:space="preserve">
<value>[slateblue1]Derived Nintendo disc key: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_wii_dump_bypass_decryption" xml:space="preserve">
<value>[slateblue1]Wii partition decryption bypass: storing encrypted partition data as read.[/]</value>
</data>
<data name="PS3_disc_key_resolved_from_0" xml:space="preserve">
<value>[slateblue1]PS3 disc key resolved from [aqua]{0}[/].[/]</value>

View File

@@ -116,6 +116,7 @@ sealed class DumpMediaCommand : Command<DumpMediaCommand.Settings>
AaruLogging.Debug(MODULE_NAME, "--max-blocks={0}", maxBlocks);
AaruLogging.Debug(MODULE_NAME, "--use-buffered-reads={0}", settings.UseBufferedReads);
AaruLogging.Debug(MODULE_NAME, "--store-encrypted={0}", settings.StoreEncrypted);
AaruLogging.Debug(MODULE_NAME, "--bypass-wii-decryption={0}", settings.BypassWiiDecryption);
AaruLogging.Debug(MODULE_NAME, "--title-keys={0}", settings.TitleKeys);
AaruLogging.Debug(MODULE_NAME, "--ignore-cdr-runouts={0}", settings.IgnoreCdrRunOuts);
AaruLogging.Debug(MODULE_NAME, "--create-graph={0}", settings.CreateGraph);
@@ -526,7 +527,8 @@ sealed class DumpMediaCommand : Command<DumpMediaCommand.Settings>
settings.CreateGraph,
(uint)settings.Dimensions,
settings.Paranoia,
settings.CureParanoia);
settings.CureParanoia,
settings.BypassWiiDecryption);
AnsiConsole.Progress()
.AutoClear(true)
@@ -757,6 +759,10 @@ sealed class DumpMediaCommand : Command<DumpMediaCommand.Settings>
[CommandOption("--store-encrypted")]
[DefaultValue(true)]
public bool StoreEncrypted { get; init; }
[LocalizedDescription(nameof(UI.Bypass_Wii_decryption_help))]
[CommandOption("--bypass-wii-decryption")]
[DefaultValue(false)]
public bool BypassWiiDecryption { get; init; }
[LocalizedDescription(nameof(UI.Try_to_read_the_title_keys_from_CSS_DVDs))]
[CommandOption("--title-keys")]
[DefaultValue(true)]