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

Cipher EX V1.5

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (145投票s)

Oct 31, 2014

GPL3

56分钟阅读

viewsIcon

356325

downloadIcon

7576

Twofish 512, Serpent 512, Rijndael 512, HX 密码, Ring-LWE, NTRU, McEliece, Rainbow, GMSS, DTM-KEX

 

引言

以下是我对几种加密算法的研究成果。我决定编写这个库是为了更多地了解它们,以及密码学。我过去曾改编过 Mono 和 Bouncy Castle 等流行库中的类,但这次我想编写自己的实现,以 C# 语言为优化目标,可能比流行的 C# 版本更快、更灵活。在我编写基类时,我也开始考虑各种攻击向量,以及如何缓解它们,并从安全角度改进现有的基元。

需要注意的是,使用具有原始密钥大小的基数密码,这些类的输出将与其他任何有效密码实现完全相同;使用 256 位密钥的 RHX (Rijndael) 是 Rijndael,使用标准密钥大小的 THX (Twofish) 是 Twofish,SHX (Serpent) 是有效的 Serpent 实现。这是经过验证的。测试部分包含每个密码最完整、最权威的测试套件。因此,如果您选择使用标准密钥长度进行部署,可以使用经过充分密码分析的配置。

必须考虑到,这些密码是在近 20 年前设计的;当时,Windows 95 是主流操作系统,与今天的标准相比,计算机硬件相当原始。因此,在密码设计中必须在速度和内存占用方面做出让步。我们现在受到的硬件限制不再那么大,因此增加密码的轮数或使用更大的密钥长度在今天已不再是问题,并且随着硬件的不断发展,其影响将越来越小。

速度仍然是本项目的重要设计标准。CTR 模式以及 CBC 和 CFB 模式的解密功能已实现并行化。如果将 ParallelBlockSize(默认为 64000 字节,但可配置)字节的块大小传递给模式,并且硬件利用了多个处理器核心,则处理会自动并行化。在一台 i7-6700 处理器上,我使用 Rijndael 在此库中达到了每分钟超过 18 GB 的速度,这使得它成为我找到的 C# 语言中最快的密码实现(同一台计算机上的 C++ 版本使用 AES-NI 已达到每秒 5882 MB!)。

我肯定对发布此代码有一些强烈的顾虑,其中最重要的是,它很可能会催生大量所谓的“AES 512”副本,而这些作者可能对算法的了解不足以评估、生产或维护安全的加密软件。这真是一个两难的境地,如果我将其留在 Github 上,没有人会看到它;如果我在这里发布它,它可能会被不负责任地使用。我强烈敦促任何考虑使用扩展密钥长度的人仔细研究工作,彻底评估实现,并做出明智的选择。
 
就我而言,我基于众所周知的版本编写了这些实现,并尽可能少地修改了密码以扩展密钥长度。我对库本身有信心,因为我一路小心测试,并认为自己对其中使用的加密原语有了很好的理解。但这仍应被视为其初衷;实验

我欢迎密码学家和程序员的输入。如果您有任何评论或疑虑,我很乐意听取您的意见。我的目标包括将我认为最好、最强的实现转移到一个 C++ 库;CEX++

V1.5 版本已完成,并且将是 .Net 版本库的最后一个版本之一,因为代码开始向 C++ 语言过渡。此版本包括后量子安全的非对称密码、密钥交换协议以及更多我认为必不可少且相关的加密实体。此版本包含了我最初为该项目设想的大部分功能,并且虽然在镜像 C++ 版本代码改进时它可能仍会演变,但这应被视为最终版本,因为该项目不再计划进行重大添加。

除了 NTRU 密码和 DTM-KEX 实现(采用 GPLv3 许可)外,所有源代码都采用 MIT 许可。库的其余部分,包括密码、摘要、Drbg、Prng 等,仍采用 MIT 许可。如果您计划在商业项目中使用 NTRU 实现,则需要与 Security Innovation 联系专利持有人。

此项目分发版中已添加文档作为可选下载,您也可以从示例窗体“帮助”菜单中的网站链接,或直接从:CEX 帮助访问。

本项目包含强大的加密技术;在下载源代码文件之前,您有责任检查您所在的国家/地区是否允许使用扩展对称密码密钥长度(512 位及以上)。如果您使用此代码,请负责任地使用,并遵守您所在地区的法律。

CEX 也可在 GitHub 上找到,名为 CEX Project。您可以在 CEX 主页上访问库 API 文档,以及库的最新版本和示例代码。

1.5 版本有什么新功能?

……然后剩下三个。在最初的分发版中,有三种根对称密码的两个变体:Rijndael、Twofish 和 Serpent,一个版本使用标准的密钥调度(扩展到 512 位密钥),另一个版本使用 HKDF 生成内部工作密钥。这些版本已合并;现在每个密码(RHX、SHX 和 THX)都配备了两个密钥扩展函数。根据 C++ 分发版,密码也已针对速度进行了优化。

“合并”的密码 Fusion、RSM 和 TSM 已被移除。并非我认为它们不安全,只是没有必要……组合两个密码的轮函数涉及大量额外处理,而我认为现有的密码(具有更大的密钥和增加的轮数)将在一段时间内保持安全。

添加了种子生成器 ISAAC 和 XorShift+,以及一个从系统收集熵的类。

对 CipherStream 和密码模式等类进行了重写,因为我逐行检查并优化了所有已进入 C++ 分发版的类。

VolumeFactory、KeyFactory 和 PackageFactory 已与它们的依赖类型一起进行了重新设计,并在 Test 项目中添加了一系列测试。

2016 年 2 月 23 日 1.5.2

  • 文档已移至 Doxygen

2016 年 3 月 16 日 1.5.3

  • 添加了种子生成器 CTRRsp 和 SP20Rsp
  • SHA-2 和 Blake 摘要已展开(优化)
  • RegistryUtils 和 EntropyPool 类已更改
  • 扩展了池并更改了 ISAAC

2016 年 4 月 8 日

  • 展开了 Rijndael 密钥调度 StandardExpand() 例程,从密钥扩展函数中移除了所有分支和循环。
  • 为 CMAC 和 HMAC 添加了新的构造函数,它们现在可以使用引擎枚举成员进行初始化。
  • 添加了 MacDescription 结构,与 CipherDescription 的用法类似。
  • 为 MacStream 添加了新的构造函数,现在可以使用 MacDescription 结构初始化任何支持的 Mac 生成器。
  • DigestFromName 帮助类已使用 GetDigestSize() 和 GetBlockSize() 函数进行了扩展。
  • 添加了新的帮助类 CipherFromDescription 和 MacFromDescription。
  • 重写了 VolumeCipher,更改了 MacStream 和 DigestStream 流类

2016 年 4 月 13 日 -v1.5.5

注意:此版本存在一些破坏性更改。

当我第一次编写使用 HKDF Expand 驱动的密钥调度的对称密码时,我追求的是一种“直到世界末日都好用”的实现,具有大的密码输入密钥,这没关系,因为密码仅用于本地文件加密应用程序。

随着 C++ 版本的不断发展,我重新考虑了这一点,并将最低密钥大小要求降低到更合理的大小,因为该版本的库将用于我正在编写的通信应用程序。

我还更改了 Twofish 和 Serpent 的实现,以便当它们使用 512 位密钥(带有“标准”密钥调度)时,轮数会自动设置为更高的值,以反映增加的安全保证。

  • 将对称密码中 HKDF 扩展密钥调度的最小密钥大小更改为摘要输出大小
  • Serpent 在标准模式下使用 512 位密钥时将轮数设置为 40
  • Twofish 使用 512 位密钥时将轮数设置为 20
  • 移除了 Salsa 和 ChaCha 的不必要循环处理器
  • 更新了整个库的文档

2016 年 5 月 28 日 -v1.5.6

注意:此版本存在一些破坏性更改。

在为 CEX++ 中的 AES-NI 版本编写 Rijndael 密钥调度时,我意识到密钥调度的 512 位密钥扩展例程可以变得更安全。当我编写扩展密钥大小例程时,并没有可以参考的来源,但重新审视代码后发现了需要进行的更改以提高安全性。强烈建议,如果您使用 512 位密钥的 Rijndael,请更新到这个更安全版本(这包括 CEX++)。但我之前的建议仍然有效:如果您需要强大的安全保证,请使用 HKDF 扩展方法而不是本地密钥调度。

  • Rijndael 512 位密钥扩展方法的更改(详情请参阅 Rijndael 部分)
  • KeyGenerator 类的更改;计数器现在在 32 位边界上旋转(请参阅 Rotate 函数)
  • MessageHeader 重命名为 MessageDescription,已更改为允许原地解密(示例项目)
  • 库中对速度和安全性进行了各种改进

库组件

库包含以下组件,随着其发展,有些将被添加,有些将被移除,并在可能的情况下,将进行更改以提高性能、安全性和文档。

非对称加密引擎

NTRU 加密

NTRU Encrypt 是一个公钥密码系统,由三位数学家(Jeffrey Hoffstein、Jill Pipher 和 Joseph H. Silverman)于 1996 年左右开发。它是一种基于格的密码,已持续发展并在许多不同的库和实际应用中使用。此版本的 NTRU 是 Tim Buktu 在 Github 上的 Java 版本的翻译。两个实现之间有一些显著的差异;此 C# 版本可以使用库中的任何 Digest 或 Prng 来驱动内部随机生成,使用可选的并行处理来生成密钥和转换数据,实现了整个 Dispose 接口,并且与库中的所有非对称密码一样,遵循一个易于使用的标准模式(大多数操作只需要几行代码即可实现)。

McEliece

McEliece 公钥密码系统由 Robert McEliece 于 1978 年首次开发,基于解码通用线性码的难度。此后,它经历了一系列安全增强,包括本实现中使用的 CCA2 安全变体。C# 版本基于 Bouncy Castle Java 版本 1.51 的 McEliece 实现,并且与本项目中的其他非对称密码一样,使用并行处理,并与库中的任何 Digest 或 Prng 实现一起运行。

Ring-LWE

基于错误的学习(Learning with errors)的概念由 Oded Regev 于 2005 年首次引入。Ring-LWE 在有限域上的多项式环中使用基于错误的学习。Ring-LWE 密码已被证明是一种高效且“抗量子”的替代方案,可替代广泛使用的 Diffie-Hellman 和椭圆曲线 Diffie-Hellman 密钥交换。此版本基于 Ruan de Clercq、Sujoy Sinha Roy、Frederik Vercauteren 和 Ingrid Verbauwhede 的论文《Efficient Software Implementation of Ring-LWE Encryption'》以及 Github 上的配套源代码。

用法

非对称密码和签名者遵循相同的设计模式,并且尽可能易于使用。

创建密钥对

