Skip to content

Commit

Permalink
refactor(jwe): switch to rust for wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
morlay committed May 17, 2024
1 parent 11121e5 commit 39eab46
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 583 deletions.
7 changes: 6 additions & 1 deletion nodepkg/jwe/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
*.wasm
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log
target/
24 changes: 24 additions & 0 deletions nodepkg/jwe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "jwe"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.92"
base64ct = { version = "1.6.0", features = ["alloc"] }
rand = "0.8"
sha1 = "0.10.6"
serde_json = "1"
aes-gcm = { version = "0.10", features = ["heapless", "getrandom"] }
serde = { version = "1.0", features = ["derive"] }
getrandom = { version = "*", features = ["js"] }
rsa = "0.9.6"

[dev-dependencies]
wasm-bindgen-test = "0.3.42"

[profile.release]
opt-level = "s"
6 changes: 6 additions & 0 deletions nodepkg/jwe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## jose

[https://github.com/panva/jose] 基于 [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)
被浏览器厂商强制实现为 https required.

在非 https 的环境,求助于 rust/wasm 来实现 jwe
13 changes: 12 additions & 1 deletion nodepkg/jwe/index.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
export function encrypt(payload: string, key: object): Promise<string>
export interface Key {
kid: string;
kty: string;
n: string;
e: string;
alg: string;
use?: string;

[k: string]: any;
}

export function encrypt(payload: string, key: Key): Promise<string>
19 changes: 5 additions & 14 deletions nodepkg/jwe/index.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import wasmURL from "./jwe.wasm?url";
import init, { encrypt as _encrypt } from "./pkg/jwe";

let wasm;
// make vite happy
import wasmURL from "./pkg/jwe_bg.wasm?url";

void import("./wasm_exec.js").then(() => {
const go = new Go();

return WebAssembly.instantiateStreaming(fetch(wasmURL), go.importObject)
.then((obj) => {
wasm = obj.instance;
return go.run(obj.instance);
});
});

export const encrypt = async (payload, key) => {
return wasm?.exports.encrypt?.(payload, JSON.stringify(key)) ?? __go_jwe_encrypt(payload, JSON.stringify(key));
export const encrypt = (payload, jwk) => {
return init(wasmURL).then(() => _encrypt(payload, JSON.stringify(jwk)));
};
50 changes: 0 additions & 50 deletions nodepkg/jwe/main.go

This file was deleted.

16 changes: 8 additions & 8 deletions nodepkg/jwe/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@innoai-tech/jwe",
"version": "0.1.3",
"version": "0.2.2",
"exports": {
".": {
"import": {
Expand All @@ -10,10 +10,11 @@
}
},
"files": [
"pkg/jwe.d.ts",
"pkg/jwe.js",
"pkg/jwe_bg.wasm",
"index.d.ts",
"index.mjs",
"wasm_exec.js",
"jwe.wasm"
"index.mjs"
],
"license": "MIT",
"repository": {
Expand All @@ -22,10 +23,9 @@
"directory": "nodepkg/jwe"
},
"scripts": {
"cp:wasm_exec": "cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js ./wasm_exec.js",
"build:wasm": "tinygo build -target=wasm -gc=leaking -no-debug -o=jwe.wasm main.go",
"check": "ls -ahl jwe*",
"build": "bun run build:wasm && bun run check"
"test": "cargo test -- --nocapture",
"build": "bunx wasm-pack build --release --target web --no-pack",
"prepublishOnly": "bun run build"
},
"type": "module"
}
122 changes: 122 additions & 0 deletions nodepkg/jwe/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
use rsa::{RsaPublicKey, BigUint, Oaep};
use sha1::Sha1;
use base64ct::{Encoding, Base64, Base64UrlUnpadded};
use aes_gcm::{
aead::{
heapless::Vec,
AeadCore, AeadInPlace, KeyInit, OsRng,
},
Aes256Gcm,
};

#[derive(Serialize, Deserialize)]
struct JWK {
alg: String,
kid: String,
kty: String,
n: String,
e: String,
}

#[derive(Serialize, Deserialize)]
struct JWEHeader {
alg: String,
kid: String,
enc: String,
}

struct JWEEncoder {
protected: JWEHeader,
key: rsa::RsaPublicKey,
aad: Option<String>,
}

impl JWEEncoder {
pub fn encode_string(&self, payload: &[u8]) -> String {
let key = Aes256Gcm::generate_key(&mut OsRng);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let aad = self.additional_data();

let mut buf: Vec<u8, 128> = Vec::new();
let _ = buf.extend_from_slice(payload);

let cipher = Aes256Gcm::new(&key);
cipher.encrypt_in_place(&nonce, aad.as_bytes(), &mut buf).unwrap();

let (ciphertext, tag) = buf.split_at(buf.len() - 16);

return format!(
"{}.{}.{}.{}.{}",
self.protected_header(),
self.encrypted_key(&key),
Base64UrlUnpadded::encode_string(nonce.as_slice()),
Base64UrlUnpadded::encode_string(ciphertext),
Base64UrlUnpadded::encode_string(tag),
);
}

fn encrypted_key(&self, key: &[u8]) -> String {
// RSA-OAEP use Sha1
// RSA-OAEP-256 use Sha256
let padding = Oaep::new::<Sha1>();
let encrypted_data = self.key.encrypt(&mut OsRng, padding, key).unwrap();
return Base64UrlUnpadded::encode_string(&encrypted_data);
}

fn additional_data(&self) -> String {
match &self.aad {
Some(aad) => format!("{}.{}", self.protected_header(), Base64UrlUnpadded::encode_string(aad.as_bytes())),
None => self.protected_header()
}
}

fn protected_header(&self) -> String {
return Base64UrlUnpadded::encode_string(serde_json::to_string(&self.protected).unwrap().as_bytes());
}
}

#[wasm_bindgen]
pub fn encrypt(payload: &str, jwk_json: &str) -> String {
let jwk: JWK = serde_json::from_str(&jwk_json).unwrap();

let n = Base64UrlUnpadded::decode_vec(&jwk.n).unwrap();
let e = Base64::decode_vec(&jwk.e).unwrap();
let key = RsaPublicKey::new(BigUint::from_bytes_be(&n), BigUint::from_bytes_be(&e)).unwrap();

let enc = JWEEncoder {
protected: JWEHeader {
alg: String::from("RSA-OAEP"),
enc: String::from("A256GCM"),
kid: jwk.kid,
},
key,
aad: Option::None,
};

return enc.encode_string(payload.as_bytes());
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn should_encrypt() {
let payload = "{}";
let key = r#"
{
"alg": "RSA-OAEP",
"e": "AQAB",
"kid": "jSaViAqt/oc",
"kty": "RSA",
"n": "wv42EvfPu3KMzc9fVNJpQnlHmov9VMK96BcAYmgudYvCwYcHyINhglp-4L-vgoxQFFzLz6tmqk6BdsBz_Q9oXurtGGARk7mrl2xS45l2dUKJzaz2E7-VzRQzE5JbEDYrtnh-Qt0ETElfdzc9mCLw6rrVYyM6AZEhhb62ylgS7z6EMxX_xhVoa0mij2MFzW1dHKLDfCuUwd5drDYheF_i7FcFyT41Gc3qEk-1EubTVsxbdyrPRmLt9oCvLx7JbOoLivyv8q13LQkuTUOCqPoYJCdkhSXrKQTi30UkCTJCul9qvyJvUcql3pqb9w-LD4IxhC4OYwt1TuO_SedUdRD8Cw",
"use": "enc"
}
"#;

println!("{}", encrypt(payload, key))
}
}

Loading

0 comments on commit 39eab46

Please sign in to comment.