Hexo 文章加密

摘要

Hexo 作为简洁高效的静态博客生成,却没有足够安全的博文加密插件。本文记录了笔者一步步探索 Hexo 在文章源代码上加密的过程

效果展示

密码是 helloworld 哦~

不要妄图查看源代码获取内容哦~

密钥

构建思路

大体的构建思路是,先用 Hexo 生成 html 文件,然后构建一个标签,里面包含我们需要加密的内容。然后正则表达式匹配到这个标签,并截取到密钥和内容然后加密。然后把加密过后的字符串拼起来,构成 html 文件。

Gulp 渲染

笔者采用 Gulp 作为渲染方式,首先要安装 Gulp

npm i -g gulp

然后用 js 写一个加密和解密的函数(需要密码字符串来加密解密)在前端解密的时候,我们把输入字符串的 md5 值和密钥的 md5 值比较来判断是否进行解密

MD5

这里是从 次碳酸钴的博客 上找到的一个简易 MD5 函数

function Md5(data){
    /**************************************************
      Author:次碳酸钴([email protected])
      Input:Uint8Array
      Output:Uint8Array
     **************************************************/
    var i,j,k;
    var tis=[],abs=Math.abs,sin=Math.sin;
    for(i=1;i<=64;i++)tis.push(0x100000000*abs(sin(i))|0);
    var l=((data.length+8)>>>6<<4)+15,s=new Uint8Array(l<<2);
    s.set(new Uint8Array(data.buffer)),s=new Uint32Array(s.buffer);
    s[data.length>>2]|=0x80<<(data.length<<3&31);
    s[l-1]=data.length<<3;
    var params=[[function(a,b,c,d,x,s,t){return C(b&c|~b&d,a,b,x,s,t);
        },0,1,7,12,17,22],[function(a,b,c,d,x,s,t){return C(b&d|c&~d,a,b,x,s,t);
        },1,5,5,9,14,20],[function(a,b,c,d,x,s,t){return C(b^c^d,a,b,x,s,t);
        },5,3,4,11,16,23],[function(a,b,c,d,x,s,t){return C(c^(b|~d),a,b,x,s,t);
        },0,7,6,10,15,21]
    ],C=function(q,a,b,x,s,t){return a=a+q+(x|0)+t,(a<<s|a>>>(32-s))+b|0;
    },m=[1732584193,-271733879],o;
    m.push(~m[0],~m[1]);
    for(i=0;i<s.length;i+=16){o=m.slice(0);
        for(k=0,j=0;j<64;j++)m[k&3]=params[j>>4][0](m[k&3],m[++k&3],m[++k&3],m[++k&3],
            s[i+(params[j>>4][1]+params[j>>4][2]*j)%16],
            params[j>>4][3+j%4],tis[j]
        );
        for(j=0;j<4;j++)m[j]=m[j]+o[j]|0;
    };
    return new Uint8Array(new Uint32Array(m).buffer);
};
function encodeUTF8(s){var i,r=[],c,x;
    for(i=0;i<s.length;i++)
        if((c=s.charCodeAt(i))<0x80)r.push(c);
    else if(c<0x800)r.push(0xC0+(c>>6&0x1F),0x80+(c&0x3F));
    else {if((x=c^0xD800)>>10==0) // 对四字节 UTF-16 转换为 Unicode
            c=(x<<10)+(s.charCodeAt(++i)^0xDC00)+0x10000,
                r.push(0xF0+(c>>18&0x7),0x80+(c>>12&0x3F));
        else r.push(0xE0+(c>>12&0xF));
        r.push(0x80+(c>>6&0x3F),0x80+(c&0x3F));
    };
    return r;
};
function md5(str){// 做一个字符串的封装
    var data=new Uint8Array(encodeUTF8(str));
    var result=Md5(data);
    var hex=Array.prototype.map.call(result,function(e){return (e<16?"0":"")+e.toString(16);
      }).join("");
    return hex;
}

字典

然后需要有一个字典,在字符和数字之间转化,这个字符帮助我们进行加密解密的运算

