diff --git a/.gitignore b/.gitignore index 3c4efe20..db96b630 100644 --- a/.gitignore +++ b/.gitignore @@ -258,4 +258,5 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc \ No newline at end of file +*.pyc +**/launchSettings.json diff --git a/libhac/CombinationStream.cs b/libhac/CombinationStream.cs new file mode 100644 index 00000000..b998c002 --- /dev/null +++ b/libhac/CombinationStream.cs @@ -0,0 +1,351 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) 2011, The Outercurve Foundation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Prabir Shrestha (prabir.me) +// https://github.com/facebook-csharp-sdk/combination-stream +//----------------------------------------------------------------------- + +/* + * Install-Package CombinationStream + * + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace libhac +{ + internal class CombinationStream : Stream + { + private readonly IList _streams; + private readonly IList _streamsToDispose; + private readonly IList _streamsStartPos; + private int _currentStreamIndex; + private Stream _currentStream; + private long _length = -1; + private long _postion; + + public CombinationStream(IList streams) + : this(streams, null) + { + } + + public CombinationStream(IList streams, IList streamsToDispose) + { + if (streams == null) + throw new ArgumentNullException("streams"); + + _streams = streams; + _streamsToDispose = streamsToDispose; + if (streams.Count > 0) + _currentStream = streams[_currentStreamIndex++]; + + _streamsStartPos = new List(streams.Count); + long pos = 0; + foreach (var strm in streams) + { + _streamsStartPos.Add(pos); + pos += strm.Length; + } + } + + public IList InternalStreams => _streams; + + public override void Flush() => _currentStream?.Flush(); + + public override long Seek(long offset, SeekOrigin origin) + { + long pos = 0; + switch (origin) + { + case SeekOrigin.Begin: + pos = offset; + break; + case SeekOrigin.Current: + pos = Position + offset; + break; + case SeekOrigin.End: + pos = Length + offset; + break; + } + int idx = 0; + while (idx+1 < _streamsStartPos.Count) + { + if (_streamsStartPos[idx + 1] > pos) + { + break; + } + idx++; + } + + _currentStreamIndex = idx; + _currentStream = _streams[_currentStreamIndex]; + _currentStream.Seek(pos - _streamsStartPos[idx], SeekOrigin.Begin); + _postion = pos; + return _postion; + //throw new InvalidOperationException("Stream is not seekable."); + } + + public override void SetLength(long value) + { + _length = value; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int result = 0; + int buffPostion = offset; + + while (count > 0) + { + int bytesRead = _currentStream.Read(buffer, buffPostion, count); + result += bytesRead; + buffPostion += bytesRead; + _postion += bytesRead; + + if (bytesRead <= count) + count -= bytesRead; + + if (count > 0) + { + if (_currentStreamIndex >= _streams.Count) + break; + + _currentStream = _streams[_currentStreamIndex++]; + } + } + + return result; + } + +#if NETFX_CORE + + public async override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int result = 0; + int buffPostion = offset; + + while (count > 0) + { + int bytesRead = await _currentStream.ReadAsync(buffer, buffPostion, count, cancellationToken); + result += bytesRead; + buffPostion += bytesRead; + _postion += bytesRead; + + if (bytesRead <= count) + count -= bytesRead; + + if (count > 0) + { + if (_currentStreamIndex >= _streams.Count) + break; + + _currentStream = _streams[_currentStreamIndex++]; + } + } + + return result; + } + + public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new InvalidOperationException("Stream is not writable"); + } + +#else + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + CombinationStreamAsyncResult asyncResult = new CombinationStreamAsyncResult(state); + if (count > 0) + { + int buffPostion = offset; + + AsyncCallback rc = null; + rc = readresult => + { + try + { + int bytesRead = _currentStream.EndRead(readresult); + asyncResult.BytesRead += bytesRead; + buffPostion += bytesRead; + _postion += bytesRead; + + if (bytesRead <= count) + count -= bytesRead; + + if (count > 0) + { + if (_currentStreamIndex >= _streams.Count) + { + // done + asyncResult.CompletedSynchronously = false; + asyncResult.SetAsyncWaitHandle(); + asyncResult.IsCompleted = true; + callback(asyncResult); + } + else + { + _currentStream = _streams[_currentStreamIndex++]; + _currentStream.BeginRead(buffer, buffPostion, count, rc, readresult.AsyncState); + } + } + else + { + // done + asyncResult.CompletedSynchronously = false; + asyncResult.SetAsyncWaitHandle(); + asyncResult.IsCompleted = true; + callback(asyncResult); + } + } + catch (Exception ex) + { + // done + asyncResult.Exception = ex; + asyncResult.CompletedSynchronously = false; + asyncResult.SetAsyncWaitHandle(); + asyncResult.IsCompleted = true; + callback(asyncResult); + } + }; + _currentStream.BeginRead(buffer, buffPostion, count, rc, state); + } + else + { + // done + asyncResult.CompletedSynchronously = true; + asyncResult.SetAsyncWaitHandle(); + asyncResult.IsCompleted = true; + callback(asyncResult); + } + + return asyncResult; + } + + public override int EndRead(IAsyncResult asyncResult) + { + // todo: check if it is of same reference + asyncResult.AsyncWaitHandle.WaitOne(); + var ar = (CombinationStreamAsyncResult)asyncResult; + if (ar.Exception != null) + { + throw ar.Exception; + } + + return ar.BytesRead; + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new InvalidOperationException("Stream is not writable"); + } + + internal class CombinationStreamAsyncResult : IAsyncResult + { + private readonly object _asyncState; + + public CombinationStreamAsyncResult(object asyncState) + { + _asyncState = asyncState; + _manualResetEvent = new ManualResetEvent(false); + } + + public bool IsCompleted { get; internal set; } + + public WaitHandle AsyncWaitHandle + { + get { return _manualResetEvent; } + } + + public object AsyncState + { + get { return _asyncState; } + } + + public bool CompletedSynchronously { get; internal set; } + + public Exception Exception { get; internal set; } + + internal void SetAsyncWaitHandle() + { + _manualResetEvent.Set(); + } + + private readonly ManualResetEvent _manualResetEvent; + public int BytesRead; + } + +#endif + + public override void Write(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException("Stream is not writable"); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (_streamsToDispose == null) + { + foreach (var stream in InternalStreams) + stream.Dispose(); + } + else + { + int i; + for (i = 0; i < InternalStreams.Count; i++) + InternalStreams[i].Dispose(); + } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get + { + if (_length == -1) + { + _length = 0; + foreach (var stream in _streams) + _length += stream.Length; + } + + return _length; + } + } + + public override long Position + { + get => _postion; + set => Seek(value, SeekOrigin.Begin); + } + } +} \ No newline at end of file diff --git a/libhac/Crypto.cs b/libhac/Crypto.cs new file mode 100644 index 00000000..557abd91 --- /dev/null +++ b/libhac/Crypto.cs @@ -0,0 +1,42 @@ +using System.IO; +using System.Security.Cryptography; + +namespace libhac +{ + public class Crypto + { + public static void DecryptEcb(byte[] key, byte[] src, int srcIndex, byte[] dest, int destIndex, int length) + { + using (var aes = Aes.Create()) + { + if(aes == null) throw new CryptographicException("Unable to create AES object"); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + var dec = aes.CreateDecryptor(); + using (var ms = new MemoryStream(dest, destIndex, length)) + using (var cs = new CryptoStream(ms, dec, CryptoStreamMode.Write)) + { + cs.Write(src, srcIndex, length); + cs.FlushFinalBlock(); + } + } + } + + public static void DecryptEcb(byte[] key, byte[] src, byte[] dest, int length) => + DecryptEcb(key, src, 0, dest, 0, length); + + public static void GenerateKek(byte[] dst, byte[] src, byte[] masterKey, byte[] kekSeed, byte[] keySeed) + { + var kek = new byte[0x10]; + var srcKek = new byte[0x10]; + DecryptEcb(masterKey, kekSeed, kek, 0x10); + DecryptEcb(kek, src, srcKek, 0x10); + + if (keySeed != null) + { + DecryptEcb(srcKek, keySeed, dst, 0x10); + } + } + } +} diff --git a/libhac/IProgressReport.cs b/libhac/IProgressReport.cs new file mode 100644 index 00000000..5938ecf7 --- /dev/null +++ b/libhac/IProgressReport.cs @@ -0,0 +1,29 @@ +namespace libhac +{ + public interface IProgressReport + { + /// + /// Sets the current value of the to . + /// + /// The value to set. + void Report(long value); + + /// + /// Adds to the current value of the . + /// + /// The amount to add. + void ReportAdd(long value); + + /// + /// Sets the maximum value of the to . + /// + /// The maximum value to set. + void SetTotal(long value); + + /// + /// Logs a message to the object. + /// + /// The message to output. + void LogMessage(string message); + } +} \ No newline at end of file diff --git a/libhac/Keyset.cs b/libhac/Keyset.cs new file mode 100644 index 00000000..bd34a105 --- /dev/null +++ b/libhac/Keyset.cs @@ -0,0 +1,171 @@ +// ReSharper disable InconsistentNaming + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace libhac +{ + public class Keyset + { + public byte[] secure_boot_key { get; set; } = new byte[0x10]; + public byte[] tsec_key { get; set; } = new byte[0x10]; + public byte[][] keyblob_keys { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] keyblob_mac_keys { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] encrypted_keyblobs { get; set; } = Util.CreateJaggedArray(0x20, 0xB0); + public byte[][] keyblobs { get; set; } = Util.CreateJaggedArray(0x20, 0x90); + public byte[][] keyblob_key_sources { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[] keyblob_mac_key_source { get; set; } = new byte[0x10]; + public byte[] master_key_source { get; set; } = new byte[0x10]; + public byte[][] master_keys { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] package1_keys { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] package2_keys { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[] package2_key_source { get; set; } = new byte[0x10]; + public byte[] aes_kek_generation_source { get; set; } = new byte[0x10]; + public byte[] aes_key_generation_source { get; set; } = new byte[0x10]; + public byte[] key_area_key_application_source { get; set; } = new byte[0x10]; + public byte[] key_area_key_ocean_source { get; set; } = new byte[0x10]; + public byte[] key_area_key_system_source { get; set; } = new byte[0x10]; + public byte[] titlekek_source { get; set; } = new byte[0x10]; + public byte[] header_kek_source { get; set; } = new byte[0x10]; + public byte[] sd_card_kek_source { get; set; } = new byte[0x10]; + public byte[][] sd_card_key_sources { get; set; } = Util.CreateJaggedArray(2, 0x20); + public byte[] encrypted_header_key { get; set; } = new byte[0x20]; + public byte[] header_key { get; set; } = new byte[0x20]; + public byte[][] titlekeks { get; set; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][][] key_area_keys { get; set; } = Util.CreateJaggedArray(0x20, 3, 0x10); + public byte[][] sd_card_keys { get; set; } = Util.CreateJaggedArray(2, 0x20); + public byte[] nca_hdr_fixed_key_modulus { get; set; } = new byte[0x100]; + public byte[] acid_fixed_key_modulus { get; set; } = new byte[0x100]; + public byte[] package2_fixed_key_modulus { get; set; } = new byte[0x100]; + + public void SetSdSeed(byte[] sdseed) + { + foreach (byte[] key in sd_card_key_sources) + { + for (int i = 0; i < 0x20; i++) + { + key[i] ^= sdseed[i & 0xF]; + } + } + DeriveKeys(); + } + + private void DeriveKeys() + { + //var cmac = new byte[0x10]; + //for (int i = 0; i < 0x20; i++) + //{ + // Crypto.DecryptEcb(tsec_key, keyblob_key_sources[i], keyblob_keys[i], 0x10); + // Crypto.DecryptEcb(secure_boot_key, keyblob_keys[i], keyblob_keys[i], 0x10); + // Crypto.DecryptEcb(keyblob_keys[i], keyblob_mac_key_source, keyblob_mac_keys[i], 0x10); + //} + + var sdKek = new byte[0x10]; + Crypto.GenerateKek(sdKek, sd_card_kek_source, master_keys[0], aes_kek_generation_source, aes_key_generation_source); + + for (int k = 0; k < sd_card_key_sources.Length; k++) + { + Crypto.DecryptEcb(sdKek, sd_card_key_sources[k], sd_card_keys[k], 0x20); + } + } + } + + public static class ExternalKeys + { + private static readonly Dictionary KeyDict = CreateKeyDict(); + + public static Keyset ReadKeyFile(string filename) + { + var keyset = new Keyset(); + using (var reader = new StreamReader(new FileStream(filename, FileMode.Open))) + { + string line; + while ((line = reader.ReadLine()) != null) + { + var a = line.Split('='); + if (a.Length != 2) continue; + + var key = a[0].Trim(); + var valueStr = a[1].Trim(); + + if (!KeyDict.TryGetValue(key, out var kv)) + { + Console.WriteLine($"Failed to match key {key}"); + continue; + } + + var value = valueStr.ToBytes(); + if (value.Length != kv.Size) + { + Console.WriteLine($"Key {key} had incorrect size {value.Length}. (Expected {kv.Size})"); + continue; + } + + kv.Assign(keyset, value); + } + } + + return keyset; + } + + private static Dictionary CreateKeyDict() + { + var keys = new List + { + new KeyValue("aes_kek_generation_source", 0x10, (set, key) => set.aes_kek_generation_source = key), + new KeyValue("aes_key_generation_source", 0x10, (set, key) => set.aes_key_generation_source = key), + new KeyValue("key_area_key_application_source", 0x10, (set, key) => set.key_area_key_application_source = key), + new KeyValue("key_area_key_ocean_source", 0x10, (set, key) => set.key_area_key_ocean_source = key), + new KeyValue("key_area_key_system_source", 0x10, (set, key) => set.key_area_key_system_source = key), + new KeyValue("titlekek_source", 0x10, (set, key) => set.titlekek_source = key), + new KeyValue("header_kek_source", 0x10, (set, key) => set.header_kek_source = key), + new KeyValue("header_key_source", 0x20, (set, key) => set.encrypted_header_key = key), + new KeyValue("header_key", 0x20, (set, key) => set.header_key = key), + new KeyValue("encrypted_header_key", 0x20, (set, key) => set.encrypted_header_key = key), + new KeyValue("package2_key_source", 0x10, (set, key) => set.package2_key_source = key), + new KeyValue("sd_card_kek_source", 0x10, (set, key) => set.sd_card_kek_source = key), + new KeyValue("sd_card_nca_key_source", 0x20, (set, key) => set.sd_card_key_sources[1] = key), + new KeyValue("sd_card_save_key_source", 0x20, (set, key) => set.sd_card_key_sources[0] = key), + new KeyValue("master_key_source", 0x10, (set, key) => set.master_key_source = key), + new KeyValue("keyblob_mac_key_source", 0x10, (set, key) => set.keyblob_mac_key_source = key), + new KeyValue("secure_boot_key", 0x10, (set, key) => set.secure_boot_key = key), + new KeyValue("tsec_key", 0x10, (set, key) => set.tsec_key = key) + }; + + for (int slot = 0; slot < 0x20; slot++) + { + int i = slot; + keys.Add(new KeyValue($"keyblob_key_source_{i:D2}", 0x10, (set, key) => set.keyblob_key_sources[i] = key)); + keys.Add(new KeyValue($"keyblob_key_{i:D2}", 0x10, (set, key) => set.keyblob_keys[i] = key)); + keys.Add(new KeyValue($"keyblob_mac_key_{i:D2}", 0x10, (set, key) => set.keyblob_mac_keys[i] = key)); + keys.Add(new KeyValue($"encrypted_keyblob_{i:D2}", 0xB0, (set, key) => set.encrypted_keyblobs[i] = key)); + keys.Add(new KeyValue($"keyblob_{i:D2}", 0x90, (set, key) => set.keyblobs[i] = key)); + keys.Add(new KeyValue($"master_key_{i:D2}", 0x10, (set, key) => set.master_keys[i] = key)); + keys.Add(new KeyValue($"package1_key_{i:D2}", 0x10, (set, key) => set.package1_keys[i] = key)); + keys.Add(new KeyValue($"package2_key_{i:D2}", 0x10, (set, key) => set.package2_keys[i] = key)); + keys.Add(new KeyValue($"titlekek_{i:D2}", 0x10, (set, key) => set.titlekeks[i] = key)); + keys.Add(new KeyValue($"key_area_key_application_{i:D2}", 0x10, (set, key) => set.key_area_keys[i][0] = key)); + keys.Add(new KeyValue($"key_area_key_ocean_{i:D2}", 0x10, (set, key) => set.key_area_keys[i][1] = key)); + keys.Add(new KeyValue($"key_area_key_system_{i:D2}", 0x10, (set, key) => set.key_area_keys[i][2] = key)); + } + + return keys.ToDictionary(k => k.Name, k => k); + } + + private class KeyValue + { + public string Name; + public int Size; + public Action Assign; + + public KeyValue(string name, int size, Action assign) + { + Name = name; + Size = size; + Assign = assign; + } + } + } +} diff --git a/libhac/Nax0.cs b/libhac/Nax0.cs new file mode 100644 index 00000000..bf097c2d --- /dev/null +++ b/libhac/Nax0.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using libhac.XTSSharp; + +namespace libhac +{ + public class Nax0 + { + public List Files = new List(); + public byte[] Hmac { get; set; } + public byte[][] EncKeys { get; } = Util.CreateJaggedArray(2, 0x10); + public byte[][] Keys { get; } = Util.CreateJaggedArray(2, 0x10); + public long Length { get; set; } + public Stream Stream { get; } + private List Streams = new List(); + + public Nax0(Keyset keyset, string path, string sdPath) + { + if (Directory.Exists(path)) + { + while (true) + { + var partName = Path.Combine(path, $"{Files.Count:D2}"); + if (!File.Exists(partName)) break; + + Files.Add(partName); + } + + } + else if (File.Exists(path)) + { + Files.Add(path); + } + else + { + throw new FileNotFoundException("Could not find the input file or directory"); + } + + foreach (var file in Files) + { + Streams.Add(new FileStream(file, FileMode.Open)); + } + + var stream = new CombinationStream(Streams); + ReadHeader(stream); + + for (int k = 0; k < 2; k++) + { + var naxSpecificKeys = Util.CreateJaggedArray(2, 0x10); + var hashKey2 = new byte[0x10]; + Array.Copy(keyset.sd_card_keys[k], hashKey2, 0x10); + + var hash2 = new HMACSHA256(hashKey2); + var sdPathBytes = Encoding.ASCII.GetBytes(sdPath); + var checksum = hash2.ComputeHash(sdPathBytes, 0, sdPathBytes.Length); + Array.Copy(checksum, 0, naxSpecificKeys[0], 0, 0x10); + Array.Copy(checksum, 0x10, naxSpecificKeys[1], 0, 0x10); + + Crypto.DecryptEcb(naxSpecificKeys[0], EncKeys[0], Keys[0], 0x10); + Crypto.DecryptEcb(naxSpecificKeys[1], EncKeys[1], Keys[1], 0x10); + } + + stream.Position = 0x20; + var hashKey = new byte[0x60]; + stream.Read(hashKey, 0, 0x60); + Array.Copy(Keys[0], 0, hashKey, 8, 0x10); + Array.Copy(Keys[1], 0, hashKey, 0x18, 0x10); + + var hash = new HMACSHA256(hashKey); + var validationMac = hash.ComputeHash(keyset.sd_card_keys[1], 0x10, 0x10); + var isValid = Util.ArraysEqual(Hmac, validationMac); + + if (!isValid) throw new ArgumentException("NAX0 key derivation failed."); + + stream.Position = 0x4000; + + var xts = XtsAes128.Create(Keys[0], Keys[1]); + Stream = new RandomAccessSectorStream(new XtsSectorStream(stream, xts, 0x4000, 0x4000)); + } + + private void ReadHeader(Stream nax0) + { + var reader = new BinaryReader(nax0); + nax0.Position = 0; + + Hmac = reader.ReadBytes(0x20); + nax0.Position += 8; //todo check magic + EncKeys[0] = reader.ReadBytes(0x10); + EncKeys[1] = reader.ReadBytes(0x10); + Length = reader.ReadInt64(); + } + } +} diff --git a/libhac/Program.cs b/libhac/Program.cs index abf83385..027df219 100644 --- a/libhac/Program.cs +++ b/libhac/Program.cs @@ -1,4 +1,4 @@ -using System; +using System.IO; namespace libhac { @@ -6,7 +6,16 @@ namespace libhac { static void Main(string[] args) { - Console.WriteLine("Hello World!"); + var keyset = ExternalKeys.ReadKeyFile(args[0]); + keyset.SetSdSeed(args[1].ToBytes()); + + var nax0 = new Nax0(keyset, args[2], args[3]); + using (var output = new FileStream(args[4], FileMode.Create)) + using (var progress = new ProgressBar()) + { + progress.LogMessage($"Writing {args[4]}"); + Util.CopyStream(nax0.Stream, output, nax0.Stream.Length, progress); + } } } } diff --git a/libhac/ProgressBar.cs b/libhac/ProgressBar.cs new file mode 100644 index 00000000..0690daec --- /dev/null +++ b/libhac/ProgressBar.cs @@ -0,0 +1,130 @@ +// Adapted from https://gist.github.com/0ab6a96899cc5377bf54 + +using System; +using System.Text; +using System.Threading; + +namespace libhac +{ + public class ProgressBar : IDisposable, IProgressReport + { + private const int BlockCount = 20; + private long _progress; + private long _total; + private readonly Timer _timer; + + private readonly TimeSpan _animationInterval = TimeSpan.FromSeconds(1.0 / 30); + private const string Animation = @"|/-\"; + + private string _currentText = string.Empty; + private bool _disposed; + private int _animationIndex; + + private StringBuilder LogText { get; } = new StringBuilder(); + + public ProgressBar() + { + var timerCallBack = new TimerCallback(TimerHandler); + _timer = new Timer(timerCallBack, 0, 0, 0); + } + + public void Report(long value) + { + Interlocked.Exchange(ref _progress, value); + } + + public void ReportAdd(long value) + { + Interlocked.Add(ref _progress, value); + } + + public void LogMessage(string message) + { + lock (_timer) + { + LogText.AppendLine(message); + } + } + + public void SetTotal(long value) + { + Interlocked.Exchange(ref _total, value); + Report(0); + } + + private void TimerHandler(object state) + { + lock (_timer) + { + if (_disposed) return; + + string text = string.Empty; + + if (_total > 0) + { + double progress = _total == 0 ? 0 : (double)_progress / _total; + int progressBlockCount = (int)Math.Min(progress * BlockCount, BlockCount); + text = $"[{new string('#', progressBlockCount)}{new string('-', BlockCount - progressBlockCount)}] {_progress}/{_total} {progress:P1} {Animation[_animationIndex++ % Animation.Length]}"; + } + UpdateText(text); + + ResetTimer(); + } + } + + private void UpdateText(string text) + { + StringBuilder outputBuilder = new StringBuilder(); + + if (LogText.Length > 0) + { + // Erase current text + outputBuilder.Append("\r"); + outputBuilder.Append(' ', _currentText.Length); + outputBuilder.Append("\r"); + outputBuilder.Append(LogText); + _currentText = string.Empty; + LogText.Clear(); + } + + // Get length of common portion + int commonPrefixLength = 0; + int commonLength = Math.Min(_currentText.Length, text.Length); + while (commonPrefixLength < commonLength && text[commonPrefixLength] == _currentText[commonPrefixLength]) + { + commonPrefixLength++; + } + + // Backtrack to the first differing character + outputBuilder.Append('\b', _currentText.Length - commonPrefixLength); + + // Output new suffix + outputBuilder.Append(text.Substring(commonPrefixLength)); + + // If the new text is shorter than the old one: delete overlapping characters + int overlapCount = _currentText.Length - text.Length; + if (overlapCount > 0) + { + outputBuilder.Append(' ', overlapCount); + outputBuilder.Append('\b', overlapCount); + } + + Console.Write(outputBuilder); + _currentText = text; + } + + private void ResetTimer() + { + _timer.Change(_animationInterval, TimeSpan.FromMilliseconds(-1)); + } + + public void Dispose() + { + lock (_timer) + { + _disposed = true; + UpdateText(string.Empty); + } + } + } +} diff --git a/libhac/Util.cs b/libhac/Util.cs new file mode 100644 index 00000000..3581219b --- /dev/null +++ b/libhac/Util.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; + +namespace libhac +{ + public static class Util + { + public static T CreateJaggedArray(params int[] lengths) + { + return (T)InitializeJaggedArray(typeof(T).GetElementType(), 0, lengths); + } + + private static object InitializeJaggedArray(Type type, int index, int[] lengths) + { + Array array = Array.CreateInstance(type, lengths[index]); + + Type elementType = type.GetElementType(); + if (elementType == null) return array; + + for (int i = 0; i < lengths[index]; i++) + { + array.SetValue(InitializeJaggedArray(elementType, index + 1, lengths), i); + } + + return array; + } + + public static bool ArraysEqual(T[] a1, T[] a2) + { + if (a1 == null || a2 == null) return false; + if (a1 == a2) return true; + if (a1.Length != a2.Length) return false; + + for (int i = 0; i < a1.Length; i++) + { + if (!a1[i].Equals(a2[i])) + { + return false; + } + } + + return true; + } + + public static bool IsEmpty(this byte[] array) + { + if (array == null) throw new ArgumentNullException(nameof(array)); + + for (int i = 0; i < array.Length; i++) + { + if (i != 0) + { + return false; + } + } + + return true; + } + + public static void CopyStream(this Stream input, Stream output, long length, IProgressReport progress = null) + { + const int bufferSize = 0x8000; + long remaining = length; + byte[] buffer = new byte[bufferSize]; + progress?.SetTotal((length + bufferSize) / bufferSize); + + int read; + while ((read = input.Read(buffer, 0, (int)Math.Min(buffer.Length, remaining))) > 0) + { + output.Write(buffer, 0, read); + remaining -= read; + progress?.ReportAdd(1); + } + } + + private static int HexToInt(char c) + { + switch (c) + { + case '0': + return 0; + case '1': + return 1; + case '2': + return 2; + case '3': + return 3; + case '4': + return 4; + case '5': + return 5; + case '6': + return 6; + case '7': + return 7; + case '8': + return 8; + case '9': + return 9; + case 'a': + case 'A': + return 10; + case 'b': + case 'B': + return 11; + case 'c': + case 'C': + return 12; + case 'd': + case 'D': + return 13; + case 'e': + case 'E': + return 14; + case 'f': + case 'F': + return 15; + default: + throw new FormatException("Unrecognized hex char " + c); + } + } + + private static readonly byte[,] ByteLookup = { + {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}, + {0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0} + }; + + public static byte[] ToBytes(this string input) + { + var result = new byte[(input.Length + 1) >> 1]; + int lastcell = result.Length - 1; + int lastchar = input.Length - 1; + for (int i = 0; i < input.Length; i++) + { + result[lastcell - (i >> 1)] |= ByteLookup[i & 1, HexToInt(input[lastchar - i])]; + } + return result; + } + } +} diff --git a/libhac/XTSSharp/RandomAccessSectorStream.cs b/libhac/XTSSharp/RandomAccessSectorStream.cs new file mode 100644 index 00000000..7ce5ff86 --- /dev/null +++ b/libhac/XTSSharp/RandomAccessSectorStream.cs @@ -0,0 +1,323 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.IO; + +namespace libhac.XTSSharp +{ + /// + /// A wraps a sector based stream and provides random access to it + /// + public class RandomAccessSectorStream : Stream + { + private readonly byte[] _buffer; + private readonly int _bufferSize; + private readonly SectorStream _s; + private readonly bool _isStreamOwned; + private bool _bufferDirty; + private bool _bufferLoaded; + private int _bufferPos; + + /// + /// Creates a new stream + /// + /// Base stream + public RandomAccessSectorStream(SectorStream s) + : this(s, false) + { + } + + /// + /// Creates a new stream + /// + /// Base stream + /// Does this stream own the base stream? i.e. should it be automatically disposed? + public RandomAccessSectorStream(SectorStream s, bool isStreamOwned) + { + _s = s; + _isStreamOwned = isStreamOwned; + _buffer = new byte[s.SectorSize]; + _bufferSize = s.SectorSize; + } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get { return _s.CanRead; } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get { return _s.CanSeek; } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get { return _s.CanWrite; } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// A long value representing the length of the stream in bytes. + public override long Length + { + get { return _s.Length + _bufferPos; } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + public override long Position + { + get { return _bufferLoaded ? (_s.Position - _bufferSize + _bufferPos) : _s.Position + _bufferPos; } + set + { + if (value < 0L) + throw new ArgumentOutOfRangeException("value"); + + var sectorPosition = (value % _bufferSize); + var position = value - sectorPosition; + + //see if its within the current sector + if (_bufferLoaded) + { + var basePosition = _s.Position - _bufferSize; + if (value > basePosition && value < basePosition + _bufferSize) + { + _bufferPos = (int)sectorPosition; + return; + } + } + //outside the current buffer + + //write it + if (_bufferDirty) + WriteSector(); + + _s.Position = position; + + //read this sector + ReadSector(); + + //bump us forward if need be + _bufferPos = (int)sectorPosition; + } + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + Flush(); + + base.Dispose(disposing); + + if (_isStreamOwned) + _s.Dispose(); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + public override void Flush() + { + if (_bufferDirty) + WriteSector(); + } + + /// + /// Sets the position within the current stream. + /// + /// + /// The new position within the current stream. + /// + /// A byte offset relative to the parameter. + /// A value of type indicating the reference point used to obtain the new position. + public override long Seek(long offset, SeekOrigin origin) + { + long newPosition; + switch (origin) + { + case SeekOrigin.Begin: + newPosition = offset; + break; + case SeekOrigin.End: + newPosition = Length - offset; + break; + default: + newPosition = Position + offset; + break; + } + + Position = newPosition; + + return newPosition; + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + public override void SetLength(long value) + { + var remainder = value % _s.SectorSize; + + if (remainder > 0) + { + value = (value - remainder) + _bufferSize; + } + + _s.SetLength(value); + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between and ( + - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + public override int Read(byte[] buffer, int offset, int count) + { + var position = Position; + + if (position + count > _s.Length) + { + count = (int)(_s.Length - position); + } + + if (!_bufferLoaded) + ReadSector(); + + var totalBytesRead = 0; + while (count > 0) + { + var bytesToRead = Math.Min(count, _bufferSize - _bufferPos); + + Buffer.BlockCopy(_buffer, _bufferPos, buffer, offset, bytesToRead); + + offset += bytesToRead; + _bufferPos += bytesToRead; + count -= bytesToRead; + + totalBytesRead += bytesToRead; + + if (_bufferPos == _bufferSize) + ReadSector(); + } + + return totalBytesRead; + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies bytes from to the current stream. + /// The zero-based byte offset in at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) + { + while (count > 0) + { + if (!_bufferLoaded) + ReadSector(); + + var bytesToWrite = Math.Min(count, _bufferSize - _bufferPos); + + Buffer.BlockCopy(buffer, offset, _buffer, _bufferPos, bytesToWrite); + + offset += bytesToWrite; + _bufferPos += bytesToWrite; + count -= bytesToWrite; + _bufferDirty = true; + + if (_bufferPos == _bufferSize) + WriteSector(); + } + } + + /// + /// Reads a sector + /// + private void ReadSector() + { + if (_bufferLoaded && _bufferDirty) + { + WriteSector(); + } + + if (_s.Position == _s.Length) + { + return; + } + + var bytesRead = _s.Read(_buffer, 0, _buffer.Length); + + //clean the end of it + if (bytesRead != _bufferSize) + Array.Clear(_buffer, bytesRead, _buffer.Length - bytesRead); + + _bufferLoaded = true; + _bufferPos = 0; + _bufferDirty = false; + } + + /// + /// Writes a sector + /// + private void WriteSector() + { + if (_bufferLoaded) + { + //go back to beginning of the current sector + _s.Seek(-_bufferSize, SeekOrigin.Current); + } + + //write it + _s.Write(_buffer, 0, _bufferSize); + _bufferDirty = false; + _bufferLoaded = false; + _bufferPos = 0; + Array.Clear(_buffer, 0, _bufferSize); + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/SectorStream.cs b/libhac/XTSSharp/SectorStream.cs new file mode 100644 index 00000000..8ba052c1 --- /dev/null +++ b/libhac/XTSSharp/SectorStream.cs @@ -0,0 +1,233 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.IO; + +namespace libhac.XTSSharp +{ + /// + /// Sector-based stream + /// + public class SectorStream : Stream + { + private readonly Stream _baseStream; + private readonly long _offset; + private ulong _currentSector; + + /// + /// Creates a new stream + /// + /// The base stream to read/write from + /// The size of the sectors to read/write + public SectorStream(Stream baseStream, int sectorSize) + : this(baseStream, sectorSize, 0) + { + } + + /// + /// Creates a new stream + /// + /// The base stream to read/write from + /// The size of the sectors to read/write + /// Offset to start counting sectors + public SectorStream(Stream baseStream, int sectorSize, long offset) + { + SectorSize = sectorSize; + _baseStream = baseStream; + _offset = offset; + } + + /// + /// The size of the sectors + /// + public int SectorSize { get; private set; } + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get { return _baseStream.CanRead; } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get { return _baseStream.CanSeek; } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get { return _baseStream.CanWrite; } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// A long value representing the length of the stream in bytes. + public override long Length + { + get { return _baseStream.Length - _offset; } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + public override long Position + { + get { return _baseStream.Position - _offset; } + set + { + ValidateSizeMultiple(value); + + //base stream gets the non-tweaked value + _baseStream.Position = value + _offset; + _currentSector = (ulong)(value / SectorSize); + } + } + + /// + /// The current sector this stream is at + /// + protected ulong CurrentSector => _currentSector; + + /// + /// Validates that the size is a multiple of the sector size + /// + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local + private void ValidateSizeMultiple(long value) + { + if (value % SectorSize != 0) + throw new ArgumentException(string.Format("Value needs to be a multiple of {0}", SectorSize)); + } + + /// + /// Validates that the size is equal to the sector size + /// + protected void ValidateSize(long value) + { + if (value != SectorSize) + throw new ArgumentException(string.Format("Value needs to be {0}", SectorSize)); + } + + /// + /// Validates that the size is equal to the sector size + /// + protected void ValidateSize(int value) + { + if (value != SectorSize) + throw new ArgumentException(string.Format("Value needs to be {0}", SectorSize)); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + /// + public override void Flush() + { + _baseStream.Flush(); + } + + /// + /// Sets the position within the current stream. + /// + /// + /// The new position within the current stream. + /// + /// A byte offset relative to the parameter. + /// A value of type indicating the reference point used to obtain the new position. + public override long Seek(long offset, SeekOrigin origin) + { + long newPosition; + switch (origin) + { + case SeekOrigin.Begin: + newPosition = offset; + break; + case SeekOrigin.End: + newPosition = Length - offset; + break; + default: + newPosition = Position + offset; + break; + } + + Position = newPosition; + + return newPosition; + } + + /// + /// Sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + public override void SetLength(long value) + { + ValidateSizeMultiple(value); + + _baseStream.SetLength(value); + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between and ( + - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + public override int Read(byte[] buffer, int offset, int count) + { + ValidateSize(count); + + var ret = _baseStream.Read(buffer, offset, count); + _currentSector++; + return ret; + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies bytes from to the current stream. + /// The zero-based byte offset in at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) + { + ValidateSize(count); + + _baseStream.Write(buffer, offset, count); + _currentSector++; + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/Xts.cs b/libhac/XTSSharp/Xts.cs new file mode 100644 index 00000000..fa93983b --- /dev/null +++ b/libhac/XTSSharp/Xts.cs @@ -0,0 +1,117 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Security.Cryptography; + +namespace libhac.XTSSharp +{ + /// + /// Xts. See and . + /// + public class Xts + { + private readonly SymmetricAlgorithm _key1; + private readonly SymmetricAlgorithm _key2; + + /// + /// Creates a new Xts implementation. + /// + /// Function to create the implementations + /// Key 1 + /// Key 2 + protected Xts(Func create, byte[] key1, byte[] key2) + { + if (create == null) + throw new ArgumentNullException("create"); + if (key1 == null) + throw new ArgumentNullException("key1"); + if (key2 == null) + throw new ArgumentNullException("key2"); + + _key1 = create(); + _key2 = create(); + + if (key1.Length != key2.Length) + throw new ArgumentException("Key lengths don't match"); + + //set the key sizes + _key1.KeySize = key1.Length * 8; + _key2.KeySize = key2.Length * 8; + + //set the keys + _key1.Key = key1; + _key2.Key = key2; + + //ecb mode + _key1.Mode = CipherMode.ECB; + _key2.Mode = CipherMode.ECB; + + //no padding - we're always going to be writing full blocks + _key1.Padding = PaddingMode.None; + _key2.Padding = PaddingMode.None; + + //fixed block size of 128 bits. + _key1.BlockSize = 16 * 8; + _key2.BlockSize = 16 * 8; + } + + /// + /// Creates an xts encryptor + /// + public XtsCryptoTransform CreateEncryptor() + { + return new XtsCryptoTransform(_key1.CreateEncryptor(), _key2.CreateEncryptor(), false); + } + + /// + /// Creates an xts decryptor + /// + public XtsCryptoTransform CreateDecryptor() + { + return new XtsCryptoTransform(_key1.CreateDecryptor(), _key2.CreateEncryptor(), true); + } + + /// + /// Verify that the key is of an expected size of bits + /// + /// Expected size of the key in bits + /// The key + /// The key + /// If the key is null + /// If the key length does not match the expected length + protected static byte[] VerifyKey(int expectedSize, byte[] key) + { + if (key == null) + throw new ArgumentNullException("key"); + + if (key.Length * 8 != expectedSize) + throw new ArgumentException(string.Format("Expected key length of {0} bits, got {1}", expectedSize, key.Length * 8)); + + return key; + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/XtsAes128.cs b/libhac/XTSSharp/XtsAes128.cs new file mode 100644 index 00000000..cdc1ad56 --- /dev/null +++ b/libhac/XTSSharp/XtsAes128.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Security.Cryptography; + +namespace libhac.XTSSharp +{ + /// + /// XTS-AES-128 implementation + /// + public class XtsAes128 : Xts + { + private const int KeyLength = 128; + private const int KeyByteLength = KeyLength / 8; + + /// + /// Creates a new instance + /// + protected XtsAes128(Func create, byte[] key1, byte[] key2) + : base(create, VerifyKey(KeyLength, key1), VerifyKey(KeyLength, key2)) + { + } + + /// + /// Creates a new implementation + /// + /// First key + /// Second key + /// Xts implementation + /// Keys need to be 128 bits long (i.e. 16 bytes) + public static Xts Create(byte[] key1, byte[] key2) + { + VerifyKey(KeyLength, key1); + VerifyKey(KeyLength, key2); + + return new XtsAes128(Aes.Create, key1, key2); + } + + /// + /// Creates a new implementation + /// + /// Key to use + /// Xts implementation + /// Key need to be 256 bits long (i.e. 32 bytes) + public static Xts Create(byte[] key) + { + VerifyKey(KeyLength * 2, key); + + byte[] key1 = new byte[KeyByteLength]; + byte[] key2 = new byte[KeyByteLength]; + + Buffer.BlockCopy(key, 0, key1, 0, KeyByteLength); + Buffer.BlockCopy(key, KeyByteLength, key2, 0, KeyByteLength); + + return new XtsAes128(Aes.Create, key1, key2); + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/XtsAes256.cs b/libhac/XTSSharp/XtsAes256.cs new file mode 100644 index 00000000..046f0a8c --- /dev/null +++ b/libhac/XTSSharp/XtsAes256.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Security.Cryptography; + +namespace libhac.XTSSharp +{ + /// + /// XTS-AES-256 implementation + /// + public class XtsAes256 : Xts + { + private const int KeyLength = 256; + private const int KeyByteLength = KeyLength / 8; + + /// + /// Creates a new instance + /// + protected XtsAes256(Func create, byte[] key1, byte[] key2) + : base(create, VerifyKey(KeyLength, key1), VerifyKey(KeyLength, key2)) + { + } + + /// + /// Creates a new implementation + /// + /// First key + /// Second key + /// Xts implementation + /// Keys need to be 256 bits long (i.e. 32 bytes) + public static Xts Create(byte[] key1, byte[] key2) + { + VerifyKey(KeyLength, key1); + VerifyKey(KeyLength, key2); + + return new XtsAes256(Aes.Create, key1, key2); + } + + /// + /// Creates a new implementation + /// + /// Key to use + /// Xts implementation + /// Keys need to be 512 bits long (i.e. 64 bytes) + public static Xts Create(byte[] key) + { + VerifyKey(KeyLength * 2, key); + + var key1 = new byte[KeyByteLength]; + var key2 = new byte[KeyByteLength]; + + Buffer.BlockCopy(key, 0, key1, 0, KeyByteLength); + Buffer.BlockCopy(key, KeyByteLength, key2, 0, KeyByteLength); + + return new XtsAes256(Aes.Create, key1, key2); + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/XtsCryptoTransform.cs b/libhac/XTSSharp/XtsCryptoTransform.cs new file mode 100644 index 00000000..7ef6da96 --- /dev/null +++ b/libhac/XTSSharp/XtsCryptoTransform.cs @@ -0,0 +1,231 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Security.Cryptography; + +namespace libhac.XTSSharp +{ + /// + /// The actual Xts cryptography transform + /// + /// + /// The reason that it doesn't implement ICryptoTransform, as the interface is different. + /// + /// Most of the logic was taken from the LibTomCrypt project - http://libtom.org and + /// converted to C# + /// + public class XtsCryptoTransform : IDisposable + { + private readonly byte[] _cc = new byte[16]; + private readonly bool _decrypting; + private readonly ICryptoTransform _key1; + private readonly ICryptoTransform _key2; + + private readonly byte[] _pp = new byte[16]; + private readonly byte[] _t = new byte[16]; + private readonly byte[] _tweak = new byte[16]; + + /// + /// Creates a new transform + /// + /// Transform 1 + /// Transform 2 + /// Is this a decryption transform? + public XtsCryptoTransform(ICryptoTransform key1, ICryptoTransform key2, bool decrypting) + { + if (key1 == null) + throw new ArgumentNullException("key1"); + + if (key2 == null) + throw new ArgumentNullException("key2"); + + _key1 = key1; + _key2 = key2; + _decrypting = decrypting; + } + + #region IDisposable Members + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// 2 + public void Dispose() + { + _key1.Dispose(); + _key2.Dispose(); + } + + #endregion + + /// + /// Transforms a single block. + /// + /// The input for which to compute the transform. + /// The offset into the input byte array from which to begin using data. + /// The number of bytes in the input byte array to use as data. + /// The output to which to write the transform. + /// The offset into the output byte array from which to begin writing data. + /// The sector number of the block + /// The number of bytes written. + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset, ulong sector) + { + //Nintendo Switch uses Little Endian + FillArrayFromSectorLittleEndian(_tweak, sector); + + int lim; + + /* get number of blocks */ + var m = inputCount >> 4; + var mo = inputCount & 15; + + /* encrypt the tweak */ + _key2.TransformBlock(_tweak, 0, _tweak.Length, _t, 0); + + /* for i = 0 to m-2 do */ + if (mo == 0) + lim = m; + else + lim = m - 1; + + for (var i = 0; i < lim; i++) + { + TweakCrypt(inputBuffer, inputOffset, outputBuffer, outputOffset, _t); + inputOffset += 16; + outputOffset += 16; + } + + /* if ptlen not divide 16 then */ + if (mo > 0) + { + if (_decrypting) + { + Buffer.BlockCopy(_t, 0, _cc, 0, 16); + MultiplyByX(_cc); + + /* CC = tweak encrypt block m-1 */ + TweakCrypt(inputBuffer, inputOffset, _pp, 0, _cc); + + /* Cm = first ptlen % 16 bytes of CC */ + int i; + for (i = 0; i < mo; i++) + { + _cc[i] = inputBuffer[16 + i + inputOffset]; + outputBuffer[16 + i + outputOffset] = _pp[i]; + } + + for (; i < 16; i++) + { + _cc[i] = _pp[i]; + } + + /* Cm-1 = Tweak encrypt PP */ + TweakCrypt(_cc, 0, outputBuffer, outputOffset, _t); + } + else + { + /* CC = tweak encrypt block m-1 */ + TweakCrypt(inputBuffer, inputOffset, _cc, 0, _t); + + /* Cm = first ptlen % 16 bytes of CC */ + int i; + for (i = 0; i < mo; i++) + { + _pp[i] = inputBuffer[16 + i + inputOffset]; + outputBuffer[16 + i + outputOffset] = _cc[i]; + } + + for (; i < 16; i++) + { + _pp[i] = _cc[i]; + } + + /* Cm-1 = Tweak encrypt PP */ + TweakCrypt(_pp, 0, outputBuffer, outputOffset, _t); + } + } + + return inputCount; + } + + /// + /// Fills a byte array from a sector number (little endian) + /// + /// The destination + /// The sector number + private static void FillArrayFromSectorLittleEndian(byte[] value, ulong sector) + { + value[0x8] = (byte)((sector >> 56) & 255); + value[0x9] = (byte)((sector >> 48) & 255); + value[0xA] = (byte)((sector >> 40) & 255); + value[0xB] = (byte)((sector >> 32) & 255); + value[0xC] = (byte)((sector >> 24) & 255); + value[0xD] = (byte)((sector >> 16) & 255); + value[0xE] = (byte)((sector >> 8) & 255); + value[0xF] = (byte)(sector & 255); + } + + /// + /// Performs the Xts TweakCrypt operation + /// + private void TweakCrypt(byte[] inputBuffer, int inputOffset, byte[] outputBuffer, int outputOffset, byte[] t) + { + for (var x = 0; x < 16; x++) + { + outputBuffer[x + outputOffset] = (byte)(inputBuffer[x + inputOffset] ^ t[x]); + } + + _key1.TransformBlock(outputBuffer, outputOffset, 16, outputBuffer, outputOffset); + + for (var x = 0; x < 16; x++) + { + outputBuffer[x + outputOffset] = (byte)(outputBuffer[x + outputOffset] ^ t[x]); + } + + MultiplyByX(t); + } + + /// + /// Multiply by x + /// + /// The value to multiply by x (LFSR shift) + private static void MultiplyByX(byte[] i) + { + byte t = 0, tt = 0; + + for (var x = 0; x < 16; x++) + { + tt = (byte)(i[x] >> 7); + i[x] = (byte)(((i[x] << 1) | t) & 0xFF); + t = tt; + } + + if (tt > 0) + i[0] ^= 0x87; + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/XtsSectorStream.cs b/libhac/XTSSharp/XtsSectorStream.cs new file mode 100644 index 00000000..661ac3c5 --- /dev/null +++ b/libhac/XTSSharp/XtsSectorStream.cs @@ -0,0 +1,155 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.IO; + +namespace libhac.XTSSharp +{ + /// + /// Xts sector-based + /// + public class XtsSectorStream : SectorStream + { + /// + /// The default sector size + /// + public const int DefaultSectorSize = 512; + + private readonly byte[] _tempBuffer; + private readonly Xts _xts; + private XtsCryptoTransform _decryptor; + private XtsCryptoTransform _encryptor; + + /// + /// Creates a new stream with the default sector size + /// + /// The base stream + /// The xts transform + public XtsSectorStream(Stream baseStream, Xts xts) + : this(baseStream, xts, DefaultSectorSize) + { + } + + /// + /// Creates a new stream + /// + /// The base stream + /// The xts transform + /// Sector size + public XtsSectorStream(Stream baseStream, Xts xts, int sectorSize) + : this(baseStream, xts, sectorSize, 0) + { + } + + /// + /// Creates a new stream + /// + /// The base stream + /// The xts transform + /// Sector size + /// Offset to start counting sectors + public XtsSectorStream(Stream baseStream, Xts xts, int sectorSize, long offset) + : base(baseStream, sectorSize, offset) + { + _xts = xts; + _tempBuffer = new byte[sectorSize]; + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (_encryptor != null) + _encryptor.Dispose(); + + if (_decryptor != null) + _decryptor.Dispose(); + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies bytes from to the current stream. + /// The zero-based byte offset in at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + public override void Write(byte[] buffer, int offset, int count) + { + ValidateSize(count); + + if (count == 0) + return; + + //get the current sector + var currentSector = CurrentSector; + + if (_encryptor == null) + _encryptor = _xts.CreateEncryptor(); + + //encrypt the sector + int transformedCount = _encryptor.TransformBlock(buffer, offset, count, _tempBuffer, 0, currentSector); + + //Console.WriteLine("Encrypting sector {0}", currentSector); + + //write it to the base stream + base.Write(_tempBuffer, 0, transformedCount); + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between and ( + - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + public override int Read(byte[] buffer, int offset, int count) + { + ValidateSize(count); + + //get the current sector + var currentSector = CurrentSector; + + //read the sector from the base stream + var ret = base.Read(_tempBuffer, 0, count); + + if (ret == 0) + return 0; + + if (_decryptor == null) + _decryptor = _xts.CreateDecryptor(); + + //decrypt the sector + var retV = _decryptor.TransformBlock(_tempBuffer, 0, ret, buffer, offset, currentSector); + + //Console.WriteLine("Decrypting sector {0} == {1} bytes", currentSector, retV); + + return retV; + } + } +} \ No newline at end of file diff --git a/libhac/XTSSharp/XtsStream.cs b/libhac/XTSSharp/XtsStream.cs new file mode 100644 index 00000000..84b2c80a --- /dev/null +++ b/libhac/XTSSharp/XtsStream.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2010 Gareth Lennox (garethl@dwakn.com) +// All rights reserved. + +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of Gareth Lennox nor the names of its +// contributors may be used to endorse or promote products derived from this +// software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System.IO; + +namespace libhac.XTSSharp +{ + /// + /// A random access, xts encrypted stream + /// + public class XtsStream : RandomAccessSectorStream + { + /// + /// Creates a new stream + /// + /// The base stream + /// Xts implementation to use + public XtsStream(Stream baseStream, Xts xts) + : this(baseStream, xts, XtsSectorStream.DefaultSectorSize) + { + } + + /// + /// Creates a new stream + /// + /// The base stream + /// Xts implementation to use + /// Sector size + public XtsStream(Stream baseStream, Xts xts, int sectorSize) + : base(new XtsSectorStream(baseStream, xts, sectorSize), true) + { + } + + + /// + /// Creates a new stream + /// + /// The base stream + /// Xts implementation to use + /// Sector size + /// Offset to start counting sectors + public XtsStream(Stream baseStream, Xts xts, int sectorSize, long offset) + : base(new XtsSectorStream(baseStream, xts, sectorSize, offset), true) + { + } + } +} \ No newline at end of file diff --git a/libhac/libhac.csproj b/libhac/libhac.csproj index 23df6047..8cd3d078 100644 --- a/libhac/libhac.csproj +++ b/libhac/libhac.csproj @@ -1,8 +1,9 @@ - + Exe netcoreapp2.1 + 7.3