diff --git a/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs b/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs new file mode 100644 index 0000000..a50090a --- /dev/null +++ b/src/N_m3u8DL-RE.Common/Enum/ExtractorType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Common.Enum +{ + public enum ExtractorType + { + MPEG_DASH, + HLS + } +} diff --git a/src/N_m3u8DL-RE.Extends/N_m3u8DL-RE.Extends.csproj b/src/N_m3u8DL-RE.Extends/N_m3u8DL-RE.Extends.csproj deleted file mode 100644 index 7958b21..0000000 --- a/src/N_m3u8DL-RE.Extends/N_m3u8DL-RE.Extends.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net6.0 - N_m3u8DL_RE.Extends - enable - enable - - - - - - - - - - - diff --git a/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs b/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs index 439ef23..d087d4a 100644 --- a/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs +++ b/src/N_m3u8DL-RE.Parser/Config/ParserConfig.cs @@ -17,29 +17,19 @@ namespace N_m3u8DL_RE.Parser.Config }; /// - /// HLS内容前置处理器. 调用顺序与列表顺序相同 + /// 内容前置处理器. 调用顺序与列表顺序相同 /// - public IList HLSContentProcessors { get; } = new List() { new DefaultHLSContentProcessor() }; - - /// - /// DASH内容前置处理器. 调用顺序与列表顺序相同 - /// - public IList DASHContentProcessors { get; } = new List() { new DefaultDASHContentProcessor() }; + public IList ContentProcessors { get; } = new List() { new DefaultHLSContentProcessor(), new DefaultDASHContentProcessor() }; /// /// 添加分片URL前置处理器. 调用顺序与列表顺序相同 /// - public IList HLSUrlProcessors { get; } = new List() { new DefaultUrlProcessor() }; + public IList UrlProcessors { get; } = new List() { new DefaultUrlProcessor() }; /// - /// DASH内容前置处理器. 调用顺序与列表顺序相同 + /// KEY解析器. 调用顺序与列表顺序相同 /// - public IList DASHUrlProcessors { get; } = new List() { new DefaultUrlProcessor() }; - - /// - /// HLS-KEY解析器. 调用顺序与列表顺序相同 - /// - public IList HLSKeyProcessors { get; } = new List() { new DefaultHLSKeyProcessor() }; + public IList KeyProcessors { get; } = new List() { new DefaultHLSKeyProcessor() }; /// diff --git a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs index c1af0c5..2b08d52 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/DASHExtractor.cs @@ -19,12 +19,15 @@ namespace N_m3u8DL_RE.Parser.Extractor //code from https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/common.py#L2076 internal class DASHExtractor : IExtractor { + public ExtractorType ExtractorType => ExtractorType.MPEG_DASH; + private string MpdUrl = string.Empty; private string BaseUrl = string.Empty; private string MpdContent = string.Empty; public ParserConfig ParserConfig { get; set; } + public DASHExtractor(ParserConfig parserConfig) { this.ParserConfig = parserConfig; @@ -54,7 +57,9 @@ namespace N_m3u8DL_RE.Parser.Extractor } } + var type = ((XmlElement)xn).GetAttribute("type"); //static dynamic var mediaPresentationDuration = ((XmlElement)xn).GetAttribute("mediaPresentationDuration"); + var ns = ((XmlElement)xn).GetAttribute("xmlns"); XmlNamespaceManager nsMgr = new XmlNamespaceManager(mpdDoc.NameTable); @@ -722,9 +727,9 @@ namespace N_m3u8DL_RE.Parser.Extractor /// private string PreProcessUrl(string url) { - foreach (var p in ParserConfig.DASHUrlProcessors) + foreach (var p in ParserConfig.UrlProcessors) { - if (p.CanProcess(url, ParserConfig)) + if (p.CanProcess(ExtractorType, url, ParserConfig)) { url = p.Process(url, ParserConfig); } @@ -735,9 +740,9 @@ namespace N_m3u8DL_RE.Parser.Extractor private void PreProcessContent() { - foreach (var p in ParserConfig.DASHContentProcessors) + foreach (var p in ParserConfig.ContentProcessors) { - if (p.CanProcess(MpdContent, ParserConfig)) + if (p.CanProcess(ExtractorType, MpdContent, ParserConfig)) { MpdContent = p.Process(MpdContent, ParserConfig); } diff --git a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs index 7a60393..e58a69f 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/HLSExtractor.cs @@ -17,6 +17,8 @@ namespace N_m3u8DL_RE.Parser.Extractor { internal class HLSExtractor : IExtractor { + public ExtractorType ExtractorType => ExtractorType.HLS; + private string M3u8Url = string.Empty; private string BaseUrl = string.Empty; private string M3u8Content = string.Empty; @@ -50,9 +52,9 @@ namespace N_m3u8DL_RE.Parser.Extractor throw new Exception(ResString.badM3u8); } - foreach (var p in ParserConfig.HLSContentProcessors) + foreach (var p in ParserConfig.ContentProcessors) { - if (p.CanProcess(M3u8Content, ParserConfig)) + if (p.CanProcess(ExtractorType, M3u8Content, ParserConfig)) { M3u8Content = p.Process(M3u8Content, ParserConfig); } @@ -64,9 +66,9 @@ namespace N_m3u8DL_RE.Parser.Extractor /// private string PreProcessUrl(string url) { - foreach (var p in ParserConfig.HLSUrlProcessors) + foreach (var p in ParserConfig.UrlProcessors) { - if (p.CanProcess(url, ParserConfig)) + if (p.CanProcess(ExtractorType, url, ParserConfig)) { url = p.Process(url, ParserConfig); } @@ -442,10 +444,11 @@ namespace N_m3u8DL_RE.Parser.Extractor private byte[] ParseKey(string method, string uriText) { - foreach (var p in ParserConfig.HLSKeyProcessors) + foreach (var p in ParserConfig.KeyProcessors) { - if (p.CanProcess(method, uriText, ParserConfig)) + if (p.CanProcess(ExtractorType, method, uriText, ParserConfig)) { + //匹配到对应处理器后不再继续 return p.Process(method, uriText, ParserConfig); } } diff --git a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs index de74826..8f1f68a 100644 --- a/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs +++ b/src/N_m3u8DL-RE.Parser/Extractor/IExtractor.cs @@ -5,11 +5,14 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using N_m3u8DL_RE.Common.Enum; namespace N_m3u8DL_RE.Parser.Extractor { internal interface IExtractor { + ExtractorType ExtractorType { get; } + ParserConfig ParserConfig { get; set; } Task> ExtractStreamsAsync(string rawText); diff --git a/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs index caa061a..60b0a87 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/ContentProcessor.cs @@ -1,4 +1,5 @@ -using N_m3u8DL_RE.Parser.Config; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Parser.Config; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ namespace N_m3u8DL_RE.Parser.Processor { public abstract class ContentProcessor { - public abstract bool CanProcess(string rawText, ParserConfig parserConfig); + public abstract bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig); public abstract string Process(string rawText, ParserConfig parserConfig); } } diff --git a/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs index a0a6b9c..aa69a94 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/DASH/DefaultDASHContentProcessor.cs @@ -1,4 +1,5 @@ -using N_m3u8DL_RE.Common.Log; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Parser.Config; using System; using System.Collections.Generic; @@ -13,8 +14,10 @@ namespace N_m3u8DL_RE.Parser.Processor.DASH /// public class DefaultDASHContentProcessor : ContentProcessor { - public override bool CanProcess(string mpdContent, ParserConfig parserConfig) + public override bool CanProcess(ExtractorType extractorType, string mpdContent, ParserConfig parserConfig) { + if (extractorType != ExtractorType.MPEG_DASH) return false; + if (mpdContent.Contains(" true; + + public override bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig paserConfig) => true; public override string Process(string oriUrl, ParserConfig paserConfig) { diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs index bcbe42e..fbcb25d 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSContentProcessor.cs @@ -1,4 +1,5 @@ -using N_m3u8DL_RE.Parser.Config; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Constants; using System; using System.Collections.Generic; @@ -11,7 +12,7 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS { public class DefaultHLSContentProcessor : ContentProcessor { - public override bool CanProcess(string rawText, ParserConfig parserConfig) => true; + public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) => extractorType == ExtractorType.HLS; public override string Process(string m3u8Content, ParserConfig parserConfig) { diff --git a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs index 6998e49..1b5fc46 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/HLS/DefaultHLSKeyProcessor.cs @@ -13,7 +13,8 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS { public class DefaultHLSKeyProcessor : KeyProcessor { - public override bool CanProcess(string method, string uriText, ParserConfig paserConfig) => true; + public override bool CanProcess(ExtractorType extractorType, string method, string uriText, ParserConfig paserConfig) => extractorType == ExtractorType.HLS; + public override byte[] Process(string method, string uriText, ParserConfig parserConfig) { @@ -43,9 +44,9 @@ namespace N_m3u8DL_RE.Parser.Processor.HLS /// private string PreProcessUrl(string url, ParserConfig parserConfig) { - foreach (var p in parserConfig.HLSUrlProcessors) + foreach (var p in parserConfig.UrlProcessors) { - if (p.CanProcess(url, parserConfig)) + if (p.CanProcess(ExtractorType.HLS, url, parserConfig)) { url = p.Process(url, parserConfig); } diff --git a/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs index 1991781..f2736d7 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/KeyProcessor.cs @@ -1,4 +1,5 @@ using N_m3u8DL_RE.Common.Entity; +using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Parser.Config; using System; using System.Collections.Generic; @@ -10,7 +11,7 @@ namespace N_m3u8DL_RE.Parser.Processor { public abstract class KeyProcessor { - public abstract bool CanProcess(string method, string uriText, ParserConfig parserConfig); + public abstract bool CanProcess(ExtractorType extractorType, string method, string uriText, ParserConfig parserConfig); public abstract byte[] Process(string method, string uriText, ParserConfig parserConfig); } } diff --git a/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs b/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs index 81a4986..6f3818e 100644 --- a/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs +++ b/src/N_m3u8DL-RE.Parser/Processor/UrlProcessor.cs @@ -1,4 +1,5 @@ -using N_m3u8DL_RE.Parser.Config; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Parser.Config; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ namespace N_m3u8DL_RE.Parser.Processor { public abstract class UrlProcessor { - public abstract bool CanProcess(string oriUrl, ParserConfig parserConfig); + public abstract bool CanProcess(ExtractorType extractorType, string oriUrl, ParserConfig parserConfig); public abstract string Process(string oriUrl, ParserConfig parserConfig); } } diff --git a/src/N_m3u8DL-RE.sln b/src/N_m3u8DL-RE.sln index 217b470..7f69da1 100644 --- a/src/N_m3u8DL-RE.sln +++ b/src/N_m3u8DL-RE.sln @@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N_m3u8DL-RE.Common", "N_m3u EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N_m3u8DL-RE.Parser", "N_m3u8DL-RE.Parser\N_m3u8DL-RE.Parser.csproj", "{0DA02925-AF3A-4598-AF01-91AE5539FCA1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "N_m3u8DL-RE.Extends", "N_m3u8DL-RE.Extends\N_m3u8DL-RE.Extends.csproj", "{99175570-6FE1-45C0-87BD-D2E1B52A35CC}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,10 +27,6 @@ Global {0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {0DA02925-AF3A-4598-AF01-91AE5539FCA1}.Release|Any CPU.Build.0 = Release|Any CPU - {99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99175570-6FE1-45C0-87BD-D2E1B52A35CC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/N_m3u8DL-RE/Crypto/AESUtil.cs b/src/N_m3u8DL-RE/Crypto/AESUtil.cs new file mode 100644 index 0000000..edb07ed --- /dev/null +++ b/src/N_m3u8DL-RE/Crypto/AESUtil.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Crypto +{ + internal class AESUtil + { + public static byte[] AES128Decrypt(string filePath, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + { + FileStream fs = new FileStream(filePath, FileMode.Open); + //获取文件大小 + long size = fs.Length; + byte[] inBuff = new byte[size]; + fs.Read(inBuff, 0, inBuff.Length); + fs.Close(); + + Aes dcpt = Aes.Create(); + dcpt.BlockSize = 128; + dcpt.KeySize = 128; + dcpt.Key = keyByte; + dcpt.IV = ivByte; + dcpt.Mode = mode; + dcpt.Padding = padding; + + ICryptoTransform cTransform = dcpt.CreateDecryptor(); + byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); + return resultArray; + } + + public static byte[] AES128Decrypt(byte[] encryptedBuff, byte[] keyByte, byte[] ivByte, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) + { + byte[] inBuff = encryptedBuff; + + Aes dcpt = Aes.Create(); + dcpt.BlockSize = 128; + dcpt.KeySize = 128; + dcpt.Key = keyByte; + dcpt.IV = ivByte; + dcpt.Mode = mode; + dcpt.Padding = padding; + + ICryptoTransform cTransform = dcpt.CreateDecryptor(); + byte[] resultArray = cTransform.TransformFinalBlock(inBuff, 0, inBuff.Length); + return resultArray; + } + } +} diff --git a/src/N_m3u8DL-RE/Crypto/CSChaCha20.cs b/src/N_m3u8DL-RE/Crypto/CSChaCha20.cs new file mode 100644 index 0000000..1fa2ca1 --- /dev/null +++ b/src/N_m3u8DL-RE/Crypto/CSChaCha20.cs @@ -0,0 +1,661 @@ +/* + * Copyright (c) 2015, 2018 Scott Bennett + * (c) 2018-2021 Kaarlo Räihä + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; // For MethodImplOptions.AggressiveInlining + +namespace CSChaCha20 +{ + /// + /// Class that can be used for ChaCha20 encryption / decryption + /// + public sealed class ChaCha20 : IDisposable + { + /// + /// Only allowed key lenght in bytes + /// + public const int allowedKeyLength = 32; + + /// + /// Only allowed nonce lenght in bytes + /// + public const int allowedNonceLength = 12; + + /// + /// How many bytes are processed per loop + /// + public const int processBytesAtTime = 64; + + private const int stateLength = 16; + + /// + /// The ChaCha20 state (aka "context") + /// + private readonly uint[] state = new uint[stateLength]; + + /// + /// Determines if the objects in this class have been disposed of. Set to true by the Dispose() method. + /// + private bool isDisposed = false; + + /// + /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens. + /// + /// + /// See ChaCha20 Spec Section 2.4 for a detailed description of the inputs. + /// + /// + /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers + /// + /// + /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers + /// + /// + /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer + /// + public ChaCha20(byte[] key, byte[] nonce, uint counter) + { + this.KeySetup(key); + this.IvSetup(nonce, counter); + } + +#if NET6_0_OR_GREATER + + /// + /// Set up a new ChaCha20 state. The lengths of the given parameters are checked before encryption happens. + /// + /// + /// See ChaCha20 Spec Section 2.4 for a detailed description of the inputs. + /// + /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers + /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers + /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer + public ChaCha20(ReadOnlySpan key, ReadOnlySpan nonce, uint counter) + { + this.KeySetup(key.ToArray()); + this.IvSetup(nonce.ToArray(), counter); + } + +#endif // NET6_0_OR_GREATER + + /// + /// The ChaCha20 state (aka "context"). Read-Only. + /// + public uint[] State + { + get + { + return this.state; + } + } + + + // These are the same constants defined in the reference implementation. + // http://cr.yp.to/streamciphers/timings/estreambench/submissions/salsa20/chacha8/ref/chacha.c + private static readonly byte[] sigma = Encoding.ASCII.GetBytes("expand 32-byte k"); + private static readonly byte[] tau = Encoding.ASCII.GetBytes("expand 16-byte k"); + + /// + /// Set up the ChaCha state with the given key. A 32-byte key is required and enforced. + /// + /// + /// A 32-byte (256-bit) key, treated as a concatenation of eight 32-bit little-endian integers + /// + private void KeySetup(byte[] key) + { + if (key == null) + { + throw new ArgumentNullException("Key is null"); + } + + if (key.Length != allowedKeyLength) + { + throw new ArgumentException($"Key length must be {allowedKeyLength}. Actual: {key.Length}"); + } + + state[4] = Util.U8To32Little(key, 0); + state[5] = Util.U8To32Little(key, 4); + state[6] = Util.U8To32Little(key, 8); + state[7] = Util.U8To32Little(key, 12); + + byte[] constants = (key.Length == allowedKeyLength) ? sigma : tau; + int keyIndex = key.Length - 16; + + state[8] = Util.U8To32Little(key, keyIndex + 0); + state[9] = Util.U8To32Little(key, keyIndex + 4); + state[10] = Util.U8To32Little(key, keyIndex + 8); + state[11] = Util.U8To32Little(key, keyIndex + 12); + + state[0] = Util.U8To32Little(constants, 0); + state[1] = Util.U8To32Little(constants, 4); + state[2] = Util.U8To32Little(constants, 8); + state[3] = Util.U8To32Little(constants, 12); + } + + /// + /// Set up the ChaCha state with the given nonce (aka Initialization Vector or IV) and block counter. A 12-byte nonce and a 4-byte counter are required. + /// + /// + /// A 12-byte (96-bit) nonce, treated as a concatenation of three 32-bit little-endian integers + /// + /// + /// A 4-byte (32-bit) block counter, treated as a 32-bit little-endian integer + /// + private void IvSetup(byte[] nonce, uint counter) + { + if (nonce == null) + { + // There has already been some state set up. Clear it before exiting. + Dispose(); + throw new ArgumentNullException("Nonce is null"); + } + + if (nonce.Length != allowedNonceLength) + { + // There has already been some state set up. Clear it before exiting. + Dispose(); + throw new ArgumentException($"Nonce length must be {allowedNonceLength}. Actual: {nonce.Length}"); + } + + state[12] = counter; + state[13] = Util.U8To32Little(nonce, 0); + state[14] = Util.U8To32Little(nonce, 4); + state[15] = Util.U8To32Little(nonce, 8); + } + + + #region Encryption methods + + /// + /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Output byte array, must have enough bytes + /// Input byte array + /// Number of bytes to encrypt + public void EncryptBytes(byte[] output, byte[] input, int numBytes) + { + this.WorkBytes(output, input, numBytes); + } + + /// + /// Encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) + /// + /// Output stream + /// Input stream + /// How many bytes to read and write at time, default is 1024 + public void EncryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) + { + this.WorkStreams(output, input, howManyBytesToProcessAtTime); + } + + /// + /// Async encrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) + /// + /// Output stream + /// Input stream + /// How many bytes to read and write at time, default is 1024 + public async Task EncryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) + { + await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime); + } + + /// + /// Encrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Output byte array, must have enough bytes + /// Input byte array + public void EncryptBytes(byte[] output, byte[] input) + { + this.WorkBytes(output, input, input.Length); + } + + /// + /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Input byte array + /// Number of bytes to encrypt + /// Byte array that contains encrypted bytes + public byte[] EncryptBytes(byte[] input, int numBytes) + { + byte[] returnArray = new byte[numBytes]; + this.WorkBytes(returnArray, input, numBytes); + return returnArray; + } + + /// + /// Encrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Input byte array + /// Byte array that contains encrypted bytes + public byte[] EncryptBytes(byte[] input) + { + byte[] returnArray = new byte[input.Length]; + this.WorkBytes(returnArray, input, input.Length); + return returnArray; + } + + /// + /// Encrypt string as UTF8 byte array, returns byte array that is allocated by method. + /// + /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform + /// Input string + /// Byte array that contains encrypted bytes + public byte[] EncryptString(string input) + { + byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(input); + byte[] returnArray = new byte[utf8Bytes.Length]; + + this.WorkBytes(returnArray, utf8Bytes, utf8Bytes.Length); + return returnArray; + } + + #endregion // Encryption methods + + + #region // Decryption methods + + /// + /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to the output buffer. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Output byte array + /// Input byte array + /// Number of bytes to decrypt + public void DecryptBytes(byte[] output, byte[] input, int numBytes) + { + this.WorkBytes(output, input, numBytes); + } + + /// + /// Decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) + /// + /// Output stream + /// Input stream + /// How many bytes to read and write at time, default is 1024 + public void DecryptStream(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) + { + this.WorkStreams(output, input, howManyBytesToProcessAtTime); + } + + /// + /// Async decrypt arbitrary-length byte stream (input), writing the resulting bytes to another stream (output) + /// + /// Output stream + /// Input stream + /// How many bytes to read and write at time, default is 1024 + public async Task DecryptStreamAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) + { + await this.WorkStreamsAsync(output, input, howManyBytesToProcessAtTime); + } + + /// + /// Decrypt arbitrary-length byte array (input), writing the resulting byte array to preallocated output buffer. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Output byte array, must have enough bytes + /// Input byte array + public void DecryptBytes(byte[] output, byte[] input) + { + WorkBytes(output, input, input.Length); + } + + /// + /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Input byte array + /// Number of bytes to encrypt + /// Byte array that contains decrypted bytes + public byte[] DecryptBytes(byte[] input, int numBytes) + { + byte[] returnArray = new byte[numBytes]; + WorkBytes(returnArray, input, numBytes); + return returnArray; + } + + /// + /// Decrypt arbitrary-length byte array (input), writing the resulting byte array that is allocated by method. + /// + /// Since this is symmetric operation, it doesn't really matter if you use Encrypt or Decrypt method + /// Input byte array + /// Byte array that contains decrypted bytes + public byte[] DecryptBytes(byte[] input) + { + byte[] returnArray = new byte[input.Length]; + WorkBytes(returnArray, input, input.Length); + return returnArray; + } + + /// + /// Decrypt UTF8 byte array to string. + /// + /// Here you can NOT swap encrypt and decrypt methods, because of bytes-string transform + /// Byte array + /// Byte array that contains encrypted bytes + public string DecryptUTF8ByteArray(byte[] input) + { + byte[] tempArray = new byte[input.Length]; + + WorkBytes(tempArray, input, input.Length); + return System.Text.Encoding.UTF8.GetString(tempArray); + } + + #endregion // Decryption methods + + private void WorkStreams(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) + { + int readBytes; + + byte[] inputBuffer = new byte[howManyBytesToProcessAtTime]; + byte[] outputBuffer = new byte[howManyBytesToProcessAtTime]; + + while ((readBytes = input.Read(inputBuffer, 0, howManyBytesToProcessAtTime)) > 0) + { + // Encrypt or decrypt + WorkBytes(output: outputBuffer, input: inputBuffer, numBytes: readBytes); + + // Write buffer + output.Write(outputBuffer, 0, readBytes); + } + } + + private async Task WorkStreamsAsync(Stream output, Stream input, int howManyBytesToProcessAtTime = 1024) + { + byte[] readBytesBuffer = new byte[howManyBytesToProcessAtTime]; + byte[] writeBytesBuffer = new byte[howManyBytesToProcessAtTime]; + int howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime); + + while (howManyBytesWereRead > 0) + { + // Encrypt or decrypt + WorkBytes(output: writeBytesBuffer, input: readBytesBuffer, numBytes: howManyBytesWereRead); + + // Write + await output.WriteAsync(writeBytesBuffer, 0, howManyBytesWereRead); + + // Read more + howManyBytesWereRead = await input.ReadAsync(readBytesBuffer, 0, howManyBytesToProcessAtTime); + } + } + + /// + /// Encrypt or decrypt an arbitrary-length byte array (input), writing the resulting byte array to the output buffer. The number of bytes to read from the input buffer is determined by numBytes. + /// + /// Output byte array + /// Input byte array + /// How many bytes to process + private void WorkBytes(byte[] output, byte[] input, int numBytes) + { + if (isDisposed) + { + throw new ObjectDisposedException("state", "The ChaCha state has been disposed"); + } + + if (input == null) + { + throw new ArgumentNullException("input", "Input cannot be null"); + } + + if (output == null) + { + throw new ArgumentNullException("output", "Output cannot be null"); + } + + if (numBytes < 0 || numBytes > input.Length) + { + throw new ArgumentOutOfRangeException("numBytes", "The number of bytes to read must be between [0..input.Length]"); + } + + if (output.Length < numBytes) + { + throw new ArgumentOutOfRangeException("output", $"Output byte array should be able to take at least {numBytes}"); + } + + uint[] x = new uint[stateLength]; // Working buffer + byte[] tmp = new byte[processBytesAtTime]; // Temporary buffer + int offset = 0; + + while (numBytes > 0) + { + // Copy state to working buffer + Buffer.BlockCopy(this.state, 0, x, 0, stateLength * sizeof(uint)); + + for (int i = 0; i < 10; i++) + { + QuarterRound(x, 0, 4, 8, 12); + QuarterRound(x, 1, 5, 9, 13); + QuarterRound(x, 2, 6, 10, 14); + QuarterRound(x, 3, 7, 11, 15); + + QuarterRound(x, 0, 5, 10, 15); + QuarterRound(x, 1, 6, 11, 12); + QuarterRound(x, 2, 7, 8, 13); + QuarterRound(x, 3, 4, 9, 14); + } + + for (int i = 0; i < stateLength; i++) + { + Util.ToBytes(tmp, Util.Add(x[i], this.state[i]), 4 * i); + } + + this.state[12] = Util.AddOne(state[12]); + if (this.state[12] <= 0) + { + /* Stopping at 2^70 bytes per nonce is the user's responsibility */ + this.state[13] = Util.AddOne(state[13]); + } + + // In case these are last bytes + if (numBytes <= processBytesAtTime) + { + for (int i = 0; i < numBytes; i++) + { + output[i + offset] = (byte)(input[i + offset] ^ tmp[i]); + } + + return; + } + + for (int i = 0; i < processBytesAtTime; i++) + { + output[i + offset] = (byte)(input[i + offset] ^ tmp[i]); + } + + numBytes -= processBytesAtTime; + offset += processBytesAtTime; + } + } + + /// + /// The ChaCha Quarter Round operation. It operates on four 32-bit unsigned integers within the given buffer at indices a, b, c, and d. + /// + /// + /// The ChaCha state does not have four integer numbers: it has 16. So the quarter-round operation works on only four of them -- hence the name. Each quarter round operates on four predetermined numbers in the ChaCha state. + /// See ChaCha20 Spec Sections 2.1 - 2.2. + /// + /// A ChaCha state (vector). Must contain 16 elements. + /// Index of the first number + /// Index of the second number + /// Index of the third number + /// Index of the fourth number + private static void QuarterRound(uint[] x, uint a, uint b, uint c, uint d) + { + x[a] = Util.Add(x[a], x[b]); + x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 16); + + x[c] = Util.Add(x[c], x[d]); + x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 12); + + x[a] = Util.Add(x[a], x[b]); + x[d] = Util.Rotate(Util.XOr(x[d], x[a]), 8); + + x[c] = Util.Add(x[c], x[d]); + x[b] = Util.Rotate(Util.XOr(x[b], x[c]), 7); + } + + #region Destructor and Disposer + + /// + /// Clear and dispose of the internal state. The finalizer is only called if Dispose() was never called on this cipher. + /// + ~ChaCha20() + { + Dispose(false); + } + + /// + /// Clear and dispose of the internal state. Also request the GC not to call the finalizer, because all cleanup has been taken care of. + /// + public void Dispose() + { + Dispose(true); + /* + * The Garbage Collector does not need to invoke the finalizer because Dispose(bool) has already done all the cleanup needed. + */ + GC.SuppressFinalize(this); + } + + /// + /// This method should only be invoked from Dispose() or the finalizer. This handles the actual cleanup of the resources. + /// + /// + /// Should be true if called by Dispose(); false if called by the finalizer + /// + private void Dispose(bool disposing) + { + if (!isDisposed) + { + if (disposing) + { + /* Cleanup managed objects by calling their Dispose() methods */ + } + + /* Cleanup any unmanaged objects here */ + Array.Clear(state, 0, stateLength); + } + + isDisposed = true; + } + + #endregion // Destructor and Disposer + } + + /// + /// Utilities that are used during compression + /// + public static class Util + { + /// + /// n-bit left rotation operation (towards the high bits) for 32-bit integers. + /// + /// + /// + /// The result of (v LEFTSHIFT c) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Rotate(uint v, int c) + { + unchecked + { + return (v << c) | (v >> (32 - c)); + } + } + + /// + /// Unchecked integer exclusive or (XOR) operation. + /// + /// + /// + /// The result of (v XOR w) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint XOr(uint v, uint w) + { + return unchecked(v ^ w); + } + + /// + /// Unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32. + /// + /// + /// See ChaCha20 Spec Section 2.1. + /// + /// + /// + /// The result of (v + w) modulo 2^32 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Add(uint v, uint w) + { + return unchecked(v + w); + } + + /// + /// Add 1 to the input parameter using unchecked integer addition. The ChaCha spec defines certain operations to use 32-bit unsigned integer addition modulo 2^32. + /// + /// + /// See ChaCha20 Spec Section 2.1. + /// + /// + /// The result of (v + 1) modulo 2^32 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint AddOne(uint v) + { + return unchecked(v + 1); + } + + /// + /// Convert four bytes of the input buffer into an unsigned 32-bit integer, beginning at the inputOffset. + /// + /// + /// + /// An unsigned 32-bit integer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint U8To32Little(byte[] p, int inputOffset) + { + unchecked + { + return ((uint)p[inputOffset] + | ((uint)p[inputOffset + 1] << 8) + | ((uint)p[inputOffset + 2] << 16) + | ((uint)p[inputOffset + 3] << 24)); + } + } + + /// + /// Serialize the input integer into the output buffer. The input integer will be split into 4 bytes and put into four sequential places in the output buffer, starting at the outputOffset. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToBytes(byte[] output, uint input, int outputOffset) + { + unchecked + { + output[outputOffset] = (byte)input; + output[outputOffset + 1] = (byte)(input >> 8); + output[outputOffset + 2] = (byte)(input >> 16); + output[outputOffset + 3] = (byte)(input >> 24); + } + } + } +} \ No newline at end of file diff --git a/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs b/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs new file mode 100644 index 0000000..0c02179 --- /dev/null +++ b/src/N_m3u8DL-RE/Crypto/ChaCha20Util.cs @@ -0,0 +1,43 @@ +using CSChaCha20; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Crypto +{ + internal class ChaCha20Util + { + public static byte[] DecryptPer1024Bytes(byte[] encryptedBuff, byte[] keyBytes, byte[] nonceBytes) + { + if (keyBytes.Length != 32) + throw new Exception("Key must be 32 bytes!"); + if (nonceBytes.Length != 12 && nonceBytes.Length != 8) + throw new Exception("Key must be 12 or 8 bytes!"); + if (nonceBytes.Length == 8) + nonceBytes = (new byte[4] { 0, 0, 0, 0 }).Concat(nonceBytes).ToArray(); + + var decStream = new MemoryStream(); + using BinaryReader reader = new BinaryReader(new MemoryStream(encryptedBuff)); + using (BinaryWriter writer = new BinaryWriter(decStream)) + while (true) + { + var buffer = reader.ReadBytes(1024); + byte[] dec = new byte[buffer.Length]; + if (buffer.Length > 0) + { + ChaCha20 forDecrypting = new ChaCha20(keyBytes, nonceBytes, 0); + forDecrypting.DecryptBytes(dec, buffer); + writer.Write(dec, 0, dec.Length); + } + else + { + break; + } + } + + return decStream.ToArray(); + } + } +} diff --git a/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj b/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj index 6f69e13..19ccf75 100644 --- a/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj +++ b/src/N_m3u8DL-RE/N_m3u8DL-RE.csproj @@ -13,7 +13,6 @@ - diff --git a/src/N_m3u8DL-RE/Processor/DemoProcessor.cs b/src/N_m3u8DL-RE/Processor/DemoProcessor.cs index f24c6a9..71ab689 100644 --- a/src/N_m3u8DL-RE/Processor/DemoProcessor.cs +++ b/src/N_m3u8DL-RE/Processor/DemoProcessor.cs @@ -1,4 +1,5 @@ -using N_m3u8DL_RE.Common.Log; +using N_m3u8DL_RE.Common.Enum; +using N_m3u8DL_RE.Common.Log; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Processor; using System; @@ -11,9 +12,10 @@ namespace N_m3u8DL_RE.Processor { internal class DemoProcessor : ContentProcessor { - public override bool CanProcess(string rawText, ParserConfig parserConfig) + + public override bool CanProcess(ExtractorType extractorType, string rawText, ParserConfig parserConfig) { - return parserConfig.Url.Contains("bitmovin"); + return extractorType == ExtractorType.MPEG_DASH && parserConfig.Url.Contains("bitmovin"); } public override string Process(string rawText, ParserConfig parserConfig) diff --git a/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs b/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs index a32737d..413c830 100644 --- a/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs +++ b/src/N_m3u8DL-RE/Processor/DemoProcessor2.cs @@ -1,5 +1,7 @@ using N_m3u8DL_RE.Common.Entity; +using N_m3u8DL_RE.Common.Enum; using N_m3u8DL_RE.Common.Log; +using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Parser.Config; using N_m3u8DL_RE.Parser.Processor; using N_m3u8DL_RE.Parser.Processor.HLS; @@ -13,15 +15,17 @@ namespace N_m3u8DL_RE.Processor { internal class DemoProcessor2 : KeyProcessor { - public override bool CanProcess(string method, string uriText, ParserConfig parserConfig) + public override bool CanProcess(ExtractorType extractorType, string method, string uriText, ParserConfig parserConfig) { - return parserConfig.Url.Contains("playertest.longtailvideo.com"); + return extractorType == ExtractorType.HLS && parserConfig.Url.Contains("playertest.longtailvideo.com"); } public override byte[] Process(string method, string uriText, ParserConfig parserConfig) { - Logger.InfoMarkUp("[white on green]My Key Processor![/]"); - return new DefaultHLSKeyProcessor().Process(method, uriText, parserConfig); + Logger.InfoMarkUp($"[white on green]My Key Processor => {uriText}[/]"); + var key = new DefaultHLSKeyProcessor().Process(method, uriText, parserConfig); + Logger.InfoMarkUp("[red]" + HexUtil.BytesToHex(key, " ") + "[/]"); + return key; } } } diff --git a/src/N_m3u8DL-RE/Program.cs b/src/N_m3u8DL-RE/Program.cs index ad68bce..a51b079 100644 --- a/src/N_m3u8DL-RE/Program.cs +++ b/src/N_m3u8DL-RE/Program.cs @@ -11,7 +11,7 @@ using N_m3u8DL_RE.Common.Log; using System.Globalization; using System.Text; using System.Text.RegularExpressions; -using N_m3u8DL_RE.Extends.Subtitle; +using N_m3u8DL_RE.Subtitle; using System.Collections.Concurrent; using N_m3u8DL_RE.Common.Util; using N_m3u8DL_RE.Processor; @@ -36,11 +36,12 @@ namespace N_m3u8DL_RE { var config = new ParserConfig(); //demo1 - config.DASHContentProcessors.Insert(0, new DemoProcessor()); + config.ContentProcessors.Insert(0, new DemoProcessor()); //demo2 - config.HLSKeyProcessors.Insert(0, new DemoProcessor2()); + config.KeyProcessors.Insert(0, new DemoProcessor2()); var url = string.Empty; + //url = "http://livesim.dashif.org/livesim/mup_300/tsbd_500/testpic_2s/Manifest.mpd"; url = "http://playertest.longtailvideo.com/adaptive/oceans_aes/oceans_aes.m3u8"; //url = "https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd"; diff --git a/src/N_m3u8DL-RE.Extends/Subtitle/WebVTTUtil.cs b/src/N_m3u8DL-RE/Subtitle/WebVTTUtil.cs similarity index 96% rename from src/N_m3u8DL-RE.Extends/Subtitle/WebVTTUtil.cs rename to src/N_m3u8DL-RE/Subtitle/WebVTTUtil.cs index fdb5eb9..c40154c 100644 --- a/src/N_m3u8DL-RE.Extends/Subtitle/WebVTTUtil.cs +++ b/src/N_m3u8DL-RE/Subtitle/WebVTTUtil.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace N_m3u8DL_RE.Extends.Subtitle +namespace N_m3u8DL_RE.Subtitle { public class WebVTTUtil {