// use a pre-defined parameter set
RLWEParameters ps = RLWEParamSets.RLWEN512Q12289;
// initialze the key generator
RLWEKeyGenerator gen = new RLWEKeyGenerator(ps);
// create the key-pair
IAsymmetricKeyPair kp = gen.GenerateKeyPair();

序列化和反序列化密钥

// convert a key to a byte array
byte[] keyArray = kp.PublicKey.ToBytes();
// deserialize a key
NTRUPublicKey npubk = new NTRUPublicKey(keyArray);
// convert a key to a stream
MemoryStream keyStream = kp.PrivateKey.ToStream();
// deserialize key
NTRUPrivateKey nprik = new RLWEPrivateKey(keyStream);

加密和解密

RLWEParameters encParams = new RLWEParameters(512, 12289, 12.18, new byte[] { 2, 5, 1 }))
RLWEKeyGenerator keyGen = new RLWEKeyGenerator(encParams);
IAsymmetricKeyPair keyPair = keyGen.GenerateKeyPair();
 
byte[] data = new byte[64];
byte[] enc, dec;

// encrypt an array
using (RLWEEncrypt cipher = new RLWEEncrypt(encParams))
{
    cipher.Initialize(keyPair.PublicKey);
    enc = cipher.Encrypt(data);
}

// decrypt the cipher text
using (RLWEEncrypt cipher = new RLWEEncrypt(encParams))
{
    cipher.Initialize(keyPair.PrivateKey);
    dec = cipher.Decrypt(enc);
}

预定义参数集访问

// from the oid
NTRUParameters params1 = NTRUParamSets.FromId(new byte[] { 2,2,2,105 });
// from enumeration
NTRUParameters params2 = NTRUParamSets.FromName(NTRUParamNames.E1171EP1);
// clone static member
NTRUParameters params3 = NTRUParamSets.APR2011743.DeepCopy();

 

非对称签名引擎

Rainbow

Rainbow 多变量方程签名方案属于一类称为“非平衡油和醋密码系统”(UOV Cryptosystems)的多变量二次方程密码系统。T. Matsumoto 和 H. Imai 于 1988 年首次提出了他们的“Matsumoto-Imai-Scheme”方案。此实现是 Bouncy Castle Java 实现的翻译。

GMSS

广义Merkle签名方案基于哈希树和一次性签名(如 Lamport 签名方案)。这也基于 Bouncy Castle Jave 实现。

对称加密引擎

基本算法

三个基本密码;RijndaelSerpentTwofish 都经过了彻底的测试,以确保它们与使用较小最大密钥长度的有效实现一致。用于转换数据的算法与任何密钥长度都相同;只有密钥调度本身(可选)被扩展了(密钥调度接受一个小的用户密钥,并将其扩展成一个较大的工作数组,用于轮函数以创建唯一的输出)。这些对密钥调度的更改以及灵活的轮数分配,增加了密码的潜在安全性,使其更难被密码分析,并且更耐受暴力攻击。

  • RHX:(Rijndael)Rijndael 算法的实现
  • SHX:(Serpent)Serpent 加密算法的实现
  • THX:(Twofish)Twofish 加密算法的实现

该库还包括 ChaCha 和 Salsa20 流密码的并行化版本。

HX 密码:基于 HMAC 的密钥调度

HX 系列密码使用标准密码的相同加密和解密算法(变换),不同之处在于密钥调度已扩展为(可选)使用基于 HMAC 的密钥派生函数(HKDF)来扩展内部工作密钥数组。

HKDF 由加密哈希函数驱动,是创建伪随机输出的最强大的密码学方法之一。基于 HKDF 的密钥调度至少需要摘要哈希大小的密钥输入,因此对于 256 位摘要,将是 32 字节;512 位摘要将是 64 字节。

HKDF 使用该输入密钥材料来生成密码学上强大的工作密钥数组。使用的摘要可以通过 HX 密码构造函数 KdfEngineType 参数定义;因此,任何已实现的摘要都可以用来驱动密钥调度的 KDF:Blake、Keccak、SHA-2 或 Skein。

使用基于 HMAC 的密钥派生函数有几个优点;更强大的工作密钥更不容易受到利用弱密钥或相关密钥的攻击向量的影响。更大的用户密钥长度也使得暴力攻击实际上不可能实现;2(256-1) 对比至少 2(512-1) 次迭代。HX 密码的另一个优点是扩散轮数(变换循环内的变换周期)可独立于初始密钥长度进行配置。

  • RHX:10 至 38 轮扩散
  • SHX:32(默认)至 64 轮扩散
  • THX:16(默认)至 32 轮扩散

密码模式

  • CBC:密码块链接,解密可选择并行化
  • CFB:密码反馈,解密可选择并行化
  • CTR:分段计数器,加密和解密可选择并行化
  • OFB:输出反馈模式
  • ECB:电子密码本模式

确定性随机字节生成器

  • CTRDrbg:基于加密计数器的确定性随机字节生成器
  • DGCDrbg:基于摘要计数器的确定性随机字节生成器
  • HKDF:基于 HMAC 的密钥派生函数
  • PBKDF2:基于哈希的密钥派生函数实现
  • KDF2Drbg:基于哈希的密钥派生函数实现
  • SP20Drbg:Salsa20 并行化确定性随机字节生成器实现

消息认证码

  • CMAC:基于密码的消息认证码:CMAC
  • HMAC:基于哈希的消息认证码
  • VMAC:基于可变修改置换组合的消息认证码

消息摘要

  • SHA256:SHA-2 的实现,具有 256 位哈希输出
  • SHA512:SHA-2 的实现,具有 512 位哈希输出
  • Keccak256:Keccak 摘要,返回 256 位
  • Keccak512:Keccak 摘要,返回 512 位
  • Blake256:Blake 摘要,返回 256 位
  • Blake512:Blake 摘要,返回 512 位
  • Skein256:Skein 摘要,返回 256 位摘要
  • Skein512:Skein 摘要,返回 512 位摘要
  • Skein1024:Skein 摘要,返回 1024 位摘要

辅助类

  • KeyGenerator:用于生成加密强度的密钥材料的辅助类
  • KeyHeader:用于管理加密密钥头结构的辅助类
  • MessageHeader:用于管理消息头结构的辅助类
  • BlockCipherFromName:使用枚举名称创建对称块密码实例
  • CipherFromDescription:使用 CipherDescription 创建对称密码和模式
  • CipherModeFromName:使用枚举名称创建对称密码模式实例
  • DigestFromName:使用枚举名称创建加密哈希实例
  • MacFromDescription:使用 MacDescription 创建 Mac 生成器实例
  • PaddingFromName:使用枚举名称创建对称密码填充实例
  • PrngFromName:使用枚举名称创建伪随机数生成器实例
  • SeedGeneratorFromName:使用枚举名称创建种子生成器实例
  • StreamCipherFromName:使用枚举名称创建流密码实例

数值

  • BigInteger:提供 BigInteger 运算,用于模运算、GCD 计算、素性测试、素数生成、位操作和其他杂项操作。
  • BigMath:与整数(表示为 int 或 BigInteger 对象)相关的数论函数类。
  • BigDecimal:此类表示不可变的任意精度十进制数。

网络

  • PacketBuffer:包含可搜索的数据包流列表的类。
  • TcpSocket:一个可处置的套接字类,包装异步和同步网络操作

填充

  • ISO7816:ISO 7816 填充。
  • PKCS7:PKCS7 填充。
  • TBC:尾部比特补码填充。
  • X923:X923 填充。
  • Zeroes:全零填充。

密钥工厂

  • KeyFactory:用于创建或提取 CipherKey 文件的辅助类
  • PackageFactory:用于创建或提取 KeyPackage 文件的辅助类
  • VolumeFactory:用于创建和提取 VolumeKey 文件的辅助类

密钥类型

  • KeyParams:用于加密密钥、向量和 Ikm 数组的容器类
  • CipherKey:与 CipherStream 类一起使用;此结构用作单次使用密钥和向量集的头
  • PackageKey:包含 KeyAuthority 结构(具有身份和来源)、附加策略、子密钥集描述以及包含密码描述的 CipherDescription 结构
  • VolumeKey:此结构用于加密一系列文件,每个文件都有唯一的密钥/IV 配对;例如目录或在线卷

处理

  • CompressionCipher:扩展 CipherStream 类,用于加密压缩目录文件。
  • CipherStream:密码辅助类;执行加密文件所需的所有操作。
  • StreamDigest:摘要辅助类;包装从 Stream 创建哈希摘要
  • StreamMac:MAC 辅助类;包装从 Stream 创建消息认证码
  • VolumeCipher:用于加密或解密目录或卷上文件系列的辅助类

伪随机数生成器 CipherStream

  • BBSG:Blum-Blum-Shub 随机数生成器
  • CCG:立方同余生成器 II 随机数生成器
  • CSPRng:使用 RNGCryptoServiceProvider 的密码安全的 PRNG
  • CTRPrng:基于加密计数器的确定性随机数生成器实现
  • DGCPrng:基于摘要计数器的随机数生成器实现
  • MODEXPG:模幂运算生成器随机数生成器
  • PBPRng:基于口令的 PKCS#5 随机数生成器实现
  • QCG1:二次同余生成器 I 随机数生成器
  • QCG2:二次同余生成器 II 随机数生成器
  • SP20Prng:Salsa20 计数器确定性随机数生成器实现
  • SecureRandom:密码安全的伪随机数生成器实现

种子生成器

  • EntropyPool:使用系统熵源提供生成器的初始状态
  • CSPRsg:使用 RNGCryptoServiceProvider 类的密码安全种子生成器
  • ISCRsg:ISAAC 伪随机生成器的实现
  • XSPRsg:XorShift+ 伪随机生成器的实现
  • CTRRsp:对称密码计数器伪随机生成器的实现
  • SP20Rsp:Salsa20 计数器伪随机生成器的实现

排队

  • JitterQueue:向排队操作添加少量随机延迟时间
  • WaitQueue:演示使用队列创建恒定时间实现

安全

  • SecureDelete:一个 5 阶段的安全文件删除类

实用程序

  • Compare:比较数组是否相等
  • Compression:一个完全实现的压缩和文件夹归档类
  • DirectoryTools:目录方法包装类
  • DriveTools:驱动器方法包装类
  • FileTools:文件方法包装类
  • NetworkTools:网络工具类
  • RegistryUtils:系统注册表实用工具类

DTM-KEX

DTM 是“延迟信任模型”的缩写,这体现在它如何处理身份验证和前向保密等机制。这些任务由软件的另一层处理,KEX 只负责会话密钥材料的传输。这是一个分层加密协议,也就是说,在密钥交换过程中存在两个独立的非对称和对称密码。

协议步骤可逻辑上分为两个部分或“阶段”;第一部分是身份验证阶段,在此阶段交换第一个非对称和对称密钥,并进行主机身份验证。

