你好我是王昊天,今天这一讲我们来一起学习密码算法问题。

温州话素以难懂著名,据传连温州附近的其他城市也很难理解。这种客观条件,为使用温州话传递秘密信息创造了土壤,因此有一种说法是,抗日战争时期中国军队使用温州话进行秘密通信。

这听起来确实还挺有可行性的,而且二战时期美军就曾使用纳瓦霍语做出类似的操作。

使用一种复杂形式的通用性语言作为“加密”方案,虽然在某种现实应用中可以奏效,但在算法选择上其实并不明智。

要知道,语言是可以翻译的。因此,如果将信息传递的安全性完全依赖于语言复杂性特质,一旦这种语言具有较大受众,对方就很可能具备该类语言的解析能力,从而使该语言失去保密效果。

在密码学中,这种使用难懂语言的加密方案可以归类到的古典密码算法,而现代社会普遍采用了现代密码学,加密信息的安全性已经不再依赖密码算法的保密性。

这一讲,我们就来一起研究密码算法的安全性。

数学层面的密码安全风险

古典密码学

古典密码学是密码学中的一个类型,主要使用替换式密码移项式密码。尽管古典密码学由于安全性不足等问题现在已经逐渐退出实际应用了,但是我们从它开始了解密码学的发展历程,可以帮助你理解更深层的密码学原理。那么接下来,我们就来看几种经典的古典密码算法。

凯撒密码是一种广为人知的加密技术,是一种替换式密码。它的加密逻辑是非常简单的,明文中的字母按照固定偏移向后取值,结果即为密文,反之即是解密过程。

明文字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZ
密文字母表:DEFGHIJKLMNOPQRSTUVWXYZABC

凯撒加密也可以使用更直观的数学公式来表示:

Res_Enc = ( plain_text + n ) mod 26
Res_Dec = ( cipher_text - n ) mod 26
其中n代表偏移量

可以看到,如此简单的加解密逻辑,在目前的技术发展下安全性是非常低的,站在当下,凯撒密码的影响如何我们已无从知晓,但是从凯撒密码的知名度和影响力来看,它确实是在当时被广泛使用的。根据现有的记载,直到公元9世纪,人们都没有任何技术能够破解这种最基本、最简单的替换密码,要知道凯撒可是生活在公元前1世纪。

在了解凯撒密码的原理之后,如果让你来强化加密算法,你会选择什么方案呢?

也许聪明的你已经想到了,最直观的方案就是,让字母的替代逻辑更加复杂。凯撒密码是一种经典的单字母替代式密码,那么它的进阶形态就变成了多字母替代式密码,在历史上它还有一个经典的名称——维吉尼亚密码。

维吉尼亚密码的运算逻辑会稍显复杂,首先会生成一个二维矩阵Matrix,然后再选择一个关键字X:

Matrix:
                A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
                B C D E F G H I J K L M N O P Q R S T U V W X Y Z A
                C D E F G H I J K L M N O P Q R S T U V W X Y Z A B
                D E F G H I J K L M N O P Q R S T U V W X Y Z A B C
                E F G H I J K L M N O P Q R S T U V W X Y Z A B C D
                F G H I J K L M N O P Q R S T U V W X Y Z A B C D E
                G H I J K L M N O P Q R S T U V W X Y Z A B C D E F
                H I J K L M N O P Q R S T U V W X Y Z A B C D E F G
                I J K L M N O P Q R S T U V W X Y Z A B C D E F G H
                J K L M N O P Q R S T U V W X Y Z A B C D E F G H I
                K L M N O P Q R S T U V W X Y Z A B C D E F G H I J
                L M N O P Q R S T U V W X Y Z A B C D E F G H I J K
                M N O P Q R S T U V W X Y Z A B C D E F G H I J K L
                N O P Q R S T U V W X Y Z A B C D E F G H I J K L M
                O P Q R S T U V W X Y Z A B C D E F G H I J K L M N
                P Q R S T U V W X Y Z A B C D E F G H I J K L M N O
                Q R S T U V W X Y Z A B C D E F G H I J K L M N O P
                R S T U V W X Y Z A B C D E F G H I J K L M N O P Q
                S T U V W X Y Z A B C D E F G H I J K L M N O P Q R
                T U V W X Y Z A B C D E F G H I J K L M N O P Q R S
                U V W X Y Z A B C D E F G H I J K L M N O P Q R S T
                V W X Y Z A B C D E F G H I J K L M N O P Q R S T U
                W X Y Z A B C D E F G H I J K L M N O P Q R S T U V
                X Y Z A B C D E F G H I J K L M N O P Q R S T U V W
                Y Z A B C D E F G H I J K L M N O P Q R S T U V W X
                Z A B C D E F G H I J K L M N O P Q R S T U V W X Y

