目录
介绍
对于开发人员来说,软件保护是一个重要方面。保护客户端软件免遭未经授权的使用和分发的一种有效方法是使用激活密钥,也称为许可证密钥、产品密钥或软件密钥。在本文中,我们将介绍创建激活密钥的过程,该密钥使用环境变量绑定到终端工作站的标识符,并使用各种加密算法加密数据。这将确保对生成的激活密钥进行可靠的保护,并且不允许攻击者伪造它们。
使用激活密钥保护软件
激活密钥是一种唯一的特殊软件标识符,可确认程序的副本是合法获得的,因为只有拥有密钥生成器并了解秘密参数的官方发布者才能创建并向最终用户提供此类密钥。这种方法可用于解决各种问题,例如在一定时间内限制程序的使用、防止在未注册的工作站上非法分发、使用登录名和密码管理用户帐户以及与在各种应用程序和系统中实施安全策略相关的其他任务。
激活密钥的创建和验证
大多数情况下,它归结为验证激活密钥有效性的代码。在最简单的情况下,只需将密钥与先前已知的值进行比较即可执行验证。但是,这种方法并不能提供足够的保护,防止未经授权的使用。
更可靠的方法是使用各种数据加密和哈希算法。该算法必须足够可靠,以防止密钥伪造和未经授权访问软件功能。在这种情况下,可以对密钥进行加密,并使用解密来执行许可证验证。此外,在验证密钥时,会根据用户提供的数据和保密的应用程序参数计算校验和。将计算出的校验和与存储在密钥中的校验和进行比较。如果校验和匹配,则认为密钥有效。作为密钥相关的附加(可选)条件,可以指定其有效期的到期日期。
密钥验证的实现通常使用一个函数来完成,该函数确定提供的密钥是否有效。如果键满足所有要求,则该函数返回true,允许用户启动应用程序。否则,客户端软件可能会显示警告或拒绝对应用程序的访问。此机制基于预定义的激活密钥有效性条件工作,并确保其与软件唯一匹配。
在更复杂的安全模型中,密钥验证可能与应用程序二进制文件的解密相结合。只有有效的密钥才能允许您解密应用程序启动和正常运行所需的文件,前提是它们包含用于加密文件的密码。这种方法允许您为应用程序提供一些信息,如果没有这些信息,其功能就无法运行,并有助于使对应用程序进行逆向工程以绕过密钥验证变得困难。
库内容
有问题的项目是一个dll库,可以在任何解决方案中使用。
System.Security.Activation命名空间包括ActivationKey类的实现以及用于处理该类的其他工具。
System.Text命名空间包括IPrintableEncoding接口和PrintableEncoding枚举。它们确定激活密钥如何以文本形式呈现。
我还添加了一个演示应用程序——一个基于这个项目的密钥生成器。它使用输入的密码、应用程序名称、处理器和网络适配器标识符生成密钥,并嵌入输入的数据。
ActivationKey类
本文介绍了该ActivationKey类,该类是用于创建、验证和管理激活密钥的工具。该类包含用于读取和写入激活密钥、使用各种加密和哈希算法创建和验证密钥以及从密钥的加密部分提取数据的方法。在这种情况下,激活密钥是一组数据,并使用哈希函数来计算此数据的校验和。在密钥验证过程中,使用用户提供的数据以及应用程序预定义的数据计算校验和,然后将其值与密钥中存储的校验和进行比较。如果校验和匹配,则认为密钥有效。作为密钥相关的附加(可选)条件,可以指定其有效期的到期日期。
该ActivationKey类可用于各种项目以保护软件。它为开发人员提供了一个方便的工具,用于创建和管理激活密钥,使他们能够确保可靠的软件保护,防止未经授权的使用。
该工具的一个特点是,它包含了根据与硬件和软件的指定绑定(所谓的环境参数)生成加密密钥的方法。另一个功能是能够为密钥设置到期日期,并直接在密钥中包含任何信息。在密钥验证期间,可以将此信息恢复为字节数组。密钥可以存储为人类可读的文本,也可以存储为其他格式,以便于传输给最终用户。
密钥的格式
最优激活密钥格式的设计产生了以下结构:
数据-哈希-种子
例如,KCATBZ14Y-VGDM2ZQ-ATSVYMI。
密钥格式是专门选择的,以确保文本表示的可读性,避免对符号的错误解释,并且如果可能的话,还可以减少其长度,同时保持加密强度。这是通过使用用于加密、哈希计算和数据文本编码的特殊算法来实现的。我们稍后会讨论这些算法,但现在让我们仔细看看密钥每个部分的组成和用途。
密钥由几个部分组成,由一个特殊符号分隔,以便于解析,其含义对最终用户是隐藏的,只有应用程序才能理解。下表显示了这些部件的名称和用途。
部分 | 描述 |
数据 | 加密到期日期和应用程序数据的内容(可选)。在成功验证密钥后,可以恢复此嵌入的数据。 |
散 列 | 密钥到期日期、加密应用程序数据和环境标识符的校验和。在验证过程中确保密钥的有效性。 |
种子 | 用于加密数据的初始化值。允许每次生成唯一密钥以增加加密强度。 |
事实上,键的所有部分本质上都是字节数组,使用一种仅使用可打印字符的特殊编码转换为文本表示。在简化形式中,类声明如下所示:
public class ActivationKey
{
public byte[] Data;
public byte[] Hash;
public byte[] Seed;
public override ToString()
{
// Return a string in the format "data-hash-seed".
}
}
未来
该项目的主要目标是为开发人员提供生成密钥的机制,并使其更容易将它们集成到完整的解决方案中,而无需担心数据转换。该生成器适用于任意数量的输入参数,以及任何哈希计算和数据加密算法。验证密钥只需在一行中编写最少的代码,这使您能够回答一个问题:是否可以使用包含密钥的给定对象成功激活此软件?
以下是一般功能的简短列表:
- 生成许多唯一的激活密钥并检查它们。
- 存储和恢复直接嵌入在激活密钥中的应用程序机密数据。
- 提供特殊对象二进制读取器和文本读取器,用于读取文本或二进制形式的解密数据。
- 使用内置或指定的加密和哈希算法。
- 许多用于将密钥转换为文本或二进制格式的工具,以及从不同文件格式、Windows注册表、数据流、字符串变量和字节数组中获取密钥的方法。
所有这些东西都是专门创建的,目的是尽可能透明地自动化从创建到验证的激活密钥管理过程,这样软件开发人员就不关心密钥将以何种形式交付给最终用户以及如何存储它。现在让我们来谈谈所有这些未来是如何实现的。
密码学
大多数密钥生成示例都遵循类似的模式:它们引入了一个具有智能算法的函数,该算法包含一个“幻数”。此函数将某些参数作为输入,并根据这些参数返回一个字符串。你在这里找不到这样的技巧。本文与其说是DIY加密教程,不如说是自动化激活密钥管理的指南。尽管该项目仍然包括作者认为对这些目的最有效的加密提供商的实施,但他甚至不会在这里列出他们的源代码。这不是主要焦点,这是有原因的。
首先,.NET程序集的一个缺点是它们相对容易被反编译。此问题超出了管理激活密钥的问题,并且需要采取其他步骤来混淆程序集代码。因此,不可能对算法保密:任何“酷黑客”都可以通过简单地下载.NET Reflector或类似工具来识别加密功能,并且感觉自己像个大师。是的,我自己也做过一百次了。
其次,如果已经有相当多的既定加密算法已被专家证明是有效的,为什么还要重新发明轮子呢?内置加密算法在当时非常流行,但该ActivationKey类的主要特性是它适用于使用继承System.Security.Cryptography命名空间中SymmetricAlgorithm和HashAlgorithm的任何嵌套算法生成实例。
使用默认密钥生成器:
var key = ActivationKey.DefaultEncryptor.Generate();
使用基于指定加密提供程序(例如AES和MD5)的密钥生成器:
var key = ActivationKey
.CreateEncryptor<AesManaged, MD5CryptoServiceProvider>()
.Generate();
虽然,如果你愿意,你可以修改库并建议你自己的算法,而无需对代码进行重大更改,但激活密钥仍将与你的算法无缝协作,就像它是专门为它创建的一样。保护最重要的事情是加密参数和数据。关于他们,在下一段中。其余的问题由ActivationKey类处理。
键绑定
好的,假设我们已经决定了一种算法,该算法允许使用加密创建密钥。我们如何确保每个唯一密钥都真正确认了仅对其合法所有者使用软件的权利,并使其不适合进行二次转售?通常,发布的每个软件许可副本都保证在具有唯一标识符的单个工作站中合理使用。该标识符通常由诸如板处理器的序列号、网络接口的MAC地址、软件环境参数等等参数组成。此外,许可应用程序的用户名、标题和版本以及其他应用程序参数都可以用作此类标识符。通过结合上述内容,我们可以生成一个密钥,该密钥仅在满足所有这些条件的地方才相关。
使用以下参数生成和验证激活密钥:
- Environment——用于绑定到环境的参数。这些可能包括应用程序的名称和版本、工作站ID、用户名等。如果未指定环境参数,则键不会接受任何边界。
- 到期日期——将程序的有效期限制为指定日期。如果省略value,则它不会过期。
- 应用程序数据——在检查密钥(以字节为单位)时恢复的嵌入信息;可能包含诸如最大启动次数、解密程序块的密钥、使用任何功能的限制和权限以及程序正确运行所需的其他参数等数据。此参数的值为null时,经过验证后将返回一个空字节数组。
若要直观地了解环境和应用程序数据之间的差异,请考虑下图。
通过这种方式,您可以在特定计算机上为软件的特定用户提供激活密钥。过期日期之后,此密钥将不再提供对受保护软件的访问权限。
每次单击“生成!”时,都会使用随机种子生成不同的密钥。但是,所有这些激活密钥都包含有关环境和应用程序数据的相同信息。
关于环境和数据参数的重要说明!尽管密钥生成器接受任何对象,但不要过于信任它。内部序列化函数最适合支持序列化的对象。但是,使用BinaryFormatter进行类序列化已被弃用。出于安全原因,建议仅使用基本类型,例如数字、字符串和固定长度结构。这不是一个严格的要求,但值得遵循的好建议。
以下是可以安全序列化的推荐类型列表:
Bool | 由运行时序列化的基元类型 |
Char,String | |
所有大小为8、16、32、64位的有符号和无符号整数或其数组 | |
长度为32、64 和128位的浮点数或它们的数组 | |
Byte[]、Char[] | |
Datetime | 内部支持的一些特殊类型的序列化 |
SecureString | |
IConvertible | 继承以下接口并实现ToString方法的类型 |
IFormattable | |
ValueType | 未托管类型 |
生成新密钥
以下是创建唯一键的基本步骤:
- 首先,将环境的所有输入参数序列化为一个字节数组,并随机获取种子。
- 根据输入参数和种子值创建加密密钥。
- 在此阶段,将创建与环境关联的加密器。
- 接下来,对数据以及到期日期进行序列化。
- 将创建一个字节数组,其中包含序列化数据。
- 使用选定的加密算法对源数据进行加密。
- 原始数据和种子值的哈希是使用选定的哈希函数创建的。
- 最后,创建一个ActivationKey新的实例,其中包含加密数据、计算出的哈希值和用于生成密钥的随机种子值。
该ActivationKeyEncryptor类负责创建可用于激活软件或服务的唯一密钥。此类提供两种密钥加密方法:
- 一种基本方法,使用内置的RC4和SipHash自定义修改来处理激活密钥和相关数据。这些算法的选择决定了足够可靠的键的创建,其长度便于以文本形式表示。
- 一种高级方法,允许用户指定特定的加密和哈希算法,用于将数据加密到激活密钥。这种方法在自定义加密过程以满足项目的特定需求方面提供了更大的灵活性。最好将此类密钥作为二进制文件进行传输。
这两种方法都以类似的方式工作,并生成强大的加密激活密钥,这些密钥具有基本相同的结构,并且可以以不同的形式进行编码。
以下是密钥生成器工作原理的一般原理:
private ICryptoTransform encryptor;
private HashAlgorithm hasher;
private byte[] seed;
// This method binds the generator to the environment.
void CreateEncryptor(SymmetricAlgorithm symmetricAlgorithm,
HashAlgorithm hashAlgorithm, params object[] environment)
{
hasher = hashAlgorithm;
seed = symmetricAlgorithm.IV;
using (PasswordDeriveBytes deriveBytes =
new PasswordDeriveBytes(Serialize(environment), seed))
{
encryptor = symmetricAlgorithm.CreateEncryptor
(deriveBytes.GetBytes(symmetricAlgorithm.KeySize / 8), seed);
}
}
// This method generates a new key.
ActivationKey Generate(DateTime expirationDate, params object[] data)
{
byte[] serializedData = Serialize(expirationDate, data);
byte[] encryptedData = encryptor.TransformFinalBlock(serializedData, 0, serializedData.Length);
byte[] hash = hasher.ComputeHash(Serialize(serializedData, seed));
return new ActivationKey(encryptedData, hash, seed);
}
您可能会注意到,环境参数并未在密钥中明确显示,它们仅用于初始化加密器并影响哈希计算。这种方法确保了密钥生成过程的透明度,并可靠地向用户隐藏了有关其创建方法的信息。
开发人员可以实现环境参数替换的混淆,使逆向工程更加困难,而不需要对密钥生成器进行混淆。如前所述,作为此类参数,您可以同时使用静态信息(例如应用程序的名称和版本)和动态数据(例如客户端设备标识符——用户名、主板序列号、处理器型号、网络接口MAC地址等)。
// Dynamic parameters.
string username;
byte[] macaddr;
// Obfuscated method that is used to obtain certain secret parameters.
// The longer the generated sequence, the better.
byte[] GetMagicNumbers()
{
/* For example, let's calculate pi squared accurate to n digit.
All means are good for this.
For example, you can intentionally make calculations more complex
or use code from third-party libraries by using DLL imports.
You can also use encrypted bytecode.
*/
}
// ...obtaining username and macaddr
object[] environment = // Collected binding identifiers of any length
{
GetMagicNumbers(), // Magic numbers. What would the world be like without them?
"MyApp", // Application name.
1, 0, // Version.
username , // Registered user.
macaddr, // MAC address of the network adapter.
}
DateTime expirationDate = DateTime.Now.AddMonths(1), // expiration date
object[] data = // Data that needs to be stored in the key
{
0x73, 0x65, 0x63,
0x72, 0x65, 0x74 // Any secret numbers
}
// As I promised, just one line of code:
var key = ActivationKey.CreateEncryptor(environment).Generate(expirationDate, data);
将密钥转换为其他类型
有多种方法可以将激活密钥从发布者转移到用户,以用于其预期用途。此过程涉及通过通信网络传递数据、将其写入文件以及将其添加到注册表中等。
确保将激活密钥的表示形式转换为所需的格式至关重要。该ActivationKey类支持不同的转换方法,这些方法可以分为两组:文本和二进制表示。这些任务可以使用称为ActivationKeyTextParser和ActivationKeyBinaryParser的专门附加类有效地完成。
该ActivationKeyBinaryParser工具可帮助您解析包含激活密钥的二进制数据。它具有用于分析这些激活密钥和创建ActivationKey类实例的方法。激活密钥显示为带有必要标头的字节序列。此标头用于确认文件格式。因此,激活密钥可以以文件的形式提供。用户在注册其应用程序副本时需要指定此文件的路径。此格式也适用于将密钥存储在Windows注册表中。
以下是序列化的激活密钥结构:
Header | 2字节 |
Data长度 | 32位整数 |
Hash长度 | 32位整数 |
Seed长度 | 32位整数 |
Data | 字节数组 |
Hash | 字节数组 |
Seed | 字节数组 |
这是C#实现的样子:
// A required header indicating that the data is in the correct format.
const ushort BinaryHeader = 0x4B61;
// Writing the activation key to the stream.
void Write(ActivationKey activationKey, Stream stream)
{
using(BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(BinaryHeader);
writer.Write(activationKey.Data.Length);
writer.Write(activationKey.Hash.Length);
writer.Write(activationKey.Seed.Length);
writer.Write(activationKey.Data);
writer.Write(activationKey.Hash);
writer.Write(activationKey.Seed);
}
}
// Parsing the stream containing an activation key.
ActivationKey Parse(Stream stream)
{
ActivationKey activationKey = new ActivationKey();
using(BinaryReader reader = new BinaryReader(stream))
{
ushort header = reader.ReadUInt16();
if (header != BinaryHeader)
throw new Exception();
int dataLength = reader.ReadInt32();
int hashLength = reader.ReadInt32();
int tailLength = reader.ReadInt32();
activationKey.Data = reader.ReadBytes(dataLength);
activationKey.Hash = reader.ReadBytes(hashLength);
activationKey.Seed = reader.ReadBytes(tailLength);
}
return activationKey;
}
ActivationKeyTextParser提供了处理代表激活键的文本数据的工具。这些键是由特定分隔符分隔的字符序列。
此类提供用于分析和生成ActivationKey类的实例的方法。ActivationKeyTextParser有几个构造函数,允许您设置解析器参数。它还包括用于分析激活密钥和创建ActivationKey实例的方法。使用Parse方法,可以将表示激活密钥的字符串转换为ActivationKey实例。另一方面,该GetString方法将激活密钥的文本表示形式返回为带分隔符的字符串。
C#中的文本解析器实现示例:
// Current printable encoding
IPrintableEncoding encoding;
// Characterthat used as delimiter between activation key parts.
char separator = '-';
string GetStringSafe(byte[] bytes)
{
return bytes == null ? string.Empty : encoding.GetString(bytes);
}
// Converting the activation key to string.
string GetString(ActivationKey activationKey)
{
return string.Format("{0}{3}{1}{3}{2}", new object[5]
{
this.GetStringSafe(activationKey.Data),
this.GetStringSafe(activationKey.Hash),
this.GetStringSafe(activationKey.Seed)
separator,
});
}
// Parsing string containing an activation key.
ActivationKey Parse(string input)
{
ActivationKey activationKey = new ActivationKey();
if (string.IsNullOrEmpty(input))
return;
string[] items = input.ToUpperInvariant().Split(separator);
if (items.Length >= 3)
{
activationKey.Data = encoding.GetBytes(items[0]);
activationKey.Hash = encoding.GetBytes(items[1]);
activationKey.Seed = encoding.GetBytes(items[2]);
}
return activationKey;
}
此外,ActivationKeyTextParser还包含用于检索激活密钥的特定部分的方法,例如数据、哈希或种子。这使开发人员能够访问激活密钥中的信息,并将其用于各种目的。文本格式的密钥便于通过电子邮件和其他在线文本服务发送。用户可以轻松地从电子邮件中复制此密钥,并将其粘贴到应用程序的文本输入字段中。这样,密钥可以存储在ini文件中以供将来使用。
选择一种在文本中表示二进制信息的方法将不可避免地将我们引向基于N的编码这个话题。这个主题已经在网上被广泛讨论(example1、example2、example3),因此我们将简要概述本文中使用的编码方法。
该ActivationKeyTextParser类提供了用于创建和获取对象的静态方法,对象是一组允许您将二进制数据转换为文本表示的方法。这对于以人类可读的格式显示二进制数据或通过基于文本的通信通道传输二进制数据非常有用。您可以使用默认方法或选择其中一个可用选项将数据转换为字符。该IPrintableEncoding实现可用于将二进制数据(例如ActivationKey)转换为易于阅读和理解的文本表示。
public interface IPrintableEncoding : ICloneable
{
string GetString(byte[] bytes);
byte[] GetBytes(string s);
}
GetString和GetBytes方法允许您对数据进行编码和解码。在处理需要以人类可读格式呈现的二进制数据时,它们非常有用。这意味着所有数据都将表示为可以复制和粘贴或使用“记事本”等程序手动输入和保存的字符。
以下是该ActivationKeyTextParser类支持的编码:
- Base32Encoding——返回32个字符的编码(默认使用)。
- Base64Encoding——返回不带尾随符号的64个字符的编码。
- DecimalEncoding——返回十进制编码。
- HexadeciamlEncoding——返回十六进制编码。
- GetEncoding(string alphabet)——如果要使用自己的字符集生成键,请根据传递的字母字符串创建自定义编码类的实例。
- GetEncoding(PrintableEncoding encoding)——返回由PrintableEncoding枚举确定的编码。
- 编写您自己的编码版本,用于实现IPrintableEncoding接口,并将其传递给ActivationKeyTextParser类构造函数。
IPrintableEncoding _encoding;
public ActivationKeyTextParser(IPrintableEncoding encoding, params char[] delimiters)
{
_encoding = encoding ?? Base32Encoding;
}
在分隔符参数中,您可以在数据、哈希和初始值之间传递将被视为分隔符的所有字符。在分隔符参数中,您可以在数据、哈希和键的种子部分之间传递将被视为分隔符的所有字符。默认值为连字符。
最方便的编码是base-32,因为它不区分大小写,并且仅由数字和拉丁字母组成。我建议使用此编码,而不是寻找其他方法。
ActivationKeyConverter是一种工具,可帮助您将ActivationKey对象转换为其他数据类型,反之亦然。它是TypeConverter类的子类,包括CanConvertFrom、CanConvertTo、ConvertFrom和ConvertTo等方法。这些方法允许您将ActivationKey对象更改为字符串和字节数组,反之亦然。
当您处理该ActivationKey类型的数据时,此转换器会很有帮助。例如,您可以在将数据存储在数据库中、通过网络传输数据或在用户界面中显示数据时使用它。
获取以前生成的密钥
通过该ActivationKeyManager类,您可以毫不费力地从各种来源读取激活密钥并验证其有效性。此类支持多种格式:
- 纯文本文件
- 二进制文件
- INI个文件
- Windows注册表项(二进制或文本类型)
- 数据流
例如,如果您的应用程序需要激活密钥进行用户身份验证,则可以利用该ActivationKeyManager类从INI文件加载密钥并对其进行验证。如果密钥证明有效,您可以继续使用该应用程序。但是,如果密钥无效,则可以显示错误消息并提示用户输入正确的密钥。所有这些都可以通过几行代码来完成。
您需要的代码可能如下所示:
if (!ActivationKey
.DefaultManager
.LoadFromIniEntry("settings.ini", "registration", "key")
.Verify())
{
// Displaying a message and closing the window.
string message = "Your version is unregistered."
+" Would you like to enter a valid activation key?";
string caption = "Registration warning";
MessageBoxButtons buttons = MessageBoxButtons.YesNo;
DialogResult result = MessageBox.Show(message, caption, buttons);
if (result == System.Windows.Forms.DialogResult.No)
this.Close();
// Calling the method for entering the activation key...
}
ini文件内容:
[Registration]
Key=FVDZTMKGJXGZS-4FPHA5Y-UVNYMNY
Owner=John
#...etc
另一个示例,如果要从Windows注册表中读取激活密钥:
if (!ActivationKey
.DefaultManager
.LoadFromRegistry("HKEY_CURRENT_USER\SOFTWARE\MyApp\Registration", "ActivationData")
.Verify())
{
// See previous example...
}
使用该ActivationKey.CreateManager方法,您可以创建一个管理器,该管理器使用自定义设置来转换激活密钥。以下是这些设置的列表:
- 二进制头,
- 可打印编码,
- 分隔符符号
var manager = ActivationKey.CreateManager(0x0123, PrintableEncoding.Base64, '-', ':', ' ', '\t');
将激活密钥转换为字符串时,将使用分隔符列表中的第一个字符。
验证密钥
但是,关于密钥创建和存储以及转换为各种格式的讨论已经足够多了。毕竟,激活密钥生命周期中最重要的事件是其验证。而指定类ActivationKeyDecryptor将帮助我们完成这个任务。
要创建ActivationKeyDecryptor类的实例,您需要将某些参数传递给其构造函数:
- activationKey——需要验证的激活密钥;
- environment——我们已经熟悉的参数用于生成唯一键。
重要提示!将环境参数传递给构造函数时,必须遵循与创建加密设备时相同的顺序。传递的参数的数量、顺序或值的任何差异都将导致哈希校验和不同,这意味着密钥将永远无法成功通过验证。
该ActivationKey类还包含一种用于快速创建解密器的CreateDecryptor方法。
该类尝试使用用户定义的算法解密密钥中包含的数据。
如果数据已成功解密,则该类将Success属性设置为true。
该类使用GetBinaryReader或GetTextReader方法返回解密的数据。
与上述密钥加密算法类似,此类实现了两种解密密钥的方法:
// Predifined example a key.
Activation key = "FVDZTMKGJXGZS-4FPHA5Y-UVNYMNY";
object[] environment = // Collected binding identifiers of any length
{
GetMagicNumbers(), // Magic numbers (obfuscated method).
"MyApp", // Application name.
1, 0, // Version.
username , // Registered user.
macaddr, // MAC address of the network adapter.
}
// Here are two methods to verify the key.
// 1. Special decryptor that can verify the key and recover encrypted data.
using(ActivationKeyDecryptor decryptor = key.CreateDecryptor(environment))
{
if(decryptor.Success)
using(TextReader reader = decryptor.GetTextReader())
{
//Now we know what's there!
string secret = reader.ReadToEnd();
}
}
// 2. Just checking the key.
bool success = key.Verify(environment);
关于该Data属性的几句话:
- 如果检查失败,则该属性为null。
- 如果键中未存储任何数据,则该属性将返回一个空数组。
- 如果数据已存储在键中,则该属性会将该数据作为字节数组包含。
该ExpirationDate属性返回激活密钥过期的实际日期。如果密钥是在未指定到期日期的情况下创建的,则DateTime.MaxValue将返回该密钥。
用法
让我们看一个例子来说明这一点。这是一个简单的控制台应用程序,它使用各种加密算法和编码方法生成密钥。它还将这些键保存为文本和二进制格式的文件。
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Security.Activation;
using System.Security.Cryptography;
using System.Net.NetworkInformation;
internal static class Program
{
// Obtaining MAC address.
byte[] macAddr =
(
from netInterface in NetworkInterface.GetAllNetworkInterfaces()
where netInterface.OperationalStatus == OperationalStatus.Up
select netInterface.GetPhysicalAddress().GetAddressBytes()
).FirstOrDefault();
// Here's an example of custom encoding that uses numbers,
// latin and cyrillic characters, in both uppercase and lowercase.
private static string Base128 =
"0123456789"+
"QWERTYUIOPASDFGHJKLZXCVBNM"+
"qwertyuiopasdfghjklzxcvbnm"+
"ЙЦУКЕЁНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ"+
"йцукеёнгшщзхъфывапролджэячсмитьбю";
// Input data. No article can be written without these simple, sincere words.
private static byte[] HelloWorld =
{
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20,
0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21
};
private static void Main(string[] args)
{
// Pass one: using the default encryptor without data.
Console.WriteLine();
Console.WriteLine("Default cryptography without data:");
using (ActivationKey key = ActivationKey.CreateEncryptor(macAddr).Generate()
{
using (ActivationKeyDecryptor decryptor = key.CreateDecryptor(macAddr)
{
Console.WriteLine("Base10: \t" + key.ToString(PrintableEncoding.Decimal));
Console.WriteLine("Base16: \t" + key.ToString(PrintableEncoding.Hexadecimal));
Console.WriteLine("Base32: \t" + key);
Console.WriteLine("Base64: \t" + key.ToString(PrintableEncoding.Base64));
Console.WriteLine("Base128:\t" + key.ToString(ActivationKeyTextParser.GetEncoding(Base128)));
ActivationKey.DefaultManager.SaveToFile(key, "key1.bin", true); //binary
ActivationKey.DefaultManager.SaveToFile(key, "key1.txt", false); // text
}
}
// Pass two: using the default encryptor with data.
Console.WriteLine();
Console.WriteLine("Default cryptography with data:");
using (ActivationKey key = ActivationKey.CreateEncryptor(macAddr).Generate(HelloWorld))
{
using (ActivationKeyDecryptor decryptor = key.CreateDecryptor(macAddr))
{
if (decryptor.Success && decryptor.Data.Length != 0)
{
using (TextReader reader = decryptor.GetTextReader(null))
{
Console.WriteLine("The key content is: " + reader.ReadToEnd());
}
}
Console.WriteLine("Base10: \t" + key.ToString(PrintableEncoding.Decimal));
Console.WriteLine("Base16: \t" + key.ToString(PrintableEncoding.Hexadecimal));
Console.WriteLine("Base32: \t" + key);
Console.WriteLine("Base64: \t" + key.ToString(PrintableEncoding.Base64));
Console.WriteLine("Base128:\t" + key.ToString(ActivationKeyTextParser.GetEncoding(Base128)));
ActivationKey.DefaultManager.SaveToFile(key, "key2.bin", true); // binary
ActivationKey.DefaultManager.SaveToFile(key, "key2.txt", false); // text
}
}
// Pass three: using the AES encryptor and MD5 hash algorithm with data.
Console.WriteLine();
Console.WriteLine("Custom cryptography (AES+MD5) with data:");
using (ActivationKey key = ActivationKey
.CreateEncryptor<AesManaged, MD5CryptoServiceProvider>(macAddr)
.Generate(HelloWorld))
{
using (ActivationKeyDecryptor decryptor =
key.CreateDecryptor<AesManaged, MD5CryptoServiceProvider>(macAddr))
{
if (decryptor.Success && (decryptor.Data.Length != 0))
{
using (TextReader reader = decryptor.GetTextReader(null))
{
Console.WriteLine("The key content is: " + reader.ReadToEnd());
}
}
Console.WriteLine("Base10: \t" + key.ToString(PrintableEncoding.Decimal));
Console.WriteLine("Base16: \t" + key.ToString(PrintableEncoding.Hexadecimal));
Console.WriteLine("Base32: \t" + key);
Console.WriteLine("Base64: \t" + key.ToString(PrintableEncoding.Base64));
Console.WriteLine("Base128:\t" + key.ToString(ActivationKeyTextParser.GetEncoding(Base128)));
ActivationKey.DefaultManager.SaveToFile(key, "key3.bin", true); // binary
ActivationKey.DefaultManager.SaveToFile(key, "key3.txt", false); // text
}
}
Console.ReadKey();
}
}
控制台输出:
请注意默认流算法RC4和AES分组密码的密钥长度差异,有数据和没有数据!因此,使用长键更适合二进制文件而不是文本文件。
总结
最后,我想做一个简短的总结。
无需混淆密钥生成方法并发明自己的加密算法。相反,您应专注于以下任务:
- 使负责获取用于将密钥绑定到环境的参数的代码复杂化。
- 将数据嵌入到密钥中,该密钥允许您解密包含可执行代码或应用程序资源的二进制文件以及对启动至关重要的其他数据。
- 使用已知的可打印编码以方便传输的形式表示密钥。
- 使用算法,使您能够生成足以实现目标的短密钥,而不会失去加密强度。
本文中描述的项目可以在各种应用程序中使用,而版本之间没有任何变化。要创建唯一键,您需要传递与当前情况相关的不同参数。
简要介绍一下嵌入式类。
类 | 描述 |
ARC4 | 由Ron Rivest ©设计的RC4加密提供程序的自定义实现,用于加密/解密数据部分。 |
SipHash | 由Jean-Philippe Aumasson和Daniel J. Bernstein. ©创建的基于add-rotate-xor的SipHash算法的32位实现。 |
Base32 | 将ZBase-32 数字系统数据分叉到由Denis Zinchenko ©设计的用于文本键表示的字符串编码器。 |
CustomEncoding | BaseNcoding的分叉——KvanTTT ©将二进制数据转换为字符串编码的另一种算法。 |
附言
如果评论家对序列化功能提供任何反馈,我将特别感激。我甚至会给出它的全部源代码:
// Converts objects to a byte array. You can improve it however you find it necessary for your own stuff.
[SecurityCritical]
internal static unsafe byte[] Serialize(params object[] objects)
{
if (objects == null)
{
return new byte[0];
}
using (MemoryStream memory = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(memory))
{
for (int j = 0; j < objects.Length; j++)
{
object obj = objects[j];
if (obj == null)
{
continue;
}
try
{
switch (obj)
{
case null:
continue;
case SecureString secureString:
if (secureString == null || secureString.Length == 0)
{
continue;
}
Encoding encoding = new UTF8Encoding();
int maxLength = encoding.GetMaxByteCount(secureString.Length);
IntPtr destPtr = Marshal.AllocHGlobal(maxLength);
IntPtr sourcePtr = Marshal.SecureStringToBSTR(secureString);
try
{
char* chars = (char*)sourcePtr.ToPointer();
byte* bptr = (byte*)destPtr.ToPointer();
int length = encoding.GetBytes(chars, secureString.Length, bptr, maxLength);
byte[] destBytes = new byte[length];
for (int i = 0; i < length; ++i)
{
destBytes[i] = *bptr;
bptr++;
}
writer.Write(destBytes);
}
finally
{
Marshal.FreeHGlobal(destPtr);
Marshal.ZeroFreeBSTR(sourcePtr);
}
continue;
case string str:
if (str.Length > 0)
writer.Write(str.ToCharArray());
continue;
case DateTime date:
writer.Write(GetBytes(date));
continue;
case bool @bool:
writer.Write(@bool);
continue;
case byte @byte:
writer.Write(@byte);
continue;
case sbyte @sbyte:
writer.Write(@sbyte);
continue;
case short @short:
writer.Write(@short);
continue;
case ushort @ushort:
writer.Write(@ushort);
continue;
case int @int:
writer.Write(@int);
continue;
case uint @uint:
writer.Write(@uint);
continue;
case long @long:
writer.Write(@long);
continue;
case ulong @ulong:
writer.Write(@ulong);
continue;
case float @float:
writer.Write(@float);
continue;
case double @double:
writer.Write(@double);
continue;
case decimal @decimal:
writer.Write(@decimal);
continue;
case byte[] buffer:
if (buffer.Length > 0)
writer.Write(buffer);
continue;
case char[] chars:
if (chars.Length > 0)
writer.Write(chars);
continue;
case Array array:
if (array.Length > 0)
foreach (object element in array)
writer.Write(Serialize(element));
continue;
case IConvertible conv:
writer.Write(conv.ToString(CultureInfo.InvariantCulture));
continue;
case IFormattable frm:
writer.Write(frm.ToString(null, CultureInfo.InvariantCulture));
continue;
case Stream stream:
if (stream.CanRead) stream.CopyTo(memory);
continue;
case ValueType @struct:
int size = Marshal.SizeOf(@struct);
byte[] bytes = new byte[size];
IntPtr handle = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(@struct, handle, false);
Marshal.Copy(handle, bytes, 0, size);
writer.Write(bytes);
}
finally
{
Marshal.FreeHGlobal(handle);
}
continue;
default:
if(!obj.GetType().IsSerializable)
throw new SerializationException(GetResourceString("Arg_SerializationException"));
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(memory, obj);
continue;
}
}
catch (Exception e)
{
#if DEBUG // This is where the debugger information will be helpful
if (Debug.Listeners.Count > 0)
{
Debug.WriteLine(DateTime.Now);
Debug.WriteLine(GetResourceString("Arg_ParamName_Name", $"{nameof(objects)}[{j}]"));
Debug.WriteLine(obj, "Object");
Debug.WriteLine(e, "Exception");
}
#endif
}
}
writer.Flush();
byte[] result = memory.ToArray();
return result;
}
}
https://www.codeproject.com/Articles/5381953/Activation-Key-Class-Library