var c2i={// char to int
    'a':0,'b':1,'c':2,'d':3,'e':4,'f':5,'g':6,
    'h':7,'i':8,'j':9,'k':10,'l':11,'m':12,'n':13,
    'o':14,'p':15,'q':16,'r':17,'s':18,'t':19,
    'u':20,'v':21,'w':22,'x':23,'y':24,'z':25,
    '0':26,'1':27,'2':28,'3':29,'4':30,'5':31,'6':32,
    '7':33,'8':34,'9':35,'.':36,',':37
};
var i2c={// int to char
    0:'a',1:'b',2:'c',3:'d',4:'e',5:'f',6:'g',
    7:'h',8:'i',9:'j',10:'k',11:'l',12:'m',13:'n',
    14:'o',15:'p',16:'q',17:'r',18:'s',19:'t',
    20:'u',21:'v',22:'w',23:'x',24:'y',25:'z',
    26:'0',27:'1',28:'2',29:'3',30:'4',31:'5',32:'6',
    33:'7',34:'8',35:'9',36:'.',37:','
};
var sz=38;// 字典的大小

有一些字符不能随意加入字典,比如空格和 <> 等。这些字符会使得密文上传上去后与本地的密文不同,就会解密出乱码。笔者只把比较必须的字符列入字典中,这意味着你使用的密码也只能在字典中选择字符

加密函数

类似将密钥循环相加的密码,我们先将明文转化为 Unicode,这样字符串就只由 a-z0-9 构成;我们自己选择一个字符 . 作为分隔符,表示字符之间的分隔,在解密的时候会用到

function encrypt(key,plain){// 加密
    var unicode="";
    for(var i=0;i<plain.length;i++){unicode+=plain.charCodeAt(i).toString(16);// 获取 Unicode
        unicode+=".";// 分隔符
    }
    console.log("unicode:\n"+unicode);
    var cipher="";// 密文
    for(var i=0;i<unicode.length;i++){cipher+=i2c[(c2i[unicode[i]]+c2i[key[i%key.length]])%sz];
    }
    return cipher;
}

解密函数

这个函数是用于调试的,实际应用的是另一个解密函数

function decrypt(key,cipher){// 对应的解密函数
    var unicode="";
    for(var i=0;i<cipher.length;i++){unicode+=i2c[(c2i[cipher[i]]-c2i[key[i%key.length]]+sz)%sz];
    }
    console.log("unicode:\n"+unicode);
    var plain="";
    lst=unicode.split('.');
    for(var i=0;i<lst.length-1;i++){// 最最后一个字符是空的
        plain+=String.fromCharCode(parseInt(lst[i],16).toString(10));
    }
    return plain;
}

Gulp 任务创建

最后就是利用 Gulp 接口来操作了。我们使用 Gulp 的 API 将上述函数和变量利用起来。GulpAPI 的使用推荐 这篇文章,它包含了大多数我们需要用到的操作