X:
                WORD

接下来根据明文长度,延展X使其和明文一样长,取得Y:

X: WORD
plain_text: I LOVE CRYPTOGRAPHY
Y: W ORDW ORDWORDWORDW

根据每一位的明文以及Y的取值,分别匹配到Matrix的坐标,即可得出密文,以第一位加密为例:

Matrix(W,I) = E

逐位运算即可取得加密结果:

cipher_text: E ZFYA QIBLHFJNOGKU

学习了替换式密码之后,我们再来看一下移位式密码。移位式密码,字母本身是不变的,但是传递过程中的顺序会按照特定的定义进行改变。举个最简单的例子:

plain_text: Hello World!
cipher_text: olleH !dlroW

可以看到,移位式密码的逻辑是比较简单的,更复杂的移位式密码也是在变换上更加复杂,但是底层逻辑是不变的。

现代密码学

现代密码学主要可以分为两个领域,对称密钥密码学非对称密钥密码学,这两者之间最核心区别就是,加密和解密的密钥是否相同。

对于对称密钥密码学,还可以进一步分为分组密码流密码两个算法种类。其中,分组密码的输入使用明文的一个区块和密钥,然后输出相同大小的密文区块;流密码相对于分组密码则更为灵活,输入中的明文可以任意长,经过与密钥轮的数学操作后,输出与明文等长的加密流。

非对称密钥密码学还有一个名字,公钥密码学。其特征就是具备公钥和私钥两个不同密钥,并且均可以参与加密与解密过程。使用公钥加密、私钥解密是典型的隐秘信息保护流程;而使用私钥加密、公钥解密则是典型的签名流程。可以说,除加密外,公钥密码学最大的贡献就是实现了数字签名,互联网上的PKI体系以及SSL/TLS等网络安全机制均以此为基础构建。

关于底层原理,公钥密码算法的难度大多体现在计算复杂度上,比如RSA源于大整数因数分解问题、DSA源于离散对数问题、椭圆曲线密码学则源于椭圆曲线相关数学难题。由于这些底层问题多涉及模数乘法或指数运算,因此计算复杂度相较于对称密钥算法会更高。

为了在实际应用中达到更高的效率,普遍采用的方案是外部使用公钥密码算法,内部使用对称密钥算法,这样既可以获得公钥密码算法的优秀特性,可以获得对称密码算法的高执行效率,业内一种实践方案是信封加密。

目前,现代密码学在一些领域已经有非常前沿的实际应用场景,如交互证明、零知识、区块链与安全多方计算等。

密码算法安全性

经典密码通常很容易破解,普遍通过唯密文攻击法,在仅知密文的情况下就可以完成攻击。以凯撒密码为例,有限的密钥个数可以通过暴力破解完成攻击;替代式密码虽然有着更大的密钥数,但是会被频率分析破解;更进一步地,维吉尼亚密码使用多个替换防止简单的频率分析,但是依然可以使用更为先进的卡西斯基试验进行破解。

和经典密码学相比,现代密码学的安全性已经不依赖于加密算法的保密性,而是基于密钥的安全性,也就是说即使在密码算法完全公开的情况下,只要攻击者无法获取密钥就无法破解密文。

