65.9K
CodeProject 正在变化。 阅读更多。
Home

激活密钥类库

starIconstarIconstarIconstarIconstarIcon

5.00/5 (21投票s)

2024年5月12日

MIT

21分钟阅读

viewsIcon

19465

downloadIcon

1634

管理加密激活密钥以保护您的应用程序

Keygen application

目录

  1. 引言
  2. 库内容
  3. ActivationKey 类
  4. 用法

引言

软件保护是开发者关注的重要方面。保护客户端软件免受未经授权的使用和分发的一种有效方法是使用激活密钥,也称为许可证密钥、产品密钥或软件密钥。在本文中,我们将探讨创建激活密钥的过程,该密钥使用环境变量绑定到最终工作站的标识符,并使用各种加密算法对数据进行加密。这将确保生成激活密钥的可靠保护,并且不允许攻击者伪造它们。

使用激活密钥保护软件

激活密钥是一种独特的特殊软件标识符,它确认程序副本是合法获得的,因为只有官方发行商拥有密钥生成器并了解秘密参数,才能创建并向最终用户提供此类密钥。这种方法可用于解决各种问题,例如限制程序在特定时间内的使用、防止在未注册工作站上的非法分发、使用登录名和密码管理用户帐户以及与在各种应用程序和系统中实施安全策略相关的其他任务。

激活密钥的创建和验证

最常见的是,它归结为验证激活密钥有效性的代码。在最简单的情况下,可以通过简单地将密钥与先前已知的值进行比较来执行验证。但是,这种方法不能提供足够的保护以防止未经授权的使用。

更可靠的方法是使用各种数据加密和哈希算法。该算法必须足够可靠,以防止密钥伪造和未经授权访问软件功能。在这种情况下,密钥可以被加密,并且使用解密来执行许可证验证。此外,在验证密钥时,会根据用户提供的数据和保密的应用程序参数计算校验和。计算出的校验和与存储在密钥中的校验和进行比较。如果校验和匹配,则密钥被认为是有效的。作为密钥相关性的附加(可选)条件,可以指定其有效期。

密钥验证的实现通常通过一个函数来完成,该函数确定提供的密钥是否有效。如果密钥满足所有要求,该函数将返回true,允许用户启动应用程序。否则,客户端软件可能会显示警告或拒绝访问应用程序。此机制根据预定义的激活密钥有效性条件工作,并确保它与软件唯一匹配。

在更复杂的安全模型中,密钥验证可能与应用程序二进制文件的解密结合使用。只有有效密钥才能解密应用程序启动和正常运行所需的文件,如果它们包含(例如)加密文件的密码。这种方法允许您向应用程序提供一些信息,没有这些信息,应用程序将无法运行,并有助于使逆向工程应用程序以绕过密钥验证变得困难。

库内容

所讨论的项目是一个DLL库,可以在任何解决方案中使用。

System.Security.Activation命名空间包含ActivationKey类的实现以及其他用于处理它的工具。

System.Text命名空间包含IPrintableEncoding接口和PrintableEncoding枚举。它们确定激活密钥如何以文本形式呈现。

我还添加了一个演示应用程序——一个基于此项目的密钥生成器。它根据输入的密码、应用程序名称、处理器和网络适配器标识符生成密钥,并嵌入输入的数据。

ActivationKey 类

本文介绍了ActivationKey类,它是一个用于创建、验证和管理激活密钥的工具。该类包含用于读取和写入激活密钥、使用各种加密和哈希算法创建和验证密钥以及从密钥的加密部分提取数据的方法。在这种情况下,激活密钥是一组数据,哈希函数用于计算此数据的校验和。在密钥验证过程中,使用用户提供的数据以及应用程序预定义的数据计算校验和,然后将其值与存储在密钥中的校验和进行比较。如果校验和匹配,则密钥被认为是有效的。作为密钥相关性的附加(可选)条件,可以指定其有效期。

ActivationKey类可用于各种项目以保护软件。它为开发人员提供了创建和管理激活密钥的便捷工具,从而确保软件可靠地免受未经授权的使用。

此工具的一个特点是它包含基于指定的硬件和软件绑定(所谓的环境参数)生成加密密钥的方法。另一个特点是能够设置密钥的有效期,并在密钥中直接包含任何信息。此信息可以在密钥验证期间作为字节数组恢复。密钥可以存储为人类可读的文本或其他允许轻松传输给最终用户的格式。

密钥格式

最优激活密钥格式的设计产生了以下结构

DATA-HASH-SEED.

例如,KCATBZ14Y-VGDM2ZQ-ATSVYMI

