Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#7.0

Invisible Ink

5.00/5 (10 votes)
24 Aug 2017CPOL14 min read 16.2K   142  
Using steganography to conceal text within a document or watermark a code file, using a whitespace encoder. Hide text in plain sight!

Table of Contents

Introduction

These days, it's commonplace to use icon fonts, such as Font Awesome, to display icons in your apps or web pages. Indeed UWP has a dedicated SymbolIcon control that materializes icons from the Segoe MDL2 font.

This hasn't always been the case. A couple of years ago, I was using unicode characters as application bar icons in a Windows Phone app. The characters were not monospaced; they didn't have a fixed width, and I needed a way to line them up correctly so that the first character of each menu item title was placed directly beneath the one above. It was then I discovered that there exists many unicode space characters, each with a different width. These characters solved my layout problem, but it also occurred to me that they could be put to a different purpose.

I've always been intrigued by ciphers and steganography, so I set about creating the means to encode plain text as unicode space characters. It would enable you to embed space-encoded text in plain sight within a document. You could watermark a document or code file, or transmit hidden information within the subject line of an email.

In this article you explore how to combine unicode space characters to represent ASCII text. You look at how to combine space-encoding with encryption, using the AES algorithm, to provide an extra layer of protection for your hidden text. You see how to harness the space-encoder in an app. Finally, you see how to generate random strings to unit test the encoder and encryptor.

Understanding the Sample Code Structure

The downloadable sample code contains three projects:

  • InvisibleInk
  • InvisibleInk.Tests
  • InvisibleInk.UI

The InvisibleInk project is a .NET Standard 1.4 class library, which means you can virtually reference it from any .NET project. See here for more info. The InvisibleInk project contains the logic for encoding and encrypting text. InvisibleInk.Tests is a unit test project for the InvisibleInk project, and InvisibleInk.UI is a UWP app that provides a user interface over the top of the InvisibleInk project.

Overview of the App

If you compile and launch the InvisibleInk.UI project, you're presented with the main page of the app. (See Figure 1.)

The Plain Text and Encoded Text text boxes are bi-directional. When you enter text into the Plain Text TextBox, the text is converted to unicode space characters and pushed into the Encoded Text TextBox. There's a copy to clipboard button beneath both boxes, to make transferring the text a easier.

To see for yourself that it really works, enter some text into the Plain Text box. Copy the Encoded Text to the clipboard and paste it into a text editor like Notepad. You'll see that it looks to be just blank space. Clear the Encoded Text box, and paste the encoded text back into the Encoded Text box. Voila, the spaces are decoded back to the original plain text.

Checking the Use Encryption check box introduces a second step in the encoding process. Before encoding is performed, the text in the Plain Text TextBox is encrypted using the AES algorithm. You explore this in greater detail later in the article.

NOTE: Enabling encryption increases the length of the encoded text.

The AES algorithm requires a valid key. If you happen to modify it, and encryption no longer works, use the refresh button beneath the Key text box. The refresh button restores the key to the first key, which was randomly generated when the app was launched for the first time.

Image 1
Figure 1. Invisible Ink App

You can place text containing both space-encoded strings and plain text into the Encoded Text text box. The app is able to recognize and extract space-encoded strings that are intermingled with plain text.

Using the Space Encoder

The SpaceEncoder class, in the downloadable sample code, contains a EncodeAsciiString method, which accepts a plain text string and returns a space encoded string. To decode a previously encoded string, use the DecodeSpaceString, as shown in the following example:

C#
string encoded = encoder.EncodeAsciiString("Make this hidden");
// encoded == "                         ";
string original = encoder.DecodeSpaceString(encoded);
// original == "Make this hidden"

SpaceEncoder uses the following array of unicode characters to encode an ASCII string into a unicode space string:

C#
readonly char[] characters =
{
    '\u0020', /* Space */
    '\u00A0', /* No-Break Space */
    '\u1680', /* Ogham Space Mark */
    '\u180E', /* Mongolian Vowel Separator */
    '\u2000', /* En Quad */
    '\u2001', /* Em Quad */
    '\u2002', /* En Space */
    '\u2003', /* Em Space */
    '\u2004', /* Three-Per-Em Space */
    '\u2005', /* Four-Per-Em Space */
    '\u2006', /* Six-Per-Em Space */
    '\u2007', /* Figure Space */
    '\u2008', /* Punctuation Space */
    '\u2009', /* Thin Space */
    '\u200A', /* Hair Space */
    '\u202F', /* Narrow No-Break Space */
    '\u205F', /* Medium Mathematical Space */
    '\u3000' /* Ideographic Space */
};

