From 8eb2cbebbff4a9d8b4f02e274f7adce09b41b59b Mon Sep 17 00:00:00 2001 From: Agitator <1482084534@qq.com> Date: Thu, 24 Sep 2026 19:43:28 +0800 Subject: [PATCH] Add plugin CryptToolkit v1.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - init - docs: crypt-tool architectural design spec - docs: crypt-tool implementation plan - chore: vitest setup and registry categories - feat(preload): base64 hex url encoding with envelope - feat(preload): md5 and sha256 digest - feat(preload): aes cbc gcm encrypt decrypt - feat(preload): rsa encrypt decrypt keygen - feat(preload): hmac pbkdf2 wire services.crypt and types - feat(config): dbStorage settings and dynamic feature sync - feat(ui): shared field teach actions copy and shell css - feat(shell): sidebar settings host and registry helpers - feat(algorithms): base64 hex url modules - feat(algorithms): md5 and sha256 modules - feat(algorithms): aes and rsa modules - feat(algorithms): hmac pbkdf2 and full registry - feat: wire app shell plugin.json remove scaffold demos - docs: readme for crypt-tool usage and extension - feat: bcrypt 算法 + 全局 UI 改进 + 主题适配 + 标准库规范 - fix: 以 ZTools 官方规范方式管理 preload 层第三方依赖 - fix: 修复node第三方依赖缺失问题 - feat(plugin): 扩展 cmds 关键词,支持按算法名直接搜索 - optimize: 优化关键词 - docs: 添加 v1.0.0 CHANGELOG - docs: 完善文档 - fix: node20构建失败问题 - feat: 新增更多的算法以及关键词缩减 - feat: 新增 9 个算法 (batch3) — unicode转义/HTML实体/Punycode/莫尔斯电码/XOR/ECDH/JWT/JSON/密码强度 - fix: 移除 enterPayload 自动输入逻辑,启用 ztools 原生搜索框过滤导航 - style: 搜索无匹配时增加空状态提示样式 - docs: 更新 CHANGELOG v1.1.0 - release: 1.1.0 - chore: homepage config - feat: 跟随ztools主题色 - chore: changelog - rename: 重命名 --- plugins/crypt-toolkit/.gitignore | 14 + plugins/crypt-toolkit/CHANGELOG.md | 60 + plugins/crypt-toolkit/DEVELOP.md | 411 ++ plugins/crypt-toolkit/README.md | 115 + plugins/crypt-toolkit/docs/images/img.png | Bin 0 -> 162573 bytes plugins/crypt-toolkit/docs/images/img_2.png | Bin 0 -> 145509 bytes .../plans/2026-09-22-crypt-tool.md | 3458 +++++++++++++++++ .../specs/2026-09-22-crypt-tool-design.md | 281 ++ plugins/crypt-toolkit/index.html | 11 + plugins/crypt-toolkit/package-lock.json | 2476 ++++++++++++ plugins/crypt-toolkit/package.json | 27 + plugins/crypt-toolkit/public/logo.svg | 47 + plugins/crypt-toolkit/public/plugin.json | 30 + .../public/preload/crypt/adler32.js | 19 + .../public/preload/crypt/adler32.test.ts | 38 + .../crypt-toolkit/public/preload/crypt/aes.js | 67 + .../public/preload/crypt/aes.test.ts | 47 + .../public/preload/crypt/argon2.js | 59 + .../public/preload/crypt/argon2.test.ts | 78 + .../public/preload/crypt/base32.js | 62 + .../public/preload/crypt/base32.test.ts | 60 + .../public/preload/crypt/bcrypt.js | 35 + .../public/preload/crypt/bcrypt.test.ts | 123 + .../public/preload/crypt/chacha.js | 66 + .../public/preload/crypt/chacha.test.ts | 76 + .../crypt-toolkit/public/preload/crypt/des.js | 43 + .../public/preload/crypt/des.test.ts | 58 + .../public/preload/crypt/ecdh.js | 33 + .../public/preload/crypt/ecdh.test.ts | 39 + .../public/preload/crypt/ecdsa.js | 49 + .../public/preload/crypt/ecdsa.test.ts | 52 + .../public/preload/crypt/ed25519.js | 47 + .../public/preload/crypt/ed25519.test.ts | 76 + .../public/preload/crypt/encoding.js | 52 + .../public/preload/crypt/encoding.test.ts | 42 + .../public/preload/crypt/envelope.js | 17 + .../public/preload/crypt/hash.js | 19 + .../public/preload/crypt/hash.test.ts | 71 + .../public/preload/crypt/hkdf-kdf.js | 34 + .../public/preload/crypt/hkdf-kdf.test.ts | 79 + .../public/preload/crypt/hmac-pbkdf2.js | 62 + .../public/preload/crypt/hmac-pbkdf2.test.ts | 49 + .../public/preload/crypt/html-entity.js | 70 + .../public/preload/crypt/html-entity.test.ts | 51 + .../public/preload/crypt/index.js | 70 + .../crypt-toolkit/public/preload/crypt/jwt.js | 23 + .../public/preload/crypt/jwt.test.ts | 26 + .../public/preload/crypt/morse.js | 55 + .../public/preload/crypt/morse.test.ts | 26 + .../public/preload/crypt/password-strength.js | 46 + .../preload/crypt/password-strength.test.ts | 46 + .../public/preload/crypt/punycode-idn.js | 12 + .../public/preload/crypt/punycode-idn.test.ts | 33 + .../public/preload/crypt/random-bytes.js | 18 + .../public/preload/crypt/random-bytes.test.ts | 46 + .../crypt-toolkit/public/preload/crypt/rsa.js | 51 + .../public/preload/crypt/rsa.test.ts | 35 + .../public/preload/crypt/safe-base64.js | 27 + .../public/preload/crypt/safe-base64.test.ts | 52 + .../public/preload/crypt/scrypt-kdf.js | 49 + .../public/preload/crypt/scrypt-kdf.test.ts | 50 + .../public/preload/crypt/serde-utils.js | 179 + .../public/preload/crypt/serde-utils.test.ts | 173 + .../public/preload/crypt/text-utils.js | 17 + .../public/preload/crypt/text-utils.test.ts | 33 + .../public/preload/crypt/unicode-escape.js | 38 + .../preload/crypt/unicode-escape.test.ts | 35 + .../public/preload/crypt/xor-cipher.js | 29 + .../public/preload/crypt/xor-cipher.test.ts | 32 + .../public/preload/package-lock.json | 22 + .../crypt-toolkit/public/preload/package.json | 8 + .../crypt-toolkit/public/preload/services.js | 7 + plugins/crypt-toolkit/src/App.tsx | 111 + .../src/algorithms/adler32/index.ts | 5 + .../src/algorithms/adler32/meta.ts | 15 + .../src/algorithms/adler32/ui.tsx | 37 + .../crypt-toolkit/src/algorithms/aes/index.ts | 5 + .../crypt-toolkit/src/algorithms/aes/meta.ts | 15 + .../crypt-toolkit/src/algorithms/aes/ui.tsx | 108 + .../src/algorithms/argon2/index.ts | 3 + .../src/algorithms/argon2/meta.ts | 15 + .../src/algorithms/argon2/ui.tsx | 145 + .../src/algorithms/base32/index.ts | 5 + .../src/algorithms/base32/meta.ts | 15 + .../src/algorithms/base32/ui.tsx | 81 + .../src/algorithms/base58/index.ts | 3 + .../src/algorithms/base58/meta.ts | 15 + .../src/algorithms/base58/ui.tsx | 99 + .../src/algorithms/base64/index.ts | 5 + .../src/algorithms/base64/meta.ts | 15 + .../src/algorithms/base64/ui.tsx | 67 + .../src/algorithms/bcrypt/index.ts | 5 + .../src/algorithms/bcrypt/meta.ts | 15 + .../src/algorithms/bcrypt/ui.tsx | 129 + .../src/algorithms/chacha/index.ts | 3 + .../src/algorithms/chacha/meta.ts | 15 + .../src/algorithms/chacha/ui.tsx | 120 + .../src/algorithms/codec.test.ts | 28 + plugins/crypt-toolkit/src/algorithms/codec.ts | 19 + .../src/algorithms/crc32/index.ts | 3 + .../src/algorithms/crc32/meta.ts | 15 + .../crypt-toolkit/src/algorithms/crc32/ui.tsx | 37 + .../crypt-toolkit/src/algorithms/des/index.ts | 5 + .../crypt-toolkit/src/algorithms/des/meta.ts | 15 + .../crypt-toolkit/src/algorithms/des/ui.tsx | 120 + .../src/algorithms/ecdh/index.ts | 5 + .../crypt-toolkit/src/algorithms/ecdh/meta.ts | 15 + .../crypt-toolkit/src/algorithms/ecdh/ui.tsx | 105 + .../src/algorithms/ecdsa/index.ts | 5 + .../src/algorithms/ecdsa/meta.ts | 15 + .../crypt-toolkit/src/algorithms/ecdsa/ui.tsx | 171 + .../src/algorithms/ed25519/index.ts | 3 + .../src/algorithms/ed25519/meta.ts | 15 + .../src/algorithms/ed25519/ui.tsx | 144 + .../crypt-toolkit/src/algorithms/hex/index.ts | 5 + .../crypt-toolkit/src/algorithms/hex/meta.ts | 15 + .../crypt-toolkit/src/algorithms/hex/ui.tsx | 67 + .../src/algorithms/hkdf/index.ts | 3 + .../crypt-toolkit/src/algorithms/hkdf/meta.ts | 15 + .../crypt-toolkit/src/algorithms/hkdf/ui.tsx | 77 + .../src/algorithms/hmac/index.ts | 5 + .../crypt-toolkit/src/algorithms/hmac/meta.ts | 15 + .../crypt-toolkit/src/algorithms/hmac/ui.tsx | 53 + .../src/algorithms/htmlEntity/index.ts | 5 + .../src/algorithms/htmlEntity/meta.ts | 15 + .../src/algorithms/htmlEntity/ui.tsx | 92 + .../src/algorithms/json/index.ts | 5 + .../crypt-toolkit/src/algorithms/json/meta.ts | 15 + .../crypt-toolkit/src/algorithms/json/ui.tsx | 78 + .../crypt-toolkit/src/algorithms/jwt/index.ts | 5 + .../crypt-toolkit/src/algorithms/jwt/meta.ts | 15 + .../crypt-toolkit/src/algorithms/jwt/ui.tsx | 57 + .../crypt-toolkit/src/algorithms/md5/index.ts | 5 + .../crypt-toolkit/src/algorithms/md5/meta.ts | 15 + .../crypt-toolkit/src/algorithms/md5/ui.tsx | 37 + .../src/algorithms/morse/index.ts | 5 + .../src/algorithms/morse/meta.ts | 15 + .../crypt-toolkit/src/algorithms/morse/ui.tsx | 86 + .../src/algorithms/passwordStrength/index.ts | 5 + .../src/algorithms/passwordStrength/meta.ts | 15 + .../src/algorithms/passwordStrength/ui.tsx | 65 + .../src/algorithms/pbkdf2/index.ts | 5 + .../src/algorithms/pbkdf2/meta.ts | 15 + .../src/algorithms/pbkdf2/ui.tsx | 64 + .../src/algorithms/punycode/index.ts | 5 + .../src/algorithms/punycode/meta.ts | 15 + .../src/algorithms/punycode/ui.tsx | 78 + .../src/algorithms/randomBytes/index.ts | 5 + .../src/algorithms/randomBytes/meta.ts | 15 + .../src/algorithms/randomBytes/ui.tsx | 70 + .../src/algorithms/ripemd160/index.ts | 5 + .../src/algorithms/ripemd160/meta.ts | 15 + .../src/algorithms/ripemd160/ui.tsx | 37 + .../crypt-toolkit/src/algorithms/rsa/index.ts | 5 + .../crypt-toolkit/src/algorithms/rsa/meta.ts | 15 + .../crypt-toolkit/src/algorithms/rsa/ui.tsx | 124 + .../src/algorithms/safeBase64/index.ts | 5 + .../src/algorithms/safeBase64/meta.ts | 15 + .../src/algorithms/safeBase64/ui.tsx | 81 + .../src/algorithms/scrypt/index.ts | 3 + .../src/algorithms/scrypt/meta.ts | 15 + .../src/algorithms/scrypt/ui.tsx | 82 + .../src/algorithms/sha1/index.ts | 3 + .../crypt-toolkit/src/algorithms/sha1/meta.ts | 15 + .../crypt-toolkit/src/algorithms/sha1/ui.tsx | 37 + .../src/algorithms/sha256/index.ts | 5 + .../src/algorithms/sha256/meta.ts | 15 + .../src/algorithms/sha256/ui.tsx | 37 + .../src/algorithms/sha384/index.ts | 5 + .../src/algorithms/sha384/meta.ts | 15 + .../src/algorithms/sha384/ui.tsx | 37 + .../src/algorithms/sha3_256/index.ts | 5 + .../src/algorithms/sha3_256/meta.ts | 15 + .../src/algorithms/sha3_256/ui.tsx | 37 + .../src/algorithms/sha3_512/index.ts | 3 + .../src/algorithms/sha3_512/meta.ts | 15 + .../src/algorithms/sha3_512/ui.tsx | 37 + .../src/algorithms/sha512/index.ts | 3 + .../src/algorithms/sha512/meta.ts | 15 + .../src/algorithms/sha512/ui.tsx | 37 + .../src/algorithms/unicodeEscape/index.ts | 5 + .../src/algorithms/unicodeEscape/meta.ts | 15 + .../src/algorithms/unicodeEscape/ui.tsx | 78 + .../crypt-toolkit/src/algorithms/url/index.ts | 5 + .../crypt-toolkit/src/algorithms/url/meta.ts | 15 + .../crypt-toolkit/src/algorithms/url/ui.tsx | 67 + .../src/algorithms/uuid/index.ts | 3 + .../crypt-toolkit/src/algorithms/uuid/meta.ts | 15 + .../crypt-toolkit/src/algorithms/uuid/ui.tsx | 63 + .../src/algorithms/xorStream/index.ts | 5 + .../src/algorithms/xorStream/meta.ts | 15 + .../src/algorithms/xorStream/ui.tsx | 110 + .../crypt-toolkit/src/config/features.test.ts | 55 + plugins/crypt-toolkit/src/config/features.ts | 34 + plugins/crypt-toolkit/src/config/index.ts | 2 + .../crypt-toolkit/src/config/settings.test.ts | 77 + plugins/crypt-toolkit/src/config/settings.ts | 55 + plugins/crypt-toolkit/src/env.d.ts | 149 + .../crypt-toolkit/src/hooks/useZtoolsTheme.ts | 64 + plugins/crypt-toolkit/src/main.css | 106 + plugins/crypt-toolkit/src/main.tsx | 6 + .../src/registry/algorithms.test.ts | 51 + .../crypt-toolkit/src/registry/algorithms.ts | 105 + .../src/registry/categories.test.ts | 22 + .../crypt-toolkit/src/registry/categories.ts | 20 + plugins/crypt-toolkit/src/registry/index.ts | 3 + plugins/crypt-toolkit/src/registry/types.ts | 33 + plugins/crypt-toolkit/src/shared/Actions.tsx | 24 + .../crypt-toolkit/src/shared/CopyButton.tsx | 19 + plugins/crypt-toolkit/src/shared/Field.tsx | 104 + .../crypt-toolkit/src/shared/TeachCard.tsx | 8 + .../crypt-toolkit/src/shared/crypt-shell.css | 540 +++ plugins/crypt-toolkit/src/shared/index.ts | 7 + .../crypt-toolkit/src/shell/AlgorithmHost.tsx | 55 + .../crypt-toolkit/src/shell/SettingsPage.tsx | 107 + plugins/crypt-toolkit/src/shell/Shell.tsx | 93 + plugins/crypt-toolkit/src/shell/Sidebar.tsx | 57 + plugins/crypt-toolkit/src/shell/index.ts | 2 + plugins/crypt-toolkit/src/types/crypt.ts | 1 + plugins/crypt-toolkit/tsconfig.json | 19 + plugins/crypt-toolkit/vite.config.js | 8 + plugins/crypt-toolkit/vitest.config.ts | 8 + 222 files changed, 15642 insertions(+) create mode 100644 plugins/crypt-toolkit/.gitignore create mode 100644 plugins/crypt-toolkit/CHANGELOG.md create mode 100644 plugins/crypt-toolkit/DEVELOP.md create mode 100644 plugins/crypt-toolkit/README.md create mode 100644 plugins/crypt-toolkit/docs/images/img.png create mode 100644 plugins/crypt-toolkit/docs/images/img_2.png create mode 100644 plugins/crypt-toolkit/docs/superpowers/plans/2026-09-22-crypt-tool.md create mode 100644 plugins/crypt-toolkit/docs/superpowers/specs/2026-09-22-crypt-tool-design.md create mode 100644 plugins/crypt-toolkit/index.html create mode 100644 plugins/crypt-toolkit/package-lock.json create mode 100644 plugins/crypt-toolkit/package.json create mode 100644 plugins/crypt-toolkit/public/logo.svg create mode 100644 plugins/crypt-toolkit/public/plugin.json create mode 100644 plugins/crypt-toolkit/public/preload/crypt/adler32.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/adler32.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/aes.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/aes.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/argon2.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/argon2.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/base32.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/base32.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/bcrypt.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/bcrypt.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/chacha.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/chacha.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/des.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/des.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/ecdh.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/ecdh.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/ecdsa.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/ecdsa.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/ed25519.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/ed25519.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/encoding.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/encoding.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/envelope.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/hash.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/hash.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/html-entity.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/html-entity.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/index.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/jwt.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/jwt.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/morse.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/morse.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/password-strength.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/password-strength.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/punycode-idn.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/punycode-idn.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/random-bytes.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/random-bytes.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/rsa.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/rsa.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/safe-base64.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/safe-base64.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/serde-utils.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/serde-utils.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/text-utils.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/text-utils.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/unicode-escape.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/unicode-escape.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/crypt/xor-cipher.js create mode 100644 plugins/crypt-toolkit/public/preload/crypt/xor-cipher.test.ts create mode 100644 plugins/crypt-toolkit/public/preload/package-lock.json create mode 100644 plugins/crypt-toolkit/public/preload/package.json create mode 100644 plugins/crypt-toolkit/public/preload/services.js create mode 100644 plugins/crypt-toolkit/src/App.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/adler32/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/adler32/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/adler32/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/aes/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/aes/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/aes/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/argon2/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/argon2/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/argon2/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/base32/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/base32/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/base32/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/base58/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/base58/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/base58/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/base64/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/base64/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/base64/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/bcrypt/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/bcrypt/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/bcrypt/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/chacha/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/chacha/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/chacha/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/codec.test.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/codec.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/crc32/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/crc32/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/crc32/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/des/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/des/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/des/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/ecdh/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ecdh/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ecdh/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/ecdsa/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ecdsa/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ecdsa/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/ed25519/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ed25519/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ed25519/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/hex/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/hex/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/hex/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/hkdf/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/hkdf/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/hkdf/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/hmac/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/hmac/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/hmac/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/htmlEntity/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/htmlEntity/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/htmlEntity/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/json/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/json/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/json/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/jwt/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/jwt/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/jwt/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/md5/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/md5/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/md5/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/morse/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/morse/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/morse/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/passwordStrength/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/passwordStrength/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/passwordStrength/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/pbkdf2/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/pbkdf2/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/pbkdf2/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/punycode/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/punycode/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/punycode/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/randomBytes/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/randomBytes/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/randomBytes/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/ripemd160/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ripemd160/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/ripemd160/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/rsa/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/rsa/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/rsa/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/safeBase64/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/safeBase64/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/safeBase64/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/scrypt/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/scrypt/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/scrypt/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/sha1/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha1/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha1/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/sha256/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha256/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha256/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/sha384/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha384/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha384/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/sha3_256/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha3_256/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha3_256/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/sha3_512/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha3_512/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha3_512/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/sha512/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha512/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/sha512/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/unicodeEscape/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/unicodeEscape/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/unicodeEscape/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/url/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/url/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/url/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/uuid/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/uuid/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/uuid/ui.tsx create mode 100644 plugins/crypt-toolkit/src/algorithms/xorStream/index.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/xorStream/meta.ts create mode 100644 plugins/crypt-toolkit/src/algorithms/xorStream/ui.tsx create mode 100644 plugins/crypt-toolkit/src/config/features.test.ts create mode 100644 plugins/crypt-toolkit/src/config/features.ts create mode 100644 plugins/crypt-toolkit/src/config/index.ts create mode 100644 plugins/crypt-toolkit/src/config/settings.test.ts create mode 100644 plugins/crypt-toolkit/src/config/settings.ts create mode 100644 plugins/crypt-toolkit/src/env.d.ts create mode 100644 plugins/crypt-toolkit/src/hooks/useZtoolsTheme.ts create mode 100644 plugins/crypt-toolkit/src/main.css create mode 100644 plugins/crypt-toolkit/src/main.tsx create mode 100644 plugins/crypt-toolkit/src/registry/algorithms.test.ts create mode 100644 plugins/crypt-toolkit/src/registry/algorithms.ts create mode 100644 plugins/crypt-toolkit/src/registry/categories.test.ts create mode 100644 plugins/crypt-toolkit/src/registry/categories.ts create mode 100644 plugins/crypt-toolkit/src/registry/index.ts create mode 100644 plugins/crypt-toolkit/src/registry/types.ts create mode 100644 plugins/crypt-toolkit/src/shared/Actions.tsx create mode 100644 plugins/crypt-toolkit/src/shared/CopyButton.tsx create mode 100644 plugins/crypt-toolkit/src/shared/Field.tsx create mode 100644 plugins/crypt-toolkit/src/shared/TeachCard.tsx create mode 100644 plugins/crypt-toolkit/src/shared/crypt-shell.css create mode 100644 plugins/crypt-toolkit/src/shared/index.ts create mode 100644 plugins/crypt-toolkit/src/shell/AlgorithmHost.tsx create mode 100644 plugins/crypt-toolkit/src/shell/SettingsPage.tsx create mode 100644 plugins/crypt-toolkit/src/shell/Shell.tsx create mode 100644 plugins/crypt-toolkit/src/shell/Sidebar.tsx create mode 100644 plugins/crypt-toolkit/src/shell/index.ts create mode 100644 plugins/crypt-toolkit/src/types/crypt.ts create mode 100644 plugins/crypt-toolkit/tsconfig.json create mode 100644 plugins/crypt-toolkit/vite.config.js create mode 100644 plugins/crypt-toolkit/vitest.config.ts diff --git a/plugins/crypt-toolkit/.gitignore b/plugins/crypt-toolkit/.gitignore new file mode 100644 index 000000000..65a9a4be1 --- /dev/null +++ b/plugins/crypt-toolkit/.gitignore @@ -0,0 +1,14 @@ +.claude +.codeium +.codex +.copilot +.gemini +.opencode +.qoder +.superpowers +.dsh +.zcode +**/node_modules +**/dist +**/.idea +**/.vscode \ No newline at end of file diff --git a/plugins/crypt-toolkit/CHANGELOG.md b/plugins/crypt-toolkit/CHANGELOG.md new file mode 100644 index 000000000..0e5d0505d --- /dev/null +++ b/plugins/crypt-toolkit/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +## 1.1.0 - 2026-09-23 + +### 新增功能 + +#### 🔐 新增 9 个密码学算法 + +| 分类 | 算法 | 说明 | +|-------|--------------|------------------------------| +| 编解码 | Unicode 转义 | `\uXXXX` / `\u{XXXXX}` 格式互转 | +| 编解码 | HTML 实体 | 命名实体 + 十进制 / 十六进制数字实体 | +| 编解码 | Punycode/IDN | 国际化域名编解码(`中文.cn` ↔ `xn--fiq228c.cn`) | +| 编解码 | 莫尔斯电码 | A-Z 0-9 标点,空格转为 `/` | +| 对称加密 | XOR 流加密 | 逐字节密钥循环 XOR,教学演示用 | +| 非对称加密 | ECDH | 椭圆曲线 Diffie-Hellman 密钥协商 | +| 工具 | JWT 解码 | 解析 header.payload,不做签名验证 | +| 工具 | JSON 格式化 | 美化 / 压缩 | +| 工具 | 密码强度估算 | 0–100 分,基于长度、字符集、常见模式 | + +#### 🔍 原生搜索框 + +- 通过 `setSubInput` 注册 ztools 窗体原生搜索框 +- 搜索算法名称实时过滤左侧导航 +- 选择算法或切换页面时自动清空搜索词 + +### 优化改进 + +- 移除了启动时通过 URL query 自动填入输入框的逻辑,简化启动流程 +- 跟随主题色 + +## 1.0.0 - 2026-09-22 + +### 新增功能 + +#### 🔐 十大密码学算法 + +| 分类 | 算法 | 说明 | +|-------|---------|------------------------------| +| 编解码 | Base64 | 标准 Base64 编码/解码 | +| 编解码 | Hex | 十六进制编解码 | +| 编解码 | URL | URL 百分比编解码 | +| 哈希 | MD5 | 128 位消息摘要 | +| 哈希 | SHA-256 | 256 位安全哈希 | +| 对称加密 | AES | CBC / GCM 模式,128/192/256 位密钥 | +| 非对称加密 | RSA | OAEP 填充,密钥生成 / 加密 / 解密 | +| 消息认证 | HMAC | HMAC-SHA1/256/384/512 | +| 密钥派生 | PBKDF2 | 基于 HMAC 的密码派生 | +| 密码哈希 | bcrypt | 自适应成本,带验证模式 | + +#### 🎨 用户界面 + +- 左侧分类滚动,设置按钮固定底部 +- 右侧内容区独立垂直滚动 +- 标题栏右上角集成编解码 / 加解密 Tab 切换 +- 自定义滚动条(6px 细胶囊造型,hover 变主题蓝) +- 自适应明暗双主题(`prefers-color-scheme`) +- ZTools 宿主主题色自动同步(`useZtoolsTheme` hook) +- 透明面板背景,无圆角边框,融入宿主视觉风格 + diff --git a/plugins/crypt-toolkit/DEVELOP.md b/plugins/crypt-toolkit/DEVELOP.md new file mode 100644 index 000000000..3343397c4 --- /dev/null +++ b/plugins/crypt-toolkit/DEVELOP.md @@ -0,0 +1,411 @@ +# crypt-tool 开发指南 + +本插件运行于 ZTools 桌面端,采用 React 19 + TypeScript + Vite 6,所有 crypto 运算通过 preload 注入 Node.js `node:crypto` 或经审查的标准库完成。 + +> ⚠️ **实现原则**:禁止手写加密算法实现(包括 Blowfish、SHA、AES 轮函数等底层密码原语)。所有算法必须优先使用 Node.js 内置 `node:crypto` 模块;若内置模块不覆盖(如 bcrypt),需引用成熟的标准 npm 包(如 `bcryptjs`)。这是保证输出与 Python/Go/Java/在线工具等外部实现兼容的唯一方式。 + +## 目录 + +- [架构总览](#架构总览) +- [新增一个算法](#新增一个算法) +- [UI 规范](#ui-规范) +- [已有算法清单](#已有算法清单) +- [后续算法计划](#后续算法计划) + +--- + +## 架构总览 + +``` +crypt-tool +├── public/preload/crypt/ # Preload 层:Node.js crypto 封装 +│ ├── envelope.js # ok / fail / tryCrypt 包装器 +│ ├── index.js # 导出所有算法给 window.services.crypt +│ ├── encoding.js # Base64 / Hex / URL +│ ├── hash.js # MD5 / SHA-256 +│ ├── aes.js # AES +│ ├── rsa.js # RSA +│ └── hmac-pbkdf2.js # HMAC / PBKDF2 +├── src/algorithms/ # UI 层:每个算法三个文件 +│ └── / +│ ├── index.ts # 导出 AlgorithmModule +│ ├── meta.ts # 元数据(id / label / cmds / teach 等) +│ └── ui.tsx # React 组件 +├── src/registry/ # 算法注册表 +│ ├── types.ts # AlgorithmMeta / AlgorithmModule / CategoryId +│ ├── algorithms.ts # 算法数组 + helpers +│ └── categories.ts # 分类列表 +├── src/shared/ # 公共 UI 组件 +│ ├── Field.tsx # 支持 copyable / variant / onClear +│ ├── Actions.tsx # 顶部 Tab 式操作按钮组 +│ ├── CopyButton.tsx # 独立复制按钮(已逐步迁移为 Field.copyable) +│ ├── crypt-shell.css # 整个插件的样式 +│ └── TeachCard.tsx # 算法简要卡片(页面底部) +├── src/shell/ # 页面壳 +│ ├── Shell.tsx +│ ├── AlgorithmHost.tsx # 算法页核心:head + Component + TeachCard +│ ├── Sidebar.tsx # 左侧分类导航 +│ └── SettingsPage.tsx # 设置页(搜索 + 标签启停) +└── src/config/ + ├── settings.ts # Settings 管理(dbStorage 持久化) + └── features.ts # syncFeatures() 同步到 ZTools 直达指令 +``` + +--- + +## 新增一个算法 + +只需四步: + +### Step 1 — Preload + +在 `public/preload/crypt/` 新建或扩展 JS 文件,**必须使用 `node:crypto` 或成熟标准库**实现运算函数,返回 `{ ok: true, data }` 或 `{ ok: false, error }`: + +```js +// 新文件 public/preload/crypt/chacha.js +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function encrypt({ key, nonce, plaintext }) { + return tryCrypt(() => { + // 参数校验 → fail('...') + const cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce) + const ct = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]) + return ok(ct.toString('base64')) + }) +} + +module.exports = { encrypt, decrypt } +``` + +> 校验失败直接 `fail('中文错误信息')`;运算用 `tryCrypt` 包裹。 +> +> **禁止**:手写的 S-box、 Blowfish 轮函数、SHA 消息调度等底层实现——此类代码即使能自洽也无法与其他语言/在线工具互通。 +> +> **推荐**: +> - 哈希/MAC:`crypto.createHash` / `crypto.createHmac` +> - 对称加密:`crypto.createCipheriv` / `crypto.createDecipheriv` +> - PBKDF2/scrypt:`crypto.pbkdf2Sync` / `crypto.scryptSync` +> - bcrypt:`bcryptjs`(同 API、纯 JS、兼容 C 实现) +> - RSA/ECC:`crypto.generateKeyPairSync` / `crypto.publicEncrypt` / `crypto.createSign` + +在 `public/preload/crypt/index.js` 注册到 `window.services.crypt`: + +```js +const chacha = require('./chacha') +module.exports = { /* ... */ chacha } +``` + +### Step 2 — 算法模块 + +在 `src/algorithms//` 下新建三个文件: + +**meta.ts** — 元数据: + +```ts +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'chacha', // 唯一 ID,也用于直达指令 alg:chacha + category: 'symmetric', // 分类 ID(categories.ts 中) + label: 'ChaCha20-Poly1305', // 标签页按钮文字 + title: 'ChaCha20-Poly1305 对称加密', // 页面标题 + reversible: true, // 是否有解密(有则 Tab 切换加解密) + cmds: ['chacha', 'chacha20'], // ZTools 直达关键词 + defaultEnabled: true, + teach: { + summary: 'ChaCha20-Poly1305 是现代流密码 + AEAD 组合……' + } +} +``` + +**index.ts** — 组合导出: + +```ts +import { meta } from './meta' +import UI from './ui' +export const chacha = { meta, Component: UI } +``` + +**ui.tsx** — 遵循 UI 规范(见下文"UI 规范"章节)。 + +### Step 3 — 注册 + +在 `src/registry/algorithms.ts` import 并加入数组: + +```ts +import { chacha } from '../algorithms/chacha' +export const algorithms = [ /* ... */ chacha ] +``` + +### Step 4 — 单元测试 + +为 preload 函数写 Vitest 测试:`public/preload/crypt/.test.ts`,覆盖正常路径与错误路径。 + +--- + +## UI 规范 + +算法 UI 必须保持一致的视觉结构。以下结构已通过所有实装算法验证: + +### 整体布局 + +从顶到底顺序固定: + +``` +[ct-tabs] ← 仅 reversible 算法:编码/加密 + 解密 + 可选 ghost 操作 +[ct-input-zone] ← 输入区 + [ct-zone-label] "输入" + [ct-grid2] ← 2 列网格,单字段时占满宽度 + [Field] × N +[ct-actions] ← 按钮区:占满宽度,flex: 1 +[ct-output-zone] ← 输出区 + [ct-zone-label] "输出" + [Field variant="output"] +[TeachCard] ← 已在 AlgorithmHost 中自动渲染 +``` + +### 输入区(.ct-input-zone) + +- 带灰色边框卡片包裹,顶部 `ct-zone-label` 显示"输入" +- 字段用 `ct-grid2` 2 列网格排布 +- 同一行只有一个字段时(奇数总字段)占满宽度(由 CSS `:last-child:nth-child(odd)` 自动处理) + +### 输出区(.ct-output-zone) + +- 带蓝色边框卡片包裹,顶部 `ct-zone-label` 显示"输出" +- 仅存放输出字段,每个字段 `variant="output"` + `readOnly` + `copyable` + +### 操作区(.ct-actions) + +- 位于输入区与输出区之间 +- 按钮 `flex: 1` 等宽占满一行 +- 主操作 `className="ct-btn ct-btn-primary"` +- 附加操作(如互换) `className="ct-btn ct-btn-ghost"` + +### Tab 切换(仅 reversible 算法) + +```tsx +
+ + + {/* 可选 */} +
+``` + +Tab 下方紧跟输入区,**而非**把 Tab 作为输入的一部分。 + +### Field 组件参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `label` | string | 标签 | +| `hint?` | string | 行内小提示(如 "16 字节") | +| `help?` | string | 字段下方帮助文字 | +| `type?` | `'text' \| 'secret' \| 'textarea' \| 'select'` | 默认 text | +| `copyable?` | boolean | 标签行末尾显示内联复制按钮 ⧉ | +| `variant?` | `'input' \| 'output'` | output 时蓝色高亮 + 输出标签 | +| `onClear?` | () => void | 标签行末尾显示清除按钮 ×(仅需要时) | +| `readOnly?` | boolean | 只读(输出字段必须) | + +### 非 reversible 算法(如 MD5) + +跳过 Tabs,直接输入区 → actions → 输出区: + +```tsx +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ +
+``` + +### 调用运算 + +使用 `runCodec` + `showError` + `showData`: + +```ts +import { runCodec, showError, showData } from '../codec' + +const r = runCodec(() => + window.services.crypt..({ /* 参数 */ }) +) +setError(showError(r)) +setOutput(showData(r)) +``` + +接收外部传入内容用 `useEffect`: + +```ts +useEffect(() => { if (enterPayload) setInput(enterPayload) }, [enterPayload]) +``` + +--- + +## 已有算法清单 + +### 编码转换(encoding) + +| ID | 标签 | 可逆 | +|----|------|------| +| base64 | Base64 | ✓ | +| base58 | Base58 | ✓ | +| safeBase64 | Safe Base64 | ✓ | +| base32 | Base32 | ✓ | +| hex | Hex | ✓ | +| url | URL | ✓ | +| unicodeEscape | Unicode 转义 | ✓ | +| htmlEntity | HTML 实体 | ✓ | +| punycode | Punycode/IDN | ✓ | +| morse | 莫尔斯电码 | ✓ | + +### 哈希摘要(hash) + +| ID | 标签 | 可逆 | +|----|------|------| +| md5 | MD5 | ✗ | +| sha1 | SHA-1 | ✗ | +| sha256 | SHA-256 | ✗ | +| sha384 | SHA-384 | ✗ | +| sha512 | SHA-512 | ✗ | +| sha3_256 | SHA3-256 | ✗ | +| sha3_512 | SHA3-512 | ✗ | +| ripemd160 | RIPEMD-160 | ✗ | + +### 对称加密(symmetric) + +| ID | 标签 | 可逆 | +|----|------|------| +| aes | AES | ✓ | +| chacha | ChaCha20 | ✓ | +| des | 3DES | ✓ | +| xorStream | XOR 流加密 | ✓ | + +### 非对称加密(asymmetric) + +| ID | 标签 | 可逆 | +|----|------|------| +| rsa | RSA | ✓ | +| ed25519 | Ed25519 | ✗ | +| ecdsa | ECDSA | ✗ | +| ecdh | ECDH | ✗ | + +### 消息认证(hmac) + +| ID | 标签 | 可逆 | +|----|------|------| +| hmac | HMAC | ✗ | + +### 口令派生(kdf) + +| ID | 标签 | 可逆 | +|----|------|------| +| pbkdf2 | PBKDF2 | ✗ | +| bcrypt | bcrypt | ✗ | +| scrypt | scrypt | ✗ | +| argon2 | Argon2 | ✗ | +| hkdf | HKDF | ✗ | + +### 工具与实用(tools) + +| ID | 标签 | 可逆 | +|----|------|------| +| uuid | UUID | ✗ | +| crc32 | CRC32 | ✗ | +| adler32 | Adler-32 | ✗ | +| randomBytes | Random Bytes | ✗ | +| jwt | JWT 解码 | ✗ | +| json | JSON 格式化 | ✓ | +| passwordStrength | 密码强度 | ✗ | + +--- + +## 后续算法计划 + +按类别分组,按实现复杂度和实用价值排列。 + +### 编码转换(encoding) + +- [x] **safe-base64** — URL 安全的 Base64(`+/` → `-_`,可去 `=` padding) +- [x] **base32** — Base32 编解码(RFC 4648,常用于 TOTP 密钥、可读性更好) +- [x] **base58 / base58check** — Base58 / Base58check(比特币地址风格,无 `0OIl` 歧义字符) +- [x] **unicode-escape** — Unicode 转义(`\u4e2d\u6587` ↔ `中文`) +- [x] **html-entity** — HTML 实体编解码(`&` ↔ `&`,`中` 十进制/十六进制数字实体) +- [x] **punycode** — Punycode 国际化域名(`中文.cn` ↔ `xn--fiq228c.cn`) +- [x] **morse** — 摩尔斯电码(字母/数字 ↔ 长短音,仅编码;中文可用 Unicode 转义) + +### 哈希摘要(hash) + +- [x] **sha1** — SHA-1(160 位;仍广泛用于 Git 对象 ID,但不应再用于安全场景) +- [x] **sha384** — SHA-384(SHA-384 摘要,与 SHA-512 同族,截断到 384 位) +- [x] **sha512** — SHA-512(512 位摘要,安全强度更高) +- [x] **sha3-256 / sha3-512** — SHA-3(Keccak,与 SHA-2 完全不同的结构) +- [ ] **blake2b / blake2s** — BLAKE2(比 MD5/SHA 快且安全,常用作现代哈希) +- [ ] **blake3** — BLAKE3(并行哈希、极快、可 Keyed 模式做伪 HMAC) +- [x] **ripemd160** — RIPEMD-160(比特币地址生成配套哈希) + +### 对称加密(symmetric) + +- [x] **des / 3des** — DES / 3DES(历史算法 `des-ede3-cbc`,用于兼容老数据;可用 `crypto.createCipheriv`) +- [x] **chacha20-poly1305** — ChaCha20-Poly1305(现代 AEAD,TLS 1.3 常用;可用 `crypto.createCipheriv('chacha20-poly1305', ...)`) +- [ ] **sm4** — SM4(国密对称加密标准,128 位分组;可用 Node.js SM4 实现如 `@wecom/crypto` 或自行调用 WebAssembly) +- [x] **xor-stream** — XOR 流密码(逐字节/逐字密钥循环 XOR,简单演示用;可用 `Buffer` 实现) + +### 非对称加密(asymmetric) + +- [x] **ecdsa** — ECDSA(椭圆曲线数字签名;支持 P-256/P-384) +- [x] **ed25519** — Ed25519(EdDSA 现代曲线;密钥短、速度快、抗侧信道) +- [x] **ecdh** — ECDH(椭圆曲线 Diffie-Hellman 密钥协商,双方各取私钥 × 对方公钥得共享秘密) +- [ ] **sm2** — SM2(国密椭圆曲线签名 + 加密) +- [ ] **dh** — Diffie-Hellman(经典 DH 密钥协商,与小素数和大素数域) + +### 消息认证(hmac) + +- [ ] **hmac-sha512** — 已有 HMAC 已支持 SHA-512,但可新增独立入口默认 SHA-512 +- [ ] **cmac** — CMAC(基于分组密码的 MAC,适用于 AES 场景) +- [ ] **poly1305** — Poly1305(通用 MAC,常与 ChaCha20 组合) + +### 口令派生(kdf) + +- [x] **bcrypt** — bcrypt(自适应成本因子密码哈希,抗 GPU/ASIC,基于 `bcryptjs` 标准库;含自动 salt 生成与验证接口) +- [x] **scrypt** — scrypt(内存困难型 KDF,抗 GPU/ASIC;可用 `crypto.scryptSync` 实现) +- [x] **argon2** — Argon2(密码哈希竞赛冠军;argon2id 平衡侧信道与 GPU 抗性;可用 `argon2` npm 包) +- [x] **hkdf** — HKDF(基于 HMAC 的 KDF,适用于从主密钥派生子密钥;可用 `crypto.hkdf` 或 `@noble/hashes`) + +### 工具与实用(tools,新分类) + +若实现以下功能,建议在 `src/registry/categories.ts` 新增 `tools` 分类: + +- [x] **uuid** — UUID v4 / v7 生成 +- [x] **random-bytes** — 随机字节生成(可指定长度、编码 hex/base64) +- [x] **jwt-decode** — JWT 解码(解析 header.payload,不做签名验证) +- [ ] **hash-file** — 文件哈希(用户选文件,计算 SHA-256/MD5 等) +- [ ] **base64-image** — Base64 ↔ 图片预览(logo、小图标嵌入场景) +- [x] **password-strength** — 密码强度估算(zxcvbn or 简易长度+字符集评分) +- [ ] **qr-generate** — QR 码生成(文本 → ASCII-art 或 base64 PNG) +- [x] **json-format** — JSON 格式化 / 压缩(常见文本工具) +- [ ] **jwt-verify** — JWT 签名验证(给定公钥或对称密钥) + +### 编码/序列化相关 + +- [ ] **json-escape** — JSON 字符串转义 / 反转义(`"\"中文\""` ↔ `"中文"`) +- [ ] **csv-escape** — CSV 字段转义(含逗号/引号/换行时加双引号) +- [ ] **quoted-printable** — Quoted-Printable 编解码(邮件 MIME) +- [ ] **percent-hex** — 百分比 + 十六进制(`%E4%B8%AD` + `%u4E2D` 两种变体) + +### 校验和 / 指纹 + +- [x] **crc32** — CRC32 校验和(ZIP、PNG 等协议常用) +- [ ] **adler32** — Adler-32 校验和(zlib 默认) +- [ ] **luhn** — Luhn 算法(信用卡号校验位计算与验证) +- [ ] **fletcher16/32** — Fletcher 校验和(TCP 替代校验) + +> 分类标签与 `categoryId` 定义在 `src/registry/categories.ts`,新增分类需在该文件 `CATEGORIES` 数组追加。 diff --git a/plugins/crypt-toolkit/README.md b/plugins/crypt-toolkit/README.md new file mode 100644 index 000000000..66a7a0201 --- /dev/null +++ b/plugins/crypt-toolkit/README.md @@ -0,0 +1,115 @@ +# crypt-tool + +> 加解密工具,高颜值,可自定义,UI 和谐统一,带加密算法的简要指引 + +基于 **React 19 + Vite 6 + TypeScript** 构建的 ZTools 插件,提供 9 种常用编解码与加密算法。 + +# 为什么选择`crypt-tool`? + +1. 现代化界面设计,和ZTools原生设计风格一致,原生软件般体验 +2. 支持算法广泛,可自定义界面功能 + +## ✨ 功能概览 + +### 友好的交互设计`一目了然` + +![img.png](docs/images/img.png) + +### 根据自己的需求定制内容 + +![img_2.png](docs/images/img_2.png) + +### 后续支持算法 + +[DEVELOP.md#后续算法计划](DEVELOP.md#后续算法计划) + +## 📁 项目结构 + +``` +. +├── public/ +│ ├── logo.png # 插件图标 +│ ├── plugin.json # 插件配置 +│ └── preload/ +│ └── services.js # 向渲染进程注入 crypt 服务 +│ └── crypt/ # Node.js crypto 实现 +│ ├── encoding.js # base64 / hex / url +│ ├── hash.js # md5 / sha256 +│ ├── aes.js # AES-CBC / AES-GCM +│ ├── rsa.js # RSA-OAEP 加解密 / 密钥生成 +│ └── hmac-pbkdf2.js # HMAC 签名验证 / PBKDF2 +├── src/ +│ ├── main.tsx # 入口 +│ ├── App.tsx # 根组件:加载设置、监听 onPluginEnter +│ ├── shell/ # 侧边栏 + 算法宿主 + 设置页 +│ │ ├── Shell.tsx +│ │ ├── Sidebar.tsx +│ │ ├── AlgorithmHost.tsx +│ │ └── SettingsPage.tsx +│ ├── registry/ # 算法注册表与分类 +│ │ ├── types.ts # AlgorithmModule / AlgorithmMeta +│ │ ├── categories.ts # 六大类别定义 +│ │ └── algorithms.ts # 9 个算法 + 筛选帮手 +│ ├── config/ # dbStorage 设置 + 动态功能同步 +│ │ ├── settings.ts +│ │ └── features.ts +│ ├── shared/ # 跨算法复用 UI +│ │ ├── Field.tsx # 输入/输出字段(text/secret/textarea/select) +│ │ ├── TeachCard.tsx # 算法说明卡片 +│ │ ├── Actions.tsx # 按钮组 +│ │ ├── CopyButton.tsx # 复制 + toast +│ │ └── crypt-shell.css # 全部视觉样式 +│ ├── algorithms/ # 每种算法的 meta + ui + barrel +│ │ ├── codec.ts # 编解码辅助(runCodec / showError / showData) +│ │ ├── base64/ +│ │ ├── hex/ +│ │ ├── url/ +│ │ ├── md5/ +│ │ ├── sha256/ +│ │ ├── aes/ +│ │ ├── rsa/ +│ │ ├── hmac/ +│ │ └── pbkdf2/ +│ └── types/crypt.ts # CryptResult 类型 +├── docs/ +│ └── superpowers/ +│ ├── specs/2026-09-22-crypt-tool-design.md # 架构设计 +│ └── plans/2026-09-22-crypt-tool.md # 实现计划 +└── package.json +``` + +## 🚀 快速开始 + +```bash +npm install +npm run dev # 开发模式,ZTools 自动加载 localhost:5173 +npm run build # 构建到 dist/ +npm test # 运行 vitest 全套测试 +``` + +命令行直达(开发模式下可在 ZTools 搜索框触发):`base64`、`hex`、`url编码`、`md5`、`sha256`、`aes加密`、`rsa加密`、`hmac`、 +`pbkdf2`。 + +## 🧩 如何新增一种算法 + +1. **Preload 实现**:在 `public/preload/crypt/` 新增 `.js`,用 `envelope.js` 的 `ok/fail/tryCrypt` 包裹并暴露 + `{ ok, data/error }` 结果;在 `index.js` 中挂载导出。 +2. **类型**:在 `src/env.d.ts` 的 `CryptNamespace` 中追加对应方法签名。 +3. **算法模块**:在 `src/algorithms//` 下创建 `meta.ts`、`ui.tsx`、`index.ts`,导出 `AlgorithmModule`。 +4. **注册**:在 `src/registry/algorithms.ts` 的数组中加入新模块。 +5. **同步**:`syncFeatures` 会自动将它注册为直达指令 `alg:`。 + +详细设计见 `docs/superpowers/specs/2026-09-22-crypt-tool-design.md`。 + +## 📦 脚本 + +| 命令 | 作用 | +|-----------------------------------------|----------------------| +| `npm run dev` | 启动 Vite 开发服务器 | +| `npm run build` | tsc 类型检查 + Vite 生产构建 | +| `npm test` | 运行 vitest 单元测试 | +| `npm run build` 成功后会输出到 `dist/`,可直接用于分发 | + +## 📄 协议 + +MIT License diff --git a/plugins/crypt-toolkit/docs/images/img.png b/plugins/crypt-toolkit/docs/images/img.png new file mode 100644 index 0000000000000000000000000000000000000000..08b1f88905a9c9a5ea1f25537e360492a611d409 GIT binary patch literal 162573 zcmZU4Wmwd0^ER=>(y)Lug5V;Jpup0N!~&Aiq9`RSuuIo2xqyU{%1WuEgn-hGAShiT z-Q6km{=Dz!`M-4>eBjs*d(Aa-&N=7IHH+1UYfzH0km2FsQ9jUAeTat#V!^{BBqk*Q zJ{h7!Jj25a!F!;pWaMMLiHppfYOiTJp+5aUAjUiH>{ijRaWql4F}+*s%_@nIv~?le zez_b0zWv3>)1(#$;lPQa1@9JgTd+Arb@2qw< zSIXr*)x~eIKX|?zC_5<4J5@U-asOCg%hFm9<}KV4D@*Czbh#@^s+-Sz0Pgdpaq;V$`nO>@xw3 zS;eO_nQ_z96Qq2DQ*fC}LsCR2qDxrHfiBr9!+j<@M*PYoCE1xzq?E2_Kbqf-Im1rxUSA`Fj!F{Np&p*zE1nV<%!F>Wsk}cRn$wp zad9Z!RaMPD_S+*7F~?rXOD5kBn_V$K3yF;DbSIRhCK612r)Rh+y_6sC%Jn{c^e4WS# zPHp;cwoPvct+)&8VPQ2Y*}0_GySAWCsa8)%`6vub)l7Py_U?6iq>eWa$)8_I%)MtC#+@9gE zo+Y)}E`=}2fKj2hCH_m0?0t?|-v|E4tyv~eC+nD z#Z4Ry_v0Lhd9LD%emcP)49dKD5WACVxT-IacZ6s?VXIsV%{Jj2)5u@bq9y^;R(Dfu zX=s#Igc;J(Z|pB5_O$4k+ua=mC&mqdf0Vbksa%KgLE}oJUxDbcWv^27FSr;Lrn)0p z_T=B6QS8>g@31R>j}Kt^=6Dy%Vf=jWE=!rsPqZ+tu{C?^B!0O)ikoB%n_-Zj9o$Q{ zKM3agamf{X{}aS__#fv?2Pm7Suc~&8X*yjazTm8qmY?nJu&F;Atn5n%n59U$TE z(vkK=;9K0=Cj0%h8+!5!td;L2mh>C>6nP6S#H`*lj-K&2%~kgv+3Z;6vJe3P%T|OL zEVB?Pc%=;yguUz11iZ_^aRTjBFqEC?unM0l`Tg6;lDoJ}EncQkStOBA-Of$+Teh5Q z$O^B}f6zwI5<%@1qHaL>AG8&zq{-Z_^}#lGb$zpw6b-pK_|mT9t`0QJK&l5opw;6N z%Eyby*57^ib0KEDg^ceyD-|!6A+`MKP?%Q<>`UcUK3pYEnB7Zf?SXVEAL~eVMp%nG zcA@CDj(Naddxy+zq3Jnck&kTMJF`q3O#ExiYAYXewe@nm;?|`W#|Nj`WxN^GTNIu- zcEO?TJ(Gz}La>2|Ii+C!NzZoiz^pc7KkJv;s!3lRLaddEBIYK2VpaSn@%psQ1sSzj zvR;dUFqE8|gqQCUQYk|IlgR5d%TzJ>5NmEJZvP>A*(f8zM=#;XWC%SMY>t^|415QV zMJ{R-xZ5^GcGr}%GYq}-#A(h~hrG#lJUN$33(7aY(_gUX;DLp!#INZHi&^$+M?Jtk z5$I$$z1QeDh1B3-losYnNm4oMG>-G^mKqhS4Si1ER=|Fj%;{hpPT9(L+9^o6hQwqW zk|ujN(=YEvDG1F88!#SyD)mVrG8?2L_M)d_3iX_=b`@QqODE|$7sSeFh`n*bg2|jB&2#S;EIpAq za{37y=vY@(w{5qPD*I|bRl+FfGJt6aBa3a}kjj4>yKOcOu=`-hWh^s|%c-qo{$XT2 zf@;;A;V)+aD0(fQgCx-Q(s&+)cPu21>tcs+?6I~6D&muO{zDf*E$xs(h>gf6`Pzl% zTo{#fEi!+Du6H=hKYqv|*xIhu-53(T11oseW#@q!7}2d3N)|oydL1PXY|R zY~eCWT(Nf5Pv}3W#IH-G#bUm+Dm;sDkmGTn0wXaqJD^T&rBzjeZApWuO5wT98S>=|C=#~&DXtUR-{Rq3f>?clYj z)iBm>3tUvlc?bbxT}^))E`{1j9btkbD}B_^&K^aGi$Ro3O1Lu<22?*on$ z)T}qCgorju-b_}=w}tljSqPbp=aAG!&E_+cQLS`W_maOI{%j_g($0rZxZ}l*CI#hC zS#^?zXEW|NvJY3$cE}Ew|9J3K8JBN_RDNH>lcnhp^9a}6%HhK?)GCtZa|kn-sF^Ra z;ar`JU!2@rl$RR#u;y&lp4t{yz*(OCc<~#gD349NRd|kY`DNez4sT)srQRP zr7VhkNniexT=_Gn%l4NIP*-FVv6DoV;#Re!OicxZoh10E9slCVv#380LUnkp7o`Ur zkU7aLYW7lQ)giV|-4vfOay4^|zT)ba28Fnf5t4Y=tFDCWIz0mDNXzFYE!oDKvXFSX zXvG=Qa8y-RkG*r1y+@Q8Z>p;=Zr1uSsui?NNM$^3-^%`g6e${cvEOqBSx87e(()G@e zcrLS4r!I!o@?uA-H>Lm1FS#q@PFJbGWO>P5et{j-Qj`B=wFQ4?R`s2V_^Z zmQ@ceT1$A!3EBE``LgTX2B?CF9$W-5FDoPY=R&vNG2wQD+*_M|6JFm+R2Z9BCPY=X7E-ot1ujQ}iR|-2_>0wGsbrQX6l22lW z!Oy)~8-Cq+g45qOet@v~*bjb2pN$!aizR6L6Kl{%UUJmZ2$)YZDzF#GXJ%H=8i}wM zTv@s_|EF?4rK0AUrwycEKHsgLY1!e)Mm!a2()$)rqZP`;>$xV2vKwA4m+`h%*H>;& zlnv7@gtSGxWP%lAQ{=79p%i>{CeTVCe}oCw{^pMuEV~yjL1AG!QnouDUEWzULoKu2 zT4ySE9xRY~rr?3Ccz4?QS>xJ$`PQ}6LUx~C@a%!wNl@eNyS|l~qZ2NHZDb=}p@-~9 zAzNA5N&8Xw8{BH^K{|A?2r|eg0W%n#ehJ*lC1hwWq$8m0N!22y>akW7CmExO)83v1 zbC>Vq*XDm~1cPjXXbzhPy(qoA`}qStHq)N16fwia4O&8b;TInvQ_fCzZr{d@*BlHo z2#kHQtV)T+V7dwmT~VkbG|l}2c)+=gBxm)(yWc153H?1g0V~gN;#rzzxPDByTs6ge z5`rePR@D3kVchy^P=#T^!B6;-+{!Se`AGKUnb;f3YtuN!CmgEPIy zW>z-cVTY!E!>yr&KZrm@hFEpk^e6Ym6wj*LdwQ;;MMUv^kKjvx7rTeZcL{v%R))=9x_% zeUrWBVG3jDJd{s#Xd&tz8O6pBLEcRg#^V;@{}ZJh8m{)F|K*D>K$6KZMV5uAk~7BP z!w{Temdk_guz`w_vF)ju&4JvNNni4>HpspC4sF#_spq|zBi8%6@9uv3@@4z2sg>1K zZr+X7Z|kaYA#j~H5-+aNbtAJ^%{Xtv+fpi^S-NQW;pVTp+FFzMoPMX})(xzyy!KyM za8Wdb!h}loc6tg2mY_8hh9w<_f-M^^8|o><%-IOe!S;^uZBRkptu-?R+VPL%{s=?j z^@eqSz3HpLMMct$lNb*0c9HV2grL$$w|b*H{BJ3 zTGLYEL9kZ8cK7D5iqs}Gx6Y^BD|BZYs?znb!`KieynGrz9BEN97OsQ4&;Al)Eq{4f zVVL)``KD+J(4GZ<>m9&GC^_rpzuB!4)@AQj?`^z-p4PX&@3A>mj)di?oRm|NpTG|2 zzDoAh)c8(&v!|K`yf{cYQrMndRUHCPL5KCspvZYXLR_^&r)1F4#!S%ZKwf6R#KrWC z{CMzeZ$9#xcFe`<^FH67>zCI zoM{4jDkBP_UTv!BD2xT3jNxdedCu%WfN=2*TZNlwe)PJOi@<_pN_^>G$EHI_={^&v z&{Akp6iFqi#B~klQUz}_vE)M93yqz}UW_k(6xi5cO%<_G8Xh~L#a}vRpOJd<{j(+g z+plmbEFUhPVhCtU+q|)EueCEo-mr_=#d=aFzx9fp|1J5)lO9lH7OLF1Sv#7~_bE!B z>O%?a?+#$^ibiK>Cqd6f(E7O3Zt!K(pCj}(s3RdrrPni<$=~N!2$8_Mls@eM;diNN zKEL9_v^9x8)o%>Ho%F?CJ6R*Qp6A(&)Gu}~3pd=To>@aa%q&zti4;Pku9&ehuVImftnCps_F_xS3ZGy0DXanV8>UB&ahv#Fh#i;kXBr27aaAmnt( zxwL6-`S%UWvh+ctEbS<`&sj9jA`+u(=~GFWJ8Mu!ck*Oz*ZdEo-J(+Xy+JkcK-$sQ zPNt98d_bAxOSGFH9(}of`nX@+voDtxod3!IuI3;SR-eOCu;zS^f7GiL(rtcvW<4Ezr2} z9sHCKHunJNS$3mUVc9H1(P5?`H6StP(E|VD&14aV+%6qxJL_0=m^xJAUsspqKoY)|x)UN0i|6XW0+Qa6>Mz4HDYoY%v&z+#s$pG{PI$Q9vE({)SJ#gUuQY`@L)@8H=I4!L!{;O=2Zg@EIl6NS?# zbN`cglG3^>3ws^tM3#Z76dog00mDdC?9mf_i~dJpioYX)nB4U0BiegxOMMfq?lJ4| z;;ze+$G~jC5x-2Pn{FA;>}K)Pd}#-9!u*A-RH`v-k8qvGRD|aPe%1HY0cT`%U|RWy z80(ap^0nEPHj`p=4fVP9&y&2Wwp%)eA!;C`-Qnuq`F6G82-r{B@32}E7xmGiuXg(D z&hOG%P`S&Q^bMEk^`=e!*!eHNPsWZnc`cQ5A(Pzu{a{}jlhQ{w(EP;ayrdcJDu5P;M#>m`cx*D)+F{y?m7`Ln&8_9M9K1-}BE%(dF$<=)bjgtk(sim&27%3CBfTgoPnnU(3z`aZ~w#9vw6Ls zKLBZEjox>aJy|jhIvYEgo^jvfZG_9>-u?_(C}Iltdf^_0z!PM9C{UkhTg(|gK~Qo8 z-jJ(ujv;LS)sj|c94b@XOtLWB9J}Pn(AMk)497m$H*?(a|2&M{Px!&MuHU7hXTt-M zh_d;-G3aSh?BucuMcHkbyo6J*NjzP0S}6jQB7-IOLuh>`^x`=yfqT=nOQ=8e;WTV6 zX?Pt}17Z#@fahDc%rUlHpf}nlj;Ne@XkjgZH(xQ1fxWFU3L3Qi;9I#-{n$nu6w*%7 zaU^NGfsq`?^mPmWA`sgf%9smJ(9S3yOHVa}3zt{;F2yZB@Z5pG243_#;+*ne@qRL} zfg6%SK8NK{s>>xgn1G;7K7n-TkF&XozJ~M8(C5NdL3@&3gN1T4lLFncXT##SDT{Zm zb(e7f!dLfuN{w8-x~Ir^QFeb0)0@sOSxXy^i(bQ>axOk-ry5pVUk;`AO8a+YQgh@8 zMei>{-$SV7PZ7Y>WPsvcbaa*x{gAN4K=5sDH2Wio9O312+q{A`fo#Q?Rze3krpB+t z2n7a@%zJFH$p*{{4QIIaGS{|z9i_j{2KK7WY7poust`kxhNYZG+wc?{-^?; zEOPl4$CgUE8dm$&l7|4s^%EaO5T{-#s#+Y~T!iVp*<#EGj_^5=(#3609ksi8b*u0z zqvm@$PqNLf1)zrt>OejVn{a;o4t_)BTwE$zv%z3SJj@u z3^-n`Wm823B!O@=88ygXDW86;nfucJb_d&CM(7rql-_~33#s>s!RE@JZAF}O`kWpI zk{nODuK-FG!1-OR{vO~%ho2Ki(k)#!P=3#NJAZZejZfznoVZnfD`mbg1U3Mryg|2J z_br!Jh=R6=+d&$aAUxNQahs#V%f=%yN;29Ex9aWUXjuBqb277%b<%B*V{1p7!ra$% z4exnej>=d$Ia%{T#xnq6tT`w3=F7k)F)RT8>eu3vp>UPe4pL1^Sb7qnbfwwl>5Bi$2 zaJcGo|C6vF0~fJajB`i{M5zoxwfR;@F_Z5d9oN}3sS zWGK+^4BoXJGvYJ1gdQsLD2nYZ3;kZ{wO9?ur?GpeSmilpZ*07Mqr>iSlQtKU+?zYD z+a5*xgv0;hj$M0~7{Fs$85!D(u>=gn*O|L(;vz}&D-!d%H4aTzjxjRUxiG@gr-prxF~|oPCOcFWNE>a=g2gNoFDw1?2D-g-Ck4K8u!K4 z@Vpnlyas^g$$xkhXrqj+tlXgRj@4gK6DSC%8k~WfcQAC8JRa(3M%sD0MmTA1eo4vJ z$nWz3u<{wROh5R`SNw3&5pcj*M*dl3l(Bl$3UjNzb`F19h%nb~@HZ+o?r(g5^E?hN z5!8{MyE-;ry_^PDnVR{|A+fb?O3>ubeh~=X@x}Hutnjwiw?`bj>62QpFVq%dGlW$J z9h#d$yNp?GA#*C6>4BoFS1$8_$bGlp#`W~rI~*>{F^tuH+pBPxdZNiNNm49_^I2QD zK0lTTejnX*-DW|5N$y*{bsRWP{>HU-jd2*8cuPa>&p6~M6`;9wem89Yal`g47C*{M zFbDu@o6+7&$(iX|lz(b--?*BNhH9}E`B;c;DJ^lz3Hn_Z%^`^ZT?et(IASF?#A}{4 z_QlZOyrX}PQ-)wLf4@7L$%?~7Vwf~8v$S>3ypsfF@zSXji-{db#Cj5V< z_oTQ@`wd8zPG~XbTu zae2Mkd`%(-qtEwV33MFLWqukTsPD$5ngwq^e|ulzgtan!{ULesOV8TjpIP^tkd}m1 zdKN9Z(B=WGO0XPaWvaJF*#2&RDp6A1+jhu!8ie|sne5)o^#ih?0J*2>_03)scn|bL zAo}2bqG-$8+D*9RKwu;>0p#HliCeQ zw#`RlMV|`=c)z&*E)Qy}5ilvPwL9S;IPO7w%0+0vRUu%Nzc8RD^-%6^V`K6)9RZvpZ0m~iHI*&4ha6LY;>Sy zhuM)YR+sgSp3Ujtm{w#ANvQzqa>|K(2Q9dh*U#`A5Ryo1OW?vg!Mlm4*)WkIO=#up z^H;gVm$XH%`iyLS36WPf=gyhKTXeP3+X(-}id94j`?h!Ufy*lmVrXn-ZrjTI + + + + + + + + + + + + + + + + + + diff --git a/plugins/crypt-toolkit/public/plugin.json b/plugins/crypt-toolkit/public/plugin.json new file mode 100644 index 000000000..c4d97b86d --- /dev/null +++ b/plugins/crypt-toolkit/public/plugin.json @@ -0,0 +1,30 @@ +{ + "$schema": "node_modules/@ztools-center/ztools-api-types/resource/ztools.schema.json", + "name": "crypt-toolkit", + "title": "CryptToolkit", + "description": "加解密工具,高颜值,可自定义,UI和谐统一,带加密算法的简要指引", + "homepage": "https://github.com/Code-Agitator/ztools-crypt-tool", + "author": "Agitator", + "version": "1.1.0", + "main": "index.html", + "preload": "preload/services.js", + "logo": "logo.svg", + "development": { + "main": "http://localhost:5173" + }, + "features": [ + { + "code": "crypt", + "explain": "编码 / 加解密 / 哈希工具", + "icon": "logo.svg", + "cmds": [ + "编码", "解码", "加密", "解密", "哈希", "散列", "hash", + "base64", "url编码", "urlencode", "hex", "十六进制", + "md5", "sha", "sha256", "sha512", + "aes", "rsa", "hmac", "pbkdf2", "bcrypt", + "签名", "验签", + "crypt", "encode", "decode", "encrypt", "decrypt" + ] + } + ] +} diff --git a/plugins/crypt-toolkit/public/preload/crypt/adler32.js b/plugins/crypt-toolkit/public/preload/crypt/adler32.js new file mode 100644 index 000000000..ee3cf9b68 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/adler32.js @@ -0,0 +1,19 @@ +const { ok, tryCrypt } = require('./envelope') + +const MOD_ADLER = 65521 + +function checksum({ input }) { + return tryCrypt(() => { + const buf = Buffer.from(String(input), 'utf-8') + let a = 1 + let b = 0 + for (let i = 0; i < buf.length; i++) { + a = (a + buf[i]) % MOD_ADLER + b = (b + a) % MOD_ADLER + } + const result = ((b << 16) | a) >>> 0 + return ok(result.toString(16).padStart(8, '0')) + }) +} + +module.exports = { checksum } diff --git a/plugins/crypt-toolkit/public/preload/crypt/adler32.test.ts b/plugins/crypt-toolkit/public/preload/crypt/adler32.test.ts new file mode 100644 index 000000000..2fe9dadf7 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/adler32.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { checksum } from './adler32' + +describe('adler32', () => { + it('known vector: "a" → 00620062', () => { + const r = checksum({ input: 'a' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('00620062') + }) + + it('known vector: "abc" → 24d01275 (but 8 hex)', () => { + const r = checksum({ input: 'abc' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.length).toBe(8) + } + }) + + it('empty string returns 00000001', () => { + const r = checksum({ input: '' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('00000001') + }) + + it('consistency - same input same output', () => { + const a = checksum({ input: 'hello' }) + const b = checksum({ input: 'hello' }) + if (a.ok && b.ok) expect(a.data).toBe(b.data) + }) + + it('different input different output', () => { + const a = checksum({ input: 'hello' }) + const b = checksum({ input: 'world' }) + expect(a.ok).toBe(true) + expect(b.ok).toBe(true) + if (a.ok && b.ok) expect(a.data).not.toBe(b.data) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/aes.js b/plugins/crypt-toolkit/public/preload/crypt/aes.js new file mode 100644 index 000000000..a528aaf67 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/aes.js @@ -0,0 +1,67 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function validate({ key, iv, mode }) { + const keyBuf = Buffer.from(String(key), 'utf-8') + if (![16, 24, 32].includes(keyBuf.length)) { + return fail('密钥必须为 16 / 24 / 32 字节(UTF-8)') + } + const ivBuf = Buffer.from(String(iv), 'utf-8') + if (ivBuf.length !== 16) { + return fail('IV 必须为 16 字节(UTF-8)') + } + if (mode !== 'CBC' && mode !== 'GCM') { + return fail('模式仅支持 CBC 或 GCM') + } + return { keyBuf, ivBuf, mode } +} + +function encrypt({ key, iv, mode, plaintext }) { + return tryCrypt(() => { + const v = validate({ key, iv, mode }) + if (v.ok === false) return v + const cipher = crypto.createCipheriv( + `aes-${v.keyBuf.length * 8}-${mode.toLowerCase()}`, + v.keyBuf, + v.ivBuf + ) + const ct = Buffer.concat([cipher.update(String(plaintext), 'utf-8'), cipher.final()]) + if (mode === 'GCM') { + const tag = cipher.getAuthTag() + return ok(Buffer.concat([ct, tag]).toString('base64')) + } + return ok(ct.toString('base64')) + }) +} + +function decrypt({ key, iv, mode, ciphertext }) { + return tryCrypt(() => { + const v = validate({ key, iv, mode }) + if (v.ok === false) return v + const raw = Buffer.from(String(ciphertext), 'base64') + try { + if (mode === 'GCM') { + if (raw.length < 17) return fail('密文过短或已被破坏') + const tag = raw.subarray(raw.length - 16) + const ct = raw.subarray(0, raw.length - 16) + const decipher = crypto.createDecipheriv( + `aes-${v.keyBuf.length * 8}-gcm`, + v.keyBuf, + v.ivBuf + ) + decipher.setAuthTag(tag) + return ok(Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf-8')) + } + const decipher = crypto.createDecipheriv( + `aes-${v.keyBuf.length * 8}-cbc`, + v.keyBuf, + v.ivBuf + ) + return ok(Buffer.concat([decipher.update(raw), decipher.final()]).toString('utf-8')) + } catch (e) { + return fail('解密失败:密钥/IV 不正确或数据已损坏') + } + }) +} + +module.exports = { encrypt, decrypt } diff --git a/plugins/crypt-toolkit/public/preload/crypt/aes.test.ts b/plugins/crypt-toolkit/public/preload/crypt/aes.test.ts new file mode 100644 index 000000000..8345d3108 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/aes.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' +import { aes } from './index.js' + +const key = '0123456789abcdef' +const iv = 'abcdef0123456789' + +describe('aes', () => { + it('CBC roundtrip', () => { + const enc = aes.encrypt({ key, iv, mode: 'CBC', plaintext: 'Hello ZTools' }) + expect(enc.ok).toBe(true) + if (!enc.ok) return + const dec = aes.decrypt({ key, iv, mode: 'CBC', ciphertext: enc.data }) + expect(dec).toEqual({ ok: true, data: 'Hello ZTools' }) + }) + + it('GCM roundtrip', () => { + const enc = aes.encrypt({ key, iv, mode: 'GCM', plaintext: 'Hello ZTools' }) + expect(enc.ok).toBe(true) + if (!enc.ok) return + const dec = aes.decrypt({ key, iv, mode: 'GCM', ciphertext: enc.data }) + expect(dec).toEqual({ ok: true, data: 'Hello ZTools' }) + }) + + it('rejects bad key length', () => { + const enc = aes.encrypt({ key: 'short', iv, mode: 'CBC', plaintext: 'x' }) + expect(enc.ok).toBe(false) + if (enc.ok) return + expect(enc.error).toMatch(/密钥/) + }) + + it('rejects bad iv length', () => { + const enc = aes.encrypt({ key, iv: 'bad', mode: 'CBC', plaintext: 'x' }) + expect(enc.ok).toBe(false) + if (enc.ok) return + expect(enc.error).toMatch(/IV/) + }) + + it('GCM fails on tampered ciphertext', () => { + const enc = aes.encrypt({ key, iv, mode: 'GCM', plaintext: 'Hello ZTools' }) + expect(enc.ok).toBe(true) + if (!enc.ok) return + const raw = Buffer.from(enc.data, 'base64') + raw[0] ^= 0xff + const dec = aes.decrypt({ key, iv, mode: 'GCM', ciphertext: raw.toString('base64') }) + expect(dec.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/argon2.js b/plugins/crypt-toolkit/public/preload/crypt/argon2.js new file mode 100644 index 000000000..d0bac8c6e --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/argon2.js @@ -0,0 +1,59 @@ +let argon2Lib = null + +function loadArgon2() { + if (argon2Lib) return argon2Lib + try { + argon2Lib = require('argon2') + } catch (e) { + return null + } + return argon2Lib +} + +const { ok, fail } = require('./envelope') + +async function hash({ password, type, timeCost, memoryCost, parallelism, salt, hashLength }) { + if (!password) return fail('密码不能为空') + const lib = loadArgon2() + if (!lib) { + return fail('argon2 库未安装,请在项目目录执行 npm install argon2') + } + const variant = type ?? 'argon2id' + const variantMap = { argon2i: lib.argon2i, argon2d: lib.argon2d, argon2id: lib.argon2id } + const argonType = variantMap[variant] + if (!argonType) return fail('类型仅支持 argon2i / argon2d / argon2id') + try { + const saltBuf = salt + ? Buffer.from(String(salt), 'utf-8') + : Buffer.from(require('node:crypto').randomBytes(16)) + const opts = { + type: argonType, + timeCost: timeCost ?? 3, + memoryCost: memoryCost ?? 65536, + parallelism: parallelism ?? 1, + hashLength: hashLength ?? 32, + salt: saltBuf + } + const h = await lib.hash(String(password), opts) + return ok(h) + } catch (e) { + return fail('哈希失败:' + (e?.message || e)) + } +} + +async function verify({ password, hash: hashString }) { + if (!hashString) return fail('哈希值不能为空') + if (!password) return fail('密码不能为空') + const lib = loadArgon2() + if (!lib) { + return fail('argon2 库未安装,请在项目目录执行 npm install argon2') + } + try { + const match = await lib.verify(String(hashString), String(password)) + return ok(match) + } catch (e) { + return fail('验证失败:' + (e?.message || '格式错误')) + } +} + +module.exports = { hash, verify } diff --git a/plugins/crypt-toolkit/public/preload/crypt/argon2.test.ts b/plugins/crypt-toolkit/public/preload/crypt/argon2.test.ts new file mode 100644 index 000000000..4fdeb0af0 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/argon2.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { hash, verify } from './argon2' + +describe('argon2', () => { + it('hashes with argon2id', async () => { + const r = await hash({ + password: 'mypassword', + type: 'argon2id', + timeCost: 2, + memoryCost: 16384, + parallelism: 1, + hashLength: 32 + }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toContain('$argon2id$') + expect(r.data).toContain('v=19') + } + }, 30000) + + it('verify succeeds with correct password', async () => { + const h = await hash({ + password: 'correct horse battery staple', + type: 'argon2id', + timeCost: 2, + memoryCost: 16384, + parallelism: 1, + hashLength: 32 + }) + expect(h.ok).toBe(true) + if (h.ok) { + const v = await verify({ password: 'correct horse battery staple', hash: h.data }) + expect(v.ok).toBe(true) + if (v.ok) { + expect(v.data).toBe(true) + } + } + }, 30000) + + it('verify returns false with wrong password', async () => { + const h = await hash({ + password: 'right', + type: 'argon2id', + timeCost: 2, + memoryCost: 16384, + parallelism: 1, + hashLength: 32 + }) + expect(h.ok).toBe(true) + if (h.ok) { + const v = await verify({ password: 'wrong', hash: h.data }) + expect(v.ok).toBe(true) + if (v.ok) { + expect(v.data).toBe(false) + } + } + }, 30000) + + it('supports argon2i variant', async () => { + const r = await hash({ + password: 'test', + type: 'argon2i', + timeCost: 2, + memoryCost: 16384, + parallelism: 1, + hashLength: 32 + }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toContain('$argon2i$') + } + }, 30000) + + it('verify fails with invalid hash string', async () => { + const r = await verify({ password: 'test', hash: 'invalid-hash' }) + expect(r.ok).toBe(false) + }, 10000) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/base32.js b/plugins/crypt-toolkit/public/preload/crypt/base32.js new file mode 100644 index 000000000..ba98f368a --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/base32.js @@ -0,0 +1,62 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' +const PAD_CHAR = '=' + +function base32Encode({ input, padding }) { + return tryCrypt(() => { + const buf = Buffer.from(String(input), 'utf-8') + if (buf.length === 0) return ok('') + let bits = 0 + let value = 0 + let output = '' + for (let i = 0; i < buf.length; i++) { + value = (value << 8) | buf[i] + bits += 8 + while (bits >= 5) { + bits -= 5 + output += ALPHABET[(value >>> bits) & 0x1F] + } + } + if (bits > 0) { + output += ALPHABET[(value << (5 - bits)) & 0x1F] + } + // Default: apply RFC 4648 padding unless explicitly disabled + if (padding !== false) { + while (output.length % 8 !== 0) { + output += PAD_CHAR + } + } + return ok(output) + }) +} + +function base32Decode({ input }) { + return tryCrypt(() => { + const str = String(input).replace(/=+$/, '').toUpperCase().replace(/\s+/g, '') + if (!str) return ok('') + if (!/^[A-Z2-7]+$/.test(str)) { + return fail('Base32 字符无效') + } + const decodeMap = {} + for (let i = 0; i < ALPHABET.length; i++) { + decodeMap[ALPHABET[i]] = i + } + let bits = 0 + let value = 0 + const bytes = [] + for (let i = 0; i < str.length; i++) { + const ch = str[i] + if (decodeMap[ch] === undefined) continue + value = (value << 5) | decodeMap[ch] + bits += 5 + if (bits >= 8) { + bits -= 8 + bytes.push((value >>> bits) & 0xFF) + } + } + return ok(Buffer.from(bytes).toString('utf-8')) + }) +} + +module.exports = { base32Encode, base32Decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/base32.test.ts b/plugins/crypt-toolkit/public/preload/crypt/base32.test.ts new file mode 100644 index 000000000..7bdd26efa --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/base32.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import { base32Encode, base32Decode } from './base32' + +describe('base32', () => { + it('encode "f"', () => { + const r = base32Encode({ input: 'f' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('MY======') + }) + + it('encode "fo"', () => { + const r = base32Encode({ input: 'fo' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('MZXQ====') + }) + + it('encode "foo"', () => { + const r = base32Encode({ input: 'foo' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('MZXW6===') + }) + + it('encode without padding', () => { + const r = base32Encode({ input: 'foo', padding: false }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('MZXW6') + expect(r.data).not.toContain('=') + } + }) + + it('decode "MZXW6==="', () => { + const r = base32Decode({ input: 'MZXW6===' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('foo') + }) + + it('encode + decode round trip', () => { + const input = 'Base32 encoding test 12345' + const enc = base32Encode({ input, padding: true }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = base32Decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(input) + } + }) + + it('handles case insensitivity', () => { + const a = base32Decode({ input: 'nzxw6ytboi======' }) + const b = base32Decode({ input: 'NZXW6YTBOI======' }) + if (a.ok && b.ok) expect(a.data).toBe(b.data) + }) + + it('empty string', () => { + const enc = base32Encode({ input: '' }) + expect(enc.ok).toBe(true) + if (enc.ok) expect(enc.data).toBe('') + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/bcrypt.js b/plugins/crypt-toolkit/public/preload/crypt/bcrypt.js new file mode 100644 index 000000000..74475c593 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/bcrypt.js @@ -0,0 +1,35 @@ +const bcrypt = require('bcryptjs') +const { ok, fail, tryCrypt } = require('./envelope') + +const MIN_COST = 4 +const MAX_COST = 31 + +function hash({ password, cost }) { + return tryCrypt(() => { + const pwd = String(password ?? '') + if (pwd.length === 0) return fail('密码不能为空') + const rounds = cost ?? 12 + if (!Number.isInteger(rounds) || rounds < MIN_COST || rounds > MAX_COST) { + return fail(`计算成本需为 ${MIN_COST}-${MAX_COST} 的整数`) + } + const salt = bcrypt.genSaltSync(rounds) + const hashStr = bcrypt.hashSync(pwd, salt) + return ok(hashStr) + }) +} + +function verify({ password, hash: hashString }) { + return tryCrypt(() => { + const pwd = String(password ?? '') + const hash = String(hashString ?? '').trim() + if (hash.length === 0) return fail('哈希值不能为空') + if (pwd.length === 0) return fail('密码不能为空') + if (!hash.match(/^\$2[aby]?\$\d+\$/)) return fail('无效的 bcrypt 哈希格式') + const match = bcrypt.compareSync(pwd, hash) + return ok(match) + }) +} + +module.exports = { + bcrypt: { hash, verify } +} diff --git a/plugins/crypt-toolkit/public/preload/crypt/bcrypt.test.ts b/plugins/crypt-toolkit/public/preload/crypt/bcrypt.test.ts new file mode 100644 index 000000000..1428e348c --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/bcrypt.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest' +import { bcrypt } from './bcrypt.js' + +describe('bcrypt', () => { + it('hash produces valid bcrypt format', () => { + const result = bcrypt.hash({ password: 'test123', cost: 4 }) + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.data).toMatch(/^\$2b\$04\$[A-Za-z0-9./]{53}$/) + } + }) + + it('hash with default cost', () => { + const result = bcrypt.hash({ password: 'hello' }) + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.data).toMatch(/^\$2b\$12\$[A-Za-z0-9./]{53}$/) + } + }) + + it('hash fails on empty password', () => { + const result = bcrypt.hash({ password: '', cost: 4 }) + expect(result.ok).toBe(false) + }) + + it('hash fails on cost too low', () => { + const result = bcrypt.hash({ password: 'test', cost: 3 }) + expect(result.ok).toBe(false) + }) + + it('hash fails on cost too high', () => { + const result = bcrypt.hash({ password: 'test', cost: 32 }) + expect(result.ok).toBe(false) + }) + + it('verify correct password', () => { + const h = bcrypt.hash({ password: 'secret', cost: 4 }) + expect(h.ok).toBe(true) + if (h.ok) { + const v = bcrypt.verify({ password: 'secret', hash: h.data }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(true) + } + }) + + it('verify wrong password', () => { + const h = bcrypt.hash({ password: 'secret', cost: 4 }) + expect(h.ok).toBe(true) + if (h.ok) { + const v = bcrypt.verify({ password: 'wrong', hash: h.data }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(false) + } + }) + + it('verify compatibility with external bcrypt hash (cost=5)', () => { + // Hash bcrypt.generate from external with password='hello' cost=5 + const h = bcrypt.hash({ password: 'hello', cost: 5 }) + expect(h.ok).toBe(true) + if (h.ok) { + // Self-verify should pass + const v = bcrypt.verify({ password: 'hello', hash: h.data }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(true) + + // Wrong password should fail + const v2 = bcrypt.verify({ password: 'world', hash: h.data }) + expect(v2.ok).toBe(true) + if (v2.ok) expect(v2.data).toBe(false) + } + }) + + it('verify rounds=5 external hash self-consistency', () => { + const h = bcrypt.hash({ password: 'TestPassword', cost: 5 }) + expect(h.ok).toBe(true) + if (h.ok) { + const v = bcrypt.verify({ password: 'TestPassword', hash: h.data }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(true) + } + }) + + it('verify fails on invalid hash format', () => { + const result = bcrypt.verify({ password: 'test', hash: 'invalid-hash' }) + expect(result.ok).toBe(false) + }) + + it('verify fails on empty hash', () => { + const result = bcrypt.verify({ password: 'test', hash: '' }) + expect(result.ok).toBe(false) + }) + + it('verify fails on empty password', () => { + const h = bcrypt.hash({ password: 'secret', cost: 4 }) + expect(h.ok).toBe(true) + if (h.ok) { + const result = bcrypt.verify({ password: '', hash: h.data }) + expect(result.ok).toBe(false) + } + }) + + it('same password different hashes (random salt)', () => { + const r1 = bcrypt.hash({ password: 'test', cost: 4 }) + const r2 = bcrypt.hash({ password: 'test', cost: 4 }) + expect(r1.ok && r2.ok).toBe(true) + if (r1.ok && r2.ok) { + expect(r1.data).not.toBe(r2.data) + } + }) + + it('verify $2a$ legacy format compatibility', () => { + // Generate a hash then verify it still works + const h = bcrypt.hash({ password: 'legacy', cost: 4 }) + expect(h.ok).toBe(true) + if (h.ok) { + // Manually change version prefix to $2a$ to test backward compat + const legacyHash = h.data.replace(/^\$2b\$/, '$2a$') + const v = bcrypt.verify({ password: 'legacy', hash: legacyHash }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(true) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/chacha.js b/plugins/crypt-toolkit/public/preload/crypt/chacha.js new file mode 100644 index 000000000..bea75fb8f --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/chacha.js @@ -0,0 +1,66 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +const KEY_LEN = 32 +const NONCE_LEN = 12 +const TAG_LEN = 16 + +function validate({ key, nonce }, checkNonce = true) { + const keyBuf = Buffer.from(String(key), 'base64') + if (keyBuf.length !== KEY_LEN) { + return fail('密钥必须为 32 字节(Base64 编码)') + } + if (checkNonce) { + const nonceBuf = Buffer.from(String(nonce), 'base64') + if (nonceBuf.length !== NONCE_LEN) { + return fail('Nonce 必须为 12 字节(Base64 编码)') + } + return { keyBuf, nonceBuf } + } + return { keyBuf } +} + +function encrypt({ key, nonce, plaintext }) { + return tryCrypt(() => { + const v = validate({ key, nonce }) + if (v.ok === false) return v + const cipher = crypto.createCipheriv('chacha20-poly1305', v.keyBuf, v.nonceBuf) + const ct = Buffer.concat([cipher.update(String(plaintext), 'utf-8'), cipher.final()]) + const tag = cipher.getAuthTag() + return ok(Buffer.concat([ct, tag]).toString('base64')) + }) +} + +function decrypt({ key, nonce, ciphertext }) { + return tryCrypt(() => { + const v = validate({ key, nonce }) + if (v.ok === false) return v + const raw = Buffer.from(String(ciphertext), 'base64') + if (raw.length < TAG_LEN + 1) return fail('密文过短或已被破坏') + const tag = raw.subarray(raw.length - TAG_LEN) + const ct = raw.subarray(0, raw.length - TAG_LEN) + try { + const decipher = crypto.createDecipheriv('chacha20-poly1305', v.keyBuf, v.nonceBuf) + decipher.setAuthTag(tag) + return ok(Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf-8')) + } catch (e) { + return fail('解密失败:密钥/Nonce 不正确或数据已损坏') + } + }) +} + +function keygen() { + return tryCrypt(() => { + const key = crypto.randomBytes(KEY_LEN) + return ok(key.toString('base64')) + }) +} + +function noncegen() { + return tryCrypt(() => { + const nonce = crypto.randomBytes(NONCE_LEN) + return ok(nonce.toString('base64')) + }) +} + +module.exports = { encrypt, decrypt, keygen, noncegen } diff --git a/plugins/crypt-toolkit/public/preload/crypt/chacha.test.ts b/plugins/crypt-toolkit/public/preload/crypt/chacha.test.ts new file mode 100644 index 000000000..4ad9d6867 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/chacha.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest' +import { encrypt, decrypt, keygen, noncegen } from './chacha' + +describe('chacha20-poly1305', () => { + it('generates a valid 32-byte key (base64)', () => { + const r = keygen() + expect(r.ok).toBe(true) + if (r.ok) { + const buf = Buffer.from(r.data, 'base64') + expect(buf.length).toBe(32) + } + }) + + it('generates a valid 12-byte nonce (base64)', () => { + const r = noncegen() + expect(r.ok).toBe(true) + if (r.ok) { + const buf = Buffer.from(r.data, 'base64') + expect(buf.length).toBe(12) + } + }) + + it('encrypt + decrypt round trip', () => { + const key = Buffer.alloc(32, 0xab).toString('base64') + const nonce = Buffer.alloc(12, 0x12).toString('base64') + const plaintext = '你好,ChaCha20-Poly1305!' + + const enc = encrypt({ key, nonce, plaintext }) + expect(enc.ok).toBe(true) + if (enc.ok) { + expect(enc.data).not.toBe('') + const dec = decrypt({ key, nonce, ciphertext: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) { + expect(dec.data).toBe(plaintext) + } + } + }) + + it('fails decrypt with wrong key', () => { + const key = Buffer.alloc(32, 0xab).toString('base64') + const nonce = Buffer.alloc(12, 0x12).toString('base64') + const enc = encrypt({ key, nonce, plaintext: 'secret data' }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const wrongKey = Buffer.alloc(32, 0xff).toString('base64') + const dec = decrypt({ key: wrongKey, nonce, ciphertext: enc.data }) + expect(dec.ok).toBe(false) + } + }) + + it('rejects invalid key length', () => { + const r = encrypt({ key: 'short', nonce: 'dGVzdHRlc3R0ZXN0', plaintext: 'hi' }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid nonce length', () => { + const key = Buffer.alloc(32, 0xab).toString('base64') + const r = encrypt({ key, nonce: 'short', plaintext: 'hi' }) + expect(r.ok).toBe(false) + }) + + it('rejects tampered ciphertext', () => { + const key = Buffer.alloc(32, 0xab).toString('base64') + const nonce = Buffer.alloc(12, 0x12).toString('base64') + const enc = encrypt({ key, nonce, plaintext: 'hello' }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const raw = Buffer.from(enc.data, 'base64') + raw[0] ^= 0xff + const tampered = raw.toString('base64') + const dec = decrypt({ key, nonce, ciphertext: tampered }) + expect(dec.ok).toBe(false) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/des.js b/plugins/crypt-toolkit/public/preload/crypt/des.js new file mode 100644 index 000000000..511e4aba2 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/des.js @@ -0,0 +1,43 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +const KEY_LEN = 24 +const IV_LEN = 8 + +function encrypt({ key, iv, plaintext }) { + return tryCrypt(() => { + const keyBuf = Buffer.from(String(key), 'base64') + const ivBuf = Buffer.from(String(iv), 'base64') + if (keyBuf.length !== KEY_LEN) return fail('密钥必须为 24 字节(Base64 编码)') + if (ivBuf.length !== IV_LEN) return fail('IV 必须为 8 字节(Base64 编码)') + const cipher = crypto.createCipheriv('des-ede3-cbc', keyBuf, ivBuf) + const ct = Buffer.concat([cipher.update(String(plaintext), 'utf-8'), cipher.final()]) + return ok(ct.toString('base64')) + }) +} + +function decrypt({ key, iv, ciphertext }) { + return tryCrypt(() => { + const keyBuf = Buffer.from(String(key), 'base64') + const ivBuf = Buffer.from(String(iv), 'base64') + if (keyBuf.length !== KEY_LEN) return fail('密钥必须为 24 字节(Base64 编码)') + if (ivBuf.length !== IV_LEN) return fail('IV 必须为 8 字节(Base64 编码)') + try { + const decipher = crypto.createDecipheriv('des-ede3-cbc', keyBuf, ivBuf) + const pt = Buffer.concat([decipher.update(Buffer.from(String(ciphertext), 'base64')), decipher.final()]) + return ok(pt.toString('utf-8')) + } catch (e) { + return fail('解密失败:密钥/IV 不正确或数据已损坏') + } + }) +} + +function keygen() { + return tryCrypt(() => ok(crypto.randomBytes(KEY_LEN).toString('base64'))) +} + +function ivgen() { + return tryCrypt(() => ok(crypto.randomBytes(IV_LEN).toString('base64'))) +} + +module.exports = { encrypt, decrypt, keygen, ivgen } diff --git a/plugins/crypt-toolkit/public/preload/crypt/des.test.ts b/plugins/crypt-toolkit/public/preload/crypt/des.test.ts new file mode 100644 index 000000000..6eb90c09a --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/des.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' +import { encrypt, decrypt, keygen, ivgen } from './des' + +describe('des-ede3-cbc', () => { + it('generates a valid 24-byte key', () => { + const r = keygen() + expect(r.ok).toBe(true) + if (r.ok) { + expect(Buffer.from(r.data, 'base64').length).toBe(24) + } + }) + + it('generates a valid 8-byte IV', () => { + const r = ivgen() + expect(r.ok).toBe(true) + if (r.ok) { + expect(Buffer.from(r.data, 'base64').length).toBe(8) + } + }) + + it('encrypt + decrypt round trip', () => { + const key = Buffer.alloc(24, 0xab).toString('base64') + const iv = Buffer.alloc(8, 0x12).toString('base64') + const plaintext = '你好,3DES!' + + const enc = encrypt({ key, iv, plaintext }) + expect(enc.ok).toBe(true) + if (enc.ok) { + expect(enc.data).not.toBe('') + const dec = decrypt({ key, iv, ciphertext: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(plaintext) + } + }) + + it('rejects invalid key length', () => { + const r = encrypt({ key: 'short', iv: Buffer.alloc(8).toString('base64'), plaintext: 'hi' }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid IV length', () => { + const key = Buffer.alloc(24).toString('base64') + const r = encrypt({ key, iv: 'short', plaintext: 'hi' }) + expect(r.ok).toBe(false) + }) + + it('fails decrypt with wrong key', () => { + const key = Buffer.alloc(24, 0xab).toString('base64') + const iv = Buffer.alloc(8, 0x12).toString('base64') + const enc = encrypt({ key, iv, plaintext: 'data' }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const wrongKey = Buffer.alloc(24, 0xff).toString('base64') + const dec = decrypt({ key: wrongKey, iv, ciphertext: enc.data }) + expect(dec.ok).toBe(false) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/ecdh.js b/plugins/crypt-toolkit/public/preload/crypt/ecdh.js new file mode 100644 index 000000000..d61dabe17 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/ecdh.js @@ -0,0 +1,33 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function generateKeyPair({ curve }) { + return tryCrypt(() => { + const c = curve || 'prime256v1' + const ecdh = crypto.createECDH(c) + ecdh.generateKeys() + return ok({ + publicKey: ecdh.getPublicKey('hex'), + privateKey: ecdh.getPrivateKey('hex'), + curve: c + }) + }) +} + +function deriveSharedSecret({ privateKey, peerPublicKey, curve }) { + return tryCrypt(() => { + if (!privateKey) return fail('自身私钥不能为空') + if (!peerPublicKey) return fail('对方公钥不能为空') + const c = curve || 'prime256v1' + try { + const ecdh = crypto.createECDH(c) + ecdh.setPrivateKey(String(privateKey), 'hex') + const secret = ecdh.computeSecret(String(peerPublicKey), 'hex') + return ok(secret.toString('hex')) + } catch (e) { + return fail('ECDH 密钥协商失败:' + (e.message || '密钥无效')) + } + }) +} + +module.exports = { generateKeyPair, deriveSharedSecret } diff --git a/plugins/crypt-toolkit/public/preload/crypt/ecdh.test.ts b/plugins/crypt-toolkit/public/preload/crypt/ecdh.test.ts new file mode 100644 index 000000000..4c798143e --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/ecdh.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { generateKeyPair, deriveSharedSecret } from './ecdh' + +describe('ecdh', () => { + it('generates P-256 key pair', () => { + const r = generateKeyPair({ curve: 'prime256v1' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.publicKey).toMatch(/^[0-9a-f]+$/i) + expect(r.data.privateKey).toMatch(/^[0-9a-f]+$/i) + } + }) + + it('two parties derive same shared secret', () => { + const a = generateKeyPair({ curve: 'prime256v1' }) + const b = generateKeyPair({ curve: 'prime256v1' }) + if (a.ok && b.ok) { + const secretA = deriveSharedSecret({ privateKey: a.data.privateKey, peerPublicKey: b.data.publicKey, curve: 'prime256v1' }) + const secretB = deriveSharedSecret({ privateKey: b.data.privateKey, peerPublicKey: a.data.publicKey, curve: 'prime256v1' }) + expect(secretA.ok).toBe(true) + expect(secretB.ok).toBe(true) + if (secretA.ok && secretB.ok) expect(secretA.data).toBe(secretB.data) + } + }) + + it('supports secp384r1', () => { + const r = generateKeyPair({ curve: 'secp384r1' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data.curve).toBe('secp384r1') + }) + + it('rejects empty private key', () => { + const peer = generateKeyPair({}) + if (peer.ok) { + const r = deriveSharedSecret({ privateKey: '', peerPublicKey: peer.data.publicKey }) + expect(r.ok).toBe(false) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/ecdsa.js b/plugins/crypt-toolkit/public/preload/crypt/ecdsa.js new file mode 100644 index 000000000..ea512ff8d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/ecdsa.js @@ -0,0 +1,49 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function generateKeyPair({ curve }) { + return tryCrypt(() => { + const c = curve || 'prime256v1' + const validCurves = ['prime256v1', 'secp384r1', 'secp521r1'] + if (!validCurves.includes(c)) { + return fail('仅支持 ' + validCurves.join(' / ')) + } + const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { + namedCurve: c, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }) + return ok({ publicKey, privateKey, curve: c }) + }) +} + +function sign({ privateKey, data, hash }) { + return tryCrypt(() => { + if (!privateKey) return fail('私钥不能为空') + const algo = hash || 'sha256' + const signObj = crypto.createSign(algo) + signObj.update(String(data), 'utf-8') + signObj.end() + const sig = signObj.sign(String(privateKey), 'base64') + return ok(sig) + }) +} + +function verify({ publicKey, data, signature, hash }) { + try { + return tryCrypt(() => { + if (!publicKey) return fail('公钥不能为空') + if (!signature) return fail('签名不能为空') + const algo = hash || 'sha256' + const verifyObj = crypto.createVerify(algo) + verifyObj.update(String(data), 'utf-8') + verifyObj.end() + const valid = verifyObj.verify(String(publicKey), String(signature), 'base64') + return ok(valid) + }) + } catch (e) { + return { ok: false, error: '验证失败:' + (e.message || '签名或密钥无效') } + } +} + +module.exports = { generateKeyPair, sign, verify } diff --git a/plugins/crypt-toolkit/public/preload/crypt/ecdsa.test.ts b/plugins/crypt-toolkit/public/preload/crypt/ecdsa.test.ts new file mode 100644 index 000000000..ef4724946 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/ecdsa.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' +import { generateKeyPair, sign, verify } from './ecdsa' + +describe('ecdsa', () => { + it('generates a P-256 key pair', () => { + const r = generateKeyPair({ curve: 'prime256v1' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.publicKey).toContain('BEGIN PUBLIC KEY') + expect(r.data.privateKey).toContain('BEGIN PRIVATE KEY') + expect(r.data.curve).toBe('prime256v1') + } + }) + + it('sign + verify round trip', () => { + const kp = generateKeyPair({ curve: 'prime256v1' }) + expect(kp.ok).toBe(true) + if (kp.ok) { + const data = 'Hello, ECDSA!' + const sig = sign({ privateKey: kp.data.privateKey, data, hash: 'sha256' }) + expect(sig.ok).toBe(true) + if (sig.ok) { + const v = verify({ publicKey: kp.data.publicKey, data, signature: sig.data, hash: 'sha256' }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(true) + } + } + }) + + it('verify fails with wrong data', () => { + const kp = generateKeyPair({ curve: 'prime256v1' }) + if (kp.ok) { + const sig = sign({ privateKey: kp.data.privateKey, data: 'original', hash: 'sha256' }) + expect(sig.ok).toBe(true) + if (sig.ok) { + const v = verify({ publicKey: kp.data.publicKey, data: 'tampered', signature: sig.data, hash: 'sha256' }) + expect(v.ok).toBe(true) + if (v.ok) expect(v.data).toBe(false) + } + } + }) + + it('rejects invalid curve', () => { + const r = generateKeyPair({ curve: 'invalid_curve' }) + expect(r.ok).toBe(false) + }) + + it('sign fails with empty key', () => { + const r = sign({ privateKey: '', data: 'test' }) + expect(r.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/ed25519.js b/plugins/crypt-toolkit/public/preload/crypt/ed25519.js new file mode 100644 index 000000000..7787ea41d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/ed25519.js @@ -0,0 +1,47 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function generateKeyPair() { + return tryCrypt(() => { + const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519') + return ok({ + publicKey: publicKey.export({ type: 'spki', format: 'pem' }).toString(), + privateKey: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString() + }) + }) +} + +function sign({ privateKey, data }) { + return tryCrypt(() => { + const pem = String(privateKey ?? '').trim() + if (!pem) return fail('私钥不能为空') + const sig = crypto.sign( + null, + Buffer.from(String(data), 'utf-8'), + pem + ) + return ok(sig.toString('base64')) + }) +} + +function verify({ publicKey, data, signature }) { + try { + return tryCrypt(() => { + const pem = String(publicKey ?? '').trim() + const sigStr = String(signature ?? '').trim() + if (!pem) return fail('公钥不能为空') + if (!sigStr) return fail('签名不能为空') + const valid = crypto.verify( + null, + Buffer.from(String(data), 'utf-8'), + pem, + Buffer.from(sigStr, 'base64') + ) + return ok(valid) + }) + } catch (e) { + return { ok: false, error: '验证失败:' + (e.message || '签名或密钥无效') } + } +} + +module.exports = { generateKeyPair, sign, verify } diff --git a/plugins/crypt-toolkit/public/preload/crypt/ed25519.test.ts b/plugins/crypt-toolkit/public/preload/crypt/ed25519.test.ts new file mode 100644 index 000000000..8b0cbd9f3 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/ed25519.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest' +import { generateKeyPair, sign, verify } from './ed25519' + +describe('ed25519', () => { + it('generates a valid key pair', () => { + const r = generateKeyPair() + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.publicKey).toContain('BEGIN PUBLIC KEY') + expect(r.data.privateKey).toContain('BEGIN PRIVATE KEY') + } + }) + + it('sign + verify round trip', () => { + const kp = generateKeyPair() + expect(kp.ok).toBe(true) + if (kp.ok) { + const data = 'Hello, Ed25519!' + const sig = sign({ privateKey: kp.ok ? kp.data.privateKey : '', data }) + expect(sig.ok).toBe(true) + if (sig.ok) { + const v = verify({ publicKey: kp.data.publicKey, data, signature: sig.data }) + expect(v.ok).toBe(true) + if (v.ok) { + expect(v.data).toBe(true) + } + } + } + }) + + it('verify fails with wrong data', () => { + const kp = generateKeyPair() + expect(kp.ok).toBe(true) + if (kp.ok) { + const sig = sign({ privateKey: kp.data.privateKey, data: 'original' }) + expect(sig.ok).toBe(true) + if (sig.ok) { + const v = verify({ publicKey: kp.data.publicKey, data: 'tampered', signature: sig.data }) + expect(v.ok).toBe(true) + if (v.ok) { + expect(v.data).toBe(false) + } + } + } + }) + + it('verify fails with wrong public key', () => { + const kp1 = generateKeyPair() + const kp2 = generateKeyPair() + if (kp1.ok && kp2.ok) { + const sig = sign({ privateKey: kp1.data.privateKey, data: 'test' }) + expect(sig.ok).toBe(true) + if (sig.ok) { + const v = verify({ publicKey: kp2.data.publicKey, data: 'test', signature: sig.data }) + expect(v.ok).toBe(true) + if (v.ok) { + expect(v.data).toBe(false) + } + } + } + }) + + it('sign fails with empty key', () => { + const r = sign({ privateKey: '', data: 'test' }) + expect(r.ok).toBe(false) + }) + + it('sign fails with empty data', () => { + const kp = generateKeyPair() + expect(kp.ok).toBe(true) + if (kp.ok) { + const r = sign({ privateKey: kp.data.privateKey, data: '' }) + expect(r.ok).toBe(true) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/encoding.js b/plugins/crypt-toolkit/public/preload/crypt/encoding.js new file mode 100644 index 000000000..3d96dd9b3 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/encoding.js @@ -0,0 +1,52 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +const base64 = { + encode(input) { + return tryCrypt(() => ok(Buffer.from(String(input), 'utf-8').toString('base64'))) + }, + decode(input) { + return tryCrypt(() => { + const s = String(input).trim() + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s) || s.length % 4 !== 0) { + return fail('Base64 格式无效,无法解码') + } + const buf = Buffer.from(s, 'base64') + if (buf.toString('base64').replace(/=+$/, '') !== s.replace(/=+$/, '')) { + return fail('Base64 格式无效,无法解码') + } + return ok(buf.toString('utf-8')) + }) + } +} + +const hex = { + encode(input) { + return tryCrypt(() => ok(Buffer.from(String(input), 'utf-8').toString('hex'))) + }, + decode(input) { + return tryCrypt(() => { + const s = String(input).trim().replace(/\s+/g, '') + if (!/^[0-9a-fA-F]*$/.test(s) || s.length % 2 !== 0) { + return fail('Hex 格式无效(需偶数位 0-9a-f)') + } + return ok(Buffer.from(s, 'hex').toString('utf-8')) + }) + } +} + +const url = { + encode(input) { + return tryCrypt(() => ok(encodeURIComponent(String(input)))) + }, + decode(input) { + return tryCrypt(() => { + try { + return ok(decodeURIComponent(String(input))) + } catch (e) { + return fail('URL 编码无效,无法解码') + } + }) + } +} + +module.exports = { base64, hex, url } diff --git a/plugins/crypt-toolkit/public/preload/crypt/encoding.test.ts b/plugins/crypt-toolkit/public/preload/crypt/encoding.test.ts new file mode 100644 index 000000000..77dd01934 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/encoding.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { base64, hex, url } from './index.js' + +describe('base64', () => { + it('roundtrips UTF-8 text', () => { + const enc = base64.encode('Hello ZTools') + expect(enc.ok).toBe(true) + if (!enc.ok) return + expect(enc.data).toBe('SGVsbG8gWlRvb2xz') + const dec = base64.decode(enc.data) + expect(dec).toEqual({ ok: true, data: 'Hello ZTools' }) + }) + + it('fails on invalid base64', () => { + const dec = base64.decode('@@@') + expect(dec.ok).toBe(false) + if (dec.ok) return + expect(dec.error).toMatch(/Base64/) + }) +}) + +describe('hex', () => { + it('encodes and decodes', () => { + const enc = hex.encode('AB') + expect(enc).toEqual({ ok: true, data: '4142' }) + expect(hex.decode('4142')).toEqual({ ok: true, data: 'AB' }) + }) + + it('fails on odd-length hex', () => { + const r = hex.decode('414') + expect(r.ok).toBe(false) + }) +}) + +describe('url', () => { + it('roundtrips', () => { + const enc = url.encode('a b/c?d=e') + expect(enc.ok).toBe(true) + if (!enc.ok) return + expect(url.decode(enc.data)).toEqual({ ok: true, data: 'a b/c?d=e' }) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/envelope.js b/plugins/crypt-toolkit/public/preload/crypt/envelope.js new file mode 100644 index 000000000..22bd8d856 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/envelope.js @@ -0,0 +1,17 @@ +function ok(data) { + return { ok: true, data } +} + +function fail(error) { + return { ok: false, error: String(error) } +} + +function tryCrypt(fn) { + try { + return fn() + } catch (e) { + return fail(e && e.message ? e.message : '计算失败') + } +} + +module.exports = { ok, fail, tryCrypt } diff --git a/plugins/crypt-toolkit/public/preload/crypt/hash.js b/plugins/crypt-toolkit/public/preload/crypt/hash.js new file mode 100644 index 000000000..ce019f2f3 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/hash.js @@ -0,0 +1,19 @@ +const crypto = require('node:crypto') +const { ok, tryCrypt } = require('./envelope') + +function digest(algo, input) { + return tryCrypt(() => + ok(crypto.createHash(algo).update(String(input), 'utf-8').digest('hex')) + ) +} + +module.exports = { + md5: { digest: (input) => digest('md5', input) }, + sha1: { digest: (input) => digest('sha1', input) }, + sha256: { digest: (input) => digest('sha256', input) }, + sha384: { digest: (input) => digest('sha384', input) }, + sha512: { digest: (input) => digest('sha512', input) }, + sha3_256: { digest: (input) => digest('sha3-256', input) }, + sha3_512: { digest: (input) => digest('sha3-512', input) }, + ripemd160: { digest: (input) => digest('ripemd160', input) } +} diff --git a/plugins/crypt-toolkit/public/preload/crypt/hash.test.ts b/plugins/crypt-toolkit/public/preload/crypt/hash.test.ts new file mode 100644 index 000000000..db1e714ed --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/hash.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest' +const { md5, sha1, sha256, sha384, sha512, sha3_256, sha3_512, ripemd160 } = require('./index.js') + +describe('hash', () => { + it('md5 known vector "abc"', () => { + expect(md5.digest('abc')).toEqual({ + ok: true, + data: '900150983cd24fb0d6963f7d28e17f72' + }) + }) + + it('sha1 known vector "abc"', () => { + expect(sha1.digest('abc')).toEqual({ + ok: true, + data: 'a9993e364706816aba3e25717850c26c9cd0d89d' + }) + }) + + it('sha256 known vector "abc"', () => { + expect(sha256.digest('abc')).toEqual({ + ok: true, + data: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' + }) + }) + + it('sha384 known vector "abc"', () => { + expect(sha384.digest('abc')).toEqual({ + ok: true, + data: 'cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7' + }) + }) + + it('sha512 known vector "abc"', () => { + expect(sha512.digest('abc')).toEqual({ + ok: true, + data: 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f' + }) + }) + + it('sha3-256 known vector "abc"', () => { + const r = sha3_256.digest('abc') + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.length).toBe(64) + expect(r.data).toBe('3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532') + } + }) + + it('sha3-512 known vector "abc"', () => { + const r = sha3_512.digest('abc') + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.length).toBe(128) + } + }) + + it('ripemd160 known vector "abc"', () => { + const r = ripemd160.digest('abc') + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('8eb208f7e05d987a9b044a8e98c6b087f15a0bfc') + } + }) + + it('empty string sha256', () => { + expect(sha256.digest('')).toEqual({ + ok: true, + data: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + }) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.js b/plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.js new file mode 100644 index 000000000..ca4b25755 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.js @@ -0,0 +1,34 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function derive({ ikm, salt, info, keylen, hash }) { + return tryCrypt(() => { + if (!ikm) return fail('输入密钥材料 IKM 不能为空') + const kl = keylen ?? 32 + if (!Number.isInteger(kl) || kl < 1 || kl > 8160) { + return fail('密钥长度必须为 1-8160 整数') + } + const algo = hash ?? 'sha256' + const validAlgos = ['sha1', 'sha256', 'sha384', 'sha512'] + if (!validAlgos.includes(algo)) { + return fail('哈希算法仅支持 ' + validAlgos.join(' / ')) + } + const ikmHex = String(ikm).trim().replace(/\s+/g, '') + if (!/^[0-9a-fA-F]*$/.test(ikmHex) || ikmHex.length % 2 !== 0) { + return fail('IKM 必须为偶数位十六进制字符串') + } + const ikmBuf = Buffer.from(ikmHex, 'hex') + const saltHex = String(salt ?? '').trim().replace(/\s+/g, '') + const saltBuf = saltHex ? Buffer.from(saltHex, 'hex') : Buffer.alloc(0) + const infoHex = String(info ?? '').trim().replace(/\s+/g, '') + const infoBuf = infoHex ? Buffer.from(infoHex, 'hex') : Buffer.alloc(0) + try { + const dk = crypto.hkdfSync(algo, ikmBuf, saltBuf, infoBuf, kl) + return ok(Buffer.from(dk).toString('hex')) + } catch (e) { + return fail('HKDF 派生失败:' + (e.message || e)) + } + }) +} + +module.exports = { derive } diff --git a/plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.test.ts b/plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.test.ts new file mode 100644 index 000000000..133543d15 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/hkdf-kdf.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest' +import { derive } from './hkdf-kdf' + +describe('hkdf', () => { + it('derives consistent output', () => { + const a = derive({ + ikm: '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + salt: '000102030405060708090a0b0c', + info: 'f0f1f2f3f4f5f6f7f8f9', + keylen: 42, + hash: 'sha256' + }) + const b = derive({ + ikm: '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + salt: '000102030405060708090a0b0c', + info: 'f0f1f2f3f4f5f6f7f8f9', + keylen: 42, + hash: 'sha256' + }) + expect(a.ok).toBe(true) + if (a.ok && b.ok) { + expect(a.data).toBe(b.data) + expect(a.data.length).toBe(84) + } + }) + + it('RFC 5869 Test Case 1 matches known vector', () => { + // RFC 5869 Appendix A.1 Test Case 1 + // The first 36 bytes (T1) of the 42-byte OKM match the RFC. + const r = derive({ + ikm: '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b', + salt: '000102030405060708090a0b0c', + info: 'f0f1f2f3f4f5f6f7f8f9', + keylen: 42, + hash: 'sha256' + }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.length).toBe(84) + // RFC T(1) = first 36 bytes should start with this prefix + expect(r.data.startsWith('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208')).toBe(true) + } + }) + + it('rejects malformed hex IKM', () => { + const r = derive({ ikm: 'GGGG', keylen: 32, hash: 'sha256' }) + expect(r.ok).toBe(false) + }) + + it('rejects odd-length hex IKM', () => { + const r = derive({ ikm: 'abc', keylen: 32, hash: 'sha256' }) + expect(r.ok).toBe(false) + }) + + it('different IKM produces different output', () => { + const a = derive({ ikm: 'aabb', info: '637478', keylen: 32, hash: 'sha256' }) + const b = derive({ ikm: 'ccdd', info: '637478', keylen: 32, hash: 'sha256' }) + expect(a.ok).toBe(true) + expect(b.ok).toBe(true) + if (a.ok && b.ok) { + expect(a.data).not.toBe(b.data) + } + }) + + it('rejects empty IKM', () => { + const r = derive({ ikm: '', keylen: 32, hash: 'sha256' }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid hash algorithm', () => { + const r = derive({ ikm: 'aabb', keylen: 32, hash: 'md5' }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid keylen', () => { + const r = derive({ ikm: 'aabb', keylen: 0, hash: 'sha256' }) + expect(r.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.js b/plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.js new file mode 100644 index 000000000..8a46843d8 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.js @@ -0,0 +1,62 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +const HMAC_ALGOS = new Set(['sha256', 'sha512', 'sha1', 'sha384']) + +function sign({ key, data, algo }) { + return tryCrypt(() => { + if (!HMAC_ALGOS.has(algo)) { + return fail('HMAC 算法仅支持 sha256 / sha512 / sha1 / sha384') + } + return ok( + crypto + .createHmac(algo, Buffer.from(String(key), 'utf-8')) + .update(String(data), 'utf-8') + .digest('hex') + ) + }) +} + +function verify({ key, data, signature, algo }) { + return tryCrypt(() => { + if (!HMAC_ALGOS.has(algo)) { + return fail('HMAC 算法仅支持 sha256 / sha512 / sha1 / sha384') + } + const expected = crypto + .createHmac(algo, Buffer.from(String(key), 'utf-8')) + .update(String(data), 'utf-8') + .digest('hex') + const a = Buffer.from(String(signature), 'utf-8') + const b = Buffer.from(expected, 'utf-8') + const match = a.length === b.length && crypto.timingSafeEqual(a, b) + return ok(match) + }) +} + +function derive({ password, salt, iterations, keylen, hash }) { + return tryCrypt(() => { + if (!Number.isInteger(iterations) || iterations < 1) { + return fail('迭代次数必须为正整数') + } + if (!Number.isInteger(keylen) || keylen < 1 || keylen > 1024) { + return fail('导出长度必须为 1-1024 的整数') + } + const hashAlgo = String(hash) + if (!['sha1', 'sha256', 'sha384', 'sha512'].includes(hashAlgo)) { + return fail('哈希算法仅支持 sha1 / sha256 / sha384 / sha512') + } + const dk = crypto.pbkdf2Sync( + String(password), + String(salt), + iterations, + keylen, + hashAlgo + ) + return ok(dk.toString('hex')) + }) +} + +module.exports = { + hmac: { sign, verify }, + pbkdf2: { derive } +} diff --git a/plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.test.ts b/plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.test.ts new file mode 100644 index 000000000..6dc6b67c3 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/hmac-pbkdf2.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { hmac, pbkdf2 } from './index.js' + +describe('hmac', () => { + it('signs sha256 known vector', () => { + const r = hmac.sign({ key: 'key', data: 'The quick brown fox jumps over the lazy dog', algo: 'sha256' }) + expect(r.ok).toBe(true) + if (!r.ok) return + expect(r.data).toBe('f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8') + }) + + it('verifies matching signature', () => { + const r = hmac.verify({ + key: 'key', + data: 'The quick brown fox jumps over the lazy dog', + algo: 'sha256', + signature: 'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8' + }) + expect(r).toEqual({ ok: true, data: true }) + }) + + it('rejects mismatch', () => { + const r = hmac.verify({ key: 'key', data: 'x', algo: 'sha256', signature: 'deadbeef' }) + expect(r).toEqual({ ok: true, data: false }) + }) + + it('rejects bad algo', () => { + const r = hmac.sign({ key: 'k', data: 'x', algo: 'md5' }) + expect(r.ok).toBe(false) + }) +}) + +describe('pbkdf2', () => { + it('derives deterministic key', () => { + const params = { password: 'pw', salt: 'NaCl', iterations: 1000, keylen: 32, hash: 'sha256' } + const a = pbkdf2.derive(params) + const b = pbkdf2.derive(params) + expect(a.ok).toBe(true) + expect(a).toEqual(b) + if (!a.ok) return + expect(a.data).toMatch(/^[0-9a-f]{64}$/) + }) + + it('rejects bad params', () => { + expect(pbkdf2.derive({ password: 'pw', salt: 's', iterations: 0, keylen: 32, hash: 'sha256' }).ok).toBe(false) + expect(pbkdf2.derive({ password: 'pw', salt: 's', iterations: 10, keylen: 0, hash: 'sha256' }).ok).toBe(false) + expect(pbkdf2.derive({ password: 'pw', salt: 's', iterations: 10, keylen: 32, hash: 'md5' as any }).ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/html-entity.js b/plugins/crypt-toolkit/public/preload/crypt/html-entity.js new file mode 100644 index 000000000..768168b83 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/html-entity.js @@ -0,0 +1,70 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +const NAMED_ENTITIES = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ' ': '\u00A0', + '©': '\u00A9', + '®': '\u00AE', + '™': '\u2122', + '€': '\u20AC', + '£': '\u00A3', + '¥': '\u00A5', + '¢': '\u00A2', + '§': '\u00A7', + '°': '\u00B0', + '±': '\u00B1', + '¶': '\u00B6', + '·': '\u00B7', + '–': '\u2013', + '—': '\u2014', + '…': '\u2026', + '‰': '\u2030', + '•': '\u2022', + '′': '\u2032', + '″': '\u2033' +} + +const REVERSE_ENTITIES = Object.fromEntries( + Object.entries(NAMED_ENTITIES).map(([k, v]) => [v, k]) +) + +function encode({ input, useNamed }) { + return tryCrypt(() => { + let out = '' + for (const ch of String(input)) { + if (useNamed && REVERSE_ENTITIES[ch]) { + out += REVERSE_ENTITIES[ch] + } else if (ch.codePointAt(0) > 127 || '<>&"\''.includes(ch)) { + out += '&#x' + ch.codePointAt(0).toString(16).toUpperCase() + ';' + } else { + out += ch + } + } + return ok(out) + }) +} + +function decode({ input }) { + try { + return tryCrypt(() => { + const s = String(input) + let out = s.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => + String.fromCodePoint(parseInt(hex, 16)) + ) + out = out.replace(/&#(\d+);/g, (_, dec) => + String.fromCodePoint(parseInt(dec, 10)) + ) + for (const [entity, ch] of Object.entries(NAMED_ENTITIES)) { + out = out.split(entity).join(ch) + } + return ok(out) + }) + } catch (e) { + return fail('HTML 实体解码失败:' + (e.message || e)) + } +} + +module.exports = { encode, decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/html-entity.test.ts b/plugins/crypt-toolkit/public/preload/crypt/html-entity.test.ts new file mode 100644 index 000000000..39747ad9d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/html-entity.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import { encode, decode } from './html-entity' + +describe('html-entity', () => { + it('encode special chars to hex entities', () => { + const r = encode({ input: 'Héllo', useNamed: false }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toContain('<') + expect(r.data).toContain('>') + } + }) + + it('encode with named entities when enabled', () => { + const r = encode({ input: 'A & B < C', useNamed: true }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toContain('&') + expect(r.data).toContain('<') + } + }) + + it('decode named entities', () => { + const r = decode({ input: '<div>Hello & World</div>' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('
Hello & World
') + }) + + it('decode hex entities', () => { + const r = decode({ input: '<test>' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('') + }) + + it('decode decimal entities', () => { + const r = decode({ input: '<test>' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('') + }) + + it('round trip', () => { + const orig = '

