目录
- 一、图片隐写原理(LSB)
- [二、 可隐写文件容量大小计算](#二、 可隐写文件容量大小计算)
- 三、文件隐写和还原步骤
- [四、为什么 PNG 可以隐写文件,而 JPG 不行?](#四、为什么 PNG 可以隐写文件,而 JPG 不行?)
- 五、隐写工具类
- 六、测试
一、图片隐写原理(LSB)
最经典的是 LSB(最低有效位)
图片像素:
java
RGB:
11111110
修改最后1位:
java
11111111
肉眼几乎看不出来,于是,每个颜色通道藏1bit
一个像素:
java
R + G + B = 3bit
即 一个像素 可隐写 3bit 数据,所以隐写容量取决于像素数量
二、 可隐写文件容量大小计算
公式:可隐写的文件大小 (单位bytes) = (width × height × 3) / 8
举例:
像素:1920×1080
可隐写文件容量:(1920×1080 × 3) / 8 ≈ 777,600 bytes ≈ 759KB
像素:2560 ×1440
可隐写文件容量:(2560 ×1440 × 3) / 8 ≈ 1,382,400 bytes ≈ 1.32MB
4K 图片像素:3840 × 2160
可隐写文件容量:(3840 × 2160 × 3) / 8 ≈ 3.1MB
三、文件隐写和还原步骤
文件隐写:
java
文件 -> AES加密 -> 文件隐写入png图片
png图片还原文件:
java
文件隐写入png图片 -> AES解密 -> 文件
四、为什么 PNG 可以隐写文件,而 JPG 不行?
因为 PNG 是无损压缩,PNG 保存图片时像素值不会变化,通过修改RGB最低位,100%保留,所以 LSB 隐写非常适合 PNG。
PNG vs JPG 隐写区别:
| 对比 | PNG | JPG |
|---|---|---|
| 压缩方式 | 无损 | 有损 |
| 能否直接LSB | 可以 | 不可以 |
| 数据稳定性 | 极高 | 极低 |
| 图片体积 | 较大 | 较小 |
| 抗平台压缩 | 差 | 差 |
| 隐写难度 | 简单 | 很复杂 |
| Java实现 | 很容易 | 很难 |
五、隐写工具类
java
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
public class FileHidenToPicUtil {
private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final int KEY_SIZE = 256;
private static final int ITERATIONS = 65536;
private static final String password = "2026@InternalFILE#Transfer";
/**
* 文件隐写入png图片文件
*
* @param inputFile 待隐写入的文件路径,如:C:\\Users\\haitang\\Desktop\\xa.pdf"
* @param coverPngImage png文件路径,如:C:\Users\haitang\Desktop\Snipaste.png
* @param outputPngImage 最终生成的 含有隐写入文件的png路径,如:如:C:\Users\haitang\Desktop\new.png
*/
public static void fileHidenToPic(String inputFile, String coverPngImage, String outputPngImage) throws Exception {
//读取文件
byte[] fileBytes = Files.readAllBytes(Paths.get(inputFile));
// gzip压缩
byte[] compressed = gzip(fileBytes);
// AES
byte[] encrypted = encrypt(compressed, password);
// 读取图片
BufferedImage image = ImageIO.read(new File(coverPngImage));
// 写入数据
BufferedImage output = embedData(image, encrypted);
// 输出PNG
ImageIO.write(output, "png", new File(outputPngImage));
System.out.println("隐写完成");
}
/**
* 隐写的png文件还原为真实文件
*
* @param inputPNGImage 待还原的隐写png文件路径,如:C:\\Users\\haitang\\Desktop\\new.png"
* @param outputFile png文件路径,如:C:\Users\haitang\Desktop\recover.pdf
*/
public static void reCoverFile(String inputPNGImage, String outputFile) throws Exception {
BufferedImage image = ImageIO.read(new File(inputPNGImage));
// 提取
byte[] encrypted = extractData(image);
// AES解密
byte[] compressed = decrypt(encrypted, password);
// gunzip
byte[] fileBytes = gunzip(compressed);
Files.write(Paths.get(outputFile), fileBytes);
System.out.println("恢复成功");
}
//AES加密
private static byte[] encrypt(byte[] data, String password) throws Exception {
// 生成salt
byte[] salt = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(salt);
// 派生密钥
SecretKey key = deriveKey(password, salt);
// 生成IV
byte[] iv = new byte[16];
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] encrypted = cipher.doFinal(data);
// 保存 salt + iv + data
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(salt);
bos.write(iv);
bos.write(encrypted);
return bos.toByteArray();
}
//AES解密
private static byte[] decrypt(byte[] encryptedData, String password) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(encryptedData);
// 读取salt
byte[] salt = new byte[16];
bis.read(salt);
// 读取iv
byte[] iv = new byte[16];
bis.read(iv);
// 剩余是密文
byte[] encrypted = new byte[bis.available()];
bis.read(encrypted);
SecretKey key = deriveKey(password, salt);
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
return cipher.doFinal(encrypted);
}
//PBKDF2 密钥派生
private static SecretKey deriveKey(String password, byte[] salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_SIZE);
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
}
/**
* 数据写入LSB
*/
private static BufferedImage embedData(BufferedImage image, byte[] data) throws Exception {
int width = image.getWidth();
int height = image.getHeight();
BufferedImage output = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 复制原图
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
output.setRGB(x, y, image.getRGB(x, y));
}
}
// 数据长度(4字节)
byte[] lengthBytes = intToBytes(data.length);
// 合并长度 + 数据
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(lengthBytes);
bos.write(data);
byte[] allData = bos.toByteArray();
//检查图片容量是否足够隐写文件
int capacity = width * height * 3 / 8;
if (allData.length > capacity) {
double capacityMB = capacity / 1024.0 / 1024.0;
double requiredMB = allData.length / 1024.0 / 1024.0;
throw new RuntimeException(
"图片隐写容量不足!\n" +
"当前图片最大隐写的文件大小: " + String.format("%.2f", capacityMB) + " MB\n" +
"当前文件压缩后大小: " + String.format("%.2f", requiredMB) + " MB\n"+
"请更换分辨率更大的png文件,或者更换更小的要隐写的文件。"
);
}
int dataIndex = 0;
int bitIndex = 0;
outer:
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = output.getRGB(x, y);
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
// R
if (dataIndex < allData.length) {
r = setLSB(r, getBit(allData[dataIndex], bitIndex));
bitIndex++;
if (bitIndex == 8) {
bitIndex = 0;
dataIndex++;
}
}
// G
if (dataIndex < allData.length) {
g = setLSB(g, getBit(allData[dataIndex], bitIndex));
bitIndex++;
if (bitIndex == 8) {
bitIndex = 0;
dataIndex++;
}
}
// B
if (dataIndex < allData.length) {
b = setLSB(b, getBit(allData[dataIndex], bitIndex));
bitIndex++;
if (bitIndex == 8) {
bitIndex = 0;
dataIndex++;
}
}
int newRgb = (r << 16) | (g << 8) | b;
output.setRGB(x, y, newRgb);
if (dataIndex >= allData.length) {
break outer;
}
}
}
return output;
}
/**
* 设置最低位
*/
private static int setLSB(int value, int bit) {
return (value & 0xFE) | bit;
}
/**
* 获取bit
*/
private static int getBit(byte b, int index) {
return (b >> (7 - index)) & 1;
}
/**
* int转byte[]
*/
private static byte[] intToBytes(int value) {
return new byte[]{
(byte) (value >> 24),
(byte) (value >> 16),
(byte) (value >> 8),
(byte) value
};
}
/**
* gzip
*/
private static byte[] gzip(byte[] data) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(bos);
gzip.write(data);
gzip.close();
return bos.toByteArray();
}
/**
* 提取LSB数据
*/
private static byte[] extractData(BufferedImage image) throws Exception {
int width = image.getWidth();
int height = image.getHeight();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int currentByte = 0;
int bitCount = 0;
int dataLength = -1;
int bytesRead = 0;
outer:
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
int[] colors = {(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff};
for (int color : colors) {
int bit = color & 1;
currentByte = (currentByte << 1) | bit;
bitCount++;
if (bitCount == 8) {
bos.write(currentByte);
bytesRead++;
// 前4字节是长度
if (bytesRead == 4) {
byte[] lenBytes = bos.toByteArray();
dataLength = bytesToInt(lenBytes);
bos.reset();
}
// 数据读取完成
if (dataLength != -1 && bos.size() == dataLength) {
break outer;
}
currentByte = 0;
bitCount = 0;
}
}
}
}
return bos.toByteArray();
}
/**
* byte[]转int
*/
private static int bytesToInt(byte[] bytes) {
return ((bytes[0] & 0xff) << 24) |
((bytes[1] & 0xff) << 16) |
((bytes[2] & 0xff) << 8) |
(bytes[3] & 0xff);
}
/**
* gunzip
*/
private static byte[] gunzip(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
GZIPInputStream gzip = new GZIPInputStream(bis);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = gzip.read(buffer)) > 0) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
}
}
六、测试
可以先下载该 png 图片,图片分辨率 5120 x 2880 大约可以隐写 5MB 的文件。
链接:https://i-blog.csdnimg.cn/direct/4b54f5f678bb404f8f92007792e46592.png

或者去 https://4kwallpapers.com/uncompressed-png-wallpapers/ 下载其他高清 png 图片
1.文件隐写入png文件
java
String inputFile = "C:\\Users\\haitang\\Desktop\\aaa.pdf";
String coverPngImage = "C:\\Users\\haitang\\Desktop\\Snipaste.png";
String outputPngImage = "C:\\Users\\haitang\\Desktop\\result.png";
FileHidenToPicUtil.fileHidenToPic(inputFile, coverPngImage, outputPngImage);
2.png文件还原文件
java
String inputPngImage="C:\\Users\\haitang\\Desktop\\result.png";
String outputFile="C:\\Users\\haitang\\Desktop\\aaaNew.pdf";
FileHidenToPicUtil.reCoverFile(inputPngImage,outputFile);