隐形墨水





5.00/5 (10投票s)
使用隐写术通过空白字符编码器将文本隐藏在文档或水印代码文件中。隐藏文本,一目了然!
- 从 Windows 应用商店安装应用 (需要 Windows 10 创意者更新)
- 下载 InvisibleInk-master.zip - 210.3 KB
- Invisible Ink GitHub 仓库
目录
引言
如今,使用图标字体(如 Font Awesome)在应用或网页中显示图标已是司空见惯。事实上,UWP 有一个专用的 SymbolIcon
控件,可以呈现来自 Segoe MDL2 字体的图标。
过去并非如此。几年前,我在一个 Windows Phone 应用中使用 Unicode 字符作为应用栏图标。这些字符不是等宽的;它们的宽度不固定,我需要一种方法来正确地对齐它们,以便每个菜单项标题的第一个字符正好位于其上方字符的正下方。那时我发现存在许多 Unicode 空格字符,它们各自的宽度不同。这些字符解决了我的布局问题,但我也意识到它们可以用于不同的目的。
我一直对密码和隐写术很感兴趣,于是我开始着手创建一种方法,将明文编码为 Unicode 空格字符。这将使您能够在文档中一目了然地嵌入空格编码的文本。您可以为文档或代码文件添加水印,或在电子邮件的主题行中传输隐藏信息。
在本文中,您将探讨如何组合 Unicode 空格字符来表示 ASCII 文本。您将了解如何将空格编码与加密(使用 AES 算法)相结合,为隐藏文本提供额外的保护层。您将看到如何在应用中利用空格编码器。最后,您将看到如何生成随机字符串来对编码器和加密器进行单元测试。
理解示例代码结构
可下载的示例代码包含三个项目
- InvisibleInk
- InvisibleInk.Tests
- InvisibleInk.UI
InvisibleInk 项目是一个 .NET Standard 1.4 类库,这意味着您可以几乎从任何 .NET 项目中引用它。有关更多信息,请参见此处。InvisibleInk 项目包含用于编码和加密文本的逻辑。InvisibleInk.Tests 是 InvisibleInk 项目的单元测试项目,而 InvisibleInk.UI 是一个 UWP 应用,它提供了 InvisibleInk 项目之上的用户界面。
应用概述
如果编译并运行 InvisibleInk.UI 项目,您将看到应用的主页面。(参见图 1。)
纯文本和编码文本文本框是双向的。当您在纯文本 TextBox
中输入文本时,该文本将被转换为 Unicode 空格字符并推送到编码文本 TextBox
中。两个框下方都有一个复制到剪贴板按钮,以方便传输文本。
为了亲自验证它是否真的有效,请在纯文本框中输入一些文本。将编码文本复制到剪贴板,然后粘贴到像记事本这样的文本编辑器中。您会看到它看起来只是空白。清空编码文本框,然后将编码文本粘贴回编码文本框。瞧,空格会被解码回原始纯文本。
勾选使用加密复选框会在编码过程中引入第二步。在执行编码之前,将使用 AES 算法加密纯文本 TextBox
中的文本。您将在后面的文章中详细探讨这一点。
注意:启用加密会增加编码文本的长度。
AES 算法需要一个有效的密钥。如果您修改了它,并且加密不再起作用,请使用密钥文本框下方的刷新按钮。刷新按钮会将密钥恢复到应用首次启动时随机生成的第一个密钥。