关于密文的破解有多种分类,其中最为普遍的划分方法是,按照攻击者获取的信息多少进行划分。在唯密文攻击中,攻击者的已知信息只有密文;在已知明文攻击中,攻击者的已知信息包括多个明文、密文对;在选择明文攻击中,攻击者可以自选任意明文,并获得相应的密文;在选择密文攻击中,攻击者可以选择任意密文,并获得相应明文。

另外一种破解分类,是按照信息来源进行分类的,像我们提到的4种攻击方式以及密码算法层的分析都被归类为主信道攻击;与之相对的是侧信道攻击,这种攻击方式重点关注加密设备在执行过程暴露的信息,比如通过分析加解密时间、错误码等来进行破解。

工程实践中的密码安全风险

除了数学理论层面的安全性风险之外,在工程实践中我们也会遇到许多密码学相关的安全风险,接下来就带你了解有哪些典型的风险场景。

硬编码密钥

在一些应用系统中,开发者可能会为了方便将加密密钥硬编码在源码中,在这种情况下,一旦应用系统被入侵,攻击者将可以轻松获得密钥,从而为后续入侵、提权、持久化埋下伏笔。

int verifyAdmin( char *password)
{
    if( strcmp(password, "68af404b513073584c4b6f22b6c63e6b") )
    {
        printf("Incorrect Password!\n");
        return(0);
    }
    return(1);
}

随机值重用

很多密码算法在应用过程中,会涉及到随机值的使用。在一些开发场景中,开发者将随机值固定为某一数值,使得随机值发生重用,这样可能会导致身份伪装等中间人攻击行为的发生。

void encryptAndSendPassword( char *password)
{
    char *tmp = "bad";
    ...
    char *data = (unsigned char*)malloc(20);
    int para_size = strlen(tmp) + strlen(password);
    char *paragraph = (char*)malloc(para_size);
    SHA1((const unsigned char*)paragraph, para_size, (unsigned char*)data);
    ...
}

不安全的加密算法

在开发过程中使用不安全的加密算法,可能会导致敏感信息的泄露,同时这会给攻击者更多攻击的机会,因为如果一个加密算法存在安全缺陷,那么对它的攻击方式很可能已经广为人知了。

也许你会好奇,是谁设计了不安全的加密算法呢?加密算法从诞生到应用,一定是经过了广泛实践检验的。但是由于近些年科技高速发展,无论是从理论算法层面发现了加密算法的缺陷,还是从算力增长的角度发现了某种现实性攻击,都使得加密算法的更新速度大幅提高,那些曾经被验证是安全的加密算法现如今也就变得不再安全了。

如下代码示例使用了DES加密算法,考虑到目前DES已经被认为是不安全的,因此这段代码的安全性存在缺陷,在实际应用中目前普遍采用AES作为替代方案。

function encryptPassword( $password )
{
    $iv_size = mcrypt_get_iv_size(MCRYPT_DS, MCRYPT_MODE_ECB);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    $key = "This is a password encryption key";
    $encrypted_password = mcrypt_encrypt(MCRYPT_DES, $key, $password, MCRYPT_MODE_ECB, $iv);
    ...
}

可预测的初始化向量(Initialization Vector, IV)
许多加密算法会使用初始化向量来强化安全性,以DES加密算法为例,其加密模式分为多种,其中CBC模式就与初始化向量相关。在设置初始化向量的过程中,如果初始化向量可以被预测,那么算法的安全性就会降低。

这里我们仍然以DES算法的CBC模式为例,来分析初始化向量对于加密算法安全性的影响。

在了解CBC模式前,你需要先了解ECB模式,这是最简单的块密码加密模式,全称是电子密码本(Eclectronic codebook)模式,ECB模式在加密前根据块的大小将明文分为若干块,之后每块使用相同的密钥单独加密,解密同理。

