前端也能玩的图片隐写术

不能说的秘密——前端也能玩的图片隐写术

上个月在千里码刷题的时候,碰到了比较有意思的一道题—— 隐写术。既然感觉有意思,又很久没有玩过 canvas,所以今天结合这两块内容带大家探索一下。

隐写术算是一种加密技术,权威的 wiki 说法是“ 隐写术是一门关于信息隐藏的技巧与科学,所谓信息隐藏指的是不让除预期的接收者之外的任何人知晓信息的传递事件或者信息的内容。” 这看似高大上的定义,并不是近代新诞生的技术,早在 13 世纪末德国人 Trithemius 就写出了《隐写术》的著作,学过密码学的同学可能知道。好了,说了这么多,隐写术到底是什么技术,让我们看一个例子。

下面是一张看似普通的图片,但其中却藏有另一个肉眼无法识别的图像哦。

 

这是如果把上图每个色彩空间和数字 3 进行逻辑与运算,再把亮度增强 85 倍,可以得到下图。

简单的说,上述的处理过程可以理解为对图片像素的处理,也就是说,加密的信息散布在每个像素点上。可是,13 世纪还没有“ 像素” 这个概念吧?!没错,上面这个例子只是隐写术的一个现代技术实现,隐藏信息的手段有很多,我们日常的钞票防伪也算是隐写术的一种,所以标题上也限定了我们的讨论范围—— 图片隐写术。

{15AAFDB9-59F0-458E-93EF-AE4E799FA5D2}

(电子水印与隐写术有一些共通点)

聚焦到载体为图片的隐写术,一起来从前端角度分析其技术原理。

我们知道图片的像素信息里存储着 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 标签,并获取到其上下文。

 

 

 

 

接着将图片先绘制在画布上,然后获取其像素数据。

 

 

打印出数据,会看到有一个非常大的数组。

 

QQ截图20160325174947

这个一维数组存储了所有的像素信息,一共有 256 * 256 * 4 = 262144 个值。其中 4 个值一组,为什么呢?在浏览器中解析图片,除了 RGB 值外,每组第 4 个值为透明度值,即像素信息实际为大家熟知的 rgba 值。 

这里的解密规则是对 R 通道进行处理,R 的分量最低位为 1 则该像素设为红色,R 的分量最低位为 0 则该像素设为黑色,直接看代码实现,完成后我们再绘制到 canvas,即可看到结果。

 

 

在 img onload 事件中调用 processData 方法,就可以看到结果啦。

得到的结果可能是这个样子的。

result

在图片中隐藏信息

讲了基础的解密过程,再来反向说说加密过程。

既然要在图片中加入文字信息,那么首先要获取文字的像素信息,这里我先用 canvas 在画布上打印文字,获取像素信息。

 

 

先保存文字的像素信息,接着加载图片获取其像素信息,然后对两组像素进行处理,我在这里抽离了一个公共方法。

 

 

上述代码做的是,接受要隐藏的数据以及隐藏的颜色通道,然后对原图进行操作,修改图片该通道分量的最低位,如果有文字信息,则最低位置为 1,否则为 0。从最文章开头的结论知道,RGB 的三个通道可以分别隐藏不同信息。

在 img.onload 中调用 mergeData(textData, \’R\’),处理好图像后,只要在浏览器中的 canvas 上右键保存图片即可。

这里的例子比较简单,只展示了基本的最低位隐藏文本信息,像二维码这些简单图形也可以这么处理。现实中隐藏画中画则需要更专业的图像处理算法,这里就不再展开了。

应用价值

图片隐写术的应用价值很广泛,比如程序员之间的表白(不限男女),不失为一种浪漫的方式~

上面的案例中我没有放出师姐的原片,这意味着如果盗用上面的图片,我是有办法识别出来的,起到了简单的一种签名作用。当然你也有办法消除掉里面的信息,而前提是你需要知道我的加密方式,可是实际应用中绝不会这么简单哦。有个成功案例就是大众点评通过这种方式,成功证明食神 app 对其图片的盗用,为自己的合法权益进行了有效维护。

 

好的,感谢阅读到最后,作为回报,我将福利隐藏在了师姐的图片中,请自行发现吧~

 

出处:http://www.alloyteam.com/2016/03/image-steganography/

=======================================================================================

从破解某设计网站谈前端水印(详细教程)