主要阶段传输最终会话密钥,该过程从头到尾都使用身份验证阶段的对称密钥加密,包括非对称参数、公钥和(双重)加密的主要会话密钥。

设计理念源于对提供最大灵活性和高度安全性的密钥交换的需求。我认为可以将库示例视为协议和协议的实现;因此,将描述分为两部分可能会有所帮助。

DTM 协议

交换有九个阶段;连接初始化预认证AuthEx认证同步PrimeEx主要已建立

会话始于 Connect 阶段;客户端Alice向服务器Bob请求交换。这也是进行参数协商的地方;交换中使用的非对称和对称密码由服务器(接受呼叫的节点)确定,方法是强制执行交换的最低安全上下文。

连接请求包含一个部分 DtmIdentity 结构:对于交换的Connect Stage,它只包含一个公共身份字段,该字段唯一标识网络上的主机。此身份字段是一个无界字节数组。它可以包含一个序列化结构,其中包含更多信息,如连接先决条件、签名 ID 字段、安全参数,以及您的组织或应用程序需求所要求的任何内容,但它应包含一个可用于主机初始标识的 ID 字段。如果服务器接受连接,它会将其自身的公共 ID 发回作为响应,此时交换会进入Init Stage。这是在交换过程中发生的四项身份检查之一,身份数据通过事件传递给应用程序,该事件还可以取消交换,发送一个带有 Refuse 选项标志的终止数据包。操作可以因拒绝身份数据或非对称密码参数而取消。

Init 阶段与 Connect 阶段类似,只是在初始传输被接受后,DtmIdentity 结构会被完全填充。它包含身份验证阶段的对称密码参数(密码类型、密钥长度、模式等)以及第一阶段公钥交换的非对称参数。

PreAuth 阶段是两个节点交换身份验证阶段的公钥。这些公钥用于加密第一个对称密码的密钥。请注意,交换是双向的;Bob 发送给 Alice 的数据使用 Bob 生成的密钥加密,Alice 发送的数据使用她的密钥加密。这样,每个参与者都负责其传输通道的安全性。

在接收到非对称公钥后,每个客户端生成一个对称密钥,使用另一个客户端的公钥加密该密钥,并发送该加密的对称密钥。这是身份验证阶段的对称密钥,从此刻起,所有内容都使用该密钥加密;主要的非对称参数、公钥和主要会话密钥。

这是密钥交换身份验证阶段的最后阶段。DtmIdentity 结构被加密并交换。与 Connect 和 Init 阶段一样,身份字段是一个无界字节数组,其中可以包含一个序列化结构,包含安全信息、操作标志、签名数据数组,或者仅仅是该主机独有的持久二次身份验证字符串。

这是密钥交换主要阶段的第一阶段。与 InitStage 一样,会交换完整的 DtmIdentity 结构,只是这次它使用身份验证阶段的对称密钥进行加密。此身份字段包含主要对称密码参数,以及非对称参数,它可以是标识预定义参数集的 OId 字段,或序列化的非对称参数。

主要非对称密码的公钥使用身份验证阶段的对称密钥进行加密,并在主机之间交换。

主要对称密钥由每个主机生成,使用主要非对称公钥进行加密,然后再次使用身份验证阶段的对称密钥进行加密。主要阶段交换有许多安全措施;对称和非对称密钥及参数可以在加密和传输前用随机字节前缀和后缀,以掩盖其大小和类型,并且可以在传输主要非对称密钥之前引入有范围的随机延迟。

这是交换的最后一个阶段,主机在此确认 VPN 已建立状态。VPN 现在已准备好进行数据传输。

DTM 实现

当我开始研究密钥交换协议时,我查看了 TLS 和 PGP,很快就意识到这些协议不仅对于简单的点对点交换来说非常复杂,而且还有很多我不需要的功能,并且缺少我想要添加的功能。于是……我开始考虑设计一个密钥交换,它具有高度的灵活性,可以应用于广泛的用途,使用后量子安全的密码构建,并提供强大而灵活的安全模型。我认为保持交换灵活性的最佳方法是将身份验证“卸载”或“推迟”到更高级别的软件,从而软件,以及用户操作和应用程序设置,至少在一定程度上决定安全级别、身份验证、否认性和交换的交易方式。

大多数密钥交换的工作方式都相同;两个主机交换非对称公钥;它们使用对方的公钥加密对称密码密钥(使用对称密码是因为它们更快,而非对称密码的消息大小有限)。对称密钥通过非对称密码的私钥进行交换和解密,然后用于加密/解密数据流。
此交换使用两阶段密钥交换机制,第一个非对称密码 Ring-LWE 加密在主机之间传输的对称密钥,然后这些对称密钥用于身份验证,并加密交换的第二部分,该部分使用第二个非对称密码(如 NTRU)加密主要对称会话密钥。这样做有几个原因;某些非对称密码的参数集不应通过开放通道传输,它们包含可能帮助攻击者的信息(如 NTRU 中消息前缀随机字节的 Db 字段)。因此,通过加密第二个密码的参数、公钥,并对主要会话密钥进行双重加密,您可以创建一个需要破解两个非对称密码才能派生主要对称会话密钥的传输机制(该密钥可以是此库中任何密码的密钥,包括 RHX 等扩展混合密码,它们本身几乎是不可破解的)。与使用 RSA 或 ECDH 的密钥交换不同,此库中的后量子安全非对称密码不受量子计算机攻击的影响,这对长期数据安全来说是一个严重的问题,因为一旦大型量子计算机建成(可能在未来十年内),RSA 和 ECDH 将被破解。

CEX 的核心设计目标之一是提供即插即用的高级加密解决方案,从而开发人员只需几行代码即可加密文件或流,因为坦率地说,许多加密库都很难使用,工作示例很少,文档稀疏,开发人员必须进行大量的研究才能在他们的应用程序中正确实现加密。所以这是另一个问题,如何包装整个密钥交换和网络堆栈,以便可以简单可靠地使用它?这非常有挑战性,但我在这里展示的内容允许这样做,只需五行代码即可完成密钥交换和加密的 TCP 通道。

CEX 解决方案包含两个测试项目:DtmServer 和 DtmClient。要运行示例,请在解决方案资源管理器中将 DtmServer 设置为启动项目,然后启动它,然后右键单击 DtmClient 项目并选择:调试->启动新实例。整个交换将在计算机的环回地址上执行,您将看到两个配置为简单聊天客户端的窗口。

一个示例

// dtm server exchange parameters X11RNS1R2
DtmParameters srvDtmParams = DtmParamSets.FromName(DtmParamSets.DtmParamNames.X42RNS1R1);       // preset contains all the settings required for the exchange

// dtm server id
DtmClient srvDmtId = new DtmClient(
    new byte[] { 3, 3, 3, 3 },      // the clients public id, (should be at least 32 bytes, can be used as a contact lookup and initial auth)
    new byte[] { 4, 4, 4, 4 });     // the clients secret id, (secret id can be anything.. a serialized structure, signed data, hash, etc)

// create the server
_dtmServer = new DtmKex(srvDtmParams, srvDmtId);
_dtmServer.IdentityReceived += new DtmKex.IdentityReceivedDelegate(OnIdentityReceived);         // returns the client public and secret id fields, used to authenticate a host
_dtmServer.PacketReceived += new DtmKex.PacketReceivedDelegate(OnPacketReceived);               // notify that a packet has been received (optional)
_dtmServer.SessionEstablished += new DtmKex.SessionEstablishedDelegate(OnSessionEstablished);   // notify when the vpn state is up
_dtmServer.PacketSent += new DtmKex.PacketReceivedDelegate(OnPacketSent);                       // notify when a packet has been sent to the remote host (optional)
_dtmServer.DataReceived += new DtmKex.DataTransferredDelegate(OnDataReceived);                  // returns the decrypted message data
_dtmServer.FileReceived += new DtmKex.FileTransferredDelegate(OnFileReceived);                  // notify that a file transfer has completed
_dtmServer.FileRequest += new DtmKex.FileRequestDelegate(OnFileRequest);                        // notify that the remote host wants to send a file, can cancel or provide a path for the new file
_dtmServer.SessionError += new DtmKex.SessionErrorDelegate(OnSessionError);                     // notify of any error conditions; includes the exception, and a severity code contained in the option flag
           

// server starts listening
_dtmServer.Listen(IPAddress.Any, Port);

上面代码的第一行是 DTM 参数集的声明,这包括使用的非对称密码、它们的参数、对称密码描述以及一些安全控制;例如在加密和传输主要公钥之前,要在其前后附加/前置多少随机数据。

下一行代码是 DtmClient 结构;它包含公共和秘密消息数组。这些数组是在密钥交换的 Connect、Init 和 Auth 阶段在 DtmIdentity 结构中传输的。这些数组是无界的,并且可以是序列化结构,包含应用程序层用于身份验证调用者的标志和数据层次结构。

下一行初始化 DtmKex 类,传入 DtmParameters 和 DtmClient 数据,这些数据用于指导交换的工作操作。

接下来我们连接事件

  • IdentityReceived 事件在收到远程主机的身份数据包时触发。它包含用于通过应用程序层身份验证节点的身份消息数据。
  • PacketReceivedPacketSent 在每次交换期间或之后发送或接收数据包时触发。
  • SessionEstablished 在交换建立时触发,并返回初始化的前向和返回主要对称密码实例,以及活动的套接字实例。如果使用此事件,则可以处置 DtmServer 类,而其余会话由外部处理。
  • DataReceived 事件用于 DtmKex 实例不仅作为交换的载体,而且作为主要的加密通道。从远程主机接收的所有数据在通过此事件发送之前都经过处理和解密。
  • FileSentFileReceived 用于指示文件传输已完成。文件传输是独立的实例,每个都有自己的密钥和网络套接字。