ECB模式的优势很明显,首先加密逻辑非常简单,其次由于上下文无关,所以有利于并行计算,最后仍然得益于上下文无关,误差不会被传递;它的劣势也是很清晰的,一方面是无法隐藏明文的模式,另一方面攻击者可以直接对明文进行主动攻击。

为了增强ECB模式的安全性,CBC模式被引入进来。CBC全称是密码分组链接(CBC,Cipher-block chaining)模式。在CBC模式下,每个明文块需要先与前一个密文块进行异或(xor),然后再进行加密,因此每个密文块都依赖于它前面的所有明文块。那么初始化向量又是在何处被引入的呢?为了保证每条消息的唯一性,在第一个明文块会直接与初始化向量进行异或。用数学语言来表述如下:

cipher_text_0 = IV
cipher_text_i = E_k{plain_text_i XOR cipher_text_{i-1}}

如果CBC模式下的初始化向量发生重复使用、全0设置等情况,就会使同样的明文产生同样的密文结果;即使初始化向量未发生重用,对于攻击者来说密文仍然是可预测的,这依然会使加密算法在面对选择明文攻击时的安全性大大降低。

如下代码使用CBC模式进行加密,但在编码过程中将初始化向量设置为全0,这就导致密文更加容易预测,并且可能会面临字典攻击等安全威胁:

public class Cipher {
    public static void main() {
        byte[] plain_text = "Hello World!".getBytes();
        byte[] iv = {
            0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
        };
        KeyGenerator kg = KeyGenerator.getInstace("DES");
        kg.init(56);
        SecretKey key = kg.generateKey();
        Cipher cipher = Cipher.getInstace("DES/CBC/PKCS5Padding");
        IvParameterSpec ips = new IvParameterSpec(iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, ips);
        // ...
    }
}

不安全的Padding

许多加密算法都支持padding机制,一方面padding是为了补全明文,使其满足加密算法的格式要求;另一方面在padding机制产生作用后,明文将更难以预测,攻击者的攻击复杂度也会相应提高。以下是一种典型的错误示例:

public Cipher getRSACipher() {
    Cipher rsa = null;
    try {
        rsa = javax.crypto.Cipher.getInstance("RSA/NONE/NoPadding");
    }
    catch (Exception e) {
        // ...
    }
    return rsa;
}

案例实战

通过一系列典型的安全风险场景,相信你对密码安全风险已经有了更加深刻的理解。接下来我们从两个更为复杂的实战案例出发,看看它在实际应用中又能给我们什么启发。

数字签名伪造

我们知道数字签名是互联网信任体系的根基,那么数字签名是否一定是可信的呢?为了寻找答案,我们要站在攻击者的视角来审视整个体系。

设想一种场景,在这种场景中攻击者试图执行一次签名诈骗攻击。首先攻击者准备了一份正常的合同m,以及一份伪造的合同m_fake,在不改变合同原本意义的情况下,通过插入逗号、空行、空格、同义词替换等行为,攻击者可以生成一系列以m和m_fake为原型的合同变体。攻击者了解在合同签署过程中使用的签名函数是f(),并通过运算在所有合同变体中找到了f(m) = f(m_fake),于是攻击者可以将m带给合作方签名。在签名完成后,攻击者可以将签名取下并附到m_fake上,以此来证明合作方签署了m_fake合同。

再设想一种场景,我们都知道在下载软件安装包时,最好的情况是去官网下载,一方面是来源更可信,另一方面是官网同时也会披露对应安装包的MD5值供用户验证。那么如果攻击者通过碰撞的方式,制造出了一个恶意应用程序,同时该恶意程序的MD5值与官方安装包一致,就可能会导致用户错误地安装恶意程序,从而被攻击者控制。

通过这两种场景我们会发现,数字签名也未必可信。目前技术的快速发展,使得数字签名的伪造成为可能,虽然这种攻击方式的门槛仍然较高,但随着算力和算法的发展,该攻击方式的实施成本会进一步降低,这也是为什么我们需要不断探索强度更高的密码算法。

HASH碰撞与生日攻击