密钥格式经过专门选择,以确保文本表示的可读性,避免对符号的错误解释,并且尽可能在保持加密强度的同时减少其长度。这是通过使用特殊的加密、哈希计算和数据文本编码算法实现的。我们稍后将讨论这些算法,但现在让我们仔细看看密钥每个部分的组成和目的。

密钥由几个部分组成,由特殊符号分隔以方便解析,其含义对最终用户隐藏,只有应用程序才能理解。下表显示了这些部分的名称和目的。

零件 描述
Data 加密的有效期应用程序数据(可选)的内容。这些嵌入数据可在成功验证密钥后恢复。
哈希 密钥有效期、加密的应用程序数据环境标识符的校验和。确保密钥在验证期间的有效性。
种子 用于加密数据的初始化值。允许每次生成唯一密钥以增加加密强度。

实际上,密钥的所有部分本质上都是字节数组,使用只使用可打印字符的特殊编码转换为文本表示。简化形式的类声明如下:

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命名空间中继承SymmetricAlgorithmHashAlgorithm的任何嵌套算法来生成实例。

使用默认密钥生成器

var key = ActivationKey.DefaultEncryptor.Generate();

使用基于指定加密提供程序(例如 AES 和 MD5)的密钥生成器

var key = ActivationKey
         .CreateEncryptor<AesManaged, MD5CryptoServiceProvider>()
         .Generate();

但是,如果您愿意,您可以修改库并提出自己的算法,而无需对代码进行重大更改,激活密钥仍将与您的算法无缝协作,就像它是专门为其创建的一样。保护最重要的是加密参数和数据。关于它们将在下一段中讨论。而其他问题由ActivationKey类负责。

密钥绑定

好吧,假设我们已经确定了一种允许使用加密创建密钥的算法。我们如何确保每个唯一的密钥真正确认只有合法所有者才有权使用软件,并使其二次转售不适用?通常,每份发行的许可软件副本都保证在具有唯一标识符的单个工作站中公平使用。此标识符通常由诸如主板处理器序列号、网络接口的 MAC 地址、软件环境参数等参数组成。此外,用户名、许可应用程序的标题和版本以及其他应用程序参数可以用作此类标识符。通过结合上述内容,我们可以生成一个仅在满足所有这些条件的地方才相关的密钥。

激活密钥使用以下参数生成和验证

  • 环境 - 绑定到环境的参数。这些可能包括应用程序的名称和版本、工作站ID、用户名等。如果您不指定环境参数,则密钥将不包含任何绑定。
  • 有效期 - 将程序的有效期限制在指定的日期。如果省略该值,则永不过期。
  • 应用程序数据 - 嵌入信息,在检查字节中的密钥时恢复;可能包含诸如最大启动次数、解密程序块的密钥、使用任何功能的限制和权限以及程序正确运行所需的其他参数等数据。此参数的值为null时,在验证时将返回空字节数组。

为了可视化环境和应用程序数据之间的区别,请看下面的插图。

这种方式允许您为特定计算机上您的软件的特定用户提供激活密钥。过了有效期后,此密钥将不再提供对受保护软件的访问。

每次点击“生成!”时,都会使用随机种子生成不同的密钥。但是,所有这些激活密钥都包含相同的环境和应用程序数据信息。

关于环境和数据参数的重要提示!尽管密钥生成器接受任何对象,但不要太信任它。内部序列化函数最适合支持序列化的对象。但是,已知使用BinaryFormatter进行类序列化已弃用。出于安全原因,建议仅使用基本类型,例如数字、字符串和固定长度结构。这不是严格要求,但值得遵循的好建议。

以下是建议可以安全序列化的类型列表

Bool

由运行时序列化的原始类型

字符,字符串

所有8、16、32、64位大小的有符号和无符号整数或其数组

32、64和128位长度的浮点数或其数组

字节[],字符[]

日期时间

内部支持的一些特殊序列化类型

安全字符串

Stream

IConvertible

继承以下接口并实现ToString方法的类型

可格式化

ValueType

非托管类型

生成新密钥

以下是创建唯一密钥的基本步骤

  • 首先,环境的所有输入参数都被序列化为字节数组,并随机获取一个种子
  • 根据输入参数和种子值创建加密密钥。
  • 在此阶段,创建了一个与环境关联的加密器。
  • 接下来,数据以及到期日期将被序列化。
  • 创建包含序列化数据的字节数组。
  • 使用选定的加密算法加密源数据。
  • 使用选定的哈希函数创建原始数据和种子值的哈希。
  • 最后,创建ActivationKey的新实例,其中包含加密的数据、计算的哈希以及用于生成密钥的随机种子值。

