概述
信息媒体的数字化为信息的存取提供了极大的方便,越来越多的业务现在都是基于网络信息完成的。与此同时,信息的泄露,篡改,盗版等也困扰这很多公司以及个人。那么如何降低这些风险或者说泄露了信息如何溯源呢?数字水印技术则在这方面提供了一系列追溯的功能,可以追溯信息在那个环节泄露。
数字水印技术由很多,基于多媒体图片,音频以及视频等技术研究比较深入,受限制于文本的特性,单独对文本的数字水印技术研究比较少,一般需要结合特定的文本格式进行解析。
不可见编码技术
Unicode 中有一类格式字符,不可见,不可打印,主要作用于调整字符的显示格式,所以我们将其称为零宽字符。
- 零宽度空格符 (zero-width space) U+200B : 用于较长单词的换行分隔
- 零宽度非断空格符 (zero width no-break space) U+FEFF : 用于阻止特定位置的换行分隔
- 零宽度连字符 (zero-width joiner) U+200D : 用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果
- 零宽度断字符 (zero-width non-joiner) U+200C :用于阿拉伯文,德文,印度语系等文字中,阻止会发生连字的字符间的连字效果
- 左至右符 (left-to-right mark) U+200E :用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语),规定排版文字书写方向为左至右
- 右至左符 (right-to-left mark) U+200F : 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左
我们可以使用零宽字符的特性对文本进行水印加密。
代码
public class WatermarkUtils { static int WATERMARK_POS_NONE = 0; static int WATERMARK_POS_HEAD = 1; static int WATERMARK_POS_TAIL = 2; /** * 字符串编码 * * @param input * @return */ public static String encode(String input) { //将字符串转换成二进制字符串,以空格相隔 String binary = strToBinary(input); //二进制字符串转换为零宽字符 String result = binaryToZeroWidth(binary); return result; } /** * 将字符串转换成二进制字符串,以空格相隔 * * @param input * @return */ private static String strToBinary(String input) { char[] strChar = input.toCharArray(); String result = ""; String tmp = ""; for (int i = 0; i < strChar.length; i++) { tmp = Integer.toBinaryString(strChar[i]); while (tmp.length() < 16) { tmp = "0" + tmp; } result += tmp; result += " "; } return result.trim(); } /** * 二进制字符串转换为零宽字符 * * @param input * @return */ private static String binaryToZeroWidth(String input) { String[] stringArray = input.split(" "); String result = ""; for (int i = 0; i < stringArray.length; i++) { for (int j = 0; j < stringArray[i].length(); j++) { //数字转换 int num = Integer.parseInt(stringArray[i].charAt(j) + ""); if (num == 1) { // \u200b 零宽度字符(zero-width space) result += '\u200b'; } else if (num == 0) { // \u200c 零宽度断字符(zero-width non-joiner) result += '\u200c'; } else { // \u200d 零宽度连字符 (zero-width joiner) result += '\u200d'; } // \ufeff 零宽度非断空格符 (zero width no-break space) result += '\ufeff'; } } return result; } /** * 添加水印数据 * * @param src 源文件字符串 * @param watermark 水印 * @param pos * @return */ public static String addWatermark(String src, String watermark, int pos) { if (pos == WATERMARK_POS_HEAD) { return watermark + src; } else if (pos == WATERMARK_POS_TAIL) { return src + watermark; } return src; } /** * 提取水印数据 * * @param input * @param pos * @return */ public static String extractWatermark(String input, int pos) { String watermark = ""; if (pos == WATERMARK_POS_HEAD) { for (int i = 0; i < input.length(); i++) { if (input.charAt(i) != '\u200b' && input.charAt(i) != '\u200c' && input.charAt(i) != '\u200d' && input.charAt(i) != '\ufeff') { watermark = input.substring(0, i); break; } } } else if (pos == WATERMARK_POS_TAIL) { for (int i = input.length() - 1; i >= 0; i--) { if (input.charAt(i) != '\u200b' && input.charAt(i) != '\u200c' && input.charAt(i) != '\u200d' && input.charAt(i) != '\ufeff') { watermark = input.substring(i + 1); break; } } } return watermark; } /** * 零宽字符解码 * * @param input * @return */ public static String decode(String input) { String binary = zeroWidthToBinary(input); String result = binaryToStr(binary); return result; } /** * 零宽字符转二进制 * * @param input * @return */ private static String zeroWidthToBinary(String input) { String result = ""; String[] binaryStr = input.split("\ufeff"); for (int i = 0; i < binaryStr.length; i++) { if (binaryStr[i].equals("\u200B")) { result += "1"; } else if (binaryStr[i].equals("\u200C")) { result += "0"; } if ((i + 1) % 16 == 0) { result += " "; } } return result; } /** * 将以空格相隔的二进制转换为字符串 * * @param input * @return */ private static String binaryToStr(String input) { String[] tempStr = input.split(" "); char[] tempChar = new char[tempStr.length]; for (int i = 0; i < tempStr.length; i++) { tempChar[i] = binstrToChar(tempStr[i]); } return String.valueOf(tempChar); } /** * 将二进制转换成字符 * * @param binStr * @return */ private static char binstrToChar(String binStr) { int[] temp = binstrToIntArray(binStr); int sum = 0; for (int i = 0; i < temp.length; i++) { sum += temp[temp.length - 1 - i] << i; } return (char) sum; } /** * 将二进制字符串转换成int数组 * * @param binStr * @return */ private static int[] binstrToIntArray(String binStr) { char[] temp = binStr.toCharArray(); int[] result = new int[temp.length]; for (int i = 0; i < temp.length; i++) { result[i] = temp[i] - 48; } return result; } }
测试
public static void main(String[] args) { String watermarkSrc = "javakf"; String input = "测试字符串添加不可见文本水印"; System.out.println("加水印前:\"" + input + "\",长度:" + input.length()); // 字符串编码 String encode = WatermarkUtils.encode(watermarkSrc); //添加水印数据 String output = WatermarkUtils.addWatermark(input, encode, WatermarkUtils.WATERMARK_POS_HEAD); System.out.println("加水印后:\"" + output + "\",长度:" + output.length()); //提取水印数据 String decode = WatermarkUtils.extractWatermark(output, WatermarkUtils.WATERMARK_POS_HEAD); //零宽字符解码 String watermark = WatermarkUtils.decode(decode); System.out.println("水印内容:" + watermark); }
执行后输出,单纯从文本上看是不是没有变化呢,不过长度有变化,水印已经嵌入文本中了。
加水印前:"测试字符串添加不可见文本水印",长度:14 加水印后:"测试字符串添加不可见文本水印",长度:206 水印内容:javakf
把输出的文本复制到sojson中,可以看到水印的占位。