19 changed files with 498 additions and 35 deletions
@ -1,2 +1,5 @@ |
|||
node_modules |
|||
dist |
|||
dist |
|||
|
|||
packages/*-wasm/target |
|||
packages/*-wasm/pkg |
|||
@ -0,0 +1,137 @@ |
|||
# This file is automatically @generated by Cargo. |
|||
# It is not intended for manual editing. |
|||
version = 4 |
|||
|
|||
[[package]] |
|||
name = "base64" |
|||
version = "0.22.1" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" |
|||
|
|||
[[package]] |
|||
name = "bumpalo" |
|||
version = "3.20.2" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" |
|||
|
|||
[[package]] |
|||
name = "cfg-if" |
|||
version = "1.0.4" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" |
|||
|
|||
[[package]] |
|||
name = "crypto" |
|||
version = "0.0.0" |
|||
dependencies = [ |
|||
"base64", |
|||
"gm-sm4", |
|||
"wasm-bindgen", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "gm-sm4" |
|||
version = "0.10.0" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "7b56818bef522abcd7de70e21f1ffcea13979d946a75ccf7c038a291a0ef92f6" |
|||
dependencies = [ |
|||
"hex", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "hex" |
|||
version = "0.4.3" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" |
|||
|
|||
[[package]] |
|||
name = "once_cell" |
|||
version = "1.21.4" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" |
|||
|
|||
[[package]] |
|||
name = "proc-macro2" |
|||
version = "1.0.106" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" |
|||
dependencies = [ |
|||
"unicode-ident", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "quote" |
|||
version = "1.0.45" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" |
|||
dependencies = [ |
|||
"proc-macro2", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "rustversion" |
|||
version = "1.0.22" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" |
|||
|
|||
[[package]] |
|||
name = "syn" |
|||
version = "2.0.117" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" |
|||
dependencies = [ |
|||
"proc-macro2", |
|||
"quote", |
|||
"unicode-ident", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "unicode-ident" |
|||
version = "1.0.24" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" |
|||
|
|||
[[package]] |
|||
name = "wasm-bindgen" |
|||
version = "0.2.118" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" |
|||
dependencies = [ |
|||
"cfg-if", |
|||
"once_cell", |
|||
"rustversion", |
|||
"wasm-bindgen-macro", |
|||
"wasm-bindgen-shared", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "wasm-bindgen-macro" |
|||
version = "0.2.118" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" |
|||
dependencies = [ |
|||
"quote", |
|||
"wasm-bindgen-macro-support", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "wasm-bindgen-macro-support" |
|||
version = "0.2.118" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" |
|||
dependencies = [ |
|||
"bumpalo", |
|||
"proc-macro2", |
|||
"quote", |
|||
"syn", |
|||
"wasm-bindgen-shared", |
|||
] |
|||
|
|||
[[package]] |
|||
name = "wasm-bindgen-shared" |
|||
version = "0.2.118" |
|||
source = "registry+https://github.com/rust-lang/crates.io-index" |
|||
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" |
|||
dependencies = [ |
|||
"unicode-ident", |
|||
] |
|||
@ -0,0 +1,12 @@ |
|||
[package] |
|||
name = "crypto" |
|||
edition = "2021" |
|||
description = "SM4 encrypt/decrypt for WebAssembly (wasm-bindgen)" |
|||
|
|||
[lib] |
|||
crate-type = ["cdylib"] |
|||
|
|||
[dependencies] |
|||
wasm-bindgen = "0.2" |
|||
gm-sm4 = "0.10" |
|||
base64 = "0.22" |
|||
@ -0,0 +1,22 @@ |
|||
# crypto-wasm |
|||
|
|||
### JavaScript / TypeScript 调用 |
|||
|
|||
```ts |
|||
import { encrypt, decrypt } from '@dm/crypto-wasm'; |
|||
|
|||
function main() { |
|||
const cipher = encrypt('hello world'); |
|||
const plain = decrypt(cipher); |
|||
|
|||
console.log(cipher); // Base64 |
|||
console.log(plain); // hello world |
|||
} |
|||
|
|||
main(); |
|||
``` |
|||
|
|||
### 3) API |
|||
|
|||
- `encrypt(data: string): string`:返回 Base64 密文 |
|||
- `decrypt(data: string): string`:返回明文字符串 |
|||
@ -0,0 +1,35 @@ |
|||
{ |
|||
"name": "@dm/crypto-wasm", |
|||
"type": "module", |
|||
"description": "SM4 encrypt/decrypt for WebAssembly (wasm-bindgen)", |
|||
"version": "0.0.1-alpha.1", |
|||
"scripts": { |
|||
"build": "dm wasm --cmd 'wasm-pack build --scope dm-wasm'" |
|||
}, |
|||
"files": [ |
|||
"./pkg/crypto_bg.js", |
|||
"./pkg/crypto_bg.wasm", |
|||
"./pkg/crypto_bg.wasm.d.ts", |
|||
"./pkg/crypto.d.ts", |
|||
"./pkg/crypto.js" |
|||
], |
|||
"main": "./pkg/crypto.js", |
|||
"types": "./pkg/crypto.d.ts", |
|||
"sideEffects": [ |
|||
"./pkg/crypto.js", |
|||
"./pkg/snippets/*" |
|||
], |
|||
"exports": { |
|||
".": { |
|||
"import": "./pkg/crypto.js", |
|||
"types": "./pkg/crypto.d.ts" |
|||
}, |
|||
"./crypto.wasm": { |
|||
"import": "./pkg/crypto_bg.wasm", |
|||
"types": "./pkg/crypto_bg.wasm.d.ts" |
|||
}, |
|||
"./crypto_bg.js": { |
|||
"import": "./pkg/crypto_bg.js" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
//! SM4 ECB + PKCS#7,与 `sm-crypto`(JuneAndGreen)默认行为一致:`sm4.encrypt` / `sm4.decrypt` 且未指定 `mode: 'cbc'` 时为 ECB。
|
|||
//! 密文对外格式:与示例中 `CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(hex))` 相同,即对原始密文字节做标准 Base64。
|
|||
|
|||
use base64::engine::general_purpose::STANDARD; |
|||
use base64::Engine; |
|||
use gm_sm4::Sm4Cipher; |
|||
use wasm_bindgen::prelude::*; |
|||
|
|||
/// 与 JS 中 `SM4_KEY_BASE64` 一致
|
|||
const SM4_KEY_BASE64: &str = "aceyfpZM9MWztRrzSCi/nA=="; |
|||
|
|||
fn sm4_key_16() -> Result<[u8; 16], ()> { |
|||
let raw = STANDARD.decode(SM4_KEY_BASE64).map_err(|_| ())?; |
|||
if raw.len() != 16 { |
|||
return Err(()); |
|||
} |
|||
let mut k = [0u8; 16]; |
|||
k.copy_from_slice(&raw); |
|||
Ok(k) |
|||
} |
|||
|
|||
fn pkcs7_pad(mut data: Vec<u8>) -> Vec<u8> { |
|||
let pad_len = 16 - (data.len() % 16); |
|||
data.extend(std::iter::repeat(pad_len as u8).take(pad_len)); |
|||
data |
|||
} |
|||
|
|||
fn pkcs7_unpad(data: &[u8]) -> Result<Vec<u8>, ()> { |
|||
if data.is_empty() || data.len() % 16 != 0 { |
|||
return Err(()); |
|||
} |
|||
let pad_len = *data.last().unwrap() as usize; |
|||
if pad_len == 0 || pad_len > 16 { |
|||
return Err(()); |
|||
} |
|||
if data[data.len() - pad_len..] |
|||
.iter() |
|||
.any(|&b| b as usize != pad_len) |
|||
{ |
|||
return Err(()); |
|||
} |
|||
Ok(data[..data.len() - pad_len].to_vec()) |
|||
} |
|||
|
|||
fn sm4_encrypt_ecb(plain: &[u8], key: &[u8; 16]) -> Result<Vec<u8>, ()> { |
|||
let cipher = Sm4Cipher::new(key.as_slice()).map_err(|_| ())?; |
|||
let padded = pkcs7_pad(plain.to_vec()); |
|||
let mut out = Vec::with_capacity(padded.len()); |
|||
for chunk in padded.chunks_exact(16) { |
|||
let block = cipher.encrypt(chunk).map_err(|_| ())?; |
|||
out.extend_from_slice(&block); |
|||
} |
|||
Ok(out) |
|||
} |
|||
|
|||
fn sm4_decrypt_ecb(cipher_bytes: &[u8], key: &[u8; 16]) -> Result<Vec<u8>, ()> { |
|||
if cipher_bytes.len() % 16 != 0 { |
|||
return Err(()); |
|||
} |
|||
let cipher = Sm4Cipher::new(key.as_slice()).map_err(|_| ())?; |
|||
let mut out = Vec::with_capacity(cipher_bytes.len()); |
|||
for chunk in cipher_bytes.chunks_exact(16) { |
|||
let block = cipher.decrypt(chunk).map_err(|_| ())?; |
|||
out.extend_from_slice(&block); |
|||
} |
|||
pkcs7_unpad(&out) |
|||
} |
|||
|
|||
/// 加密:明文为 UTF-8 字符串(与 `sm-crypto` 对 string 输入按 UTF-8 编码一致);成功返回 Base64,失败返回空串。
|
|||
#[wasm_bindgen(js_name = encrypt)] |
|||
pub fn encrypt(data: &str) -> String { |
|||
if data.is_empty() { |
|||
return String::new(); |
|||
} |
|||
let Ok(key) = sm4_key_16() else { |
|||
return String::new(); |
|||
}; |
|||
match sm4_encrypt_ecb(data.as_bytes(), &key) { |
|||
Ok(ct) => STANDARD.encode(ct), |
|||
Err(()) => String::new(), |
|||
} |
|||
} |
|||
|
|||
/// 解密:输入为 Base64 密文;成功返回 UTF-8 明文(非法 UTF-8 字节用替换符处理,与 JS `String.fromCharCode` 式解码有差异时以可显示为准),失败返回空串。
|
|||
#[wasm_bindgen(js_name = decrypt)] |
|||
pub fn decrypt(data: &str) -> String { |
|||
if data.is_empty() { |
|||
return String::new(); |
|||
} |
|||
let Ok(key) = sm4_key_16() else { |
|||
return String::new(); |
|||
}; |
|||
let ct = match STANDARD.decode(data) { |
|||
Ok(b) => b, |
|||
Err(_) => return String::new(), |
|||
}; |
|||
match sm4_decrypt_ecb(&ct, &key) { |
|||
Ok(pt) => String::from_utf8_lossy(&pt).into_owned(), |
|||
Err(()) => String::new(), |
|||
} |
|||
} |
|||
|
|||
#[cfg(test)] |
|||
mod tests { |
|||
use super::{decrypt, encrypt}; |
|||
|
|||
#[test] |
|||
fn round_trip_ascii() { |
|||
let plain = "hello world"; |
|||
let enc = encrypt(plain); |
|||
assert!(!enc.is_empty(), "encrypt should succeed"); |
|||
assert_eq!(decrypt(&enc), plain); |
|||
} |
|||
|
|||
#[test] |
|||
fn round_trip_unicode() { |
|||
let plain = "你好 sm4"; |
|||
let enc = encrypt(plain); |
|||
assert!(!enc.is_empty()); |
|||
assert_eq!(decrypt(&enc), plain); |
|||
} |
|||
|
|||
#[test] |
|||
fn empty_plaintext() { |
|||
assert_eq!(encrypt(""), ""); |
|||
} |
|||
|
|||
/// 与 `sm-crypto@0.4.0` + `crypto-js@4.2.0`(`sm4.js` 同款封装)对该明文的 Base64 一致,防止与前端漂移。
|
|||
#[test] |
|||
fn encrypt_matches_sm_crypto_fixture() { |
|||
let plain = r#"{"username":"admin","password":"admin123","code":"35","uuid":"a70a4e306dde4abe889f3faec3a219a3","clientId":"ihF3cJj0Mxf0xYh9rUqrtcJs-f7iQMrF","clientSecret":"-hs4ENSrdMPGhJUxM3E4CogQaGGb_TCReB51KlWxklhJDP-Dkz5JgFTozeshWdqF1UFjKCOiKR--ny53tusqhw"}"#; |
|||
let expected = "ryfanf2S7ADSCLhO1rCXLZxHYuPGMXJKJMdUfVzKTKGnsgdccIqABqviVPXsTAnK785eNkSeTa6Mt6FzbrZa0kLQ+QTbMGE33dEDAkcYj03V4grD8GVrT575MFGt78RMs78jPRFWCLRiOAFEmrSKTPaMX3u5zCq+F6Bwkg5S0KmmAwUEYnnL5STqRJpK+d86MRhKDQpNvzgkR4iJYXCBSi5SxtD7dH4uXtdOz/NlJZ7tPMYJ2BD83e2PzjTe2dxJlTx+cDAPfbruJOeivTNayiM3QevGtfbsJHRPy/Ge7dwyqcJSGVkcUiLvzkANBfiZ6YBUwSopYXY2aLKS5fIGWg=="; |
|||
assert_eq!(encrypt(plain), expected); |
|||
assert_eq!(decrypt(expected), plain); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import { defineProject } from 'vitest/config' |
|||
|
|||
export default defineProject({ |
|||
test: { |
|||
name: "crypto-wasm", |
|||
exclude: [], |
|||
include: ['**/*.test.ts'], |
|||
}, |
|||
}) |
|||
@ -1,11 +1,17 @@ |
|||
import fire from "@dm/core" |
|||
import { decrypt, encrypt } from "@dm/crypto-wasm"; |
|||
import { encrypt as sm4Encrypt } from "./sm4"; |
|||
|
|||
fire.on("fuck", (a) => { |
|||
console.log(a); |
|||
/** 与 sm-crypto@0.4.0 + crypto-js@4.2.0(ECB/PKCS#7、密钥见 sm4.js)对同一段 UTF-8 明文应得到相同 Base64。 */ |
|||
const plain = `{"username":"admin","password":"admin123","code":"40","uuid":"7e9d6dae9c6740c8864d91445b79741d","clientId":"ihF3cJj0Mxf0xYh9rUqrtcJs-f7iQMrF","clientSecret":"-hs4ENSrdMPGhJUxM3E4CogQaGGb_TCReB51KlWxklhJDP-Dkz5JgFTozeshWdqF1UFjKCOiKR--ny53tusqhw"}`; |
|||
const validateText = "ryfanf2S7ADSCLhO1rCXLZxHYuPGMXJKJMdUfVzKTKGnsgdccIqABqviVPXsTAnKPWbqDBT0ceITDKfxWH+Aa9n+HPYwcabe1fGR3N0QhV/PI7p9mjLS1N+Sn1EWjrDbs78jPRFWCLRiOAFEmrSKTPaMX3u5zCq+F6Bwkg5S0KmmAwUEYnnL5STqRJpK+d86MRhKDQpNvzgkR4iJYXCBSi5SxtD7dH4uXtdOz/NlJZ7tPMYJ2BD83e2PzjTe2dxJlTx+cDAPfbruJOeivTNayiM3QevGtfbsJHRPy/Ge7dwyqcJSGVkcUiLvzkANBfiZ6YBUwSopYXY2aLKS5fIGWg==" |
|||
|
|||
}) |
|||
const cipherWasm = encrypt(plain); |
|||
const cipherJs = sm4Encrypt(plain); |
|||
|
|||
console.log("[align] wasm === sm-crypto:", cipherWasm === cipherJs); |
|||
console.log("[hello-wasm] ciphertext (base64):", cipherWasm); |
|||
console.log("[hello-wasm] validateText === cipherWasm:", validateText === cipherWasm); |
|||
console.log("[hello-wasm] validateText === cipherJs:", validateText === cipherJs); |
|||
|
|||
setTimeout(() => { |
|||
fire.emitSync("fuck", "fuck you") |
|||
}, 2000); |
|||
const recovered = decrypt(cipherWasm); |
|||
console.log("[hello-wasm] round-trip OK:", recovered === plain); |
|||
|
|||
@ -0,0 +1,2 @@ |
|||
export function encrypt(data: string): string |
|||
export function decrypt(data: string): string |
|||
@ -0,0 +1,59 @@ |
|||
import smCrypto from 'sm-crypto' |
|||
import CryptoJS from 'crypto-js' |
|||
|
|||
/** |
|||
* SM4 密钥(Base64 编码) |
|||
* @constant {string} |
|||
*/ |
|||
const SM4_KEY_BASE64 = 'aceyfpZM9MWztRrzSCi/nA==' |
|||
|
|||
/** |
|||
* SM4 密钥(Hex 编码) |
|||
* @constant {string} |
|||
*/ |
|||
const SM4_KEY_HEX = CryptoJS.enc.Base64.parse(SM4_KEY_BASE64).toString(CryptoJS.enc.Hex) |
|||
|
|||
/** |
|||
* SM4 加密方法 |
|||
* @param {string} data - 需要加密的数据 |
|||
* @returns {string} Base64 编码的加密结果,失败返回空字符串 |
|||
* @example |
|||
* const encrypted = encrypt('hello world') |
|||
*/ |
|||
export function encrypt(data) { |
|||
if (!data) { |
|||
console.error('[SM4] 加密失败:数据不能为空') |
|||
return '' |
|||
} |
|||
|
|||
try { |
|||
const result = smCrypto.sm4.encrypt(data, SM4_KEY_HEX, 0) |
|||
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Hex.parse(result)) |
|||
} catch (e) { |
|||
console.error('[SM4] 加密失败:', e) |
|||
return '' |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* SM4 解密方法 |
|||
* @param {string} data - Base64 编码的加密数据 |
|||
* @returns {string} 解密后的原始数据,失败返回空字符串 |
|||
* @example |
|||
* const decrypted = decrypt(encrypted) |
|||
*/ |
|||
export function decrypt(data) { |
|||
if (!data) { |
|||
console.error('[SM4] 解密失败:数据不能为空') |
|||
return '' |
|||
} |
|||
|
|||
try { |
|||
const hexData = CryptoJS.enc.Base64.parse(data).toString(CryptoJS.enc.Hex) |
|||
const result = smCrypto.sm4.decrypt(hexData, SM4_KEY_HEX) |
|||
return result |
|||
} catch (e) { |
|||
console.error('[SM4] 解密失败:', e) |
|||
return '' |
|||
} |
|||
} |
|||
@ -1,5 +1,9 @@ |
|||
import { defineConfig } from "vite" |
|||
import wasm from "vite-plugin-wasm"; |
|||
|
|||
export default defineConfig({ |
|||
base: "./" |
|||
base: "./", |
|||
plugins: [ |
|||
wasm() |
|||
] |
|||
}) |
|||
Loading…
Reference in new issue