HASH碰撞在数学上有一个原型叫“生日攻击”,问题是“一个班级需要有多少人,才能保证每个同学的生日都不一样?”。这里我先直接说答案,你肯定会十分吃惊,如果要求出现相同生日的同学概率不超过5%,那么这个班只能有7个人;如果概率是50%,那么这个班只需要23个人。

如果按照HASH碰撞的角度来理解,哈希值的空间范围是365,只需要计算23个哈希就有50%的概率出现碰撞。接下来我们就以50%为标准,来判断通用意义上HASH碰撞的可行性。

数学推导

这里我们仍然以生日攻击为例,来推导数学公式。如果所有人的生日都不相同,那么意味着每个同学需要在选择自己生日时,排除已经被选择掉的天数,在剩余的日期中做出选择。

p(n) = 1 · (1-1/365) · (1-2/365) · ... · (1-(n-1)/365)

参考泰勒公式:

e^x = 1 + x + x^2/2 + x^3/6 + ...
在x很小的情况下 -> e^x ≈ 1 + x

将泰勒公式带入p(n):

p(n) ≈ 1 · e^(-1/365) · (-2/365) ··· (-(n-1)/365)
= e^(-n(n-1)/730)

进一步将p(n)通用化,并将结果从不碰撞转换为碰撞的概率:

p(n,h) = 1 - e^(-n(n-1)/2h)

进行简单的数学变换:

n(p,h) = (2h·ln(1/(1-p)))^1/2

实际应用中,暂时我们以50%为标准,将0.5代入p:

n(0.5,H) = 1.1774 · (h ^ 1/2)

抽象理解和安全验证边界

从抽象层面来看,生日攻击的理念类似于以空间换时间的攻击方式。主要原因是生日攻击的目标一般是HASH碰撞,而HASH计算的本质是将近乎无限的输入映射到定长或者有限长的hash串,这就注定了多对一的映射关系。因此,必然存在两个输入M1和M2能够满足HASH(M1)=HASH(M2),这其中的M1和M2就是我们定义的HASH碰撞攻击结果

尤其值得注意的是,这里我们探讨的HASH碰撞与生日攻击,没有利用任何HASH函数内部的实现机制,因此这种攻击是具有通用型的,而防御方式也相对简单,只需要增加HASH的长度,提高攻击者的计算成本即可。

总结

这节课我们学习了密码学相关的知识。

古代战争中使用的密令、密码本等都是密码学的一种缩影,它们大都可以划分到古典密码学分支。在现代密码学分支出现之前,古典密码学依靠加密算法的保密性,发挥了巨大的作用,并且诞生了以凯撒密码、维吉尼亚密码为首的一系列经典的替换式密码以及位移式密码算法。

随着算法研究的不断深入以及计算机算力的增长,经典密码学的安全性难以得到保障,于是诞生了现代密码学分支。在现代密码学分支中,根据加密/解密密钥是否相同又可以划分为对称加密算法与非对称加密算法,其中DES、AES、RC4等都是知名的对称加密算法,而RSA、椭圆曲线加密等都是知名的非对称加密算法。

在Web应用开发中,我们既会面临密码学相关技术的工程化风险,包括硬编码密钥、随机值重用、使用不安全的算法、可预测的初始化向量以及Padding相关的安全问题;也会面临理论层面比如HASH值空间碰撞风险。这些都需要我们了解并熟悉加密算法的原理,这样才能够很好的驾驭这架复杂又强大的机器。

相信通过本节课的学习,你已经构建了清晰宏观的密码学视角,在面对特定安全场景时能够处理得安全又不失优雅。密码学是一个快速发展的数学分支,深入地了解其中原理一定能够帮助你构建更加强大、安全、可靠的应用系统!

思考题

  1. 你可以尝试使用卡西斯基试验破解维吉尼亚密码吗?
  2. 你可以尝试计算我们日常使用的HASH能够抵御多少量级的碰撞攻击吗?

欢迎在评论区留下你的思考,我们下节课再见。