6
\$\begingroup\$

I'm looking at a task to obfuscate strings in my application like URLs, paths and queries at compile time. This is not for encrypting connectionStrings, passwords or other sensitive data, that is being done with protected section in App.Config and has already been handled in other questions.

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings-and-configuration-files#appconfig-example

https://stackoverflow.com/a/4155307/3850405

I'm also fully aware that whatever I do can be broken and yes security through obscurity has been rejected as far back as 1851 and in more recent years even for .Net. This is not the point of the question.

https://stackoverflow.com/a/506301/3850405

With these statements out of the way let's look at the problem at hand:

I decided to write a Stand-Alone Code Analysis Tool for the .NET Compiler Platform Roslyn.

https://github.com/dotnet/roslyn/wiki/Getting-Started-C%23-Syntax-Analysis

The program encrypts a string using the following algorithm:

https://stackoverflow.com/a/10177020/3850405

The program then creates a random password for every string making the same values unique and then adds the decrypt method to the solution. Requires using System.Security.Cryptography; for the decrypt method.

The code has some limitations with Attributes and handling " that is escaped in strings etc. However I would more like to know If I'm on the right path or have missed something obvious.

Program created for the task:

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Text;
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.CSharp;
    using Microsoft.CodeAnalysis.CSharp.Symbols;
    using Microsoft.CodeAnalysis.CSharp.Syntax;
    using Microsoft.CodeAnalysis.Editing;
    using Microsoft.CodeAnalysis.Formatting;
    using Microsoft.CodeAnalysis.MSBuild;
    using Microsoft.CodeAnalysis.Simplification;
    using Microsoft.CodeAnalysis.Text;

    namespace StringObfuscation
    {
        class Program
        {
            static void Main(string[] args)
            {
                var workspace = new AdhocWorkspace();
                var projectId = ProjectId.CreateNewId();
                var versionStamp = VersionStamp.Create();
                var projectInfo = ProjectInfo.Create(projectId, versionStamp, "NewProject", "projName", LanguageNames.CSharp);
                var newProject = workspace.AddProject(projectInfo);

                var sourceText = SourceText.From(
                    @"
                      using System;
                      using System.IO;
                      using System.Linq;
                      using System.Security.Cryptography;
                      using System.Text;

                      namespace HelloWorld
                      {
                          class Program
                          {
                              static void Main(string[] args)
                              {
                                  var varTest1 = ""test var"";

                                  var varTest2 = ""test var"";

                                  string test1 = ""test string"";

                                  String test2 = ""test String"";

                                  const string test3 = ""test const""; 

                                  readonly string test4 = ""test readonly""; 

                                  int i = 0;

                                  var i2 = 0;

                                  var test5 = Test(""Testing"");

                                  var test6 = Test(""Testing"", ""Testing 2"", 1);

                                  string test7 = ""test symbols \r\n © @ {} [] <> | / \ $£@!\#¤%&/()=?`""; 
                              }

                              public static string Test(string test)
                              {
                                  return test;
                              }

                              public static string Test(string test, string test2, int test3)
                              {
                                  return test + test2 + test3;
                              }
                          }
                      }");

                var document = workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText);
                var syntaxRoot = document.GetSyntaxRootAsync().Result;

                var root = (CompilationUnitSyntax)syntaxRoot;

                var mainNode = root.DescendantNodes()
                    .OfType<MethodDeclarationSyntax>().FirstOrDefault(x => x.Identifier.ValueText == "Main"
                                                                           && x.ParameterList.Parameters.FirstOrDefault().Identifier.ValueText == "args");

                var editor = DocumentEditor.CreateAsync(document).Result;

                SeparatedSyntaxList<ParameterSyntax> parametersList = new SeparatedSyntaxList<ParameterSyntax>().AddRange
                (new ParameterSyntax[]
                    {
                        SyntaxFactory.Parameter(SyntaxFactory.Identifier("one")).WithType(SyntaxFactory.ParseTypeName("string")),
                        SyntaxFactory.Parameter(SyntaxFactory.Identifier("two")).WithType(SyntaxFactory.ParseTypeName("string")),
                    }
                );

                var syntax = SyntaxFactory.ParseStatement(@"
                const int k = 256;

                const int dI = 1000;

                var cTBWSAI = Convert.FromBase64String(one);
                var ssB = cTBWSAI.Take(k / 8).ToArray();
                var iSB = cTBWSAI.Skip(k / 8).Take(k / 8).ToArray();
                var cTB = cTBWSAI.Skip((k / 8) * 2).Take(cTBWSAI.Length - ((k / 8) * 2)).ToArray();

                using (var p = new Rfc2898DeriveBytes(two, ssB, dI))
                {
                    var kB = p.GetBytes(k / 8);
                    using (var sK = new RijndaelManaged())
                    {
                        sK.BlockSize = 256;
                        sK.Mode = CipherMode.CBC;
                        sK.Padding = PaddingMode.PKCS7;
                        using (var d = sK.CreateDecryptor(kB, iSB))
                        {
                            using (var mS = new MemoryStream(cTB))
                            {
                                using (var cS = new CryptoStream(mS, d, CryptoStreamMode.Read))
                                {
                                    var pTB = new byte[cTB.Length];
                                    var dBC = cS.Read(pTB, 0, pTB.Length);
                                    mS.Close();
                                    cS.Close();
                                    return Encoding.UTF8.GetString(pTB, 0, dBC);
                                }
                            }
                        }
                    }
                }
            ");

                //Add StringManipulation method 
                var newMethod = SyntaxFactory.MethodDeclaration(
                    SyntaxFactory.List<AttributeListSyntax>(),
                    SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)),
                    SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)),
                    null,
                    SyntaxFactory.Identifier("D"),
                    null,
                    SyntaxFactory.ParameterList(parametersList),
                    SyntaxFactory.List<TypeParameterConstraintClauseSyntax>(),
                    SyntaxFactory.Block(syntax),
                    null
                );

                newMethod = newMethod.NormalizeWhitespace();

                //Insert after mainNode
                editor.InsertAfter(mainNode, newMethod);

                var invocationExpressions = root.DescendantNodes()
                    .OfType<InvocationExpressionSyntax>();

                foreach (var invocationExpressionSyntax in invocationExpressions)
                {
                    //Replace method calls with hard coded strings
                    if (invocationExpressionSyntax.ArgumentList.Arguments.Any(x =>
                        x.Expression.Kind() == SyntaxKind.StringLiteralExpression))
                    {

                        var stringList = new List<string>();

                        for (int i = 0; i < invocationExpressionSyntax.ArgumentList.Arguments.Count(); i++)
                        {
                            if (invocationExpressionSyntax.ArgumentList.Arguments[i].Expression.Kind() == SyntaxKind.StringLiteralExpression)
                            {
                                var value = invocationExpressionSyntax.ArgumentList.Arguments[i].Expression.GetFirstToken().ValueText;

                                var password = RandomString(40);

                                var encryptedText = StringCipher.Encrypt(value, password);
                                stringList.Add($"D(\"{encryptedText}\", \"{password}\")");
                            }
                            else
                            {
                                stringList.Add(invocationExpressionSyntax.ArgumentList.Arguments[i].Expression
                                    .GetFirstToken().ValueText);
                            }
                        }

                        SeparatedSyntaxList<ArgumentSyntax> arguments = new SeparatedSyntaxList<ArgumentSyntax>().AddRange
                        (new ArgumentSyntax[]
                            {
                                SyntaxFactory.Argument(SyntaxFactory.ParseExpression($"{string.Join(",", stringList)}")),
                            }
                        );

                        var newMethodWithStringObfuscation =
                            SyntaxFactory
                                .InvocationExpression(SyntaxFactory.IdentifierName(invocationExpressionSyntax.Expression
                                    .GetFirstToken().ValueText))
                                .WithArgumentList(
                                    SyntaxFactory.ArgumentList()
                                        .WithOpenParenToken(
                                            SyntaxFactory.Token(
                                                SyntaxKind.OpenParenToken))
                                        .WithArguments(arguments)
                                        .WithCloseParenToken(
                                            SyntaxFactory.Token(
                                                SyntaxKind.CloseParenToken)));

                        newMethodWithStringObfuscation = newMethodWithStringObfuscation.NormalizeWhitespace();

                        Console.WriteLine($"Replacing values for method {invocationExpressionSyntax.Expression.GetFirstToken().ValueText}");

                        editor.ReplaceNode(invocationExpressionSyntax, newMethodWithStringObfuscation);
                    }
                }

                var localDeclaration = new LocalDeclarationVirtualizationVisitor();
                localDeclaration.Visit(root);

                var localDeclarations = localDeclaration.LocalDeclarations;

                foreach (var localDeclarationStatementSyntax in localDeclarations)
                {
                    foreach (VariableDeclaratorSyntax variable in localDeclarationStatementSyntax.Declaration.Variables)
                    {

                        var stringKind = variable.Initializer.Value.Kind();

                        //Replace string variables
                        if (stringKind == SyntaxKind.StringLiteralExpression)
                        {
                            var value = variable.Initializer.Value.ToString();
                            //Remove " from start and end of string
                            value = value.Remove(0, 1);
                            value = value.Remove(value.Length - 1, 1);
                            var password = RandomString(40);

                            var encryptedText = StringCipher.Encrypt(value, password);

                            var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = D(\"{encryptedText}\", \"{password}\"); {System.Environment.NewLine}");
                            //var newVariable = SyntaxFactory.ParseStatement($"string {variable.Identifier.ValueText} = D(\"{value}\"); {System.Environment.NewLine}").WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);

                            //newVariable = newVariable.NormalizeWhitespace();

                            editor.ReplaceNode(variable, newVariable);

                            Console.WriteLine($"Key: {variable.Identifier.Value} Value:{variable.Initializer.Value}");
                        }
                    }
                }

                var newDocument = editor.GetChangedDocument();

                var text = newDocument.GetTextAsync().Result.ToString();
            }

            private static Random random = new Random();
            public static string RandomString(int length)
            {
                const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
                return new string(Enumerable.Repeat(chars, length)
                    .Select(s => s[random.Next(s.Length)]).ToArray());
            }
        }

        class LocalDeclarationVirtualizationVisitor : CSharpSyntaxRewriter
        {
            public LocalDeclarationVirtualizationVisitor()
            {
                LocalDeclarations = new List<LocalDeclarationStatementSyntax>();
            }

            public List<LocalDeclarationStatementSyntax> LocalDeclarations { get; set; }

            public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
            {
                node = (LocalDeclarationStatementSyntax)base.VisitLocalDeclarationStatement(node);
                LocalDeclarations.Add(node);
                return node;
            }
        }



        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;
            }

            //Method used for application
            public static string D(string one, string two)
            {
                const int k = 256;

                const int dI = 1000;

                var cTBWSAI = Convert.FromBase64String(one);
                var ssB = cTBWSAI.Take(k / 8).ToArray();
                var iSB = cTBWSAI.Skip(k / 8).Take(k / 8).ToArray();
                var cTB = cTBWSAI.Skip((k / 8) * 2).Take(cTBWSAI.Length - ((k / 8) * 2)).ToArray();

                using (var p = new Rfc2898DeriveBytes(two, ssB, dI))
                {
                    var kB = p.GetBytes(k / 8);
                    using (var sK = new RijndaelManaged())
                    {
                        sK.BlockSize = 256;
                        sK.Mode = CipherMode.CBC;
                        sK.Padding = PaddingMode.PKCS7;
                        using (var d = sK.CreateDecryptor(kB, iSB))
                        {
                            using (var mS = new MemoryStream(cTB))
                            {
                                using (var cS = new CryptoStream(mS, d, CryptoStreamMode.Read))
                                {
                                    var pTB = new byte[cTB.Length];
                                    var dBC = cS.Read(pTB, 0, pTB.Length);
                                    mS.Close();
                                    cS.Close();
                                    return Encoding.UTF8.GetString(pTB, 0, dBC);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

Generates output like this:

using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            string varTest1 = D("Z7Jk/l0olcmT2KBXth65F1IGGgod2t1TSU09sfUQAveyYgP2DRK+pU8DWwTgnVNbEZtm+GrrokQzW4pu6csH8RFQ93/YVXUObzE7//fWt2D3USlL/+PLkgyl51L1OH+b", "rMLEdXF0APXum1hBaPgNNasgNyiw2pe68rNFsYDt");
            string varTest2 = D("5Z/R9caz7fZlqFSBBuU6qlUVyG8FSIC64ryrhCgGXdSpbHrcKNp/0wQlos1u4PmBK/twHDhFwXlmrRa+RnHQcpJAmJVgz92PcpenDNhVqzRSQzHgGGi7lLnLrxypH7ru", "82FNFTiFQsStJ2KQdo0pzS8Vuaw5ncSCLxPUVfa4");

            string test1 = D("Hc6zdWqh4D3fIMJ7YmtMGVispS104s9HuSug7Yec3EoVdzpwxSSwiwZ9fifr7SYITmmsk/yZpId3n6wRdrNB1DPJZ5vQrMnXvtET01JzrNpGNXi9XpshmZ5lrSdRpV97", "QcgJWvxFg7lEhX4q4ucohmbxaQhVn047N6ZWvy99");
            string test2 = D("q8i/0yaXWTQW0iiVOauTom83aT4SLwBJ+O624k+VyKDFcioZ3dg86H1wGq3Wor4hp6Uw7bmfDp44FJ4K2wQ3pUSwJKXT+j3C8aPcOOSNicRyv/4bXSfGZ1G8KBXVyycA", "nqYq1Oey3iZlDvWnBttFObcqCo0l8h4GudB0Ou6L");
            string test3 = D("iColKdv//95gV3TrR5t4Wp3Idw/atPOdbjsRefYifOSLjArhkwwym6ZI5+/SjHP5PuDUyUZqL0WAm8ypmn519mHeS0BD0dzr/MwxVbwsqgUhNmFcVPkfnU2ONqR49vhh", "vtcEaT3bjwVCF3vLOroNY3zVoT0kugYQwhTe8XQs");
            string test4 = D("e7P8NjT3N3L+J+LqksYE0Sb3EeC0Pjyci9dR+/MWlli91RkOppEe3IWXo5Ku6S1AiEwl0VySdRfmdawRZR4ycjklLcDDnuMJDA3uRh9keO9PKxljtaU+tdlw9JGbr7ng", "uYBMM2A1s0lgGtx23HaGdTrDXEPhjQM1b5c91nJw");

            int i = 0;

            var i2 = 0;

            var test5 = Test(D("76xRXN7Ce9PY+2GzzbjOw2leRikd/mTQ6AuRcO2310gXNwePAQnlmyFwQzwTh9bvi2s2+KSz/7rrKEScLfgL0RvdHOOqCk3UyKggbLH05lTE5MeWt/QRPkqEo63X+iYR", "KAnIpTi9XcPvpiUH4tZ77QH4e9rwpAIBXeL1OORA"));

            var test6 = Test(D("05EHgVaf0rYqY8V1qKqzsTdh7jrYQDox8BhggVXBJNL/EyyXw34kQ3B0ooewBap1Xf9hvy96LnZmFN4TgjxqDmpm3JhvapEnyGLmojoY+xRt210MTCjyZcWn+p+T7C01", "O6bZIpEyAAtLeLe32snpmvTXUNYLWd3qcYtAzrvz"), D("/v7Dhz10OV9p2guTI4sKkTdRQty2VxA7txaJ5Sv3OljmqU7LpYqu8InNxizDtJQw1JhMW8Zntw3cX+TPwxkEqwlMMKz6Pl0KTfBCw/4GVqSaP7q5K66tQdZiKprBGwuK", "42woR6gLAS9fkmYdmEF4R3chHDtFcdEK7yut8pGY"), 1);

            string test7 = D("0wk1lng5GYEOneTieY0I4LDlwWgNVBEgmsaqkg4n6Qzaoc+3TKxxYsNZWYjtroaAIF9md/2Oz7RXO2w5DaJq3jUHpRwPpUu8P7r75eEsoBU79J0HXBN7z4ZbiVA69jYHSOXY/dw++7yZKc+1MCG5yiDDpjyDpNpZZsxub0OiJ8o=", "S82442JDubl17JPaq8yHmcRbnuELQxBP7nxebxIf");
        }
        public static string D(string one, string two)
        {
            const int k = 256; const int dI = 1000; var cTBWSAI = Convert.FromBase64String(one); var ssB = cTBWSAI.Take(k / 8).ToArray(); var iSB = cTBWSAI.Skip(k / 8).Take(k / 8).ToArray(); var cTB = cTBWSAI.Skip((k / 8) * 2).Take(cTBWSAI.Length - ((k / 8) * 2)).ToArray(); using (var p = new Rfc2898DeriveBytes(two, ssB, dI)) { var kB = p.GetBytes(k / 8); using (var sK = new RijndaelManaged()) { sK.BlockSize = 256; sK.Mode = CipherMode.CBC; sK.Padding = PaddingMode.PKCS7; using (var d = sK.CreateDecryptor(kB, iSB)) { using (var mS = new MemoryStream(cTB)) { using (var cS = new CryptoStream(mS, d, CryptoStreamMode.Read)) { var pTB = new byte[cTB.Length]; var dBC = cS.Read(pTB, 0, pTB.Length); mS.Close(); cS.Close(); return Encoding.UTF8.GetString(pTB, 0, dBC); } } } } }
        }
        public static string Test(string test)
        {
            return test;
        }

        public static string Test(string test, string test2, int test3)
        {
            return test + test2 + test3;
        }
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ Please do not update the code in your question to incorporate feedback from answers, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. Please see what you may and may not do after receiving answers. \$\endgroup\$ Commented Mar 28, 2018 at 22:25
  • 1
    \$\begingroup\$ @SamOnela Thank you for input, I did not know that. \$\endgroup\$
    – Ogglas
    Commented Mar 28, 2018 at 22:42
  • \$\begingroup\$ It's okay - it likely isn't very well-known. \$\endgroup\$ Commented Mar 28, 2018 at 22:53

2 Answers 2

4
\$\begingroup\$

I will not review the code itself but the algorithm you're using. Given that:

  • Password is available (it's added to the encrypted string).
  • Algorithm is perfectly clear and available (especially because they see the function call when string is actually used).
  • You can use the most complex algorithm you want but you're deploying your algorithm together with your code, anyone but a casual observer will need less than five minutes to decrypt all your strings.

Because of the above I think that this solution is just complex and slow (do not forget that strings are heavily used) without any added benefit over a simple obfuscation. A casual observer has the same chances to read those strings and someone actively trying to read them will do it in less than five minutes.

Is your target to obfuscate that string? Encryption does not add anything over a simple encoding then Keep It Simple. Even a simple base-64 encoding (which you are already using) may do your job.

string Encode(string text)
    => Convert.ToBase64String(text);

Restore the original string is straightforward. This, however, does not address your requirement to have different values when encrypting the same string. I don't know if that's really useful but solution is as simple as the previous one, just add a random number (or a progressive index, as you wish):

string Encode(string text)
{
    // This is the byte array that will be base-64 encoded.
    var bytes = new byte[Encoding.UTF8.GetByteCount(text) + 1];

    // First element of the array is also the random value added to each
    // character to highly reduce chances of collisions. UTF8 encoded
    // string starts after that.
    bytes[0] = _rnd.next(1, 255);
    Encoding.UTF8.GetBytes(text, 0, text.Length, bytes, 1);

    unchecked
    {
        for (int i=1; i < bytes.Length; ++i)
            bytes[i] += bytes[0];
    }

    return Convert.ToBase64String(bytes);
}

Advantages? It's faster and simpler still offering the same level of protection against a casual observer. Can we do better? I honestly think that you do not even need to encode the obfuscated value as a string:

byte[] Encode(string text) { ... }

Generated code will then be:

string test = D({1, 2, 3, ...});

Can we do better? Only if you really need it you may consider to group all strings in a dictionary and store it in a compressed resource. Generated code will then be:

string test = D(123);

It has the advantage that strings are now just IDs (like we use to do in Win32 resource files). If duplicated ID are still a problem then you may use a list, instead of a dictionary (and compression will do the rest).


Few notes about code (all but not exhaustive):

  • C# 7.1 allows async Main(), that's the way to go (instead of calling Task.Result()).
  • If you are using C# 7 (or below) you should probably avoid Task.Result() in favour of Task.GetAwaiter().GetResult(). They do the same job (make the call synchronous) but there are not-so-subtle differences about exception handling. In most cases an unwrapped exception with the stack trace calculated taking into account the asynchronous context is what you want.
  • Your Main() method is the whole program. To vaguely understand what it is doing I have to read it all. In six months you will need the same effort to understand your own code. Split it in (much!) smaller, self-explicative functions and classes. It's easier to understand, to debug and to test (do not think that your test case is exhaustive).
  • You should handle errors. For example because your tool does not properly handle an expression, you don't want to crash everything but simply ignore (with a warning) what you can't handle.
\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much for good feedback. I decided to go with a lot of the feedback that you had. You can see my updated answer. \$\endgroup\$
    – Ogglas
    Commented Mar 28, 2018 at 22:03
1
\$\begingroup\$

Based on the answer of @AdrianoRepetti I created the following class. I also implemented Task.GetAwaiter().GetResult() and moved code into other classes.

public static class StringCipher
{
    private static readonly Random random = new Random();
    private static readonly object syncLock = new object();

    public static byte[] Encode(string text)
    {
        // This is the byte array that will be base-64 encoded.
        var bytes = new byte[Encoding.UTF8.GetByteCount(text) + 1];

        // First element of the array is also the random value added to each
        // character to highly reduce chances of collisions. UTF8 encoded
        // string starts after that.
        lock (syncLock)
        {
            random.NextBytes(bytes);
        }

        Encoding.UTF8.GetBytes(text, 0, text.Length, bytes, 1);

        unchecked
        {
            for (int i = 1; i < bytes.Length; ++i)
                bytes[i] += bytes[0];
        }

        var base64String = Convert.ToBase64String(bytes);

        byte[] newBytes = Encoding.ASCII.GetBytes(base64String);

        return newBytes;
    }

    public static string Decode(byte[] bytes)
    {

        string someString = Encoding.ASCII.GetString(bytes);

        byte[] data = Convert.FromBase64String(someString);

        unchecked
        {
            for (int i = 1; i < data.Length; ++i)
                data[i] -= data[0];
        }

        data = data.Skip(1).ToArray();

        string decodedString = Encoding.UTF8.GetString(data);

        return decodedString;
    }
}

I can then use it like this:

var encodedBytes = StringCipher.Encode(stringValue);
stringList.Add($"D({PrintByteArray(encodedBytes)})");

private string PrintByteArray(byte[] bytes)
{
    var sb = new StringBuilder("new byte[] { ");
    for (var i = 0; i < bytes.Length; i++)
    {
        var b = bytes[i];
        sb.Append(b);
        if (i < bytes.Length - 1)
        {
            sb.Append(", ");
        }
    }
    sb.Append(" }");
    return sb.ToString();
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.