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://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:
- Make the compiler obfuscate strings. I've seen some examples in C++ using a preprocessor at compile time but this is not what I want. Given the C# preprocessor directives I don't think it can be done like this anyway. https://stackoverflow.com/a/4102533/3850405 and https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/
- Generated symbols should not collide
- Identical strings should not look the same
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;
}
}
}