You can retrieve the characters from a SpaceEncoder object using its SpaceCharacters property.

The SpaceEncoder class only supports the encoding of ASCII text. Using ASCII means that the encoded output is a lot shorter because of ASCII's limited character set. I didn't want to come up with a scheme for both unicode and ASCII. Encoding a string begins by ensuring that the string is only comprised of ASCII characters. (See Listing 1.) If a non-ASCII character is encountered—a character that has a value greater than 255 (0xFF hex)—it is replaced with a ? character.

Listing 1. SpaceEncoder.ConvertStringToAscii method

C#
static byte[] ConvertStringToAscii(string text)
{
    int length = text.Length;
    byte[] result = new byte[length];
    
    for (var ix = 0; ix < length; ++ix)
    {
        char c = text[ix];
        if (c <= 0xFF)
        {
            result[ix] = (byte)c;
        }
        else
        {
            result[ix] = (byte)'?';
        }
    }
    
    return result;
}

The SpaceEncoder.EncodeAsciiString method takes the result of the ConvertStringToAscii method and creates a digram representation of each ASCII character. (See Listing 2.)

We only have at most 18 space characters to work with, so we use two space characters to cover the 256 unique ASCII characters. This includes the standard ASCII characters (0-127) and Extended ASCII characters (128-255). I chose to use only the first 16 space characters, which gives us 256 (16 x 16) two character combinations.

The resulting character array is concatenated and returned as a string.

Listing 2. EncodeAsciiString method.

C#
public string EncodeAsciiString(string text)
{
    byte[] asciiBytes = ConvertStringToAscii(text);
    char[] encryptedBytes = new char[asciiBytes.Length * 2];
    int encryptedByteCount = 0;
    
    int stringLength = asciiBytes.Length;
    
    for (var i = 0; i < stringLength; i++)
    {
        short asciiByte = asciiBytes[i];
        short highPart = (short)(asciiByte / 16);
        short lowPart = (short)(asciiByte % 16);
        
        encryptedBytes[encryptedByteCount] = characters[highPart];
        encryptedBytes[encryptedByteCount + 1] = characters[lowPart];
        encryptedByteCount += 2;
    }
    
    var result = string.Join(string.Empty, encryptedBytes);
    return result;
}

Decoding a space-encoded string is performed by the SpaceEncoder.DecodeSpaceString method. (See Listing 3.)

The index of a space character in the characters array serves as its value in the encoding algorithm. The ASCII character's numeric value is equal to the first space character's index * 16 + the second space character's index.

Encoding.ASCII is used to turn the resulting byte array back into a string.

Listing 3. SpaceEncoder.DecodeSpaceString method

C#
public string DecodeSpaceString(string spaceString)
{
    int spaceStringLength = spaceString.Length;
    
    byte[] asciiBytes = new byte[spaceStringLength / 2];
    
    int arrayLength = 0;
    
    for (int i = 0; i < spaceStringLength; i += 2)
    {
        char space1 = spaceString[i];
        char space2 = spaceString[i + 1];
        short index1 = characterIndexDictionary[space1];
        short index2 = characterIndexDictionary[space2];
        
        int highPart = index1 * 16;
        short lowPart = index2;
        
        int asciiByte = highPart + lowPart;
        asciiBytes[arrayLength] = (byte)asciiByte;
        arrayLength++;
    }
    
    string result = asciiEncoding.GetString(asciiBytes, 0, asciiBytes.Length);
    return result;
}

To revert the ASCII bytes back to the original string we use the asciiEncoding field, which is defined as shown:

C#
readonly Encoding asciiEncoding = Encoding.GetEncoding("iso-8859-1");

Notice that we didn't use the Encoding.Ascii to revert the ASCII bytes back to their original form. That's because SpaceEncoder supports the extended set of ASCII characters—the entire set of 256 characters—and Encoding.Ascii only includes the standard seven-bit ASCII characters.

Combining Encoding with Encryption

For most purposes you probably won't want to use encryption; it increases the length of the encoded string. If, however, you really want to keep the text away from prying eyes, you need to apply a layer of encryption.

The AesEncryptor class, in the downloadable sample code, is a helper class that leverages the System.Security.Cryptography.Aes class to encrypt text before space-encoding, and to decrypt text after it has been decoded.