最后一行;Listen(address, port开始监听连接,在此之前传递 IP 地址和端口号。

设置 DtmClient 的代码完全相同,只是使用 Connect(address, port) 代替 Listen 方法。

DtmParameters 和 DtmParamSets

DtmParameters 类,就像非对称密码参数一样,定义了用于密钥交换和加密通道的内部设置

  • OId:DtmParameters 标识符字段;应为 16 字节,描述参数集
  • AuthPkeId:身份验证阶段非对称参数 OId;可以是 Asymmetric 密码参数 OId,或序列化的 Asymmetric Parameters 类
  • PrimaryPkeId:主要阶段非对称参数 OId;可以是 Asymmetric 密码参数 OId,或序列化的 Asymmetric Parameters 类
  • AuthSession:身份验证阶段对称会话密码参数;包含对称密码的完整描述
  • PrimarySession:主要阶段对称会话密码参数;包含对称密码的完整描述
  • RandomEngine:用于填充消息的 Prng 类型
  • MaxAsmKeyAppend:(可选)加密前附加到主要阶段非对称公钥的最大伪随机字节数
  • MaxAsmKeyPrePend:(可选)加密前前置到主要阶段非对称公钥的最大伪随机字节数
  • MaxAsmParamsAppend:(可选)加密前附加到主要阶段客户端身份的最大伪随机字节数
  • MaxAsmParamsPrePend:(可选)加密前前置到主要阶段客户端身份的最大伪随机字节数
  • MaxSymKeyAppend:(可选)加密前附加到主要阶段对称密钥的最大伪随机字节数
  • MaxSymKeyPrePend:(可选)加密前前置到主要阶段对称密钥的最大伪随机字节数
  • MaxMessageAppend:(可选)加密后消息附加的最大伪随机字节数
  • MaxMessagePrePend:(可选)加密前消息前置的最大伪随机字节数
  • MaxAsmKeyDelayMS:(可选)发送主要阶段非对称密钥前的最大延迟时间;最小时间为最大值的一半,值为 0 表示无延迟
  • MaxSymKeyDelayMS:(可选)发送主要阶段对称密钥前的最大延迟时间;最小时间为最大值的一半,值为 0 表示无延迟
  • MaxMessageDelayMS:(可选)发送交换后消息流量前的最大延迟时间;最小时间为 0,值为 0 表示无延迟

这些参数大部分不是必需的,并且有时很难将非对称密码的有时很小的最大消息大小与对称密钥和向量大小匹配。这就是为什么有一个 DtmParamSets 类,其中包含按安全分类排序的多个预设。参数集是可序列化的、可克隆的,并且可以通过 OId、枚举或静态实例的深拷贝来检索。安全上下文枚举按安全分类对预设 X1 到 X4 进行排序,X1 为最高安全性,X4 为速度优化。

DtmKex 内部

这个类有很多底层的东西;心跳计时器、自动加密流重同步、自动重连、IPv6 就绪、并行文件传输……我可以专门写一篇文章来介绍这个类。因此,项目上有数千页的文档,并且我特意对该类进行了详细注释。所以,如果您有兴趣,可以调试、阅读、再调试……以下是一些亮点:

属性

  • AutoReconnect:获取/设置尝试重新连接到主机,如果连接因错误或超时而丢失
  • ConnectionTimeOut:获取/设置在连接被视为丢失之前,连续错过的保活包(以一秒为间隔)的数量
  • IsConnected:获取连接状态;如果连接到远程主机,则为 true
  • IsEstablished:获取 VPN 已建立
  • FileBufferSize:获取/设置:TCP 和缓冲区队列元素的 TCP 文件大小
  • MaxResend:获取/设置数据包可以重发的最大次数;默认为 1024
  • MessageBufferSize:获取/设置消息 TCP 和缓冲区队列元素的大小
  • ResendThreshold:获取/设置触发重发的队列消息包数量
  • Socket:获取 TcpSocket 类实例

方法

  • Connect(address, port):使用主机名或 IP 地址连接;发送连接请求,开始密钥交换。
  • ForwardKeyRequest 允许在 TCP 流上实时进行密钥棘轮
  • Listen(address, port):初始化服务器并监听传入连接
  • Disconnect():断开与远程主机的连接并拆除连接
  • SendReceive(MemoryStream, int):阻塞收发器;发送数据包并等待响应
  • Send(Stream):在交换后用于加密数据,然后再发送给客户端
  • SendFile(string):用于初始化文件传输序列。每个文件都在其自己的 TCP 套接字上发送,使用唯一的密钥。这样做是为了提高可靠性(丢失一个数据包意味着需要重新同步主通道),并为大型数据实现缓冲并行传输机制。
  • Dispose():清理资源并处置类

事件

  • DataReceived:每次通过交换后加密通道接收到数据时触发,包含解密后的接收数据
  • Disconnected:连接被处置时触发
  • FileReceived:文件接收操作完成时触发
  • FileSent:文件发送操作完成时触发
  • FileRequest:主机收到待处理文件传输通知时触发
  • IdentityReceived:收到包含身份数据的包时触发
  • PacketReceived:每次收到有效数据包时触发
  • PacketSent:每次发送有效数据包时触发
  • ProgressPercent:以整数百分比返回接收到的文件字节处理情况
  • SessionError:发生错误时触发
  • SessionEstablished:VPN 建立时触发

对称处理器

CompressionCipher

Compression 密码通过首先压缩目标目录,然后加密压缩文件来包装 CipherStream 类。解密则将目录解压缩到目标路径。

public static void CompressionCipherTest(string InputDirectory, string OutputDirectory, string CompressedFilePath)
{
    KeyParams kp = new KeyGenerator().GetKeyParams(32, 16);
    // Create an archive //
    // create the cipher
    using (ICipherMode cipher = new CTR(new RHX()))
    {
        // initialize the cipher for encryption
        cipher.Initialize(true, kp);

        // create the archive file
        using (FileStream fs = new FileStream(CompressedFilePath, FileMode.Create))
        {
            // compress and encrypt directory
            using (CompressionCipher cc = new CompressionCipher(true, cipher))
            {
                // set the input folder path and archive output stream
                cc.Initialize(InputDirectory, fs);
                // write the compressed and encrypted archive to file
                cc.Write();
            }
        }
    }

    // Inflate an archive //
    // create the cipher
    using (ICipherMode cipher = new CTR(new RHX()))
    {
        // initialize the cipher for decryption
        cipher.Initialize(false, kp);

        // open the archive
        using (FileStream decmp = new FileStream(CompressedFilePath, FileMode.Open))
        {
            // decrypt and inflate to output directory
            using (CompressionCipher cc = new CompressionCipher(false, cipher))
            {
                // set the output folder path and archive path
                cc.Initialize(OutputDirectory, decmp);
                // decrypt and inflate the directory
                cc.Write();
            }
        }
    }
}

VolumeCipher

这是一种文件系统级别的加密。整个目录,包括子目录,都可以被加密,每个文件都用其唯一的密钥和向量进行加密。单个文件或整个目录都可以使用 VolumeKey 解密;VolumeKey 是一个由 VolumeFactory 类创建和提取的专用密钥。

加密卷

private void VolumeEncrypt(string[] VolumeFiles, string KeyPath)
{
    // key will be written to this stream
    MemoryStream keyStream = new MemoryStream();

    // encrypt the files in the directory
    using (VolumeCipher vc = new VolumeCipher())
    {
        keyStream = vc.CreateKey(CipherDescription.AES256CTR, VolumeFiles.Length);
        vc.ProgressPercent += OnVolumeProgressPercent;
        vc.Initialize(keyStream);
        vc.Encrypt(VolumeFiles);
    }

    // write the key
    keyStream.Seek(0, SeekOrigin.Begin);
    using (FileStream outStream = new FileStream(KeyPath, FileMode.Create, FileAccess.ReadWrite))
        keyStream.CopyTo(outStream);

    // clean up
    keyStream.Dispose();
}

解密卷中的文件(s)

private void VolumeDecryptFile(string FilePath, string KeyPath, DecVolume)
{
    string[] paths = new string[1];

    if (DecVolume)
        paths = FileUtilities.DirectoryGetFiles(Path.GetDirectoryName(FilePath));
    else
        paths[0] = FilePath;

    using (FileStream ks = new FileStream(KeyPath, FileMode.Open, FileAccess.ReadWrite))
    {
        using (VolumeCipher vc = new VolumeCipher())
        {
            vc.Initialize(ks);
            vc.Decrypt(paths);
        }
    }
}

密钥包

Example 项目使用包密钥;这些是复合密钥或“子密钥集”,包含单个加密周期所需的密钥材料。包密钥解决了加密密钥分发中的许多安全问题。例如,你把一把钥匙给朋友,但你怎么知道他不会把钥匙分发给别人,或者将该钥匙用于另一次加密?包密钥不仅包含密钥材料,还包含可组合的标志,用于控制密钥的处理方式;谁可以使用该密钥,该密钥可以用于解密的次数,限制子密钥仅用于一次加密,甚至可以在解密后安全地从集中删除子密钥。一个包密钥文件可以包含数千个子密钥,具有分层的安全访问方案,并且可以通过基于口令的加密密码本身进行加密。包密钥使用 PackageFactory 类创建。

// populate a KeyAuthority structure
KeyAuthority authority =  new KeyAuthority(domainId, originId, packageId, packageTag, keyPolicy);
// create a key file
new PackageFactory(KeyPath, authority).Create(PackageKey);

// populate a KeyAuthority structure
KeyAuthority authority =  new KeyAuthority(domainId, originId, packageId, packageTag, keyPolicy);
KeyParams keyparam;
CipherDescription description;
byte[] extKey;
byte[] keyId;

// extract a key for decryption
using (PackageFactory factory = new PackageFactory(KeyPath, authority))
    factory.Extract(keyId, out description, out keyparam, out extKey);

// populate a KeyAuthority structure
KeyAuthority authority =  new KeyAuthority(domainId, originId, packageId, packageTag, keyPolicy);
KeyParams keyparam;
CipherDescription description;
byte[] extKey;

// get the next available encryption subkey
using (PackageFactory factory = new PackageFactory(KeyPath, authority))
    keyId = factory.NextKey(out description, out keyparam, out extKey)

CipherStream

CipherStream 类为您完成所有工作……它计算理想的并行块大小(基于可配置的 BlockProfile 设置),提供进度事件,并将由密码处理的数据从一个流写入另一个流。它尽可能简单易用。看看……

KeyParams kp;

using (KeyGenerator kg = new KeyGenerator())
    kp = kg.GetKeyParams(64, 16);

using (ICipherMode cipher = new CTR(new RHX()))
{
    using (CipherStream cstrm = new CipherStream(cipher))
    {
        cstrm.ProgressPercent += new CipherStream.ProgressDelegate(TestProgressPercent);
        cstrm.Initialize(true, kp);
        cstrm.Write(new FileStream(FileMode.Open, FileAccess.Read), new FileStream(outFile, FileMode.Create, FileAccess.Write));
    }
}

在示例中,我们创建了一个由 KeyGenerator 类填充的 KeyParams 结构。然后创建一个包装在 CTR 模式下的密码实例。在这种情况下,它是 RHX 密码(但它适用于所有密码实现)。我们使用该密码初始化 CipherStream 类。添加进度事件,然后使用加密标志和 KeyParams 类初始化该类。输入流和输出流被传递给 Write() 方法,该方法启动流处理。然后为您加密或解密输入流。海量加密,代码仅需五行……

密钥和向量创建

KeyGenerator 类使用两阶段伪随机生成方法。它使用种子生成器(可以通过不同的熵源进行初始化)来创建种子字节,以及一个递增的可变长度计数器;两者结合并由加密哈希函数(HMAC 也与 SHA-2 一起使用)处理,以生成伪随机输出:Digest(Rsg() + ctr)。

KeyGenerator 构造函数包含两个可选参数;SeedEngine 和 DigestEngine。种子材料可以使用任何已实现的 Rsg 生成。摘要引擎可以是任何摘要。计数器使用机器本地 RngCrypto 提供程序,但种子生成器(使用 XSPRsg 和 ISCRsg 时)可以使用 EntropyPool 机制进行初始化。因此,您可以使用 ISAAC 和 1024 位 Skein 的组合,或者 RngCrypto 和 Keccak……或者您选择的任何其他种子/摘要组合来创建密钥材料……KeyGenerator 可以返回字节数组,或填充的 KeyParams 类。

byte[] rand;
using (KeyGenerator gen = new KeyGenerator())
    // generate pseudo random bytes
    rand = gen.Generate(Size);

Headers

CipherKey
CipherKey 结构包含密码描述;重新创建密码实例所需的所有参数。它还包含一个唯一的 ID 字段,以及 16 字节的随机数据,用于加密文件扩展名。在 CipherDescription 中,有两个字段用于确定 HMAC 设置;MacSize 整数,存储摘要输出的大小,以及 MacEngine,存储用于创建 MAC 代码的摘要。如果 MacSize 设置为零,则表示没有对消息文件进行 HMAC 处理。如果 MacSize 非零,则认为它是签名密钥。
 
Message Description
Message Description 包含用于加密该消息的密钥的唯一 ID,以及可选 MAC 代码的长度。消息可以包含 MAC 代码;与 KeyHeaderStruct 中的 MAC 密钥一起使用,以验证消息的完整性。Message Description 本身不包含 MAC 的任何描述,也不包含是否使用了 MAC。MAC 代码源自一个密钥化的摘要,该摘要通过处理消息密文来获取其代码。MAC 本身是一种伪随机置换,因此攻击者无法将其与密文区分开来。没有密钥,攻击者将无法知道消息在哪里结束,因为没有密钥文件,他们不知道 MAC 字段的长度,它可能是 32、64 或 128 字节,或者根本没有 MAC……关键是,我们尽可能少地给对手提供信息。
 
KeyFactory
KeyFactory 类是一个辅助类,用于创建或提取 Key 文件。它非常容易创建密钥文件,或将文件提取到包含密码描述的 CipherKey 头和包含密钥材料的 KeyParams 类。如果在创建过程中未指定 KeyParams 实例,则会自动使用 KeyGenerator 类生成随机密钥材料。

创建密钥文件

// populate a keyheader
CipherKey keyHeader = new CipherKey(
    Engines.RHX,        // cipher engine
    64,                 // key size in bytes
    IVSizes.V128,       // cipher iv size enum
    CipherModes.CTR,    // cipher mode enum
    PaddingModes.X923,  // cipher padding mode enum
    BlockSizes.B128,    // block size enum
    RoundCounts.R18,    // diffusion rounds enum
    Digests.Skein512,   // cipher kdf engine
    64,                 // mac size
    Digests.Keccak);    // mac digest

// create the key file
new KeyFactory(KeyPath).Create(keyHeader);

提取密钥文件

// local vars
KeyParams keyparam;
CipherKey header;

new KeyFactory(KeyPath).Extract(out keyparam, out header);

消息认证

消息认证可以通过 StreamMac 类处理。与 CipherStream 类一样,所有设置都为您完成。推荐的 MAC 创建方式是在加密之后。在示例中,mac 流使用 MAC 密钥进行初始化,要处理的文件流在 Initialize 方法中定义,ComputeMac 返回 MAC 代码。

using (MacStream mstrm = new MacStream(new SHA512HMAC(keyParam.IKM)))
{
    // initialize mac stream
    mstrm.Initialize(outStream);
    // get the hash
    byte[] hash = mstrm.ComputeMac();
}
要验证 MAC,只需再次在文件上运行 mac 流,并将输出与消息中包含的 mac 代码进行比较。Example 项目包含一个工作示例。
 

EntropyPool

EntropyPool 类收集来自系统各种计数器、计时器和句柄的状态,将它们分布到 8 个内部池中,然后使用 Keccak 512 压缩这些状态。收集的状态包括存储在注册表中的 Clsid 字符串、进程和线程句柄及统计信息、系统计时器以及 RngCrypto 随机提供程序的字节。这些状态以循环方式逐字节分布到八个队列中,然后从大约 7200 字节压缩到 512 字节,分布在 16 个池成员中。该池可用于播种 Drbg 或种子生成器,进而用于生成密钥材料。

对称密码概述

在开始查看一些密码并深入研究实现细节之前,我认为有必要对已完成的工作进行一个总体概述,并澄清文章中使用的一些概念和术语。

首先,密钥调度密钥调度是一个函数,它接收少量的用户提供数据(输入密码密钥),并将其扩展,通常扩展成一个较大的整数数组。例如;Rijndael 接受一个 32 字节的密钥(256 位),并将其扩展成 60 个整数,或 240 字节的密钥材料。该整数数组有时被称为“轮密钥”、“子密钥”或“工作密钥”数组,我将使用“工作密钥”一词,因为它清楚地表明它是一个派生密钥。一些密钥调度具有简单的代数表达式;例如 Rijndael,它通过简单地对两个前一个密钥进行异或来派生大部分工作密钥。Serpent 使用一个更复杂的密钥调度,旨在抵抗某些形式的密码分析。这些工作密钥由密钥调度创建,用于创建唯一的密文,而一个好的密码设计是这样的:更改单个密码密钥位会导致完全不同的输出,这被称为“雪崩”属性。工作密钥通常通过简单的加法或异或添加到状态(变换某个阶段处理的数据)中。
 
更大的密钥在密码的安全性中起着重要作用。许多用于“破解”密码的技术涉及减少用密码解密输出测试唯一密钥的次数,换句话说,它们减少了解密输出所需的暴力尝试次数。
 
每次将密钥长度增加一位,都会使前一个潜在总和的大小加倍。因此,256 位密钥代表一个整数,其最大值约为 1.15 x 1077。这是一个巨大的数字……人们可能会认为计算机永远不够快,无法运行如此多次的解密周期,这绝对是真的,但一些密码分析攻击旨在减少这个数字,有时会大大减少。更大的密钥使这一点更加难以实现;鉴于密码密钥和生成的工作密钥是以密码学方式强大的方式完成的,更大的密钥是可行的,通过使用这些更长的密钥,数据可以被保留在技术能力之外更长的时间。
 
还有证据表明密钥调度在提供对线性密码分析和差分密码分析的抵抗力方面起着作用,并且 Rijndael 曾受到一些严重的攻击,这些攻击利用了弱密钥调度。因此,一个密码学上强大的密钥调度可以帮助创建一个更强大的密码。

在本文中,变换是一个执行数据实际加密的函数,正如可逆迭代块密码中的逆变换执行解密一样。在基于轮的密码(有时称为积密码)中,可以被视为一次完整的变换序列,而变换函数(或轮函数)可能循环通过若干轮并使用白化阶段。

在本文提出的三个块密码(Rijndael、Serpent 和 Twofish)中,输入字节首先被复制到四个整数中。这些整数(在 Rijndael 和 Twofish 的情况下)与工作密钥的成员进行异或(密钥白化)。然后,这些状态整数在一系列轮中进行处理,通过一系列替换、置换和模运算来改变状态(在各个阶段添加密钥),最后,处理过的状态被白化并复制到输出字节数组中。

让我们看一轮 Serpent

R0 ^= _exKey[keyCtr++];
R1 ^= _exKey[keyCtr++];
R2 ^= _exKey[keyCtr++];
R3 ^= _exKey[keyCtr++];
Sb0(ref R0, ref R1, ref R2, ref R3);
LinearTransform(ref R0, ref R1, ref R2, ref R3);

R0 到 R3 是状态整数。在每轮之前,状态与工作密钥的成员进行异或。然后状态通过八个比特切片 S-Box 之一进行处理,然后进行线性变换。这清楚地说明了工作密钥在轮周期中的作用;工作密钥用于混合状态,以产生该密钥独有的输出,这是它们的目的,而在这些密码中,它们不以任何其他方式与变换函数交互。

在密码的语境中,人们经常听到“轮数”这个词,指的是可以使用攻击密码的攻击所能破解的轮数;Rijndael 已被证明易受 AES-128 的简化 8 轮版本的已知密钥区分攻击。这些攻击通常针对简化版本,其中较少轮数可以被破解,这既是提供有限计算能力的证明,也是提出可能逆转完全变换的方法。这是因为对于大多数密码来说,增加轮数可以通过使差分或线性密码分析更加困难来提高密码的安全性。有许多著名的密码学家曾表示,Rijndael 使用的轮数应该增加,其简单的代数描述使其在当前的轮数(10、12 和 14)下易受攻击,并且应增加到 22 轮或更多轮以确保其持续的完整性。

HX 密码

本项目的一个核心目标是使用现有且经过验证的加密原语来创建最强大的密码。另一个重要目标是尝试更好地理解各种攻击向量,并创建一种更具抵抗力的东西。

三个基本密码有一个共同点;它们都以类似的方式使用工作密钥;改变或“白化”状态值以创建唯一的输出,除此之外,它们不与用于变换状态的实际计算过程交互。这意味着密钥如何扩展(只要以安全的方式进行)不直接影响数据变换。使用更安全的方式(如哈希函数)创建扩展密钥可以提高密码本身的整体安全性。

HX 密码;RHXSHXTHX 都可以使用 HKDF Expand,这是一种基于 HMAC 的密钥派生函数,一种密钥拉伸函数或伪随机生成器。默认情况下,HKDF 由HMAC驱动,一种密钥化哈希函数。这是创建伪随机输出的最强大的密码学方法之一;即使是像 Serpent 使用的密钥调度那样强大的密钥调度,也不如使用此方法生成工作密钥。除了安全性提高外,用基于 HMAC 的 KDF 替换密钥调度还有两个额外优点;它更能抵抗弱密钥和滑动攻击,并且更长的密码密钥长度使得暴力攻击不可能实现。

计时攻击利用给定参数集执行任务所需时间长度的离散差异。在攻击密钥调度的情况下,它测量诸如分支和表查找等事项的时间距离,以对密钥做出预测;例如,函数中分支之间的差异,或给定特定表成员的值计算输出的计算时间。SHA-2 对计时攻击的脆弱性较低,因为运行所需的时间通常比 Rijndael 密钥调度等更恒定。

另一个优点是密钥长度;HX 密码在 HKDF 扩展模式下的最小密钥长度是哈希函数输出的大小。因此,这些密码的密钥最小为 32 字节,使用 256 位消息摘要,但可扩展到任何大小,以哈希函数的输出大小为倍数。即使在量子计算机最终将对称密码密钥空间减半时,仍然可以保持很大的安全裕度。如果出现威胁密码完整性的新攻击,它现在足够灵活,可以通过扩展密钥长度和增加变换轮数来应对这些威胁。

基准测试

在配备 I7-6700T 处理器的 HP 一体机上进行的性能测试,12GB DDR3 RAM,编译为 Release/Any CPU。
测试使用蒙特卡洛方法变换字节数组。
大小单位为 MB(1,000,000 字节)。时间格式为秒.毫秒,密钥大小以位为单位。速率为 MB/分钟。
迄今为止最高速率是 RHX,密钥为 128 位:每分钟 18.18 GB,并行化的 Salsa 速率为 46GB!

我在这里放入了一个 CEX++ 的基准作为参考。它演示了结合 AES-NI、内在函数、快速精简代码和更高效的语言可以做什么。

密码模式功能时间速率
RHXCTRENC0.4214285
THXCTRENC0.3020000
SHXCTRENC1.075607
SALSACTRENC0.1346153
CHACHACTRENC0.1442857
     

CFB 和 CBC 模式的并行解密函数是相当的,大约慢 10-20%。

我的 i7 CPU 使用 8 个线程,其他具有更多或更少核心的处理器、不同的内存速度/配置等将产生不同的结果。

C++ 版本使用 AES-NI/CTR 在同一台机器上现在输出约 7GB/秒,C++ 的作用真大……

RHX (Rijndael)

密钥调度

RHX 中的密钥调度是该密码与其他版本之间区别的关键;有两种操作模式,标准:使用原始密钥扩展例程,以及安全:使用内部 HKDF Expand 函数来派生工作密钥。使用哪种方法由类的构造函数决定。如果 KdfEngineType 枚举参数设置为消息摘要函数(默认为 None),则密码以 HKDF 扩展模式初始化。

标准扩展例程在使用高达 256 位的密钥时会产生预期输出(没有 512 位密钥的标准测试)。这已通过测试项目中的 NIST AESAVS 已知答案测试得到证明。

安全密钥扩展方法使用基于 HMAC 的伪随机生成器来创建工作密钥。HKDF Expand 是一种密钥派生函数,它使用加密哈希函数作为其扩散引擎。这是生成伪随机密钥材料最强大的方法之一,在熵分散方面远优于 Rijndael,甚至优于 Serpent 的本地密钥调度。

HKDF 使用最多三个输入;一个称为信息字符串(DistributionCode 属性)的 nonce 值,一个 Ikm(密钥),以及一个 Salt 值。HMAC Rfc 2104 建议密钥大小等于摘要输出,在这种情况下,使用 SHA512 时为 64 字节。Salt 的派生是大于密钥大小的任何值,因此 128 字节的密钥将调用 HKDF Extract,其密钥为 64 字节,Salt 为 64 字节。

首先,我们必须查看 HX 密码的构造函数,在这里可以指定轮数、块大小和 HKDF 摘要引擎。

public RHX(int BlockSize = BLOCK16, int Rounds = ROUNDS22, Digests KdfEngineType = Digests.None)
{
    if (BlockSize != BLOCK16 && BlockSize != BLOCK32)
        throw new CryptoSymmetricException("RHX:CTor", "Invalid block size! Supported block sizes are 16 and 32 bytes.", new ArgumentException());

    _kdfEngineType = KdfEngineType;
    // add standard key lengths
    _legalKeySizes[0] = 16;
    _legalKeySizes[1] = 24;
    _legalKeySizes[2] = 32;
    _legalKeySizes[3] = 64;
    _blockSize = BlockSize;

    if (KdfEngineType != Digests.None)
    {
        if (Rounds < MIN_ROUNDS || Rounds > MAX_ROUNDS || Rounds % 2 > 0)
            throw new CryptoSymmetricException("RHX:CTor", "Invalid rounds size! Rounds assignment option only available in HKDF extended mode.", new ArgumentException());

        _legalRounds = new int[] { 10, 12, 14, 22, 24, 26, 28, 30, 32, 34, 36, 38 };
        // set the hmac key size
        _ikmSize =  GetIkmSize(KdfEngineType);

        // hkdf extended key sizes
        for (int i = 4; i < _legalKeySizes.Length; ++i)
            _legalKeySizes[i] = (_legalKeySizes[3] + _ikmSize * (i - 3));

        _dfnRounds = Rounds;
    }
    else
    {
        _legalRounds = new int[] { 10, 12, 14, 22 };
        Array.Resize(ref _legalKeySizes, 4);
    }
}

在上面的代码中,指定了块大小,轮数(在标准模式下自动确定),以及 HKDF 摘要类型枚举器(在调用 Initialize 方法时实例化)。

当用户输入的密码通过 Initialize(bool, KeyParams) 方法添加时,会测试密钥扩展方法 ExpandKey(),该方法将扩展指向相应的方法。

private void ExpandKey(byte[] Key, bool Encryption)
{
    if (_kdfEngineType != Digests.None)
    {
        // hkdf key expansion
        _expKey = SecureExpand(Key);
    }
    else
    {
        // standard rijndael key expansion + k512
        _expKey = StandardExpand(Key);
    }

    // inverse cipher
    if (!Encryption)
    {
        int blkWords = _blockSize / 4;

        // reverse key
        for (int i = 0, k = _expKey.Length - blkWords; i < k; i += blkWords, k -= blkWords)
        {
            for (int j = 0; j < blkWords; j++)
            {
                uint temp = _expKey[i + j];
                _expKey[i + j] = _expKey[k + j];
                _expKey[k + j] = temp;
            }
        }
        // sbox inversion
        for (int i = blkWords; i < _expKey.Length - blkWords; i++)
        {
            _expKey[i] = IT0[SBox[(_expKey[i] >> 24)]] ^
                IT1[SBox[(byte)(_expKey[i] >> 16)]] ^
                IT2[SBox[(byte)(_expKey[i] >> 8)]] ^
                IT3[SBox[(byte)_expKey[i]]];
        }
    }
}

您可以看到安全和标准扩展函数之间的切换,并且如果密码是为解密而初始化的,则需要逆密码的密钥反转循环。

这是新密钥调度的一部分,它将 512 位输入密钥扩展为所需数量的轮密钥。

private uint[] StandardExpand(byte[] Key)
{
    // block in 32 bit words
    int blkWords = _blockSize / 4;
    // key in 32 bit words
    int keyWords = Key.Length / 4;
    // rounds calculation, 512 gets 22 rounds
    _dfnRounds = (blkWords == 8 && keyWords != 16) ? 14 : keyWords + 6;
    // setup expanded key
    uint[] expKey = new uint[blkWords * (_dfnRounds + 1)];


    if (keyWords == 16)
    {
        expKey[0] = IntUtils.BytesToBe32(Key, 0);
        expKey[1] = IntUtils.BytesToBe32(Key, 4);
        expKey[2] = IntUtils.BytesToBe32(Key, 8);
        expKey[3] = IntUtils.BytesToBe32(Key, 12);
        expKey[4] = IntUtils.BytesToBe32(Key, 16);
        expKey[5] = IntUtils.BytesToBe32(Key, 20);
        expKey[6] = IntUtils.BytesToBe32(Key, 24);
        expKey[7] = IntUtils.BytesToBe32(Key, 28);
        expKey[8] = IntUtils.BytesToBe32(Key, 32);
        expKey[9] = IntUtils.BytesToBe32(Key, 36);
        expKey[10] = IntUtils.BytesToBe32(Key, 40);
        expKey[11] = IntUtils.BytesToBe32(Key, 44);
        expKey[12] = IntUtils.BytesToBe32(Key, 48);
        expKey[13] = IntUtils.BytesToBe32(Key, 52);
        expKey[14] = IntUtils.BytesToBe32(Key, 56);
        expKey[15] = IntUtils.BytesToBe32(Key, 60);


        // k512 R: 16,24,32,40,48,56,64,72,80,88, S: 20,28,36,44,52,60,68,76,84
        ExpandRotBlock(expKey, 16, 16, 1);
        ExpandSubBlock(expKey, 20, 16);
        ExpandRotBlock(expKey, 24, 16, 2);
        ExpandSubBlock(expKey, 28, 16);
        ExpandRotBlock(expKey, 32, 16, 3);
        ExpandSubBlock(expKey, 36, 16);
        ExpandRotBlock(expKey, 40, 16, 4);
        ExpandSubBlock(expKey, 44, 16);
        ExpandRotBlock(expKey, 48, 16, 5);
        ExpandSubBlock(expKey, 52, 16);
        ExpandRotBlock(expKey, 56, 16, 6);
        ExpandSubBlock(expKey, 60, 16);
        ExpandRotBlock(expKey, 64, 16, 7);
        ExpandSubBlock(expKey, 68, 16);
        ExpandRotBlock(expKey, 72, 16, 8);
        ExpandSubBlock(expKey, 76, 16);
        ExpandRotBlock(expKey, 80, 16, 9);
        ExpandSubBlock(expKey, 84, 16);
        ExpandRotBlock(expKey, 88, 16, 10);


        if (blkWords == 8)
        {
            ExpandSubBlock(expKey, 92, 16);
            ExpandRotBlock(expKey, 96, 16, 11);
            ExpandSubBlock(expKey, 100, 16);
            ExpandRotBlock(expKey, 104, 16, 12);
            ExpandSubBlock(expKey, 108, 16);
            ExpandRotBlock(expKey, 112, 16, 13);
            ExpandSubBlock(expKey, 116, 16);
            ExpandRotBlock(expKey, 120, 16, 14);
            ExpandSubBlock(expKey, 124, 16);
            ExpandRotBlock(expKey, 128, 16, 15);
            ExpandSubBlock(expKey, 132, 16);
            ExpandRotBlock(expKey, 136, 16, 16);
            ExpandSubBlock(expKey, 140, 16);
            ExpandRotBlock(expKey, 144, 16, 17);
            ExpandSubBlock(expKey, 148, 16);
            ExpandRotBlock(expKey, 152, 16, 18);
            ExpandSubBlock(expKey, 156, 16);
            ExpandRotBlock(expKey, 160, 16, 19);
            ExpandSubBlock(expKey, 164, 16);
            ExpandRotBlock(expKey, 168, 16, 20);
            ExpandSubBlock(expKey, 172, 16);
            ExpandRotBlock(expKey, 176, 16, 21);
            ExpandSubBlock(expKey, 180, 16);
        }
    }
    else if(keyWords == 8)
    {

...

首先,就像其他密钥大小一样,输入密码密钥被复制到扩展密钥数组的开头。ExpandRotBlock() 和 ExpandSubBlock() 方法交替进行,与 256 位密钥一样,每个 Rot 周期都接收一个唯一的 Rcon 常量。这完全镜像了 256 位密钥扩展例程,只是将更长的输入密钥分散到 22 轮所需的更多轮密钥中(相比之下,256 位密钥有 14 轮)。

这是两个扩展例程

private void ExpandRotBlock(uint[] Key, int KeyIndex, int KeyOffset, int RconIndex)
{
    int sub = KeyIndex - KeyOffset;


    Key[KeyIndex] = Key[sub] ^ SubByte((Key[KeyIndex - 1] << 8) | ((Key[KeyIndex - 1] >> 24) & 0xFF)) ^ Rcon[RconIndex];
    // Note: you can insert noise before each subsequent mix to further equalize timing; ex. add between each mix line:
    // (uint) tmp = SubByte((Key[Index - 1] << 8) | ((Key[Index - 1] >> 24) & 0xFF))];
    Key[++KeyIndex] = Key[++sub] ^ Key[KeyIndex - 1];
    Key[++KeyIndex] = Key[++sub] ^ Key[KeyIndex - 1];
    Key[++KeyIndex] = Key[++sub] ^ Key[KeyIndex - 1];
}

private void ExpandSubBlock(uint[] Key, int KeyIndex, int KeyOffset)
{
    int sub = KeyIndex - KeyOffset;


    Key[KeyIndex] = SubByte(Key[KeyIndex - 1]) ^ Key[sub];
    // can equalize timing here as well..
    Key[++KeyIndex] = Key[++sub] ^ Key[KeyIndex - 1];
    Key[++KeyIndex] = Key[++sub] ^ Key[KeyIndex - 1];
    Key[++KeyIndex] = Key[++sub] ^ Key[KeyIndex - 1];
}

通过展开密钥调度并使用最少的变量创建,这些例程都更快,并且更难进行计时。AES-NI (AHX) 密码和 CEX++ 库中的 RHX 使用类似的功能。

安全的扩展例程由 HKDF 提供支持,使用定义的摘要,它可以生成不同大小的工作密钥数组,以适应灵活的轮数分配。

private uint[] SecureExpand(byte[] Key)
{
    // block in 32 bit words
    int blkWords = _blockSize / 4;
    // expanded key size
    int expSize = blkWords * (_dfnRounds + 1);
    // kdf return array
    int keyBytes = expSize * 4;
    byte[] rawKey = new byte[keyBytes];
    int saltSize = Key.Length - _ikmSize;
    // hkdf input
    byte[] hkdfKey = new byte[_ikmSize];
    byte[] hkdfSalt = new byte[0];

    // copy hkdf key and salt from user key
    Buffer.BlockCopy(Key, 0, hkdfKey, 0, _ikmSize);
    if (saltSize > 0)
    {
        hkdfSalt = new byte[saltSize];
        Buffer.BlockCopy(Key, _ikmSize, hkdfSalt, 0, saltSize);
    }
    // HKDF generator expands array
    using (HKDF gen = new HKDF(_kdfEngine, false))
    {
        gen.Initialize(hkdfSalt, hkdfKey, _hkdfInfo);
        gen.Generate(rawKey);
    }

    // initialize working key
    uint[] expKey = new uint[expSize];
    // copy bytes to working key
    Buffer.BlockCopy(rawKey, 0, expKey, 0, keyBytes);

    return expKey;
}

如您所见,kdf 生成运行设定的轮数所需的字节数,并将这些字节复制到工作密钥整数数组中。

最后,这是其中一个轮函数,用于加密 16 字节。此方法很灵活,因为它可以在主循环中运行任意偶数轮。此方法通过面向字节的方法优化了速度,该方法使用预乘查找表。

private void Encrypt16(byte[] Input, int InOffset, byte[] Output, int OutOffset)
{
    int LRD = _expKey.Length - 5;
    int keyCtr = 0;

    // round 0
    uint X0 = IntUtils.BytesToBe32(Input, InOffset) ^ _expKey[keyCtr];
    uint X1 = IntUtils.BytesToBe32(Input, InOffset + 4) ^ _expKey[++keyCtr];
    uint X2 = IntUtils.BytesToBe32(Input, InOffset + 8) ^ _expKey[++keyCtr];
    uint X3 = IntUtils.BytesToBe32(Input, InOffset + 12) ^ _expKey[++keyCtr];

    // round 1
    uint Y0 = T0[X0 >> 24] ^ T1[(byte)(X1 >> 16)] ^ T2[(byte)(X2 >> 8)] ^ T3[(byte)X3] ^ _expKey[++keyCtr];
    uint Y1 = T0[X1 >> 24] ^ T1[(byte)(X2 >> 16)] ^ T2[(byte)(X3 >> 8)] ^ T3[(byte)X0] ^ _expKey[++keyCtr];
    uint Y2 = T0[X2 >> 24] ^ T1[(byte)(X3 >> 16)] ^ T2[(byte)(X0 >> 8)] ^ T3[(byte)X1] ^ _expKey[++keyCtr];
    uint Y3 = T0[X3 >> 24] ^ T1[(byte)(X0 >> 16)] ^ T2[(byte)(X1 >> 8)] ^ T3[(byte)X2] ^ _expKey[++keyCtr];

    while (keyCtr != LRD)
    {
        X0 = T0[Y0 >> 24] ^ T1[(byte)(Y1 >> 16)] ^ T2[(byte)(Y2 >> 8)] ^ T3[(byte)Y3] ^ _expKey[++keyCtr];
        X1 = T0[Y1 >> 24] ^ T1[(byte)(Y2 >> 16)] ^ T2[(byte)(Y3 >> 8)] ^ T3[(byte)Y0] ^ _expKey[++keyCtr];
        X2 = T0[Y2 >> 24] ^ T1[(byte)(Y3 >> 16)] ^ T2[(byte)(Y0 >> 8)] ^ T3[(byte)Y1] ^ _expKey[++keyCtr];
        X3 = T0[Y3 >> 24] ^ T1[(byte)(Y0 >> 16)] ^ T2[(byte)(Y1 >> 8)] ^ T3[(byte)Y2] ^ _expKey[++keyCtr];
        Y0 = T0[X0 >> 24] ^ T1[(byte)(X1 >> 16)] ^ T2[(byte)(X2 >> 8)] ^ T3[(byte)X3] ^ _expKey[++keyCtr];
        Y1 = T0[X1 >> 24] ^ T1[(byte)(X2 >> 16)] ^ T2[(byte)(X3 >> 8)] ^ T3[(byte)X0] ^ _expKey[++keyCtr];
        Y2 = T0[X2 >> 24] ^ T1[(byte)(X3 >> 16)] ^ T2[(byte)(X0 >> 8)] ^ T3[(byte)X1] ^ _expKey[++keyCtr];
        Y3 = T0[X3 >> 24] ^ T1[(byte)(X0 >> 16)] ^ T2[(byte)(X1 >> 8)] ^ T3[(byte)X2] ^ _expKey[++keyCtr];
    }

    // final round
    Output[OutOffset] = (byte)(SBox[Y0 >> 24] ^ (byte)(_expKey[++keyCtr] >> 24));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y1 >> 16)] ^ (byte)(_expKey[keyCtr] >> 16));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y2 >> 8)] ^ (byte)(_expKey[keyCtr] >> 8));
    Output[++OutOffset] = (byte)(SBox[(byte)Y3] ^ (byte)_expKey[keyCtr]);

    Output[++OutOffset] = (byte)(SBox[Y1 >> 24] ^ (byte)(_expKey[++keyCtr] >> 24));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y2 >> 16)] ^ (byte)(_expKey[keyCtr] >> 16));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y3 >> 8)] ^ (byte)(_expKey[keyCtr] >> 8));
    Output[++OutOffset] = (byte)(SBox[(byte)Y0] ^ (byte)_expKey[keyCtr]);

    Output[++OutOffset] = (byte)(SBox[Y2 >> 24] ^ (byte)(_expKey[++keyCtr] >> 24));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y3 >> 16)] ^ (byte)(_expKey[keyCtr] >> 16));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y0 >> 8)] ^ (byte)(_expKey[keyCtr] >> 8));
    Output[++OutOffset] = (byte)(SBox[(byte)Y1] ^ (byte)_expKey[keyCtr]);

    Output[++OutOffset] = (byte)(SBox[Y3 >> 24] ^ (byte)(_expKey[++keyCtr] >> 24));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y0 >> 16)] ^ (byte)(_expKey[keyCtr] >> 16));
    Output[++OutOffset] = (byte)(SBox[(byte)(Y1 >> 8)] ^ (byte)(_expKey[keyCtr] >> 8));
    Output[++OutOffset] = (byte)(SBox[(byte)Y2] ^ (byte)_expKey[keyCtr]);
}