Keygen diagram

ActivationKeyEncryptor类负责创建可用于激活软件或服务的唯一密钥。此类提供两种密钥加密方法

  1. 一种基本方法,使用内置的RC4SipHash自定义修改,用于处理激活密钥和相关数据。这些算法的选择决定了创建足够可靠的密钥,其长度便于以文本形式表示。
  2. 一种高级方法,允许用户指定特定的加密和哈希算法,用于将数据加密到激活密钥中。此方法在自定义加密过程方面提供了更大的灵活性,以满足项目的特定需求。最好将此类密钥作为二进制文件传输。

这两种方法都以类似的方式工作,并生成强大的加密激活密钥,这些密钥本质上具有相同的结构,并且可以以不同的形式编码。

以下是密钥生成器的一般工作原理

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类支持不同的转换方法,可分为两组:文本表示和二进制表示。这些任务通过专门的附加类ActivationKeyTextParserActivationKeyBinaryParser有效地完成。

ActivationKeyBinaryParser 是一个帮助您解析包含激活密钥的二进制数据的工具。它具有解析这些激活密钥和创建 ActivationKey 类实例的方法。激活密钥显示为带有必要头部的字节序列。此头部用于确认文件格式。因此,激活密钥可以以文件形式提供。用户需要在注册其应用程序副本时指定此文件的路径。此格式也适用于将密钥存储在 Windows 注册表中。

以下是序列化的激活密钥结构

标题 2 字节
数据长度 32位整数
哈希长度 32位整数
种子长度 32位整数
Data 字节数组
哈希 字节数组
种子 字节数组

这是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的编码。这个主题在网上已被广泛讨论(示例1示例2示例3),因此我们将简要概述本文中使用的编码方法。

ActivationKeyTextParser类提供了创建和获取对象(这是一组允许将二进制数据转换为文本表示的方法)的静态方法。这对于以人类可读的格式显示二进制数据或通过基于文本的通信通道传输二进制数据非常有用。您可以使用默认方法或选择一个可用选项将数据转换为字符。IPrintableEncoding实现可用于将二进制数据(例如ActivationKey)转换为易于阅读和理解的文本表示。

public interface IPrintableEncoding : ICloneable
{
    string GetString(byte[] bytes);
    byte[] GetBytes(string s);
}

GetStringGetBytes方法允许您编码和解码数据。当处理需要以人类可读格式呈现的二进制数据时,它们会很有用。这意味着所有数据都将以字符形式表示,可以复制粘贴或手动输入,并使用“记事本”等程序保存。

以下是ActivationKeyTextParser类支持的编码

  • Base32Encoding - 返回32字符编码(默认使用)。
  • Base64Encoding - 返回不带尾部符号的64字符编码。
  • DecimalEncoding - 返回十进制编码。
  • HexadecimalEncoding - 返回十六进制编码。
  • 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类的子类,包含CanConvertFromCanConvertToConvertFromConvertTo等方法。这些方法允许您将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。
该类使用GetBinaryReaderGetTextReader方法返回解密的数据。
与前面提到的密钥加密算法类似,该类实现了两种解密密钥的方法

  1. 一种基本方法,使用内置的RC4SipHash自定义修改,用于处理激活密钥和相关数据。
  2. 一种高级方法,允许用户指定特定的加密和哈希算法来解密激活密钥中的数据。
// 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();
    }
}

控制台输出

Test application

请注意,默认流算法 RC4 和 AES 分组密码的密钥长度差异,有数据和无数据!因此,使用长密钥更适合二进制文件而不是文本文件。

摘要

最后,我想做一个简短的总结。

无需混淆密钥生成方法并发明自己的加密算法。相反,您应该专注于以下任务

  1. 使负责获取用于将密钥绑定到环境的参数的代码复杂化。
  2. 将数据嵌入到密钥中,该数据允许您解密包含可执行代码或应用程序资源以及其他启动关键数据的二进制文件。
  3. 使用已知的可打印编码以方便传输的形式表示密钥。
  4. 使用能够生成足以实现您的目标且不损失加密强度的短密钥的算法。

本文中描述的项目可以在各种应用程序中使用,无需版本之间的任何更改。要创建唯一的密钥,您需要传递与当前情况相关的不同参数。

简要介绍嵌入式类。

描述
ARC4 Ron Rivest © 设计的RC4密码学提供者的自定义实现,用于加密/解密数据部分。
SipHash Jean-Philippe Aumasson 和 Daniel J. Bernstein © 创建的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;
    }
}

↑ 返回目录

© . All rights reserved.