diff --git a/Manifest.cs b/Manifest.cs index 734c0b5..c71a01f 100644 --- a/Manifest.cs +++ b/Manifest.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using SteamAuth; using System; using System.Collections.Generic; @@ -6,9 +6,15 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Security.Cryptography; public class Manifest { + private const int PBKDF2_ITERATIONS = 50000; //Set to 50k to make program not unbearably slow. May increase in future. + private const int SALT_LENGTH = 8; + private const int KEY_SIZE_BYTES = 32; + private const int IV_LENGTH = 16; + [JsonProperty("encrypted")] public bool Encrypted { get; set; } @@ -35,11 +41,6 @@ public class Manifest private static Manifest _manifest { get; set; } - public static string GetExecutableDir() - { - return Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); - } - public static Manifest GetManifest(bool forceLoad = false) { // Check if already staticly loaded @@ -49,7 +50,7 @@ public class Manifest } // Find config dir and manifest file - string maFile = Program.SteamGuardPath + "/manifest.json"; + string maFile = Path.Combine(Program.SteamGuardPath, "manifest.json"); // If there's no config dir, create it if (!Directory.Exists(Program.SteamGuardPath)) @@ -77,11 +78,6 @@ public class Manifest _manifest.Save(); } - if (_manifest.Encrypted) - { - throw new NotSupportedException("Encrypted maFiles are not supported at this time."); - } - _manifest.RecomputeExistingEntries(); return _manifest; @@ -110,7 +106,6 @@ public class Manifest // Take a pre-manifest version and generate a manifest for it. if (scanDir) { - if (Directory.Exists(Program.SteamGuardPath)) { DirectoryInfo dir = new DirectoryInfo(Program.SteamGuardPath); @@ -131,15 +126,16 @@ public class Manifest }; newManifest.Entries.Add(newEntry); } - catch (Exception) + catch (Exception ex) { + if (Program.Verbose) Console.WriteLine("warn: {0}", ex.Message); } } if (newManifest.Entries.Count > 0) { newManifest.Save(); - newManifest.PromptSetupPassKey("This version of SDA has encryption. Please enter a passkey below, or hit cancel to remain unencrypted"); + newManifest.PromptSetupPassKey(true); } } } @@ -155,6 +151,8 @@ public class Manifest public class IncorrectPassKeyException : Exception { } public class ManifestNotEncryptedException : Exception { } + // TODO: move PromptForPassKey to Program.cs + // TODO: make PromptForPassKey more secure public string PromptForPassKey() { if (!this.Encrypted) @@ -179,40 +177,34 @@ public class Manifest return passKey; } - public string PromptSetupPassKey(string initialPrompt = "Enter passkey, or hit cancel to remain unencrypted.") + // TODO: move PromptSetupPassKey to Program.cs + public string PromptSetupPassKey(bool inAccountSetupProcess = false) { - Console.Write("Would you like to use encryption? [Y/n] "); - string doEncryptAnswer = Console.ReadLine(); - if (doEncryptAnswer == "n" || doEncryptAnswer == "N") + if (inAccountSetupProcess) { - Console.WriteLine("WARNING: You chose to not encrypt your files. Doing so imposes a security risk for yourself. If an attacker were to gain access to your computer, they could completely lock you out of your account and steal all your items."); - return null; + Console.Write("Would you like to use encryption? [Y/n] "); + string doEncryptAnswer = Console.ReadLine(); + if (doEncryptAnswer == "n" || doEncryptAnswer == "N") + { + Console.WriteLine("WARNING: You chose to not encrypt your files. Doing so imposes a security risk for yourself. If an attacker were to gain access to your computer, they could completely lock you out of your account and steal all your items."); + return null; + } } string newPassKey = ""; string confirmPassKey = ""; do { - Console.Write("Enter passkey: "); + Console.Write("Enter" + (inAccountSetupProcess ? " " : " new ") + "passkey: "); newPassKey = Console.ReadLine(); - Console.Write("Confirm passkey: "); + Console.Write("Confirm" + (inAccountSetupProcess ? " " : " new ") + "passkey: "); confirmPassKey = Console.ReadLine(); if (newPassKey != confirmPassKey) { Console.WriteLine("Passkeys do not match."); } - } while (newPassKey != confirmPassKey); - - if (!this.ChangeEncryptionKey(null, newPassKey)) - { - Console.WriteLine("Unable to set passkey."); - return null; - } - else - { - Console.WriteLine("Passkey successfully set."); - } + } while (newPassKey != confirmPassKey || newPassKey == ""); return newPassKey; } @@ -224,16 +216,7 @@ public class Manifest List accounts = new List(); foreach (var entry in this.Entries) { - string fileText = File.ReadAllText(Path.Combine(Program.SteamGuardPath, entry.Filename)); - if (this.Encrypted) - { - throw new NotSupportedException("Encrypted maFiles are not supported at this time."); - //string decryptedText = FileEncryptor.DecryptData(passKey, entry.Salt, entry.IV, fileText); - //if (decryptedText == null) return new SteamGuardAccount[0]; - //fileText = decryptedText; - } - - var account = JsonConvert.DeserializeObject(fileText); + var account = GetAccount(entry, passKey); if (account == null) continue; accounts.Add(account); @@ -244,9 +227,42 @@ public class Manifest return accounts.ToArray(); } - public bool ChangeEncryptionKey(string oldKey, string newKey) + public SteamGuardAccount GetAccount(ManifestEntry entry, string passKey = null) { - throw new NotSupportedException("Encrypted maFiles are not supported at this time."); + string fileText = ""; + Stream stream = null; + RijndaelManaged aes256; + + if (this.Encrypted) + { + MemoryStream ms = new MemoryStream(Convert.FromBase64String(File.ReadAllText(Path.Combine(Program.SteamGuardPath, entry.Filename)))); + byte[] key = GetEncryptionKey(passKey, entry.Salt); + + aes256 = new RijndaelManaged + { + IV = Convert.FromBase64String(entry.IV), + Key = key, + Padding = PaddingMode.PKCS7, + Mode = CipherMode.CBC + }; + + ICryptoTransform decryptor = aes256.CreateDecryptor(aes256.Key, aes256.IV); + stream = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); + } + else + { + FileStream fileStream = File.OpenRead(Path.Combine(Program.SteamGuardPath, entry.Filename)); + stream = fileStream; + } + + if (Program.Verbose) Console.WriteLine("Decrypting..."); + using (StreamReader reader = new StreamReader(stream)) + { + fileText = reader.ReadToEnd(); + } + stream.Close(); + + return JsonConvert.DeserializeObject(fileText); } public bool VerifyPasskey(string passkey) @@ -286,22 +302,14 @@ public class Manifest return false; } - public bool SaveAccount(SteamGuardAccount account, bool encrypt, string passKey = null) + public bool SaveAccount(SteamGuardAccount account, bool encrypt, string passKey = null, string salt = null, string iV = null) { - if (encrypt && String.IsNullOrEmpty(passKey)) return false; - if (!encrypt && this.Encrypted) return false; + if (encrypt && (String.IsNullOrEmpty(passKey) || String.IsNullOrEmpty(salt) || String.IsNullOrEmpty(iV))) return false; - string salt = null; - string iV = null; string jsonAccount = JsonConvert.SerializeObject(account); - if (encrypt) - { - throw new NotSupportedException("Encrypted maFiles are not supported at this time."); - } - - string filename = account.Session.SteamID.ToString() + ".maFile"; + if (Program.Verbose) Console.WriteLine($"Saving account {account.AccountName} to {filename}..."); ManifestEntry newEntry = new ManifestEntry() { @@ -328,7 +336,7 @@ public class Manifest } bool wasEncrypted = this.Encrypted; - this.Encrypted = encrypt || this.Encrypted; + this.Encrypted = encrypt; if (!this.Save()) { @@ -338,27 +346,68 @@ public class Manifest try { - File.WriteAllText(Program.SteamGuardPath + filename, jsonAccount); + Stream stream = null; + MemoryStream ms = null; + RijndaelManaged aes256; + + if (encrypt) + { + ms = new MemoryStream(); + byte[] key = GetEncryptionKey(passKey, newEntry.Salt); + + aes256 = new RijndaelManaged + { + IV = Convert.FromBase64String(newEntry.IV), + Key = key, + Padding = PaddingMode.PKCS7, + Mode = CipherMode.CBC + }; + + ICryptoTransform encryptor = aes256.CreateEncryptor(aes256.Key, aes256.IV); + stream = new CryptoStream(ms, encryptor, CryptoStreamMode.Write); + } + else + { + // An unencrypted maFile is shorter than the encrypted version, + // so when an unencrypted maFile gets written this way, the file does not get wiped + // leaving encrypted text after the final } bracket. Deleting and recreating the file fixes this. + File.Delete(Path.Combine(Program.SteamGuardPath, newEntry.Filename)); + stream = File.OpenWrite(Path.Combine(Program.SteamGuardPath, newEntry.Filename)); // open or create + } + + using (StreamWriter writer = new StreamWriter(stream)) + { + writer.Write(jsonAccount); + } + + if (encrypt) + { + File.WriteAllText(Path.Combine(Program.SteamGuardPath, newEntry.Filename), Convert.ToBase64String(ms.ToArray())); + } + + stream.Close(); return true; } - catch (Exception) + catch (Exception ex) { + if (Program.Verbose) Console.WriteLine("error: {0}", ex.ToString()); return false; } } public bool Save() { - - string filename = Program.SteamGuardPath + "manifest.json"; + string filename = Path.Combine(Program.SteamGuardPath, "manifest.json"); if (!Directory.Exists(Program.SteamGuardPath)) { try { + if (Program.Verbose) Console.WriteLine("Creating {0}", Program.SteamGuardPath); Directory.CreateDirectory(Program.SteamGuardPath); } - catch (Exception) + catch (Exception ex) { + if (Program.Verbose) Console.WriteLine($"error: {ex.Message}"); return false; } } @@ -369,8 +418,9 @@ public class Manifest File.WriteAllText(filename, contents); return true; } - catch (Exception) + catch (Exception ex) { + if (Program.Verbose) Console.WriteLine($"error: {ex.Message}"); return false; } } @@ -420,4 +470,59 @@ public class Manifest [JsonProperty("steamid")] public ulong SteamID { get; set; } } + + /* + Crypto Functions + */ + + /// + /// Returns an 8-byte cryptographically random salt in base64 encoding + /// + /// + public static string GetRandomSalt() + { + byte[] salt = new byte[SALT_LENGTH]; + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(salt); + } + return Convert.ToBase64String(salt); + } + + /// + /// Returns a 16-byte cryptographically random initialization vector (IV) in base64 encoding + /// + /// + public static string GetInitializationVector() + { + byte[] IV = new byte[IV_LENGTH]; + using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(IV); + } + return Convert.ToBase64String(IV); + } + + + /// + /// Generates an encryption key derived using a password, a random salt, and specified number of rounds of PBKDF2 + /// + /// + /// + /// + private static byte[] GetEncryptionKey(string password, string salt) + { + if (string.IsNullOrEmpty(password)) + { + throw new ArgumentException("Password is empty"); + } + if (string.IsNullOrEmpty(salt)) + { + throw new ArgumentException("Salt is empty"); + } + using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(salt), PBKDF2_ITERATIONS)) + { + return pbkdf2.GetBytes(KEY_SIZE_BYTES); + } + } } diff --git a/Program.cs b/Program.cs index 05f5159..15cb861 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using SteamAuth; using System; using System.Collections.Generic; @@ -22,6 +22,8 @@ public static class Program [STAThread] public static void Main(string[] args) { + string action = "generate-code"; + string user = ""; // Parse cli arguments @@ -29,13 +31,34 @@ public static class Program { Console.WriteLine("steamguard-cli - v0.0"); Console.WriteLine(); - Console.WriteLine("--help, -h Display this help message."); - Console.WriteLine("--verbose, -v Display some extra information when the program is running."); - Console.WriteLine("--user, -u Specify an account for which to generate a Steam Gaurd code."); - Console.WriteLine(" Otherwise, the first account will be selected."); + Console.WriteLine("--help, -h Display this help message."); + Console.WriteLine("--verbose, -v Display some extra information when the program is running."); + Console.WriteLine("--user, -u Specify an account for which to generate a Steam Gaurd code."); + Console.WriteLine(" Otherwise, the first account will be selected."); + Console.WriteLine("--generate-code Generate a Steam Guard code and exit. (default)"); + Console.WriteLine("--encrypt Encrypt your maFiles or change your encryption passkey."); + Console.WriteLine("--decrypt Remove encryption from your maFiles."); return; } Verbose = args.Contains("-v") || args.Contains("--verbose"); + // Actions + if (args.Contains("--generate-code")) + { + action = "generate-code"; + } + else if (args.Contains("--encrypt")) + { + action = "encrypt"; + } + else if (args.Contains("--decrypt")) + { + action = "decrypt"; + } + else if (args.Contains("--setup")) + { + action = "setup"; + } + // Misc if (args.Contains("--user") || args.Contains("-u")) { int u = Array.IndexOf(args, "--user"); @@ -72,13 +95,44 @@ public static class Program } if (Verbose) Console.WriteLine("maFiles path: {0}", SteamGuardPath); - // Generate the code + if (Verbose) Console.WriteLine("Action: {0}", action); + // Perform desired action + switch (action) + { + case "generate-code": + GenerateCode(user); + break; + case "encrypt": // Can also be used to change passkey + Console.WriteLine(Encrypt()); + break; + case "decrypt": + Console.WriteLine(Decrypt()); + break; + case "setup": + throw new NotSupportedException(); + break; + default: + Console.WriteLine("error: Unknown action: {0}", action); + return; + } + } + + static void GenerateCode(string user = "") + { if (Verbose) Console.WriteLine("Aligning time..."); TimeAligner.AlignTime(); if (Verbose) Console.WriteLine("Opening manifest..."); Manifest = Manifest.GetManifest(true); if (Verbose) Console.WriteLine("Reading accounts from manifest..."); - SteamGuardAccounts = Manifest.GetAllAccounts(); + if (Manifest.Encrypted) + { + string passkey = Manifest.PromptForPassKey(); + SteamGuardAccounts = Manifest.GetAllAccounts(passkey); + } + else + { + SteamGuardAccounts = Manifest.GetAllAccounts(); + } if (SteamGuardAccounts.Length == 0) { Console.WriteLine("error: No accounts read."); @@ -110,4 +164,59 @@ public static class Program else Console.WriteLine("error: No Steam accounts found in {0}", SteamGuardAccounts); } + + static bool Encrypt() + { + if (Verbose) Console.WriteLine("Opening manifest..."); + Manifest = Manifest.GetManifest(true); + if (Verbose) Console.WriteLine("Reading accounts from manifest..."); + if (Manifest.Encrypted) + { + string passkey = Manifest.PromptForPassKey(); + SteamGuardAccounts = Manifest.GetAllAccounts(passkey); + } + else + { + SteamGuardAccounts = Manifest.GetAllAccounts(); + } + + string newPassKey = Manifest.PromptSetupPassKey(); + + for (int i = 0; i < SteamGuardAccounts.Length; i++) + { + var account = SteamGuardAccounts[i]; + var salt = Manifest.GetRandomSalt(); + var iv = Manifest.GetInitializationVector(); + bool success = Manifest.SaveAccount(account, true, newPassKey, salt, iv); + if (Verbose) Console.WriteLine("Encrypted {0}: {1}", account.AccountName, success); + if (!success) return false; + } + return true; + } + + static bool Decrypt() + { + if (Verbose) Console.WriteLine("Opening manifest..."); + Manifest = Manifest.GetManifest(true); + if (Verbose) Console.WriteLine("Reading accounts from manifest..."); + if (Manifest.Encrypted) + { + string passkey = Manifest.PromptForPassKey(); + SteamGuardAccounts = Manifest.GetAllAccounts(passkey); + } + else + { + if (Verbose) Console.WriteLine("Decryption not required."); + return true; + } + + for (int i = 0; i < SteamGuardAccounts.Length; i++) + { + var account = SteamGuardAccounts[i]; + bool success = Manifest.SaveAccount(account, false); + if (Verbose) Console.WriteLine("Decrypted {0}: {1}", account.AccountName, success); + if (!success) return false; + } + return true; + } } diff --git a/makefile b/makefile index c281411..734f938 100644 --- a/makefile +++ b/makefile @@ -1,9 +1,9 @@ -all: Program.cs +all: Program.cs mkdir -p build/ nuget restore SteamAuth/SteamAuth/SteamAuth.sln mcs -target:library -out:build/SteamAuth.dll -r:SteamAuth/SteamAuth/packages/Newtonsoft.Json.7.0.1/lib/net45/Newtonsoft.Json.dll SteamAuth/SteamAuth/APIEndpoints.cs SteamAuth/SteamAuth/AuthenticatorLinker.cs SteamAuth/SteamAuth/Confirmation.cs SteamAuth/SteamAuth/SessionData.cs SteamAuth/SteamAuth/SteamGuardAccount.cs SteamAuth/SteamAuth/SteamWeb.cs SteamAuth/SteamAuth/TimeAligner.cs SteamAuth/SteamAuth/UserLogin.cs SteamAuth/SteamAuth/Util.cs SteamAuth/SteamAuth/Properties/AssemblyInfo.cs cp SteamAuth/SteamAuth/packages/Newtonsoft.Json.7.0.1/lib/net45/Newtonsoft.Json.dll build/ - mcs -out:build/steamguard -r:build/SteamAuth.dll -r:build/Newtonsoft.Json.dll Program.cs Manifest.cs + mcs -out:build/steamguard -r:build/SteamAuth.dll -r:build/Newtonsoft.Json.dll -r:/usr/lib/mono/4.5/System.Security.dll Program.cs Manifest.cs run: build/steamguard -v