在当今的信息时代,安全传递信息变得尤为重要。如何在不被察觉的情况下传递一段文本,成为了谍战故事中的重要一环。而将文本隐藏在图片中,则是一种巧妙的方式。

本文将介绍如何将文本转换为图片,并在图片中嵌入文本信息,最终通过专业工具实现文本的加密与解密。

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 使用同一个图片就行, 可能也还需要适当裁剪来保证尺寸, 具体细节就不在这里讨论了。


通过这两个过程,我们已经能够将文本成功地嵌入到图片中,并在必要时解密出来。虽然我们只展示了较为基础的加密和解密过程,但在实际应用中,这种方法可以扩展并与其他加密技术结合,使得信息传递更加安全。利用这一技巧,你可以将任何需要隐秘传输的文本信息嵌入到图片或视频中,从而提高信息安全性。