前言
哈喽大家好~我是荣顶,马上中秋节啦 ~ 先祝大家中秋节高兴,阖家团圆,幸福健康~
这次之所以会出一期版权保护的内容,是因为前段时间有群友说他的文章被盗了,于是冲着对版权保护办法的好奇,就有了这篇文章 ~
版权保护办法有很多:
这次文章次要和大家讲讲如何在文本与图片中暗藏本人的版权信息(下一篇文章会给大家具体聊聊抠不掉的水印元素)
本文参加了中秋流动,所以文中的例子次要围绕着中秋的主题进行解说,大家点赞反对一下 ~ 谢谢 ~ 🥰
看完本文,你将学会 👇
-
文本隐写:通过某种办法居然能够在字符串中读取和批改暗藏信息”我是荣顶”⇄”我是[前埔寨]荣顶”,点这里体验
-
图片隐写:在图片中增加暗藏的图文(还能往里面写文件!!!)点这里体验
文本隐写
首先咱们须要理解一下,什么是隐写术(Steganography)
隐写术:将信息暗藏在多种载体中,如:视频、硬盘和图像,将须要暗藏的信息通过非凡的形式嵌入到载体中,而又不侵害载体原来信息的表白。旨在爱护须要暗藏的信息不被别人辨认。
当然信息隐蔽技术必定不止隐写术,大略有:1)隐写术、2)数字水印、3)荫蔽信道、4)阀下信道、5)匿名信道 …
实现原理
通过将字符串中每个字符
转换为只有 1 和 0 的示意
,而后通过零宽字符
示意 0 和 1,就能实现通过零宽字符来示意一串看不见的字符串
首先咱们理解一下什么是零宽字符?
顾名思义: 就是字节宽度为 0 的特殊字符。🙄 这解释…那我走?
😀 零宽字符: 是一种不可打印的 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 : 用于在混合文字方向的多种语言文本中,规定排版文字书写方向为右至左
思考到 Unicode 中有所谓的Surrogate Pair
的状况,所以这里咱们解决字符串的外围办法是codePointAt
和fromCodePoint
Surrogate Pair
:是 UTF-16 中用于扩大字符而应用的编码方式,是一种采纳四个字节(两个 UTF-16 编码)来示意一个字符。Unicode 编码单元(code points)的范畴从 0 到 1,114,111。结尾的 128 个 Unicode 编码单元和 ASCII 字符编码一样。
如果指定的 index 小于 0 或大于字符串的长度,则 charCodeAt 返回 NaN
例如:汉字“𠮷”(非”吉”)的码点是 0x20BB7,而 UTF-16 编码为 0xD842 0xDFB7(十进制为 55362 57271),须要 4 个字节贮存。
对于这种 4 个字节的字符,JavaScript 不能正确处理,字符串长度会误判为 2,"𠮷".length // 2
而且 charAt()办法无奈读取整个字符,charCodeAt()办法只能别离返回前两个字节和后两个字节的值。
ES6 提供了 codePointAt()办法,可能正确处理 4 个字节贮存的字符,返回一个字符的码点。
//一个字符由两个字节还是由四个字节组成的最简略办法。
"我".codePointAt(0) > 0xFFFF;//false
"A".codePointAt(0) > 0xFFFF;//false
"3".codePointAt(0) > 0xFFFF;//false
"𠮷".codePointAt(0) > 0xFFFF;//true
console.log(String.fromCodePoint(0x20bb7)); // "𠮷"
//或者十进制
console.log(String.fromCodePoint(134071)); // "𠮷"
//也能够多个参数
console.log(String.fromCodePoint(25105,29233,0x4f60)); // "我爱你"
String.fromCodePoint
办法是 ES6 新减少的个性,ES6 提供了 String.fromCodePoint()办法,能够辨认大于 0xFFFF 的字符,补救了 String.fromCharCode()办法的有余。一些老的浏览器可能还不反对。能够通过应用这段 polyfill 代码来保障浏览器的反对
当然,如果不须要解决简单的字符,你也能够用charCodeAt
和fromCharCode
两个办法来对字符进行解决
好了,铺垫完了,开干~
先说加密
咱们将文本通过codePointAt()
办法得出每个字符的Unicode编码点
值,而后将它们转为2进制
,每个字符间通过空格
分隔开,而后咱们用零宽字符​
来示意1
,用零宽字符‌
来示意0
,用零宽字符‍
来示意空格
,这样就能够失去一段齐全用零宽字符示意的看不见的字符串
多了不说,少了不唠 ~ 先看代码!
// 字符串转零宽字符串
function encodeStr(val = "暗藏的文字") {
return (
val
.split("")
.map((char) => char.codePointAt(0).toString(2))
.join(" ")
.split("")
.map((binaryNum) => {
if (binaryNum === "1") {
return ""; // 零宽空格符​
} else if (binaryNum === "0") {
return ""; // 零宽不连字符‌
} else {
return ""; //空格 -> 零宽连字符‍
}
})
.join("")
);
}
console.log("str:",encodeStr(),"length:",encodeStr().length)//大家能够把这段copy到控制台执行以下看看
这一块一开始return
进来的都是间接写在双引号中的零宽字符,然而你们复制到浏览器中预计也不不便看,反正它是看不到的,平台要是给我过滤了,那更难堪
想在 vscode
或者atom
中看到代码里是否有零宽字符
,能够下载Highlight Bad Chars
插件来实现零宽字符的高亮(如上图所示)
为了不便大家容易辨别空字符串与零宽字符,这里我把代码改了一下(这里是用 vue 做的,当前我的所有例子都会在这个仓库下当然它也有线上的体验地址,欢送大家来指导~)
// 字符串转零宽字符串
encodeStr() {
this.cipherText = this.text.split("");
//在字符串中的随机一个地位插入加密文本
this.cipherText.splice(
parseInt(Math.random() * (this.text.length + 1)),
0,
//加密的文本
this.hiddenText
.split("")
//['荣', '顶' ]
.map((char) => char.codePointAt(0).toString(2))
// ['1000001101100011','1001100001110110']
.join(" ")
//"1000001101100011 1001100001110110"
.split("")
/* [ '1', '0', '0', '0', '0', '0', '1', '1', '0', '1', '1', '0', '0', '0', '1', '1', ' ',
'1', '0', '0', '1', '1', '0', '0', '0', '0', '1', '1', '1', '0', '1', '1', '0'] */
.map((binaryNum) => {
if (binaryNum === "1") {
return String.fromCharCode(8203); // 零宽空格符​
} else if (binaryNum === "0") {
return String.fromCharCode(8204); // 零宽不连字符‌
} else {
return String.fromCharCode(8205); //空格 -> 零宽连字符‍
}
})
//对下面所有的数组元素进行解决,生成一个新的数组['', '', ''......]其中每一个元素都是零宽字符,别离代表0和1以及
.join(String.fromCharCode(8206))
// 用左至右符‎来把下面的数组相连成一个零宽字符串=>""
);
this.cipherText = this.cipherText.join("");
console.log(this.cipherText, "cipherText");
}
其中随机混入零宽字符串次要是这段伪代码
let str = "qwe12345789".split("");
//在字符串中的随机一个地位插入加密文本
str.splice(parseInt(Math.random()*(str.length+1)),0,"加密文本").join("")
加密后的文本通过 trim 或者通过网络发送都是没问题的
"中秋节高兴".trim().length//114
大家都理解如何加密后,咱们再看以下如何对字符串进行解密
解密首先须要理解如何提取零宽度字符
"点赞激励~😀".replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
解密的过程就是加密的反向操作,咱们先提取文本中的零宽字符串,用 1
来示意零宽字符​
,用0
来示意零宽字符‌
,用空格
来示意零宽字符‍
,这样就能够失去一段由 1 和 0 组成空格分隔的字符串,咱们将每段 1/0 示意的字符串转成十进制后,通过String.fromCharCode
办法将其转回为能够看得见的文字即可~
// 零宽字符转字符串
decodeStr() {
if (!this.tempText) {
this.decodeText = "";
return;
}
let text = this.tempText.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
let hiddenText = this.tempText.replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
console.log(text, "text");
console.log(hiddenText, "hiddenText");
this.decodeText = hiddenText
.split("") //不是空字符串,是 ‎
.map((char) => {
if (char === "" /* 不是空字符串,是​ */) {
return "1";
} else if (char === "" /* 不是空字符串,是‌ */) {
return "0";
} else {
/* 是‍时,用空格替换 */
return " ";
}
})
.join("")
//转数组
.split(" ")
//依据指定的 Unicode 编码中的序号值来返回一个字符串。
.map((binaryNum) => String.fromCharCode(parseInt(binaryNum, 2)))
.join("");
console.log(text + hiddenText);
},
演示一波
外围实现办法就是以上,当初咱们来演示一下,大家看图前,首先能够把这小段文字复制到任何中央打印下(就 F12 控制台看最不便)console.log("中秋高兴123456".length)
长度肯定不是 10😀
这里的 demo 我曾经上传至我的 github,你能够点击这里体验一下~
利用场景
- 数据防爬
将零宽度字符插入文本中,烦扰关键字匹配。爬虫失去的带有零宽度字符的数据会影响他们的剖析,但不会影响用户的浏览数据。 - 暗藏文本信息
将自定义组合的零宽度字符插入文本中,用户复制后会携带不可见信息,达到暗藏文本信息的作用。 - 逃脱敏感词过滤
你在游戏里骂人,你妈*
必定是发不进来的,然而你发你xxxxx妈yyyyy*
就不一样了 😀,这个货色还是钻研起来很有意思的。 - 嵌入暗藏的代码还有很多等等等…
试想当在电子书籍 PDF 或者一些正版影视音乐作品中嵌入购买人的个人信息,那还有多少人敢间接传给他人看呢?
像上面这种明水印,加了和没加没啥区别,很容易就被他人扣掉了
而且还被他人更换了水印,打上了广告,啧啧啧…如果一本电子书里嵌入了大量的隐写内容,对于盗版的流传行为,其实是能够起到很好的追责作用的(各大出版商,电子书能够搞起来 😀 买电子书都有购买者的联系方式和个人信息的)
诸如像 toG 的我的项目中,有很多敏感内容都是有个人信息的明水印,和隐写的个人信息,传出去了就晓得是谁传的 (至多对于不懂技术的人来说,隐写是真的很难被发现的)
预防零宽字符的植入
说了这么多如何对字符进行零宽字符的植入和提取,那么像掘金哪天不容许你们加这玩意在外面了,他们对所有的文本用正则这么搞一遍就没了
预防用户上传的文本中含有零宽字符,个别咱们先做一次替换即可
str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
图片隐写
什么是图片隐写?
lsb 隐写很实用,算法简略,能存储的信息量也大,更何况是 CTF 较量中的常客(起源于 1996 年 DEFCON 寰球黑客大会)
实现原理
图片的隐写有很多形式,我这里只用最简略的形式 (LSB:二进制最低位,不是”老色 X”😅) 做演示,当然也是因为我菜 😥 数学不好,要不然我也能够用傅里叶变换通过解决图片的色波 (图像实质上就是各种色调波的叠加 这个概念的具体解释能够看阮一峰老师的这篇文章 ) 来做,那样又帅又飘~害!
二进制最低位
什么是二进制最低位?就是二进制最初面一位
好了,多了不说少了不唠 ~ 咱们一起看图谈话 ~
每个二进制的值的最低位都能够示意一个 1bit 的数据,一个像素须要 RGBA,4 个值共 4 * 8=32 个 bit 所以至多须要八个像素 (32 个值) 的最低位能力示意一个像素的 RGBA 值 (这里的 2560000 / 4 = 640000 个像素能够用来贮存 2560000 / 32 = 80000 个像素的 RGBA 值)
即:每 32 个值的二进制最低位能够用来示意一个规范像素的 RGBA 值(为不便了解,如下图所示 👇)
拿到须要暗藏的数据
这一步,咱们能够暗藏图片,暗藏文字,也能够本人手绘一些货色在画布上
将咱们曾经在主画布上绘制或加载的图片转换为 URL ,通过创立一个长期小画布, 将主画布生成的 URL 通过drawImage
的形式,放大的绘制到长期小画布上
(这里为什么要这么做?还是因为下面提到的 主画布的 640000 个像素值的最低位只能贮存 80000 个像素的 RGBA 值)
//将画布上的信息绘制到小画布上保存起来
saveHiddenImageData() {
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
//小画布的长宽=大画布的像素/8后再开平方
//因为须要八个像素的最低位才能够示意一个小画布的像素的RGBA值
tempCanvas.width = Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8));
tempCanvas.height = Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8));
var image = new Image();
image.src = this.canvas.toDataURL("image/png");
image.onload = () => {
//绘制图像到长期的小画布
tempCtx.drawImage(image, 0, 0, tempCanvas.width, tempCanvas.height);
this.hiddenData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
this.hiddenData.binaryList = Array.from(this.hiddenData.data, (color) => {
color = color.toString(2).padStart(8, "0").split("");
return color;
});
console.log(this.hiddenData, "hiddenData");
this.$message({
type: "success",
message: "保留胜利!请抉择指标图片~",
});
this.canvas.clear();
};
},
再拿指标图的数据
在拿指标图(你要把暗藏的数据隐写进的指标图)的数据时候,咱们须要事后解决一下所有的色彩值
这一步很要害,能够看到,我下面的图,画布加载完指标图片时,咱们能够通过getImageData
办法读取到页面的所有像素值,我这里将所有像素的二进制最低位全副归零,即把所有的色彩值都解决成偶数/非奇非偶数(0)
咱们通过操作每个值的最低位,用来储备要保留的数据, 肉眼是无奈看出你对最低位的更改的
//获取画布像素数据
getCanvasData() {
this.targetData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
//将数字化为非奇数
function evenNum(num) {
num = num > 254 ? num - 1 : num;
num = num % 2 == 1 ? num - 1 : num;
return num;
}
//存一个二进制的数值示意
this.targetData.binaryList = Array.from(this.targetData.data, (color, index) => {
this.targetData.data[index] = evenNum(this.targetData.data[index]);
color = evenNum(color).toString(2).padStart(8, "0").split("");
return color;
});
console.log(this.targetData);
this.loading = false;
},
写入暗藏的数据
到这里,咱们曾经拿到了暗藏的数据,和指标图像的数据,接下来咱们须要做的就是将这 318,096 个色彩值写入到指标图片的 2560000 个色彩值的二进制最低位中,即可实现图片的隐写
再次须要留神的是:通过最低位来做的话,咱们隐写到图片中的数据是无限的,即 图片的整体像素 / 8 = 图片中能够暗藏的像素(个)
这里咱们用的是800*800
的画布,有640000
个像素,其中能够暗藏 640000 / 8 = 80000个
像素
所以咱们暗藏的数据只能绘画到 Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8))
这里是 282 的宽高的 canvas 中(79,524 个像素,318,096 个色彩值),这里求出的数只能向下取整,要不然会溢出,导致失落暗藏的数据!
这里的操作就像是咱们藏头诗一样,咱们把数据藏在尾部
RGB 重量值的小量变动,是肉眼无奈分辨的,不影响对图片的辨认。你看不出来这个+1 的区别
多了不说少了不唠~先看代码
//将隐写的资源图片数据存到指标图片的二进制最低位中
drawHiddenData() {
//将暗藏的数据的二进制全副放到一个数组外面
let bigHiddenList = [];
for (let i = 0; i < this.hiddenData.binaryList.length; i++) {
bigHiddenList.push(...this.hiddenData.binaryList[i]);
}
console.log(bigHiddenList, "bigHiddenList");
this.targetData.binaryList.forEach((item, index) => {
bigHiddenList[index] && (item[7] = bigHiddenList[index]);
});
this.canvas.clear();
this.targetData.data.forEach((item, index) => {
this.targetData.data[index] = parseInt(
this.targetData.binaryList[index].join(""),
2
);
});
const tempCanvas = document.createElement("canvas");
tempCanvas.width = 800;
tempCanvas.height = 800;
let ctx = tempCanvas.getContext("2d");
ctx.putImageData(this.targetData, 0, 0);
fabric.Image.fromURL(tempCanvas.toDataURL(), (i) => {
this.canvas.clear();
this.canvas.add(i);
this.canvas.renderAll();
});
this.$message({
type: "success",
message: "加密胜利!",
});
},
能够看到,这里曾经将一张图片暗藏到了另一张图片中,(咱们这里将一个月饼图隐写进了月亮图中)
解析加密后的图片
实现了图片加密后咱们接下来须要做的是解析加密后的图片
首先咱们选取本地图片后,将其渲染到画布上,在通过getImageData
取得画布的像素数据,建设一个二进制的存储示意,前面将通过它的最低位取出暗藏在指标图中的图片
//获取画布像素数据
getCanvasData() {
this.targetData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
//存一个二进制的数值示意
this.targetData.binaryList = Array.from(this.targetData.data, (color, index) => {
color = color.toString(2).padStart(8, "0").split("");
return color;
});
console.log(this.targetData);
this.loading = false;
},
冲图片中抽取暗藏的色彩值
这里咱们次要将主画布的二进制色彩值的最低位抽离进去,组装成暗藏图片的像素值数组,最初通过putImageData
来绘制出暗藏其中的图片
这里十分须要留神的是: putImageData
的第一个参数data
的长度必须为两个边的乘积的4的倍数
否则就会报错
所以,这里咱们取像素的时候,须要这么取Math.pow(Math.floor(Math.sqrt(2560000 / 32)), 2) * 4
,因为我这里是 800 * 800的所以是2560000个值,你间接写成变量canvas.width * canvas.height *4
即可
//解析图片
decryptImage() {
const c = document.getElementById("decryptCanvas");
const ctx = c.getContext("2d");
let decryptImageData = [];
for (let i = 0; i < this.targetData.binaryList.length; i += 8) {
let tempColorData = [];
for (let j = 0; j < 8; j++) {
tempColorData.push(this.targetData.binaryList[i + j][7]);
}
decryptImageData.length < Math.pow(Math.floor(Math.sqrt(2560000 / 32)), 2) * 4 &&
decryptImageData.push([...tempColorData]);
}
decryptImageData = Uint8ClampedArray.from(decryptImageData, (z) => {
z = parseInt(z.join(""), 2);
return z;
});
console.log(decryptImageData, "decryptImageData");
//须要留神的是putImageData的data的长度必须为两个边的乘积的4的倍数
ctx.putImageData(
new ImageData(
decryptImageData,
Math.floor(Math.sqrt(2560000 / 8 / 4)),
Math.floor(Math.sqrt(2560000 / 8 / 4))
),
0,
0
);
},
通过这一步后,咱们能够看到,就可能从主画布中提取出暗藏在其中的图片!!!
哇 ~ 是不是十分的有意思?
还须要留神的点 :LSB 形式的隐写图片只能存储为 PNG 或者 BMP 图片格式,并且不能够再用有损压缩(比方 JPEG),否则会失落隐写的数据!
不要用隐写做违法犯罪的好事!!!因为它是能够防(某墙)监控的,比方你把坏孩子图藏进了好孩子图里边,我去太坏了 😢
然而你身为程序员,你能够用它去给你可爱的宝贝表白撒,不失为一种浪漫的形式~
感兴趣的小伙伴还能够看一下这篇论文StegaStamp: Invisible Hyperlinks in Physical Photographs(隐写邮票:天然照片中嵌入不可见超链接),作者来自加州大学伯克利分校。他们的我的项目主页在这
以上是文本和图片的隐写相干内容,当然隐写的媒介不可能只局限于此,还有很多中,只有是计算机能用数字表白所有货色,按情理来说都是能够用来做隐写的,例如音频的隐写,以及视频的隐写
最初
隐写术是一门很深、利用很宽泛的学识,这里讲的很泛,权当做抛砖引玉。图片和文字的隐写只是其中最简略的一部分,有趣味的同学能够看一本叫《数据暗藏技术揭秘》的书。须要的小伙伴也能够加我,我发给你!
文中的例子曾经放在了我的github中,当然你也能够通过这里体验一下
我是荣顶,很快乐能在这里和你一起变强!一起面向高兴编程! 😉
如果你也十分酷爱前端相干技术!欢送进入我的小密圈 ~ 外面都是大佬,带你飞! 🦄 扫描👇~
发表回复