@charset “UTF-8”;
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; overflow-x: hidden; color: rgba(43, 43, 43, 1); font-family: -apple-system, system-ui, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; background-image: linear-gradient(90deg, rgba(159, 219, 252, 0.15) 3%, rgba(0, 0, 0, 0) 0), linear-gradient(1turn, rgba(159, 219, 252, 0.15) 3%, rgba(0, 0, 0, 0) 0); background-size: 20px 20px; background-position: center }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { padding: 30px 0; margin-top: 35px; margin-bottom: 10px; color: rgba(77, 208, 225, 1) }
.markdown-body h1 { font-size: 30px; text-align: center; position: relative; width: max-content; margin: 0 auto }
.markdown-body h1:before { position: absolute; content: “”; z-index: -1; top: -20px; height: 100%; width: 100px; left: 0; right: 0; margin: 0 auto; background: url(“data:image/png;base64,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”) center / 64px 64px no-repeat; opacity: 0.84 }
.markdown-body h1:after { position: absolute; content: “”; width: 150%; left: -25%; height: 50%; bottom: 12px; border-radius: 50%; background: linear-gradient(rgba(0, 0, 0, 0) 80%, rgba(77, 208, 225, 0.8)); opacity: 0.6; animation: 6s linear infinite h1animate }
@keyframes h1Animate { 0% { background-position: right bottom } 50% { background-position: right } 100% { background-position: right bottom } }
.markdown-body h2 { display: block; border-bottom: 4px solid rgba(77, 208, 225, 1); position: relative; font-size: 24px; padding: 12px 32px; margin: 30px 0 }
.markdown-body h2:before { width: 24px; height: 24px; left: 0; top: 0; margin: auto; background-size: 24px 24px; background-image: url(“data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADGklEQVRYR81X32vTYBQ999s6mFjQgQ+DrbHiVFZYU4cDcQ/6pGhTFVYFEXGi82H+Bz448UnEF1Fx9ccEEcXpZE3d5tP2ooKiTacTHaLNpigMHDgnU9tcSbrWrkwWR0sbyEOSe885ObnfvV8IRT6oyPwoLQHBx+OVM5WJvSyEVAhnBOjt7yU/+/rr6r6l8TMO+F/EN0JQhICqQpD/xaRpcpAc9tS+M+9lBCia/oqBamK+zeDuQogQZaKJk3wcQjxSva7tGQGB2Ke1zIk3DNyMyNL+QpCnMQOaPsDAVuGAp9cjvbYc8Ec/bCYSg0zoiHilk1tHxqsqEsYlML4kjIpT/eurJxRNPweQU5VdrWaOEo1fgKAVbBgXIz73kF3R/ph+ghgdzMYWM29eAWlBJqgZaFlFYtC6nhWpaDqnSGlIlV1WjJ3DloDNgyNLncudqgX//Ucg3LxuStHGuhi8pqKCW3rqV342rwFjRznKm+/LNaN2yC237ThgF2wxcfMLeP6+ncrKzoPoKTGeLQbYbg4TNoC5iZPJY5HGVRdSNZAWYBclD3FzBQzrR8hACAKdzBzKA/4/IYioDQaOskBbpEG6PO8qKKSAEi3CnEb0Pw4oMf0OmKbTDWqh3Lw6EIiNBZi5lxh3wz4puBD5ovqAMvxhHSdFKxE1CQe3m/07TeTX4lcJdAhE+1Sv65Z5P/ByvIGTRowIZ9igbtXnmrOsbTvgj+kHBNMuBu9OdVw8EeU4nC1A0cYmAHZOTRrLhra4Z8ywnSN6vZHAFTA2WnnMfQB3qz73ddsOZM8CACFDIPSgQXqebXEgqgeZcAeEe6pXasm1f8ew3igMtAHWac0Uc/jYdyAaP0xEBwFsmgUPqbJ0NE2UKj4EGcahiOzuyhagaHpnmtgcVgTcCMuua7YdyAHbA3ArQNscVFbb4635aD6fnYaTvxxi9UNP7ddMXaRWVBdAcaLk6bDXPZCNZ9uBXEsDUX1T2Cc9yjig6Z0EHg3LK8/aqf6MwJKchkXfks1+0+JtSq3qLPa23BRR1B+T/6nkfMaW1r9hPt/MLtYfTLEpP+T9FNoAAAAASUVORK5CYII=”) }
.markdown-body h2:after, .markdown-body h2:before { content: “”; display: block; position: absolute; bottom: 0 }
.markdown-body h2:after { right: 0; width: 400px; height: 10px; border-top-right-radius: 24px; background: linear-gradient(90deg, rgba(255, 255, 255, 1), rgba(77, 208, 225, 1)); max-width: 50vw }
.markdown-body h3 { margin: 30px 0; font-size: 18px; position: relative; padding: 4px 32px; width: max-content }
.markdown-body h3:before { border-bottom: 2px solid rgba(77, 208, 225, 1); width: 100%; content: “”; display: block; height: 28px; position: absolute; left: 0; top: 0; bottom: -2px; margin: auto; background-size: 28px 28px; background-image: url(“data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABRklEQVRYR2NkGGDAOMD2M4w6YDQERkNg+ITAppcfY/8zMv3wF+NdTUrZQpUQ2PT6cz8Dw/8CkMWMDIwNvqK8jcQ6gmIHNN19EaXPx1XPyMCghrCUKcpPlGc5MY6gyAE+Fx52MjL8j3cU5a1UYWXtZGBkEAVb+p8hxU+Mby5NHQCxnKEMaskzJ37uFmUetkmMjAzrfUX4woixHBJlZAA0y2EmPPYU4enLkhGeQIqRJDsAh+UgO7duNpD3IcVykkOA2paT5ABaWE60A2hlOdEO8D3/4CMDIyMfWvySFefoaYSoROh74eFXBgYGLiTNVLGc+BC48PAnAwMDG9QBVLOcaAd8P5ox+x/jf5AjGLgYfnwnKqv9/8/PwPO/kFF/MSj0cAKiouD/0bgYoixFU8RovWgJIX1EOYCQIZTIjzpgNARGQ2DAQwAAvHBaIdB7zxsAAAAASUVORK5CYII=”); background-repeat: no-repeat; animation: 2s infinite alternate h3animationbefore }
@keyframes h3AnimationBefore { 0% { width: 28px } 25% { width: 100% } 50% { width: 100% } 100% { width: 100% } }
.markdown-body h3:after { content: “”; display: block; width: 28px; height: 28px; position: absolute; border: 2px solid rgba(77, 208, 225, 1); border-radius: 50%; right: -15px; top: 0; bottom: 0; margin: auto; background-size: 28px 28px; background-image: url(“data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABRklEQVRYR2NkGGDAOMD2M4w6YDQERkNg+ITAppcfY/8zMv3wF+NdTUrZQpUQ2PT6cz8Dw/8CkMWMDIwNvqK8jcQ6gmIHNN19EaXPx1XPyMCghrCUKcpPlGc5MY6gyAE+Fx52MjL8j3cU5a1UYWXtZGBkEAVb+p8hxU+Mby5NHQCxnKEMaskzJ37uFmUetkmMjAzrfUX4woixHBJlZAA0y2EmPPYU4enLkhGeQIqRJDsAh+UgO7duNpD3IcVykkOA2paT5ABaWE60A2hlOdEO8D3/4CMDIyMfWvySFefoaYSoROh74eFXBgYGLiTNVLGc+BC48PAnAwMDG9QBVLOcaAd8P5ox+x/jf5AjGLgYfnwnKqv9/8/PwPO/kFF/MSj0cAKiouD/0bgYoixFU8RovWgJIX1EOYCQIZTIjzpgNARGQ2DAQwAAvHBaIdB7zxsAAAAASUVORK5CYII=”); animation: 2s infinite alternate h3animationafter }
@keyframes h3AnimationAfter { 0% { } 10% { } 50% { transform: rotate(-1turn) } 100% { transform: rotate(-1turn) } }
.markdown-body h4 { font-size: 16px }
.markdown-body h5 { font-size: 15px }
.markdown-body h6 { margin-top: 5px }
.markdown-body p { line-height: inherit; margin: 22px 0; letter-spacing: 2px; font-size: 14px; word-spacing: 2px }
.markdown-body img { max-width: 80%; border-radius: 6px; display: block; margin: 20px auto !important; object-fit: contain; box-shadow: 0 0 16px rgba(110, 110, 110, 0.45) }
.markdown-body figcaption { display: block; font-size: 13px; color: rgba(43, 43, 43, 1) }
.markdown-body figcaption:before { content: “”; background-image: url(“data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAGFBMVEVHcExAuPtAuPpAuPtAuPpAuPtAvPxAuPokzOX5AAAAB3RSTlMAkDLqNegkoiUM7wAAAGBJREFUKM9jYBhcgMkBTUDVBE1BeDGqEtXychNUBeXlKEqACsrLQxB8lnCQQClCiWt5OYoSiAIkJVAF5eVBqAqAShTAAs7l5ShKWMwRAmAlSArASpAVgJUkCqIAscESHwCVVjMBK9JnbQAAAABJRU5ErkJggg==”); display: inline-block; width: 18px; height: 18px; background-size: 18px; background-repeat: no-repeat; background-position: center; margin-right: 5px; margin-bottom: -5px }
.markdown-body hr { border-top: 1px solid rgba(77, 208, 225, 1); border-right: none; border-bottom: none; border-left: none; margin-top: 32px; margin-bottom: 32px }
.markdown-body del { color: rgba(77, 208, 225, 1) }
.markdown-body code { border-radius: 2px; overflow-x: auto; background-color: rgba(77, 208, 225, 0.08); color: rgba(38, 198, 218, 1); padding: 0.195em 0.4em }
.markdown-body pre { font-family: Menlo, Monaco, Consolas, Courier New, monospace; overflow: auto; position: relative; line-height: 1.75; box-shadow: 0 0 8px rgba(110, 110, 110, 0.45); border-radius: 4px; margin: 16px }
.markdown-body pre:before { content: “”; display: block; height: 30px; width: 100%; margin-bottom: -7px; background: url(“data:image/png;base64,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”) 10px 10px / 40px no-repeat }
.markdown-body pre>code { font-size: 12px; padding: 15px 12px; margin: 0; word-break: normal; display: block; overflow-x: auto; color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1) }
.markdown-body a { color: rgba(77, 208, 225, 1); border-bottom: 1px solid rgba(77, 208, 225, 1); font-weight: 400; text-decoration: none; margin: 0 4px }
.markdown-body a:active, .markdown-body a:hover { background-color: rgba(77, 208, 225, 0.1) }
.markdown-body strong { color: rgba(38, 198, 218, 1) }
.markdown-body strong:before { content: “「” }
.markdown-body strong:after { content: “」” }
.markdown-body em { font-style: normal; color: rgba(77, 208, 225, 1); font-weight: 700 }
.markdown-body table { display: inline-block !important; font-size: 12px; width: auto; max-width: 100%; overflow: auto; border: 1px solid rgba(246, 246, 246, 1) }
.markdown-body thead { background: rgba(246, 246, 246, 1); color: rgba(0, 0, 0, 1); text-align: left }
.markdown-body tr:nth-child(2n) { background-color: rgba(77, 208, 225, 0.05) }
.markdown-body td, .markdown-body th { padding: 12px 7px; line-height: 24px }
.markdown-body td { min-width: 120px }
.markdown-body blockquote { margin: 2em 0; padding: 24px 32px; border-left: 4px solid rgba(38, 198, 218, 1); background: rgba(77, 208, 225, 0.15); position: relative }
.markdown-body blockquote:before { content: “❝”; top: 8px; left: 8px; color: rgba(77, 208, 225, 1); font-size: 30px; line-height: 1; font-weight: 700; position: absolute; opacity: 0.7 }
.markdown-body blockquote:after { content: “❞”; font-size: 30px; position: absolute; right: 8px; bottom: 0; color: rgba(77, 208, 225, 1); opacity: 0.7 }
.markdown-body blockquote p { color: rgba(89, 89, 89, 1); line-height: 2 }
.markdown-body ol, .markdown-body ul { color: rgba(89, 89, 89, 1); padding-left: 28px }
.markdown-body ol li, .markdown-body ul li { margin-bottom: 0 }
.markdown-body ol li .task-list-item, .markdown-body ul li .task-list-item { list-style: none }
.markdown-body ol li .task-list-item ol, .markdown-body ol li .task-list-item ul, .markdown-body ul li .task-list-item ol, .markdown-body ul li .task-list-item ul { margin-top: 0 }
.markdown-body ol ol, .markdown-body ol ul, .markdown-body ul ol, .markdown-body ul ul { margin-top: 3px }
.markdown-body ol li { padding-left: 6px }
@media (max-width: 720px) { .markdown-body h1 { font-size: 24px } .markdown-body h2 { font-size: 20px } .markdown-body h3 { font-size: 18px } }

前言

最近在写公众号的时候,常常会自己做首图,并且慢慢地发现沉迷于制作首图,感觉扁平化的设计的真好好看。慢慢地萌生了一个做一个属于自己的首图生成器的想法。

制作呢,当然也不是拍拍脑袋就开始,在开始之前,就去研究了一下某在线设计网站(如果有人不知道的话,可以说一下,这是一个在线制作海报之类的网站 T T 像我们这种内容创作者用的比较多),毕竟人家已经做了很久了,我只是想做个方便个人使用的。毕竟以上用 PS 做着还是有一些废时间,由于组成的元素都很简单,做一个自动化生成的完全可以。

但是研究着研究着,就看到了某在线设计网站的水印,像这种技术支持的网站,最重要的防御措施就是水印了,水印能够很好的保护知识产权。

慢慢地路就走偏了,开始对它的水印感兴趣了。不禁发现之前只是大概知道水印的生成方法,但是从来没有仔细研究过,本文将以以下的路线进行讲解。以下所有代码示例均在

github.com/hua1995116/…

watermark-simple

明水印

水印(watermark)是一种容易识别、被夹于内,能够透过光线穿过从而显现出各种不同阴影的技术。

水印的类型有很多,有一些是整图覆盖在图层上的水印,还有一些是在角落。

那么这个水印怎么实现呢?熟悉 PS 的朋友,都知道 PS 有个叫做图层的概念。

网页也是如此。我们可以通过绝对定位,来将水印覆盖到我们的页面之上。

image-20201123230659874

最终变成了这个样子。

1606144217031

等等,但是发现少了点什么。直接覆盖上去,就好像是一个蒙层,我都知道这样是无法触发底下图层的事件的,此时就要介绍一个css属性pointer-events

pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target

当它的被设置为 none 的时候,能让元素实体虚化,虽然存在这个元素,但是该元素不会触发鼠标事件。详情可以查看 CSS3 pointer-events:none应用举例及扩展 « 张鑫旭-鑫空间-鑫生活

这下理清了实现原理,等于成功了一半了!

明水印的生成

明水印的生成方式主要可以归为两类,一种是 纯 html 元素(纯div),另一种则为背景图(canvas/svg)。

下面我分别来介绍一下,两种方式。

div实现

我们首先来讲比较简单的 div 生成的方式。就按照我们刚才说的。

// 文本内容
<div class="app">
        <h1>秋风</h1>
        <p>hello</p>
</div>
复制代码

首先我们来生成一个水印块,就是上面的 一个个秋风的笔记。这里主要有一点就是设置一个透明度(为了让水印看起来不是那么明显,从而不遮挡我们的主要页面),另一个就是一个旋转,如果是正的水平会显得不是那么好看,最后一点就是使用 userSelect 属性,让此时的文字无法被选中。

userSelect

CSS 属性 user-select 控制用户能否选中文本。除了文本框内,它对被载入为 chrome 的内容没有影响。

function cssHelper(el, prototype) {
  for (let i in prototype) {
    el.style[i] = prototype[i]
  }
}
const item = document.createElement(\'div\')
item.innerHTML = \'秋风的笔记\'
cssHelper(item, {
  position: \'absolute\',
  top: `50px`,
  left: `50px`,
  fontSize: `16px`,
  color: \'#000\',
  lineHeight: 1.5,
  opacity: 0.1,
  transform: `rotate(-15deg)`,
  transformOrigin: \'0 0\',
  userSelect: \'none\',
  whiteSpace: \'nowrap\',
  overflow: \'hidden\',
})
复制代码

有了一个水印片,我们就可以通过计算屏幕的宽高,以及水印的大小来计算我们需要生成的水印个数。

const waterHeight = 100;
const waterWidth = 180;
const { clientWidth, clientHeight } = document.documentElement || document.body;
const column = Math.ceil(clientWidth / waterWidth);
const rows = Math.ceil(clientHeight / waterHeight);
for (let i = 0; i < column * rows; i++) {
    const wrap = document.createElement(\'div\');
    cssHelper(wrap, Object.create({
        position: \'relative\',
        width: `${waterWidth}px`,
        height: `${waterHeight}px`,
        flex: `0 0 ${waterWidth}px`,
        overflow: \'hidden\',
    }));
    wrap.appendChild(createItem());
    waterWrapper.appendChild(wrap)
}
document.body.appendChild(waterWrapper)
复制代码

这样子我们就完美地实现了上面我们给出的思路的样子啦。

image-20201130003407877

背景图实现

canvas

canvas的实现很简单,主要是利用canvas 绘制一个水印,然后将它转化为 base64 的图片,通过canvas.toDataURL() 来拿到文件流的 url ,关于文件流相关转化可以参考我之前写的文章一文带你层层解锁「文件下载」的奥秘, 然后将获取的 url 填充在一个元素的背景中,然后我们设置背景图片的属性为重复。

.watermark {
    position: fixed;
    top: 0px;
    right: 0px;
    bottom: 0px;
    left: 0px;
    pointer-events: none;
    background-repeat: repeat;
}
复制代码
function createWaterMark() {
  const angle = -20;
  const txt = \'秋风的笔记\'
  const canvas = document.createElement(\'canvas\');
  canvas.width = 180;
  canvas.height = 100;
  const ctx = canvas.getContext(\'2d\');
  ctx.clearRect(0, 0, 180, 100);
  ctx.fillStyle = \'#000\';
  ctx.globalAlpha = 0.1;
  ctx.font = `16px serif`
  ctx.rotate(Math.PI / 180 * angle);
  ctx.fillText(txt, 0, 50);
  return canvas.toDataURL();
}
const watermakr = document.createElement(\'div\');
watermakr.className = \'watermark\';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr);
复制代码

svg

svg 和 canvas 类似,主要还是生成背景图片。

function createWaterMark() {
  const svgStr =
    `<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="100px">
      <text x="0px" y="30px" dy="16px"
      text-anchor="start"
      stroke="#000"
      stroke-opacity="0.1"
      fill="none"
      transform="rotate(-20)"
      font-weight="100"
      font-size="16"
      >
      	秋风的笔记
      </text>
    </svg>`;
  return `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
}
const watermakr = document.createElement(\'div\');
watermakr.className = \'watermark\';
watermakr.style.backgroundImage = `url(${createWaterMark()})`
document.body.appendChild(watermakr);
复制代码

明水印的破解一

以上就很快实现了水印的几种方案。但是对于有心之人来说,肯定会想着破解,以上破解也很简单。

打开了 Chrome Devtools 找到对应的元素,直接按 delete 即可删除。

image-20201128175505927

明水印的防御

这样子的水印对于大概知道控制台操作的小白就可以轻松破解,那么有什么办法能防御住这样的操作呢?

答案是肯定的,js 有一个方法叫做 MutationObserver,能够监控元素的改动。

MutationObserver 对现代浏览的兼容性还是不错的,MutationObserver是元素观察器,字面上就可以理解这是用来观察Node(节点)变化的。MutationObserver是在DOM4规范中定义的,它的前身是MutationEvent事件,最低支持版本为 ie9 ,目前已经被弃用。

img

在这里我们主要观察的有三点

  • 水印元素本身是否被移除
  • 水印元素属性是否被篡改(display: none …)
  • 水印元素的子元素是否被移除和篡改 (element生成的方式 )

来通过 MDN 查看该方法的使用示例。

const targetNode = document.getElementById(\'some-id\');

// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };

// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
    // Use traditional \'for loops\' for IE 11
    for(let mutation of mutationsList) {
        if (mutation.type === \'childList\') {
            console.log(\'A child node has been added or removed.\');
        }
        else if (mutation.type === \'attributes\') {
            console.log(\'The \' + mutation.attributeName + \' attribute was modified.\');
        }
    }
};

// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
复制代码

MutationObserver主要是监听子元素的改动,因此我们的监听对象为 document.body, 一旦监听到我们的水印元素被删除,或者属性修改,我们就重新生成一个。通过以上示例,加上我们的思路,很快我们就写一个监听删除元素的示例。(监听属性修改也是类似就不一一展示了)

// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional \'for loops\' for IE 11
  for (let mutation of mutationsList) {
    mutation.removedNodes.forEach(function (item) {
      if (item === watermakr) {
      	document.body.appendChild(watermakr);
      }
    });
  }
};
// 监听元素
const targetNode = document.body;
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
复制代码

我们打开控制台来检验一下。

2020-11-28-21.11.25

这回完美了,能够完美抵御一些开发小白了。

那么这样就万无一失了吗?显然,道高一尺魔高一丈,毕竟前端的一切都是不安全的。

明水印的破解二

在一个高级前端工程师面前,一切都是纸老虎。接下来我就随便介绍三种破解的方式。

第一种

打开 Chrome Devtools,点击设置 – Debugger – Disabled JavaScript .

然后再打开页面,delete我们的水印元素。

image-20201128212007999

第二种

复制一个 body 元素,然后将原来 body 元素的删除。

image-20201128212148446

第三种

打开一个代理工具,例如 charles,将生成水印相关的代码删除。

破解实践

接下来我们实战一下,通过预先分析,我们看到某设计网站的内容是以 div 的方式实现的,所以可以利用这种方案。

打开控制台,Ctrl + F 搜索 watermark 相关字眼。(这一步是作为一个程序员的直觉,基本上你要找什么,搜索对应的英文就可以 ~)

image-20201128212425716

很快我们就找到了水印图。发现直接删除,没有办法删除水印元素,根据我们刚才学习的,肯定是利用了MutationObserver 方法。我们使用我们的第一个破解方法,将 JavaScript 禁用,再将元素删除。

水印已经消失了。

但是这样真的就万事大吉了吗?

不知道你有没有听过一种东西,看不见摸不着,但是它却真实存在,他的名字叫做暗水印,我们将时间倒流到 16 年间的月饼门事件,因为有员工将内网站点截图了,但是很快被定位出是谁截图了。

虽然你将一些可见的水印去除了,但是还会存在一些不可见的保护版权的水印。(这就是防止一些坏人拿去作另外的用途)

暗水印

暗水印是一种肉眼不可见的水印方式,可以保持图片美观的同时,保护你的资源版权。

暗水印的生成方式有很多,常见的为通过修改RGB 分量值的小量变动、DWT、DCT 和 FFT 等等方法。

通过介绍前端实现 RGB 分量值的小量变动 来揭秘其中的奥秘,主要参考 不能说的秘密——前端也能玩的图片隐写术 | AlloyTeam

我们都知道图片都是有一个个像素点构成的,每个像素点都是由 RGB 三种元素构成。当我们把其中的一个分量修改,人的肉眼是很难看出其中的变化,甚至是像素眼的设计师也很难分辨出。

image-20201128213551039

你能看出其中的差别吗?根据这个原理,我们就来实践吧。(女孩子可以掌握方法后可以拿以下图片进行试验测试)

qiufeng-super

首先拿到以上图片,我们先来讲解解码方式,解码其实很简单,我们需要创建一个规律,再通过我们的规律去解码。现在假设的规律为,我们将所有像素的 R 通道的值为奇数的时候我们创建的通道密码,举个简单的例子。

image-20201128220542389

例如我们把以上当做是一个图形,加入他要和一个中文的 “一” 放进图像,例如我们将 “一” 放入第二行。按照我们的算法,我们的图像会变成这个样子。

image-20201128220833657

解码的时候,我们拿到所有的奇数像素将它渲染出来,例如这里的 \’5779\’ 是不是正好是一个 “一”,下面就转化为实践。

解码过程

首先创建一个 canvas 标签。

 <canvas id="canvas" width="256" height="256"></canvas>
复制代码
var ctx = document.getElementById(\'canvas\').getContext(\'2d\');
var img = new Image();
var originalData;
img.onload = function () {
  // canvas像素信息
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  console.log()
  processData(ctx, originalData)
};
img.src = \'qiufeng-super.png\';
复制代码

我们打印出这个数组,会有一个非常大的数组,一共有 256 * 256 * 4 = 262144 个值。因为每个像素除了 RGB 外还有一个 alpha 通道,也就是我们常用的透明度。

image-20201128215615494

上面也说了,我们的 R 通道为奇数的时候 ,就我们的解密密码。因此我们只需要所有的像素点的 R 通道为奇数的时候,将它填填充,不为奇数的时候就不填充,很快我们就能得到我们的隐藏图像。

var processData = function (ctx, originalData) {
    var data = originalData.data;
    for (var i = 0; i < data.length; i++) {
        if (i % 4 == 0) {
            // R分量
            if (data[i] % 2 == 0) {
                data[i] = 0;
            } else {
                data[i] = 255;
            }
        } else if (i % 4 == 3) {
            // alpha通道不做处理
            continue;
        } else {
            // 关闭其他分量,不关闭也不影响答案
            data[i] = 0;
        }
    }
    // 将结果绘制到画布
    ctx.putImageData(originalData, 0, 0);
}
processData(ctx, originalData)
复制代码

解密完会出现类似于以下这个样子。

那我们如何加密的,那就相反的方式就可以啦。(这里都用了 不能说的秘密——前端也能玩的图片隐写术 中的例子,= = 我也能写出一个例子,但是觉得没必要,别人已经写得很好了,我们只是讲述这个方法,需要代码来举例而已)

编码过程

加密呢,首先我们需要获取加密的图像信息。

var textData;
var ctx = document.getElementById(\'canvas\').getContext(\'2d\');
ctx.font = \'30px Microsoft Yahei\';
ctx.fillText(\'秋风的笔记\', 60, 130);
textData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
复制代码

然后提取加密信息在待加密的图片上进行处理。

var mergeData = function (ctx, newData, color, originalData) {
    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,可以想想上面的斑点效果是怎么实现的
                oData[i]++;
            }
        }
    }
    ctx.putImageData(originalData, 0, 0);
}
复制代码

主要的思路还是我一开始所讲的,在有像素信息的点,将 R 偶数的通道+1。在没有像素点的地方将 R 通道转化成偶数,最后在 img.onload 调用 processData(ctx, originalData)

img.onload = function () {
  // 获取指定区域的canvas像素信息
  ctx.drawImage(img, 0, 0);
  originalData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  console.log(originalData)
	processData(ctx, originalData)
};
复制代码

以上方法就是一种比较简单的加密方式。以上代码都放到了仓库 watermark/demo/canvas-dark-watermark.html 路径下,方法都封装好了~。

但是实际过程需要更专业的加密方式,例如利用傅里叶变化公式,来进行频域制定数字盲水印,这里就不详细展开讲了,以后研究完再详细讲~

img

破解实践

听完上述的介绍,那么某设计网站是不是很有可能使用了暗水印呢?

当然啦,通过我对某设计网站的分析,我分析了以下几种情况,我们一一来进行测试。

暗水印2

我们先通过免费下载的图片来进行分析。打开 www.xxx.com/design?id=1…

image-20201128230510959

image-20201128230557383

通过实验(实验主要是去分析他各个场景下触发的请求),发现在下载免费图片的时候,发现它都会去向阿里云发送一个 POST 请求,这熟悉的请求域名以及熟悉的数据封装方式,这不就是 阿里云 OSS 客户端上传方式嘛。这就好办了,我们去查询一下阿里云是否有生成暗水印的相关方式,从而来看看某设计网站是否含有暗水印。很快我们就从官方文档搜索到了相关的文档,且对于低 QPS 是免费的。(这就是最好理解的连带效应,例如我们觉得耐克阿迪啥卖运动类服饰,你买了他的鞋子,可能还会想买他的衣服)

image-20201128231110192

const { RPCClient } = require("@alicloud/pop-core");
var client = new RPCClient({
  endpoint: "http://imm.cn-shenzhen.aliyuncs.com",
  accessKeyId: \'xxx\',
  accessKeySecret: \'xxx\',
  apiVersion: "2017-09-06",
});
(async () => {
  try {
        var params = {
          Project: "test-project",
          ImageUri: "oss://watermark-shenzheng/source/20201009-182331-fd5a.png",
            TargetUri: "oss://watermark-shenzheng/dist/20201009-182331-fd5a-out.jpg",
            Model: "DWT"
        };
        var result = await client.request("DecodeBlindWatermark", params);
        
        console.log(result);
      } catch (err) {
        console.log(err);
      }
})()
复制代码

我们写了一个demo进行了测试。由于阿里云含有多种暗水印加密方式,为啥我使用了 DWT 呢?因为其他几种都需要原图,而我们刚才的测试,他上传只会上传一个文件到 OSS ,因此大致上排除了需要原图的方案。

image-20201128231801100

但是我们的结果却没有发现任何加密的迹象。

为什么我们会去猜想阿里云的图片暗水印的方式?因为从上传的角度来考虑,我们上传的图片 key 的地址即是我们下载的图片,也就是现在存在两种情况,一就是通过阿里云的盲水印方案,另一种就是上传前进行了水印的植入。现在看来不是阿里云水印的方案,那么只是能是上传前就有了水印。

这个过程就有两种情况,一是生成的过程中加入的水印,前端加入的水印。二是物料图含有水印。

对于第一种情况,我们可以通过 dom-to-image 这个库,在前端直接进行下载,或者使用截图的方式。目前通过直接下载和通过站点内生成,发现元素略有不同。

image-20201128235427912

第一个为我通过 dom-to-image 的方式下载,第二种为站点内下载,明显大了一些。(有点怀疑他在图片生成中可能做了什么手脚)

但是感觉前端加密的方式比较容易破解,最坏的情况想到了对素材进行了加密,但是这样的话就无从破解了(但是查阅了一些资料,由于某设计稿网站站点素材大多是透明背景的,这种加密效果可能会弱一些,以后牛逼了再来补充)。目前这一块暂时还不清楚,探究止步于此了。

攻击实验

那如果一张图经过暗水印加密,他的抵抗攻击性又是如何呢?

1605680005172-out1

1605680005172-decode2

这是一张通过阿里云 DWT暗水印进行的加密,解密后的样子为”秋风”字样,我们分别来测试一下。

加一些元素

1605680005172-out-el

1605680005172-decode-out-el

结果: 识别效果不错

截图

1605680005172-out-cut1

1605680005172-decode-out-cut1

结果: 识别效果不错

大小变化

1605680005172-out-scale

1605680005172-out-decode-scale

结果:识别效果不错

加蒙层

1605680005172-out-bg

1605680005172-decode-out-bg

结果: 直接就拉胯了。

可见,暗水印的抵抗攻击性还是蛮强的,是一种比较好的抵御攻击的方式~

最后

以上仅仅为技术交流~ 大家不要在实际的场景盲目使用,使用正规的途径 ~ 或者期待一下我接下来想搞的这个个人免费首图生成器~ 喜欢文章的小伙伴可以点个赞哦 ~ 欢迎关注公众号 秋风的笔记 ,学习前端不迷路。

参考

imm.console.aliyun.com/cn-shenzhen…

help.aliyun.com/document_de…

oss.console.aliyun.com/bucket/oss-…

juejin.cn/post/684490…

www.zhihu.com/question/50…

www.zhihu.com/question/50…

.markdown-body pre, .markdown-body pre>code.hljs { color: rgba(51, 51, 51, 1); background: rgba(248, 248, 248, 1) }
.hljs-comment, .hljs-quote { color: rgba(153, 153, 136, 1); font-style: italic }
.hljs-keyword, .hljs-selector-tag, .hljs-subst { color: rgba(51, 51, 51, 1); font-weight: 700 }
.hljs-literal, .hljs-number, .hljs-tag .hljs-attr, .hljs-template-variable, .hljs-variable { color: rgba(0, 128, 128, 1) }
.hljs-doctag, .hljs-string { color: rgba(221, 17, 68, 1) }
.hljs-section, .hljs-selector-id, .hljs-title { color: rgba(153, 0, 0, 1); font-weight: 700 }
.hljs-subst { font-weight: 400 }
.hljs-class .hljs-title, .hljs-type { color: rgba(68, 85, 136, 1); font-weight: 700 }
.hljs-attribute, .hljs-name, .hljs-tag { color: rgba(0, 0, 128, 1); font-weight: 400 }
.hljs-link, .hljs-regexp { color: rgba(0, 153, 38, 1) }
.hljs-bullet, .hljs-symbol { color: rgba(153, 0, 115, 1) }
.hljs-built_in, .hljs-builtin-name { color: rgba(0, 134, 179, 1) }
.hljs-meta { color: rgba(153, 153, 153, 1); font-weight: 700 }
.hljs-deletion { background: rgba(255, 221, 221, 1) }
.hljs-addition { background: rgba(221, 255, 221, 1) }
.hljs-emphasis { font-style: italic }
.hljs-strong { font-weight: 700 }

 

出处:https://juejin.cn/post/6900713052270755847

版权声明:本文为mq0036原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/mq0036/p/14138572.html