The EncryptString method accepts a plain-text string, and encrypts the string using the provided key and IV (initialization vector). (See Listing 4.) We look at the IV in a later section.

An Aes object is created via the static Aes.Create() method. Aes implements IDisposable so we wrap it in a using statement. The plain text is encrypted by writing it to a CryptoStream.

Listing 4. AesEncryptor.EncryptString method

C#
public byte[] EncryptString(string plainText, byte[] key, byte[] iv)
{
    if (string.IsNullOrEmpty(plainText))
    {
    	throw new ArgumentNullException(nameof(plainText));
    }
    
    if (key == null || key.Length <= 0)
    {
    	throw new ArgumentException(nameof(key));
    }
    
    if (iv == null || iv.Length <= 0)
    {
    	throw new ArgumentException(nameof(iv));
    }
    
    byte[] encrypted;
    
    using (Aes aes = Aes.Create())
    {
    	aes.Key = key;
    	aes.IV = iv;
    
    	ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
    
        using (MemoryStream memoryStream = new MemoryStream())
        {
            using (CryptoStream cryptoStream = new CryptoStream(
            			memoryStream, encryptor, CryptoStreamMode.Write))
            {
            	using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
            	{
                    streamWriter.Write(plainText);
            	}
            	encrypted = memoryStream.ToArray();
            }
        }
    }
    
    return encrypted;
}

To decrypt a byte array, call the AesEncryptor object's DecryptBytes method. DecryptBytes works similarly to EncryptString, but rather than writing to the CryptoStream, it reads from it. (See Listing 5.)

Listing 5. AesEncryptor.DecryptBytes method

C#
public string DecryptBytes(byte[] cipherText, byte[] key, byte[] iv)
{
    if (cipherText == null || cipherText.Length <= 0)
    {
    	throw new ArgumentException(nameof(cipherText));
    }
    
    if (key == null || key.Length <= 0)
    {
    	throw new ArgumentException(nameof(key));
    }
    
    if (iv == null || iv.Length <= 0)
    {
    	throw new ArgumentNullException(nameof(iv));
    }
    
    string result;

    using (Aes aes = Aes.Create())
    {
        aes.Key = key;
        aes.IV = iv;
        
        ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

        using (MemoryStream memoryStream = new MemoryStream(cipherText))
        {
            using (CryptoStream cryptoStream = new CryptoStream(
            			memoryStream, decryptor, CryptoStreamMode.Read))
            {
                using (StreamReader streamReader = new StreamReader(cryptoStream))
                {
                    result = streamReader.ReadToEnd();
                }
            }
        }
    }

    return result;
}

Harnessing the Space-Encoder in an App

Now let's turn our attention to the app. In this section you see how to use the SpaceEncoder and AesEncryptor classes in a UWP app.

The MainViewModel class in the InvisibleInk.UI project is where all of the logic resides for this simple app.

The app uses the Codon Framework for dependency injection, settings, and INPC (INotifyPropertyChanged) notifications.

The MainViewModel class relies on dependency injection to receive an instance Codon's ISettingsService. It's a cross-platform compatible way to store transient, roaming, and local settings. A Codon ILog instance is also required. There are default services for these types, so no initialization is necessary.

When MainViewModel is instantiated, it looks for the last used AES key value. (See Listing 6.) If it doesn't find one, it uses the AesEncryptor to generate a random key, which is then stored as a setting.

Listing 6. MainViewModel constructor

C#
public MainViewModel(ISettingsService settingsService, ILog log)
{
    this.settingsService = settingsService 
        ?? throw new ArgumentNullException(nameof(settingsService));
    this.log = log ?? throw new ArgumentNullException(nameof(log));
    
    string keySetting;
    if (settingsService.TryGetSetting(keySettingId, out keySetting) 
    		&& !string.IsNullOrWhiteSpace(keySetting))
    {
    	key = keySetting;
    }
    else
    {
    	AesParameters parameters = encryptor.GenerateAesParameters();
    	var keyBytes = parameters.Key;
    	key = Convert.ToBase64String(keyBytes);
    	settingsService.SetSetting(keySettingId, key);
    	settingsService.SetSetting(firstKeyId, key);
    }
}

MainViewModel has the following public properties:

  • PlainText (string)
  • EncodedText (string)
  • Key (string)
  • CopyToClipboardCommand (ActionCommand)
  • UseEncryption (nullable bool)