SHX(Serpent)

SHX,与 RHX 一样(可选),使用 HKDF 生成器将用户提供的密钥扩展为工作密钥整数数组。它还可以接受用户定义的轮数,范围从 32(正常轮数)到 64(正常轮数的两倍),以 8 轮为一组。32 或 48 轮的轮数已经足够,因为到目前为止的理论攻击只能破解多达 12 轮,并且需要大量的内存和处理能力。

SHX 中的变换与 Serpent 实现 SPX 相同,它通过首先将字节输入数组移动到 4 个整数中,然后在 while 循环中处理轮次来进行处理。每一轮由状态字(Rn)与密钥的异或、这些字的 S-Box 变换以及线性变换组成。8 个 S-Box 中的每一个都在循环周期内依次使用。最后一轮将最后 4 个密钥与状态进行异或,并将它们移回输出字节数组。

Serpent 加密变换

private void Encrypt16(byte[] Input, int InOffset, byte[] Output, int OutOffset)
{
    int LRD = _expKey.Length - 5;
    int keyCtr = -1;

    // input round
    uint R0 = IntUtils.BytesToBe32(Input, InOffset + 12);
    uint R1 = IntUtils.BytesToBe32(Input, InOffset + 8);
    uint R2 = IntUtils.BytesToBe32(Input, InOffset + 4);
    uint R3 = IntUtils.BytesToBe32(Input, InOffset);

    // process 8 round blocks
    do
    {
        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb0(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3);

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb1(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3);

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb2(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3); ;

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb3(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3);

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb4(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3);

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb5(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3);

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb6(ref R0, ref R1, ref R2, ref R3);
        LinearTransform(ref R0, ref R1, ref R2, ref R3);

        R0 ^= _expKey[++keyCtr];
        R1 ^= _expKey[++keyCtr];
        R2 ^= _expKey[++keyCtr];
        R3 ^= _expKey[++keyCtr];
        Sb7(ref R0, ref R1, ref R2, ref R3);

        // skip on last block
        if (keyCtr != LRD)
            LinearTransform(ref R0, ref R1, ref R2, ref R3);
    }
    while (keyCtr != LRD);

    // last round
    IntUtils.Be32ToBytes(_expKey[++keyCtr] ^ R0, Output, OutOffset + 12);
    IntUtils.Be32ToBytes(_expKey[++keyCtr] ^ R1, Output, OutOffset + 8);
    IntUtils.Be32ToBytes(_expKey[++keyCtr] ^ R2, Output, OutOffset + 4);
    IntUtils.Be32ToBytes(_expKey[++keyCtr] ^ R3, Output, OutOffset);
}

