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.
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:
string encoded = encoder.EncodeAsciiString("Make this hidden");
string original = encoder.DecodeSpaceString(encoded);
SpaceEncoder
uses the following array of unicode characters to encode an ASCII string into a unicode space string:
readonly char[] characters =
{
'\u0020',
'\u00A0',
'\u1680',
'\u180E',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u202F',
'\u205F',
'\u3000'
};
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
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.
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
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:
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
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
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
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
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
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;
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
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
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:
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
void RefreshKey(object arg)
{
string originalKey;
if (settingsService.TryGetSetting(firstKeyId, out originalKey))
{
Key = originalKey;
}
else
{
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:
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
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
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
<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
<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
[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
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
[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.