var gulp = require('gulp')
var through = require('through2');
gulp.task('encrypt', function() {gulp.src('./public/**/*.html')// 所有 html 文件
    .pipe(through.obj(function (file,encode,callback) {var filestr=file.contents.toString();// 流文件转换为字符串
        var pat=new RegExp("<encrypt[^>]*>[\\s\\S]*?</encrypt>","g");// 匹配标签的正则表达式
        if(pat.test(filestr)){// 判断是否有需要加密的内容
            // 分离加密内容和常规内容
            var enc=filestr.match(pat);
            var oth=filestr.split(pat);

            for(var i=0;i<enc.length;i++){// 加密
                // 提取密钥
                var key=enc[i].match(/password=".*"/);
                if(key==null)key=".";// 默认密钥为'.'
                else key=key[0].replace("password","").replace(/\"/g,"").replace("=","");
                console.log("The origin key:"+"\033[43m"+key+"\033[0m");

                // 匹配标签中的明文
                var str=enc[i].replace(/<encrypt[^>]*>/,"").replace(/<\/encrypt>/,"");
                console.log("\033[42mPlaintext:\n"+str+"\033[0m");

                // 文本加密
                str=encrypt(key,str);
                //console.log("\033[44mCiphertext:\n"+str+"\033[0m");

                // 调试时用
                //var st2=decrypt(key,str);
                //console.log("\033[45mDecrypt:\n"+st2+"\033[0m");

                // 外面套一层标签
                str='<div id="encrypted'+i.toString()+'" style="display: none;">'+str+'</div>';
                // 按钮和密钥输入框
                str+='<div id="encButton'+i.toString()+'"><p> 密钥 <input type="text" id="key'+i.toString()+'"value="."> <input type="submit"value=" 解密 "onclick="decrypt('+i.toString()+')"></p></div>'
                // 当前密码的 md5 值,放在一个隐藏的标签里
                str+='<div id="keyMd5'+i.toString()+'" style="visibility: hidden;">'+md5(key)+'</div>'
                enc[i]=str;// 替换明文为密文
                console.log("\033[44mCiphertext:\n"+str+"\033[0m");
            }
            filestr="";
            for(var i=0;i<oth.length;i++){// 重组 html 文件
                filestr+=oth[i];
                if(i<enc.length)filestr+=enc[i];
            }
            file.contents= new Buffer(filestr);
        }
        this.push(file);
        callback();}))
    .pipe(gulp.dest('./public'));
});

这个 GulpTask 大概干了这样一件事情

  1. 用正则表达式匹配判断是否有 encrypt 标签,从而确定是否有需要加密的内容
  2. 如果有,用正则表达式把所有需要加密的内容匹配出来,存到一个列表里面,其余的不需要加密的内容也存到一个列表里面;
  3. 对于第一个列表里的每一个元素,用正则表达式分别匹配出密钥和明文,然后调用 encrypt 函数加密明文,然后在得到的密文周围套一个标签并给一它个 id,同时生成对应的按钮和密钥输入框;然后我们需要将密钥的 MD5 值也挂在网页上,方法就是建立一个隐藏的标签,然后把 MD5 值丢进去
  4. 加密完后,把两个列表的元素交叉连接,得到新的 html 文件

举个例子,假设我们的 html 文件长这样

<h1>Title</h1>

<encrypt password="helloworld0123456789.">
    <p>hello</p>
    <p>world</p>
    <p>0123456789</p>
</encrypt>

<p>helloworld</p>

<div id="box">
    <encrypt>
        <p>Alen</p>
        <p>Bob</p>
    </encrypt>
</div>

首先正则表达式匹配,发现有需要加密的内容。然后它会被割为两个列表。

第一个列表如下

<encrypt password="helloworld0123456789.">
    <p>hello</p>
    <p>world</p>
    <p>0123456789</p>
</encrypt>
    <encrypt>
        <p>Alen</p>
        <p>Bob</p>
    </encrypt>

第二个列表就是剩余部分

<h1>Title</h1>

<p>helloworld</p>

<div id="box">
</div>

你可以发现,两个列表交叉排列就是原来的文件。然后我们逐一处理第一个列表中的每一个元素,最后第一个列表变成了这样

<div id="encrypted0" style="display: none;">
hcb,mmcpb3yrq1v742w71lcfhmqhpffyv41y.4y.70mcg,mnsplbqp0ts3wv6zyf7njjkmipbvw0x931x63af.djfymhqbvp0u8365yx894jbcuefj62zxr2w.5zx8.5jceufij6sztw2w05z48.ajcluftj55zxr2w.587
</div>
<div id="encButton0">
    <p> 密钥 <input type="text" id="key0" value="."> <input type="submit" value="解密" onclick="decrypt(0)"></p>
</div>
<div id="keyMd50" style="visibility: hidden;">
    8b8075b9e8a755259f4f3025546e1995
</div>
<div id="encrypted1" style="display: none;">
.80y80y80y80y80y80y80y80y81a85y81c82z84a84384c81a80d85y81c8.80y80y80y80y80y80y80y80y81a85y81c82084d84081a80d85y81c8.80y80y80y80y8
</div>
<div id="encButton1">
    <p> 密钥 <input type="text" id="key1" value="."> <input type="submit" value="解密" onclick="decrypt(1)"></p>
</div>
<div id="keyMd51" style="visibility: hidden;">
    5058f1af8388633f609cadb75a75dc9d
</div>

可以发现两个元素的 id 尾缀都是不同的数字标号,按钮的触发函数的传参也不同;

最后把两个列表交叉合并,文件变成了这样

<h1>Title</h1>

<div id="encrypted0" style="display: none;">
hcb,mmcpb3yrq1v742w71lcfhmqhpffyv41y.4y.70mcg,mnsplbqp0ts3wv6zyf7njjkmipbvw0x931x63af.djfymhqbvp0u8365yx894jbcuefj62zxr2w.5zx8.5jceufij6sztw2w05z48.ajcluftj55zxr2w.587
</div>
<div id="encButton0">
    <p> 密钥 <input type="text" id="key0" value="."> <input type="submit" value="解密" onclick="decrypt(0)"></p>
</div>
<div id="keyMd50" style="visibility: hidden;">
    8b8075b9e8a755259f4f3025546e1995
</div>

<p>helloworld</p>

<div id="box">
    <div id="encrypted1" style="display: none;">
        .80y80y80y80y80y80y80y80y81a85y81c82z84a84384c81a80d85y81c8.80y80y80y80y80y80y80y80y81a85y81c82084d84081a80d85y81c8.80y80y80y80y8
    </div>
    <div id="encButton1">
        <p> 密钥 <input type="text" id="key1" value="."> <input type="submit" value="解密" onclick="decrypt(1)"></p>
    </div>
    <div id="keyMd51" style="visibility: hidden;">
        5058f1af8388633f609cadb75a75dc9d
    </div>
</div>

这就是上述代码的过程。然后把上面的代码块按顺序一个一个复制到 gulpfile.js 就是完整的渲染代码了

解密函数的 JS 文件

上述 Gulp 渲染给出了密文,那么我们需要给出一个对应的解密函数。MD5 的函数我们仍然需要,用于检验密码是否初步正确(同一 MD5 的密码也不一定能成功解密)。对于这个解密函数,它的传参与加密函数有所不同。因为每一个密文的 id 和其密钥的 MD5 是有不同的数字作为标号的,因此我们只传递这个数字就行,然后用 document.getElementById 方法获取我们需要的信息。

不说废话,在 theme/next/source/js/src 新建 crypt.js,写入以下内容

注意,这个文件中的字典务必与 gulpfile.js 中的字典相同

function Md5(data){
    var i,j,k;
    var tis=[],abs=Math.abs,sin=Math.sin;
    for(i=1;i<=64;i++)tis.push(0x100000000*abs(sin(i))|0);
    var l=((data.length+8)>>>6<<4)+15,s=new Uint8Array(l<<2);
    s.set(new Uint8Array(data.buffer)),s=new Uint32Array(s.buffer);
    s[data.length>>2]|=0x80<<(data.length<<3&31);
    s[l-1]=data.length<<3;
    var params=[[function(a,b,c,d,x,s,t){return C(b&c|~b&d,a,b,x,s,t);
        },0,1,7,12,17,22],[function(a,b,c,d,x,s,t){return C(b&d|c&~d,a,b,x,s,t);
        },1,5,5,9,14,20],[function(a,b,c,d,x,s,t){return C(b^c^d,a,b,x,s,t);
        },5,3,4,11,16,23],[function(a,b,c,d,x,s,t){return C(c^(b|~d),a,b,x,s,t);
        },0,7,6,10,15,21]
    ],C=function(q,a,b,x,s,t){return a=a+q+(x|0)+t,(a<<s|a>>>(32-s))+b|0;
    },m=[1732584193,-271733879],o;
    m.push(~m[0],~m[1]);
    for(i=0;i<s.length;i+=16){o=m.slice(0);
        for(k=0,j=0;j<64;j++)m[k&3]=params[j>>4][0](m[k&3],m[++k&3],m[++k&3],m[++k&3],
            s[i+(params[j>>4][1]+params[j>>4][2]*j)%16],
            params[j>>4][3+j%4],tis[j]
        );
        for(j=0;j<4;j++)m[j]=m[j]+o[j]|0;
    };
    return new Uint8Array(new Uint32Array(m).buffer);
};
function encodeUTF8(s){var i,r=[],c,x;
    for(i=0;i<s.length;i++)
        if((c=s.charCodeAt(i))<0x80)r.push(c);
    else if(c<0x800)r.push(0xC0+(c>>6&0x1F),0x80+(c&0x3F));
    else {if((x=c^0xD800)>>10==0) // 对四字节 UTF-16 转换为 Unicode
            c=(x<<10)+(s.charCodeAt(++i)^0xDC00)+0x10000,
                r.push(0xF0+(c>>18&0x7),0x80+(c>>12&0x3F));
        else r.push(0xE0+(c>>12&0xF));
        r.push(0x80+(c>>6&0x3F),0x80+(c&0x3F));
    };
    return r;
};
function md5(str){var data=new Uint8Array(encodeUTF8(str));
    var result=Md5(data);
    var hex=Array.prototype.map.call(result,function(e){return (e<16?"0":"")+e.toString(16);
      }).join("");
    return hex;
}
var c2i={
    'a':0,'b':1,'c':2,'d':3,'e':4,'f':5,'g':6,
    'h':7,'i':8,'j':9,'k':10,'l':11,'m':12,'n':13,
    'o':14,'p':15,'q':16,'r':17,'s':18,'t':19,
    'u':20,'v':21,'w':22,'x':23,'y':24,'z':25,
    '0':26,'1':27,'2':28,'3':29,'4':30,'5':31,'6':32,
    '7':33,'8':34,'9':35,'.':36,',':37
};
var i2c={
    0:'a',1:'b',2:'c',3:'d',4:'e',5:'f',6:'g',
    7:'h',8:'i',9:'j',10:'k',11:'l',12:'m',13:'n',
    14:'o',15:'p',16:'q',17:'r',18:'s',19:'t',
    20:'u',21:'v',22:'w',23:'x',24:'y',25:'z',
    26:'0',27:'1',28:'2',29:'3',30:'4',31:'5',32:'6',
    33:'7',34:'8',35:'9',36:'.',37:','
};
var sz=38;
function decrypt(idx){// 对应的解密函数
    var key=document.getElementById('key'+idx.toString()).value;
    var keymd5=md5(key);
    if(document.getElementById('keyMd5'+idx.toString()).innerHTML!=keymd5){console.log('Your key is not correct!');
        return;
    }
    document.getElementById('encrypted'+idx.toString()).style.display="";
    var cipher=document.getElementById('encrypted'+idx.toString()).innerHTML;
    //console.log("key:\n"+key);
    //console.log("cipher:\n"+cipher);
    var unicode="";
    for(var i=0;i<cipher.length;i++){unicode+=i2c[(c2i[cipher[i]]-c2i[key[i%key.length]]+sz)%sz];
    }
    //console.log("unicode:\n"+unicode);
    var plain="";
    lst=unicode.split('.');
    for(var i=0;i<lst.length-1;i++){// 最最后一个字符是空的
        plain+=String.fromCharCode(parseInt(lst[i],16).toString(10));
    }
    //console.log("plain:\n"+plain);
    document.getElementById('encrypted'+idx.toString()).innerHTML=plain;
    document.getElementById('encButton'+idx.toString()).style.display="none";
}

写好 JS 文件后,就要调用它,在 next/layout/_scripts/pages/post-details.swig 文件中追加

<script src="{{ url_for(theme.js) }}/src/crypt.js?v={{version}}"></script>

这样就完成了 html 端的布置,但这只是在 html 文件上的渲染,我们还需要在 Hexo 渲染 markdown 文件的时候,渲染出 encrypt 标签才行。

Markdown 端的调用接口

一开始,作者尝试直接在 markdown 文件里写原生的 html 标签,但是这么写导致的问题是,Hexo 将不会对 encrypt 标签内的文本渲染 Markdown,相当于标签内的文本是直接以 markdown 源代码呈现的,这显然不符合我们的要求。

后来,笔者在查阅与 Hexo 代码块折叠有关的 文章 时,发现该配置可以类比到博文加密的配置。NexT 有一种 的 API 调用接口,这不算是 markdown 语法,其中的本质是调用 js 函数来渲染。因此我们可以自己设计一个 API,让内部渲染 markdown 的同时,外面套上一层 encrypt 的标签,就可以解决问题啦。具体操作如下

/themes/next/scripts/ 中新建文件 tags.js 并添加如下代码:

/*
  @haohuawu
  修复 Nunjucks 的 tag 里写 ``` 代码块 ```,最终都会渲染成 undefined 的问题
  https://github.com/hexojs/hexo/issues/2400
*/
const rEscapeContent = /<escape(?:[^>]*)>([\s\S]*?)<\/escape>/g;
const placeholder = '\uFFFD';
const rPlaceholder = /(?:<|&lt;)\!--\uFFFD(\d+)--(?:>|&gt;)/g;
const cache = [];
function escapeContent(str) {return '<!--' + placeholder + (cache.push(str) - 1) + '-->';
}
hexo.extend.filter.register('before_post_render', function(data) {data.content = data.content.replace(rEscapeContent, function(match, content) {return escapeContent(content);
  });
  return data;
});
hexo.extend.filter.register('after_post_render', function(data) {data.content = data.content.replace(rPlaceholder, function() {return cache[arguments[1]];
  });
  return data;
});

在同一目录下新建文件 encrypt.js 添加如下代码:

/* global hexo */
// Usage: {% encrypt password %} Something {% endencrypt %}
function encrypt (args, content) {var psw = args[0];
    if(!psw) psw = ".";
    return '<encrypt password="'+psw+'">\n' + hexo.render.renderSync({text: content, engine: 'markdown'}) + '\n</encrypt>';
}// 套一层标签
hexo.extend.tag.register('encrypt', encrypt, {ends: true});
```

使用方法如下

```
{% encrypt password %}
Something
{% endencrypt %}

大功告成

参考文献

次碳酸钴。简易 MD5 函数(JavaScript 实现). https://www.web-tinker.com/article/20705.html

qq20004604. 就算萌新也能看得懂的 gulp 教程 (1). https://blog.csdn.net/qq20004604/article/details/78398859

JerryHan. Hexo 博客搭建及优化(二):添加代码折叠功能。https://jerryhanjj.github.io/2018/04/05/Hexo%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E5%8F%8A%E4%BC%98%E5%8C%96%EF%BC%88%E4%BA%8C%EF%BC%89%EF%BC%9A%E6%B7%BB%E5%8A%A0%E4%BB%A3%E7%A0%81%E6%8A%98%E5%8F%A0%E5%8A%9F%E8%83%BD/

yeiqing000. 利用 JS 做到隐藏 div 和显示 div. https://www.imooc.com/article/15383


  转载请注明: Sshwy's Blog Hexo 文章加密

 上一篇
NexT 文章随机背景图 NexT 文章随机背景图
摘要 NexT 主题做为 Hexo 博客中相当受欢迎的主题之一,得益于它简洁精致的外观,丰富多样的设置。不过相比与动态博客而言仍然少了许多重量级的功能。之前笔者介绍了 Hexo 文章加密的配置方法,今天作者来介绍 Hexo 博客随机背景头图
2019.03.30
下一篇 
镇海省选集训 Day1 镇海省选集训 Day1
摘要 镇海中学的都是神仙吧 A.Line 平面上有 n 条两两不同的直线 ,现在给出一个左下角为 右上角为 的矩形,问有多少个直线对
2019.03.17
  目录