您可以将包含空格编码字符串和纯文本的文本放入编码文本框中。该应用能够识别并提取与纯文本混合在一起的空格编码字符串。
使用空格编码器
可下载示例代码中的 SpaceEncoder
类包含一个 EncodeAsciiString
方法,该方法接受纯文本字符串并返回一个空格编码字符串。要解码先前编码的字符串,请使用 DecodeSpaceString
,如下例所示
string encoded = encoder.EncodeAsciiString("Make this hidden");
// encoded == " ";
string original = encoder.DecodeSpaceString(encoded);
// original == "Make this hidden"
SpaceEncoder
使用以下 Unicode 字符数组将 ASCII 字符串编码为 Unicode 空格字符串
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 */
};
您可以使用 SpaceEncoder
对象的 SpaceCharacters
属性检索字符。
SpaceEncoder
类仅支持 ASCII 文本的编码。使用 ASCII 意味着编码输出由于 ASCII 的字符集有限而更短。我不想为 Unicode 和 ASCII 制定一种方案。编码字符串首先确保字符串仅由 ASCII 字符组成。(参见清单 1。)如果遇到非 ASCII 字符(值大于 255(0xFF 十六进制)的字符),则将其替换为 ?
字符。
清单 1. SpaceEncoder.ConvertStringToAscii 方法
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;
}
SpaceEncoder.EncodeAsciiString
方法获取 ConvertStringToAscii
方法的结果,并为每个 ASCII 字符创建 二元组 表示。(参见清单 2。)
我们最多只有 18 个空格字符可供使用,因此我们使用两个空格字符来覆盖 256 个唯一的 ASCII 字符。这包括标准 ASCII 字符(0-127)和扩展 ASCII 字符(128-255)。我选择只使用前 16 个空格字符,这给了我们 256 种(16 x 16)双字符组合。
生成的字符数组被连接起来并作为字符串返回。
清单 2. EncodeAsciiString 方法。
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;
}
解码空格编码字符串由 SpaceEncoder.DecodeSpaceString
方法执行。(参见清单 3。)
空格字符在 characters
数组中的索引作为其在编码算法中的值。ASCII 字符的数值等于第一个空格字符的索引 * 16 + 第二个空格字符的索引。
Encoding.ASCII
用于将生成的字节数组还原为字符串。
清单 3. SpaceEncoder.DecodeSpaceString 方法
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;
}
为了将 ASCII 字节还原回原始字符串,我们使用 asciiEncoding
字段,该字段定义如下
readonly Encoding asciiEncoding = Encoding.GetEncoding("iso-8859-1");
请注意,我们没有使用 Encoding.Ascii
将 ASCII 字节还原回其原始形式。这是因为 SpaceEncoder
支持扩展 ASCII 字符集——整个 256 个字符集——而 Encoding.Ascii
只包含标准的七位 ASCII 字符。
将编码与加密相结合
在大多数情况下,您可能不想使用加密;它会增加编码字符串的长度。但是,如果您确实想保护文本不被窥探,则需要应用加密层。
可下载示例代码中的 AesEncryptor
类是一个辅助类,它利用 System.Security.Cryptography.Aes
类在空格编码之前加密文本,并在解码后解密文本。
EncryptString
方法接受明文字符串,并使用提供的密钥和 IV(初始化向量)加密字符串。(参见清单 4。)我们将在后面的部分讨论 IV。
通过静态 Aes.Create()
方法创建 Aes
对象。Aes
实现 IDisposable
,因此我们将其包装在 using
语句中。通过将明文写入 CryptoStream
来加密明文。
清单 4. AesEncryptor.EncryptString 方法
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;
}
要解密字节数组,请调用 AesEncryptor
对象的 DecryptBytes
方法。DecryptBytes
的工作方式与 EncryptString
类似,但它从 CryptoStream
读取数据,而不是写入数据。(参见清单 5。)
清单 5. AesEncryptor.DecryptBytes 方法
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;
}
在应用中利用空格编码器
现在我们来看一下这个应用。在本节中,您将看到如何在 UWP 应用中使用 SpaceEncoder
和 AesEncryptor
类。
InvisibleInk.UI 项目中的 MainViewModel
类包含了这个简单应用的所有逻辑。
该应用使用 Codon 框架 进行依赖注入、设置和 INPC(INotifyPropertyChanged)通知。
MainViewModel
类依赖于依赖注入来接收 Codon 的 ISettingsService
实例。这是一种跨平台兼容的存储瞬态、漫游和本地设置的方法。还需要 Codon 的 ILog
实例。对于这些类型有默认服务,因此无需进行初始化。
当实例化 MainViewModel
时,它会查找最后使用的 AES 密钥值。(参见清单 6。)如果找不到,它会使用 AesEncryptor
生成一个随机密钥,然后将其存储为设置。
清单 6. MainViewModel 构造函数
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
具有以下公共属性
- PlainText (string)
- EncodedText (string)
- Key (string)
- CopyToClipboardCommand (ActionCommand)
- UseEncryption (nullable bool)
这些属性与 MainPage.xaml 中的控件进行数据绑定。当 PlainText
属性被修改时,它会触发文本的编码。(参见清单 7。)
Set
方法位于 Codon 的 ViewModelBase
类中。在 Codon 中,INPC 会在 UI 线程上自动引发,这避免了在异步代码中到处使用 Dispatcher Invoke 调用。
纯文本和编码文本框是双向的;如果您在一个框中输入文本,另一个框就会更新。这就是为什么在 PlainText
属性的 setter 中为 EncodedText
属性调用 OnPropertyChanged
。如果我们使用 EncodedText
属性来设置新值,就会发生堆栈溢出异常。
清单 7. MainViewModel.PlainText 属性
string plainText;
public string PlainText
{
get => plainText;
set
{
if (Set(ref plainText, value) == AssignmentResult.Success)
{
encodedText = Encode(plainText);
OnPropertyChanged(nameof(EncodedText));
}
}
}
当 PlainText
属性的值更改时,会调用 Encode
方法来编码新文本。(参见清单 8。)
如果使用加密复选框未选中,则使用 SpaceEncoder
来编码文本。反之,如果启用了加密,则在文本编码之前会进行另一个步骤。使用 encryptor
生成 IV,并在加密和解密过程中使用它。IV 每次都不同,用于随机化结果。IV 被添加到字节数组的前面。
如果加密级别发生变化,我们需要记录 IV 的长度。这是通过为 IV 的长度保留结果字节数组的前两个字节来实现的。我们通过将 ivLength
强制转换为 byte
来填充第一个字节。第二个 byte
通过位移 8 位来接收下一个“高位”。
注意: Int32 需要 4 个字节才能正确表示。但是,由于我们希望尽量减小编码文本的长度,并且我预计 IV 不会超过 65K 个字符,所以我选择只使用 2 个字节。
然后将 ivBytes
和 encryptedBytes
合并并转换为 Base64。我使用 Base64 编码是为了确保传递给 SpaceEncoder
的字节数组仅包含 ASCII 字符。
清单 8. MainViewModel.Encode 方法
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;
}
在 MainViewModel 中解码文本
当用户修改“编码文本”框中的文本时,会调用 Decode
方法。(参见清单 9。)
粘贴到“编码文本”框中的文本可能包含空格编码文本和纯文本的组合。Decode
方法使用 SpaceEncoder
对象 SpaceCharacters
集合中字符的代码,构造一个正则表达式。如果它找到连续 4 个或更多空格字符的子字符串,它就会假定这是一个空格编码的部分;然后尝试对其进行解码。
回顾一下,我可能本可以使用一小串空格来标记序列的开始,但算了。
清单 9. MainViewModel.Decode 方法
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();
}
每个匹配项都会传递给 MainViewModel
对象的 DecodeSubstring
方法。(参见清单 10。)
SpaceEncoder
解码编码的文本。如果未启用加密,则解码的文本将简单地返回给 Decode
方法。
如果启用了加密,则使用 allBytes
数组的前两个字节确定 IV 的长度。IV 字节和加密文本字节被分成两个数组,允许 AesEntryptor
使用 IV 字节数组解密加密文本字节数组。
清单 10. MainViewModel.DecodeSubstring 方法
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;
}
刷新 AES 密钥
AES 密钥在应用首次启动时随机生成。如果密钥被修改且长度无效,则会阻止 AES 加密和解密正常工作。因此,用户可以选择将密钥恢复到应用首次启动时的值。
MainViewModel
包含一个 RefreshKeyCommand
,如下面的摘录所示
ActionCommand refreshKeyCommand;
public ICommand RefreshKeyCommand => refreshKeyCommand
?? (refreshKeyCommand = new ActionCommand(RefreshKey));
当用户点击主页上的“刷新”按钮时,RefreshKeyCommand
会调用 RefreshKey
方法。(参见清单 11。)
RefreshKey
尝试将 Key
属性设置为 firstKeyId
设置的值,该值在应用首次启动时存储。
如果由于任何原因,设置中不包含第一个密钥设置,则会生成并存储一个新的密钥。
清单 11. MainViewModel.RefreshKey 方法
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);
}
}
设置 Key
属性会导致其值存储在设置中,如下面的摘录所示
string key;
public string Key
{
get => key;
set
{
if (Set(ref key, value) == AssignmentResult.Success)
{
settingsService.SetSetting(keySettingId, key);
}
}
}
检查 MainPage
MainViewModel
在 MainPage
构造函数中创建。(参见清单 12。)
Codon 的 Dependency
类用于从 IoC 容器中检索 MainViewModel
的实例。MainPage
类有一个 ViewModel
属性,这样我们就可以在 MainPage.xaml 中的绑定表达式中使用 x:Bind
。
清单 12. MainPage 构造函数
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);
}
高亮编码文本
编码文本框的选择高亮颜色设置为允许用户看到生成的空格。当 PlainText
或 UseEncryption
视图模型属性被修改时,会调用编码文本框的 SelectAll
方法。(参见清单 13。)这会以 SelectionHighlightColorWhenNotFocused
颜色显示空格。
调用 SelectAll
被推送到 UI 线程队列中,以便在文本替换到编码文本框之后执行。我可能使用了另一种更侧重于视图模型的文本选择方法。但坦率地说,这更简单。
清单 13. MainPage.HandleViewModelPropertyChanged 方法
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 包含三个 TextBox
控件,分别代表纯文本、编码文本和 AES 密钥。(参见清单 14。)CheckBox
用于启用或禁用加密。
您可能想知道为什么“纯文本”和“编码文本”TextBox
控件使用的是 {Binding ...}
而不是 x:Bind
。原因是 x:Bind
不支持 UpdateSourceTrigger
参数。通常,当用户修改 TextBox
中的文本时,该值直到控件失去焦点后才会推送到其绑定源。我们希望在文本修改后立即执行编码和解码。
清单 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>
编码器和加密器的单元测试
为了对 InvisibleInk (.NET Standard) 项目进行单元测试,我创建了一个桌面 CLR 单元测试项目。
在使用 .NET Standard 时,重要的是要了解针对特定 .NET Standard 版本的各种 .NET 实现,并理解 Microsoft Docs 上的 .NET Standard 中的兼容性矩阵。
即便如此,当我创建 InvisibleInk.Tests 项目并引用 InvisibleInk (.NET Standard) 项目时,也遇到了一些令人困惑的构建问题。测试项目和 InvisibleInk 项目期望不同版本的System.Security.Cryptography.Algorithms 和 System.Security.Cryptography.Primitives 程序集。我尝试了测试项目的 .NET 版本,并在 InvisibleInk 项目的 .NET Standard 版本之间切换,但都没有解决问题。最后,经过一些搜索,我找到了一个解决方法;我在测试项目的 app.config 文件中添加了一个绑定重定向。(参见清单 15。)
绑定重定向强制测试项目使用 .NET Standard 项目预期的相同版本。
清单 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>
InvisibleInk.Tests 项目包含 SpaceEncoder
和 AesEncryptor
类的单元测试。
AesEncryptor 类的单元测试
为了测试 SpaceEncoder
,我们生成一个随机字符串,将其提供给编码器,然后对其进行解码以验证结果是否相同。(参见清单 16。)
我们通过确保编码器的空格字符包含编码文本的每个字符来确保字符串只包含空格字符。
清单 16. SpaceEncoderTests 类
[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);
}
}
}
InvisibleInk.Tests 项目中的 StringGenerator 类使用简洁的 LINQ 表达式生成随机字符串,该表达式受到此 StackOverflow 回答的启发。(参见清单 17。)
我们通过使用 Enumerable.Range
方法并将每个值强制转换为 char
来生成一个包含 256 个 ASCII 字符的字符串。
清单 17. StringGenerator 类
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());
}
}
AesEncryptor 类的单元测试
AesEncryptorTests
类使用相同的 StringGenerator
类来测试 AesEncryptor
是否能够正确地加密和解密随机字符串。(参见清单 18。)
清单 18. AesEncryptorTests 类
[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);
}
}
}
结论
在本文中,您探讨了如何使用 Unicode 空格字符来表示 ASCII 文本。您了解了如何将空格编码与加密(使用 AES 算法)相结合,为隐藏文本提供额外的保护层。您看到了如何在应用中利用空格编码器。最后,您看到了如何生成随机字符串来对编码器和加密器进行单元测试。
我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。
历史
- 2017/08/24 首次发布。