mirror of
https://github.com/fzxx/XiangYue.git
synced 2025-11-24 19:12:55 +08:00
v1.3.0.0
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# 更新日志
|
||||
|
||||
## v1.3.0.0
|
||||
|
||||
- 密钥派生算法升级为`Argon2id + HKDF-SHA512`,减少密文长度,速度不变的情况下**极限提升安全性**,并且解密兼容旧版本密文
|
||||
|
||||
- 优化页面、加/解密、映射密文、随机密码、缓存管理的逻辑
|
||||
- 更换压缩库,提升压缩率,兼容旧版本
|
||||
|
||||
## v1.2.3.0
|
||||
|
||||
- 修改零宽密文 加/解密 逻辑,使**可见字符在加密后可自定义修改**
|
||||
|
||||
82
README.md
82
README.md
@@ -1,6 +1,6 @@
|
||||
# 想曰
|
||||
|
||||
[](https://github.com/fzxx/XiangYue) [](https://github.com/fzxx/XiangYue) [](https://github.com/fzxx/XiangYue) [](https://github.com/fzxx/XiangYue/issues?q=is%3Aissue+is%3Aclosed) [](https://github.com/fzxx/XiangYue/blob/main/main/License.txt)
|
||||
[](https://github.com/fzxx/XiangYue) [](https://github.com/fzxx/XiangYue) [](https://github.com/fzxx/XiangYue/issues?q=is%3Aissue+is%3Aclosed) [](https://github.com/fzxx/XiangYue/blob/main/main/License.txt)
|
||||
[](https://github.com/fzxx/XiangYue?tab=readme-ov-file#%EF%B8%8F-%E6%8A%80%E6%9C%AF%E7%BB%86%E8%8A%82) [](https://github.com/fzxx/XiangYue?tab=readme-ov-file#-%E7%89%B9%E7%82%B9)
|
||||
|
||||
<span style="color: Blue;"><strong><a href="https://github.com/fzxx/XiangYue" style="color: inherit;">想曰(yuē)</a></strong></span> 是基于现代加密技术的文本加密工具,使用**多算法级联加密**方案,确保数据在本地完成加密/解密,保护隐私安全。
|
||||
@@ -9,7 +9,7 @@
|
||||
## 🌟 特点
|
||||
|
||||
- ㊙️**密文**:支持 `中文 / Base64 / Emoji / 零宽` 密文
|
||||
- 🔐**密钥**:`PBKDF2-SHA256 + HKDF-SHA256`,有效抵御暴力破解
|
||||
- 🔐**密钥**:`Argon2id + HKDF-SHA512`,有效抵御暴力破解
|
||||
- 🔒**级联算法**:采用 `AES256-CTR` 与 `ChaCha20-Poly1305-IETF` 级联加密,**安全性极高**
|
||||
- 📄**数据**:所有操作在本地完成,数据不离开设备
|
||||
|
||||
@@ -32,12 +32,12 @@ J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa5
|
||||
##### Emoji 密文
|
||||
|
||||
```plaintext
|
||||
🧕🛕🐱🌉🛐🤴🌄🏸🚆🎇🤴🦈🛸🧭🚡💒🤑🚤🔁🚬💰🍣⛴️🎽🔣😚❣️♻️🍖🧺🚨⛪️🛁📞🍤👦🍊🦘🦀🚅💓🏏🚪☪️😠💲🦊🧭🐠🎻🪣🚢⏲️⏯️😒🗻🧂🚠👻💗🪲🦽🐍🚲⏭️⏸️😍🛖🫐🛫🥓👴🐪👰⏰🏬🍱🤎🧄ℹ️⚾️🉑🚐🕎🐪😜🦖🚭🦐👽🧎🍢🥦🧘🐄🥖🔢🏃🎸🍤♎️🌆🐆🌋🤍☮️🫓🐑
|
||||
🍿⏫🐜🤕🚛🤧🎲▶️🦕🦢🛴🔣🏨💽⏹️🦴🐫🏓🔌🧂😚🚠🚢📷🪲🪃🚬🦋🤒🍏⏫♎️🦼📀📼😴⚜️👩👾🦇📟🍌🦏🕍⚱️😃🔭🧽🦍🍎🤫🌉🌆🥔🚔🍢😈😐🐌🏓🔋❣️💸🍑
|
||||
```
|
||||
|
||||
##### 零宽密文(可藏在中文、网址、提取码、英文、Base64等可见字符中)
|
||||
|
||||
`https://github.com/fzxx/͏͏͏͏͏͏XiangYue`
|
||||
`https://github.com/fzxx/͏͏͏XiangYue`
|
||||
|
||||
## 🖥️在线与离线使用
|
||||
|
||||
@@ -47,83 +47,39 @@ J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa5
|
||||
|
||||
[](https://github.com/fzxx/XiangYue/blob/main/CHANGELOG.md)
|
||||
|
||||
## 🛡️ 技术细节
|
||||
## 🛡️ 安全细节
|
||||
|
||||
#### 加密流程
|
||||
|
||||
```plaintext
|
||||
明文 → Deflate压缩 → AES256-CTR加密 → ChaCha20-Poly1305加密 → Base64编码 → 密文
|
||||
↳ 映射中文/Emoji/零宽 → 密文
|
||||
```
|
||||
|
||||
#### 密钥派生流程
|
||||
|
||||
```plaintext
|
||||
密码 + 随机盐(16字节)
|
||||
↳ PBKDF2-SHA256(50万次迭代) → HKDF-SHA256
|
||||
↳ 派生AES-CTR密钥(256位)
|
||||
↳ 派生ChaCha20密钥(256位)
|
||||
```
|
||||
|
||||
#### 数据结构
|
||||
|
||||
```plaintext
|
||||
[中文、Emoji、零宽/Base64 密文]
|
||||
↳ 映射→解码/解码
|
||||
↳[二进制数据]
|
||||
↳ 前16字节 → 盐值(Salt)
|
||||
↳ 接下来12字节 → ChaCha20的Nonce
|
||||
↳ 剩余部分 = ChaCha20密文
|
||||
↳ 解密 → 前16字节 → AES-CTR的Nonce
|
||||
↳ 剩余部分 = AES-CTR密文
|
||||
↳ 解密 → Deflate解压 → [明文]
|
||||
```
|
||||
|
||||
#### 安全要素
|
||||
|
||||
| 要素 | 长度 | 方式 | 用途 |
|
||||
| -------------- | ------- | ------------- | ------------------------ |
|
||||
| 加密算法 | - | 级联 | 增强数据安全性 |
|
||||
| 密钥派生 | - | PBKDF2 + HKDF | 防止暴力破解、彩虹表攻击 |
|
||||
| 盐 (Salt) | 16 字节 | 随机生成 | 确保每次加密生成唯一密钥 |
|
||||
| CTR_Nonce | 16 字节 | 随机生成 | 初始计数器值 |
|
||||
| ChaCha20_Nonce | 12 字节 | 随机生成 | 一次性随机数 |
|
||||
[](https://github.com/fzxx/XiangYue/blob/main/SECURITY.md)
|
||||
|
||||
## 😕 疑问
|
||||
|
||||
##### 发送给多人如何管理多个密码?
|
||||
#### 发送给多人如何管理多个密码?如何安全地传递密码?
|
||||
|
||||
- 使用可靠的密码管理器,例如:**KeePass**、KeePassXC、**Bitwarden**
|
||||
- 使用**开源的阅后即焚文档**传递密码,例如:**PrivateBin**、pastebin
|
||||
|
||||
##### 少量文字也会生成较长的文本,能缩短吗?
|
||||
#### 少量文字也会生成较长的文本,能缩短吗?
|
||||
|
||||
- 因为**追求安全性**,所以添加了随机盐、Nonce等参数,密文中存储这些参数导致的;去掉参数**追求短密文会削弱安全性**,所以无短密文计划。
|
||||
- 因为**追求安全性**,所以添加了随机种子、校验标签参数,密文中存储这些参数导致的;去掉参数**追求短密文会削弱安全性**,目前已做到保障安全性的情况下最短了。
|
||||
|
||||
##### 未来还会添加的算法?
|
||||
#### 感觉加密/解密慢?
|
||||
|
||||
- 暂定密钥派生使用**Argon2id**,加密算法使用三种进行级联。
|
||||
- 使用**Argon2id算法(慢哈希算法)**的正常现象;Base64模式下,短文本理应1秒内加/解密完成,5M的文本(大约200万字)应3秒左右,这是可以接受的速度。
|
||||
|
||||
##### 为什么不使用PBKDF2-SHA512?
|
||||
|
||||
- 如果使用`PBKDF2-SHA512`迭代50万次,加密短文本也会感到明显的延迟,而`PBKDF2-SHA256`能平衡速度与安全。
|
||||
|
||||
##### 感觉加密/解密慢?
|
||||
|
||||
- PBKDF2迭代50万次所以慢,是正常现象;短文本理应1秒内加/解密完成,5M的文本3秒左右,这是可以接受的速度。
|
||||
|
||||
##### 支持加密/解密的最大容量?
|
||||
#### 支持加密/解密的最大容量?
|
||||
|
||||
- 不影响速度的情况下,**建议5M以下**(测试支持50M+);超大的文本请使用压缩包或者其它方式加密。
|
||||
|
||||
##### 可以给密文再次加密?
|
||||
#### 可以给密文再次加密?或者嵌入?
|
||||
|
||||
- `明文 → emoji密文 → 中文密文 → ......` 可以这样无限套娃,但不会增加安全性,安全性取决于你的密码,因此不建议这样做。
|
||||
- `明文 → emoji密文 → 中文密文 → ......` 可以这样无限套娃加密,但不会增加安全性,安全性取决于你的密码,因此不建议这样做。
|
||||
- **中文密文**嵌入**零宽密文**,会优先解密零宽,达成一文双解的效果,但并不能瞒过机器识别。
|
||||
|
||||
##### 经过某些软件发送密文后,解密错误?
|
||||
#### 经过某些软件发送密文后,解密错误?
|
||||
|
||||
- 是因为**某些软件喜欢折叠聊天内容**,或者发送过长的密文被截断,建议你检查密文的完整性;Eomij密文可能因为不同设备内置的表情不一导致解密错误,零宽密文有被某些平台过滤字符的可能。
|
||||
|
||||
##### 零宽密文只有两个字符?可见字符可以自定义?
|
||||
#### 零宽密文只有两个字符?可见字符可以自定义?
|
||||
|
||||
- 密文总长度没有变,只是**转为不可见字符藏在两个可见字符中间**,是为了让人知道密文在哪,方便复制。
|
||||
- 可以,只需**手动替换**密文中的可见字符(零宽密文**只是对人不可见**,机器是秒识别的,但识别不代表能解密)。
|
||||
@@ -133,3 +89,5 @@ J7ni11NnCUEe1+GtZcIWoJcKNgzsyN8K8BQBKnDn/1mLPkv2ul1VUcedyoIgZpXcNUKfy3HhZI6soaa5
|
||||
[想曰](https://github.com/fzxx/XiangYue) - [私下研究专用许可](https://github.com/fzxx/XiangYue?tab=License-1-ov-file#)
|
||||
|
||||
[libsodium.js](https://github.com/jedisct1/libsodium.js/) - ISC 许可证
|
||||
|
||||
[pako](https://github.com/nodeca/pako) - MIT 许可证
|
||||
|
||||
24
SECURITY.md
24
SECURITY.md
@@ -1,21 +1,21 @@
|
||||
## 🛡️ 安全细节
|
||||
|
||||
#### 加密流程
|
||||
### 加密流程
|
||||
|
||||
```plaintext
|
||||
明文 → 压缩 → AES256-CTR加密 → ChaCha20-Poly1305加密 → Base64编码 → 密文
|
||||
↳ 映射中文/Emoji/零宽 → 密文
|
||||
```
|
||||
|
||||
#### 密钥、IV、Nonce派生流程(新旧)
|
||||
### 密钥、IV、Nonce派生流程(新旧)
|
||||
|
||||
```plaintext
|
||||
密码 + Seed(16字节)
|
||||
↳ Argon2id → HKDF-SHA512
|
||||
↳ 派生AES-CTR密钥(256位)
|
||||
↳ 派生ChaCha20密钥(256位)
|
||||
↳ 派生AES-CTR IV(16字节)
|
||||
↳ 派生ChaCha20 Nonce(12字节)
|
||||
密码 + Seed(16字节) →
|
||||
↳ Argon2id = MasterKey + Seed → HKDF-SHA512
|
||||
↳ 派生AES-CTR密钥(256位)
|
||||
↳ 派生ChaCha20密钥(256位)
|
||||
↳ 派生AES-CTR IV(16字节)
|
||||
↳ 派生ChaCha20 Nonce(12字节)
|
||||
```
|
||||
|
||||
```plaintext
|
||||
@@ -25,7 +25,7 @@
|
||||
↳ 派生ChaCha20密钥(256位)
|
||||
```
|
||||
|
||||
#### 数据结构(新旧)
|
||||
### 数据结构(新旧)
|
||||
|
||||
```plaintext
|
||||
[中文、Emoji、零宽/Base64 密文]
|
||||
@@ -48,7 +48,7 @@
|
||||
↳ 解密 → Deflate解压 → [明文]
|
||||
```
|
||||
|
||||
#### 安全要素
|
||||
### 安全要素
|
||||
|
||||
| 要素 | 长度 | 方式 | 用途 |
|
||||
| -------------- | ------- | --------------- | ------------------------ |
|
||||
@@ -59,6 +59,6 @@
|
||||
| AES-CTR IV | 16 字节 | 随机种子派生 | 计数器初始值 |
|
||||
| ChaCha20 Nonce | 12 字节 | 随机种子派生 | 一次性随机数 |
|
||||
|
||||
#### 漏洞报告
|
||||
### 漏洞报告
|
||||
|
||||
[](https://github.com/fzxx/XiangYue/issues)
|
||||
[](https://github.com/fzxx/XiangYue/issues)
|
||||
|
||||
10
css/all.min.css
vendored
10
css/all.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
// 三层级联,待开发
|
||||
// 非对称加密算法,待开发
|
||||
1
js/library/pako-2.1.0.js
Normal file
1
js/library/pako-2.1.0.js
Normal file
File diff suppressed because one or more lines are too long
1
js/library/sodium-sumo-0.7.15.js
Normal file
1
js/library/sodium-sumo-0.7.15.js
Normal file
File diff suppressed because one or more lines are too long
83
js/library/tailwind-3.4.16.js
Normal file
83
js/library/tailwind-3.4.16.js
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
const CharSets1={"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":["生","块","地","易","直","黑","李","问","狼","往"],"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 combinedCharMap={};for(const[base64Char,chineseChars]of Object.entries(CharSets1)){for(const chineseChar of chineseChars){combinedCharMap[chineseChar]=base64Char;}}
|
||||
const mappingMode1={base64ToChinese:function(base64){let chinese='';for(let i=0;i<base64.length;i++){const char=base64[i];const charSet=CharSets1[char];if(charSet){const randomIndex=Math.floor(Math.random()*charSet.length);chinese+=charSet[randomIndex];}}
|
||||
return chinese;},chineseToBase64:function(chinese){let base64='';for(let i=0;i<chinese.length;i++){const char=chinese[i];const base64Char=combinedCharMap[char];if(base64Char){base64+=base64Char;}}
|
||||
return base64;},combinedCharMap};
|
||||
const CharSets1={"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":["生","块","地","易","直","黑","李","问","狼","往"],"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 CN2B64=new Map();for(const[b64,arr]of Object.entries(CharSets1)){const code=b64.charCodeAt(0);for(const ch of arr){CN2B64.set(ch,code);}}
|
||||
const mappingMode1={base64ToChinese(b64){const out=[];for(const ch of b64){const pool=CharSets1[ch];if(pool)out.push(pool[(Math.random()*10)|0]);}
|
||||
return out.join('');},chineseToBase64(str){const out=[];for(const ch of str){const code=CN2B64.get(ch);if(code!==undefined)out.push(String.fromCharCode(code));}
|
||||
return out.join('');}};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,43 @@
|
||||
const CharSets3={'0':'\u200C','1':'\u200D','2':'\u2060','3':'\u2061','4':'\u2062','5':'\u2063','6':'\u2064','7':'\u206A','8':'\u206B','9':'\u206C','a':'\u206D','b':'\u206E','c':'\u206F','d':'\u034F','e':'\uFEFF','f':'\u061C'};const ReverseCharSets3=Object.fromEntries(Object.entries(CharSets3).map(([k,v])=>[v,k]));const PAD_MARKER='\u200B';const ZERO_WIDTH_CHARS=new Set([...Object.values(CharSets3),PAD_MARKER]);const generateRandomBase64=length=>{const base64Chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';return Array.from({length},()=>base64Chars.charAt(Math.floor(Math.random()*base64Chars.length))).join('');};const mappingMode3={base64ToZeroWidth(str,prefixLength=1,suffixLength=1){if(typeof str!=='string')throw new TypeError('输入必须是字符串');const prefix=generateRandomBase64(prefixLength);const suffix=generateRandomBase64(suffixLength);const hexStr=Array.from(new TextEncoder().encode(str),b=>b.toString(16).padStart(2,'0')).join('');const padBits=(4-(hexStr.length*4%4))%4;const padMarker=CharSets3[(padBits/4).toString(16)];const fullHex=hexStr+'0'.repeat(padBits/4);const zeroWidthStr=fullHex.split('').map(c=>CharSets3[c]).join('');return`${prefix}${zeroWidthStr}${padMarker}${suffix}`;},zeroWidthToBase64(str){if(typeof str!=='string')throw new TypeError('输入必须是字符串');const isZeroWidth=char=>ZERO_WIDTH_CHARS.has(char);let start=0;while(start<str.length&&!isZeroWidth(str[start]))start++;let end=str.length-1;while(end>=0&&!isZeroWidth(str[end]))end--;if(start>end)return str;const coreStr=str.slice(start,end+1);const padMarker=coreStr.at(-1)||'';const validPad=Math.min(Math.max(ReverseCharSets3[padMarker]?parseInt(ReverseCharSets3[padMarker],16)*4:0,0),3);const hexStr=coreStr.slice(0,-1).split('').filter(ch=>ReverseCharSets3.hasOwnProperty(ch)).map(ch=>ReverseCharSets3[ch]).join('');const dataHex=hexStr.slice(0,hexStr.length-(validPad/4));const alignedHex=dataHex.length%2?dataHex.slice(0,dataHex.length-1):dataHex;if(!alignedHex.length)return'';try{const bytes=new Uint8Array(alignedHex.length/2);for(let i=0;i<bytes.length;i++){bytes[i]=parseInt(alignedHex.slice(i*2,(i+1)*2),16);}
|
||||
return new TextDecoder().decode(bytes);}catch(error){console.error('零宽密文解码过程出错:',error);return str;}},async encodeBlob(blob,prefixLength=4,suffixLength=4){if(!(blob instanceof Blob))throw new TypeError('输入必须是Blob对象');return new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=e=>{try{const base64Str=(e.target.result.split(',')[1]||'');resolve(this.base64ToZeroWidth(base64Str,prefixLength,suffixLength));}catch(error){reject(error);}};reader.onerror=()=>reject(reader.error);reader.readAsDataURL(blob);});},decodeToBlob(str,mimeType='text/plain'){try{const decodedBase64=this.zeroWidthToBase64(str);const byteArray=new Uint8Array(atob(decodedBase64).split('').map(c=>c.charCodeAt(0)));return new Blob([byteArray],{type:mimeType});}catch(error){console.error('Blob解码出错:',error);return new Blob([`零宽字符解密失败: ${error.message}`],{type:'text/plain'});}}};
|
||||
const CharSets3={'0':'\u200C','1':'\u200D','2':'\u2060','3':'\u2061','4':'\u2062','5':'\u2063','6':'\u2064','7':'\u206A','8':'\u206B','9':'\u206C','a':'\u206D','b':'\u206E','c':'\u206F','d':'\u034F','e':'\uFEFF','f':'\u061C'};const ReverseCharSets3=Object.fromEntries(Object.entries(CharSets3).map(([k,v])=>[v,k]));const PAD_MARKER='\u200B';const BYTE2ZW=new Array(256);for(let i=0;i<256;i++){BYTE2ZW[i]=CharSets3[(i>>>4).toString(16)]+CharSets3[(i&0x0F).toString(16)];}
|
||||
const C4=CharSets3;const BASE64_POOL='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';const randB64=n=>{if(n<=0)return'';const buf=new Uint32Array(n);crypto.getRandomValues(buf);let s='';for(let i=0;i<n;i++)s+=BASE64_POOL[buf[i]&63];return s;};const mappingMode3={base64ToZeroWidth(str,prefixLen=1,suffixLen=1){const bytes=new TextEncoder().encode(str);const zw=new Array(bytes.length);for(let i=0;i<bytes.length;i++)zw[i]=BYTE2ZW[bytes[i]];const padBits=(4-((bytes.length*8)&3))&3;return randB64(prefixLen)+zw.join('')+C4[padBits.toString(16)]+randB64(suffixLen);},zeroWidthToBase64(str){const nib=[];for(const ch of str){const n=parseInt(ReverseCharSets3[ch],16);if(!isNaN(n))nib.push(n);}
|
||||
if(nib.length<2)return'';const padBits=nib.pop()*4;const dataBits=nib.length*4-padBits;const byteLen=dataBits>>3;if(byteLen<=0)return'';const bytes=new Uint8Array(byteLen);let buf=0,bits=0,idx=0;for(let i=0;i<nib.length;i++){buf=(buf<<4)|nib[i];bits+=4;if(bits>=8){bytes[idx++]=buf>>>(bits-8);bits-=8;}}
|
||||
return new TextDecoder().decode(bytes);},async encodeBlob(blob,prefixLen=4,suffixLen=4,chunkSize=512*1024){if(!(blob instanceof Blob))throw new TypeError('输入必须是Blob对象');const cores=navigator.hardwareConcurrency||4;const tasks=Math.ceil(blob.size/chunkSize);const results=new Array(tasks);let done=0;const workerSrc=`
|
||||
|
||||
const C4 = ${JSON.stringify(CharSets3)};
|
||||
|
||||
const BYTE2ZW = ${JSON.stringify(BYTE2ZW)};
|
||||
|
||||
const BASE64_POOL = '${BASE64_POOL}';
|
||||
|
||||
const rand = n => {
|
||||
|
||||
const buf = new Uint32Array(n);
|
||||
|
||||
crypto.getRandomValues(buf);
|
||||
|
||||
let s = '';
|
||||
|
||||
for (let i = 0; i < n; ++i) s += BASE64_POOL[buf[i] & 63];
|
||||
|
||||
return s;
|
||||
|
||||
};
|
||||
|
||||
onmessage = async ({data}) => {
|
||||
|
||||
const slice = data.blob.slice(data.offset, data.offset + data.chunkSize);
|
||||
|
||||
const bytes = new Uint8Array(await slice.arrayBuffer());
|
||||
|
||||
let zw = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) zw += BYTE2ZW[bytes[i]];
|
||||
|
||||
const padBits = (4 - ((bytes.length * 8) & 3)) & 3;
|
||||
|
||||
postMessage({idx: data.idx, res: rand(data.prefixLen) + zw + C4[padBits.toString(16)] + rand(data.suffixLen)});
|
||||
|
||||
};
|
||||
|
||||
`;const url=URL.createObjectURL(new Blob([workerSrc],{type:'text/javascript'}));return new Promise((resolve,reject)=>{const workers=Array.from({length:cores},()=>new Worker(url));workers.forEach(w=>{w.onmessage=({data})=>{results[data.idx]=data.res;if(++done===tasks){workers.forEach(x=>x.terminate());URL.revokeObjectURL(url);resolve(results.join(''));}};w.onerror=e=>{reject(e);workers.forEach(x=>x.terminate());URL.revokeObjectURL(url);};});for(let i=0;i<Math.min(cores,tasks);i++){workers[i].postMessage({blob,prefixLen,suffixLen,idx:i,offset:i*chunkSize,chunkSize});}});},decodeToBlob(str,mimeType='text/plain'){try{const b64=this.zeroWidthToBase64(str);const bin=atob(b64);const bytes=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);return new Blob([bytes],{type:mimeType});}
|
||||
catch{return new Blob(['decode error'],{type:'text/plain'});}},getEmojiLength:s=>[...s].length};
|
||||
15
js/utils.js
15
js/utils.js
@@ -1,9 +1,8 @@
|
||||
const DEFAULT_PASSWORD='a184f7b849ffffed24d266a30298c72ef2f5ad040db73bf37151fac767630728';const STORAGE_KEYS={THEME_MODE:'theme_mode',OUTPUT_MODE:'output_mode'};(function applyThemeEarly(){const STORAGE_KEYS={THEME_MODE:'theme_mode'};const savedThemeMode=localStorage.getItem(STORAGE_KEYS.THEME_MODE);const systemDarkMode=window.matchMedia('(prefers-color-scheme: dark)').matches;const isDarkMode=savedThemeMode?savedThemeMode==='dark':systemDarkMode;if(isDarkMode){document.documentElement.classList.add('dark');}})();async function handleStream(stream,writerData){const writer=stream.writable.getWriter();writer.write(writerData);writer.close();const reader=stream.readable.getReader();const chunks=[];while(true){const{done,value}=await reader.read();if(done)break;chunks.push(value);}
|
||||
const DEFAULT_PASSWORD='a184f7b849ffffed24d266a30298c72ef2f5ad040db73bf37151fac767630728';const STORAGE_KEYS={THEME_MODE:'theme_mode',OUTPUT_MODE:'output_mode'};const ZERO_WIDTH_REGEX=/[\u200B-\u200F\uFEFF\u202A-\u202E\u2060-\u206F]/u;const BASE64_CHARS_REGEX=/[^A-Za-z0-9+/=]/g;const BASE64_FORMAT_REGEX=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;const EMOJI_REGEX=/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;const CHINESE_REGEX=/[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]/u;const PASSWORD_CHARSET="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]:;?><,./-='\"\\";const textEncoder=new TextEncoder();(function applyThemeEarly(){const savedThemeMode=localStorage.getItem(STORAGE_KEYS.THEME_MODE);const isDarkMode=savedThemeMode?savedThemeMode==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(isDarkMode){document.documentElement.classList.add('dark');}})();async function handleStream(stream,writerData){const writer=stream.writable.getWriter();await writer.write(writerData);await writer.close();const reader=stream.readable.getReader();const chunks=[];while(true){const{done,value}=await reader.read();if(done)break;chunks.push(value);}
|
||||
return new Blob(chunks).arrayBuffer();}
|
||||
const utils={arrayBufferToBase64(buffer){let binary='';const bytes=new Uint8Array(buffer);for(let i=0;i<bytes.byteLength;i++){binary+=String.fromCharCode(bytes[i]);}
|
||||
return btoa(binary);},base64ToArrayBuffer(base64){const binaryString=atob(base64);const bytes=new Uint8Array(binaryString.length);for(let i=0;i<binaryString.length;i++){bytes[i]=binaryString.charCodeAt(i);}
|
||||
return bytes.buffer;},stringToArrayBuffer(str){return new TextEncoder().encode(str).buffer;},arrayBufferToString(buffer){return new TextDecoder().decode(buffer);},generateRandomBytes(length){return crypto.getRandomValues(new Uint8Array(length));},async compressData(data,method='deflate'){if(!window.CompressionStream)throw new Error('当前浏览器不支持CompressionStream API');const cs=new CompressionStream(method);return handleStream(cs,data);},async decompressData(data,method='deflate'){if(!window.DecompressionStream)throw new Error('当前浏览器不支持DecompressionStream API');const ds=new DecompressionStream(method);return handleStream(ds,data);},generatePassword(length){const charset='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]:;?><,./-=';let password='';for(let i=0;i<length;i++){const randomIndex=Math.floor(Math.random()*charset.length);password+=charset.charAt(randomIndex);}
|
||||
return password;},copyToClipboard(text){navigator.clipboard.writeText(text).catch(err=>{console.error('复制失败:',err);showNotification('复制到剪贴板失败,请手动复制。',false);});},detectCiphertextType(ciphertext){const limitedText=ciphertext.slice(0,1000);if(/[\u200B-\u200F\uFEFF\u202A-\u202E\u2060-\u206F]/u.test(limitedText)){return'zero-width';}
|
||||
const base64Chars=limitedText.replace(/[^A-Za-z0-9+/=]/g,'');if(base64Chars.length/limitedText.length>0.8&&/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(base64Chars)){return'base64';}
|
||||
if(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u.test(limitedText)){return'emoji';}
|
||||
if(/[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]/u.test(limitedText)){return'chinese';}},saveThemeMode(mode){if(['light','dark'].includes(mode)){localStorage.setItem(STORAGE_KEYS.THEME_MODE,mode);}},getSavedThemeMode(){return localStorage.getItem(STORAGE_KEYS.THEME_MODE)||null;},saveOutputMode(mode){if(['chinese','base64','emoji','zero-width'].includes(mode)){localStorage.setItem(STORAGE_KEYS.OUTPUT_MODE,mode);}},getSavedOutputMode(){return localStorage.getItem(STORAGE_KEYS.OUTPUT_MODE)||'chinese';}};
|
||||
const toUint8Array=data=>data instanceof Uint8Array?data:new Uint8Array(typeof data==='string'?textEncoder.encode(data):data);const utils={arrayBufferToBase64:buffer=>sodium.to_base64(new Uint8Array(buffer),sodium.base64_variants.ORIGINAL),base64ToArrayBuffer:base64=>sodium.from_base64(base64,sodium.base64_variants.ORIGINAL).buffer,stringToArrayBuffer:str=>sodium.from_string(str).buffer,arrayBufferToString:buffer=>sodium.to_string(new Uint8Array(buffer)),generateRandomBytes:length=>sodium.randombytes_buf(length),compressData(data){if(typeof pako==='undefined'){throw new Error('压缩失败,pako库未加载');}
|
||||
return pako.deflateRaw(toUint8Array(data),{level:9});},decompressData(data){if(typeof pako==='undefined'){throw new Error('解压失败,pako库未加载');}
|
||||
const uint8Data=toUint8Array(data);try{return pako.inflateRaw(uint8Data);}catch{return pako.inflate(uint8Data);}},generatePassword(length){return Array.from({length},()=>PASSWORD_CHARSET[sodium.randombytes_uniform(PASSWORD_CHARSET.length)]).join('');},copyToClipboard(text){navigator.clipboard.writeText(text).catch(err=>{console.error('复制失败:',err);showNotification('复制到剪贴板失败,请手动复制。',false);});},detectCiphertextType(ciphertext){const limitedText=ciphertext.slice(0,1000);if(ZERO_WIDTH_REGEX.test(limitedText)){return'zero-width';}
|
||||
const base64Chars=limitedText.replace(BASE64_CHARS_REGEX,'');const base64Ratio=base64Chars.length/limitedText.length;if(base64Ratio>0.8&&BASE64_FORMAT_REGEX.test(base64Chars)){return'base64';}
|
||||
if(EMOJI_REGEX.test(limitedText)){return'emoji';}
|
||||
if(CHINESE_REGEX.test(limitedText)){return'chinese';}},saveThemeMode(mode){if(mode==='light'||mode==='dark'){localStorage.setItem(STORAGE_KEYS.THEME_MODE,mode);}},getSavedThemeMode(){return localStorage.getItem(STORAGE_KEYS.THEME_MODE);},saveOutputMode(mode){const validModes=['chinese','base64','emoji','zero-width'];if(validModes.includes(mode)){localStorage.setItem(STORAGE_KEYS.OUTPUT_MODE,mode);}},getSavedOutputMode(){return localStorage.getItem(STORAGE_KEYS.OUTPUT_MODE)||'chinese';}};
|
||||
25
js/web.js
25
js/web.js
@@ -1,20 +1,17 @@
|
||||
function showNotification(message,isSuccess=true){const notificationContainer=document.createElement('div');notificationContainer.className='fixed right-4 transition-all duration-300 ease-in-out translate-x-full z-50';const navHeight=document.querySelector('nav')?.offsetHeight||0;notificationContainer.style.top=`${navHeight + 10}px`;const setNotificationWidth=()=>{notificationContainer.style.width=window.innerWidth<640?'90%':'280px';};setNotificationWidth();const isDarkMode=document.documentElement.classList.contains('dark');const baseClass=`bg-white rounded-lg shadow-lg p-3 flex items-start ${isSuccess ? 'border-l-4 border-green-500' : 'border-l-4 border-red-500'}`;const darkClass=`bg-gray-800 border-gray-700 text-white`;const finalClass=isDarkMode?`${baseClass} ${darkClass}`:baseClass;notificationContainer.innerHTML=`
|
||||
<div class="${finalClass}">
|
||||
<div class="flex-shrink-0 mt-0.5 mr-2 ${isSuccess ? 'text-green-500' : 'text-red-500'}">
|
||||
<i class="fa-solid fa-${isSuccess ? 'check' : 'exclamation'}-circle"></i>
|
||||
function showNotification(message,isSuccess=true){const container=document.createElement('div');container.className='fixed right-4 transition-all duration-300 ease-in-out translate-x-full z-50';container.style.top=`${(document.querySelector('nav')?.offsetHeight || 0) + 10}px`;const setWidth=()=>container.style.width=window.innerWidth<640?'90%':'280px';setWidth();window.addEventListener('resize',setWidth);const isDark=document.documentElement.classList.contains('dark');const[color,icon]=isSuccess?['green','check']:['red','exclamation'];container.innerHTML=`
|
||||
<div class="rounded-lg shadow-lg p-3 flex items-start border-l-4 border-${color}-500 ${isDark ? 'bg-gray-800 border-gray-700 text-white' : 'bg-white'}">
|
||||
<div class="flex-shrink-0 mt-0.5 mr-2 text-${color}-500">
|
||||
<i class="fa-solid fa-${icon}-circle"></i>
|
||||
</div>
|
||||
<div class="max-w-[90%]">
|
||||
<p class="text-sm font-medium text-gray-800 dark:text-white">${message}</p>
|
||||
<p class="text-sm font-medium ${isDark ? 'dark:text-white' : 'text-gray-800'}">${message}</p>
|
||||
</div>
|
||||
<button class="ml-auto flex-shrink-0 inline-flex text-gray-400 hover:text-gray-600 focus:outline-none" onclick="this.parentElement.parentElement.remove()">
|
||||
<button class="ml-auto flex-shrink-0 inline-flex text-gray-400 hover:text-gray-600 focus:outline-none">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;document.body.appendChild(notificationContainer);setTimeout(()=>{notificationContainer.classList.remove('translate-x-full');},10);const removeNotification=()=>{notificationContainer.classList.add('translate-x-full');setTimeout(()=>{notificationContainer.remove();},300);};const timeoutId=setTimeout(removeNotification,5000);notificationContainer.querySelector('button').addEventListener('click',()=>{clearTimeout(timeoutId);removeNotification();});window.addEventListener('resize',setNotificationWidth);return notificationContainer;}
|
||||
function updateNotificationStyle(notificationContainer){const isDarkMode=document.documentElement.classList.contains('dark');const baseClass=`bg-white rounded-lg shadow-lg p-3 flex items-start ${notificationContainer.querySelector('div').classList.contains('border-l-4.border-green-500') ? 'border-l-4 border-green-500' : 'border-l-4 border-red-500'}`;const darkClass=`bg-gray-800 border-gray-700 text-white`;const finalClass=isDarkMode?`${baseClass} ${darkClass}`:baseClass;notificationContainer.querySelector('div').className=finalClass;}
|
||||
document.addEventListener('DOMContentLoaded',()=>{const elements={textInput:document.getElementById('text-input'),resultOutput:document.getElementById('result-output'),encryptionKey:document.getElementById('encryption-key'),encryptBtn:document.getElementById('encrypt-btn'),decryptBtn:document.getElementById('decrypt-btn'),copyBtn:document.getElementById('copy-btn'),clearBtn:document.getElementById('clear-btn'),toggleKeyVisibility:document.getElementById('toggle-key-visibility'),generatePassword:document.getElementById('generate-password'),resultStatus:document.getElementById('result-status'),themeToggle:document.getElementById('theme-toggle'),base64Toggle:document.getElementById('base64-toggle'),pasteBtn:document.getElementById('paste-btn')};let savedThemeMode=utils.getSavedThemeMode();const systemDarkMode=window.matchMedia('(prefers-color-scheme: dark)').matches;const isDarkMode=savedThemeMode?savedThemeMode==='dark':systemDarkMode;if(isDarkMode){elements.themeToggle.innerHTML='<i class="fa-solid fa-moon text-white"></i>';}else{elements.themeToggle.innerHTML='<i class="fa-solid fa-sun text-dark"></i>';}
|
||||
let outputMode=utils.getSavedOutputMode();const icons={chinese:'fa-solid fa-language',base64:'fa-solid fa-code',emoji:'fa-regular fa-face-smile','zero-width':'fa-brands fa-creative-commons-zero'};elements.base64Toggle.innerHTML=`<i class="${icons[outputMode]} theme-toggle-icon"></i>`;const checkBrowserSupport=()=>{const checks=[{test:()=>!!window.crypto?.subtle,msg:'不支持Web Crypto API'},{test:()=>!!window.CompressionStream&&!!window.DecompressionStream,msg:'不支持压缩/解压缩API'}];const fail=checks.find(c=>!c.test());if(fail){showNotification(fail.msg+',无法使用加密/压缩功能',false);[elements.encryptBtn,elements.decryptBtn].forEach(b=>b.disabled=true);return false;}
|
||||
return true;};checkBrowserSupport();elements.generatePassword.addEventListener('click',()=>{const password=utils.generatePassword(25);elements.encryptionKey.value=password;utils.copyToClipboard(password);showNotification('已生成并复制25位随机密码');});elements.toggleKeyVisibility.addEventListener('click',()=>{const type=elements.encryptionKey.getAttribute('type')==='password'?'text':'password';elements.encryptionKey.setAttribute('type',type);elements.toggleKeyVisibility.innerHTML=`<i class="fa-regular fa-${type === 'password' ? 'eye-slash' : 'eye'}"></i>`;});elements.base64Toggle.addEventListener('click',()=>{switch(outputMode){case'chinese':outputMode='base64';break;case'base64':outputMode='emoji';break;case'emoji':outputMode='zero-width';break;case'zero-width':outputMode='chinese';break;}
|
||||
utils.saveOutputMode(outputMode);elements.base64Toggle.innerHTML=`<i class="${icons[outputMode]} theme-toggle-icon"></i>`;const modeNames={chinese:'中文',base64:'Base64',emoji:'Emoji','zero-width':'零宽'};showNotification(`已切换到 ${modeNames[outputMode]} 密文`);});elements.pasteBtn.addEventListener('click',()=>{elements.textInput.focus();navigator.clipboard.readText().then(t=>{elements.textInput.value=t;showNotification('已从剪贴板粘贴文本');}).catch(()=>{elements.textInput.select();});});const handleEncryptionDecryption=async(action,inputText,password,isDefaultPassword)=>{const showStatus=(text,className)=>{elements.resultStatus.textContent=text;elements.resultStatus.className=className;elements.resultStatus.classList.remove('hidden');setTimeout(()=>{elements.resultStatus.classList.add('hidden');},1200);};try{showStatus(`${action}中......`,'absolute top-3 right-3 px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200');const result=action==='加密'?await encryptionMethod1.encrypt(inputText,password,outputMode):await encryptionMethod1.decrypt(inputText,password);elements.resultOutput.value=result;showStatus(`${action}成功`,'absolute top-3 right-3 px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200');if(isDefaultPassword){showNotification(`未检测到密码,使用默认密码${action},建议更换为安全的密码。`,true);}else{showNotification(`文本已被${action}了`);}}catch(error){elements.resultOutput.value='';showStatus(`${action}失败`,'absolute top-3 right-3 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200');showNotification(error.message,false);}};elements.encryptBtn.addEventListener('click',async()=>{const plaintext=elements.textInput.value;let password=elements.encryptionKey.value.trim();let isDefaultPassword=false;if(!plaintext)return showNotification('请输入要加密的文本',false);if(!password){password=DEFAULT_PASSWORD;isDefaultPassword=true;}
|
||||
await handleEncryptionDecryption('加密',plaintext,password,isDefaultPassword);});elements.decryptBtn.addEventListener('click',async()=>{const ciphertext=elements.textInput.value.trim();let password=elements.encryptionKey.value.trim();let isDefaultPassword=false;if(!ciphertext)return showNotification('请输入要解密的文本',false);if(!password){password=DEFAULT_PASSWORD;isDefaultPassword=true;}
|
||||
const detectedMode=utils.detectCiphertextType(ciphertext);const modeNames={chinese:'中文',base64:'Base64',emoji:'Emoji','zero-width':'零宽'};showNotification(`已识别密文类型: ${modeNames[detectedMode]}`);await handleEncryptionDecryption('解密',ciphertext,password,isDefaultPassword);});elements.copyBtn.addEventListener('click',()=>{const result=elements.resultOutput.value;if(!result)return showNotification('没有可复制的结果',false);utils.copyToClipboard(result);showNotification('结果已复制到剪贴板');});elements.clearBtn.addEventListener('click',()=>{elements.textInput.value='';elements.resultOutput.value='';elements.encryptionKey.value='';elements.resultStatus.classList.add('hidden');showNotification('已清空所有内容');});elements.themeToggle.addEventListener('click',()=>{const isDark=document.documentElement.classList.toggle('dark');const newMode=isDark?'dark':'light';utils.saveThemeMode(newMode);elements.themeToggle.innerHTML=`<i class="fa-solid fa-${isDark ? 'moon' : 'sun'} text-${isDark ? 'white' : 'dark'}"></i>`;const notifications=document.querySelectorAll('.fixed.right-4.transition-all.duration-300.ease-in-out.z-50');notifications.forEach(notification=>{updateNotificationStyle(notification);});});});
|
||||
`;document.body.appendChild(container);const closeBtn=container.querySelector('button');setTimeout(()=>container.classList.remove('translate-x-full'),10);const remove=()=>{container.classList.add('translate-x-full');setTimeout(()=>container.remove(),300);};const timeoutId=setTimeout(remove,5000);closeBtn.addEventListener('click',()=>{clearTimeout(timeoutId);remove();});return container;}
|
||||
function clearCryptoCache(){if(window.cryptoTempData){Object.values(window.cryptoTempData).forEach(data=>{if(data instanceof ArrayBuffer)new Uint8Array(data).fill(0);});window.cryptoTempData={};}
|
||||
window.cryptoContext=null;typeof window.gc==='function'&&window.gc();}
|
||||
document.addEventListener('DOMContentLoaded',()=>{const els=Object.fromEntries(['text-input','result-output','encryption-key','encrypt-btn','decrypt-btn','copy-btn','clear-btn','toggle-key-visibility','generate-password','result-status','theme-toggle','base64-toggle','paste-btn'].map(id=>[id.replace(/-./g,m=>m[1].toUpperCase()),document.getElementById(id)]));window.cryptoTempData={};window.cryptoContext=null;const savedTheme=utils.getSavedThemeMode();const isDark=(savedTheme||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'))==='dark';document.documentElement.classList.toggle('dark',isDark);els.themeToggle.innerHTML=`<i class="fa-solid fa-${isDark ? 'moon' : 'sun'} ${isDark ? 'text-white' : 'text-dark'}"></i>`;const modes=['chinese','base64','emoji','zero-width'];const modeNames={chinese:'中文',base64:'Base64',emoji:'Emoji','zero-width':'零宽'};const icons={chinese:'fa-solid fa-language',base64:'fa-solid fa-code',emoji:'fa-regular fa-face-smile','zero-width':'fa-brands fa-creative-commons-zero'};let outputMode=utils.getSavedOutputMode();els.base64Toggle.innerHTML=`<i class="${icons[outputMode]} theme-toggle-icon"></i>`;if(!window.crypto?.subtle){showNotification('不支持Web Crypto API,无法使用加/解密功能',false);els.encryptBtn.disabled=els.decryptBtn.disabled=true;}
|
||||
const baseStatusClass='absolute top-3 right-3 px-2 py-1 rounded-full text-xs font-medium';let isProcessing=false;const handleAction=async(action,inputGetter)=>{if(isProcessing)return showNotification('操作处理中,请稍候',false);const input=inputGetter();if(!input)return showNotification(`请输入要${action}的文本`,false);let password=els.encryptionKey.value.trim();const isDefault=!password;isDefault&&(password=DEFAULT_PASSWORD);try{isProcessing=true;els.encryptBtn.disabled=els.decryptBtn.disabled=true;els.resultStatus.textContent=`${action}中......`;els.resultStatus.className=`${baseStatusClass} bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200`;const result=await new Promise((resolve,reject)=>{setTimeout(async()=>{try{resolve(action==='加密'?await encryptionMethod1.encrypt(input,password,outputMode):await encryptionMethod1.decrypt(input,password));}catch(e){reject(e);}},0);});els.resultOutput.value=result;els.resultStatus.textContent=`${action}成功`;els.resultStatus.className=`${baseStatusClass} bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200`;showNotification(isDefault?`未检测到密码,现使用默认密码${action},建议更换为安全的密码。`:`文本已被${action}了`,true);}catch(e){els.resultOutput.value='';els.resultStatus.textContent=`${action}失败`;els.resultStatus.className=`${baseStatusClass} bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200`;showNotification(e.message,false);}finally{clearCryptoCache();setTimeout(()=>els.resultStatus.classList.add('hidden'),1200);isProcessing=false;els.encryptBtn.disabled=els.decryptBtn.disabled=false;}};Object.entries({generatePassword:()=>{const pwd=utils.generatePassword(25);els.encryptionKey.value=pwd;utils.copyToClipboard(pwd);showNotification('已生成并复制25位随机密码');},toggleKeyVisibility:()=>{const isPassword=els.encryptionKey.type==='password';els.encryptionKey.type=isPassword?'text':'password';els.toggleKeyVisibility.innerHTML=`<i class="fa-regular fa-${isPassword ? 'eye' : 'eye-slash'}"></i>`;},base64Toggle:()=>{outputMode=modes[(modes.indexOf(outputMode)+1)%modes.length];utils.saveOutputMode(outputMode);els.base64Toggle.innerHTML=`<i class="${icons[outputMode]} theme-toggle-icon"></i>`;showNotification(`已切换到 ${modeNames[outputMode]} 密文`);},pasteBtn:()=>navigator.clipboard.readText().then(text=>{els.textInput.value=text;showNotification('已从剪贴板粘贴文本');}).catch(()=>els.textInput.select()),encryptBtn:()=>handleAction('加密',()=>els.textInput.value),decryptBtn:()=>handleAction('解密',()=>els.textInput.value.trim()),copyBtn:()=>{if(!els.resultOutput.value)return showNotification('没有可复制的结果',false);utils.copyToClipboard(els.resultOutput.value);showNotification('结果已复制到剪贴板');},clearBtn:()=>{els.textInput.value=els.resultOutput.value=els.encryptionKey.value='';els.resultStatus.classList.add('hidden');showNotification('已清空所有内容');},themeToggle:()=>{const isDark=document.documentElement.classList.toggle('dark');utils.saveThemeMode(isDark?'dark':'light');els.themeToggle.innerHTML=`<i class="fa-solid fa-${isDark ? 'moon' : 'sun'} ${isDark ? 'text-white' : 'text-dark'}"></i>`;document.querySelectorAll('.fixed.right-4.transition-all.duration-300.ease-in-out.z-50').forEach(el=>el.remove());}}).forEach(([key,handler])=>els[key]?.addEventListener('click',handler));});
|
||||
Reference in New Issue
Block a user