上个月在千里码刷题的时候,碰到了比较有意思的一道题—— 隐写术。既然感觉有意思,又很久没有玩过 canvas,所以今天结合这两块内容带大家探索一下。
隐写术算是一种加密技术,权威的 wiki 说法是“ 隐写术是一门关于信息隐藏的技巧与科学,所谓信息隐藏指的是不让除预期的接收者之外的任何人知晓信息的传递事件或者信息的内容。” 这看似高大上的定义,并不是近代新诞生的技术,早在 13 世纪末德国人 Trithemius 就写出了《隐写术》的著作,学过密码学的同学可能知道。好了,说了这么多,隐写术到底是什么技术,让我们看一个例子。
下面是一张看似普通的图片,但其中却藏有另一个肉眼无法识别的图像哦。
这是如果把上图每个色彩空间和数字 3 进行逻辑与运算,再把亮度增强 85 倍,可以得到下图。
简单的说,上述的处理过程可以理解为对图片像素的处理,也就是说,加密的信息散布在每个像素点上。可是,13 世纪还没有“ 像素” 这个概念吧?!没错,上面这个例子只是隐写术的一个现代技术实现,隐藏信息的手段有很多,我们日常的钞票防伪也算是隐写术的一种,所以标题上也限定了我们的讨论范围—— 图片隐写术。
(电子水印与隐写术有一些共通点)
聚焦到载体为图片的隐写术,一起来从前端角度分析其技术原理。
我们知道图片的像素信息里存储着 RGB 的色值,R、G、B 分别为该像素的红、绿、蓝通道,每个通道的分量值范围在 0~255,16 进制则是 00~FF。在 CSS 中经常使用其 16 进制形式,比如指定博客头部背景色为 #A9D5F4。其中 R(红色)的 16 进制值为 A9,换算成十进制为 169。这时候,对 R 分量的值+1,即为 170,整个像素 RGB 值为 #AAD5F4,别说你看不出差别,就连火眼金金的“ 像素眼” 设计师都察觉不出来呢。于此同时,修改 G、B 的分量值,也是我们无法察觉的。因此可以得出重要结论:RGB 分量值的小量变动,是肉眼无法分辨的,不影响对图片的识别。
有了这个结论,那就给我们了利用空间,常用手段的就是对二进制最低位进行操作,下面就用 canvas 来演示一下。
解开图中的秘密
这是一张我们当家美女小兰师姐的照片,为了让例子足够简单,里面的 R 通道分量被我加入了文本信息,想知道其中的信息,可以跟我用 canvas 代码来解开。
首先在页面加入一个 canvas 标签,并获取到其上下文。
1 |
<canvas id="canvas" width="256" height="256"></canvas> |
1 |
var ctx = document.getElementById('canvas').getContext('2d'); |
接着将图片先绘制在画布上,然后获取其像素数据。
1 2 3 4 5 6 7 8 9 |
var img = new Image(); var originalData; img.onload = function() { ctx.drawImage(img, 0, 0); // 获取指定区域的canvas像素信息 originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); console.log(originalData); }; img.src = 'xiaolan.png'; |
打印出数据,会看到有一个非常大的数组。
这个一维数组存储了所有的像素信息,一共有 256 * 256 * 4 = 262144 个值。其中 4 个值一组,为什么呢?在浏览器中解析图片,除了 RGB 值外,每组第 4 个值为透明度值,即像素信息实际为大家熟知的 rgba 值。
这里的解密规则是对 R 通道进行处理,R 的分量最低位为 1 则该像素设为红色,R 的分量最低位为 0 则该像素设为黑色,直接看代码实现,完成后我们再绘制到 canvas,即可看到结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var processData = function(originalData){ var data = originalData.data; for(var i = 0; i < data.length; i++){ if(i % 4 == 0){ // 红色分量 if(data[i] % 2 == 0){ data[i] = 0; } else { data[i] = 255; } } else if(i % 4 == 3){ // alpha通道不做处理 continue; } else { // 关闭其他分量,不关闭也不影响答案,甚至更美观 o(^▽^)o data[i] = 0; } } // 将结果绘制到画布 ctx.putImageData(originalData, 0, 0); } |
在 img onload 事件中调用 processData 方法,就可以看到结果啦。
得到的结果可能是这个样子的。
在图片中隐藏信息
讲了基础的解密过程,再来反向说说加密过程。
既然要在图片中加入文字信息,那么首先要获取文字的像素信息,这里我先用 canvas 在画布上打印文字,获取像素信息。
1 2 3 4 5 |
var textData; // 这些canvas API,好久没用,需要查API文档了T_T ctx.font = '30px Microsoft Yahei'; ctx.fillText('广告位招租u', 60, 130); textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data; |
先保存文字的像素信息,接着加载图片获取其像素信息,然后对两组像素进行处理,我在这里抽离了一个公共方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
var mergeData = function(newData, color){ var oData = originalData.data; var bit, offset; // offset的作用是找到alpha通道值,这里需要大家自己动动脑筋 switch(color){ case 'R': bit = 0; offset = 3; break; case 'G': bit = 1; offset = 2; break; case 'B': bit = 2; offset = 1; break; } for(var i = 0; i < oData.length; i++){ if(i % 4 == bit){ // 只处理目标通道 if(newData[i + offset] === 0 && (oData[i] % 2 === 1)){ // 没有信息的像素,该通道最低位置0,但不要越界 if(oData[i] === 255){ oData[i]--; } else { oData[i]++; } } else if (newData[i + offset] !== 0 && (oData[i] % 2 === 0)){ // // 有信息的像素,该通道最低位置1,可以想想上面的斑点效果是怎么实现的 if(oData[i] === 255){ oData[i]--; } else { oData[i]++; } } } } ctx.putImageData(originalData, 0, 0); } |
上述代码做的是,接受要隐藏的数据以及隐藏的颜色通道,然后对原图进行操作,修改图片该通道分量的最低位,如果有文字信息,则最低位置为 1,否则为 0。从最文章开头的结论知道,RGB 的三个通道可以分别隐藏不同信息。
在 img.onload 中调用 mergeData(textData, 'R'),处理好图像后,只要在浏览器中的 canvas 上右键保存图片即可。
这里的例子比较简单,只展示了基本的最低位隐藏文本信息,像二维码这些简单图形也可以这么处理。现实中隐藏画中画则需要更专业的图像处理算法,这里就不再展开了。
应用价值
图片隐写术的应用价值很广泛,比如程序员之间的表白(不限男女),不失为一种浪漫的方式~
上面的案例中我没有放出师姐的原片,这意味着如果盗用上面的图片,我是有办法识别出来的,起到了简单的一种签名作用。当然你也有办法消除掉里面的信息,而前提是你需要知道我的加密方式,可是实际应用中绝不会这么简单哦。有个成功案例就是大众点评通过这种方式,成功证明食神 app 对其图片的盗用,为自己的合法权益进行了有效维护。
好的,感谢阅读到最后,作为回报,我将福利隐藏在了师姐的图片中,请自行发现吧~
阿林十一 2017 年 11 月 14 日
截屏怎么办 – –
李白 2017 年 3 月 30 日
找外包,想要稳定靠谱、费用还低的外包商?难!
怕被坑?上空心 www.kxhtml.com 一家 100 元/页的软件开发云平台!
在招人,海招海筛、培训,到头来上手还是慢!
用结果打脸!上空心 www.kxhtml.com 一家先看开发结果后付费的平台!
想创业,有 idea? 到处找 CTO? 技术难关攻不破?
立即上线!上空心 www.kxhtml.com 一家开发神速火箭般输出页面的平台!
xxx 2017 年 3 月 11 日
保护版权?盗用的人只要转个图像格式,数据就废掉。
前端的一周mark(2016-03-28~2016-04-03 – 项目经验积累与分享 2016 年 12 月 19 日
[…] 图片隐写术 […]
漂亮的夜猫 2016 年 11 月 14 日
那个斑点是怎么来的啊
碧青 2016 年 12 月 1 日
着色的时候做个随机过程
李剑 2016 年 10 月 18 日
这么漂亮还当程序员啊,好想来哦
xx 2016 年 8 月 7 日
加密后的图像直接在 canvas 上显示 然后我用 todataurl() 把 canvas 上已经加密的内容保存成图片再对图片进行解密 结果是全红色的我想问下大神 用 todataurl() 为什么会出现我描述的情况 canvas 的内容还有什么办法压缩保存为图片 使原本的 canvas 内容不会被改变 并且可以解密成功
急需 2016 年 8 月 4 日
originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); 报错了 Failed to execute ‘getImageData’ on ‘CanvasRenderingContext2D’: The canvas has been tainted by cross-origin data.
苏孟梁 2016 年 7 月 12 日
sheranli 目前单身 wlecome to AlloyTeam(是单身女生?)
碧青 2016 年 6 月 15 日
如果图片有跨域是需要的,我测试时是同域的哈
小意 2016 年 5 月 12 日
如果想像你一样实现 3 个颜色通道均加入信息,代码里面我的改正是在 mergeData 方法中加入回调函数 var mergeData = function(newData, color, callback){} 每次处理完一个通道后,得到的 orginalData;然后回调函数中再写入新的通道的信息,调用 mergeData 方法,可是实际解码除了 R 通道,其他不对……是我回调函数写的不对?
碧青 2016 年 5 月 13 日
有没有你完整一些的代码给我看看?
Dont 2016 年 5 月 9 日
最后一个判断:oData % 2 === 0 既然都能被 2 整除了,那不大可能===255 了吧
碧青 2016 年 5 月 9 日
是的,很细心
于枫 2016 年 5 月 4 日
怎么叫盗用你师姐的图片?你又怎么识别的呢?
Farris 2016 年 4 月 11 日
welcome to AlloyTeam [给力]
啥东西~ 2016 年 4 月 9 日
好像 mergeData 的方法并不能成功,解密的可以出来~
在线工具 2016 年 4 月 9 日
同上,merge 加密出来的图片,使用 processData 方法解不出来,就是图像变红了~
碧青 2016 年 4 月 10 日
确认下图片是否又被压缩过?
在线工具 2016 年 4 月 10 日
没有,我完全按照文章中的代码写的,解密大师姐出来了,加密图片不行。
我看了另外一个 github 上算法可以:http://www.atool.org/steganography.php
xoyoz 2016 年 4 月 6 日
我也单身 [害羞]
朱雀 2016 年 4 月 5 日
妹子我预定了
小逝水 2016 年 4 月 4 日
我好懒,如果图片压缩一下,会造成解密不出来吗,比如有损压缩。明天试试
碧青 2016 年 4 月 7 日
会的,这个很容易理解的哈
哈哈 2016 年 3 月 31 日
又是你。。。
碧青 2016 年 4 月 7 日
我怎么了?
小盘鸡同学 2016 年 3 月 29 日
这么漂酿,我才不信单身呢!!!
碧青 2016 年 3 月 30 日
少年你知道的太多了 [嘻嘻]