EXP4 视频流Hash
参考实验:
一、读取视频流并转换为二进制数据格式
python调用opencv读取视频,可以直接调用cv2.VideoCapture()
函数直接读取视频或摄像头,通过传入数字来读取对应的摄像头,或者通过传入一个路径字符串来读取对应的视频文件。
然后用read()
逐个读取视频帧并用cv2.imencode()[1].tobytes()
转换为字节形式。其中cv2.imencode()函数将图像编码为指定格式(例如JPEG、PNG等),并返回一个包含编码图像数据的numpy数组。第一个参数表示编码后的图像格式为JPEG格式,第二个参数表示要编码的图像。该函数返回一个包含两个元素的元组,其中第一个元素是一个布尔值,表示编码是否成功,第二个元素是一个包含编码后图像数据的numpy数组。接着使用.tobytes()方法将numpy数组转换为二进制数据。
def video_to_bytes(self): # 读入视频流存储到二进制变量中
video = cv2.VideoCapture(self.__video)
frames = "".encode("utf-8")
while True:
ret, frame = video.read() # 读取视频帧。返回值为一个布尔值和一个NumPy数组,布尔值表示读取是否成功,当读取到视频的最后一帧时,read()会返回False;NumPy数组则是表示读取到的视频帧的图像数据。
if not ret: # 检查是否已经读到最后一帧
break
# 将帧转换为字节串
binary_frame = cv2.imencode(".jpg", frame)[1].tobytes()
# 将图像编码为指定格式(例如JPEG、PNG等),并返回一个包含编码图像数据的numpy数组。
# 返回一个包含两个元素的元组,其中第一个元素是一个布尔值,表示编码是否成功,第二个元素是一个包含编码后图像数据的numpy数组。
frames += binary_frame
video.release() # 释放资源
return frames
这样做有问题,运行时间很长,且视频二哈希值计算结果是"0f9214e9d48303efc6366b5a923f19875fc02b48d38c88f2af2f445de969ab8e”
,计算错误,说明不能一帧读取照片,而应该用read()
方法读取流文件。
倒着读取文件,从指定位置开始读取指定大小的数据:file_obj.seek(last_pos - read_size)
data = file_obj.read(read_size)
。并且打开文件使用with open(self.__video, 'rb') as file
语句,以二进制模式读取文件,文件内容以字节(bytes)为单位进行读取和写入而非字符。
二、SHA-256算法简介
对于任意长度的消息,SHA256都会产生一个256位的哈希值,称作消息摘要,通常用一个长度为64的十六进制字符串表示。
待哈希的消息在进行哈希计算之前首先要进行以下两个步骤:
①对消息进行填充,使得补充后的长度是512b的整数倍;
②以512b为单位将消息进行分块处理为 M ( 1 ) , M ( 2 ) , . . . , M ( N ) M^{(1)},M^{(2)},...,M^{(N)} M(1),M(2),...,M(N)。
然后分别对每个消息区块进行处理,其中初始哈希值 H ( 0 ) H^{(0)} H(0)固定:
H ( i ) = H ( i − 1 ) + C M ( i ) ( H ( i − 1 ) ) H^{(i)}=H^{(i-1)}+C_{M^{(i)}}(H^{(i-1)}) H(i)=H(i−1)+CM(i)(H(i−1))
其中 C C C是SHA-256的压缩函数,+是 m o d 2 32 mod2^{32} mod232加法,即将前一个区块的哈希值和本区块消息值加在一起,并对 2 32 2^{32} 232取余即可得到消息区块的哈希值
1.常量初始化
SHA-256算法中用到了8个哈希初值以及64个哈希常量。
初始哈希取自自然数中前面8个素数(2,3,5,7,11,13,17,19)的平方根的小数部分,并且取前面的32位。例如 2 ≈ 0.414213562373095048 \sqrt{2} \approx 0.414213562373095048 2≈0.414213562373095048,而 0.414213562373095048 ≈ 61 6 − 1 + a 1 6 − 2 + 0 ∗ 1 6 − 3 + . . . 0.414213562373095048 \approx 616^{-1} + a16^{-2} + 0*16^{-3} + ... 0.414213562373095048≈616−1+a16−2+0∗16−3+... ,则前12位对应0x6a09e667。
以此类推:
H 1 ( 0 ) = 6 a 09 e 667 H 2 ( 0 ) = b b 67 a e 85 H 3 ( 0 ) = 3 c 6 e f 372 . . . . . . H_1^{(0)}=6a09e667 \\ H_2^{(0)}=bb67ae85 \\ H_3^{(0)}=3c6ef372 \\ ...... H1(0)=6a09e667H2(0)=bb67ae85H3(0)=3c6ef372......
其他64个常数取自自然数中前64个素数的立方根的小数部分的前32位。
2.消息预处理
- 填充比特:假设消息M的二进制编码长度为l位。首先在消息末尾补上一位"1", 然后再补上k个"0", 其中k为下列方程的最小非负整数:
l + 1 + k ≡ 448 m o d 512 l+1+k \equiv 448 mod 512 l+1+k≡448mod512
- 附加长度值:接着将原始数据(第一步填充前的消息)的长度信息补到已经进行了填充操作的消息之后。SHA-256用一个长为64位的数据表示原始消息的长度。
3.计算消息摘要
-
拆分消息:将M拆分为N个大小为512b的块。
-
构造64个字:对于每一块,将其分解为16个32b的big-endian的字,记为w[0], …, w[15]。前16个字直接由消息的第i个块分解得到,其余的字由如下迭代公式得到:
W t = σ 1 ( W t − 2 ) + W t − 7 + σ 0 ( W t − 15 ) + W t − 16 W_t= \sigma_1(W_{t-2})+W_{t-7}+\sigma_0(W_{t-15})+W_{t-16} Wt=σ1(Wt−2)+Wt−7+σ0(Wt−15)+Wt−16
-
进行64次加密循环完成一次迭代:
哈希计算算法定义如下:
-
F o r i = 1 → N ( N 为填充后的消息块数 ) For\ i\ =\ 1\rightarrow N\left( N\text{为填充后的消息块数} \right) For i = 1→N(N为填充后的消息块数)
用第(i-1)个中间哈希值对a,b,c,d,e,f,g,h进行初始化, 当i=1时,即使用初始化哈希,表示如下:
a ← H 1 ( i − 1 ) b ← H 2 ( i − 1 ) . . . h ← H 8 ( i − 1 ) a \leftarrow H_1^{(i-1)} \\ b \leftarrow H_2^{(i-1)} \\ ... \\ h \leftarrow H_8^{(i-1)} a←H1(i−1)b←H2(i−1)...h←H8(i−1)
-
应用SHA-256压缩函数更新a,b,…,h:
F o r j = 0 → 63 For\ j \ = \ 0 \ \rightarrow \ 63 For j = 0 → 63
其中逻辑函数定义如下:
运算符号定义如下:
-
计算第i个中间哈希值 H ( i ) H^{(i)} H(i):
H 1 ( i ) ← a + H 1 ( i − 1 ) H 2 ( i ) ← b + H 2 ( i − 1 ) . . . H 8 ( i ) ← h + H 8 ( i − 1 ) H_1^{(i)} \leftarrow a + H_1^{(i-1)} \\ H_2^{(i)} \leftarrow b + H_2^{(i-1)} \\ ... \\ H_8^{(i)} \leftarrow h + H_8^{(i-1)} \\ H1(i)←a+H1(i−1)H2(i)←b+H2(i−1)...H8(i)←h+H8(i−1)
则 H ( N ) = ( H 1 ( N ) , H 2 ( N ) , . . . , H 8 ( N ) ) H^{(N)}=(H_1^{(N)},H_2^{(N)},...,H_8^{(N)}) H(N)=(H1(N),H2(N),...,H8(N))为M最终的哈希值。
💡 Python中SHA-256哈希运算举例:
import hashlib
# 创建一个SHA-256对象
sha256 = hashlib.sha256()
# 更新哈希对象的数据,在原消息后追加消息并更新哈希值
sha256.update(b'Hello, world!')
# 计算哈希值
hash_value = sha256.digest()
# 将哈希值转换为16进制字符串表示
hex_value = hash_value.hex()
print(hex_value) # 输出:'c8b5b685638bba8dbe173f70f8c06912df9c2b08cf3c3a8f90ab1cdac8ebf925'
(也可使用Crypto.SHA256):
sha256 = SHA256.new()
sha256.update(chunk)
if last_hash:
sha256.update(last_hash)
last_hash = sha256.digest()
三、题目思想
网站不计算整个文件的哈希值,而是将文件分成 1KB 的数据块(1024 字节)。它计算最后一个块的哈希并将值附加到倒数第二个块。然后它计算这个增强的倒数第二个块的散列,并将得到的散列附加到最后的第三个块。此过程从最后一个块继续到第一个块,如下图所示:
网站将最终的哈希值通过认证信道分发给用户。
在用户端,浏览器每次下载文件的一个数据块,以及上述所示的附加哈希值,并进行验证,若验证通过则播放第一个视频块。具体的,通过验证与是否相等来验证第一个区块的完整性;通过验证与是否相等来验证第二个区块的完整性;以此类推直至最后一个区块。这样,每个块都会在收到时进行验证和播放,无需等到整个文件下载完毕。容易证明,只要哈希函数满足抗碰撞性质,那么攻击者的篡改行为无法通过上述完整性验证。
本次实验使用 SHA-256 作为哈希函数,实现上述视频验证程序。每一个哈希值以二进制数据的形式和视频数据块进行链接。若视频数据大小不是 1KB的整倍数,那么最后一个区块可以小于 1KB,其余的数据区块则需要刚好是1KB 的整倍数。
程序能够根据输入的文件计算得到相应的,以验证收到文件的正确性。具体的,请在实验报告中给出视频1的值(使用 hex 编码)。
测试用例:视频2的值为:
03c08f4ee0b576fe319338139c045c89c3e8e9409633bea29442e21425006ea8
对于 SHA-256 的实现,允许使用现有的加密库,例如 PyCrypto (Python)、Crypto++ (C++) 或任何其他库。
四、实验代码
# -*- coding = utf-8 -*-
# @Time : 2023/5/7 14:56
# @Author : cherry
# @Software : PyCharm
import os
import hashlib
class Hash: # 定义Hash方法
def __init__(self, video_name, block_size):
self.__video = video_name
self.__size = os.path.getsize(self.__video) # 获取文件的大小(以字节为单位)
self.__normal_size = block_size
self.__last_size = self.__normal_size if self.__size % self.__normal_size == 0 else self.__size % self.__normal_size
def read_reversed(self, file_obj): # 倒序读取文件
block_num = 0 # 分组个数
last_pos = self.__size # 最后读取的位置
while last_pos > 0: # 没有读到最前面
read_size = self.__last_size if block_num == 0 else self.__normal_size # 读取最后一块时不用按照整块读取
file_obj.seek(last_pos - read_size) # 改变读写指针的位置
data = file_obj.read(read_size) # 读取指定字节的数据
if not data: # 数据为空
break
block_num += 1
last_pos -= read_size
yield data # 返回一个迭代器对象,允许函数暂停和继续执行的状态保持,这样生成器就可以迭代生成一系列的值,而不是一次性生成并返回所有的值
def hash(self): # 计算hash值
# 读取文件内容
with open(self.__video, 'rb') as file: # 二进制模式读取文件时,文件内容以字节(bytes)为单位进行读取和写入而非字符
last_hash = b'' # 最后一个hash值
for block in self.read_reversed(file):
# 创建一个SHA-256对象
sha256 = hashlib.sha256()
sha256.update(block) # 将提供的字节数据追加到当前的消息中,并更新摘要
if last_hash:
sha256.update(last_hash) # 追加上一个hash
last_hash = sha256.digest()
return last_hash.hex() # 十六进制字符串
if __name__ == "__main__":
video_1 = "./6.1.intro.mp4"
video_2 = "./6.2.birthday.mp4"
block_size = 1024
hash1 = Hash(video_1, block_size)
hash2 = Hash(video_2, block_size)
hash_value2 = "03c08f4ee0b576fe319338139c045c89c3e8e9409633bea29442e21425006ea8"
hash_test2 = hash2.hash()
print("测试视频2的h0值为:", hash_test2)
# 验证是否正确
if hash_test2 == hash_value2:
print("测试通过!")
hash_test1 = hash1.hash()
print("测试视频1的h0值为:", hash_test1)
else:
print("测试失败,请检查代码!")
运行结果: