diff --git a/LyndaDecryptor.sln b/LyndaDecryptor.sln index 6f73b37..cf9467b 100644 --- a/LyndaDecryptor.sln +++ b/LyndaDecryptor.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LyndaDecryptor", "LyndaDecryptor\LyndaDecryptor.csproj", "{58EBF661-A637-45AE-956C-5EC1A602EDC3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LyndaDecryptorTests", "LyndaDecryptorTests\LyndaDecryptorTests.csproj", "{F8726170-D017-4517-AFB9-B1870A282E81}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,6 +20,12 @@ Global {58EBF661-A637-45AE-956C-5EC1A602EDC3}.Release|Any CPU.Build.0 = Release|Any CPU {58EBF661-A637-45AE-956C-5EC1A602EDC3}.TestDB|Any CPU.ActiveCfg = TestDB|Any CPU {58EBF661-A637-45AE-956C-5EC1A602EDC3}.TestDB|Any CPU.Build.0 = TestDB|Any CPU + {F8726170-D017-4517-AFB9-B1870A282E81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8726170-D017-4517-AFB9-B1870A282E81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8726170-D017-4517-AFB9-B1870A282E81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8726170-D017-4517-AFB9-B1870A282E81}.Release|Any CPU.Build.0 = Release|Any CPU + {F8726170-D017-4517-AFB9-B1870A282E81}.TestDB|Any CPU.ActiveCfg = Release|Any CPU + {F8726170-D017-4517-AFB9-B1870A282E81}.TestDB|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LyndaDecryptor/App.config b/LyndaDecryptor/App.config index 339e783..211f09a 100644 --- a/LyndaDecryptor/App.config +++ b/LyndaDecryptor/App.config @@ -1,29 +1,14 @@ - + -
- - + - - - - - - - - - - - - - - - + + - \ No newline at end of file + diff --git a/LyndaDecryptor/Decryptor.cs b/LyndaDecryptor/Decryptor.cs new file mode 100644 index 0000000..77ac9e9 --- /dev/null +++ b/LyndaDecryptor/Decryptor.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using static LyndaDecryptor.Utils; + +namespace LyndaDecryptor +{ + public class Decryptor + { + #region Fields & Properties + + // Cryptographics + RijndaelManaged RijndaelInstace; + byte[] KeyBytes; + + // Database Connection + SQLiteConnection DatabaseConnection; + + // Threading + List TaskList = new List(); + SemaphoreSlim Semaphore = new SemaphoreSlim(5); + object SemaphoreLock = new object(); + + // IO + List InvalidPathCharacters = new List(), InvalidFileCharacters = new List(); + DirectoryInfo OutputDirectory = null; + + // Decryptor Options + public DecryptorOptions Options = new DecryptorOptions(); + + #endregion + + public Decryptor() + { + InvalidPathCharacters.AddRange(Path.GetInvalidPathChars()); + InvalidPathCharacters.AddRange(new char[] { ':', '?', '"', '\\', '/' }); + + InvalidFileCharacters.AddRange(Path.GetInvalidFileNameChars()); + InvalidFileCharacters.AddRange(new char[] { ':', '?', '"', '\\', '/' }); + } + + /// + /// Constructs an object with decryptor options
+ /// If specified this constructor inits the database + ///
+ /// + public Decryptor(DecryptorOptions options) : this() + { + Options = options; + + if (options.UseDatabase) + Options.UseDatabase = InitDB(options.DatabasePath); + } + + #region Methods + + /// + /// Create the RSA Instance and EncryptedKeyBytes + /// + /// secret cryptographic key + public void InitDecryptor(string EncryptionKey) + { + WriteToConsole("[START] Init Decryptor..."); + RijndaelInstace = new RijndaelManaged + { + KeySize = 0x80, + Padding = PaddingMode.Zeros + }; + + KeyBytes = new ASCIIEncoding().GetBytes(EncryptionKey); + WriteToConsole("[START] Decryptor successful initalized!" + Environment.NewLine, ConsoleColor.Green); + } + + /// + /// Create a SqliteConnection to the specified or default application database. + /// + /// Path to database file + /// true if init was successful + public bool InitDB(string databasePath) + { + WriteToConsole("[DB] Creating db connection..."); + + // Check for databasePath + if (string.IsNullOrEmpty(databasePath)) + { + // Try to figure out default app db path + var AppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "lynda.com", "video2brain Desktop App"); + + if (!Directory.Exists(AppPath)) + AppPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "lynda.com", "lynda.com Desktop App"); + + // Find db file or databasePath = default(string) + databasePath = Directory.EnumerateFiles(AppPath, "*.sqlite", SearchOption.AllDirectories).FirstOrDefault(); + } + + // Check if databasePath is present (specific or default) + if (!string.IsNullOrEmpty(databasePath)) + { + DatabaseConnection = new SQLiteConnection($"Data Source={databasePath}; Version=3;FailIfMissing=True"); + DatabaseConnection.Open(); + + WriteToConsole("[DB] DB successfully connected and opened!" + Environment.NewLine, ConsoleColor.Green); + return true; + } + else + { + WriteToConsole("[DB] Couldn't find db file!" + Environment.NewLine, ConsoleColor.Red); + return false; + } + } + + /// + /// Decrypt all files in a given folder + /// + /// path to folder with encrypted .lynda files + /// specify an output folder + public void DecryptAll(string folderPath, string outputFolder = "") + { + if (!Directory.Exists(folderPath)) + throw new DirectoryNotFoundException(); + + if (!string.IsNullOrWhiteSpace(outputFolder)) + OutputDirectory = Directory.Exists(outputFolder) ? new DirectoryInfo(outputFolder) : Directory.CreateDirectory(outputFolder); + + foreach (string entry in Directory.EnumerateFiles(folderPath, "*.lynda", SearchOption.AllDirectories)) + { + string newPath = string.Empty; + var item = entry; + + if (Options.UseDatabase) + { + try + { + // get metadata with courseID and videoID + var videoInfo = GetVideoInfoFromDB(new DirectoryInfo(Path.GetDirectoryName(item)).Name, Path.GetFileName(item).Split('_')[0]); + + if(videoInfo != null) + { + // create new path and folder + var complexTitle = $"E{videoInfo.VideoIndex} - {videoInfo.VideoTitle}.mp4"; + var simpleTitle = $"E{videoInfo.VideoIndex}.mp4"; + + newPath = Path.Combine(OutputDirectory.FullName, CleanPath(videoInfo.CourseTitle), + CleanPath(videoInfo.ChapterTitle), CleanPath(complexTitle)); + + if (newPath.Length > 240) + { + newPath = Path.Combine(OutputDirectory.FullName, CleanPath(videoInfo.CourseTitle), + CleanPath(videoInfo.ChapterTitle), CleanPath(simpleTitle)); + } + + if (!Directory.Exists(Path.GetDirectoryName(newPath))) + Directory.CreateDirectory(Path.GetDirectoryName(newPath)); + } + } + catch (Exception e) + { + WriteToConsole($"[ERR] Could not retrive media information from database! Exception: {e.Message} Falling back to default behaviour!", ConsoleColor.Yellow); + } + } + + if(String.IsNullOrWhiteSpace(newPath)) + { + newPath = Path.ChangeExtension(item, ".mp4"); + } + + Semaphore.Wait(); + TaskList.Add(Task.Run(() => + { + Decrypt(item, newPath); + lock (SemaphoreLock) + { + Semaphore.Release(); + } + })); + } + + Task.WhenAll(TaskList).Wait(); + WriteToConsole("Decryption completed!", ConsoleColor.DarkGreen); + } + + /// + /// Decrypt a single encrypted file into decrypted file path + /// + /// Path to encrypted file + /// Path to decrypted file + /// Remove encrypted file after decryption? + public void Decrypt(string encryptedFilePath, string decryptedFilePath) + { + if (!File.Exists(encryptedFilePath)) + { + WriteToConsole("[ERR] Couldn't find encrypted file...", ConsoleColor.Red); + return; + } + + FileInfo encryptedFileInfo = new FileInfo(encryptedFilePath); + + if (File.Exists(decryptedFilePath)) + { + FileInfo decryptedFileInfo = new FileInfo(decryptedFilePath); + + if (decryptedFileInfo.Length == encryptedFileInfo.Length) + { + WriteToConsole("[DEC] File " + decryptedFilePath + " exists already and will be skipped!", ConsoleColor.Yellow); + return; + } + else + WriteToConsole("[DEC] File " + decryptedFilePath + " exists already but seems to differ in size...", ConsoleColor.Blue); + + decryptedFileInfo = null; + } + + + byte[] buffer = new byte[0x50000]; + + if (encryptedFileInfo.Extension != ".lynda") + { + WriteToConsole("[ERR] Couldn't load file: " + encryptedFilePath, ConsoleColor.Red); + return; + } + + using (FileStream inStream = new FileStream(encryptedFilePath, FileMode.Open)) + { + using (CryptoStream decryptionStream = new CryptoStream(inStream, RijndaelInstace.CreateDecryptor(KeyBytes, KeyBytes), CryptoStreamMode.Read)) + using (FileStream outStream = new FileStream(decryptedFilePath, FileMode.Create)) + { + WriteToConsole("[DEC] Decrypting file " + encryptedFileInfo.Name + "..."); + + while ((inStream.Length - inStream.Position) >= buffer.Length) + { + decryptionStream.Read(buffer, 0, buffer.Length); + outStream.Write(buffer, 0, buffer.Length); + } + + buffer = new byte[inStream.Length - inStream.Position]; + decryptionStream.Read(buffer, 0, buffer.Length); + outStream.Write(buffer, 0, buffer.Length); + outStream.Flush(); + outStream.Close(); + + WriteToConsole("[DEC] File decryption completed: " + decryptedFilePath, ConsoleColor.DarkGreen); + } + + inStream.Close(); + buffer = null; + } + + if (Options.RemoveFilesAfterDecryption) + encryptedFileInfo.Delete(); + + encryptedFileInfo = null; + } + + /// + /// Retrive video infos from the database + /// + /// course id + /// video id + /// VideoInfo instance or null + private VideoInfo GetVideoInfoFromDB(string courseID, string videoID) + { + VideoInfo videoInfo = null; + + try + { + var cmd = DatabaseConnection.CreateCommand(); + + // Query all required tables and fields from the database + cmd.CommandText = @"SELECT Video.ID, Video.ChapterId, Video.CourseId, + Video.Title, Filename, Course.Title as CourseTitle, + Video.SortIndex, Chapter.Title as ChapterTitle, + Chapter.SortIndex as ChapterIndex + FROM Video, Course, Chapter + WHERE Video.ChapterId = Chapter.ID + AND Course.ID = Video.CourseId + AND Video.CourseId = @courseId + AND Video.ID = @videoId"; + + cmd.Parameters.Add(new SQLiteParameter("@courseId", courseID)); + cmd.Parameters.Add(new SQLiteParameter("@videoId", videoID)); + + var reader = cmd.ExecuteReader(); + + if (reader.Read()) + { + videoInfo = new VideoInfo(); + + videoInfo.CourseTitle = reader.GetString(reader.GetOrdinal("CourseTitle")); + videoInfo.ChapterTitle = reader.GetString(reader.GetOrdinal("ChapterTitle")); + videoInfo.ChapterIndex = reader.GetInt32(reader.GetOrdinal("ChapterIndex")); + videoInfo.VideoIndex = reader.GetInt32(reader.GetOrdinal("SortIndex")); + videoInfo.VideoTitle = reader.GetString(reader.GetOrdinal("Title")); + + videoInfo.ChapterTitle = $"{videoInfo.ChapterIndex} - {videoInfo.ChapterTitle}"; + + videoInfo.VideoID = videoID; + videoInfo.CourseID = courseID; + } + } + catch (Exception e) + { + WriteToConsole($"[ERR] Exception occured during db query ({courseID}/{videoID}): {e.Message}", ConsoleColor.Yellow); + videoInfo = null; + } + + return videoInfo; + } + + /// + /// Clean the input string and remove all invalid chars + /// + /// input path + /// + private string CleanPath(string path) + { + foreach (var invalidChar in InvalidPathCharacters) + path = path.Replace(invalidChar, '-'); + + return path; + } + + #endregion + } +} diff --git a/LyndaDecryptor/DecryptorOptions.cs b/LyndaDecryptor/DecryptorOptions.cs new file mode 100644 index 0000000..24b822f --- /dev/null +++ b/LyndaDecryptor/DecryptorOptions.cs @@ -0,0 +1,15 @@ +namespace LyndaDecryptor +{ + public class DecryptorOptions + { + public Mode UsageMode { get; set; } + public bool UseDatabase { get; set; } + public bool UseOutputFolder { get; set; } + public bool RemoveFilesAfterDecryption { get; set; } + + public string InputPath { get; set; } + public string OutputPath { get; set; } + public string OutputFolder { get; set; } + public string DatabasePath { get; set; } + } +} diff --git a/LyndaDecryptor/LyndaDecryptor.csproj b/LyndaDecryptor/LyndaDecryptor.csproj index 4f2adad..b267fde 100644 --- a/LyndaDecryptor/LyndaDecryptor.csproj +++ b/LyndaDecryptor/LyndaDecryptor.csproj @@ -9,7 +9,7 @@ Properties LyndaDecryptor LyndaDecryptor - v4.5 + v4.5.2 512 true @@ -71,21 +71,35 @@ lynda-logo.ico - - + + ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\System.dll + + + ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\System.Core.dll + ..\packages\System.Data.SQLite.Core.1.0.102.0\lib\net45\System.Data.SQLite.dll True - + + ..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\System.Data.dll + + + + + - - + + Designer + + + Designer + diff --git a/LyndaDecryptor/Program.cs b/LyndaDecryptor/Program.cs index acc4987..996145b 100644 --- a/LyndaDecryptor/Program.cs +++ b/LyndaDecryptor/Program.cs @@ -7,133 +7,48 @@ using System.Security.Cryptography; using System.Threading; +using static LyndaDecryptor.Utils; + namespace LyndaDecryptor { - [Flags] - enum Mode + public enum Mode { - None = 1, - Single = 2, - Folder = 4, - DB_Usage = 8, - RemoveFiles = 16, - SpecialOutput = 32 + None = 0, + File, + Folder }; - class Program + public class Program { - static List invalidPathChars = new List(), invalidFileChars = new List(); - static char[] partTwo = new char[] { '\'', '*', '\x00b2', '"', 'C', '\x00b4', '|', '\x00a7', '\\' }; - static string ENCRYPTION_KEY = "~h#\x00b0" + new string(partTwo) + "3~."; - static SQLiteConnection sqlite_db_connection; - - static RijndaelManaged rijndael; - static byte[] enc_key_bytes; - - static ConsoleColor color_default = Console.ForegroundColor; - static Mode usage_mode = Mode.None; - - static string directory, file_source, file_destination, db_path = string.Empty, out_path = string.Empty; - static object console_lock = new object(); - static SemaphoreSlim semaphore = new SemaphoreSlim(5); - static object sem_lock = new object(); - - static void Main(string[] args) { + Decryptor decryptor; + DecryptorOptions decryptorOptions = new DecryptorOptions(); + try { - invalidPathChars.AddRange(Path.GetInvalidPathChars()); - invalidPathChars.AddRange(new char[] { ':', '?', '"', '\\', '/' }); - invalidFileChars.AddRange(Path.GetInvalidFileNameChars()); - invalidFileChars.AddRange(new char[] { ':', '?', '"', '\\', '/' }); - - int arg_index = 0; + decryptorOptions = ParseCommandLineArgs(args); + decryptor = new Decryptor(decryptorOptions); - foreach (string arg in args) - { - if (string.IsNullOrWhiteSpace(arg)) - { - arg_index++; - continue; - } - - switch (arg.ToUpper()) - { - case "/D": - if (Directory.Exists(args[arg_index + 1])) - { - directory = args[arg_index + 1]; - usage_mode = Mode.Folder; - WriteToConsole("[ARGS] Changing mode to Folder decryption!", ConsoleColor.Yellow); - } - else - { - WriteToConsole("[ARGS] The directory path is missing..." + Environment.NewLine, ConsoleColor.Red); - Usage(); - goto End; - } - break; - - case "/F": - if (File.Exists(args[arg_index + 1]) && !string.IsNullOrWhiteSpace(args[arg_index + 2]) && !File.Exists(args[arg_index + 2])) - { - file_source = args[arg_index + 1]; - file_destination = args[arg_index + 2]; - usage_mode = Mode.Single; - WriteToConsole("[ARGS] Changing mode to Single decryption!", ConsoleColor.Yellow); - - } - else - { - WriteToConsole("[ARGS] Some relevant args are missing..." + Environment.NewLine, ConsoleColor.Red); - Usage(); - goto End; - } - break; - - case "/DB": - usage_mode |= Mode.DB_Usage; - - if (args.Length-1 > arg_index && File.Exists(args[arg_index + 1])) - db_path = args[arg_index + 1]; - break; - - case "/RM": - usage_mode |= Mode.RemoveFiles; - WriteToConsole("[ARGS] Removing files after decryption..." + Environment.NewLine, ConsoleColor.Yellow); - WriteToConsole("[ARGS] Press any key to continue..." + Environment.NewLine, ConsoleColor.Yellow); - Console.ReadKey(); - break; - - case "/OUT": - usage_mode |= Mode.SpecialOutput; - - if (args.Length - 1 > arg_index) - out_path = args[arg_index + 1]; - break; - } - - arg_index++; - } - - if((usage_mode & Mode.None) == Mode.None) + if(decryptorOptions.UsageMode == Mode.None) { Usage(); goto End; } - else - InitDecryptor(); - - if ((usage_mode & Mode.DB_Usage) == Mode.DB_Usage) - InitDB(); - - if ((usage_mode & Mode.Folder) == Mode.Folder) - DecryptAll(directory, out_path, (usage_mode & Mode.DB_Usage) == Mode.DB_Usage); - else if ((usage_mode & Mode.Single) == Mode.Single) - Decrypt(file_source, file_destination); + else if(decryptorOptions.RemoveFilesAfterDecryption) + { + WriteToConsole("[ARGS] Removing files after decryption." + Environment.NewLine, ConsoleColor.Yellow); + WriteToConsole("[ARGS] Press any key to continue or CTRL + C to break..." + Environment.NewLine, ConsoleColor.Yellow); + Console.ReadKey(); + } + decryptor.InitDecryptor(ENCRYPTION_KEY); + + if (decryptorOptions.UsageMode == Mode.Folder) + decryptor.DecryptAll(decryptorOptions.InputPath, decryptorOptions.OutputPath); + else if (decryptorOptions.UsageMode == Mode.File) + decryptor.Decrypt(decryptorOptions.InputPath, decryptorOptions.OutputPath); } catch (Exception e) { @@ -143,50 +58,11 @@ static void Main(string[] args) End: WriteToConsole(Environment.NewLine + "Press any key to exit the program..."); Console.ReadKey(); - - if (sqlite_db_connection != null && sqlite_db_connection.State == System.Data.ConnectionState.Open) - sqlite_db_connection.Close(); - } - - private static void InitDB() - { - WriteToConsole("[DB] Creating db connection..."); - - if (string.IsNullOrEmpty(db_path)) - { - var files = Directory.EnumerateFiles(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "lynda.com", "video2brain Desktop App"), "*.sqlite", SearchOption.AllDirectories); - - foreach (string path in files) - { - db_path = path; - } - } - - if (!string.IsNullOrEmpty(db_path)) - { - sqlite_db_connection = new SQLiteConnection($"Data Source={db_path}; Version=3;FailIfMissing=True"); - sqlite_db_connection.Open(); - - WriteToConsole("[DB] DB successfully connected and opened!" + Environment.NewLine, ConsoleColor.Green); - } - else - WriteToConsole("[DB] Couldn't find db file!" + Environment.NewLine, ConsoleColor.Red); - } - - private static void InitDecryptor() - { - WriteToConsole("[START] Init Decryptor..."); - rijndael = new RijndaelManaged - { - KeySize = 0x80, - Padding = PaddingMode.Zeros - }; - - enc_key_bytes = new ASCIIEncoding().GetBytes(ENCRYPTION_KEY); - - WriteToConsole("[START] Decryptor successful initalized!" + Environment.NewLine, ConsoleColor.Green); } + /// + /// Print usage instructions + /// static void Usage() { Console.WriteLine("Usage (Directory): LyndaDecryptor /D PATH_TO_FOLDER [OPTIONS]"); @@ -195,195 +71,9 @@ static void Usage() Console.WriteLine(Environment.NewLine + Environment.NewLine + "Flags: "); Console.WriteLine("\t/D\tSource files are located in a folder."); Console.WriteLine("\t/F\tSource and Destination file are specified."); - Console.WriteLine("\t/DB\tSearch for Database or specify the location on your system."); + Console.WriteLine("\t/DB [PATH]\tSearch for Database or specify the location on your system."); Console.WriteLine("\t/RM\tRemoves all files after decryption is complete."); - Console.WriteLine("\t/OUT\tSpecifies an output directory instead of using default directory."); - } - - static async void DecryptAll(string folderPath, string outputFolder = "", bool useDB = false) - { - if (!Directory.Exists(folderPath)) - throw new DirectoryNotFoundException(); - - List taskList = new List(); - - foreach (string entry in Directory.EnumerateFiles(folderPath, "*.lynda", SearchOption.AllDirectories)) - { - string newPath = outputFolder; - var item = entry; - - if (useDB) - { - DirectoryInfo containingDir; - DirectoryInfo info = new DirectoryInfo(Path.GetDirectoryName(item)); - string videoID = Path.GetFileName(item).Split('_')[0]; - - if (!string.IsNullOrWhiteSpace(outputFolder)) - { - if (Directory.Exists(outputFolder)) - containingDir = new DirectoryInfo(outputFolder); - else - containingDir = Directory.CreateDirectory(outputFolder); - } - else - containingDir = info; - - var cmd = sqlite_db_connection.CreateCommand(); - - //Changed the query to get course title - cmd.CommandText = @"SELECT Video.ID, Video.ChapterId, Video.CourseId, Video.Title, Filename, Course.Title as CourseTitle, Video.SortIndex, Chapter.Title as ChapterTitle, Chapter.SortIndex as ChapterIndex - FROM Video, Course, Chapter - WHERE Video.ChapterId = Chapter.ID - AND Course.ID = Video.CourseId - AND Video.CourseId = @courseId - AND Video.ID = @videoId"; - - cmd.Parameters.Add(new SQLiteParameter("@courseId", info.Name)); - cmd.Parameters.Add(new SQLiteParameter("@videoId", videoID)); - - var reader = cmd.ExecuteReader(System.Data.CommandBehavior.Default); - - if (reader.Read()) - { - // Get course name to create a new directory or use this as the working directory - var courseTitle = reader.GetString(reader.GetOrdinal("CourseTitle")); - foreach (char invalidChar in invalidPathChars) - courseTitle = courseTitle.Replace(invalidChar, '-'); - - // Use the existing directory or create a new folder with the courseTitle - if (!Directory.Exists(Path.Combine(containingDir.FullName, courseTitle))) - containingDir = containingDir.CreateSubdirectory(courseTitle); - else - containingDir = new DirectoryInfo(Path.Combine(containingDir.FullName, courseTitle)); - - // Create or use another folder to group by the Chapter - var chapterTitle = reader.GetString(reader.GetOrdinal("ChapterTitle")); - foreach (char invalidChar in invalidPathChars) - chapterTitle = chapterTitle.Replace(invalidChar, '-'); - - var chapterIndex = reader.GetInt32(reader.GetOrdinal("ChapterIndex")); - chapterTitle = $"{chapterIndex} - {chapterTitle}"; - - if (!Directory.Exists(Path.Combine(containingDir.FullName, chapterTitle))) - containingDir = containingDir.CreateSubdirectory(chapterTitle); - else - containingDir = new DirectoryInfo(Path.Combine(containingDir.FullName, chapterTitle)); - - var videoIndex = reader.GetInt32(reader.GetOrdinal("SortIndex")); - var title = reader.GetString(reader.GetOrdinal("Title")); - foreach (char invalidChar in invalidFileChars) - title = title.Replace(invalidChar, '-'); - - newPath = Path.Combine(containingDir.FullName, $"E{videoIndex.ToString()} - {title}.mp4"); - - if (newPath.Length > 240) - newPath = Path.Combine(containingDir.FullName, $"E{videoIndex.ToString()}.mp4"); - } - else - { - WriteToConsole("[STATUS] Couldn't find db entry for file: " + item + Environment.NewLine + "[STATUS] Using the default behaviour!", ConsoleColor.DarkYellow); - newPath = Path.ChangeExtension(item, ".mp4"); - } - - if (!reader.IsClosed) - reader.Close(); - } - else - { - await semaphore.WaitAsync(); - newPath = Path.ChangeExtension(item, ".mp4"); - } - - - semaphore.Wait(); - taskList.Add(Task.Run(() => - { - Decrypt(item, newPath); - lock(sem_lock) - { - semaphore.Release(); - } - })); - } - - await Task.WhenAll(taskList); - WriteToConsole("Decryption completed!", ConsoleColor.DarkGreen); - } - - static void Decrypt(string encryptedFilePath, string decryptedFilePath) - { - if (!File.Exists(encryptedFilePath)) - { - WriteToConsole("[ERR] Couldn't find encrypted file...", ConsoleColor.Red); - return; - } - - FileInfo encryptedFileInfo = new FileInfo(encryptedFilePath); - - if (File.Exists(decryptedFilePath)) - { - FileInfo decryptedFileInfo = new FileInfo(decryptedFilePath); - - if (decryptedFileInfo.Length == encryptedFileInfo.Length) - { - WriteToConsole("[DEC] File " + decryptedFilePath + " exists already and will be skipped!", ConsoleColor.Yellow); - return; - } - else - WriteToConsole("[DEC] File " + decryptedFilePath + " exists already but seems to differ in size...", ConsoleColor.Blue); - - decryptedFileInfo = null; - } - - - byte[] buffer = new byte[0x50000]; - - if (encryptedFileInfo.Extension != ".lynda") - { - WriteToConsole("[ERR] Couldn't load file: " + encryptedFilePath, ConsoleColor.Red); - return; - } - - using (FileStream inStream = new FileStream(encryptedFilePath, FileMode.Open)) - { - using (CryptoStream decryptionStream = new CryptoStream(inStream, rijndael.CreateDecryptor(enc_key_bytes, enc_key_bytes), CryptoStreamMode.Read)) - using (FileStream outStream = new FileStream(decryptedFilePath, FileMode.Create)) - { - WriteToConsole("[DEC] Decrypting file " + encryptedFileInfo.Name + "..."); - - while ((inStream.Length - inStream.Position) >= buffer.Length) - { - decryptionStream.Read(buffer, 0, buffer.Length); - outStream.Write(buffer, 0, buffer.Length); - } - - buffer = new byte[inStream.Length - inStream.Position]; - decryptionStream.Read(buffer, 0, buffer.Length); - outStream.Write(buffer, 0, buffer.Length); - outStream.Flush(); - outStream.Close(); - - WriteToConsole("[DEC] File decryption completed: " + decryptedFilePath, ConsoleColor.DarkGreen); - } - - inStream.Close(); - buffer = null; - } - - if((usage_mode & Mode.RemoveFiles) == Mode.RemoveFiles) - encryptedFileInfo.Delete(); - - encryptedFileInfo = null; - } - - static void WriteToConsole(string Text, ConsoleColor color = ConsoleColor.Gray) - { - lock(console_lock) - { - Console.ForegroundColor = color; - Console.WriteLine(Text); - Console.ForegroundColor = color_default; - } + Console.WriteLine("\t/OUT [PATH]\tSpecifies an output directory instead of using default directory."); } } } diff --git a/LyndaDecryptor/Properties/AssemblyInfo.cs b/LyndaDecryptor/Properties/AssemblyInfo.cs index 0c643e3..3df7ab3 100644 --- a/LyndaDecryptor/Properties/AssemblyInfo.cs +++ b/LyndaDecryptor/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern // übernehmen, indem Sie "*" eingeben: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.2.0.0")] -[assembly: AssemblyFileVersion("1.2.0.0")] +[assembly: AssemblyVersion("1.3.0.0")] +[assembly: AssemblyFileVersion("1.3.0.0")] diff --git a/LyndaDecryptor/Utils.cs b/LyndaDecryptor/Utils.cs new file mode 100644 index 0000000..d15abaa --- /dev/null +++ b/LyndaDecryptor/Utils.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LyndaDecryptor +{ + public static class Utils + { + private static ConsoleColor color_default; + private static object console_lock = new object(); + + public static string ENCRYPTION_KEY = "~h#\x00b0" + new string(new char[] { '\'', '*', '\x00b2', '"', 'C', '\x00b4', '|', '\x00a7', '\\' }) + "3~."; + + static Utils() + { + color_default = Console.ForegroundColor; + } + + public static void WriteToConsole(string Text, ConsoleColor color = ConsoleColor.Gray) + { + lock (console_lock) + { + Console.ForegroundColor = color; + Console.WriteLine(Text); + Console.ForegroundColor = color_default; + } + } + + public static DecryptorOptions ParseCommandLineArgs(string[] args) + { + DecryptorOptions options = new DecryptorOptions(); + int index = 0; + int length = args.Length; + + foreach (string arg in args) + { + if (string.IsNullOrWhiteSpace(arg)) + { + index++; + continue; + } + + switch (arg.ToUpper()) + { + case "/D": // Directory Mode + if (length-1 > index && Directory.Exists(args[index + 1])) + { + options.InputPath = args[index + 1]; + options.UsageMode = Mode.Folder; + WriteToConsole("[ARGS] Changing mode to Folder decryption!", ConsoleColor.Yellow); + } + else + { + WriteToConsole("[ARGS] The directory path is missing..." + Environment.NewLine, ConsoleColor.Red); + throw new FileNotFoundException("Directory path is missing or specified directory was not found!"); + } + break; + + case "/F": // File Mode + if (length-1 > index && File.Exists(args[index + 1])) + { + options.InputPath = args[index + 1]; + + if (length - 1 > index + 1 && !string.IsNullOrWhiteSpace(args[index + 2])) + { + if (File.Exists(args[index + 2])) + throw new IOException("File already exists: " + args[index + 2]); + + options.OutputPath = args[index + 2]; + } + else + throw new FormatException("Output file path is missing..."); + + options.UsageMode = Mode.File; + WriteToConsole("[ARGS] Changing mode to Single decryption!", ConsoleColor.Yellow); + } + else + { + throw new FileNotFoundException("Input file is missing or not specified!"); + } + break; + + case "/DB": // Use Database + options.UseDatabase = true; + + if (length - 1 > index && File.Exists(args[index + 1])) + options.DatabasePath = args[index + 1]; + break; + + case "/RM": // Remove encrypted files after decryption + options.RemoveFilesAfterDecryption = true; + break; + + case "/OUT": + options.UseOutputFolder = true; + + if (args.Length - 1 > index) + options.OutputFolder = args[index + 1]; + break; + } + + index++; + } + + return options; + } + } +} diff --git a/LyndaDecryptor/VideoInfo.cs b/LyndaDecryptor/VideoInfo.cs new file mode 100644 index 0000000..73f3eb9 --- /dev/null +++ b/LyndaDecryptor/VideoInfo.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LyndaDecryptor +{ + public class VideoInfo + { + public string CourseTitle { get; set; } + public string ChapterTitle { get; set; } + public string VideoTitle { get; set; } + public string VideoID { get; set; } + public string CourseID { get; set; } + + public int ChapterIndex { get; set; } + public int VideoIndex { get; set; } + } +} diff --git a/LyndaDecryptor/packages.config b/LyndaDecryptor/packages.config index 61e6a43..7bb5374 100644 --- a/LyndaDecryptor/packages.config +++ b/LyndaDecryptor/packages.config @@ -1,8 +1,5 @@  - - - \ No newline at end of file diff --git a/LyndaDecryptorTests/CommandLineParserTest.cs b/LyndaDecryptorTests/CommandLineParserTest.cs new file mode 100644 index 0000000..59e0edf --- /dev/null +++ b/LyndaDecryptorTests/CommandLineParserTest.cs @@ -0,0 +1,122 @@ +#define TEST + +using System; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LyndaDecryptor; +using System.Collections.Generic; +using System.IO; + +namespace LyndaDecryptorTests +{ + [TestClass] + public class CommandLineParserTest + { + [TestMethod] + public void TestFileMode() + { + List args = new List(); + args.Add("/F"); + args.Add("TestFiles\\88067_2195c10678b4f73e34795af641ad1ecc.lynda"); + args.Add("output.mp4"); + + DecryptorOptions options = new DecryptorOptions(); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.UsageMode == Mode.File); + Assert.AreEqual("TestFiles\\88067_2195c10678b4f73e34795af641ad1ecc.lynda", options.InputPath); + Assert.AreEqual("output.mp4", options.OutputPath); + + args.Add("/DB"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.UseDatabase); + Assert.AreEqual(null, options.DatabasePath); + + args.Add("TestDB\\db_de.sqlite"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.AreEqual(args.Last(), options.DatabasePath); + + args.Add("/RM"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.RemoveFilesAfterDecryption); + + args.Add("/OUT"); + args.Add("testfolder"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.UseOutputFolder); + Assert.AreEqual(args.Last(), options.OutputFolder); + } + + [TestMethod] + public void TestFolderMode() + { + List args = new List(); + args.Add("/D"); + args.Add("TestFiles"); + + DecryptorOptions options = new DecryptorOptions(); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.UsageMode == Mode.Folder); + Assert.AreEqual(options.InputPath, "TestFiles"); + + args.Add("/RM"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.RemoveFilesAfterDecryption); + + args.Add("/DB"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.UseDatabase); + Assert.AreEqual(null, options.DatabasePath); + + args.Add("TestDB\\db_de.sqlite"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.AreEqual(args.Last(), options.DatabasePath); + + args.Add("/OUT"); + args.Add("testfolder"); + options = Utils.ParseCommandLineArgs(args.ToArray()); + + Assert.IsTrue(options.UseOutputFolder); + Assert.AreEqual(options.OutputFolder, args.Last()); + } + + [TestMethod, ExpectedException(typeof(FormatException))] + public void MissingOutputArgShouldFailWithException() + { + List args = new List(); + args.Add("/F"); + args.Add("TestFiles\\88067_2195c10678b4f73e34795af641ad1ecc.lynda"); + + Utils.ParseCommandLineArgs(args.ToArray()); + } + + [TestMethod, ExpectedException(typeof(IOException))] + public void OutputFileAlreadyExistShouldFailWithException() + { + List args = new List(); + args.Add("/F"); + args.Add("TestFiles\\88067_2195c10678b4f73e34795af641ad1ecc.lynda"); + args.Add("TestFiles\\88071_4650ab745df849fd96f1fdbdb016a5e6.lynda"); + + Utils.ParseCommandLineArgs(args.ToArray()); + } + + [TestMethod, ExpectedException(typeof(FileNotFoundException))] + public void TestMissingFolder() + { + List args = new List(); + args.Add("/D"); + args.Add("TestFiles2"); + + Utils.ParseCommandLineArgs(args.ToArray()); + } + } +} diff --git a/LyndaDecryptorTests/DecryptionTest.cs b/LyndaDecryptorTests/DecryptionTest.cs new file mode 100644 index 0000000..384bd5c --- /dev/null +++ b/LyndaDecryptorTests/DecryptionTest.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using LyndaDecryptor; + +namespace LyndaDecryptorTests +{ + [TestClass] + public class DecryptionTest + { + [TestMethod] + public void TestSingleDecryption() + { + DecryptorOptions options = new DecryptorOptions + { + UsageMode = Mode.File, + InputPath = "TestFiles\\88067_2195c10678b4f73e34795af641ad1ecc.lynda", + OutputPath = "TestFiles\\88067_2195c10678b4f73e34795af641ad1ecc.mp4", + RemoveFilesAfterDecryption = false + }; + + Decryptor decryptor = new Decryptor(options); + + decryptor.InitDecryptor(Utils.ENCRYPTION_KEY); + decryptor.Decrypt(options.InputPath, options.OutputPath); + + FileInfo encryptedFile = new FileInfo(options.InputPath); + FileInfo decryptedFile = new FileInfo(options.OutputPath); + + Assert.AreEqual(encryptedFile.Length, decryptedFile.Length); + } + + [TestMethod] + public void TestSingleDecryptionWithDB() + { + + } + + [TestMethod] + public void TestFolderDecryption() + { + + } + + [TestMethod] + public void TestFolderDecryptionWithDB() + { + + } + } +} diff --git a/LyndaDecryptorTests/LyndaDecryptorTests.csproj b/LyndaDecryptorTests/LyndaDecryptorTests.csproj new file mode 100644 index 0000000..0a71d2c --- /dev/null +++ b/LyndaDecryptorTests/LyndaDecryptorTests.csproj @@ -0,0 +1,110 @@ + + + + Debug + AnyCPU + {F8726170-D017-4517-AFB9-B1870A282E81} + Library + Properties + LyndaDecryptorTests + LyndaDecryptorTests + v4.5.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + False + + + + + + + + + + + + {58ebf661-a637-45ae-956c-5ec1a602edc3} + LyndaDecryptor + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/LyndaDecryptorTests/Properties/AssemblyInfo.cs b/LyndaDecryptorTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a00ed04 --- /dev/null +++ b/LyndaDecryptorTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über folgende +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("LyndaDecryptorTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LyndaDecryptorTests")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Wenn ComVisible auf "false" festgelegt wird, sind die Typen innerhalb dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(false)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("f8726170-d017-4517-afb9-b1870a282e81")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern +// übernehmen, indem Sie "*" eingeben: +// [Assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LyndaDecryptorTests/TestDB/db_de.sqlite b/LyndaDecryptorTests/TestDB/db_de.sqlite new file mode 100644 index 0000000..b16069b Binary files /dev/null and b/LyndaDecryptorTests/TestDB/db_de.sqlite differ diff --git a/LyndaDecryptorTests/TestFiles/88067_2195c10678b4f73e34795af641ad1ecc.lynda b/LyndaDecryptorTests/TestFiles/88067_2195c10678b4f73e34795af641ad1ecc.lynda new file mode 100644 index 0000000..8c59272 Binary files /dev/null and b/LyndaDecryptorTests/TestFiles/88067_2195c10678b4f73e34795af641ad1ecc.lynda differ diff --git a/LyndaDecryptorTests/TestFiles/88071_4650ab745df849fd96f1fdbdb016a5e6.lynda b/LyndaDecryptorTests/TestFiles/88071_4650ab745df849fd96f1fdbdb016a5e6.lynda new file mode 100644 index 0000000..a6ef988 Binary files /dev/null and b/LyndaDecryptorTests/TestFiles/88071_4650ab745df849fd96f1fdbdb016a5e6.lynda differ diff --git a/LyndaDecryptorTests/TestFiles/88087_44c891cdef18ee48d968a018afe3befd.lynda b/LyndaDecryptorTests/TestFiles/88087_44c891cdef18ee48d968a018afe3befd.lynda new file mode 100644 index 0000000..fe6efe6 Binary files /dev/null and b/LyndaDecryptorTests/TestFiles/88087_44c891cdef18ee48d968a018afe3befd.lynda differ diff --git a/LyndaDecryptorTests/TestFiles/88089_191d15e08d44c0e84f2237f433c67a67.lynda b/LyndaDecryptorTests/TestFiles/88089_191d15e08d44c0e84f2237f433c67a67.lynda new file mode 100644 index 0000000..6718db4 Binary files /dev/null and b/LyndaDecryptorTests/TestFiles/88089_191d15e08d44c0e84f2237f433c67a67.lynda differ