diff --git a/src/Jackett/Jackett.csproj b/src/Jackett/Jackett.csproj index 78732f22e..7e5476a0f 100644 --- a/src/Jackett/Jackett.csproj +++ b/src/Jackett/Jackett.csproj @@ -319,6 +319,7 @@ + diff --git a/src/Jackett/Services/ProtectionService.cs b/src/Jackett/Services/ProtectionService.cs index 710b4f154..58f0bfd7d 100644 --- a/src/Jackett/Services/ProtectionService.cs +++ b/src/Jackett/Services/ProtectionService.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using Jackett.Utils; namespace Jackett.Services { @@ -18,6 +19,7 @@ namespace Jackett.Services public class ProtectionService : IProtectionService { DataProtectionScope PROTECTION_SCOPE = DataProtectionScope.LocalMachine; + private const string JACKETT_KEY = "JACKETT_KEY"; const string APPLICATION_KEY = "Dvz66r3n8vhTGip2/quiw5ISyM37f7L2iOdupzdKmzkvXGhAgQiWK+6F+4qpxjPVNks1qO7LdWuVqRlzgLzeW8mChC6JnBMUS1Fin4N2nS9lh4XPuCZ1che75xO92Nk2vyXUo9KSFG1hvEszAuLfG2Mcg1r0sVyVXd2gQDU/TbY="; IServerService serverService; @@ -34,6 +36,34 @@ namespace Jackett.Services } public string Protect(string plainText) + { + var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY); + + if (jackettKey == null) + { + return ProtectDefaultMethod(plainText); + } + else + { + return ProtectUsingKey(plainText, jackettKey); + } + } + + public string UnProtect(string plainText) + { + var jackettKey = Environment.GetEnvironmentVariable(JACKETT_KEY); + + if (jackettKey == null) + { + return UnProtectDefaultMethod(plainText); + } + else + { + return UnProtectUsingKey(plainText, jackettKey); + } + } + + private string ProtectDefaultMethod(string plainText) { if (string.IsNullOrEmpty(plainText)) return string.Empty; @@ -72,7 +102,7 @@ namespace Jackett.Services return Convert.ToBase64String(protectedBytes); } - public string UnProtect(string plainText) + private string UnProtectDefaultMethod(string plainText) { if (string.IsNullOrEmpty(plainText)) return string.Empty; @@ -111,6 +141,16 @@ namespace Jackett.Services return Encoding.UTF8.GetString(unprotectedBytes); } + private string ProtectUsingKey(string plainText, string key) + { + return StringCipher.Encrypt(plainText, key); + } + + private string UnProtectUsingKey(string plainText, string key) + { + return StringCipher.Decrypt(plainText, key); + } + public void Protect(T obj) { var type = obj.GetType(); diff --git a/src/Jackett/Utils/StringCipher.cs b/src/Jackett/Utils/StringCipher.cs new file mode 100644 index 000000000..17124f660 --- /dev/null +++ b/src/Jackett/Utils/StringCipher.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Jackett.Utils +{ + public static class StringCipher + { + // This constant is used to determine the keysize of the encryption algorithm in bits. + // We divide this by 8 within the code below to get the equivalent number of bytes. + private const int Keysize = 256; + + // This constant determines the number of iterations for the password bytes generation function. + private const int DerivationIterations = 1000; + + public static string Encrypt(string plainText, string passPhrase) + { + // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text + // so that the same Salt and IV values can be used when decrypting. + var saltStringBytes = Generate256BitsOfRandomEntropy(); + var ivStringBytes = Generate256BitsOfRandomEntropy(); + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)) + { + var keyBytes = password.GetBytes(Keysize / 8); + using (var symmetricKey = new RijndaelManaged()) + { + symmetricKey.BlockSize = 256; + symmetricKey.Mode = CipherMode.CBC; + symmetricKey.Padding = PaddingMode.PKCS7; + using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes)) + { + using (var memoryStream = new MemoryStream()) + { + using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) + { + cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); + cryptoStream.FlushFinalBlock(); + // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes. + var cipherTextBytes = saltStringBytes; + cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray(); + cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray(); + memoryStream.Close(); + cryptoStream.Close(); + return Convert.ToBase64String(cipherTextBytes); + } + } + } + } + } + } + + public static string Decrypt(string cipherText, string passPhrase) + { + // Get the complete stream of bytes that represent: + // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText] + var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText); + // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes. + var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray(); + // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes. + var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray(); + // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string. + var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((Keysize / 8) * 2)).ToArray(); + + using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations)) + { + var keyBytes = password.GetBytes(Keysize / 8); + using (var symmetricKey = new RijndaelManaged()) + { + symmetricKey.BlockSize = 256; + symmetricKey.Mode = CipherMode.CBC; + symmetricKey.Padding = PaddingMode.PKCS7; + using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes)) + { + using (var memoryStream = new MemoryStream(cipherTextBytes)) + { + using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) + { + var plainTextBytes = new byte[cipherTextBytes.Length]; + var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); + memoryStream.Close(); + cryptoStream.Close(); + return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount); + } + } + } + } + } + } + + private static byte[] Generate256BitsOfRandomEntropy() + { + var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits. + using (var rngCsp = new RNGCryptoServiceProvider()) + { + // Fill the array with cryptographically secure random bytes. + rngCsp.GetBytes(randomBytes); + } + return randomBytes; + } + } +}