THX(Twofish)

这是一个有趣的密码,具有键控 S-Box 和复杂的代数描述等方法,与 Serpent 或 Rijndael 有很大不同。因此,它需要更多地考虑如何实现扩展密钥大小。我所做的与其他密码类似;即从现有函数中汲取模式,并以最佳方式扩展这些模式,同时利用更大的密码密钥并保持原始设计的连贯性。

Twofish 加密变换

private void Encrypt16(byte[] Input, int InOffset, byte[] Output, int OutOffset)
{
    int LRD = _expKey.Length - 1;
    int keyCtr = 0;
    UInt32 X0 = IntUtils.BytesToLe32(Input, InOffset) ^ _expKey[keyCtr];
    UInt32 X1 = IntUtils.BytesToLe32(Input, InOffset + 4) ^ _expKey[++keyCtr];
    UInt32 X2 = IntUtils.BytesToLe32(Input, InOffset + 8) ^ _expKey[++keyCtr];
    UInt32 X3 = IntUtils.BytesToLe32(Input, InOffset + 12) ^ _expKey[++keyCtr];
    UInt32 T0, T1;

    keyCtr = 7;
    do
    {
        T0 = Fe0(X0);
        T1 = Fe3(X1);
        X2 ^= T0 + T1 + _expKey[++keyCtr];
        X2 = (X2 >> 1) | X2 << 31;
        X3 = (X3 << 1 | (X3 >> 31)) ^ (T0 + 2 * T1 + _expKey[++keyCtr]);

        T0 = Fe0(X2);
        T1 = Fe3(X3);
        X0 ^= T0 + T1 + _expKey[++keyCtr];
        X0 = (X0 >> 1) | X0 << 31;
        X1 = (X1 << 1 | (X1 >> 31)) ^ (T0 + 2 * T1 + _expKey[++keyCtr]);

    } while (keyCtr != LRD);

    keyCtr = 4;
    IntUtils.Le32ToBytes(X2 ^ _expKey[keyCtr], Output, OutOffset);
    IntUtils.Le32ToBytes(X3 ^ _expKey[++keyCtr], Output, OutOffset + 4);
    IntUtils.Le32ToBytes(X0 ^ _expKey[++keyCtr], Output, OutOffset + 8);
    IntUtils.Le32ToBytes(X1 ^ _expKey[++keyCtr], Output, OutOffset + 12);
}

