EJS - 3: Gridea 加密的一种解决方案

Gridea 加密的解决方案。

前言

Gridea 作为一个静态网站编辑并且发布的软件来说已经做的非常 OK 了。但它仍然缺少一个在我看来比较重要的功能,那就是文章加密。尽管对于前段来说,加密并没有太大的用途,但是有句话说得好

“即使是最简单的密码功能也足以阻止90%的访问者。”

在这种没有提供加密的情况下,我们仍然可以使用 EJS 本身的特性来使得 Gridea 可以完成文章加密。

步骤

首先我们要了解 EJS 是可以在本地采用跟 js 一样的操作字符串的方法来操作网页内容的。这也就是说我们可以在 EJS 输出网站内容之前提前对所有内容进行加密然后在输出到网站的内容里。关于具体如何提前操作输出内容可以参考我关于 EJS 的文章

但是什么时候需要加密也是需要去判断的。因为 Gridea 本身并没有提供关于设置文章加密的方法,最简单的方法就是在文章的标签处加入 password 表示这篇文章需要加密。所以只要通过读取 tag 我们就可以判断一片文章需不需要加密。

Base64 加密

但是由于 EJS 本身的限制我们无法直接引用外部的 JS 文件(当然还是可以自己写函数的)。所以本篇文章采用了较为简单的基于 Base64 的加密。

Base64 的加密函数[1]

function Base64() {   
    _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; //加密的核心

    this.encode = function (input) {  
        var output = "";  
        var chr1, chr2, chr3, enc1, enc2, enc3, enc4;  
        var i = 0;  
        input = _utf8_encode(input);  
        while (i < input.length) {  
            chr1 = input.charCodeAt(i++);  
            chr2 = input.charCodeAt(i++);  
            chr3 = input.charCodeAt(i++);  
            enc1 = chr1 >> 2;  
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);  
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);  
            enc4 = chr3 & 63;  
            if (isNaN(chr2)) {  
                enc3 = enc4 = 64;  
            } else if (isNaN(chr3)) {  
                enc4 = 64;  
            }  
            output = output +  
            _keyStr.charAt(enc1) + _keyStr.charAt(enc2) +  
            _keyStr.charAt(enc3) + _keyStr.charAt(enc4);  
        }  
        return output;  
    }  
 
    _utf8_encode = function (string) {  
        string = string.replace(/\r\n/g,"\n");  
        var utftext = "";  
        for (var n = 0; n < string.length; n++) {  
            var c = string.charCodeAt(n);  
            if (c < 128) {  
                utftext += String.fromCharCode(c);  
            } else if((c > 127) && (c < 2048)) {  
                utftext += String.fromCharCode((c >> 6) | 192);  
                utftext += String.fromCharCode((c & 63) | 128);  
            } else {  
                utftext += String.fromCharCode((c >> 12) | 224);  
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);  
                utftext += String.fromCharCode((c & 63) | 128);  
            }  

        }  
        return utftext;  
    }  
}

var str = '124中文内容';  
var base = new Base64();  
var result = base.encode(str);

result 的内容就是我们最终加密后的内容。其中代码处最核心的就是 _keyStr 变量,这里面存放的内容简单理解来说就是我们所需要的 Key(后文将用 Encrypt.js 来称呼这段代码)。由于加密方法的限制,它要求我们的 key 的内容必须为这些字母、数字、符号内容。但是关键的是key 里面的字母、数字以及符号是可以改变顺序的。按照理论来说,按照最简单的遍历方法,要硬解密码的次数:

64!=1.26887×108964!=1.26887\times 10^{89}

最终,在前端我们需要的就是用户输入密码之后我们进行解密,解密代码:

