本关任务:利用哈希算法统计每个字符串出现的个数。
为了完成本关任务,你需要掌握:1.密码学哈希函数的概念及特性,2.安全哈希算法。
我们需要理解的第一个密码学的基础知识是密码学哈希函数,哈希函数是一个数学函数,具有以下三个特性: ● 其输入可为任意大小的字符串。 ● 它产生固定大小的输出。为使本章讨论更具体,我们假设输出值大小为256
位,但是,我们的讨论适用于任意规模的输出,只要其足够大。 ● 它能进行有效计算,简单来说就是对于特定的输入字符串,在合理时间内,我们可以算出哈希函数的输出。更准确地说,对应n位的字符串,其哈希值计算的复杂度为O(n)
。 这些特性定义了一般哈希函数,以这个函数为基础,我们可以创建数据结构,例如哈希表。我们将只专注于加密的哈希函数,要使哈希函数达到密码安全,我们要求其具有以下三个附加特性:
collision−resistance
)hiding
)puzzle−friendliness
)下面具体阐述这三个特性
H(⋅)
,没有人能够找到碰撞,我们则称该函数具有碰撞阻力。即:如果无法找到两个值,x
和y
,x\\=y
,而H(x)=H(y)
,则称哈希函数H
具有碰撞阻力y=H(x)
,我们没有可行的办法算出输入值x
。问题是,上述的表示形式不一定是正确的。考虑以下简单的例子:我们做一个抛硬币的实验,如果抛硬币结果为正面,我们会宣布字符串哈希为“正面”;如果结果为反面,我们会宣布字符串哈希为“反面”。 然后我们问我们的对手,在他没有见到抛硬币,而只见到哈希函数的输出的前提下说出哈希函数的输入字符串。为了回答问题,对手会简单计算“正面”字符串的哈希值及“反面”字符串的哈希值,然后对手便可以知道他得到的是哪一个。这样,只需要几步,对手就能反解出输入值。 对手能够猜出字符串,这是因为x
只有两个可能,他可以很轻易地将两个可能对应的哈希值计算出来。为了能够实现隐秘性,我们需要x的取值来自一个非常广泛的集合,也就是说,仅仅通过尝试几个特定的x
,就能找到输出值的方式将不会发生了。 现在的问题是:在类似抛硬币的“正面”、“反面”实验中,如果我们想要的反解的输入值并非来自分散的集合,我们是否还能得到隐秘性?幸运的是,对于这个问题答案是肯定的!我们甚至能够通过与另一个较为分散的输入进行结合,而将一个并不分散的输入进行隐秘。现在我们可以更精确地表示隐秘的含义了(双竖线‖
为连接符号,代表把一系列事件、事情等联系起来)。即:哈希函数H具有隐秘性,如果:当其输入r
选自一个高阶最小熵(highmin−entroy
)的概率分布,在给定H(r‖x)
条件下来确定x
是不可行的。y
值所对应的输入,假定在输入集合中,有一部分是非常随机的,那么他将非常难以求得y
值对应的输入。即: 如果对于任意n
位输出值y
,假定k选自高阶最小熵分布,如果无法找到一个可行的方法,在比2n
小很多时间内找到x
,保证H(k‖x)=y
成立,那么我们称哈希函数H
为谜题友好。安全哈希算法(SecureHashAlgorithm256
,简称SHA−256
)。哈希函数有很多,但SHA−256
是一个主要被比特币世界采用,并且效果还很不错的哈希函数。 回想一下,我们要求哈希函数可以用于任意长度输入。幸运的是,只要我们能建立一个用于固定长度输入的哈希函数,然后通过一般方法,就可以将接受固定长度的哈希函数转化为可接受任意长度输入的哈希函数,我们称这个转换过程为MD
(Merkle−Damgard
)变换,SHA−256
是采用这种变换方法的常用哈希函数之一。在通用术语中,这种基础型,可用于固定长度,具备碰撞阻力的哈希函数被称为是压缩函数(compressionfunction
)。经过验证,如果基本压缩函数具有碰撞阻力的特性,那么经过转换而生成的哈希函数也具有碰撞阻力。 MD
变换很简单。比如压缩函数代入长度为m
的输入值,并产生长度短一些为n
的输出值。哈希函数的输入(可为任意大小)被分为长度为m−n
的区块。MD
变换运作过程如下:将每个区块与之前区块的输出一起代入压缩函数,注意,输入长度则变为(m−n)+n=m
,也刚好就是压缩函数的输入长度。对于第一个区块而言,之前没有的区块,我们需要选取一个初始向量。每次调用哈希函数,这个数字都会被再一次使用,而在实践中,你可以直接在标准文档中找到它。最后一个区块的输出也就是你返回的结果。 SHA−256
函数利用了这样的一个压缩函数,这个压缩函数把一个768
位的输入压缩成一个256
位的输出,每一个区块的大小是512
位。
根据提示,在右侧编辑器补充代码,输出每个字符串的次数,具体格式看样例,输出按照字符串的字典序从小到大输出。
平台会对你编写的代码进行测试:
测试输入:
5 a ab abc a ab
预期输出:
a 2 ab 2 abc 1
提示: 可以使用map
用map统计一下每个字符串出现的次数即可。
#include
using namespace std;
//在下面Begin和End之间补全代码
/*********** Begin ***********/
int main()
{
int n;
cin>>n;
string x;
map<string, int> mp;
for(int i=0; i<n; i++)
{
cin>>x;
auto it = mp.find(x);
if (it != mp.end()) { // 键存在
mp[x] = mp[x] + 1;
} else {
mp[x] = 1;
}
}
for (auto i : mp) {
cout << i.first << " " << i.second << endl;
}
return 0;
}
/*********** End ***********/
本关任务:对给定的身份以及消息进行数字签名。
为了完成本关任务,你需要掌握:1.数字签名的概念和性质,2.公钥即身份。
数字签名是密码学中的第二个重要部分。数字签名被认为是对纸上手写签名的数字模拟。我们对数字签名有两个特性要求,使其与我们对手写签名的预期一致。
对于手写签名来说,第二条就如同确保别人不能将你的签名从一份文件上剪下来,贴到另一份文件的末尾那样。 那我们如何通过密码学来构建这些性质呢?首先,让我们把之前的直观讨论说得更具体一些,以便今后可以更好地论证数字签名方案,并讨论其安全特性。 数字签名方案
(1)数字签名方案由以下三个算法构成: ● (sk,pk):=generateKeys(keysize)``generateKeys
方法把keysize
作为输入,来产生一对公钥和私钥。私钥sk
被安全保存,并用来签名一段消息;公钥pk
是人人都可以找到的,拿到它,就可以用来验证你的签名。 ● sig:=sign(sk,message)
签名过程是把一段消息和私钥作为一个输入,对于消息输出是签名。 ●isValid:=verify(pk,message,sig)
验证过程是通过把一段消息和签名消息与公钥作为输入,如果返回的结果是真,证明签名属实;如果返回的结果为假,证明签名消息为假。 (2)我们要求以下两个性质有效: ● 有效签名可以通过验证,即: verify(pk,message,sign(sk,message))==true
● 签名不可伪造。
让我们来看一下与数字签名并行的一个有用技巧,基本想法是从数字签名模式中拿出一个公共验证密钥,并将其与一个人或一个系统参与者的身份对等。如果你见到一条消息的签名被公钥pk
正确验证,那么你可以认为pk
就是在表达这条消息。你真的可以将公钥认为是参与者或者系统的一方,他可以通过签署声明而发布声明。从这个角度来说,公钥就是身份,让某人能为pk
身份发声,他必须知道相应的密钥sk
。 将公钥视为身份的一个结果是,你可以随时制定新的身份——你可以简单通过数字签名方案中的generateKeys
程序,生成新的密钥对sk
和pk
。pk
是你可以使用的新的公共身份,sk
是相应的密钥,只有你自己知道并可以让你代表身份为pk
发声。在实践中,你可能会使用pk
的哈希作为你的身份,这是因为公钥很大。如果是这样的话,为了验证消息来自你的身份,人们会需要验证:
pk
的哈希;pk
验证。此外,在默认情况下,你的公钥pk
基本上看起来是随机的,也并没有人能够通过检查pk
发现你的现实身份(当然,一旦你开始使用这个身份发表声明,这些声明可能泄露信息,而让别人将你的真实身份与pk
联系起来。我们很快会更详细地讨论这个问题)。你可以生成一个看起来随机的新身份,看起来像人群中的一张脸,但这些都只有你能够控制。
根据提示,在右侧编辑器补充代码,根据公钥即身份,我们可以模拟一次消息签名,假如你的身份是e
,有哈希函数H(x)=ax+b
,那么我们可以把pk=H(e)
作为公钥,sk=H(e)−1modq
作为私钥,我们用私钥对消息m进行加密,即en(m)=sk∗m
,那么我们解密就可以这样计算de(en(m))=en(m)∗pk
。这里q
保证是素数。 输入
e a b q m
输出
pk sk en(m)
输入
3 4 5 11 6
输出
17212
提示: 费马定理求逆元,可以查询一下。
b 与 p 互质情况下,若 a/b ≡ a*x (mod p),则 x 为 b 的乘法逆元
可转化为 b * x ≡ 1 (mod p)
费马定理:若 p 为质数,得:bp-1 ≡ 1 (mod p)
又有:b * x ≡ 1 (mod p)
得到:b * bp-2 ≡ 1 (mod p),所以 x = bp-2
// 题目规定 p 是质数,我们使用费马定理需要 a p 是互质并且 p 是质数
// 只有 a 是 p 得倍数时,不互质
if (a % p == 0) System.out.println("impossible");
else System.out.println(quick_power(a, p - 2, p));
// 快速幂
private static long quick_power(long a, long k, long p) {
long ans = 1;
while (k > 0) {
if ((k & 1) == 1) ans = ans * a % p;
k >>= 1;
a = a * a % p;
}
return ans;
}
#include
using namespace std;
int e,a,b,q,m;
// 快速幂求逆元
long ksm(long a, long k, long p) {
long ans = 1;
while (k > 0) {
if ((k & 1) == 1) ans = ans * a % p;
k >>= 1;
a = a * a % p;
}
return ans;
}
// 哈希函数
int H(int x) {
return a * x + b;
}
// 公钥
int pk() {
return H(e);
}
// 私钥
int sk() {
return ksm(H(e), q-2, q);
}
// 加密
int en() {
return sk() * m;
}
int main()
{
cin>>e>>a>>b>>q>>m;
cout << pk() << endl;
cout << sk() << endl;
cout << en() << endl;
return 0;
}