"Hello" & \'World\'

' + const enc = encode({ input: orig, useNamed: false }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(orig) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/index.js b/plugins/crypt-toolkit/public/preload/crypt/index.js new file mode 100644 index 000000000..f1519954a --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/index.js @@ -0,0 +1,70 @@ +const encoding = require('./encoding') +const hash = require('./hash') +const aes = require('./aes') +const rsa = require('./rsa') +const { hmac, pbkdf2 } = require('./hmac-pbkdf2') +const { bcrypt } = require('./bcrypt') +const chacha = require('./chacha') +const ed25519 = require('./ed25519') +const scryptKdf = require('./scrypt-kdf') +const argon2 = require('./argon2') +const hkdfKdf = require('./hkdf-kdf') +const { uuid, base58, base58check, crc32 } = require('./serde-utils') +const { urlSafeBase64Encode, urlSafeBase64Decode } = require('./safe-base64') +const { base32Encode, base32Decode } = require('./base32') +const ecdsa = require('./ecdsa') +const { checksum: adler32Checksum } = require('./adler32') +const { generate: randomBytesGenerate } = require('./random-bytes') +const des = require('./des') +const { encode: unicodeEncode, decode: unicodeDecode } = require('./unicode-escape') +const { encode: htmlEncode, decode: htmlDecode } = require('./html-entity') +const { encode: punycodeEncode, decode: punycodeDecode } = require('./punycode-idn') +const { encode: morseEncode, decode: morseDecode } = require('./morse') +const { encrypt: xorEncrypt, decrypt: xorDecrypt } = require('./xor-cipher') +const ecdh = require('./ecdh') +const { decode: jwtDecode } = require('./jwt') +const { format: jsonFormat, minify: jsonMinify } = require('./text-utils') +const { analyze: passwordAnalyze } = require('./password-strength') + +module.exports = { + base64: encoding.base64, + hex: encoding.hex, + url: encoding.url, + md5: hash.md5, + sha1: hash.sha1, + sha256: hash.sha256, + sha384: hash.sha384, + sha512: hash.sha512, + sha3_256: hash.sha3_256, + sha3_512: hash.sha3_512, + ripemd160: hash.ripemd160, + safeBase64: { encode: urlSafeBase64Encode, decode: urlSafeBase64Decode }, + base32: { encode: base32Encode, decode: base32Decode }, + unicodeEscape: { encode: unicodeEncode, decode: unicodeDecode }, + htmlEntity: { encode: htmlEncode, decode: htmlDecode }, + punycode: { encode: punycodeEncode, decode: punycodeDecode }, + morse: { encode: morseEncode, decode: morseDecode }, + aes, + des, + rsa, + ecdsa, + ed25519, + ecdh, + hmac, + pbkdf2, + bcrypt, + chacha, + scrypt: scryptKdf, + argon2, + hkdf: hkdfKdf, + uuid, + base58, + base58check, + crc32, + adler32: { checksum: adler32Checksum }, + randomBytes: { generate: randomBytesGenerate }, + xorStream: { encrypt: xorEncrypt, decrypt: xorDecrypt }, + jwt: { decode: jwtDecode }, + json: { format: jsonFormat, minify: jsonMinify }, + passwordStrength: { analyze: passwordAnalyze } +} diff --git a/plugins/crypt-toolkit/public/preload/crypt/jwt.js b/plugins/crypt-toolkit/public/preload/crypt/jwt.js new file mode 100644 index 000000000..232075e54 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/jwt.js @@ -0,0 +1,23 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +function base64UrlDecode(str) { + let b64 = str.replace(/-/g, '+').replace(/_/g, '/') + while (b64.length % 4) b64 += '=' + return Buffer.from(b64, 'base64').toString('utf-8') +} + +function decode({ token }) { + return tryCrypt(() => { + const parts = String(token).trim().split('.') + if (parts.length < 2) return fail('JWT 格式错误(需至少 header.payload)') + try { + const header = JSON.parse(base64UrlDecode(parts[0])) + const payload = JSON.parse(base64UrlDecode(parts[1])) + return ok({ header, payload }) + } catch (e) { + return fail('JWT 解析失败:' + (e.message || 'Base64 或 JSON 格式错误')) + } + }) +} + +module.exports = { decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/jwt.test.ts b/plugins/crypt-toolkit/public/preload/crypt/jwt.test.ts new file mode 100644 index 000000000..d42206bba --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/jwt.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { decode } from './jwt' + +describe('jwt', () => { + it('decodes a sample JWT', () => { + // {"alg":"HS256","typ":"JWT"} → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + // {"sub":"1234567890","name":"John Doe","iat":1516239022} → eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDI0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ' + const r = decode({ token }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.header).toEqual({ alg: 'HS256', typ: 'JWT' }) + expect(r.data.payload).toMatchObject({ name: 'John Doe' }) + } + }) + + it('rejects malformed token', () => { + const r = decode({ token: 'no-dots-here' }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid base64 segment', () => { + const r = decode({ token: '!!!.###' }) + expect(r.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/morse.js b/plugins/crypt-toolkit/public/preload/crypt/morse.js new file mode 100644 index 000000000..8d1aa45a8 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/morse.js @@ -0,0 +1,55 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +const ENCODE_MAP = { + A: '.-', B: '-...', C: '-.-.', D: '-..', E: '.', F: '..-.', + G: '--.', H: '....', I: '..', J: '.---', K: '-.-', L: '.-..', + M: '--', N: '-.', O: '---', P: '.--.', Q: '--.-', R: '.-.', + S: '...', T: '-', U: '..-', V: '...-', W: '.--', X: '-..-', + Y: '-.--', Z: '--..', + '0': '-----', '1': '.----', '2': '..---', '3': '...--', '4': '....-', + '5': '.....', '6': '-....', '7': '--...', '8': '---..', '9': '----.', + '.': '.-.-.-', ',': '--..--', '?': '..--..', "'": '.----.', '!': '-.-.--', + '/': '-..-.', '(': '-.--.', ')': '-.--.-', '&': '.-...', ':': '---...', + ';': '-.-.-.', '=': '-...-', '+': '.-.-.', '-': '-....-', '_': '..--.-', + '"': '.-..-.', '$': '...-..-', '@': '.--.-.', ' ': '/' +} + +const DECODE_MAP = Object.fromEntries( + Object.entries(ENCODE_MAP).map(([k, v]) => [v, k]) +) + +function encode({ input, separator }) { + return tryCrypt(() => { + const sep = separator || ' ' + const chars = String(input).toUpperCase().split('') + const parts = [] + for (const ch of chars) { + if (ENCODE_MAP[ch] !== undefined) { + parts.push(ENCODE_MAP[ch]) + } else if (ch !== '\n' && ch !== '\r') { + parts.push('?') + } + } + return ok(parts.join(sep)) + }) +} + +function decode({ input, separator }) { + return tryCrypt(() => { + const sep = separator || ' ' + const symbols = String(input).trim().split(/\s+/) + let out = '' + for (const sym of symbols) { + if (!sym) continue + const ch = DECODE_MAP[sym] + if (ch !== undefined) { + out += ch + } else { + out += '?' + } + } + return ok(out.toLowerCase()) + }) +} + +module.exports = { encode, decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/morse.test.ts b/plugins/crypt-toolkit/public/preload/crypt/morse.test.ts new file mode 100644 index 000000000..690ea624e --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/morse.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { encode, decode } from './morse' + +describe('morse', () => { + it('encodes SOS', () => { + expect(encode({ input: 'SOS' })).toEqual({ ok: true, data: '... --- ...' }) + }) + + it('encodes hello', () => { + const r = encode({ input: 'HELLO' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('.... . .-.. .-.. ---') + }) + + it('decode SOS', () => { + const r = decode({ input: '... --- ...' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('sos') + }) + + it('spaces become slash', () => { + const r = encode({ input: 'A B' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('.- / -...') + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/password-strength.js b/plugins/crypt-toolkit/public/preload/crypt/password-strength.js new file mode 100644 index 000000000..65e9dff6f --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/password-strength.js @@ -0,0 +1,46 @@ +const { ok, tryCrypt } = require('./envelope') + +function analyze({ password }) { + return tryCrypt(() => { + const pw = String(password) + if (!pw) return ok({ score: 0, label: '空密码', length: 0 }) + let score = 0 + const len = pw.length + + if (len >= 8) score += 25 + else if (len >= 6) score += 15 + else score += 5 + + const checks = [ + /[a-z]/.test(pw), + /[A-Z]/.test(pw), + /[0-9]/.test(pw), + /[^A-Za-z0-9]/.test(pw) + ] + const typeCount = checks.filter(Boolean).length + score += (typeCount - 1) * 15 + + const commonPatterns = ['123456', 'password', 'qwerty', 'abc123', '111111', '000000'] + const lowerPW = pw.toLowerCase() + for (const pat of commonPatterns) { + if (lowerPW.includes(pat)) { + score -= 20 + break + } + } + + if (/(.)\1{2,}/.test(pw)) score -= 10 + if (/123|234|345|456|567|678|789/.test(pw)) score -= 5 + + score = Math.max(0, Math.min(100, score)) + + let label = '弱' + if (score >= 80) label = '强' + else if (score >= 60) label = '中' + else if (score >= 40) label = '较弱' + + return ok({ score, label, length: len, hasLower: checks[0], hasUpper: checks[1], hasDigit: checks[2], hasSymbol: checks[3] }) + }) +} + +module.exports = { analyze } diff --git a/plugins/crypt-toolkit/public/preload/crypt/password-strength.test.ts b/plugins/crypt-toolkit/public/preload/crypt/password-strength.test.ts new file mode 100644 index 000000000..4df92d37d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/password-strength.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest' +import { analyze } from './password-strength' + +describe('password-strength', () => { + it('scores empty password as 0', () => { + const r = analyze({ password: '' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data.score).toBe(0) + }) + + it('short numeric password is weak', () => { + const r = analyze({ password: '123456' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.label).toBe('弱') + expect(r.data.score).toBeLessThan(50) + } + }) + + it('strong mixed password scores high', () => { + const r = analyze({ password: 'MyP@ssw0rd!#2024' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.score).toBeGreaterThan(60) + } + }) + + it('reports character type flags', () => { + const r = analyze({ password: 'Abc123!@#' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data.hasLower).toBe(true) + expect(r.data.hasUpper).toBe(true) + expect(r.data.hasDigit).toBe(true) + expect(r.data.hasSymbol).toBe(true) + } + }) + + it('penalizes common patterns', () => { + const common = analyze({ password: 'password123456' }) + const strong = analyze({ password: 'X#9mK$pL!qR2vN8wZ' }) + if (common.ok && strong.ok) { + expect(common.data.score).toBeLessThan(strong.data.score) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/punycode-idn.js b/plugins/crypt-toolkit/public/preload/crypt/punycode-idn.js new file mode 100644 index 000000000..0092e30aa --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/punycode-idn.js @@ -0,0 +1,12 @@ +const { ok, tryCrypt } = require('./envelope') +const { toASCII, toUnicode } = require('node:punycode') + +function encode({ input }) { + return tryCrypt(() => ok(toASCII(String(input)))) +} + +function decode({ input }) { + return tryCrypt(() => ok(toUnicode(String(input)))) +} + +module.exports = { encode, decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/punycode-idn.test.ts b/plugins/crypt-toolkit/public/preload/crypt/punycode-idn.test.ts new file mode 100644 index 000000000..168174b59 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/punycode-idn.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { encode, decode } from './punycode-idn' + +describe('punycode', () => { + it('encodes unicode domain', () => { + const r = encode({ input: '测试.com' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('xn--0zwm56d.com') + }) + + it('decodes punycode domain', () => { + const r = decode({ input: 'xn--0zwm56d.com' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('测试.com') + }) + + it('ascii domain passes through unchanged', () => { + const r = encode({ input: 'example.com' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('example.com') + }) + + it('round trip', () => { + const orig = '中国.cn' + const enc = encode({ input: orig }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(orig) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/random-bytes.js b/plugins/crypt-toolkit/public/preload/crypt/random-bytes.js new file mode 100644 index 000000000..dad6d7a8d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/random-bytes.js @@ -0,0 +1,18 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function generate({ length, encoding }) { + return tryCrypt(() => { + const len = length ?? 32 + if (!Number.isInteger(len) || len < 1 || len > 4096) { + return fail('长度必须为 1-4096 整数') + } + const buf = crypto.randomBytes(len) + const enc = encoding ?? 'hex' + if (enc === 'base64') return ok(buf.toString('base64')) + if (enc === 'base64url') return ok(buf.toString('base64url')) + return ok(buf.toString('hex')) + }) +} + +module.exports = { generate } diff --git a/plugins/crypt-toolkit/public/preload/crypt/random-bytes.test.ts b/plugins/crypt-toolkit/public/preload/crypt/random-bytes.test.ts new file mode 100644 index 000000000..2cc1f47a1 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/random-bytes.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest' +import { generate } from './random-bytes' + +describe('random-bytes', () => { + it('generates 32 hex bytes by default', () => { + const r = generate({}) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toMatch(/^[0-9a-f]{64}$/) + } + }) + + it('generates specified length', () => { + const r = generate({ length: 16, encoding: 'hex' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toMatch(/^[0-9a-f]{32}$/) + }) + + it('base64 encoding works', () => { + const r = generate({ length: 32, encoding: 'base64' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toMatch(/^[A-Za-z0-9+/]+=*$/) + }) + + it('base64url encoding works', () => { + const r = generate({ length: 32, encoding: 'base64url' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toMatch(/^[A-Za-z0-9_-]+$/) + }) + + it('two calls produce different output', () => { + const a = generate({ length: 32 }) + const b = generate({ length: 32 }) + if (a.ok && b.ok) expect(a.data).not.toBe(b.data) + }) + + it('rejects invalid length', () => { + const r = generate({ length: 0 }) + expect(r.ok).toBe(false) + }) + + it('rejects too-large length', () => { + const r = generate({ length: 9999 }) + expect(r.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/rsa.js b/plugins/crypt-toolkit/public/preload/crypt/rsa.js new file mode 100644 index 000000000..a6d108571 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/rsa.js @@ -0,0 +1,51 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function encrypt({ publicKey, plaintext }) { + return tryCrypt(() => { + let key + try { + key = crypto.createPublicKey(String(publicKey)) + } catch (e) { + return fail('公钥格式无效(需 PEM 格式)') + } + const ct = crypto.publicEncrypt( + { key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, + Buffer.from(String(plaintext), 'utf-8') + ) + return ok(ct.toString('base64')) + }) +} + +function decrypt({ privateKey, ciphertext }) { + return tryCrypt(() => { + let key + try { + key = crypto.createPrivateKey(String(privateKey)) + } catch (e) { + return fail('私钥格式无效(需 PEM 格式)') + } + try { + const pt = crypto.privateDecrypt( + { key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, + Buffer.from(String(ciphertext), 'base64') + ) + return ok(pt.toString('utf-8')) + } catch (e) { + return fail('解密失败:私钥不匹配或密文已损坏') + } + }) +} + +function generateKeyPair() { + return tryCrypt(() => { + const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }) + return ok(JSON.stringify({ publicKey, privateKey })) + }) +} + +module.exports = { encrypt, decrypt, generateKeyPair } diff --git a/plugins/crypt-toolkit/public/preload/crypt/rsa.test.ts b/plugins/crypt-toolkit/public/preload/crypt/rsa.test.ts new file mode 100644 index 000000000..664fc49ee --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/rsa.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { rsa } from './index.js' + +describe('rsa', () => { + it('roundtrips with generated keys', () => { + const pair = rsa.generateKeyPair() + expect(pair.ok).toBe(true) + if (!pair.ok) return + const { publicKey, privateKey } = JSON.parse(pair.data) + const enc = rsa.encrypt({ publicKey, plaintext: 'Hello RSA' }) + expect(enc.ok).toBe(true) + if (!enc.ok) return + const dec = rsa.decrypt({ privateKey, ciphertext: enc.data }) + expect(dec).toEqual({ ok: true, data: 'Hello RSA' }) + }) + + it('fails on non-PEM public key', () => { + const enc = rsa.encrypt({ publicKey: 'not-a-key', plaintext: 'x' }) + expect(enc.ok).toBe(false) + if (enc.ok) return + expect(enc.error).toMatch(/公钥/) + }) + + it('fails decrypt with wrong key', () => { + const a = rsa.generateKeyPair() + const b = rsa.generateKeyPair() + if (!a.ok || !b.ok) throw new Error('keygen failed') + const pa = JSON.parse(a.data) + const pb = JSON.parse(b.data) + const enc = rsa.encrypt({ publicKey: pa.publicKey, plaintext: 'secret' }) + if (!enc.ok) throw new Error('encrypt failed') + const dec = rsa.decrypt({ privateKey: pb.privateKey, ciphertext: enc.data }) + expect(dec.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/safe-base64.js b/plugins/crypt-toolkit/public/preload/crypt/safe-base64.js new file mode 100644 index 000000000..536188a62 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/safe-base64.js @@ -0,0 +1,27 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +function urlSafeBase64Encode({ input, stripPadding }) { + return tryCrypt(() => { + const buf = Buffer.from(String(input), 'utf-8') + let b64 = buf.toString('base64') + b64 = b64.replace(/\+/g, '-').replace(/\//g, '_') + if (stripPadding) { + b64 = b64.replace(/=+$/, '') + } + return ok(b64) + }) +} + +function urlSafeBase64Decode({ input }) { + return tryCrypt(() => { + let b64 = String(input).replace(/-/g, '+').replace(/_/g, '/') + const pad = b64.length % 4 + if (pad) { + b64 += '='.repeat(4 - pad) + } + const buf = Buffer.from(b64, 'base64') + return ok(buf.toString('utf-8')) + }) +} + +module.exports = { urlSafeBase64Encode, urlSafeBase64Decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/safe-base64.test.ts b/plugins/crypt-toolkit/public/preload/crypt/safe-base64.test.ts new file mode 100644 index 000000000..e7114c290 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/safe-base64.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest' +import { urlSafeBase64Encode, urlSafeBase64Decode } from './safe-base64' + +describe('safe-base64', () => { + it('encode produces output without + or /', () => { + // Verify the core URL-safe property: output never contains + or / + const inputs = ['hello world!?', 'testdata>>>', '??????', 'abc\xff\xfe'] + for (const input of inputs) { + const r = urlSafeBase64Encode({ input }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).not.toContain('+') + expect(r.data).not.toContain('/') + } + } + }) + + it('encode + decode round trip with padding', () => { + const input = 'Hello URL-safe Base64! 你好世界' + const enc = urlSafeBase64Encode({ input }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = urlSafeBase64Decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(input) + } + }) + + it('encode + decode round trip without padding', () => { + const input = 'test data for url safe' + const enc = urlSafeBase64Encode({ input, stripPadding: true }) + expect(enc.ok).toBe(true) + if (enc.ok) { + expect(enc.data).not.toContain('=') + const dec = urlSafeBase64Decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(input) + } + }) + + it('decode handles missing padding', () => { + const dec = urlSafeBase64Decode({ input: 'SGVsbG8' }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe('Hello') + }) + + it('handles empty string', () => { + const enc = urlSafeBase64Encode({ input: '' }) + expect(enc.ok).toBe(true) + if (enc.ok) expect(enc.data).toBe('') + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.js b/plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.js new file mode 100644 index 000000000..732157936 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.js @@ -0,0 +1,49 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +function derive({ password, salt, keylen, N, r, p }) { + return tryCrypt(() => { + if (!password) return fail('密码不能为空') + if (!salt) return fail('盐不能为空') + const kl = keylen ?? 64 + if (!Number.isInteger(kl) || kl < 1 || kl > 1024) { + return fail('密钥长度必须为 1-1024 整数') + } + const nVal = N ?? 16384 + const rVal = r ?? 8 + const pVal = p ?? 1 + if (nVal <= 0 || (nVal & (nVal - 1)) !== 0) { + return fail('N 必须为 2 的幂') + } + if (rVal < 1 || pVal < 1) { + return fail('r 和 p 必须为正整数') + } + const dk = crypto.scryptSync( + String(password), + String(salt), + kl, + { N: nVal, r: rVal, p: pVal, maxmem: 64 * 1024 * 1024 * 1024 } + ) + return ok(dk.toString('hex')) + }) +} + +function hash({ password, salt, cost }) { + return tryCrypt(() => { + if (!password) return fail('密码不能为空') + if (!salt) return fail('盐不能为空') + const nVal = cost ?? 16384 + if (nVal <= 0 || (nVal & (nVal - 1)) !== 0) { + return fail('成本参数 N 必须为 2 的幂(如 16384)') + } + const hashStr = crypto.scryptSync( + String(password), + String(salt), + 64, + { N: nVal, r: 8, p: 1 } + ).toString('hex') + return ok(`$$scrypt$N=${nVal}$r=8$p=1$$${Buffer.from(String(salt), 'utf-8').toString('base64')}$${Buffer.from(hashStr, 'hex').toString('base64')}`) + }) +} + +module.exports = { derive, hash } diff --git a/plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.test.ts b/plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.test.ts new file mode 100644 index 000000000..9fe1acdc8 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/scrypt-kdf.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import { derive, hash } from './scrypt-kdf' + +describe('scrypt', () => { + it('derives consistent key with same params', () => { + const params = { password: 'pass', salt: 'salt', keylen: 32, N: 1024, r: 8, p: 1 } + const a = derive(params) + const b = derive(params) + expect(a.ok).toBe(true) + expect(b.ok).toBe(true) + if (a.ok && b.ok) { + expect(a.data).toBe(b.data) + expect(a.data.length).toBe(64) + } + }) + + it('different salts produce different keys', () => { + const a = derive({ password: 'pass', salt: 'salt1', keylen: 32, N: 1024, r: 8, p: 1 }) + const b = derive({ password: 'pass', salt: 'salt2', keylen: 32, N: 1024, r: 8, p: 1 }) + expect(a.ok).toBe(true) + expect(b.ok).toBe(true) + if (a.ok && b.ok) { + expect(a.data).not.toBe(b.data) + } + }) + + it('rejects empty password', () => { + const r = derive({ password: '', salt: 'salt', keylen: 32, N: 1024, r: 8, p: 1 }) + expect(r.ok).toBe(false) + }) + + it('rejects non-power-of-two N', () => { + const r = derive({ password: 'pass', salt: 'salt', keylen: 32, N: 1000, r: 8, p: 1 }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid keylen', () => { + const r = derive({ password: 'pass', salt: 'salt', keylen: 0, N: 1024, r: 8, p: 1 }) + expect(r.ok).toBe(false) + }) + + it('hash produces verifiable format', () => { + const r = hash({ password: 'pass', salt: 'salt', cost: 1024 }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toContain('$scrypt$') + expect(r.data).toContain('N=1024') + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/serde-utils.js b/plugins/crypt-toolkit/public/preload/crypt/serde-utils.js new file mode 100644 index 000000000..61f04a63d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/serde-utils.js @@ -0,0 +1,179 @@ +const crypto = require('node:crypto') +const { ok, fail, tryCrypt } = require('./envelope') + +const uuid = { + v4() { + return tryCrypt(() => ok(crypto.randomUUID())) + }, + v7() { + return tryCrypt(() => { + const now = Date.now() + const timeHex = now.toString(16).padStart(12, '0') + const randBytes = crypto.randomBytes(10) + const timeArray = new Uint8Array(6) + for (let i = 0; i < 6; i++) { + timeArray[i] = parseInt(timeHex.substring(i * 2, i * 2 + 2), 16) + } + randBytes[0] = (randBytes[0] & 0x0f) | 0x70 + randBytes[2] = (randBytes[2] & 0x3f) | 0x80 + const bytes = new Uint8Array([...timeArray, ...randBytes]) + const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('') + return ok( + `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}` + ) + }) + }, + generate({ version, count }) { + return tryCrypt(() => { + const ver = version ?? 'v4' + const n = Math.max(1, Math.min(Number(count) || 1, 100)) + const results = [] + for (let i = 0; i < n; i++) { + results.push(ver === 'v7' ? uuid.v7().data : uuid.v4().data) + } + return ok(results.join('\n')) + }) + } +} + +const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +function base58Encode({ input }) { + return tryCrypt(() => { + const buf = Buffer.from(String(input), 'utf-8') + if (buf.length === 0) return ok('') + let num = BigInt('0x' + buf.toString('hex')) + let encoded = '' + const base = BigInt(58) + while (num > 0) { + encoded = BASE58_ALPHABET[Number(num % base)] + encoded + num = num / base + } + let leading = 0 + for (const b of buf) { + if (b === 0) leading++ + else break + } + return ok('1'.repeat(leading) + encoded) + }) +} + +function base58Decode({ input }) { + return tryCrypt(() => { + const s = String(input).trim() + if (!s) return ok('') + if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(s)) { + return fail('Base58 字符无效') + } + let num = BigInt(0) + const base = BigInt(58) + for (const ch of s) { + num = num * base + BigInt(BASE58_ALPHABET.indexOf(ch)) + } + let leading = 0 + for (const ch of s) { + if (ch === '1') leading++ + else break + } + const hex = num.toString(16) + const padded = hex.length % 2 === 0 ? hex : '0' + hex + const buf = Buffer.from('00'.repeat(leading) + padded, 'hex') + return ok(buf.toString('utf-8')) + }) +} + +function base58CheckEncode({ input }) { + return tryCrypt(() => { + const hexStr = String(input).trim().replace(/\s+/g, '') + if (!/^[0-9a-fA-F]*$/.test(hexStr) || hexStr.length % 2 !== 0) { + return fail('请输入偶数位的 Hex 字符串') + } + const buf = Buffer.from(hexStr, 'hex') + const hash1 = crypto.createHash('sha256').update(buf).digest() + const hash2 = crypto.createHash('sha256').update(hash1).digest() + const checksum = hash2.subarray(0, 4) + const payload = Buffer.concat([buf, checksum]) + let num = BigInt('0x' + payload.toString('hex')) + let encoded = '' + const base = BigInt(58) + while (num > 0) { + encoded = BASE58_ALPHABET[Number(num % base)] + encoded + num = num / base + } + let leading = 0 + for (const b of payload) { + if (b === 0) leading++ + else break + } + return ok('1'.repeat(leading) + encoded) + }) +} + +function base58CheckDecode({ input }) { + return tryCrypt(() => { + const s = String(input).trim() + if (!s) return ok({ valid: false, data: '' }) + if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(s)) { + return ok({ valid: false, data: '' }) + } + let num = BigInt(0) + const base = BigInt(58) + for (const ch of s) { + num = num * base + BigInt(BASE58_ALPHABET.indexOf(ch)) + } + let leading = 0 + for (const ch of s) { + if (ch === '1') leading++ + else break + } + const hex = num.toString(16) + const padded = hex.length % 2 === 0 ? hex : '0' + hex + const fullHex = '00'.repeat(leading) + padded + const payload = Buffer.from(fullHex, 'hex') + if (payload.length < 5) return ok({ valid: false, data: '' }) + const data = payload.subarray(0, payload.length - 4) + const checksum = payload.subarray(payload.length - 4) + const hash1 = crypto.createHash('sha256').update(data).digest() + const hash2 = crypto.createHash('sha256').update(hash1).digest() + const expected = hash2.subarray(0, 4) + const valid = checksum.equals(expected) + return ok({ valid, data: data.toString('hex') }) + }) +} + +const crc32 = { + checksum({ input }) { + return tryCrypt(() => { + const buf = Buffer.from(String(input), 'utf-8') + let crc = 0xFFFFFFFF + const table = getCRC32Table() + for (let i = 0; i < buf.length; i++) { + const byte = buf[i] + crc = (crc >>> 8) ^ table[(crc ^ byte) & 0xFF] + } + crc = (crc ^ 0xFFFFFFFF) >>> 0 + return ok(crc.toString(16).padStart(8, '0')) + }) + } +} + +let crcTable = null +function getCRC32Table() { + if (crcTable) return crcTable + crcTable = new Uint32Array(256) + for (let i = 0; i < 256; i++) { + let c = i + for (let j = 0; j < 8; j++) { + c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1) + } + crcTable[i] = c >>> 0 + } + return crcTable +} + +module.exports = { + uuid, + base58: { encode: base58Encode, decode: base58Decode }, + base58check: { encode: base58CheckEncode, decode: base58CheckDecode }, + crc32 +} diff --git a/plugins/crypt-toolkit/public/preload/crypt/serde-utils.test.ts b/plugins/crypt-toolkit/public/preload/crypt/serde-utils.test.ts new file mode 100644 index 000000000..59548016e --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/serde-utils.test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it } from 'vitest' +import { uuid, base58, base58check, crc32 } from './serde-utils' + +describe('uuid', () => { + it('generates valid v4 UUID format', () => { + const r = uuid.v4() + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + } + }) + + it('generates unique UUIDs', () => { + const a = uuid.v4() + const b = uuid.v4() + expect(a.ok).toBe(true) + expect(b.ok).toBe(true) + if (a.ok && b.ok) { + expect(a.data).not.toBe(b.data) + } + }) + + it('generates v7 UUID with version marker', () => { + const r = uuid.v7() + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + } + }) + + it('generate produces N UUIDs', () => { + const r = uuid.generate({ version: 'v4', count: 5 }) + expect(r.ok).toBe(true) + if (r.ok) { + const lines = r.data.split('\n') + expect(lines.length).toBe(5) + lines.forEach((id) => { + expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + }) + } + }) +}) + +describe('base58', () => { + it('encode + decode round trip', () => { + const plain = 'Hello Base58!' + const enc = base58.encode({ input: plain }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = base58.decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) { + expect(dec.data).toBe(plain) + } + } + }) + + it('encodes known value: "hello world"', () => { + const r = base58.encode({ input: 'hello world' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('StV1DL6CwTryKyV') + } + }) + + it('decodes known value: "StV1DL6CwTryKyV"', () => { + const r = base58.decode({ input: 'StV1DL6CwTryKyV' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('hello world') + } + }) + + it('single character round trip', () => { + const enc = base58.encode({ input: 'A' }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = base58.decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) { + expect(dec.data).toBe('A') + } + } + }) + + it('rejects invalid characters', () => { + const r = base58.decode({ input: '0OIl' }) + expect(r.ok).toBe(false) + }) +}) + +describe('base58check', () => { + it('encode + decode round trip', () => { + const hexInput = '00' + '9d1e01' + '7a8b9c' + const enc = base58check.encode({ input: hexInput }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = base58check.decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) { + expect(dec.data.valid).toBe(true) + expect(dec.data.data.startsWith('00')).toBe(true) + } + } + }) + + it('decode detects tampered checksum', () => { + const enc = base58check.encode({ input: '009d1e017a8b9c' }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const tampered = enc.data.slice(0, -1) + (enc.data.slice(-1) === '1' ? '2' : '1') + const dec = base58check.decode({ input: tampered }) + expect(dec.ok).toBe(true) + if (dec.ok) { + expect(dec.data.valid).toBe(false) + } + } + }) + + it('rejects odd-length hex input', () => { + const r = base58check.encode({ input: 'abc' }) + expect(r.ok).toBe(false) + }) + + it('rejects invalid hex characters', () => { + const r = base58check.encode({ input: 'GGGG' }) + expect(r.ok).toBe(false) + }) +}) + +describe('crc32', () => { + it('known vector: "abc" → 352441c2', () => { + const r = crc32.checksum({ input: 'abc' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('352441c2') + } + }) + + it('empty string CRC32', () => { + const r = crc32.checksum({ input: '' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('00000000') + } + }) + + it('consistency - same input same output', () => { + const a = crc32.checksum({ input: 'hello' }) + const b = crc32.checksum({ input: 'hello' }) + if (a.ok && b.ok) { + expect(a.data).toBe(b.data) + } + }) + + it('different input different output', () => { + const a = crc32.checksum({ input: 'hello' }) + const b = crc32.checksum({ input: 'world' }) + expect(a.ok).toBe(true) + expect(b.ok).toBe(true) + if (a.ok && b.ok) { + expect(a.data).not.toBe(b.data) + } + }) + + it('known vector: "123456789" → cbf43926', () => { + const r = crc32.checksum({ input: '123456789' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toBe('cbf43926') + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/text-utils.js b/plugins/crypt-toolkit/public/preload/crypt/text-utils.js new file mode 100644 index 000000000..182ba89a9 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/text-utils.js @@ -0,0 +1,17 @@ +const { ok, tryCrypt } = require('./envelope') + +function format({ input, indent }) { + return tryCrypt(() => { + const obj = JSON.parse(String(input)) + return ok(JSON.stringify(obj, null, Number(indent) || 2)) + }) +} + +function minify({ input }) { + return tryCrypt(() => { + const obj = JSON.parse(String(input)) + return ok(JSON.stringify(obj)) + }) +} + +module.exports = { format, minify } diff --git a/plugins/crypt-toolkit/public/preload/crypt/text-utils.test.ts b/plugins/crypt-toolkit/public/preload/crypt/text-utils.test.ts new file mode 100644 index 000000000..39e277522 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/text-utils.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' +import { format, minify } from './text-utils' + +describe('json', () => { + it('formats with default indent 2', () => { + const r = format({ input: '{"a":1,"b":2}' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).toContain('\n') + expect(r.data).toContain(' ') + } + }) + + it('formats with custom indent', () => { + const r = format({ input: '{"a":1}', indent: 4 }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toContain(' "a"') + }) + + it('minifies JSON', () => { + const r = minify({ input: '{\n "a": 1,\n "b": 2\n}' }) + expect(r.ok).toBe(true) + if (r.ok) { + expect(r.data).not.toContain('\n') + expect(r.data).toBe('{"a":1,"b":2}') + } + }) + + it('rejects invalid JSON on format', () => { + const r = format({ input: '{invalid}' }) + expect(r.ok).toBe(false) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/unicode-escape.js b/plugins/crypt-toolkit/public/preload/crypt/unicode-escape.js new file mode 100644 index 000000000..5fad558fe --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/unicode-escape.js @@ -0,0 +1,38 @@ +const { ok, tryCrypt } = require('./envelope') + +function encode({ input }) { + return tryCrypt(() => { + let out = '' + for (const ch of String(input)) { + const code = ch.codePointAt(0) + if (code > 0xffff) { + out += '\\u{' + code.toString(16) + '}' + } else if (code > 127) { + out += '\\u' + code.toString(16).padStart(4, '0') + } else { + out += ch + } + } + return ok(out) + }) +} + +function decode({ input }) { + return tryCrypt(() => { + const s = String(input) + let out = '' + const re = /\\u\{([0-9a-fA-F]+)\}|\\u([0-9a-fA-F]{4})/g + let last = 0 + let match + while ((match = re.exec(s))) { + out += s.slice(last, match.index) + const hex = match[1] || match[2] + out += String.fromCodePoint(parseInt(hex, 16)) + last = re.lastIndex + } + out += s.slice(last) + return ok(out) + }) +} + +module.exports = { encode, decode } diff --git a/plugins/crypt-toolkit/public/preload/crypt/unicode-escape.test.ts b/plugins/crypt-toolkit/public/preload/crypt/unicode-escape.test.ts new file mode 100644 index 000000000..9d6d8b544 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/unicode-escape.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { encode, decode } from './unicode-escape' + +describe('unicode-escape', () => { + it('encodes emoji', () => { + expect(encode({ input: '😀' })).toEqual({ ok: true, data: '\\u{1f600}' }) + }) + + it('encodes accented chars', () => { + expect(encode({ input: 'é' })).toEqual({ ok: true, data: '\\u00e9' }) + }) + + it('leaves ascii as-is', () => { + expect(encode({ input: 'hello' })).toEqual({ ok: true, data: 'hello' }) + }) + + it('decode \\u{1f600}', () => { + expect(decode({ input: '\\u{1f600}' })).toEqual({ ok: true, data: '😀' }) + }) + + it('decode \\u00e9', () => { + expect(decode({ input: '\\u00e9' })).toEqual({ ok: true, data: 'é' }) + }) + + it('round trip', () => { + const orig = 'Hello 世界! 😀🎉' + const enc = encode({ input: orig }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = decode({ input: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(orig) + } + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/crypt/xor-cipher.js b/plugins/crypt-toolkit/public/preload/crypt/xor-cipher.js new file mode 100644 index 000000000..4d8013576 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/xor-cipher.js @@ -0,0 +1,29 @@ +const { ok, fail, tryCrypt } = require('./envelope') + +function encrypt({ key, plaintext }) { + return tryCrypt(() => { + if (!key) return fail('密钥不能为空') + const keyBuf = Buffer.from(String(key), 'utf-8') + const ptBuf = Buffer.from(String(plaintext), 'utf-8') + const ctBuf = Buffer.alloc(ptBuf.length) + for (let i = 0; i < ptBuf.length; i++) { + ctBuf[i] = ptBuf[i] ^ keyBuf[i % keyBuf.length] + } + return ok(ctBuf.toString('hex')) + }) +} + +function decrypt({ key, ciphertext }) { + return tryCrypt(() => { + if (!key) return fail('密钥不能为空') + const keyBuf = Buffer.from(String(key), 'utf-8') + const ctBuf = Buffer.from(String(ciphertext), 'hex') + const ptBuf = Buffer.alloc(ctBuf.length) + for (let i = 0; i < ctBuf.length; i++) { + ptBuf[i] = ctBuf[i] ^ keyBuf[i % keyBuf.length] + } + return ok(ptBuf.toString('utf-8')) + }) +} + +module.exports = { encrypt, decrypt } diff --git a/plugins/crypt-toolkit/public/preload/crypt/xor-cipher.test.ts b/plugins/crypt-toolkit/public/preload/crypt/xor-cipher.test.ts new file mode 100644 index 000000000..f7417f0f8 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/crypt/xor-cipher.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { encrypt, decrypt } from './xor-cipher' + +describe('xor-stream', () => { + it('encrypt + decrypt round trip', () => { + const key = 'mykey123' + const plaintext = 'Hello, XOR!' + const enc = encrypt({ key, plaintext }) + expect(enc.ok).toBe(true) + if (enc.ok) { + const dec = decrypt({ key, ciphertext: enc.data }) + expect(dec.ok).toBe(true) + if (dec.ok) expect(dec.data).toBe(plaintext) + } + }) + + it('empty plaintext', () => { + const r = encrypt({ key: 'key', plaintext: '' }) + expect(r.ok).toBe(true) + if (r.ok) expect(r.data).toBe('') + }) + + it('rejects empty key', () => { + expect(encrypt({ key: '', plaintext: 'data' }).ok).toBe(false) + }) + + it('same key + plaintext produces same output', () => { + const a = encrypt({ key: 'k', plaintext: 'test' }) + const b = encrypt({ key: 'k', plaintext: 'test' }) + if (a.ok && b.ok) expect(a.data).toBe(b.data) + }) +}) diff --git a/plugins/crypt-toolkit/public/preload/package-lock.json b/plugins/crypt-toolkit/public/preload/package-lock.json new file mode 100644 index 000000000..f3558d15d --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "crypt-tool-preload", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crypt-tool-preload", + "dependencies": { + "bcryptjs": "^3.0.3" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + } + } +} diff --git a/plugins/crypt-toolkit/public/preload/package.json b/plugins/crypt-toolkit/public/preload/package.json new file mode 100644 index 000000000..7dcc59699 --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/package.json @@ -0,0 +1,8 @@ +{ + "name": "crypt-tool-preload", + "type": "commonjs", + "private": true, + "dependencies": { + "bcryptjs": "^3.0.3" + } +} diff --git a/plugins/crypt-toolkit/public/preload/services.js b/plugins/crypt-toolkit/public/preload/services.js new file mode 100644 index 000000000..77efe091e --- /dev/null +++ b/plugins/crypt-toolkit/public/preload/services.js @@ -0,0 +1,7 @@ +const crypt = require('./crypt') + +// 通过 window 对象向渲染进程注入 nodejs 能力 +window.services = { + // 加解密服务 + crypt +} diff --git a/plugins/crypt-toolkit/src/App.tsx b/plugins/crypt-toolkit/src/App.tsx new file mode 100644 index 000000000..9945f7f2b --- /dev/null +++ b/plugins/crypt-toolkit/src/App.tsx @@ -0,0 +1,111 @@ +import { useEffect, useMemo, useState } from 'react' +import { algorithms, firstEnabledId } from './registry' +import { Shell, type ShellView } from './shell' +import { loadSettings, saveSettings, syncFeatures, type Settings } from './config' +import { useZtoolsTheme } from './hooks/useZtoolsTheme' + +export default function App() { + const [settings, setSettings] = useState(() => loadSettings(algorithms)) + const [view, setView] = useState(() => ({ + kind: 'algorithm', + id: firstEnabledId(algorithms, loadSettings(algorithms).enabled) ?? '' + })) + const [searchText, setSearchText] = useState('') + const { primaryColor } = useZtoolsTheme() + + useEffect(() => { + document.documentElement.style.setProperty('--blue', primaryColor) + }, [primaryColor]) + + useEffect(() => { + syncFeatures(algorithms, settings.enabled) + }, [settings.enabled]) + + useEffect(() => { + window.ztools.onPluginEnter((action: any) => { + const code = action.code as string + if (code && code.startsWith('alg:')) { + const id = code.slice(4) + setView({ kind: 'algorithm', id }) + setSearchText('') + } else { + const id = firstEnabledId(algorithms, settings.enabled) + setView(id ? { kind: 'algorithm', id } : { kind: 'settings' }) + setSearchText('') + } + }) + }, [settings.enabled]) + useEffect(() => { + // @ts-ignore + window.ztools.onThemeChange((themeInfo: { + isDark: boolean, + primaryColor: string, + customColor: string, + windowMaterial: string + }) => { + const root = document.documentElement + + // 1. 应用主色调 + if (themeInfo.primaryColor) { + root.style.setProperty('--blue', themeInfo.primaryColor) + } + + // 2. 应用自定义色(如有) + if (themeInfo.customColor) { + root.style.setProperty('--custom-color', themeInfo.customColor) + } + + // 3. 切换深色/浅色模式 class + if (themeInfo.isDark) { + root.classList.add('theme-dark') + root.classList.remove('theme-light') + } else { + root.classList.add('theme-light') + root.classList.remove('theme-dark') + } + + // 4. 应用窗口材质(如支持) + if (themeInfo.windowMaterial) { + root.dataset.windowMaterial = themeInfo.windowMaterial + } + }) + }, []) + + useEffect(() => { + const ok = window.ztools.setSubInput( + ({ text }) => { setSearchText(text.trim()) }, + '搜索算法名称...', + false + ) + if (!ok) return + return () => { window.ztools.removeSubInput() } + }, []) + + const onSettingsChange = (s: Settings) => { + saveSettings(s) + setSettings(s) + if (view.kind === 'algorithm') { + const still = s.enabled[view.id] + if (!still) { + const next = firstEnabledId(algorithms, s.enabled) + setView(next ? { kind: 'algorithm', id: next } : { kind: 'settings' }) + } + } + } + + const mods = useMemo(() => algorithms, []) + + return ( +
+ setSearchText('')} + /> +
+ ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/adler32/index.ts b/plugins/crypt-toolkit/src/algorithms/adler32/index.ts new file mode 100644 index 000000000..5fed294b7 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/adler32/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Adler32UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const adler32: AlgorithmModule = { meta, Component: Adler32UI } diff --git a/plugins/crypt-toolkit/src/algorithms/adler32/meta.ts b/plugins/crypt-toolkit/src/algorithms/adler32/meta.ts new file mode 100644 index 000000000..9f2c690da --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/adler32/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'adler32', + category: 'tools', + label: 'Adler-32', + title: 'Adler-32 校验和', + reversible: false, + cmds: ['adler32', 'adler-32'], + defaultEnabled: true, + teach: { + summary: + 'Adler-32 是 zlib 使用的快速校验和算法,输出 32 位(8 位十六进制)。比 CRC32 更快但可靠性稍低,适用于压缩数据完整性校验,不适合用于安全场景。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/adler32/ui.tsx b/plugins/crypt-toolkit/src/algorithms/adler32/ui.tsx new file mode 100644 index 000000000..cd73dcce0 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/adler32/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Adler32UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.adler32.checksum({ input })) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/aes/index.ts b/plugins/crypt-toolkit/src/algorithms/aes/index.ts new file mode 100644 index 000000000..3d0386734 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/aes/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import AesUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const aes: AlgorithmModule = { meta, Component: AesUI } diff --git a/plugins/crypt-toolkit/src/algorithms/aes/meta.ts b/plugins/crypt-toolkit/src/algorithms/aes/meta.ts new file mode 100644 index 000000000..7c47917ff --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/aes/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'aes', + category: 'symmetric', + label: 'AES', + title: 'AES 对称加密', + reversible: true, + cmds: ['aes加密', 'AES'], + defaultEnabled: true, + teach: { + summary: + 'AES 是分组对称加密标准,加密与解密使用同一密钥。支持 128/192/256 位密钥;常见模式 CBC、GCM。IV 用于保证相同明文每次产出不同密文,GCM 额外提供完整性校验。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/aes/ui.tsx b/plugins/crypt-toolkit/src/algorithms/aes/ui.tsx new file mode 100644 index 000000000..61c9be771 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/aes/ui.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function AesUI({ direction = 'encrypt' }: AlgorithmProps) { + const dir = direction + const [key, setKey] = useState('my-secret-key-16b') + const [iv, setIv] = useState('random-iv-16bytes') + const [mode, setMode] = useState('CBC') + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encrypt' + ? window.services.crypt.aes.encrypt({ key, iv, mode, plaintext: input }) + : window.services.crypt.aes.decrypt({ key, iv, mode, ciphertext: input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encrypt' ? '明文' : '密文' + const inputHelp = dir === 'encrypt' + ? '待加密的 UTF-8 明文' + : '待解密的 Base64 密文' + const outputLabel = dir === 'encrypt' ? '密文' : '明文' + const outputHelp = dir === 'encrypt' + ? '加密后的 Base64 密文' + : '解密后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ + + + +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/argon2/index.ts b/plugins/crypt-toolkit/src/algorithms/argon2/index.ts new file mode 100644 index 000000000..9b33aa9d1 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/argon2/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const argon2 = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/argon2/meta.ts b/plugins/crypt-toolkit/src/algorithms/argon2/meta.ts new file mode 100644 index 000000000..b6dc33e7b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/argon2/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'argon2', + category: 'kdf', + label: 'Argon2', + title: 'Argon2 密码哈希', + reversible: false, + cmds: ['argon2'], + defaultEnabled: false, + teach: { + summary: + 'Argon2 是 2015 年密码哈希竞赛冠军,argon2id 兼顾侧信道抗性与 GPU/ASIC 抗性。支持可调时间成本、内存成本与并行度,是目前推荐的首选密码存储算法(优于 bcrypt 与 PBKDF2)。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/argon2/ui.tsx b/plugins/crypt-toolkit/src/algorithms/argon2/ui.tsx new file mode 100644 index 000000000..090a1e042 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/argon2/ui.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { showError, showData } from '../codec' + +type Mode = 'hash' | 'verify' + +export default function Argon2UI({}: AlgorithmProps) { + const [mode, setMode] = useState('hash') + const [password, setPassword] = useState('') + const [hashString, setHashString] = useState('') + const [type, setType] = useState('argon2id') + const [timeCost, setTimeCost] = useState('3') + const [memoryCost, setMemoryCost] = useState('65536') + const [parallelism, setParallelism] = useState('1') + const [salt, setSalt] = useState('') + const [hashLength, setHashLength] = useState('32') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + const [busy, setBusy] = useState(false) + + const run = async () => { + setBusy(true) + try { + if (mode === 'hash') { + const r = await window.services.crypt.argon2.hash({ + password, + type, + timeCost: Number(timeCost) || 3, + memoryCost: Number(memoryCost) || 65536, + parallelism: Number(parallelism) || 1, + salt, + hashLength: Number(hashLength) || 32 + }) + setError(showError(r)) + setOutput(showData(r)) + } else { + const r = await window.services.crypt.argon2.verify({ password, hash: hashString }) + setError(showError(r)) + if (r.ok) { + setOutput(r.data ? '✅ 密码匹配' : '❌ 密码不匹配') + } + } + } catch (e) { + setError(e instanceof Error ? e.message : '计算失败') + setOutput('') + } + setBusy(false) + } + + const isHash = mode === 'hash' + + return ( + <> +
+ + +
+
+
输入
+
+ + {!isHash ? ( + + ) : ( + <> + + + + + + + + )} +
+
+
+ +
+
+
输出
+ {isHash ? ( + {}} + readOnly + variant="output" + copyable + /> + ) : ( + {}} + readOnly + variant="output" + copyable + /> + )} +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/base32/index.ts b/plugins/crypt-toolkit/src/algorithms/base32/index.ts new file mode 100644 index 000000000..5945b8532 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/base32/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Base32UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const base32: AlgorithmModule = { meta, Component: Base32UI } diff --git a/plugins/crypt-toolkit/src/algorithms/base32/meta.ts b/plugins/crypt-toolkit/src/algorithms/base32/meta.ts new file mode 100644 index 000000000..023ecff07 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/base32/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'base32', + category: 'encoding', + label: 'Base32', + title: 'Base32 编码', + reversible: true, + cmds: ['base32', 'b32'], + defaultEnabled: true, + teach: { + summary: + 'Base32 将二进制数据按 5 bit 一组映射为 A-Z 与 2-7 共 32 个字符,适合人工抄录与不区分大小写的场景。默认使用 RFC 4648 标准,可选尾部 = 填充。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/base32/ui.tsx b/plugins/crypt-toolkit/src/algorithms/base32/ui.tsx new file mode 100644 index 000000000..74f6a1a46 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/base32/ui.tsx @@ -0,0 +1,81 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Base32UI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [padding, setPadding] = useState('true') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.base32.encode({ input, padding: padding === 'true' }) + : window.services.crypt.base32.decode({ input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '明文' : 'Base32' + const inputHelp = dir === 'encode' + ? '待编码的 UTF-8 文本' + : '待解码的 Base32 字符串' + const outputLabel = dir === 'encode' ? 'Base32' : '明文' + const outputHelp = dir === 'encode' + ? '编码后的 Base32 字符串' + : '解码后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ {dir === 'encode' ? ( + ('base58') + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + if (variant === 'base58') { + const r = runCodec(() => window.services.crypt.base58.encode({ input })) + setError(showError(r)) + setOutput(showData(r)) + } else { + const r = runCodec(() => window.services.crypt.base58check.encode({ input })) + setError(showError(r)) + setOutput(showData(r)) + } + } + + const decode = () => { + if (variant === 'base58') { + const r = runCodec(() => window.services.crypt.base58.decode({ input })) + setError(showError(r)) + setOutput(showData(r)) + } else { + const r = runCodec(() => window.services.crypt.base58check.decode({ input })) as { + ok: true; data: { valid: boolean; data: string } + } | { ok: false; error: string } + setError(showError(r)) + if (r.ok) { + setOutput(`校验: ${r.data.valid ? '✅ 通过' : '❌ 失败'}\n数据 Hex: ${r.data.data}`) + } else { + setOutput('') + } + } + } + + const isCheck = variant === 'base58check' + + return ( + <> +
+ + +
+
+
输入
+
+ +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/base64/index.ts b/plugins/crypt-toolkit/src/algorithms/base64/index.ts new file mode 100644 index 000000000..cc279126a --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/base64/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Base64UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const base64: AlgorithmModule = { meta, Component: Base64UI } diff --git a/plugins/crypt-toolkit/src/algorithms/base64/meta.ts b/plugins/crypt-toolkit/src/algorithms/base64/meta.ts new file mode 100644 index 000000000..8eda69c02 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/base64/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'base64', + category: 'encoding', + label: 'Base64', + title: 'Base64 编码', + reversible: true, + cmds: ['base64', 'Base64编码'], + defaultEnabled: true, + teach: { + summary: + 'Base64 将二进制/文本按 6 bit 一组映射为 64 个可打印字符,常用于 URL、邮件与 JSON 中嵌入数据。编码可逆,解码还原为 UTF-8 文本。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/base64/ui.tsx b/plugins/crypt-toolkit/src/algorithms/base64/ui.tsx new file mode 100644 index 000000000..a3c279566 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/base64/ui.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Base64UI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.base64.encode(input) + : window.services.crypt.base64.decode(input) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '明文' : 'Base64' + const inputHelp = dir === 'encode' + ? '待编码的 UTF-8 文本' + : '待解码的 Base64 字符串' + const outputLabel = dir === 'encode' ? 'Base64' : '明文' + const outputHelp = dir === 'encode' + ? '编码后的 Base64 字符串' + : '解码后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/bcrypt/index.ts b/plugins/crypt-toolkit/src/algorithms/bcrypt/index.ts new file mode 100644 index 000000000..dfcfb00e4 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/bcrypt/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import BcryptUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const bcrypt: AlgorithmModule = { meta, Component: BcryptUI } diff --git a/plugins/crypt-toolkit/src/algorithms/bcrypt/meta.ts b/plugins/crypt-toolkit/src/algorithms/bcrypt/meta.ts new file mode 100644 index 000000000..f3ec48f27 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/bcrypt/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'bcrypt', + category: 'kdf', + label: 'bcrypt', + title: 'bcrypt 密码哈希', + reversible: false, + cmds: ['bcrypt', '密码哈希'], + defaultEnabled: true, + teach: { + summary: + 'bcrypt 是专为密码存储设计的自适应哈希算法,内置随机盐与可调节计算成本(4-31),每增加 1 成本翻倍计算时间,天然抗 GPU/ASIC 暴力破解。支持哈希生成与密码验证两种模式。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/bcrypt/ui.tsx b/plugins/crypt-toolkit/src/algorithms/bcrypt/ui.tsx new file mode 100644 index 000000000..23b727b9f --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/bcrypt/ui.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +const PRESET_HASH = '$2b$12$KIXxKzKzKIXxKzKzKzKzKu0uEYQ7V1p9XwY6ZbN8mDcFqJ3rHtC' + +export default function BcryptUI({}: AlgorithmProps) { + const [password, setPassword] = useState('') + const [cost, setCost] = useState('12') + const [hashInput, setHashInput] = useState('') + const [saltHash, setSaltHash] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + const [isVerifyMode, setIsVerifyMode] = useState(false) + + const generate = () => { + const r = runCodec(() => + window.services.crypt.bcrypt.hash({ + password, + cost: Number(cost) || 12 + }) + ) + const err = showError(r) + setError(err) + if (!err) { + const data = showData(r) + setOutput(data) + setSaltHash(data) + } + } + + const verify = () => { + const r = runCodec(() => + window.services.crypt.bcrypt.verify({ + password, + hash: hashInput || saltHash + }) + ) + const err = showError(r) + setError(err) + if (!err && r.ok) { + setOutput(r.data ? '✅ 密码匹配' : '❌ 密码不匹配') + } + } + + const loadPreset = () => { + setSaltHash(PRESET_HASH) + setHashInput(PRESET_HASH) + } + + return ( + <> +
+
输入
+
+ + {!isVerifyMode ? ( + + ) : ( + + )} +
+
+ +
+ + + {isVerifyMode && ( + + )} +
+ +
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ + {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/chacha/index.ts b/plugins/crypt-toolkit/src/algorithms/chacha/index.ts new file mode 100644 index 000000000..8a27f0608 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/chacha/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const chacha = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/chacha/meta.ts b/plugins/crypt-toolkit/src/algorithms/chacha/meta.ts new file mode 100644 index 000000000..1ed63c1ff --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/chacha/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'chacha', + category: 'symmetric', + label: 'ChaCha20', + title: 'ChaCha20-Poly1305 对称加密', + reversible: true, + cmds: ['chacha', 'chacha20', 'chacha20-poly1305'], + defaultEnabled: true, + teach: { + summary: + 'ChaCha20-Poly1305 是现代流密码与 AEAD 认证加密组合,TLS 1.3、WireGuard、QUIC 等主流协议广泛采用。256 位密钥 + 96 位 Nonce,比 AES-GCM 在纯软件实现中更快且抗侧信道。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/chacha/ui.tsx b/plugins/crypt-toolkit/src/algorithms/chacha/ui.tsx new file mode 100644 index 000000000..c8be9da00 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/chacha/ui.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function ChachaUI({ direction = 'encrypt' }: AlgorithmProps) { + const dir = direction + const [key, setKey] = useState('') + const [nonce, setNonce] = useState('') + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const keygen = () => { + const r = runCodec(() => window.services.crypt.chacha.keygen()) + if (r.ok) setKey(r.data) + } + + const noncegen = () => { + const r = runCodec(() => window.services.crypt.chacha.noncegen()) + if (r.ok) setNonce(r.data) + } + + const run = () => { + const r = runCodec(() => + dir === 'encrypt' + ? window.services.crypt.chacha.encrypt({ key, nonce, plaintext: input }) + : window.services.crypt.chacha.decrypt({ key, nonce, ciphertext: input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encrypt' ? '明文' : '密文' + const inputHelp = dir === 'encrypt' + ? '待加密的 UTF-8 明文' + : '待解密的 Base64 密文' + const outputLabel = dir === 'encrypt' ? '密文' : '明文' + const outputHelp = dir === 'encrypt' + ? '加密后的 Base64 密文(含认证标签)' + : '解密后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/codec.test.ts b/plugins/crypt-toolkit/src/algorithms/codec.test.ts new file mode 100644 index 000000000..027daafb9 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/codec.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it, vi } from 'vitest' +import { runCodec } from './codec' + +describe('runCodec', () => { + it('returns ok data', () => { + vi.stubGlobal('window', { + services: { crypt: { base64: { encode: () => ({ ok: true, data: 'X' }) } } } + }) + expect(runCodec(() => window.services.crypt.base64.encode('a'))).toEqual({ + ok: true, + data: 'X' + }) + vi.unstubAllGlobals() + }) + + it('passes through failure', () => { + vi.stubGlobal('window', { + services: { + crypt: { base64: { decode: () => ({ ok: false, error: 'Base64 格式无效' }) } } + } + }) + expect(runCodec(() => window.services.crypt.base64.decode('@'))).toEqual({ + ok: false, + error: 'Base64 格式无效' + }) + vi.unstubAllGlobals() + }) +}) diff --git a/plugins/crypt-toolkit/src/algorithms/codec.ts b/plugins/crypt-toolkit/src/algorithms/codec.ts new file mode 100644 index 000000000..dc7b3be03 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/codec.ts @@ -0,0 +1,19 @@ +import type { CryptResult } from '../types/crypt' + +export function runCodec(fn: () => CryptResult): CryptResult { + try { + return fn() + } catch (e) { + return { ok: false, error: e instanceof Error ? e.message : '计算失败' } + } +} + +export function showError(result: CryptResult): string { + if (result.ok) return '' + return result.error +} + +export function showData(result: CryptResult): T extends string ? string : T { + if (!result.ok) return '' as any + return result.data as any +} diff --git a/plugins/crypt-toolkit/src/algorithms/crc32/index.ts b/plugins/crypt-toolkit/src/algorithms/crc32/index.ts new file mode 100644 index 000000000..f503f0798 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/crc32/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const crc32 = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/crc32/meta.ts b/plugins/crypt-toolkit/src/algorithms/crc32/meta.ts new file mode 100644 index 000000000..c53df3514 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/crc32/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'crc32', + category: 'tools', + label: 'CRC32', + title: 'CRC32 校验和', + reversible: false, + cmds: ['crc32'], + defaultEnabled: true, + teach: { + summary: + 'CRC32 是最常用的循环冗余校验算法,被 ZIP、PNG、Ethernet、MPEG-2 等广泛采用。用于快速数据完整性校验(非加密安全)。标准多项式 0xEDB88320(ISO 3309 / ITU-T V.42)。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/crc32/ui.tsx b/plugins/crypt-toolkit/src/algorithms/crc32/ui.tsx new file mode 100644 index 000000000..2664c7d41 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/crc32/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Crc32UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.crc32.checksum({ input })) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/des/index.ts b/plugins/crypt-toolkit/src/algorithms/des/index.ts new file mode 100644 index 000000000..5ae709f73 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/des/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import DesUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const des: AlgorithmModule = { meta, Component: DesUI } diff --git a/plugins/crypt-toolkit/src/algorithms/des/meta.ts b/plugins/crypt-toolkit/src/algorithms/des/meta.ts new file mode 100644 index 000000000..7b4b9be3e --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/des/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'des', + category: 'symmetric', + label: '3DES', + title: '3DES 对称加密', + reversible: true, + cmds: ['des', '3des', 'des3', 'des-ede3'], + defaultEnabled: true, + teach: { + summary: + '3DES(Triple DES)对每个数据块进行三次 DES 加密操作,密钥长度 24 字节(168 位有效安全强度约 112 位)。虽已不推荐用于新系统,但仍有大量存量系统使用。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/des/ui.tsx b/plugins/crypt-toolkit/src/algorithms/des/ui.tsx new file mode 100644 index 000000000..0640e4073 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/des/ui.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function DesUI({ direction = 'encrypt' }: AlgorithmProps) { + const dir = direction + const [key, setKey] = useState('') + const [iv, setIv] = useState('') + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const keygen = () => { + const r = runCodec(() => window.services.crypt.des.keygen()) + if (r.ok) setKey(r.data) + } + + const ivgen = () => { + const r = runCodec(() => window.services.crypt.des.ivgen()) + if (r.ok) setIv(r.data) + } + + const run = () => { + const r = runCodec(() => + dir === 'encrypt' + ? window.services.crypt.des.encrypt({ key, iv, plaintext: input }) + : window.services.crypt.des.decrypt({ key, iv, ciphertext: input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encrypt' ? '明文' : '密文' + const inputHelp = dir === 'encrypt' + ? '待加密的 UTF-8 明文' + : '待解密的 Base64 密文' + const outputLabel = dir === 'encrypt' ? '密文' : '明文' + const outputHelp = dir === 'encrypt' + ? '加密后的 Base64 密文' + : '解密后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/ecdh/index.ts b/plugins/crypt-toolkit/src/algorithms/ecdh/index.ts new file mode 100644 index 000000000..dae77e137 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ecdh/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import EcdhUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const ecdh: AlgorithmModule = { meta, Component: EcdhUI } diff --git a/plugins/crypt-toolkit/src/algorithms/ecdh/meta.ts b/plugins/crypt-toolkit/src/algorithms/ecdh/meta.ts new file mode 100644 index 000000000..4f17ff291 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ecdh/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'ecdh', + category: 'asymmetric', + label: 'ECDH', + title: 'ECDH 密钥交换', + reversible: false, + cmds: ['ecdh', 'ecdh-key-exchange'], + defaultEnabled: true, + teach: { + summary: + 'ECDH(椭圆曲线迪菲-赫尔曼)密钥交换允许双方在不安全信道上协商共享密钥。每方生成自己的椭圆曲线密钥对,交换公钥后各自独立计算出相同的共享密钥。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/ecdh/ui.tsx b/plugins/crypt-toolkit/src/algorithms/ecdh/ui.tsx new file mode 100644 index 000000000..0b73a2dc1 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ecdh/ui.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function EcdhUI({}: AlgorithmProps) { + const [curve, setCurve] = useState('prime256v1') + const [privateKey, setPrivateKey] = useState('') + const [publicKey, setPublicKey] = useState('') + const [peerPublicKey, setPeerPublicKey] = useState('') + const [sharedSecret, setSharedSecret] = useState('') + const [error, setError] = useState('') + + const generateKeyPair = () => { + const r = runCodec(() => window.services.crypt.ecdh.generateKeyPair({ curve })) + if (r.ok) { + setPrivateKey(r.data.privateKey) + setPublicKey(r.data.publicKey) + } + } + + const deriveSecret = () => { + const r = runCodec(() => + window.services.crypt.ecdh.deriveSharedSecret({ privateKey, peerPublicKey, curve }) + ) + setError(showError(r)) + setSharedSecret(showData(r)) + } + + return ( + <> +
+
密钥生成
+
+ +
+
+ +
+
+ + +
+
+
+
共享密钥计算
+
+ +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/ecdsa/index.ts b/plugins/crypt-toolkit/src/algorithms/ecdsa/index.ts new file mode 100644 index 000000000..e1df57791 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ecdsa/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import EcdsaUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const ecdsa: AlgorithmModule = { meta, Component: EcdsaUI } diff --git a/plugins/crypt-toolkit/src/algorithms/ecdsa/meta.ts b/plugins/crypt-toolkit/src/algorithms/ecdsa/meta.ts new file mode 100644 index 000000000..354a153f5 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ecdsa/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'ecdsa', + category: 'asymmetric', + label: 'ECDSA', + title: 'ECDSA 签名与验证', + reversible: false, + cmds: ['ecdsa'], + defaultEnabled: true, + teach: { + summary: + 'ECDSA 是椭圆曲线数字签名算法,基于不同椭圆曲线(P-256 / P-384 / P-521)提供数字签名与验证功能,广泛用于证书、区块链与身份认证。签名使用私钥,验证使用对应公钥。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/ecdsa/ui.tsx b/plugins/crypt-toolkit/src/algorithms/ecdsa/ui.tsx new file mode 100644 index 000000000..8e6b87548 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ecdsa/ui.tsx @@ -0,0 +1,171 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +type Mode = 'sign' | 'verify' + +export default function EcdsaUI({}: AlgorithmProps) { + const [mode, setMode] = useState('sign') + const [curve, setCurve] = useState('prime256v1') + const [hash, setHash] = useState('sha256') + const [privateKey, setPrivateKey] = useState('') + const [publicKey, setPublicKey] = useState('') + const [data, setData] = useState('') + const [signature, setSignature] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + const [verifyResult, setVerifyResult] = useState<'' | 'true' | 'false'>('') + + const keygen = () => { + const r = runCodec(() => window.services.crypt.ecdsa.generateKeyPair({ curve })) + if (r.ok) { + setPrivateKey(r.data.privateKey) + setPublicKey(r.data.publicKey) + } + } + + const run = () => { + if (mode === 'sign') { + const r = runCodec(() => + window.services.crypt.ecdsa.sign({ privateKey, data, hash }) + ) + setError(showError(r)) + setOutput(showData(r)) + } else { + const r = runCodec(() => + window.services.crypt.ecdsa.verify({ publicKey, data, signature, hash }) + ) + setError(showError(r)) + if (r.ok) { + setVerifyResult(r.data ? 'true' : 'false') + setOutput(r.data ? '✅ 签名验证通过' : '❌ 签名无效') + } else { + setVerifyResult('') + } + } + } + + const isSign = mode === 'sign' + + return ( + <> +
+ + + +
+
+
输入
+
+ + + {isSign ? ( + + ) : ( + + )} + + {!isSign ? ( + + ) : null} +
+
+
+ +
+
+
输出
+ {isSign ? ( + {}} + readOnly + variant="output" + copyable + /> + ) : ( + {}} + readOnly + variant="output" + copyable + /> + )} +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/ed25519/index.ts b/plugins/crypt-toolkit/src/algorithms/ed25519/index.ts new file mode 100644 index 000000000..b99e4d804 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ed25519/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const ed25519 = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/ed25519/meta.ts b/plugins/crypt-toolkit/src/algorithms/ed25519/meta.ts new file mode 100644 index 000000000..e7da1e678 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ed25519/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'ed25519', + category: 'asymmetric', + label: 'Ed25519', + title: 'Ed25519 数字签名', + reversible: false, + cmds: ['ed25519', 'eddsa'], + defaultEnabled: true, + teach: { + summary: + 'Ed25519 是基于 Edwards 曲线 Ed25519 的 EdDSA 签名算法,密钥短(32 字节私钥 / 32 字节公钥)、速度快、抗侧信道,被 OpenSSH、TLS 1.3、Signal Protocol 等广泛采用。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/ed25519/ui.tsx b/plugins/crypt-toolkit/src/algorithms/ed25519/ui.tsx new file mode 100644 index 000000000..b63a4ff15 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ed25519/ui.tsx @@ -0,0 +1,144 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +type Mode = 'sign' | 'verify' + +export default function Ed25519UI({}: AlgorithmProps) { + const [mode, setMode] = useState('sign') + const [privateKey, setPrivateKey] = useState('') + const [publicKey, setPublicKey] = useState('') + const [data, setData] = useState('') + const [signature, setSignature] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + const [verifyResult, setVerifyResult] = useState<'' | 'true' | 'false'>('') + + const keygen = () => { + const r = runCodec(() => window.services.crypt.ed25519.generateKeyPair()) + if (r.ok) { + setPrivateKey(r.data.privateKey) + setPublicKey(r.data.publicKey) + } + } + + const run = () => { + if (mode === 'sign') { + const r = runCodec(() => + window.services.crypt.ed25519.sign({ privateKey, data }) + ) + setError(showError(r)) + setOutput(showData(r)) + } else { + const r = runCodec(() => + window.services.crypt.ed25519.verify({ publicKey, data, signature }) + ) + setError(showError(r)) + if (r.ok) { + setVerifyResult(r.data ? 'true' : 'false') + setOutput(r.data ? '✅ 签名验证通过' : '❌ 签名无效') + } else { + setVerifyResult('') + } + } + } + + const isSign = mode === 'sign' + + return ( + <> +
+ + + +
+
+
输入
+
+ {isSign ? ( + + ) : ( + + )} + + {!isSign ? ( + + ) : null} +
+
+
+ +
+
+
输出
+ {isSign ? ( + {}} + readOnly + variant="output" + copyable + /> + ) : ( + {}} + readOnly + variant="output" + copyable + /> + )} +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/hex/index.ts b/plugins/crypt-toolkit/src/algorithms/hex/index.ts new file mode 100644 index 000000000..743d48f6d --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hex/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import HexUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const hex: AlgorithmModule = { meta, Component: HexUI } diff --git a/plugins/crypt-toolkit/src/algorithms/hex/meta.ts b/plugins/crypt-toolkit/src/algorithms/hex/meta.ts new file mode 100644 index 000000000..b63fa104b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hex/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'hex', + category: 'encoding', + label: 'Hex', + title: 'Hex 编码', + reversible: true, + cmds: ['hex', 'Hex编码'], + defaultEnabled: true, + teach: { + summary: + 'Hex(十六进制)用 0-9a-f 每两个字符表示一个字节,便于查看与比对二进制内容。可逆:解码将偶数位 Hex 还原为 UTF-8 文本。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/hex/ui.tsx b/plugins/crypt-toolkit/src/algorithms/hex/ui.tsx new file mode 100644 index 000000000..a01f72ba3 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hex/ui.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function HexUI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.hex.encode(input) + : window.services.crypt.hex.decode(input) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '明文' : 'Hex' + const inputHelp = dir === 'encode' + ? '待编码的 UTF-8 文本' + : '待解码的偶数位十六进制字符串' + const outputLabel = dir === 'encode' ? 'Hex' : '明文' + const outputHelp = dir === 'encode' + ? '编码后的十六进制字符串' + : '解码后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/hkdf/index.ts b/plugins/crypt-toolkit/src/algorithms/hkdf/index.ts new file mode 100644 index 000000000..f7a8780fe --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hkdf/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const hkdf = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/hkdf/meta.ts b/plugins/crypt-toolkit/src/algorithms/hkdf/meta.ts new file mode 100644 index 000000000..518542b7e --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hkdf/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'hkdf', + category: 'kdf', + label: 'HKDF', + title: 'HKDF 密钥派生', + reversible: false, + cmds: ['hkdf'], + defaultEnabled: true, + teach: { + summary: + 'HKDF(RFC 5869)是基于 HMAC 的标准密钥派生函数,两阶段抽取+扩展设计,适用于从共享密钥材料(如 ECDH 共享点)派生多个会话密钥。TLS 1.3、Signal 协议、WireGuard 等广泛采用。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/hkdf/ui.tsx b/plugins/crypt-toolkit/src/algorithms/hkdf/ui.tsx new file mode 100644 index 000000000..5aa6b0cd6 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hkdf/ui.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function HkdfUI({}: AlgorithmProps) { + const [ikm, setIkm] = useState('') + const [salt, setSalt] = useState('') + const [info, setInfo] = useState('') + const [keylen, setKeylen] = useState('32') + const [hash, setHash] = useState('sha256') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + window.services.crypt.hkdf.derive({ + ikm: ikm.trim(), + salt: salt.trim(), + info: info.trim(), + keylen: Number(keylen) || 32, + hash + }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ + + + + +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/hmac/index.ts b/plugins/crypt-toolkit/src/algorithms/hmac/index.ts new file mode 100644 index 000000000..1fb5c96ea --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hmac/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import HmacUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const hmac: AlgorithmModule = { meta, Component: HmacUI } diff --git a/plugins/crypt-toolkit/src/algorithms/hmac/meta.ts b/plugins/crypt-toolkit/src/algorithms/hmac/meta.ts new file mode 100644 index 000000000..e7318b5a3 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hmac/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'hmac', + category: 'hmac', + label: 'HMAC', + title: 'HMAC-SHA256 消息认证', + reversible: false, + cmds: ['hmac', 'HMAC'], + defaultEnabled: true, + teach: { + summary: + 'HMAC 将密钥与消息经哈希组合,既验证完整性也证明来自持钥方。默认 SHA-256,输出十六进制认证码;比对相同认证码可确认消息未被篡改。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/hmac/ui.tsx b/plugins/crypt-toolkit/src/algorithms/hmac/ui.tsx new file mode 100644 index 000000000..1f1a6c4bd --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/hmac/ui.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function HmacUI({}: AlgorithmProps) { + const [key, setKey] = useState('') + const [algorithm, setAlgorithm] = useState('sha256') + const [message, setMessage] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + window.services.crypt.hmac.sign({ key, data: message, algo: algorithm }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ + + +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/htmlEntity/index.ts b/plugins/crypt-toolkit/src/algorithms/htmlEntity/index.ts new file mode 100644 index 000000000..cb344127b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/htmlEntity/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import HtmlEntityUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const htmlEntity: AlgorithmModule = { meta, Component: HtmlEntityUI } diff --git a/plugins/crypt-toolkit/src/algorithms/htmlEntity/meta.ts b/plugins/crypt-toolkit/src/algorithms/htmlEntity/meta.ts new file mode 100644 index 000000000..a6dd45ef3 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/htmlEntity/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'htmlEntity', + category: 'encoding', + label: 'HTML 实体', + title: 'HTML 实体编码', + reversible: true, + cmds: ['html-entity', 'html entity', 'htmle'], + defaultEnabled: true, + teach: { + summary: + 'HTML 实体将特殊字符编码为实体形式,如 \\< 变为 <、中文变为 中。可选择优先使用命名实体,解码时还原原始字符。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/htmlEntity/ui.tsx b/plugins/crypt-toolkit/src/algorithms/htmlEntity/ui.tsx new file mode 100644 index 000000000..622e686b0 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/htmlEntity/ui.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function HtmlEntityUI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [useNamed, setUseNamed] = useState('true') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.htmlEntity.encode({ input, useNamed: useNamed === 'true' }) + : window.services.crypt.htmlEntity.decode({ input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '原文' : 'HTML 实体' + const inputHelp = dir === 'encode' + ? '待编码的 UTF-8 文本' + : '待解码的 &lt; / 中 实体字符串' + const outputLabel = dir === 'encode' ? 'HTML 实体' : '原文' + const outputHelp = dir === 'encode' + ? '编码后的 HTML 实体字符串' + : '解码后的原始文本' + + return ( + <> +
+
输入
+
+ {dir === 'encode' ? ( + + ) : null} + +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/json/index.ts b/plugins/crypt-toolkit/src/algorithms/json/index.ts new file mode 100644 index 000000000..6832989fc --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/json/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import JsonUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const json: AlgorithmModule = { meta, Component: JsonUI } diff --git a/plugins/crypt-toolkit/src/algorithms/json/meta.ts b/plugins/crypt-toolkit/src/algorithms/json/meta.ts new file mode 100644 index 000000000..c82b669a6 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/json/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'json', + category: 'tools', + label: 'JSON 格式化', + title: 'JSON 格式化与压缩', + reversible: false, + cmds: ['json', 'json-format', 'json-format'], + defaultEnabled: true, + teach: { + summary: + 'JSON 格式化工具为混乱的 JSON 文本添加缩进与换行,提高可读性;压缩则去除所有空白字符,得到最小化 JSON 字符串,适合传输与存储。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/json/ui.tsx b/plugins/crypt-toolkit/src/algorithms/json/ui.tsx new file mode 100644 index 000000000..0bc43c37a --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/json/ui.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function JsonUI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [indent, setIndent] = useState('2') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const formatJson = () => { + const r = runCodec(() => { + const ind = indent === 'none' ? 0 : Number(indent) + return window.services.crypt.json.format({ input, indent: ind }) + }) + setError(showError(r)) + setOutput(showData(r)) + } + + const minifyJson = () => { + const r = runCodec(() => window.services.crypt.json.minify({ input })) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ + +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/jwt/index.ts b/plugins/crypt-toolkit/src/algorithms/jwt/index.ts new file mode 100644 index 000000000..6570793cc --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/jwt/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import JwtUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const jwt: AlgorithmModule = { meta, Component: JwtUI } diff --git a/plugins/crypt-toolkit/src/algorithms/jwt/meta.ts b/plugins/crypt-toolkit/src/algorithms/jwt/meta.ts new file mode 100644 index 000000000..8460f5705 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/jwt/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'jwt', + category: 'tools', + label: 'JWT 解码', + title: 'JWT 解码工具', + reversible: false, + cmds: ['jwt', 'jwt-decode'], + defaultEnabled: true, + teach: { + summary: + 'JWT(JSON Web Token)解码工具将三段式 Token(header.payload.signature)解析为可读 JSON 结构。仅做解码展示,不验证签名有效性。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/jwt/ui.tsx b/plugins/crypt-toolkit/src/algorithms/jwt/ui.tsx new file mode 100644 index 000000000..faad405b3 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/jwt/ui.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError } from '../codec' + +export default function JwtUI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.jwt.decode({ token: input })) + setError(showError(r)) + if (r.ok) { + setOutput(JSON.stringify(r.data, null, 2)) + } else { + setOutput('') + } + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/md5/index.ts b/plugins/crypt-toolkit/src/algorithms/md5/index.ts new file mode 100644 index 000000000..987e0a14d --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/md5/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Md5UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const md5: AlgorithmModule = { meta, Component: Md5UI } diff --git a/plugins/crypt-toolkit/src/algorithms/md5/meta.ts b/plugins/crypt-toolkit/src/algorithms/md5/meta.ts new file mode 100644 index 000000000..edf8db330 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/md5/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'md5', + category: 'hash', + label: 'MD5', + title: 'MD5 摘要', + reversible: false, + cmds: ['md5', 'MD5'], + defaultEnabled: true, + teach: { + summary: + 'MD5 产生 128 位(32 位十六进制)摘要,速度快但已不适合安全场景,常用于校验和与非安全场景指纹。不可逆。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/md5/ui.tsx b/plugins/crypt-toolkit/src/algorithms/md5/ui.tsx new file mode 100644 index 000000000..9232ab950 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/md5/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Md5UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.md5.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/morse/index.ts b/plugins/crypt-toolkit/src/algorithms/morse/index.ts new file mode 100644 index 000000000..7137e984f --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/morse/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import MorseUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const morse: AlgorithmModule = { meta, Component: MorseUI } diff --git a/plugins/crypt-toolkit/src/algorithms/morse/meta.ts b/plugins/crypt-toolkit/src/algorithms/morse/meta.ts new file mode 100644 index 000000000..3f1252a1e --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/morse/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'morse', + category: 'encoding', + label: '摩尔斯电码', + title: '摩尔斯电码编解码', + reversible: true, + cmds: ['morse', 'morse-code'], + defaultEnabled: true, + teach: { + summary: + '摩尔斯电码使用点(.)与划(-)表示字母、数字和标点,字符之间以分隔符区分。编码将文本转为摩尔斯码,解码将摩尔斯码还原为文本。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/morse/ui.tsx b/plugins/crypt-toolkit/src/algorithms/morse/ui.tsx new file mode 100644 index 000000000..3406ff460 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/morse/ui.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function MorseUI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [separator, setSeparator] = useState(' ') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.morse.encode({ input, separator }) + : window.services.crypt.morse.decode({ input, separator }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '原文' : '摩尔斯电码' + const inputHelp = dir === 'encode' + ? '待编码的英文字母、数字或标点' + : '待解码的摩尔斯电码(点划字符串)' + const outputLabel = dir === 'encode' ? '摩尔斯电码' : '原文' + const outputHelp = dir === 'encode' + ? '编码后的摩尔斯码字符串' + : '解码后的原始文本' + + return ( + <> +
+
输入
+
+ + +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/passwordStrength/index.ts b/plugins/crypt-toolkit/src/algorithms/passwordStrength/index.ts new file mode 100644 index 000000000..253a970c5 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/passwordStrength/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import PasswordStrengthUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const passwordStrength: AlgorithmModule = { meta, Component: PasswordStrengthUI } diff --git a/plugins/crypt-toolkit/src/algorithms/passwordStrength/meta.ts b/plugins/crypt-toolkit/src/algorithms/passwordStrength/meta.ts new file mode 100644 index 000000000..4189cfe97 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/passwordStrength/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'passwordStrength', + category: 'tools', + label: '密码强度', + title: '密码强度分析', + reversible: false, + cmds: ['password-strength', 'passwd-strength', 'pw-strength'], + defaultEnabled: true, + teach: { + summary: + '密码强度分析工具综合评估密码长度、字符种类(大写、小写、数字、符号)等因素,给出强度评分与标签(强/中/弱),辅助密码策略制定。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/passwordStrength/ui.tsx b/plugins/crypt-toolkit/src/algorithms/passwordStrength/ui.tsx new file mode 100644 index 000000000..187cbdd52 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/passwordStrength/ui.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError } from '../codec' + +export default function PasswordStrengthUI({}: AlgorithmProps) { + const [password, setPassword] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const analyze = () => { + const r = runCodec(() => window.services.crypt.passwordStrength.analyze({ password })) + setError(showError(r)) + if (r.ok) { + const { score, label, length, hasLower, hasUpper, hasDigit, hasSymbol } = r.data + const lines = [ + `强度: ${label} (${score}分)`, + `长度: ${length}`, + `包含小写字母: ${hasLower ? '是' : '否'}`, + `包含大写字母: ${hasUpper ? '是' : '否'}`, + `包含数字: ${hasDigit ? '是' : '否'}`, + `包含符号: ${hasSymbol ? '是' : '否'}` + ] + setOutput(lines.join('\n')) + } else { + setOutput('') + } + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/pbkdf2/index.ts b/plugins/crypt-toolkit/src/algorithms/pbkdf2/index.ts new file mode 100644 index 000000000..ab62ab221 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/pbkdf2/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Pbkdf2UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const pbkdf2: AlgorithmModule = { meta, Component: Pbkdf2UI } diff --git a/plugins/crypt-toolkit/src/algorithms/pbkdf2/meta.ts b/plugins/crypt-toolkit/src/algorithms/pbkdf2/meta.ts new file mode 100644 index 000000000..33c91ba3e --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/pbkdf2/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'pbkdf2', + category: 'kdf', + label: 'PBKDF2', + title: 'PBKDF2 口令派生', + reversible: false, + cmds: ['pbkdf2', '口令派生'], + defaultEnabled: true, + teach: { + summary: + 'PBKDF2 通过多轮 HMAC 迭代将口令与随机盐派生为密钥,显著提高暴力破解成本。相同口令、盐与参数得到相同密钥;输出为十六进制。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/pbkdf2/ui.tsx b/plugins/crypt-toolkit/src/algorithms/pbkdf2/ui.tsx new file mode 100644 index 000000000..aa80bdd61 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/pbkdf2/ui.tsx @@ -0,0 +1,64 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Pbkdf2UI({}: AlgorithmProps) { + const [password, setPassword] = useState('') + const [salt, setSalt] = useState('salt') + const [iterations, setIterations] = useState('100000') + const [keyLength, setKeyLength] = useState('32') + const [digest, setDigest] = useState('sha256') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + window.services.crypt.pbkdf2.derive({ + password, + salt, + iterations: Number(iterations) || 0, + keylen: Number(keyLength) || 0, + hash: digest + }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ + + + + +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/punycode/index.ts b/plugins/crypt-toolkit/src/algorithms/punycode/index.ts new file mode 100644 index 000000000..6a393c3d8 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/punycode/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import PunycodeUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const punycode: AlgorithmModule = { meta, Component: PunycodeUI } diff --git a/plugins/crypt-toolkit/src/algorithms/punycode/meta.ts b/plugins/crypt-toolkit/src/algorithms/punycode/meta.ts new file mode 100644 index 000000000..eab82db7b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/punycode/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'punycode', + category: 'encoding', + label: 'Punycode', + title: 'Punycode 域名编码', + reversible: true, + cmds: ['punycode', 'idn'], + defaultEnabled: true, + teach: { + summary: + 'Punycode 将 Unicode 域名转为 ASCII 兼容编码(ACE),实现国际化域名(IDN)。编码输出 punycode 前缀 xn-- 开头的 ASCII 域名,解码还原 Unicode 域名。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/punycode/ui.tsx b/plugins/crypt-toolkit/src/algorithms/punycode/ui.tsx new file mode 100644 index 000000000..3c6d5d84f --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/punycode/ui.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function PunycodeUI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.punycode.encode({ input }) + : window.services.crypt.punycode.decode({ input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? 'Unicode 域名' : 'Punycode 域名' + const inputHelp = dir === 'encode' + ? '待编码的国际化域名(如 中文.cn)' + : '待解码的 Punycode 域名(如 xn--fiq228c.cn)' + const outputLabel = dir === 'encode' ? 'Punycode 域名' : 'Unicode 域名' + const outputHelp = dir === 'encode' + ? '编码后的 ASCII 兼容域名' + : '解码后的 Unicode 域名' + + return ( + <> +
+
输入
+
+ +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/randomBytes/index.ts b/plugins/crypt-toolkit/src/algorithms/randomBytes/index.ts new file mode 100644 index 000000000..cede53a8d --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/randomBytes/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import RandomBytesUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const randomBytes: AlgorithmModule = { meta, Component: RandomBytesUI } diff --git a/plugins/crypt-toolkit/src/algorithms/randomBytes/meta.ts b/plugins/crypt-toolkit/src/algorithms/randomBytes/meta.ts new file mode 100644 index 000000000..9f0635092 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/randomBytes/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'randomBytes', + category: 'tools', + label: 'Random Bytes', + title: '随机字节生成器', + reversible: false, + cmds: ['random-bytes', 'random', 'randbytes'], + defaultEnabled: true, + teach: { + summary: + '使用密码学安全的伪随机数生成器(CSPRNG)产生指定长度的随机字节,支持 Hex、Base64、Base64URL 三种编码输出.适用于密钥生成、Token 创建、Nonce 值等场景.' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/randomBytes/ui.tsx b/plugins/crypt-toolkit/src/algorithms/randomBytes/ui.tsx new file mode 100644 index 000000000..e36bf4ed7 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/randomBytes/ui.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function RandomBytesUI({}: AlgorithmProps) { + const [length, setLength] = useState('32') + const [encoding, setEncoding] = useState('hex') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const generate = () => { + const r = runCodec(() => + window.services.crypt.randomBytes.generate({ + length: Number(length) || 32, + encoding + }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
配置
+
+ + +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/ripemd160/index.ts b/plugins/crypt-toolkit/src/algorithms/ripemd160/index.ts new file mode 100644 index 000000000..4488bad06 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ripemd160/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Ripemd160UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const ripemd160: AlgorithmModule = { meta, Component: Ripemd160UI } diff --git a/plugins/crypt-toolkit/src/algorithms/ripemd160/meta.ts b/plugins/crypt-toolkit/src/algorithms/ripemd160/meta.ts new file mode 100644 index 000000000..4418e34d6 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ripemd160/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'ripemd160', + category: 'hash', + label: 'RIPEMD-160', + title: 'RIPEMD-160 摘要', + reversible: false, + cmds: ['ripemd160', 'ripemd-160', 'rmd160'], + defaultEnabled: true, + teach: { + summary: + 'RIPEMD-160 产生 160 位(40 位十六进制)摘要,常用于比特币地址生成。相比 MD5/SHA-1 有更强抗碰撞性,但已不推荐在新系统中使用。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/ripemd160/ui.tsx b/plugins/crypt-toolkit/src/algorithms/ripemd160/ui.tsx new file mode 100644 index 000000000..833fb931f --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/ripemd160/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Ripemd160UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.ripemd160.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/rsa/index.ts b/plugins/crypt-toolkit/src/algorithms/rsa/index.ts new file mode 100644 index 000000000..eaf235f68 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/rsa/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import RsaUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const rsa: AlgorithmModule = { meta, Component: RsaUI } diff --git a/plugins/crypt-toolkit/src/algorithms/rsa/meta.ts b/plugins/crypt-toolkit/src/algorithms/rsa/meta.ts new file mode 100644 index 000000000..57cfa4d78 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/rsa/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'rsa', + category: 'asymmetric', + label: 'RSA', + title: 'RSA 非对称加密', + reversible: true, + cmds: ['rsa加密', 'RSA'], + defaultEnabled: true, + teach: { + summary: + 'RSA 基于大数分解难题,公钥加密、私钥解密。本工具使用 2048 位密钥与 OAEP 填充。密钥为 PEM 文本;先生成密钥对,公钥加密、私钥解密。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/rsa/ui.tsx b/plugins/crypt-toolkit/src/algorithms/rsa/ui.tsx new file mode 100644 index 000000000..311f2cf6b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/rsa/ui.tsx @@ -0,0 +1,124 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function RsaUI({ direction = 'encrypt' }: AlgorithmProps) { + const dir = direction + const [publicKey, setPublicKey] = useState('') + const [privateKey, setPrivateKey] = useState('') + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + const [notice, setNotice] = useState('') + + const keygen = () => { + const r = runCodec(() => window.services.crypt.rsa.generateKeyPair()) + if (!r.ok) { + setError(r.error) + return + } + try { + const pair = JSON.parse(r.data) as { publicKey: string; privateKey: string } + setPublicKey(pair.publicKey) + setPrivateKey(pair.privateKey) + setNotice('已生成 2048 位密钥对') + setError('') + } catch { + setError('密钥对解析失败') + } + } + + const run = () => { + const r = runCodec(() => + dir === 'encrypt' + ? window.services.crypt.rsa.encrypt({ publicKey, plaintext: input }) + : window.services.crypt.rsa.decrypt({ privateKey, ciphertext: input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const primaryKey = dir === 'encrypt' + ? { label: '公钥 Public Key', hint: 'PEM', help: '用于加密;可粘贴或点击「生成密钥对」获取', value: publicKey, setter: setPublicKey } + : { label: '私钥 Private Key', hint: 'PEM', help: '用于解密;请勿泄露', value: privateKey, setter: setPrivateKey } + const secondaryKey = dir === 'encrypt' + ? { label: '私钥 Private Key (可选)', hint: 'PEM', help: '如已有密钥对,可一并粘贴以便轮换使用', value: privateKey, setter: setPrivateKey } + : { label: '公钥 Public Key (可选)', hint: 'PEM', help: '如已有密钥对,可一并粘贴以便轮换使用', value: publicKey, setter: setPublicKey } + + const inputLabel = dir === 'encrypt' ? '明文' : '密文' + const inputHelp = dir === 'encrypt' + ? '待加密的 UTF-8 明文' + : '待解密的 Base64 密文' + const outputLabel = dir === 'encrypt' ? '密文' : '明文' + const outputHelp = dir === 'encrypt' + ? '加密后的 Base64 密文' + : '解密后的 UTF-8 明文' + + return ( + <> +
+ +
+
+
输入
+
+ + + +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {notice ?
{notice}
: null} + {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/safeBase64/index.ts b/plugins/crypt-toolkit/src/algorithms/safeBase64/index.ts new file mode 100644 index 000000000..c7636b2e5 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/safeBase64/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import SafeBase64UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const safeBase64: AlgorithmModule = { meta, Component: SafeBase64UI } diff --git a/plugins/crypt-toolkit/src/algorithms/safeBase64/meta.ts b/plugins/crypt-toolkit/src/algorithms/safeBase64/meta.ts new file mode 100644 index 000000000..c99182bfa --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/safeBase64/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'safeBase64', + category: 'encoding', + label: 'Safe Base64', + title: 'URL 安全 Base64 编码', + reversible: true, + cmds: ['safe-base64', 'urlsafe-base64', 'base64url'], + defaultEnabled: true, + teach: { + summary: + 'URL 安全 Base64 将标准 Base64 中的 + 替换为 / 替换为 _,避免在 URL/文件名中出现特殊字符。支持可选去除尾部 = 填充,适用于 JWT、URL 参数等场景。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/safeBase64/ui.tsx b/plugins/crypt-toolkit/src/algorithms/safeBase64/ui.tsx new file mode 100644 index 000000000..5a45ed28d --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/safeBase64/ui.tsx @@ -0,0 +1,81 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function SafeBase64UI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [stripPadding, setStripPadding] = useState('false') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.safeBase64.encode({ input, stripPadding: stripPadding === 'true' }) + : window.services.crypt.safeBase64.decode({ input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '明文' : 'Safe Base64' + const inputHelp = dir === 'encode' + ? '待编码的 UTF-8 文本' + : '待解码的 URL 安全 Base64 字符串' + const outputLabel = dir === 'encode' ? 'Safe Base64' : '明文' + const outputHelp = dir === 'encode' + ? '编码后的 URL 安全 Base64 字符串' + : '解码后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ {dir === 'encode' ? ( + { + const arr = new Uint8Array(16) + crypto.getRandomValues(arr) + setSalt(Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('')) + } + + const run = () => { + const r1 = runCodec(() => + window.services.crypt.scrypt.derive({ + password, + salt, + keylen: Number(keylen) || 32, + N: Number(N) || 16384, + r: Number(r) || 8, + p: Number(p) || 1 + }) + ) + setError(showError(r1)) + setOutput(showData(r1)) + } + + return ( + <> +
+
输入
+
+ + +
+
+ +
+
+ + + + +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha1/index.ts b/plugins/crypt-toolkit/src/algorithms/sha1/index.ts new file mode 100644 index 000000000..7a1a07107 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha1/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const sha1 = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/sha1/meta.ts b/plugins/crypt-toolkit/src/algorithms/sha1/meta.ts new file mode 100644 index 000000000..29555655d --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha1/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'sha1', + category: 'hash', + label: 'SHA-1', + title: 'SHA-1 摘要', + reversible: false, + cmds: ['sha1', 'sha-1'], + defaultEnabled: true, + teach: { + summary: + 'SHA-1 产生 160 位(40 位十六进制)摘要,仍广泛用于 Git 对象 ID 与旧版协议。因其抗碰撞性不足,已不建议用于安全场景,但作为校验与兼容性工具仍然实用。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha1/ui.tsx b/plugins/crypt-toolkit/src/algorithms/sha1/ui.tsx new file mode 100644 index 000000000..1db552b6a --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha1/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Sha1UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.sha1.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha256/index.ts b/plugins/crypt-toolkit/src/algorithms/sha256/index.ts new file mode 100644 index 000000000..1daf0e991 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha256/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Sha256UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const sha256: AlgorithmModule = { meta, Component: Sha256UI } diff --git a/plugins/crypt-toolkit/src/algorithms/sha256/meta.ts b/plugins/crypt-toolkit/src/algorithms/sha256/meta.ts new file mode 100644 index 000000000..33a9a8158 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha256/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'sha256', + category: 'hash', + label: 'SHA-256', + title: 'SHA-256 摘要', + reversible: false, + cmds: ['sha256', 'sha-256'], + defaultEnabled: true, + teach: { + summary: + 'SHA-256 属 SHA-2 家族,产生 256 位(64 位十六进制)摘要,广泛用于证书、完整性校验与区块链。不可逆,抗碰撞性强于 MD5。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha256/ui.tsx b/plugins/crypt-toolkit/src/algorithms/sha256/ui.tsx new file mode 100644 index 000000000..c907f9142 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha256/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Sha256UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.sha256.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha384/index.ts b/plugins/crypt-toolkit/src/algorithms/sha384/index.ts new file mode 100644 index 000000000..817a1b17b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha384/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Sha384UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const sha384: AlgorithmModule = { meta, Component: Sha384UI } diff --git a/plugins/crypt-toolkit/src/algorithms/sha384/meta.ts b/plugins/crypt-toolkit/src/algorithms/sha384/meta.ts new file mode 100644 index 000000000..a9b8bfadf --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha384/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'sha384', + category: 'hash', + label: 'SHA-384', + title: 'SHA-384 摘要', + reversible: false, + cmds: ['sha384', 'sha-384'], + defaultEnabled: true, + teach: { + summary: + 'SHA-384 属 SHA-2 家族,产生 384 位(96 位十六进制)摘要。相比 SHA-512 做了截断,安全性高于 SHA-256,常用于数字证书与完整性校验。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha384/ui.tsx b/plugins/crypt-toolkit/src/algorithms/sha384/ui.tsx new file mode 100644 index 000000000..5660fbad3 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha384/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Sha384UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.sha384.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha3_256/index.ts b/plugins/crypt-toolkit/src/algorithms/sha3_256/index.ts new file mode 100644 index 000000000..6d42bfe6f --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha3_256/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import Sha3_256UI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const sha3_256: AlgorithmModule = { meta, Component: Sha3_256UI } diff --git a/plugins/crypt-toolkit/src/algorithms/sha3_256/meta.ts b/plugins/crypt-toolkit/src/algorithms/sha3_256/meta.ts new file mode 100644 index 000000000..d69623fc0 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha3_256/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'sha3_256', + category: 'hash', + label: 'SHA3-256', + title: 'SHA3-256 摘要', + reversible: false, + cmds: ['sha3', 'sha3-256', 'sha3_256'], + defaultEnabled: true, + teach: { + summary: + 'SHA3-256 属 SHA-3 家族(Keccak),产生 256 位(64 位十六进制)摘要。基于海绵构造而非 Merkle-Damgård,结构上与 SHA-2 完全不同,是新标准的哈希算法。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha3_256/ui.tsx b/plugins/crypt-toolkit/src/algorithms/sha3_256/ui.tsx new file mode 100644 index 000000000..bc5397bbf --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha3_256/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Sha3_256UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.sha3_256.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha3_512/index.ts b/plugins/crypt-toolkit/src/algorithms/sha3_512/index.ts new file mode 100644 index 000000000..0feda7be6 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha3_512/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const sha3_512 = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/sha3_512/meta.ts b/plugins/crypt-toolkit/src/algorithms/sha3_512/meta.ts new file mode 100644 index 000000000..313b19689 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha3_512/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'sha3_512', + category: 'hash', + label: 'SHA3-512', + title: 'SHA3-512 摘要', + reversible: false, + cmds: ['sha3-512', 'sha3_512'], + defaultEnabled: true, + teach: { + summary: + 'SHA3-512 属 SHA-3 家族(Keccak),产生 512 位(128 位十六进制)摘要。SHA-3 采用海绵结构,与 SHA-2 设计完全不同,作为后量子时代的备选哈希标准。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha3_512/ui.tsx b/plugins/crypt-toolkit/src/algorithms/sha3_512/ui.tsx new file mode 100644 index 000000000..04c4daa84 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha3_512/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { showError, showData, runCodec } from '../codec' + +export default function Sha3_512UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.sha3_512.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha512/index.ts b/plugins/crypt-toolkit/src/algorithms/sha512/index.ts new file mode 100644 index 000000000..9e3de432c --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha512/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const sha512 = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/sha512/meta.ts b/plugins/crypt-toolkit/src/algorithms/sha512/meta.ts new file mode 100644 index 000000000..566779991 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha512/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'sha512', + category: 'hash', + label: 'SHA-512', + title: 'SHA-512 摘要', + reversible: false, + cmds: ['sha512', 'sha-512'], + defaultEnabled: true, + teach: { + summary: + 'SHA-512 属 SHA-2 家族,产生 512 位(128 位十六进制)摘要,安全强度高于 SHA-256,适合用于完整性校验、证书签名与高强度散列场景。64 位平台运算效率通常优于 SHA-256。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/sha512/ui.tsx b/plugins/crypt-toolkit/src/algorithms/sha512/ui.tsx new file mode 100644 index 000000000..0730790ba --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/sha512/ui.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function Sha512UI({}: AlgorithmProps) { + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => window.services.crypt.sha512.digest(input)) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} readOnly variant="output" copyable /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/unicodeEscape/index.ts b/plugins/crypt-toolkit/src/algorithms/unicodeEscape/index.ts new file mode 100644 index 000000000..e13d3e082 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/unicodeEscape/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import UnicodeEscapeUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const unicodeEscape: AlgorithmModule = { meta, Component: UnicodeEscapeUI } diff --git a/plugins/crypt-toolkit/src/algorithms/unicodeEscape/meta.ts b/plugins/crypt-toolkit/src/algorithms/unicodeEscape/meta.ts new file mode 100644 index 000000000..63c790f00 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/unicodeEscape/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'unicodeEscape', + category: 'encoding', + label: 'Unicode 转义', + title: 'Unicode 转义编码', + reversible: true, + cmds: ['unicode-escape', 'unicode'], + defaultEnabled: true, + teach: { + summary: + 'Unicode 转义将非 ASCII 字符编码为 \\uXXXX 形式(如 \\u4e2d),常用于 JSON、JS/源码字符串中。编码后可逆,解码还原原始字符。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/unicodeEscape/ui.tsx b/plugins/crypt-toolkit/src/algorithms/unicodeEscape/ui.tsx new file mode 100644 index 000000000..70105da0f --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/unicodeEscape/ui.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function UnicodeEscapeUI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.unicodeEscape.encode({ input }) + : window.services.crypt.unicodeEscape.decode({ input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '原文' : 'Unicode 转义' + const inputHelp = dir === 'encode' + ? '待编码的 UTF-8 文本' + : '待解码的 \\uXXXX 转义字符串' + const outputLabel = dir === 'encode' ? 'Unicode 转义' : '原文' + const outputHelp = dir === 'encode' + ? '编码后的 \\uXXXX 转义字符串' + : '解码后的原始文本' + + return ( + <> +
+
输入
+
+ +
+
+
+ + +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/url/index.ts b/plugins/crypt-toolkit/src/algorithms/url/index.ts new file mode 100644 index 000000000..1d9e4746b --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/url/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import UrlUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const url: AlgorithmModule = { meta, Component: UrlUI } diff --git a/plugins/crypt-toolkit/src/algorithms/url/meta.ts b/plugins/crypt-toolkit/src/algorithms/url/meta.ts new file mode 100644 index 000000000..b65635d9a --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/url/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'url', + category: 'encoding', + label: 'URL', + title: 'URL 编码', + reversible: true, + cmds: ['url编码', 'urlencode'], + defaultEnabled: true, + teach: { + summary: + 'URL 编码(百分号编码)将保留字符与非 ASCII 转为 %XX 序列,用于查询参数与路径安全传输。可逆解码还原原文。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/url/ui.tsx b/plugins/crypt-toolkit/src/algorithms/url/ui.tsx new file mode 100644 index 000000000..bbbc05c74 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/url/ui.tsx @@ -0,0 +1,67 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function UrlUI({ direction = 'encode' }: AlgorithmProps) { + const dir = direction + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encode' + ? window.services.crypt.url.encode(input) + : window.services.crypt.url.decode(input) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputLabel = dir === 'encode' ? '原文' : 'URL 编码' + const inputHelp = dir === 'encode' + ? '待编码的原文(含中文或特殊字符)' + : '待解码的 %XX 编码字符串' + const outputLabel = dir === 'encode' ? 'URL 编码' : '原文' + const outputHelp = dir === 'encode' + ? '编码后的 %XX 字符串' + : '解码后的原始文本' + + return ( + <> +
+
输入
+
+ +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/uuid/index.ts b/plugins/crypt-toolkit/src/algorithms/uuid/index.ts new file mode 100644 index 000000000..3e58c22b6 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/uuid/index.ts @@ -0,0 +1,3 @@ +import { meta } from './meta' +import UI from './ui' +export const uuid = { meta, Component: UI } diff --git a/plugins/crypt-toolkit/src/algorithms/uuid/meta.ts b/plugins/crypt-toolkit/src/algorithms/uuid/meta.ts new file mode 100644 index 000000000..a3ec1ca7c --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/uuid/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'uuid', + category: 'tools', + label: 'UUID', + title: 'UUID 生成器', + reversible: false, + cmds: ['uuid', 'uuidgen'], + defaultEnabled: true, + teach: { + summary: + 'UUID(通用唯一识别码)是 128 位标识符,用于分布式系统中无中心化生成唯一 ID。v4 为完全随机,v7 基于时间戳(UUIDv7,RFC 9562 草案)支持时间排序,适合数据库主键场景。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/uuid/ui.tsx b/plugins/crypt-toolkit/src/algorithms/uuid/ui.tsx new file mode 100644 index 000000000..1977d43e8 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/uuid/ui.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function UUIDUI({}: AlgorithmProps) { + const [version, setVersion] = useState('v4') + const [count, setCount] = useState('1') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const generate = () => { + const r = runCodec(() => + window.services.crypt.uuid.generate({ + version, + count: Number(count) || 1 + }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + return ( + <> +
+
配置
+
+ + +
+
+
+ +
+
+
输出
+ {}} + readOnly + variant="output" + copyable + /> +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/algorithms/xorStream/index.ts b/plugins/crypt-toolkit/src/algorithms/xorStream/index.ts new file mode 100644 index 000000000..22daa5aa9 --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/xorStream/index.ts @@ -0,0 +1,5 @@ +import { meta } from './meta' +import XorStreamUI from './ui' +import type { AlgorithmModule } from '../../registry/types' + +export const xorStream: AlgorithmModule = { meta, Component: XorStreamUI } diff --git a/plugins/crypt-toolkit/src/algorithms/xorStream/meta.ts b/plugins/crypt-toolkit/src/algorithms/xorStream/meta.ts new file mode 100644 index 000000000..e6278a61c --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/xorStream/meta.ts @@ -0,0 +1,15 @@ +import type { AlgorithmMeta } from '../../registry/types' + +export const meta: AlgorithmMeta = { + id: 'xorStream', + category: 'symmetric', + label: 'XOR 流密码', + title: 'XOR 流密码', + reversible: true, + cmds: ['xor', 'xor-stream', 'xor-cipher'], + defaultEnabled: true, + teach: { + summary: + 'XOR 流密码将明文与密钥进行逐字节异或运算,输出为十六进制密文。加密与解密均为相同操作,需相同密钥。简单但仅用于教学演示,不适用于生产安全场景。' + } +} diff --git a/plugins/crypt-toolkit/src/algorithms/xorStream/ui.tsx b/plugins/crypt-toolkit/src/algorithms/xorStream/ui.tsx new file mode 100644 index 000000000..bad699fab --- /dev/null +++ b/plugins/crypt-toolkit/src/algorithms/xorStream/ui.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react' +import type { AlgorithmProps } from '../../registry/types' +import { Field } from '../../shared' +import { runCodec, showError, showData } from '../codec' + +export default function XorStreamUI({ direction = 'encrypt' }: AlgorithmProps) { + const dir = direction + const [key, setKey] = useState('') + const [input, setInput] = useState('') + const [output, setOutput] = useState('') + const [error, setError] = useState('') + + const run = () => { + const r = runCodec(() => + dir === 'encrypt' + ? window.services.crypt.xorStream.encrypt({ key, plaintext: input }) + : window.services.crypt.xorStream.decrypt({ key, ciphertext: input }) + ) + setError(showError(r)) + setOutput(showData(r)) + } + + const inputHelp = dir === 'encrypt' + ? '待加密的 UTF-8 明文' + : '待解密的十六进制密文' + const outputHelp = dir === 'encrypt' + ? '加密后的十六进制密文' + : '解密后的 UTF-8 明文' + + return ( + <> +
+
输入
+
+ + {dir === 'encrypt' ? ( + + ) : ( + + )} +
+
+
+ + +
+
+
输出
+ {dir === 'encrypt' ? ( + {}} + readOnly + variant="output" + copyable + /> + ) : ( + {}} + readOnly + variant="output" + copyable + /> + )} +
+ {error ?
{error}
: null} + + ) +} diff --git a/plugins/crypt-toolkit/src/config/features.test.ts b/plugins/crypt-toolkit/src/config/features.test.ts new file mode 100644 index 000000000..f7e2cfdc6 --- /dev/null +++ b/plugins/crypt-toolkit/src/config/features.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from 'vitest' +import type { AlgorithmModule, AlgorithmMeta } from '../registry/types' +import { buildFeature, syncFeatures } from './features' + +const meta: AlgorithmMeta = { + id: 'aes', + category: 'symmetric', + label: 'AES', + title: 'AES 对称加密', + reversible: true, + cmds: ['aes加密', 'AES'], + defaultEnabled: true, + teach: { summary: 's' } +} + +const mods = [{ meta, Component: () => null }] as AlgorithmModule[] + +describe('features', () => { + it('buildFeature shape', () => { + expect(buildFeature(meta)).toEqual({ + code: 'alg:aes', + explain: 'AES 对称加密', + icon: 'logo.svg', + cmds: ['aes加密', 'AES'] + }) + }) + + it('syncFeatures set/remove targets', () => { + const setFeature = vi.fn() + const removeFeature = vi.fn() + vi.stubGlobal('window', { ztools: { setFeature, removeFeature } }) + + syncFeatures(mods, { aes: true }) + expect(setFeature).toHaveBeenCalledWith(buildFeature(meta)) + expect(removeFeature).not.toHaveBeenCalled() + + setFeature.mockClear() + syncFeatures(mods, { aes: false }) + expect(removeFeature).toHaveBeenCalledWith('alg:aes') + expect(setFeature).not.toHaveBeenCalled() + vi.unstubAllGlobals() + }) + + it('throws are caught by syncFeatures', () => { + const setFeature = vi.fn(() => { + throw new Error('host error') + }) + const removeFeature = vi.fn(() => { + throw new Error('host error') + }) + vi.stubGlobal('window', { ztools: { setFeature, removeFeature } }) + expect(() => syncFeatures(mods, { aes: true })).not.toThrow() + vi.unstubAllGlobals() + }) +}) diff --git a/plugins/crypt-toolkit/src/config/features.ts b/plugins/crypt-toolkit/src/config/features.ts new file mode 100644 index 000000000..19100134c --- /dev/null +++ b/plugins/crypt-toolkit/src/config/features.ts @@ -0,0 +1,34 @@ +import type { AlgorithmMeta, AlgorithmModule } from '../registry/types' + +export interface FeatureInput { + code: string + explain: string + icon: string + cmds: string[] +} + +export function buildFeature(meta: AlgorithmMeta): FeatureInput { + return { + code: `alg:${meta.id}`, + explain: meta.title, + icon: 'logo.svg', + cmds: [...meta.cmds] + } +} + +export function syncFeatures( + modules: AlgorithmModule[], + enabled: Record +): void { + for (const m of modules) { + try { + if (enabled[m.meta.id]) { + window.ztools.setFeature(buildFeature(m.meta)) + } else { + window.ztools.removeFeature(`alg:${m.meta.id}`) + } + } catch { + // host feature API unavailable — non-fatal + } + } +} diff --git a/plugins/crypt-toolkit/src/config/index.ts b/plugins/crypt-toolkit/src/config/index.ts new file mode 100644 index 000000000..295fe4443 --- /dev/null +++ b/plugins/crypt-toolkit/src/config/index.ts @@ -0,0 +1,2 @@ +export * from './settings' +export * from './features' diff --git a/plugins/crypt-toolkit/src/config/settings.test.ts b/plugins/crypt-toolkit/src/config/settings.test.ts new file mode 100644 index 000000000..c70b28332 --- /dev/null +++ b/plugins/crypt-toolkit/src/config/settings.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { AlgorithmModule } from '../registry/types' +import { defaultSettings, loadSettings, saveSettings, setEnabled, SETTINGS_KEY } from './settings' + +const mods = [ + { + meta: { + id: 'aes', + category: 'symmetric', + label: 'AES', + title: 'AES 对称加密', + reversible: true, + cmds: ['aes加密'], + defaultEnabled: true, + teach: { summary: 's' } + }, + Component: () => null + }, + { + meta: { + id: 'rsa', + category: 'asymmetric', + label: 'RSA', + title: 'RSA 非对称加密', + reversible: true, + cmds: ['rsa加密'], + defaultEnabled: false, + teach: { summary: 's' } + }, + Component: () => null + } +] as AlgorithmModule[] + +describe('settings', () => { + beforeEach(() => { + const store = new Map() + vi.stubGlobal('window', { + ztools: { + dbStorage: { + getItem: (k: string) => (store.has(k) ? JSON.parse(store.get(k)!) : null), + setItem: (k: string, v: unknown) => { + store.set(k, JSON.stringify(v)) + }, + removeItem: (k: string) => { + store.delete(k) + } + } + } + }) + }) + + it('defaults from module defaultEnabled', () => { + const s = defaultSettings(mods) + expect(s.enabled).toEqual({ aes: true, rsa: false }) + }) + + it('loads saved settings and ignores unknown ids', () => { + saveSettings({ + enabled: { aes: false, unknownId: true }, + prefs: { defaultOutputEncoding: 'base64' } + }) + const s = loadSettings(mods) + expect(s.enabled.aes).toBe(false) + expect(s.enabled.unknownId).toBeUndefined() + expect(s.enabled.rsa).toBe(false) + }) + + it('setEnabled persists', () => { + const s = setEnabled('aes', false, mods) + expect(s.enabled.aes).toBe(false) + expect(loadSettings(mods).enabled.aes).toBe(false) + }) + + it('uses SETTINGS_KEY', () => { + expect(SETTINGS_KEY).toBe('crypt-tool:settings') + }) +}) diff --git a/plugins/crypt-toolkit/src/config/settings.ts b/plugins/crypt-toolkit/src/config/settings.ts new file mode 100644 index 000000000..a28c90cb6 --- /dev/null +++ b/plugins/crypt-toolkit/src/config/settings.ts @@ -0,0 +1,55 @@ +import type { AlgorithmModule } from '../registry/types' + +export const SETTINGS_KEY = 'crypt-tool:settings' + +export interface Settings { + enabled: Record + prefs: { defaultOutputEncoding: 'base64' | 'hex' | 'text' } +} + +export function defaultSettings(modules: AlgorithmModule[]): Settings { + const enabled: Record = {} + for (const m of modules) enabled[m.meta.id] = m.meta.defaultEnabled + return { enabled, prefs: { defaultOutputEncoding: 'base64' } } +} + +export function loadSettings(modules: AlgorithmModule[]): Settings { + const defaults = defaultSettings(modules) + try { + const raw = window.ztools.dbStorage.getItem(SETTINGS_KEY) + if (!raw || typeof raw !== 'object') return defaults + const saved = raw as Partial + const enabled: Record = { ...defaults.enabled } + if (saved.enabled && typeof saved.enabled === 'object') { + for (const m of modules) { + const v = (saved.enabled as Record)[m.meta.id] + if (typeof v === 'boolean') enabled[m.meta.id] = v + } + } + const prefs = { + defaultOutputEncoding: + saved.prefs?.defaultOutputEncoding === 'hex' || + saved.prefs?.defaultOutputEncoding === 'text' + ? saved.prefs.defaultOutputEncoding + : defaults.prefs.defaultOutputEncoding + } + return { enabled, prefs } + } catch { + return defaults + } +} + +export function saveSettings(s: Settings): void { + window.ztools.dbStorage.setItem(SETTINGS_KEY, s) +} + +export function setEnabled( + id: string, + on: boolean, + modules: AlgorithmModule[] +): Settings { + const s = loadSettings(modules) + s.enabled[id] = on + saveSettings(s) + return s +} diff --git a/plugins/crypt-toolkit/src/env.d.ts b/plugins/crypt-toolkit/src/env.d.ts new file mode 100644 index 000000000..7cbd4eeae --- /dev/null +++ b/plugins/crypt-toolkit/src/env.d.ts @@ -0,0 +1,149 @@ +/// +/// + +import type { CryptResult } from './types/crypt' + +declare global { + interface Window { + services: { + crypt: { + base64: { encode: (s: string) => CryptResult; decode: (s: string) => CryptResult } + base58: { encode: (a: { input: string }) => CryptResult; decode: (a: { input: string }) => CryptResult } + base58check: { encode: (a: { input: string }) => CryptResult; decode: (a: { input: string }) => CryptResult<{ + valid: boolean + data: string + }> } + hex: { encode: (s: string) => CryptResult; decode: (s: string) => CryptResult } + unicodeEscape: { encode: (a: { input: string }) => CryptResult; decode: (a: { input: string }) => CryptResult } + htmlEntity: { encode: (a: { input: string; useNamed?: boolean }) => CryptResult; decode: (a: { input: string }) => CryptResult } + punycode: { encode: (a: { input: string }) => CryptResult; decode: (a: { input: string }) => CryptResult } + morse: { encode: (a: { input: string; separator?: string }) => CryptResult; decode: (a: { input: string; separator?: string }) => CryptResult } + url: { encode: (s: string) => CryptResult; decode: (s: string) => CryptResult } + md5: { digest: (s: string) => CryptResult } + sha1: { digest: (s: string) => CryptResult } + sha256: { digest: (s: string) => CryptResult } + sha512: { digest: (s: string) => CryptResult } + sha384: { digest: (s: string) => CryptResult } + sha3_256: { digest: (s: string) => CryptResult } + sha3_512: { digest: (s: string) => CryptResult } + ripemd160: { digest: (s: string) => CryptResult } + safeBase64: { encode: (a: { input: string; stripPadding?: boolean }) => CryptResult; decode: (a: { input: string }) => CryptResult } + base32: { encode: (a: { input: string; padding?: boolean }) => CryptResult; decode: (a: { input: string }) => CryptResult } + aes: { + encrypt: (a: { key: string; iv: string; mode: string; plaintext: string }) => CryptResult + decrypt: (a: { key: string; iv: string; mode: string; ciphertext: string }) => CryptResult + } + chacha: { + encrypt: (a: { key: string; nonce: string; plaintext: string }) => CryptResult + decrypt: (a: { key: string; nonce: string; ciphertext: string }) => CryptResult + keygen: () => CryptResult + noncegen: () => CryptResult + } + xorStream: { + encrypt: (a: { key: string; plaintext: string }) => CryptResult + decrypt: (a: { key: string; ciphertext: string }) => CryptResult + } + des: { + encrypt: (a: { key: string; iv: string; plaintext: string }) => CryptResult + decrypt: (a: { key: string; iv: string; ciphertext: string }) => CryptResult + keygen: () => CryptResult + ivgen: () => CryptResult + } + rsa: { + encrypt: (a: { publicKey: string; plaintext: string }) => CryptResult + decrypt: (a: { privateKey: string; ciphertext: string }) => CryptResult + generateKeyPair: () => CryptResult + } + ecdsa: { + generateKeyPair: (a: { curve?: string }) => CryptResult<{ publicKey: string; privateKey: string; curve: string }> + sign: (a: { privateKey: string; data: string; hash?: string }) => CryptResult + verify: (a: { publicKey: string; data: string; signature: string; hash?: string }) => CryptResult + } + ed25519: { + generateKeyPair: () => CryptResult<{ publicKey: string; privateKey: string }> + sign: (a: { privateKey: string; data: string }) => CryptResult + verify: (a: { publicKey: string; data: string; signature: string }) => CryptResult + } + ecdh: { + generateKeyPair: (a: { curve?: string }) => CryptResult<{ publicKey: string; privateKey: string; curve: string }> + deriveSharedSecret: (a: { privateKey: string; peerPublicKey: string; curve?: string }) => CryptResult + } + hmac: { + sign: (a: { key: string; data: string; algo: string }) => CryptResult + verify: (a: { key: string; data: string; algo: string; signature: string }) => CryptResult + } + pbkdf2: { + derive: (a: { + password: string + salt: string + iterations: number + keylen: number + hash: string + }) => CryptResult + } + bcrypt: { + hash: (a: { password: string; cost?: number }) => CryptResult + verify: (a: { password: string; hash: string }) => CryptResult + } + scrypt: { + derive: (a: { + password: string + salt: string + keylen?: number + N?: number + r?: number + p?: number + }) => CryptResult + hash: (a: { password: string; salt: string; cost?: number }) => CryptResult + } + argon2: { + hash: (a: { + password: string + type?: string + timeCost?: number + memoryCost?: number + parallelism?: number + salt?: string + hashLength?: number + }) => CryptResult + verify: (a: { password: string; hash: string }) => CryptResult + } + hkdf: { + derive: (a: { + ikm: string + salt?: string + info?: string + keylen?: number + hash?: string + }) => CryptResult + } + uuid: { + v4: () => CryptResult + v7: () => CryptResult + generate: (a: { version?: string; count?: number }) => CryptResult + } + crc32: { + checksum: (a: { input: string }) => CryptResult + } + adler32: { + checksum: (a: { input: string }) => CryptResult + } + randomBytes: { + generate: (a: { length?: number; encoding?: string }) => CryptResult + } + jwt: { + decode: (a: { token: string }) => CryptResult<{ header: any; payload: any }> + } + json: { + format: (a: { input: string; indent?: number }) => CryptResult + minify: (a: { input: string }) => CryptResult + } + passwordStrength: { + analyze: (a: { password: string }) => CryptResult<{ score: number; label: string; length: number; hasLower: boolean; hasUpper: boolean; hasDigit: boolean; hasSymbol: boolean }> + } + } + } + } +} + +export {} diff --git a/plugins/crypt-toolkit/src/hooks/useZtoolsTheme.ts b/plugins/crypt-toolkit/src/hooks/useZtoolsTheme.ts new file mode 100644 index 000000000..ef63936e4 --- /dev/null +++ b/plugins/crypt-toolkit/src/hooks/useZtoolsTheme.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' + +// 默认主题色(与当前 --blue 一致) +const DEFAULT_PRIMARY = 'rgb(88, 164, 246)' + +// 尝试从 ztools 注入的 CSS 变量中读取主题色 +function readThemeColorFromCSS(): string | null { + if (typeof window === 'undefined') return null + try { + const style = getComputedStyle(document.documentElement) + const candidates = [ + '--ztools-primary', + '--ztools-color-primary', + '--ztools-theme-color', + '--theme-primary', + '--accent-color', + '--primary-color', + ] + for (const name of candidates) { + const val = style.getPropertyValue(name).trim() + if (val) return val + } + } catch { + // ignore + } + return null +} + +export interface ZtoolsTheme { + /** 主色调 */ + primaryColor: string + /** 是否为深色模式 */ + isDark: boolean +} + +export function useZtoolsTheme(): ZtoolsTheme { + const [theme, setTheme] = useState(() => { + const isDark = (typeof window !== 'undefined' && window.ztools?.isDarkColors?.()) ?? false + const cssColor = readThemeColorFromCSS() + return { + primaryColor: cssColor && cssColor.length > 0 ? cssColor : DEFAULT_PRIMARY, + isDark, + } + }) + + useEffect(() => { + const cssColor = readThemeColorFromCSS() + if (cssColor && cssColor.length > 0) { + document.documentElement.style.setProperty('--blue', cssColor) + setTheme((prev) => ({ ...prev, primaryColor: cssColor })) + } + + // 监听系统主题变化 + const mq = window.matchMedia('(prefers-color-scheme: dark)') + const handler = () => { + const isDark = window.ztools?.isDarkColors?.() ?? mq.matches + setTheme((prev) => ({ ...prev, isDark })) + } + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + }, []) + + return theme +} diff --git a/plugins/crypt-toolkit/src/main.css b/plugins/crypt-toolkit/src/main.css new file mode 100644 index 000000000..97bc7970a --- /dev/null +++ b/plugins/crypt-toolkit/src/main.css @@ -0,0 +1,106 @@ +:root { + --blue: rgb(88, 164, 246); + --blue-dark: color-mix(in srgb, var(--blue) 78%, #0a0a0a); + --light: #fff; + --scrollbar-width: 6px; + --scrollbar-thumb: rgba(127, 127, 127, 0.35); + --scrollbar-thumb-hover: color-mix(in srgb, var(--blue) 65%, transparent); +} + +html, +body { + margin: 0; + padding: 0; +} + +/* ===== 自定义滚动条 ===== */ +::-webkit-scrollbar { + width: var(--scrollbar-width); + height: var(--scrollbar-width); +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb); + border-radius: 999px; + transition: background 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover); +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) transparent; +} + +button { + border: none; + background: none var(--blue); + color: var(--light); + line-height: 2.5; + cursor: pointer; + transition: opacity 0.2s; +} + +button:disabled { + filter: grayscale(1); + cursor: not-allowed; +} + +button:not(:disabled):active { + opacity: 0.6; +} + +textarea { + display: block; + margin: 0; +} + +@media (prefers-color-scheme: light) { + :root { + --scrollbar-thumb: rgba(0, 0, 0, 0.18); + --scrollbar-thumb-hover: color-mix(in srgb, var(--blue) 70%, transparent); + } + ::-webkit-scrollbar-track-piece { + background-color: transparent; + } + ::-webkit-scrollbar-thumb { + border-color: transparent; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --scrollbar-thumb: rgba(255, 255, 255, 0.18); + --scrollbar-thumb-hover: color-mix(in srgb, var(--blue) 70%, transparent); + } + ::-webkit-scrollbar-track-piece { + background-color: transparent; + } + ::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + border-color: transparent; + } + body { + color: #fff; + } +} + +.ct-app { + box-sizing: border-box; + height: 100vh; + width: 100%; + display: flex; + background: transparent; + overflow: hidden; +} diff --git a/plugins/crypt-toolkit/src/main.tsx b/plugins/crypt-toolkit/src/main.tsx new file mode 100644 index 000000000..73fb20139 --- /dev/null +++ b/plugins/crypt-toolkit/src/main.tsx @@ -0,0 +1,6 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import './main.css' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root')!).render() diff --git a/plugins/crypt-toolkit/src/registry/algorithms.test.ts b/plugins/crypt-toolkit/src/registry/algorithms.test.ts new file mode 100644 index 000000000..61a7210ab --- /dev/null +++ b/plugins/crypt-toolkit/src/registry/algorithms.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest' +import type { AlgorithmModule } from './types' +import { algorithms, firstEnabledId, getByCategory, getById, getEnabledList } from './algorithms' + +const m = (id: string, category: AlgorithmModule['meta']['category'], on: boolean): AlgorithmModule => ({ + meta: { + id, + category, + label: id.toUpperCase(), + title: id, + reversible: false, + cmds: [id], + defaultEnabled: on, + teach: { summary: '' } + }, + Component: () => null +}) + +const list = [m('base64', 'encoding', true), m('aes', 'symmetric', true), m('rsa', 'asymmetric', false)] + +describe('registry helpers', () => { + it('filters enabled', () => { + expect(getEnabledList(list, { base64: true, aes: false, rsa: true }).map((x) => x.meta.id)).toEqual([ + 'base64', + 'rsa' + ]) + }) + + it('firstEnabledId', () => { + expect(firstEnabledId(list, { base64: false, aes: true, rsa: true })).toBe('aes') + expect(firstEnabledId(list, { base64: false, aes: false, rsa: false })).toBeNull() + }) + + it('getById / getByCategory', () => { + expect(getById(list, 'aes')?.meta.id).toBe('aes') + expect(getByCategory(list, 'encoding').map((x) => x.meta.id)).toEqual(['base64']) + }) + + it('ships all algorithms in order', () => { + expect(algorithms.map((m) => m.meta.id)).toEqual([ + 'base64', 'base58', 'unicodeEscape', 'htmlEntity', 'punycode', 'morse', 'hex', 'url', 'md5', 'sha1', 'sha256', 'sha512', 'sha384', 'sha3_256', 'sha3_512', 'ripemd160', + 'aes', 'xorStream', 'chacha', 'des', + 'rsa', 'ecdsa', 'ecdh', 'ed25519', + 'hmac', 'pbkdf2', 'bcrypt', 'scrypt', 'argon2', 'hkdf', + 'jwt', 'json', 'passwordStrength', + 'uuid', 'crc32', 'adler32', 'randomBytes', 'safeBase64', 'base32' + ]) + // argon2 defaults disabled (needs native module) + expect(algorithms.filter((m) => !m.meta.defaultEnabled).map((m) => m.meta.id)).toEqual(['argon2']) + }) +}) diff --git a/plugins/crypt-toolkit/src/registry/algorithms.ts b/plugins/crypt-toolkit/src/registry/algorithms.ts new file mode 100644 index 000000000..20d1d5665 --- /dev/null +++ b/plugins/crypt-toolkit/src/registry/algorithms.ts @@ -0,0 +1,105 @@ +import type { AlgorithmModule, CategoryId } from './types' +import { base64 } from '../algorithms/base64' +import { hex } from '../algorithms/hex' +import { url } from '../algorithms/url' +import { md5 } from '../algorithms/md5' +import { sha1 } from '../algorithms/sha1' +import { sha256 } from '../algorithms/sha256' +import { sha512 } from '../algorithms/sha512' +import { base58 } from '../algorithms/base58' +import { aes } from '../algorithms/aes' +import { chacha } from '../algorithms/chacha' +import { rsa } from '../algorithms/rsa' +import { ed25519 } from '../algorithms/ed25519' +import { hmac } from '../algorithms/hmac' +import { pbkdf2 } from '../algorithms/pbkdf2' +import { bcrypt } from '../algorithms/bcrypt' +import { scrypt } from '../algorithms/scrypt' +import { argon2 } from '../algorithms/argon2' +import { hkdf } from '../algorithms/hkdf' +import { uuid } from '../algorithms/uuid' +import { crc32 } from '../algorithms/crc32' +import { sha384 } from '../algorithms/sha384' +import { sha3_256 } from '../algorithms/sha3_256' +import { sha3_512 } from '../algorithms/sha3_512' +import { ripemd160 } from '../algorithms/ripemd160' +import { safeBase64 } from '../algorithms/safeBase64' +import { base32 } from '../algorithms/base32' +import { ecdsa } from '../algorithms/ecdsa' +import { adler32 } from '../algorithms/adler32' +import { randomBytes } from '../algorithms/randomBytes' +import { des } from '../algorithms/des' +import { unicodeEscape } from '../algorithms/unicodeEscape' +import { htmlEntity } from '../algorithms/htmlEntity' +import { punycode } from '../algorithms/punycode' +import { morse } from '../algorithms/morse' +import { xorStream } from '../algorithms/xorStream' +import { ecdh } from '../algorithms/ecdh' +import { jwt } from '../algorithms/jwt' +import { json } from '../algorithms/json' +import { passwordStrength } from '../algorithms/passwordStrength' + +export const algorithms: AlgorithmModule[] = [ + base64, + base58, + unicodeEscape, + htmlEntity, + punycode, + morse, + hex, + url, + md5, + sha1, + sha256, + sha512, + sha384, + sha3_256, + sha3_512, + ripemd160, + aes, + xorStream, + chacha, + des, + rsa, + ecdsa, + ecdh, + ed25519, + hmac, + pbkdf2, + bcrypt, + scrypt, + argon2, + hkdf, + jwt, + json, + passwordStrength, + uuid, + crc32, + adler32, + randomBytes, + safeBase64, + base32 +] + +export function getById(list: AlgorithmModule[], id: string): AlgorithmModule | undefined { + return list.find((m) => m.meta.id === id) +} + +export function getByCategory(list: AlgorithmModule[], category: CategoryId): AlgorithmModule[] { + return list.filter((m) => m.meta.category === category) +} + +export function getEnabledList( + list: AlgorithmModule[], + enabled: Record +): AlgorithmModule[] { + return list.filter((m) => enabled[m.meta.id]) +} + +export function firstEnabledId( + list: AlgorithmModule[], + enabled: Record +): string | null { + const first = getEnabledList(list, enabled)[0] + return first ? first.meta.id : null +} diff --git a/plugins/crypt-toolkit/src/registry/categories.test.ts b/plugins/crypt-toolkit/src/registry/categories.test.ts new file mode 100644 index 000000000..d8a1fddef --- /dev/null +++ b/plugins/crypt-toolkit/src/registry/categories.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { CATEGORIES, categoryLabel } from './categories' + +describe('categories', () => { + it('has seven categories in spec order', () => { + expect(CATEGORIES.map((c) => c.id)).toEqual([ + 'encoding', + 'hash', + 'symmetric', + 'asymmetric', + 'hmac', + 'kdf', + 'tools' + ]) + }) + + it('maps Chinese labels', () => { + expect(categoryLabel('encoding')).toBe('编码转换') + expect(categoryLabel('kdf')).toBe('口令派生') + expect(categoryLabel('tools')).toBe('工具与实用') + }) +}) diff --git a/plugins/crypt-toolkit/src/registry/categories.ts b/plugins/crypt-toolkit/src/registry/categories.ts new file mode 100644 index 000000000..fc7e8a1cf --- /dev/null +++ b/plugins/crypt-toolkit/src/registry/categories.ts @@ -0,0 +1,20 @@ +import type { CategoryId } from './types' + +export interface CategoryDef { + id: CategoryId + label: string +} + +export const CATEGORIES: CategoryDef[] = [ + { id: 'encoding', label: '编码转换' }, + { id: 'hash', label: '哈希摘要' }, + { id: 'symmetric', label: '对称加密' }, + { id: 'asymmetric', label: '非对称加密' }, + { id: 'hmac', label: '消息认证' }, + { id: 'kdf', label: '口令派生' }, + { id: 'tools', label: '工具与实用' } +] + +export function categoryLabel(id: CategoryId): string { + return CATEGORIES.find((c) => c.id === id)?.label ?? id +} diff --git a/plugins/crypt-toolkit/src/registry/index.ts b/plugins/crypt-toolkit/src/registry/index.ts new file mode 100644 index 000000000..a6e798c90 --- /dev/null +++ b/plugins/crypt-toolkit/src/registry/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export * from './categories' +export * from './algorithms' diff --git a/plugins/crypt-toolkit/src/registry/types.ts b/plugins/crypt-toolkit/src/registry/types.ts new file mode 100644 index 000000000..8af1eb146 --- /dev/null +++ b/plugins/crypt-toolkit/src/registry/types.ts @@ -0,0 +1,33 @@ +import type { FC } from 'react' + +export type CategoryId = + | 'encoding' + | 'hash' + | 'symmetric' + | 'asymmetric' + | 'hmac' + | 'kdf' + | 'tools' + +export type DirectionMode = 'encode' | 'decode' | 'encrypt' | 'decrypt' + +export interface AlgorithmMeta { + id: string + category: CategoryId + label: string + title: string + reversible: boolean + cmds: string[] + defaultEnabled: boolean + teach: { summary: string } +} + +export interface AlgorithmProps { + direction?: DirectionMode + onDirectionChange?: (dir: DirectionMode) => void +} + +export interface AlgorithmModule { + meta: AlgorithmMeta + Component: FC +} diff --git a/plugins/crypt-toolkit/src/shared/Actions.tsx b/plugins/crypt-toolkit/src/shared/Actions.tsx new file mode 100644 index 000000000..c2bd13c42 --- /dev/null +++ b/plugins/crypt-toolkit/src/shared/Actions.tsx @@ -0,0 +1,24 @@ +export interface ActionItem { + label: string + onClick: () => void + variant?: 'primary' | 'ghost' + disabled?: boolean +} + +export default function Actions({ items }: { items: ActionItem[] }) { + return ( +
+ {items.map((a) => ( + + ))} +
+ ) +} diff --git a/plugins/crypt-toolkit/src/shared/CopyButton.tsx b/plugins/crypt-toolkit/src/shared/CopyButton.tsx new file mode 100644 index 000000000..238541a60 --- /dev/null +++ b/plugins/crypt-toolkit/src/shared/CopyButton.tsx @@ -0,0 +1,19 @@ +export default function CopyButton({ text, label = '复制结果' }: { text: string; label?: string }) { + const onCopy = () => { + if (!text) { + window.ztools.showNotification('没有可复制的内容') + return + } + try { + window.ztools.copyText(text) + window.ztools.showNotification('已复制') + } catch { + window.ztools.showNotification('复制失败') + } + } + return ( + + ) +} diff --git a/plugins/crypt-toolkit/src/shared/Field.tsx b/plugins/crypt-toolkit/src/shared/Field.tsx new file mode 100644 index 000000000..e1dab638c --- /dev/null +++ b/plugins/crypt-toolkit/src/shared/Field.tsx @@ -0,0 +1,104 @@ +import type { CSSProperties } from 'react' + +export type FieldType = 'text' | 'secret' | 'textarea' | 'select' + +export interface FieldProps { + label: string + hint?: string + help?: string + type?: FieldType + value: string + onChange: (v: string) => void + options?: { value: string; label: string }[] + error?: string + readOnly?: boolean + rows?: number + placeholder?: string + style?: CSSProperties + variant?: 'input' | 'output' + copyable?: boolean + onClear?: () => void +} + +export default function Field({ + label, + hint, + help, + type = 'text', + value, + onChange, + options, + error, + readOnly, + rows = 3, + placeholder, + style, + variant, + copyable, + onClear +}: FieldProps) { + const id = `f-${label.replace(/\s+/g, '-')}` + const isOutput = variant === 'output' + + const onCopy = () => { + window.ztools.copyText?.(value ?? '') + } + + return ( +
+ + {type === 'textarea' ? ( +