These properties are data-bound to controls in MainPage.xaml. When the PlainText property is modified, it triggers encoding of the text. (See Listing 7.)

The Set method is located in Codon's ViewModelBase class. In Codon, INPC is automatically raised on the UI thread, which alleviates having to litter your asynchronous code with Dispatcher Invoke calls.

The Plain Text and Encoded Text boxes are bi-directional; if you enter text in one, the other updates. That is why OnPropertyChanged is called for the EncodedText property in the PlainText property's setter. If we were to use the EncodedText property to set the new value, a stack overflow exception would ensue.

Listing 7. MainViewModel.PlainText property

C#
string plainText;

public string PlainText
{
    get => plainText;
    set
    {
        if (Set(ref plainText, value) == AssignmentResult.Success)
        {
            encodedText = Encode(plainText);
            OnPropertyChanged(nameof(EncodedText));
        }
    }
}

When the PlainText property's value changes, the Encode method is called to encode the new text. (See Listing 8.)

If the Use Encryption check-box is unchecked, then the SpaceEncoder is used to encode the text. Conversely, if encryption is enabled, another step takes place before the text is encoded. An IV is generated using the encryptor and used during encryption and decryption. The IV is different every time and serves to randomize the result. The IV is prepended to the byte array.

Should the level of encryption change, we need to record the length of the IV. This is done by reserving the first two bytes of the resulting byte array for the length of the IV. We fill up the first byte by casting ivLength to a byte. The second byte receives the next 'high-part' by bit-shifting 8 places.

NOTE: An Int32 requires 4 bytes to be represented correctly. Since we are, however, trying to keep the length of the encoded text to a minimum and I can't foresee the IV being larger than 65K characters, I chose to use only 2 bytes.

ivBytes and encryptedBytes are then combined and converted to Base64. I used Base64 encoding to ensure that the byte array that is passed to the SpaceEncoder contains only ASCII characters.

Listing 8. MainViewModel.Encode method

C#
string Encode(string text)
{
    if (string.IsNullOrEmpty(text))
    {
    	return string.Empty;
    }
    
    string textTemp;
    
    if (useEncryption == true)
    {
        try
        {
            byte[] ivBytes = encryptor.GenerateAesParameters().IV;
            byte[] encryptedBytes = encryptor.EncryptString(text, GetBytes(key), ivBytes);
            
            byte[] allBytes = new byte[ivBytes.Length + encryptedBytes.Length + 2];
            int ivLength = ivBytes.Length;
            /* The first two bytes store the length of the IV. */
            allBytes[0] = (byte)ivLength;
            allBytes[1] = (byte)(ivLength >> 8);
            
            Array.Copy(ivBytes, 0, allBytes, 2, ivLength);
            Array.Copy(encryptedBytes, 0, allBytes, ivLength + 2, encryptedBytes.Length);
            
            textTemp = Convert.ToBase64String(allBytes);
        }
        catch (Exception ex)
        {
            Dependency.Resolve<ILog>().Debug("Encoding failed.", ex);
            return string.Empty;
        }
    }
    else
    {
    	textTemp = text;
    }
    
    var result = encoder.EncodeAsciiString(textTemp);
    return result;
}

Decoding Text within MainViewModel

When the user modifies the text in the 'Encoded Text' box, the Decode method is called. (See Listing 9.)

Text that is pasted into the 'Encoded Text' box may contain a combination of space-encoded text intermingled with plain text. The Decode method constructs a regular expression using the character codes of the characters in the SpaceEncoder object's SpaceCharacters collection. If it finds a substring containing 4 or more space characters in sequence, then it assumes it's a space-encoded section; and attempts to decode it.

In retrospect, I probably could have used a short sequence of spaces to mark the beginning of a sequence, but hey ho.

Listing 9. MainViewModel.Decode method

C#
string Decode(string encodedText)
{
    var spaceCharacters = encoder.SpaceCharacters;
    
    var sb = new StringBuilder();
    
    foreach (char c in spaceCharacters)
    {
    	string encodedValue = "\\u" + ((int)c).ToString("x4");
    	sb.Append(encodedValue);
    }
    
    string regexString = "(?<spaces>[" + sb.ToString() + "]{4,})";
    Regex regex = new Regex(regexString);
    
    var matches = regex.Matches(encodedText);
    
    sb.Clear();
    
    foreach (Match match in matches)
    {
    	string spaces = match.Groups["spaces"].Value;
    	try
    	{
            string decodedSubstring = DecodeSubstring(spaces);
            sb.AppendLine(decodedSubstring);
    	}
    	catch (Exception ex)
    	{
            log.Debug("Failed to decode substring.", ex);
    	}
    }
    
    return sb.ToString();
}