建议

第一个建议是使用 CEX++,而不是这个库。C++ 版本库速度更快(在许多情况下快 20 倍或更多),编写更好,更安全,选项更多。
该库的版本从这里结束开始;我现在将此库视为学习过程的一部分,而 C++ 版本则是在实现灵活且高性能的后量子安全密码库的严肃努力。如果时间允许,此库最终将基于 CEX++ 重写,但目前,我所有的努力都集中在该项目上。
 
在扩展密钥大小(HKDF 安全模式)下使用 RHX,并增加轮数。我理解一些人在这种情况下对新事物感到犹豫,但在我看来,RHX 的威力比标准 AES 大很多数量级。将密钥调度从弱 PRF 更改为密码学上强大的 DRBG 是自然的进展,应该能确保密码的完整性长久不衰。其他 HX 密码也是如此,更长的用户提供的密钥与增加的轮数分配相结合,可以创建更强的密码。KDF 中使用的摘要是可分配的,因此可以使用一种哈希算法,该算法在多大程度上免疫时序攻击,并且还可以消除弱密钥和相关密钥攻击的漏洞。
 
如果您要加密文件,请使用 CipherStream 类。该示例演示了如何使用 KeyHeaderStruct 通过该类构造密码实例,我相信该框架是库的最佳用途。有些人可能会被诱惑从库中提取加密类并在他们的项目中将其用作独立实例,这会创建一个更弱的系统。MessageHeader 仅向攻击者提供两件信息:一个随机的密钥 ID 和一个加密的文件扩展名。攻击者不知道使用了什么加密算法、有多少轮、KDF 引擎、填充、模式、mac 大小……等等。本质上,攻击者对此系统一无所知,并且鉴于加密可能已使用如此多的未知变量进行,这使得对消息文件进行密码分析变得更加困难。
 
