已知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
接着,安排上依赖
[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
食用?
编译源码
- 加载 crate(上板条!)
extern crate aes;
extern crate cfb_mode;
- 声明依赖,或者说
import
use aes::Aes256; // 使用 256
use cfb_mode::stream_cipher::{NewStreamCipher, StreamCipher}; // cfb是基于流加密其中一种
use cfb_mode::Cfb; // 需要用这个结构体
- 声明一个类型别名,方便使用
Rust 可以声明类型别名。为了后面方便实用,定义一个
// 使用 Cfb 结构体作为加密类型,Cfb 本身又需要一个类型…使用 Aes256 结构体声明长度
type AesCfb = Cfb<Aes256>;
- 随便写个加密
#[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
的代码改改
import {test} from 'crypto-test'
console.log(test())
因为 Rust
是强类型的语言,所以在类型推断不会有太多麻烦。同时,在通过工具编译到wasm
时会多编译一个d.ts
文件。这样,就算暴露给JavaScript
的代码再复杂,只要使用 VSCode 或者支持 TS 的 Language Server 也没有太大压力
所以这样我就得到一个加密数据集合,Vec<u8>
到JavaScript
那边会直接变成UTF8
类型数组,所以我们会打印出这东西
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
画风是怎样的呢?给个节选参考一下
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