java 使用PNG图片隐写文件

目录

一、图片隐写原理(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);
相关推荐
有梦想的小何1 小时前
Cursor AI 编程实战(篇一):Prompt 与案例总结
java·linux·prompt·ai编程
河阿里1 小时前
SpringBoot:Spring Task定时任务完整使用教学
java·spring boot·spring
jiayong231 小时前
Tool Permission 与 Sandbox 的安全流水线:Agent 工具系统的工程边界
java·数据库·安全·agent
rururunu1 小时前
Windows 下切换 Java 环境太复杂了,我做了个 cli 工具,可以快速安装,切换 Java 版本
java
qq_452396231 小时前
第十一篇:《性能压测基础:JMeter线程模型与压测策略设计》
java·开发语言·jmeter
澈2072 小时前
二叉搜索树:高效增删查的秘诀
java·开发语言·算法
青云计划2 小时前
Spring
java·后端·spring
yychen_java2 小时前
深度解析电力交易系统的“硬核”战场
java·能源