对于扩展模式下的 HX 密码,请使用与 HKDF 摘要引擎的哈希返回大小对齐的密钥大小。推荐大小为(摘要函数的输入块大小)。这确保了有足够数量的熵用于为 HKDF 播种。
 
在这种情况下,没有必要使用 512 位摘要与 HKDF,256 位摘要(如 Skein 或 Blake)已经足够。
 
HX 密码在扩展模式下的推荐最小密钥大小
 
摘要最低推荐
Blake-25632 字节64 字节
Blake-51264 字节128 字节
Keccak 51264 字节128 字节
SHA-25632 字节64 字节
SHA-51264 字节128 字节
Skein-25632 字节64 字节
Skein-51264 字节128 字节
 
HX 密码的推荐轮数
 
密码
RHX22
SHX40
THX20

我还建议在实现中使用 KeyGenerator 类来生成密钥和向量。如果用户在创建密钥时可以选择要使用的种子生成器和摘要引擎,这会增加任何涉及密钥预测的密码分析的未知数,同时通过两阶段 PRNG/摘要引擎创建密码学上强大的密钥材料。

测试

项目中包含大量不同的测试,用于验证 RDX、SPX、TFX 实现的标准配置(128、192 和 256 位密钥)的有效性。还有 I/O、模式相等性、填充、HMAC、HKDF、SHA KAT 测试以及性能比较;

  • AESAVS 已知答案测试;给定密钥、iv 或明文的设定参数,变换的输出是已知的。使用用于认证 AES 实现的 AESAVS 集中的完整明文和密钥向量,总共 960 个向量测试。
  • AES 蒙特卡洛测试;在 AES 规范 Fips 197 中定义,以及由 Brian Gladman 创建的向量。
  • Blake 来自 Blake SHA-3 提交包的向量 KAT;测试 256/512 摘要。
  • ChaCha 和 Salsa20;来自 bouncy castle jdk 1.51 实现的 ChaCha 和 Salsa20 的 KAT 测试 ChaCha 和 Salsa20。
  • CMAC; 来自 RFC 4493 的测试向量。
  • HKDF;使用 HKDF RFC 5869 中的已知答案测试集来测试 HKDF 实现。
  • HMAC;使用来自 RFC 4231 的已知答案测试来测试 HMAC 实现。
  • Keccak; 来自 Bouncy Castle jdk 1.51 SHA-3 测试的向量,包括 NIST 向量。
  • 密码模式;测试了来自 Nist SP800-38A 的 ECB、CBC、CFB、OFB 和 CTR 模式的完整向量集。
  • PSC 相等性;将并行模式的输出与标准模式的输出进行比较以进行相等性测试。
  • I/O;测试密码初始化和处理方法。
  • Rijndael Vector;使用源自 Bouncy Castle RijndaelTest.cs 和 Nessie 未经验证向量的测试向量进行 256 位块大小的已知答案测试。
  • SecureRandom;测试 SecureRandom 访问方法和返回范围。
  • Serpent Vector;Nessie 验证向量的完整集 Nessie verified vectors,包括 100 轮和 1000 轮蒙特卡洛测试,共 2865 个向量。
  • SHA-2;向量:在 NIST SHA 测试向量文档补充中使用的 KAT 测试。
  • Skein;将 Skein 的 256、512 和 1024 位版本与已知测试向量进行测试。
  • Twofish;官方 Twofish Vector KAT 测试,包括六万轮蒙特卡洛测试。
  • VMPCMAC;Bouncy Castle 使用的向量测试。

指导出版物

许可证

NTRU 实现和 DTM-KEX 的许可证是 GPLv3,其余库(包括密码实现)是 MIT。

结论

1946 年春,ENIAC 计算机项目完成。世界各地的报纸称其为“超级大脑”和“爱因斯坦机器”。它是当时最强大的计算机;拥有超过 17,000 个真空管、7,200 个晶体二极管、1,500 个继电器、70,000 个电阻器、10,000 个电容器和大约 500 万个手工焊接点。它重达 30 吨,占地 1800 平方英尺,消耗 150 千瓦的功率。它每秒能够进行惊人的 385 次乘法运算。

想象一下,您是设计师之一,和同事们一起喝了几杯酒,您提出,在短短 25 年内,任何人都可以走进一家 Sears 商店,花 10 美元买到一个计算能力和速度是其数千倍的便携式计算器。我认为您会受到极大的质疑;“不可能”、“不现实”、“晶体管和电路路径不可能做得那么小”……您将遭受一系列科学理论的抨击,这些理论认为这样的事情永远不会发生……至少,在一百年左右的时间里不会。

在《连线》杂志近期的一篇文章 wired 中,量子计算机的顶尖专家之一 John Martinis 表示,新的 Google Quantum AI 实验室的目标是每年将量子比特的数量翻倍。最近,一项关于如何测量量子态的新突破可能比当前方法 快 350 倍。随着我们对量子过程理解的不断深入,这类突破正变得越来越频繁。因此,以如此不断加速的速度,要多久才能出现能够破解当前加密技术的巨大处理能力的计算机?无法确定,但重大突破可能会比预期的时间更早地实现。所以,当你听到 AES 不太可能被破解时,请记住 ENIAC,并考虑我们在过去 100 年里技术取得了多大的进步。

存在反对强大加密的论点,该论点通常与国家机构正在为保护我们而开发大规模监控系统、像 犹他州的设施将仅用于针对罪犯和恐怖分子,并且强大的加密阻碍了他们的努力的观点相关联。我认为大多数人都明白,情况并非如此,这项技术最终可能被用于某种“人民控制”系统,而这些机构的意图充其量是模糊的。一旦大规模监控系统落入暴君之手,它将成为镇压的毁灭性工具。
此外,这些机构声称要针对的人,如罪犯和恐怖分子;其中最糟糕的人根本不使用电子通信,或者他们已经开发了有效的规避策略,并且能够使用一次性密码本和隐写术等不可破解且不可探测的方法。

我认为,我们在过去 100 年里取得如此快速的进步,是因为我们生活在一个前所未有的个人权利和自由的时代,可以自由表达我们的想法并与之交流,而不必担心干涉或报复。这是我们社会向前发展的主要驱动力,如果我们要保持这种势头,并希望为子孙后代创造一个更美好的世界,就必须维护这些自由。加密技术在未来发挥着关键作用,我相信我们应该努力开发能够保护信息在其完整生命周期内安全的技术,所有形式的电子通信都应作为标准纳入强大的加密技术,并且这些技术应不断与技术变革的预期速度进行比较和演进。

 

所以……希望您喜欢这篇文章,请留下评论,或者如果它是技术性的,您可以通过此方式或 我的网站给我发邮件。

祝好,
John
© . All rights reserved.