function Decrypt(pass) {  
    _keyStr = pass;
    this.decode = function (input) {  
        var output = "";  
        var chr1, chr2, chr3;  
        var enc1, enc2, enc3, enc4;  
        var i = 0;  
        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");  
        while (i < input.length) {  
            enc1 = _keyStr.indexOf(input.charAt(i++));  
            enc2 = _keyStr.indexOf(input.charAt(i++));  
            enc3 = _keyStr.indexOf(input.charAt(i++));  
            enc4 = _keyStr.indexOf(input.charAt(i++));  
            chr1 = (enc1 << 2) | (enc2 >> 4);  
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);  
            chr3 = ((enc3 & 3) << 6) | enc4;  
            output = output + String.fromCharCode(chr1);  
            if (enc3 != 64) {  
                output = output + String.fromCharCode(chr2);  
            }  
            if (enc4 != 64) {  
                output = output + String.fromCharCode(chr3);  
            }  
        }  
        output = _utf8_decode(output);  
        return output;  
    } 
    _utf8_encode = function (string) {  
        string = string.replace(/\r\n/g,"\n");  
        var utftext = "";  
        for (var n = 0; n < string.length; n++) {  
            var c = string.charCodeAt(n);  
            if (c < 128) {  
                utftext += String.fromCharCode(c);  
            } else if((c > 127) && (c < 2048)) {  
                utftext += String.fromCharCode((c >> 6) | 192);  
                utftext += String.fromCharCode((c & 63) | 128);  
            } else {  
                utftext += String.fromCharCode((c >> 12) | 224);  
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);  
                utftext += String.fromCharCode((c & 63) | 128);  
            }  

        }  
        return utftext;  
    }  
    _utf8_decode = function (utftext) {  
        var string = "";  
        var i = 0;  
        var c = c1 = c2 = 0;  
        while ( i < utftext.length ) {  
            c = utftext.charCodeAt(i);  
            if (c < 128) {  
                string += String.fromCharCode(c);  
                i++;  
            } else if((c > 191) && (c < 224)) {  
                c2 = utftext.charCodeAt(i+1);  
                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));  
                i += 2;  
            } else {  
                c2 = utftext.charCodeAt(i+1);  
                c3 = utftext.charCodeAt(i+2);  
                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));  
                i += 3;  
            }  
        }  
        return string;  
    }  
}

其中 pass 就是我们需要传递的 key。我们可以把这一部分封装为一个 JS 文件方面调用(后文将用 Decrypt.js 来称呼这段代码)

运用到 EJS

说了这么多关于 Base64 加密,接下来就要把它和 EJS 的本地渲染结合起来。参照之前写的 EJS 文章内容的渲染方法,我们可以用如下代码:

<%- Content();%><%  -%>
<%
function Base64() {   
  //上文 Encrypt.js 的内容
}
function Content(){
	let con= post.content;
	var count = 0;
	con = refineContent(con);
	post.tags.forEach(function(tag){  
		if (tag.name == 'password'){
			var base = new Base64();  
			con = base.encode(con);  
			count = 1;
		}
	});
	if (count == 1){
		con = '<div class="hbe-input-container" id="hbeInput"><input type="password"id="hbePass"placeholder=""><label for="hbePass">Please input password to read passage.</label><div class="bottom-line"></div></div>' + con + '<script src="Decrypt.js"></script>'
	}
	return con;
}
%>

通过如上的方法,我们就可以完成对 post.Content 的加密。注意,输出到 con 后面的 Decrypt.js 需要替换成你自己的 Decrypt.js 的地址。

前端输入判断

细心的话大家应该可以发现,我在加密的过程中在 返回字符串的前面添加了一个新的div,这个 div 就是我们用来包装前端给用户输入密码的界面的。这个输入界面借鉴了hexo-blog-encrypt的设计[2]。这个 div 的 css 如下:

.hbe-input-container {
  width: 80%;
  max-width: 800px;
  position: relative;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: reverse;
  -ms-flex-flow: column-reverse;
  flex-flow: column-reverse;
  -webkit-box-align: start;
  -ms-flex-align: start;
  align-items: flex-start;
  margin: 100px auto;
}
.hbe-input-container input {
  -webkit-box-ordinal-group: 11;
  order: 10;
  -ms-flex-order: 10;
  outline: none;
  border: none;
  width: 100%;
  padding: 15px 0;
  font-size: 20px;
  border-bottom: 1px solid #d5d5d5;
  text-indent: 10px;
}
.hbe-input-container input::-moz-placeholder {
  opacity: 0;
}
.hbe-input-container input::-webkit-input-placeholder {
  opacity: 0;
}
.hbe-input-container input:-ms-input-placeholder {
  opacity: 0;
}
.hbe-input-container input,
.hbe-input-container label {
  transition: all 0.3s;
}
.hbe-input-container label {
  -webkit-box-ordinal-group: 101;
  -ms-flex-order: 100;
  font-size: 18px;
  order: 100;
  color: #3f4f5b;
  -webkit-transform-origin: left bottom;
  transform-origin: left bottom;
  -webkit-transform: translate(10px, 40px);
  transform: translate(0px, 40px);
}
.hbe-input-container .bottom-line {
  order: 2;
  width: 0;
  height: 2px;
  background: #658db5;
  transition: all 0.3s;
}
.hbe-input-container input:focus {
  border-bottom-color: #fff;
}
.hbe-input-container input:focus ~ div,
.hbe-input-container input:not(:placeholder-shown) ~ div {
  width: 100%;
}
.hbe-input-container input:focus + label,
.hbe-input-container input:not(:placeholder-shown) + label {
  color: #52616c;
  -webkit-transform: translate(10px) scale(0.9);
  transform: translate(10px) scale(0.9);
}

.hbe-button {
  width: 130px;
  height: 40px;
  background: linear-gradient(to bottom, #4eb5e5 0%,#389ed5 100%); /* W3C */
  border: none;
  border-radius: 5px;
  position: relative;
  border-bottom: 4px solid #2b8bc6;
  color: #fbfbfb;
  font-weight: 600;
  font-family: 'Open Sans', sans-serif;
  text-shadow: 1px 1px 1px rgba(0,0,0,.4);
  font-size: 15px;
  text-align: left;
  text-indent: 5px;
  box-shadow: 0px 3px 0px 0px rgba(0,0,0,.2);
  cursor: pointer;

  display: block;
  margin: 0 auto;
  margin-bottom: 20px;
}

.hbe-button:active {
  box-shadow: 0px 2px 0px 0px rgba(0,0,0,.2);
  top: 1px;
}

.hbe-button:after {
  content: "";
  width: 0;
  height: 0;
  display: block;
  border-top: 20px solid #187dbc;
  border-bottom: 20px solid #187dbc;
  border-left: 16px solid transparent;
  border-right: 20px solid #187dbc;
  position: absolute;
  opacity: 0.6;
  right: 0;
  top: 0;
  border-radius: 0 5px 5px 0;
}

通过把这个 div 添加到被加密内容的上面,我们就可以实现如下效果:

前端输入密码效果
前端输入密码效果

对于这个 input 输入框,我们需要采用 js 绑定时间才可以使得 js 接收 input 内容:

var hbePass = document.getElementById("hbePass");
hbePass.onkeypress=function(event){
 if(event.which === 13) {
	var base = new Encrypt(document.getElementById('hbePass').value);
	var result2 = base.decode(document.getElementById("md_block").innerText);  //获取存放在 md_block 里面的加密后的文章内容
	document.getElementById("md_block").innerHTML = result2; 
	document.getElementById("hbeInput").innerHTML = "";
    // Put other codes you want to execute in here.
}
}

注意,因为这篇文章采用的不是密码比对的方法,而是直接使用密码去解密;如果输入了错误的密码,看到的文章内容将会是乱码,并且需要重新刷新页面才能继续尝试。这也算是一种防止恶意破解的方法吧。

进阶

这个功能可以被整合进 Gridea 内:

  • 用户在主题处设置是否启用加密
  • 用户决定哪个 tag 需要被加密
  • 用户输入自定义的密码

由于我偷懒,进阶的功能不提供解决方法。相信如果你能看懂上面的内容,完成进阶内容也轻而易举了!加密后的效果可以查看这篇文章!


  1. Javascript实现base64的加密解密 ↩︎

  2. hexo-blog-encrypt 设计 ↩︎

EOF