Each match is passed to the MainViewModel object's DecodeSubstring method. (See Listing 10.)

The SpaceEncoder decodes the encoded text. If encryption is not enabled, then the decoded text is simply returned to the Decode method.

If encryption is enabled, then the length of the IV is determined using the first two bytes of the allBytes array. The IV bytes and the encrypted text bytes are split into two arrays, allowing the AesEntryptor to decrypt the encrypted text byte array using the IV byte array.

Listing 10. MainViewModel.DecodeSubstring method

C#
string DecodeSubstring(string encodedText)
{
    if (string.IsNullOrEmpty(encodedText))
    {
    	return string.Empty;
    }
    
    string unencodedText = encoder.DecodeSpaceString(encodedText);

    if (useEncryption == true)
    {
        try
        {
            byte[] allBytes = Convert.FromBase64String(unencodedText);
            int ivLength = allBytes[0] + allBytes[1];
            
            byte[] ivBytes = new byte[ivLength];
            Array.Copy(allBytes, 2, ivBytes, 0, ivLength);
            
            int encryptedBytesLength = allBytes.Length - (ivLength + 2);
            byte[] encryptedBytes = new byte[encryptedBytesLength];
            Array.Copy(allBytes, ivLength + 2, encryptedBytes, 0, encryptedBytesLength);
            
            string text = encryptor.DecryptBytes(encryptedBytes, GetBytes(key), ivBytes);
            return text;
        }
        catch (Exception ex)
        {
            log.Debug("Failed to decrypt bytes.", ex);
            return string.Empty;
        }
    }

    return unencodedText;
}

Refreshing the AES Key

The AES key is randomly generated when the app is launched for the first time. If the key is modified, and has an invalid length, it prevents the AES encryption and decryption from functioning. For that reason, the user can choose to restore the key to its value when the app was launched for the first time.

The MainViewModel contains a RefreshKeyCommand as shown in the following excerpt:

C#
ActionCommand refreshKeyCommand;

public ICommand RefreshKeyCommand => refreshKeyCommand
        ?? (refreshKeyCommand = new ActionCommand(RefreshKey));

RefreshKeyCommand calls the RefreshKey method when the user taps the Refresh button on the main page. (See Listing 11.)

RefreshKey attempts to set the Key property to the value of the firstKeyId setting, which was stored when the app was launched for the first time.

If, for whatever reason, the settings does not contain a first key setting, then a new one is generated and stored.

Listing 11. MainViewModel.RefreshKey method

C#
void RefreshKey(object arg)
{
    string originalKey;
    if (settingsService.TryGetSetting(firstKeyId, out originalKey))
    {
        Key = originalKey;
    }
    else
    {
        /* Shouldn't get here unless something went awry with the settings. */
        AesParameters parameters = encryptor.GenerateAesParameters();
        var keyBytes = parameters.Key;
        
        Key = Convert.ToBase64String(keyBytes);
        settingsService.SetSetting(firstKeyId, key);
    }
}

Setting the Key property causes its value to be stored in the settings, as shown in the following excerpt:

C#
string key;

public string Key
{
    get => key;
    set
    {
        if (Set(ref key, value) == AssignmentResult.Success)
        {
            settingsService.SetSetting(keySettingId, key);
        }
    }
}

Examing the MainPage

The MainViewModel is created within the MainPage constructor. (See Listing 12.)

Codon's Dependency class is used to retrieve an instance of MainViewModel from the IoC container. The MainPage class has a ViewModel property so that we can make use of x:Bind in our binding expression in MainPage.xaml.

Listing 12. MainPage constructor

C#
public MainPage()
{
    var vm = Dependency.Resolve<MainViewModel>();
    ViewModel = vm;
    vm.PropertyChanged += HandleViewModelPropertyChanged;
    DataContext = vm;
    		
    InitializeComponent();
    
    encodedTextBox.SelectionHighlightColorWhenNotFocused = new SolidColorBrush(Colors.PowderBlue);
    encodedTextBox.SelectionHighlightColor = new SolidColorBrush(Colors.PowderBlue);
}

