From 3bb46cdfb1dfdf41ddb296294751830fe84bfd4a Mon Sep 17 00:00:00 2001 From: Roman Vanicek Date: Sat, 9 Nov 2024 01:02:06 +0100 Subject: [PATCH] Webshare download password generator --- .gitignore | 2 + JellyfinCinemaStream.csproj | 10 ++ JellyfinCinemaStream.sln | 25 ++++ Program.cs | 11 ++ Webshare/BaseEncoding.cs | 244 ++++++++++++++++++++++++++++++++++++ Webshare/LinkGenerator.cs | 138 ++++++++++++++++++++ 6 files changed, 430 insertions(+) create mode 100644 .gitignore create mode 100644 JellyfinCinemaStream.csproj create mode 100644 JellyfinCinemaStream.sln create mode 100644 Program.cs create mode 100644 Webshare/BaseEncoding.cs create mode 100644 Webshare/LinkGenerator.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e9693e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +obj +bin \ No newline at end of file diff --git a/JellyfinCinemaStream.csproj b/JellyfinCinemaStream.csproj new file mode 100644 index 0000000..fd4bd08 --- /dev/null +++ b/JellyfinCinemaStream.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/JellyfinCinemaStream.sln b/JellyfinCinemaStream.sln new file mode 100644 index 0000000..ee13ad8 --- /dev/null +++ b/JellyfinCinemaStream.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JellyfinCinemaStream", "JellyfinCinemaStream.csproj", "{7DE997A9-52B8-41E4-9958-1115BA3E481D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DE997A9-52B8-41E4-9958-1115BA3E481D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DE997A9-52B8-41E4-9958-1115BA3E481D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DE997A9-52B8-41E4-9958-1115BA3E481D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DE997A9-52B8-41E4-9958-1115BA3E481D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BC6836D6-55EE-4B26-84CB-1ED7014C362D} + EndGlobalSection +EndGlobal diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..b9c1be3 --- /dev/null +++ b/Program.cs @@ -0,0 +1,11 @@ +using System; +using JellyfinCinemaStream.Webshare; + +class Program +{ + private static void Main(string[] args) + { + string a = LinkGenerator.CalculatePassword(downloadId: "12kRBqAYQS2", downloadName: "bIaFGdkvKiiTBo2_S3E", salt: "UX8y8Fpa"); + Console.WriteLine(a); + } +} \ No newline at end of file diff --git a/Webshare/BaseEncoding.cs b/Webshare/BaseEncoding.cs new file mode 100644 index 0000000..fdacfe0 --- /dev/null +++ b/Webshare/BaseEncoding.cs @@ -0,0 +1,244 @@ +using System; +using System.Text; + +namespace JellyfinCinemaStream.Webshare; + +/// +/// Performs generic binary-to-text encoding. +/// +class BaseEncoding : Encoding +{ + int _bitCount; + int _bitMask; + string _characters; + bool _msbComesFirst; + Dictionary _values; + + /// + /// Defines a binary-to-text encoding. + /// + /// The characters of the encoding. + /// + /// true to begin with the most-significant bit of each byte. + /// Otherwise, the encoding begins with the least-significant bit. + /// + public BaseEncoding(string characterSet, bool msbComesFirst) + : this(characterSet, msbComesFirst, null) + { + } + + /// + /// Defines a binary-to-text encoding. + /// Additional decode characters let you add aliases, and a filter callback can be used + /// to make decoding case-insensitive among other things. + /// + /// The characters of the encoding. + /// + /// true to begin with the most-significant bit of each byte. + /// Otherwise, the encoding begins with the least-significant bit. + /// + /// + /// A dictionary of alias characters, or null if no aliases are desired. + /// + /// + /// A callback to map arbitrary characters onto the characters that can be decoded. + /// + public BaseEncoding(string characterSet, bool msbComesFirst, + IDictionary additionalDecodeCharacters) + { + if (characterSet == null) + throw new ArgumentNullException(); + + if (!IsPositivePowerOf2(characterSet.Length)) + { + throw new ArgumentException("Length must be a power of 2.", "characterSet"); + } + + if (characterSet.Length > 256) + { + throw new ArgumentException("Character sets with over 256 characters are not supported.", "characterSet"); + } + + _bitCount = 31 - CountLeadingZeros(characterSet.Length); + _bitMask = (1 << _bitCount) - 1; + _characters = characterSet; + _msbComesFirst = msbComesFirst; + + _values = additionalDecodeCharacters != null + ? new Dictionary(additionalDecodeCharacters) + : new Dictionary(); + for (int i = 0; i < characterSet.Length; i++) + { + char ch = characterSet[i]; + if (_values.ContainsKey(ch)) + { + throw new ArgumentException("Duplicate characters are not supported.", "characterSet"); + } + _values.Add(ch, (byte)i); + } + } + + /// + /// Gets the value corresponding to the specified character. + /// + /// A character. + /// A value, or -1 if the character is not part of the encoding. + public virtual int GetValue(char character) + { + int value; + return _values.TryGetValue(character, out value) ? value : -1; + } + + /// + /// Gets the character corresponding to the specified value. + /// + /// A value. + /// A character. + public virtual char GetChar(int value) + { + return _characters[value & BitMask]; + } + + /// + /// The bit mask for a single character in the current encoding. + /// + public int BitMask + { + get { return _bitMask; } + } + + /// + /// The number of bits per character in the current encoding. + /// + public int BitsPerCharacter + { + get { return _bitCount; } + } + + /// + /// true if the encoding begins with the most-significant bit of each byte. + /// Otherwise, the encoding begins with the least-significant bit. + /// + public bool MsbComesFirst + { + get { return _msbComesFirst; } + } + + public override int GetMaxCharCount(int byteCount) + { + if (byteCount < 0) + throw new ArgumentOutOfRangeException(); + + return checked((byteCount * 8 + BitsPerCharacter - 1) / BitsPerCharacter); + } + + /// + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + int charCount = GetCharCount(bytes, byteIndex, byteCount); + + return GetChars(bytes, byteIndex, byteCount, chars, charIndex, charCount); + } + + /// + /// Converts bytes from their binary representation to a text representation. + /// + /// An input array of bytes. + /// The index of the first byte. + /// The number of bytes to read. + /// An output array of characters. + /// The index of the first character. + /// The number of characters to write. + /// The number of characters written. + public int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex, int charCount) + { + if (charIndex < 0 || charCount < 0 || chars.Length - charIndex < charCount + || byteIndex < 0 || byteCount < 0 || bytes.Length - byteIndex < byteCount) + throw new ArgumentOutOfRangeException(); + + int byteEnd = checked(byteIndex + byteCount); + + int bitStartOffset = 0; + for (int i = 0; i < charCount; i++) + { + byte value; + + byte thisByte = byteIndex + 0 < byteEnd ? bytes[byteIndex + 0] : (byte)0; + byte nextByte = byteIndex + 1 < byteEnd ? bytes[byteIndex + 1] : (byte)0; + + int bitEndOffset = bitStartOffset + BitsPerCharacter; + if (MsbComesFirst) + { + value = ShiftRight(thisByte, 8 - bitStartOffset - BitsPerCharacter); + if (bitEndOffset > 8) + { + value |= ShiftRight(nextByte, 16 - bitStartOffset - BitsPerCharacter); + } + } + else + { + value = ShiftRight(thisByte, bitStartOffset); + if (bitEndOffset > 8) + { + value |= ShiftRight(nextByte, bitStartOffset - 8); + } + } + + bitStartOffset = bitEndOffset; + if (bitStartOffset >= 8) + { + bitStartOffset -= 8; byteIndex++; + } + + chars[i] = GetChar(value); + } + + return charCount; + } + + /// + public override int GetCharCount(byte[] bytes, int index, int count) + { + if (index < 0 || count < 0 || bytes.Length - index < count) + throw new ArgumentOutOfRangeException(); + + return GetMaxCharCount(count); + } + + public override int GetByteCount(char[] chars, int index, int count) + { + throw new NotSupportedException(); + } + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + throw new NotSupportedException(); + } + + public override int GetMaxByteCount(int charCount) + { + throw new NotSupportedException(); + } + + private static bool IsPositivePowerOf2(int value) + { + return 0 < value && 0 == (value & (value - 1)); + } + + private static int CountLeadingZeros(int value) + { + int count; + for (count = 0; count < 32 && 0 == (value & (0x80000000 >> count)); count++) ; + return count; + } + + private static byte ShiftLeft(byte value, int bits) + { + return (byte)(bits > 0 ? value << bits : value >> (-bits)); + } + + private static byte ShiftRight(byte value, int bits) + { + return ShiftLeft(value, -bits); + } +} diff --git a/Webshare/LinkGenerator.cs b/Webshare/LinkGenerator.cs new file mode 100644 index 0000000..ee6920f --- /dev/null +++ b/Webshare/LinkGenerator.cs @@ -0,0 +1,138 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace JellyfinCinemaStream.Webshare; + +public class LinkGenerator +{ + private static readonly BaseEncoding UnixMD5 = new BaseEncoding("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", false); + + /// + /// Calculates the Webshare download password from download identifier, name and salt. + /// + /// Webshare download identifier. + /// Webshare download name. + /// Download salt obtained from https://webshare.cz/api/file_password_salt/. + /// Password to use to obtain a download link using https://webshare.cz/api/file_link/. + public static string CalculatePassword(string downloadId, string downloadName, string salt) + { + if (downloadId == null || downloadName == null || salt == null) + throw new ArgumentNullException(); + + // Example + // name = "bIaFGdkvKiiTBo2_S3E" + // id = "12kRBqAYQS2" + // password = sha256(name + id) = "1e98873b57b7660cba7267c191f2d1fb7d198d062eb2fe0686502883042c4105" + // crypt = md5crypt(password, salt: "UX8y8Fpa") = "$1$UX8y8Fpa$stY.bUYGBsmAlc6IIxnHU0" + // result = sha1(crypt) = "8ceff8e5abf9aa375a8a5b05871b6cd6fb7fd185") + string toHash = downloadName + downloadId; + byte[] password = SHA256.HashData(Encoding.UTF8.GetBytes(toHash)); + string passwordS = Convert.ToHexStringLower(password); + string crypt = MD5Crypt(passwordS, salt); + byte[] result = SHA1.HashData(Encoding.ASCII.GetBytes(crypt)); + return Convert.ToHexStringLower(result); + } + + private static string MD5Crypt(string password, string salt) + { + string prefixString = "$1$"; + byte[] prefixBytes = Encoding.ASCII.GetBytes(prefixString); + byte[] saltBytes = Encoding.ASCII.GetBytes(salt); + + byte[] key = Encoding.ASCII.GetBytes(password); + byte[] truncatedSalt = TruncateAndCopy(saltBytes, 8); + byte[] crypt = Crypt(key, truncatedSalt, prefixBytes, MD5.Create()); + + string result = prefixString + + salt + '$' + + UnixMD5.GetString(crypt); + + return result.TrimEnd('='); + } + + private static byte[] Crypt(byte[] key, byte[] salt, byte[] prefix, HashAlgorithm A) + { + byte[] H = null, I = null; + + A.Initialize(); + AddToDigest(A, key); + AddToDigest(A, salt); + AddToDigest(A, key); + FinishDigest(A); + + I = (byte[])A.Hash.Clone(); + + A.Initialize(); + AddToDigest(A, key); + AddToDigest(A, prefix); + AddToDigest(A, salt); + + AddToDigestRolling(A, I, 0, I.Length, key.Length); + + int length = key.Length; + for (int i = 0; i < 31 && length != 0; i++) + { + AddToDigest(A, new[] { (length & (1 << i)) != 0 ? (byte)0 : key[0] }); + length &= ~(1 << i); + } + FinishDigest(A); + + H = (byte[])A.Hash.Clone(); + + for (int i = 0; i < 1000; i++) + { + A.Initialize(); + if ((i & 1) != 0) { AddToDigest(A, key); } + if ((i & 1) == 0) { AddToDigest(A, H); } + if ((i % 3) != 0) { AddToDigest(A, salt); } + if ((i % 7) != 0) { AddToDigest(A, key); } + if ((i & 1) != 0) { AddToDigest(A, H); } + if ((i & 1) == 0) { AddToDigest(A, key); } + FinishDigest(A); + + Array.Copy(A.Hash, H, H.Length); + } + + byte[] crypt = new byte[H.Length]; + int[] permutation = new[] { 11, 4, 10, 5, 3, 9, 15, 2, 8, 14, 1, 7, 13, 0, 6, 12 }; + Array.Reverse(permutation); + for (int i = 0; i < crypt.Length; i++) + { + crypt[i] = H[permutation[i]]; + } + + return crypt; + } + + private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer) + { + AddToDigest(algorithm, buffer, 0, buffer.Length); + } + + private static void AddToDigest(HashAlgorithm algorithm, byte[] buffer, int offset, int count) + { + algorithm.TransformBlock(buffer, offset, count, buffer, offset); + } + + private static void AddToDigestRolling(HashAlgorithm algorithm, byte[] buffer, int offset, int inputCount, int outputCount) + { + int count; + for (count = 0; count < outputCount; count += inputCount) + { + AddToDigest(algorithm, buffer, offset, Math.Min(outputCount - count, inputCount)); + } + } + + private static void FinishDigest(HashAlgorithm algorithm) + { + algorithm.TransformFinalBlock(new byte[0], 0, 0); + } + + private static byte[] TruncateAndCopy(byte[] buffer, int maxLength) + { + byte[] truncatedBuffer = new byte[Math.Min(buffer.Length, maxLength)]; + Array.Copy(buffer, truncatedBuffer, truncatedBuffer.Length); + return truncatedBuffer; + } +}