From 96c19502bd7b57dbe5e62fc80f5fd7937edd8a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E4=BA=9A=E6=98=95?= <1549469775@qq.com> Date: Fri, 17 Apr 2026 16:09:04 +0800 Subject: [PATCH] feat: add crypto-wasm package for SM4 encryption/decryption, update dependencies, and enhance build scripts --- .gitignore | 5 +- .vscode/settings.json | 3 +- buildin/dm/src/index.ts | 40 ++++++---- bun.lock | 20 ++++- packages/core/vitest.config.ts | 3 - packages/crypto-wasm/Cargo.lock | 137 ++++++++++++++++++++++++++++++++++ packages/crypto-wasm/Cargo.toml | 12 +++ packages/crypto-wasm/README.md | 22 ++++++ packages/crypto-wasm/package.json | 35 +++++++++ packages/crypto-wasm/src/lib.rs | 136 +++++++++++++++++++++++++++++++++ packages/crypto-wasm/vitest.config.ts | 9 +++ packages/dx/vitest.config.ts | 5 +- packages/example/package.json | 8 +- packages/example/src/index.ts | 20 +++-- packages/example/src/sm4.d.ts | 2 + packages/example/src/sm4.js | 59 +++++++++++++++ packages/example/vite.config.ts | 6 +- readme.md | 6 ++ tsconfig.base.json | 5 +- 19 files changed, 498 insertions(+), 35 deletions(-) create mode 100644 packages/crypto-wasm/Cargo.lock create mode 100644 packages/crypto-wasm/Cargo.toml create mode 100644 packages/crypto-wasm/README.md create mode 100644 packages/crypto-wasm/package.json create mode 100644 packages/crypto-wasm/src/lib.rs create mode 100644 packages/crypto-wasm/vitest.config.ts create mode 100644 packages/example/src/sm4.d.ts create mode 100644 packages/example/src/sm4.js diff --git a/.gitignore b/.gitignore index 76add87..84011de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules -dist \ No newline at end of file +dist + +packages/*-wasm/target +packages/*-wasm/pkg \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 23ab567..6564eb0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,6 @@ "editor.codeActionsOnSave": { "source.fixAll.oxc": "explicit" }, - "js/ts.tsdk.path": "./node_modules/typescript/lib" + "js/ts.tsdk.path": "./node_modules/typescript/lib", + "typescript.tsdk": "./node_modules/typescript/lib" } \ No newline at end of file diff --git a/buildin/dm/src/index.ts b/buildin/dm/src/index.ts index 36240dc..26f83b7 100644 --- a/buildin/dm/src/index.ts +++ b/buildin/dm/src/index.ts @@ -1,8 +1,11 @@ import cac from 'cac' import pkg from "../package.json" import { build } from "tsdown" +import { exec } from 'child_process' +import { promisify } from 'util' const cli = cac() +const execAsync = promisify(exec) cli.version(pkg.version) @@ -12,29 +15,38 @@ cli.option('--entry ', 'Choose entry path', { default: 'src/index.ts', }) -cli.command('dev [module]', '开发').action((_, options) => { - build({ +const createSharedBuildConfig = (entryPath?: string) => ({ + entry: entryPath ? [entryPath] : undefined, + sourcemap: false, + outExtensions: () => ({ js: '.js', dts: '.d.ts' }), +}) + +cli.command('dev [module]', '开发').action(async (_module, options) => { + await build({ + ...createSharedBuildConfig(options.entry), watch: true, - entry: options.entry ? [options.entry] : undefined, - sourcemap: false, dts: false, - outExtensions: () => { - return { js: '.js', dts: '.d.ts' } - } }) }) -cli.command('build [module]', '构建').action((_, options) => { - build({ - entry: options.entry ? [options.entry] : undefined, - sourcemap: false, +cli.command('build [module]', '构建').action(async (_module, options) => { + await build({ + ...createSharedBuildConfig(options.entry), dts: true, format: ['esm', 'cjs'], - outExtensions: () => { - return { js: '.js', dts: '.d.ts' } - } }) }) +cli.command('wasm [module]', 'wasm') + .option('--cmd ', '执行命令') + .action(async (_module, options) => { + if (!options.cmd) { + throw new Error('请通过 --cmd 传入要执行的命令') + } + + const { stdout, stderr } = await execAsync(options.cmd) + if (stdout) process.stdout.write(stdout) + if (stderr) process.stderr.write(stderr) + }) cli.parse() diff --git a/bun.lock b/bun.lock index f66d7ff..07986e8 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@changesets/cli": "^2.30.0", "@prettier/plugin-oxc": "^0.1.3", "@types/node": "^22.7.9", - "dm": "workspace:*", + "dm": "workspace", "lefthook": "^2.1.5", "oxlint": "^1.59.0", "tsdown": "^0.21.8", @@ -31,6 +31,10 @@ "name": "@dm/core", "version": "0.0.1-alpha.1", }, + "packages/crypto-wasm": { + "name": "@dm/crypto-wasm", + "version": "0.0.1-alpha.1", + }, "packages/dx": { "name": "@dm/dx", "version": "0.0.1-alpha.1", @@ -42,10 +46,14 @@ "name": "example", "dependencies": { "@dm/core": "workspace:*", + "@dm/crypto-wasm": "workspace:*", "@dm/dx": "workspace:*", + "crypto-js": "4.2.0", + "sm-crypto": "0.4.0", }, "devDependencies": { "vite": "^8.0.8", + "vite-plugin-wasm": "^3.6.0", }, }, }, @@ -102,6 +110,8 @@ "@dm/core": ["@dm/core@workspace:packages/core"], + "@dm/crypto-wasm": ["@dm/crypto-wasm@workspace:packages/crypto-wasm"], + "@dm/dx": ["@dm/dx@workspace:packages/dx"], "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], @@ -306,6 +316,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], @@ -380,6 +392,8 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], @@ -514,6 +528,8 @@ "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "sm-crypto": ["sm-crypto@0.4.0", "", { "dependencies": { "jsbn": "^1.1.0" } }, "sha512-OexH2V1EqmhXuOIPGoCl55OjMF0wwPUM/zhUjT0Q6vHBeopSRvTNRy76/1eRoFs3VBKt39hdFnxwpFmooHYa2A=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], @@ -558,6 +574,8 @@ "vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], + "vite-plugin-wasm": ["vite-plugin-wasm@3.6.0", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw=="], + "vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 6610e98..0acf204 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -6,8 +6,5 @@ export default defineProject({ exclude: [], include: ['src/**/*.test.ts'], environment: 'node', - alias: { - "@": "./src" - }, }, }) \ No newline at end of file diff --git a/packages/crypto-wasm/Cargo.lock b/packages/crypto-wasm/Cargo.lock new file mode 100644 index 0000000..d017055 --- /dev/null +++ b/packages/crypto-wasm/Cargo.lock @@ -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", +] diff --git a/packages/crypto-wasm/Cargo.toml b/packages/crypto-wasm/Cargo.toml new file mode 100644 index 0000000..05c10c3 --- /dev/null +++ b/packages/crypto-wasm/Cargo.toml @@ -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" diff --git a/packages/crypto-wasm/README.md b/packages/crypto-wasm/README.md new file mode 100644 index 0000000..06c837e --- /dev/null +++ b/packages/crypto-wasm/README.md @@ -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`:返回明文字符串 diff --git a/packages/crypto-wasm/package.json b/packages/crypto-wasm/package.json new file mode 100644 index 0000000..e39884b --- /dev/null +++ b/packages/crypto-wasm/package.json @@ -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" + } + } +} \ No newline at end of file diff --git a/packages/crypto-wasm/src/lib.rs b/packages/crypto-wasm/src/lib.rs new file mode 100644 index 0000000..2bb2bab --- /dev/null +++ b/packages/crypto-wasm/src/lib.rs @@ -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) -> Vec { + 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, ()> { + 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, ()> { + 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, ()> { + 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); + } +} diff --git a/packages/crypto-wasm/vitest.config.ts b/packages/crypto-wasm/vitest.config.ts new file mode 100644 index 0000000..79810ef --- /dev/null +++ b/packages/crypto-wasm/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from 'vitest/config' + +export default defineProject({ + test: { + name: "crypto-wasm", + exclude: [], + include: ['**/*.test.ts'], + }, +}) \ No newline at end of file diff --git a/packages/dx/vitest.config.ts b/packages/dx/vitest.config.ts index 6610e98..8f482b8 100644 --- a/packages/dx/vitest.config.ts +++ b/packages/dx/vitest.config.ts @@ -2,12 +2,9 @@ import { defineProject } from 'vitest/config' export default defineProject({ test: { - name: "core", + name: "dx", exclude: [], include: ['src/**/*.test.ts'], environment: 'node', - alias: { - "@": "./src" - }, }, }) \ No newline at end of file diff --git a/packages/example/package.json b/packages/example/package.json index d7b8cb4..3c85079 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -5,10 +5,14 @@ "dev": "vite" }, "dependencies": { + "@dm/crypto-wasm": "workspace:*", "@dm/core": "workspace:*", - "@dm/dx": "workspace:*" + "@dm/dx": "workspace:*", + "crypto-js": "4.2.0", + "sm-crypto": "0.4.0" }, "devDependencies": { - "vite": "^8.0.8" + "vite": "^8.0.8", + "vite-plugin-wasm": "^3.6.0" } } \ No newline at end of file diff --git a/packages/example/src/index.ts b/packages/example/src/index.ts index 1e1a623..e41bdb8 100644 --- a/packages/example/src/index.ts +++ b/packages/example/src/index.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); \ No newline at end of file +const recovered = decrypt(cipherWasm); +console.log("[hello-wasm] round-trip OK:", recovered === plain); diff --git a/packages/example/src/sm4.d.ts b/packages/example/src/sm4.d.ts new file mode 100644 index 0000000..b577861 --- /dev/null +++ b/packages/example/src/sm4.d.ts @@ -0,0 +1,2 @@ +export function encrypt(data: string): string +export function decrypt(data: string): string diff --git a/packages/example/src/sm4.js b/packages/example/src/sm4.js new file mode 100644 index 0000000..c05ac4e --- /dev/null +++ b/packages/example/src/sm4.js @@ -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 '' + } +} diff --git a/packages/example/vite.config.ts b/packages/example/vite.config.ts index 47e50c7..86f07c8 100644 --- a/packages/example/vite.config.ts +++ b/packages/example/vite.config.ts @@ -1,5 +1,9 @@ import { defineConfig } from "vite" +import wasm from "vite-plugin-wasm"; export default defineConfig({ - base: "./" + base: "./", + plugins: [ + wasm() + ] }) \ No newline at end of file diff --git a/readme.md b/readme.md index ab7bf0a..0c7349a 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,11 @@ - `bun@1.3.11` +**wasm** + +- 安装rust环境 +- `cargo install wasm-pack` + ## 开发 安装依赖`bun install`,之后需要运行`bun run cli:build`命令安装内部命令行. @@ -12,6 +17,7 @@ 1. 开发时不同包之间的的引用最好直接写报名,不要携带路径,统一导出 2. 最好统一包的入口为`src/index.ts` 3. 包的导出记得加上`"development": "./src/index.ts"` +4. wasm包文件夹类似与`*-wasm` ### 子包安装依赖 diff --git a/tsconfig.base.json b/tsconfig.base.json index ff0028c..286864d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,5 +27,8 @@ "packages/*/src/index.ts" ] } - } + }, + "exclude": [ + "packages/*-wasm" + ] } \ No newline at end of file