Highlighting Encoded Text

The selection highlight color of the encoded text box is set so that the user can see the spaces being generated. When either the PlainText or UseEncryption view-model properties are modified, the encoded text box's SelectAll method is called. (See Listing 13.) This then shows the spaces in the SelectionHighlightColorWhenNotFocused color.

The call to SelectAll is pushed onto the UI thread queue so that it occurs after the text is replaced in the encoded text box. I might have used a different, more view-model centric, approach for selecting the text. But frankly, this was simpler.

Listing 13. MainPage.HandleViewModelPropertyChanged method

C#
void HandleViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    var propertyName = e.PropertyName;
    if (propertyName == nameof(MainViewModel.PlainText) 
        || propertyName == nameof(MainViewModel.UseEncryption))
    {
        Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            encodedTextBox.SelectAll();
        });
    }
}

MainPage.xaml contains three TextBox controls, representing the plain text, encoded text and AES key. (See Listing 14.) A CheckBox is used to enable or disable encryption.

You might wonder why the 'Plain Text' and 'Encoded Text' TextBox controls are using {Binding ...} and not x:Bind. The reason is that x:Bind does not support the UpdateSourceTrigger parameter. Ordinarily, when the text in a TextBox is modified by the user, the value is not pushed to its binding source until the control loses focus. We want encoding and decoding to be performed as soon as the text is modified.

Listing 14. MainPage.xaml

XML
<Page x:Class="Outcoder.Cryptography.InvisibleInkApp.MainPage" ...>
    
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
    	Margin="12">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        
        <StackPanel Grid.Row="0" Margin="{StaticResource ListItemMargin}">
        	<TextBlock Text="Place text in the 'Plain Text' box. 
        It is encoded as spaces in the 'Encoded Text' box. 
        Paste text into the 'Encoded Text' box and it is converted back to plain text."
                    TextWrapping="WrapWholeWords" />
        </StackPanel>
        
        <TextBox Header="Plain Text"
            Text="{Binding PlainText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Margin="{StaticResource ListItemMargin}"
            TextWrapping="Wrap"
            AcceptsReturn="True"
            ScrollViewer.VerticalScrollBarVisibility="Auto"
            Grid.Row="1" />
        <Button Command="{x:Bind ViewModel.CopyToClipboardCommand}"
            	CommandParameter="PlainText"
            	Margin="{StaticResource ButtonMargin}"
            	Grid.Row="2">
            <SymbolIcon Symbol="Copy"/>
        </Button>
        
        <TextBox x:Name="encodedTextBox"
            Header="Encoded Text"
            Text="{Binding EncodedText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Margin="{StaticResource ListItemMargin}"
            TextWrapping="Wrap"
            AcceptsReturn="True"
            IsTextPredictionEnabled="False"
            ScrollViewer.VerticalScrollBarVisibility="Auto"
            Grid.Row="3" />
        
        <Button Command="{x:Bind ViewModel.CopyToClipboardCommand}"
            CommandParameter="EncodedText"
            Margin="{StaticResource ButtonMargin}"
            Grid.Row="4">
            <SymbolIcon Symbol="Copy" />
        </Button>
        
        <Border BorderThickness="2" BorderBrush="Gray" 
            CornerRadius="0" Padding="12"
            Grid.Row="5" Margin="0,24,0,0">
            <StackPanel>
                <CheckBox Content="Use Encryption"
                    IsChecked="{x:Bind ViewModel.UseEncryption, Mode=TwoWay}"
                    Margin="0" />
                <TextBlock Text="Increases the length of the encoded string if enabled."
                    TextWrapping="WrapWholeWords"
                    Margin="{StaticResource ListItemMargin}" />
                <TextBox Header="Key"
                    Text="{x:Bind ViewModel.Key, Mode=TwoWay}"
                    Margin="{StaticResource ListItemMargin}" />
                <Button Command="{x:Bind ViewModel.RefreshKeyCommand}"
                    Margin="{StaticResource ButtonMargin}">
                    <SymbolIcon Symbol="Refresh"/>
            	</Button>
            </StackPanel>
        </Border>
    </Grid>
</Page>

Unit Testing the Encoder and Encryptor

For unit testing the InvisibleInk (.NET Standard) project I created a desktop CLR unit test project.

When using .NET Standard its important to be cognizant of the various .NET implementations targeting specific versions of .NET Standard, and to understand the compatibility matrix over at .NET Standard at Microsoft Docs.

