在视频中隐藏秘密:如何将文本嵌入图片,并完美解密
在当今的信息时代,安全传递信息变得尤为重要。如何在不被察觉的情况下传递一段文本,成为了谍战故事中的重要一环。而将文本隐藏在图片中,则是一种巧妙的方式。
本文将介绍如何将文本转换为图片,并在图片中嵌入文本信息,最终通过专业工具实现文本的加密与解密。
1. 从文本到图片的初步实现
这里我们先做将文本转成图片, 然后从图片上看不出来文本的汉字, 但是专业工具能解密出来. 也即我们实现一个 encode 函数 和一个 decode 函数。
这一次尝试, 我们只保证转出来是个图片.
1.1 转换文本为二进制字节流
在这一步中,假设我们的文本是"有内鬼终止交易"重复一万次, 首先我们将该字符串转为二进制 bytes.:
# 导入必要的包
from PIL import Image
import numpy as np
import struct
import math
# 文本转二进制字节流
text = '有内鬼终止交易.'*1000
text_bytes = text.encode('utf-8')
print(text_bytes[:100]) # 打印前200位数据
1.2 将字节流转换为图片
我们可以利用二进制流不区分类型的特点,将其转换为任意导出为二进制的变量。为了避免深入研究图片协议的头部信息,我们使用 numpy 作为中转工具。每张图片都可以表示为一个 长度 × 宽度 × 3(RGB)的矩阵,矩阵元素的取值范围是 0-255,分别对应红、绿、蓝三色的深度。由于处理二进制流不便,大多数情况下按 8 位处理,而 8 位二进制可以表示 0-255, 因此,text_bytes 的长度可以与图片中像素点数量 ×3 对应上。
只需要 2 步骤就可以完成二进制流转图片:
- 生成一个能装的下文本二进制流的图片, 需要图片中像素点*3 大于文本字节数。
- 将文本字节流和字节流长度信息放到图片 numpy 字节流中, 保存成图片。
这里需要放 字节流长度是因为图片容器字节数大于内容需要的字节数。因此,我们需要在 text_bytes 前添加一个表示其长度的二进制数据。这样在操作时 a) 读取图片时,先读取前 4 个字节获取内容长度。b) 根据长度读取对应大小的字节,获取原始文本的二进制流。 在很多涉及到字节流的地方, 都需要这样处理。
主要分为 1. 计算图片边长 2. 将字节流伪装成图片数据塞入图片。
# 计算最小图片正方形边长
text_bytes_contain_lens = struct.pack('>l',len(text_bytes)) + text_bytes
img_width = math.ceil((len(text_bytes_contain_lens)/3)**(1/2))
print(f'正方形图片的边长是 {img_width}')
# 生成并保存图片
img_path = "text.png"
picMap = np.random.randint(0,255,(img_width, img_width,3),dtype=np.uint8).tobytes()
picMap = text_bytes_contain_lens + picMap[len(text_bytes_contain_lens):]
picMapNpy = np.frombuffer(picMap,dtype=np.uint8).reshape((img_width,img_width,3))
Image.fromarray(picMapNpy).save(img_path)
生成的图片看起来虽然像是损坏的图片,但它确实有效。接下来,我们将演示如何从图片中提取出隐藏的文本。
1.3 从图片中解密文本
一旦我们将文本嵌入到图片中,接下来我们需要一个解密的过程,从图片中提取出隐藏的文本信息。这里主要的代码为之前代码反向的过程, 以下代码展示了如何解密并恢复原始文本:
# 从图片中恢复文本
img_path = "text.png"
pic = Image.open(img_path).convert("RGB")
picBytes = np.array(pic).tobytes()
lenData = struct.unpack('>l',picBytes[:4])[0]
byteData = picBytes[4:4+lenData]
text = byteData.decode('utf-8')
print(text[:100])
1.4 我们将之前的代码稍加封装即可得到两个函数
def saveText2Img(text, img_path):
text_bytes = text.encode('utf-8')
text_bytes_contain_lens = struct.pack('>l',len(text_bytes)) + text_bytes # struct.pack('>l',...) 是获取该变量二进制流
img_width = math.ceil((len(text_bytes_contain_lens)/3)**(1/2))
picMap = np.random.randint(0,255,(img_width, img_width,3),dtype=np.uint8).tobytes()
picMap = text_bytes_contain_lens + picMap[len(text_bytes_contain_lens):] # picMap前面的内容替换为text_bytes
picMapNpy = np.frombuffer(picMap,dtype=np.uint8).reshape((img_width,img_width,3)) # 基于该字节流拿到rgb矩阵
Image.fromarray(picMapNpy).save(img_path)
def loadTextFromImg(img_path):
pic = Image.open(img_path).convert("RGB")
picBytes = np.array(pic).tobytes()
lenData = struct.unpack('>l',picBytes[:4])[0] # 读取前4个字节, 为数据字节流的长度.
byteData = picBytes[4:4+lenData]
text = byteData.decode('utf-8')
return text
saveText2Img('有内鬼终止交易.'*10,'version1.png')
loadTextFromImg('version1.png')
2. 生成更“好看”的图片
我们发现生成的图片, 完全无意义, 明显就感觉是一个“坏图片”, 那我们能不能在看起来正常的图片里面加入我们的数据呢?
刚刚生成的图片很难看是因为我们将数据直接转成了图片, 本身图片不含内容。那更好的做法是我们将图片实际值和预期值的差作为信息,来存储数据。 比如我们使用纯色图片作为基础, 在纯色基础上稍加扰动来存储信息。这样从外部看还是一个普通的图片, 但是实际上已经存储了我们希望的文本。 这里为了简单, 我们将一个 byte 也就是 8 个 bit 拆成 8 个 byte. 具体可以参考下面的代码.
2.1 实现信息加密与解密
我们首先定义了几个辅助函数,用来实现字节与比特的相互转换,以及进行异或加密:
def byte_to_bits(byte_data):
bit_bytes = []
for byte in byte_data:
bits = f'{byte:08b}'
for bit in bits:
bit_bytes.append(int(bit).to_bytes(1, byteorder='big'))
return b''.join(bit_bytes)
def bits_to_byte(bit_bytes):
if len(bit_bytes) % 8 != 0:
raise ValueError("输入字节流的长度必须是8的倍数。")
byte_list = []
for i in range(0, len(bit_bytes), 8):
bit_chunk = bit_bytes[i:i+8]
bit_str = ''.join(str(int(b)) for b in bit_chunk)
byte = int(bit_str, 2)
byte_list.append(byte)
return bytes(byte_list)
def xor_bytes(bytes1, bytes2):
if len(bytes1) != len(bytes2):
raise ValueError("两个字节数组的长度必须相同。")
result = bytes(a ^ b for a, b in zip(bytes1, bytes2))
return result
通过下面测试发现, 可以通过差异信息来存储信息。
2.2 将文本嵌入“好看”的图片中
接下来,我们基于上面的辅助函数,对之前的代码进行修改,实现一个看起来正常的图片中嵌入文本信息的过程:
def saveText2Img(text, img_path, baseColor=(70, 130, 180)):
text_bytes = text.encode('utf-8')
text_bytes_contain_lens = struct.pack('>l',len(text_bytes)) + text_bytes # struct.pack('>l',...) 是获取该变量二进制流
img_width = math.ceil((len(text_bytes_contain_lens)/3*8)**(1/2))
baseImg = Image.new("RGB", (img_width, img_width), baseColor)
baseImgBytes = np.array(baseImg).tobytes()
sparsetextBytes = byte_to_bits(text_bytes_contain_lens)
baseImgWithInfoBytes = xor_bytes(baseImgBytes[:len(sparsetextBytes)],sparsetextBytes)+baseImgBytes[len(sparsetextBytes):]
picMapNpy = np.frombuffer(baseImgWithInfoBytes,dtype=np.uint8).reshape((img_width,img_width,3)) # 基于该字节流拿到rgb矩阵
Image.fromarray(picMapNpy).save(img_path)
saveText2Img('有内鬼终止交易.'*10,'version2.png')
2.3 从“好看”的图片中解密文本
同样,我们也需要对解密过程进行更新,使其能够正确提取出隐藏在“好看”图片中的文本信息:
def loadTextFromImg(img_path,baseColor=(70, 130, 180)):
pic = Image.open(img_path).convert("RGB")
img_width = pic.size[0]
baseImg = Image.new("RGB", (img_width, img_width), baseColor)
baseImgBytes = np.array(baseImg).tobytes()
baseImgBytesFirst = baseImgBytes[:4*8]
picBytes = np.array(pic).tobytes()
picBytesFirst = picBytes[:4*8]
lenBytes = bits_to_byte(xor_bytes(baseImgBytesFirst,picBytesFirst))
lenData = struct.unpack('>l',lenBytes)[0] # 读取前4个字节, 为数据字节流的长度.
byteData = picBytes[4*8:(4+lenData)*8]
baseImgBytesData = baseImgBytes[4*8:(4+lenData)*8]
byteData = bits_to_byte(xor_bytes(byteData,baseImgBytesData))
text = byteData.decode('utf-8')
return text
loadTextFromImg('version2.png')
2.4 使用真实的图片
使用真实图片的话和纯色图片类似, 只需要保证 encode 和 decode 使用同一个图片就行, 可能也还需要适当裁剪来保证尺寸, 具体细节就不在这里讨论了。
通过这两个过程,我们已经能够将文本成功地嵌入到图片中,并在必要时解密出来。虽然我们只展示了较为基础的加密和解密过程,但在实际应用中,这种方法可以扩展并与其他加密技术结合,使得信息传递更加安全。利用这一技巧,你可以将任何需要隐秘传输的文本信息嵌入到图片或视频中,从而提高信息安全性。