已知Rust是个很硬核的编程语言

又已知JavaScript或者说在浏览器上的JavaScript在某种情况无法胜任工作。

So?

🦀 Rust + 🕸 Wasm = ❤

但是,单纯把教程或者 tutorial 拿出来复述是没意思的。So,搞点事情

那么前提是,你已经弄清楚了 wasm-pack 是怎么回事了

题干?

最近着手一个项目,使用浏览器的crypto实现了加解密,加解密都需要在浏览器处理。但毕竟解释型,最多只是混淆。即使我在编写时已经使用了花里胡哨的东西,就差整上钓鱼的那套手段了。不过,毕竟二进制的东西,总比混淆型更不容易肉眼解析,人脑编译

这里倒是可以交代,我用到了aes-256-cfb,所以我们大概需要这些东西

板条箱?

毕竟是 Rust,注定是“简陋”的,所以我决定直接去找现有的密码学类库。目前已经亲测的密码学库有:

如果运行在 wasm,第一个库需要使用另外一个有针对适应的

在写文章前我已经都“绕”过一遍了…在这之间反复横跳。刚开始以为这些库无法适用wasm(报错无法定位),后来觉得用起来好难(写得很绕),再后来发现是我写错了…

接着,可能需要一个随机数库,只能是rand了,而且也是crates.io下载量最多的(为什么…)

创建?

> cargo generate --git https://github.com/rustwasm/wasm-pack-template

接着,安排上依赖

1
2
3
4
5
[dependencies]
wasm-bindgen = "0.2" # 核心
rand = "0.7.2" # 但接下来用不上
aes = "0.3.2" # 添加这个只是为了分组依赖与类型
cfb-mode = "0.3.2" # 这个才是加密核心

然后运行一遍cargo build或者wasm-pack build,因为rust在编译时会检查依赖情况,所以索性我就直接通过这种方式安装依赖了,就像写swift随手cmd + b

食用?

编译源码

  1. 加载 crate(上板条!)
1
2
extern crate aes;
extern crate cfb_mode;
  1. 声明依赖,或者说import
1
2
3
use aes::Aes256; // 使用 256
use cfb_mode::stream_cipher::{NewStreamCipher, StreamCipher}; // cfb是基于流加密其中一种
use cfb_mode::Cfb; // 需要用这个结构体
  1. 声明一个类型别名,方便使用
    Rust 可以声明类型别名。为了后面方便实用,定义一个
1
2
// 使用 Cfb 结构体作为加密类型,Cfb 本身又需要一个类型…使用 Aes256 结构体声明长度
type AesCfb = Cfb<Aes256>;
  1. 随便写个加密
1
2
3
4
5
6
7
8
9
10
11
12
13
#[wasm_bindgen]
pub fn test() -> Vec<u8> {

let key: &[u8; 32] = b"nashizhendeniup,,nashizhendeniup";
let iv: &[u8; 16] = b"unique,un,unique";
let msg = "那你是真的牛皮";

let mut buffer = msg.as_bytes().to_vec();

AesCfb::new_var(key, iv).unwrap().encrypt(&mut buffer);

buffer
}

到前端使用

经过编译wasm-pack build,可以得到一个pkg目录。目录下的文件就很熟悉了!

  • package.json
  • <xxx>.wasm
  • <xxx>.d.ts
  • ……

你甚至可以直接把这个包上到npm,让更多人可以使用。这里我们就只是yarn link,然后创建一个前端项目

> npm init wasm-app www

接着,有着 50 年前端经验的老前端应该都会接下来的步骤了:yarn -> yarn link <xxx>

然后,把JavaScript的代码改改

1
2
3
import {test} from 'crypto-test'

console.log(test())

因为 Rust 是强类型的语言,所以在类型推断不会有太多麻烦。同时,在通过工具编译到wasm时会多编译一个d.ts文件。这样,就算暴露给JavaScript的代码再复杂,只要使用 VSCode 或者支持 TS 的 Language Server 也没有太大压力

所以这样我就得到一个加密数据集合,Vec<u8>JavaScript那边会直接变成UTF8类型数组,所以我们会打印出这东西

1
Uint8Array(21) [230, 156, 59, 211, 78, 162, 142, 118, 193, 154, 45, 255, 203, 56, 123, 8, 143, 173, 46, 120, 25]

node的话,一般会把加密数据转成字符串保存(比如我提到的我在做的项目),这里就先裸着吧

wasm画风是怎样的呢?给个节选参考一下

1
2
3
4
5
6
7
8
9
10
11
12
  get_local $p0
i32.load
get_local $p1
call $core::fmt::Write::write_char::h90a3bac002e2aa8d)
(func $<&T_as_core::fmt::Debug>::fmt::h110ce52a73dd639b (type $t6) (param $p0 i32) (param $p1 i32) (result i32)
get_local $p0
i32.load
get_local $p1
call $core::fmt::num::<impl_core::fmt::Debug_for_usize>::fmt::hf84d386a4f5a1afb)
(func $__rdl_dealloc (type $t7) (param $p0 i32) (param $p1 i32) (param $p2 i32)
i32.const 1056732
get_local $p0

看得我都有女装的冲动了(大雾

结束?

文章的目标只有两个:

  • 体验rust + wasm
  • 干一手加密,看看是否能取代浏览器的crypto,不考虑性能

因为Rust的发展快接近完整了,这个时候入坑应该挺合适。所以接下来我就指望靠这个语言接近计科的世界了

最后,有一点需要注意,所选择的AES长度不同,会影响你需要的秘钥长度。所以,这个时候可以唠唠加密?

(附加资料)加密?

分组密码

分组密码将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密。所以,这种加密方式带来的问题就是:对越长的字符串进行加密,代价越大

AES

对称加密的一种(对称加密就不解释了),也是目前最流行的对称加密算法之一。该算法属于分组加密算法。

AES的区块长度固定为128比特,密钥长度则可以是128,192或256比特。

加密方式也有很多模式:ecb, cfb, gcm, cbc。其中 ecb 没有 iv

我们在使用密码库的时候,都会接触到 key, iv 还有可能需要padding

iv?

初始化向量(IV,Initialization Vector)是许多任务作模式中用于将加密随机化的一个位块,由此即使同样的明文被多次加密也会产生不同的密文,避免了较慢的重新产生密钥的过程。

在大多数情况中,不应当在使用同一密钥的情况下两次使用同一个IV。对于CBC和CFB,重用IV会导致泄露明文首个块的某些信息,亦包括两个不同消息中相同的前缀。对于OFB和CTR而言,重用IV会导致完全失去安全性。

一般来说,向量用于分组加密中其中第一个块的加密,其他块均为自动生成(就是提供向量)

key?

加密密钥,对于 aes 来说就是每个块使用到的加密密钥

padding?

padding 是用来填充最后一块使得变成一整块,所以对于加密解密两端需要使用同一的 PADDING 模式,大部分 PADDING 模式为PKCS5, PKCS7, NOPADDING。

AES256? 128?

其中,iv肯定是 16 位。因为加密块的长度就是这么限制的

区别在于密钥长度,Aes128 的密钥长度需要 16 位,而 Aes256 需要的密钥长度是 32 位

为什么呢?算一下不就知道了

AES256CFB?

就是使用 aes,长度 256,那么 cfb 呢?

密文反馈(CFB,Cipher feedback),可以理解是反向 CBC,因为 CFB 的解密过程几乎就是颠倒的CBC的加密过程

那,也不用想太多,就是使用 AES 加密,长度使用 256,模式使用 cfb