Even so, when I created the InvisibleInk.Tests project and referenced the InvisibleInk (.NET Standard) project, I encountered a confusing build issue. The test project and the InvisibleInk project expected different versions of the System.Security.Cryptography.Algorithms and the System.Security.Cryptography.Primitives assemblies. I fiddled with the .NET version of the test project and switched between the .NET Standard version of the InvisibleInk project, but nothing resolved the issue. Finally, after some searching I came across a work around; I added a binding redirect in the test projects app.config file. (See Listing 15.)

The binding redirect forces the test project to use the same version that the .NET Standard project expected.

Listing 15. InvisibleInk.Tests App.config

XML
<configuration>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="System.Security.Cryptography.Algorithms" 
                    publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
                <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="System.Security.Cryptography.Primitives" 
                    publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
                <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.0.1.0" />
            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

The InvisibleInk.Tests project contains unit test for the SpaceEncoder and AesEncryptor classes.

Unit Testing the AesEncryptor Class

To test the SpaceEncoder we generate a random string, feed it to the encoder and then decode it to verify the result is the same. (See Listing 16.)

We ensure that the string contains only white-space characters by ensuring that the encoder's space characters contains each character of the encoded text.

Listing 16. SpaceEncoderTests class

C#
[TestClass]
public class SpaceEncoderTests
{
    readonly Random random = new Random();
    
    [TestMethod]
    public void ShouldEncodeAndDecode()
    {
        var encoder = new SpaceEncoder();
        
        string whiteSpaceCharacters = encoder.GetAllSpaceCharactersAsString();
        var stringGenerator = new StringGenerator();
        
        for (int i = 0; i < 1000; i++)
        {
            string s = stringGenerator.CreateRandomString(random.Next(0, 30));
            var encoded = encoder.EncodeAsciiString(s);
            
            Assert.IsNotNull(encoded);
            
            foreach (char c in encoded)
            {
            	Assert.IsTrue(whiteSpaceCharacters.Contains(c));
            }
            
            var unencoded = encoder.DecodeSpaceString(encoded);
            Assert.AreEqual(s, unencoded);
        } 
    }
}

The StringGenerator class, in the InvisibleInk.Tests project, generates a random string using a concise LINQ expression, inspired by this StackOverflow answer. (See Listing 17.)

We generate a string containing the 256 ASCII characters by using the Enumerable.Range method and casting each value to a char.

Listing 17. StringGenerator class

C#
class StringGenerator
{
    readonly Random random = new Random();
    readonly string asciiCharacters
        = new string(Enumerable.Range(0, 255).Select(x => (char)x).ToArray());
    
    public string CreateRandomString(int length)
    {
        return new string(Enumerable.Repeat(asciiCharacters, length)
        	.Select(s => s[random.Next(s.Length)]).ToArray());
    }
}

Unit Testing the AesEncryptor Class

The AesEncryptorTests class uses the same StringGenerator class to test that the AesEncryptor is able to correctly encrypt and decrypt random strings. (See Listing 18.)

Listing 18. AesEncryptorTests class

C#
[TestClass]
public class AesEncryptorTests
{
    readonly Random random = new Random();
    
    [TestMethod]
    public void ShouldEncryptAndDecrypt()
    {
        var aesEncryptor = new AesEncryptor();
        var stringGenerator = new StringGenerator();
        
        for (int i = 0; i < 1000; i++)
        {
            string randomString = stringGenerator.CreateRandomString(random.Next(1, 30));
            
            var parameters = aesEncryptor.GenerateAesParameters();
            byte[] keyBytes = parameters.Key;
            byte[] ivBytes = parameters.IV;
            
            byte[] encryptedBytes = aesEncryptor.EncryptString(randomString, keyBytes, ivBytes);
            
            Assert.IsNotNull(encryptedBytes);
            
            string unencrypted = aesEncryptor.DecryptBytes(encryptedBytes, keyBytes, ivBytes);
            
            Assert.AreEqual(randomString, unencrypted);
        }
    }
}

Conclusion

In this article you explored how to use unicode space characters to represent ASCII text. You looked at how to combine space-encoding with encryption, using the AES algorithm, to provide an extra layer of protection for your hidden text. You saw how to harness the space-encoder in an app. Finally, you saw how to generate random strings to unit test the encoder and encryptor.

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

History

  • 2017/08/24 First published.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)