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